Merge branch 'master' into python
commit
12dbdb078c
|
@ -123,8 +123,8 @@ jobs:
|
|||
- name: Set Up Source
|
||||
run: rsync --filter=":- .gitignore" -r ./ pkg/snap/solvespace-snap-src
|
||||
- name: Build Snap
|
||||
uses: snapcore/action-build@v1
|
||||
id: build
|
||||
uses: diddlesnaps/snapcraft-multiarch-action@v1
|
||||
with:
|
||||
path: pkg/snap
|
||||
- name: Upload & Release to Edge
|
||||
|
@ -142,40 +142,6 @@ jobs:
|
|||
snap: ${{ steps.build.outputs.snap }}
|
||||
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:
|
||||
name: Upload Release Assets
|
||||
needs: [build_release_windows, build_release_windows_openmp, build_release_macos]
|
||||
|
|
|
@ -21,6 +21,11 @@ jobs:
|
|||
dir_name="solvespace-${version}"
|
||||
archive_name="${dir_name}.tar.xz"
|
||||
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_path::${archive_path}"
|
||||
|
|
|
@ -42,3 +42,20 @@ jobs:
|
|||
run: .github/scripts/install-macos.sh ci
|
||||
- name: Build & Test
|
||||
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 }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
3.x - since the 3.0 release, only available in edge builds
|
||||
3.1
|
||||
---
|
||||
|
||||
Constraints:
|
||||
|
|
|
@ -40,7 +40,7 @@ include(GetGitCommitHash)
|
|||
|
||||
string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH)
|
||||
project(solvespace
|
||||
VERSION 3.0
|
||||
VERSION 3.1
|
||||
LANGUAGES C CXX ASM)
|
||||
|
||||
set(ENABLE_GUI ON CACHE BOOL
|
||||
|
@ -91,6 +91,10 @@ endif()
|
|||
if(MINGW)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static-libgcc")
|
||||
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()
|
||||
|
||||
# Ensure that all platforms use 64-bit IEEE floating point operations for consistency;
|
||||
|
@ -182,12 +186,13 @@ if(APPLE)
|
|||
set(CMAKE_FIND_FRAMEWORK LAST)
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
set(M_LIBRARY "" CACHE STRING "libm (not necessary)" FORCE)
|
||||
endif()
|
||||
|
||||
message(STATUS "Using in-tree libdxfrw")
|
||||
add_subdirectory(extlib/libdxfrw)
|
||||
|
||||
message(STATUS "Using in-tree eigen")
|
||||
include_directories(extlib/eigen)
|
||||
|
||||
message(STATUS "Using in-tree mimalloc")
|
||||
set(MI_OVERRIDE 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)
|
||||
find_package(Eigen3 CONFIG)
|
||||
endif()
|
||||
if(FORCE_VENDORED_Eigen3 OR NOT EIGEN3_FOUND)
|
||||
if(FORCE_VENDORED_Eigen3 OR NOT EIGEN3_INCLUDE_DIRS)
|
||||
message(STATUS "Using in-tree Eigen")
|
||||
set(EIGEN3_FOUND YES)
|
||||
set(EIGEN3_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/eigen)
|
||||
else()
|
||||
message(STATUS "Using system Eigen: ${EIGEN3_INCLUDE_DIRS}")
|
||||
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
|
||||
# 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
|
||||
|
@ -269,7 +277,7 @@ else()
|
|||
find_package(ZLIB REQUIRED)
|
||||
find_package(PNG REQUIRED)
|
||||
find_package(Freetype REQUIRED)
|
||||
pkg_check_modules(CAIRO REQUIRED cairo)
|
||||
find_package(Cairo REQUIRED)
|
||||
endif()
|
||||
|
||||
# GUI dependencies
|
||||
|
@ -303,6 +311,8 @@ if(ENABLE_GUI)
|
|||
elseif(APPLE)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_library(APPKIT_LIBRARY AppKit REQUIRED)
|
||||
elseif(EMSCRIPTEN)
|
||||
# Everything is built in
|
||||
else()
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(SpaceWare)
|
||||
|
@ -373,9 +383,19 @@ if(MSVC)
|
|||
# Same for the (C99) __func__ special variable; we use it only in C++ code.
|
||||
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
|
||||
# follows: /w4062=-Wswitch
|
||||
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()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
|
|
111
README.md
111
README.md
|
@ -33,6 +33,14 @@ automatically built by the SolveSpace maintainers for each stable release.
|
|||
|
||||
[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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
@ -106,13 +114,7 @@ sudo dnf install git gcc-c++ cmake zlib-devel libpng-devel \
|
|||
mesa-libGL-devel mesa-libGLU-devel libspnav-devel
|
||||
```
|
||||
|
||||
Before building, check out the project and the necessary submodules:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/solvespace/solvespace
|
||||
cd solvespace
|
||||
git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen
|
||||
```
|
||||
Before building, [check out the project and the necessary submodules](#via-source-code).
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Before building, check out the project and the necessary submodules:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/solvespace/solvespace
|
||||
cd solvespace
|
||||
git submodule update --init
|
||||
```
|
||||
Before building, [check out the project and the necessary submodules](#via-source-code).
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
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];
|
||||
it requires a free Apple ID.
|
||||
|
||||
Before building, check out the project and the necessary submodules:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/solvespace/solvespace
|
||||
cd solvespace
|
||||
git submodule update --init
|
||||
```
|
||||
Before building, [check out the project and the necessary submodules](#via-source-code).
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Before building, check out the project and the necessary submodules:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/solvespace/solvespace
|
||||
cd solvespace
|
||||
git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen
|
||||
```
|
||||
Before building, [check out the project and the necessary submodules](#via-source-code).
|
||||
|
||||
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
|
||||
(either Visual C++ or MinGW). If using Visual C++, Visual Studio 2015
|
||||
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
|
||||
|
||||
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.
|
||||
Press "Configure" and "Generate", then open `build\solvespace.sln` with
|
||||
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:
|
||||
|
||||
```bat
|
||||
git clone https://github.com/solvespace/solvespace
|
||||
cd solvespace
|
||||
git submodule update --init
|
||||
mkdir build
|
||||
cd build
|
||||
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:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/solvespace/solvespace
|
||||
cd solvespace
|
||||
git submodule update --init
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
|
|
|
@ -4,12 +4,12 @@ function(disable_warnings)
|
|||
if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -w" PARENT_SCOPE)
|
||||
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()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w" PARENT_SCOPE)
|
||||
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()
|
||||
endfunction()
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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()
|
|
@ -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)
|
|
@ -4,3 +4,7 @@ if(MSVC)
|
|||
set(CMAKE_C_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
|
||||
set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
set(CMAKE_C_FLAGS_DEBUG_INIT "-g4")
|
||||
endif()
|
||||
|
|
|
@ -4,3 +4,7 @@ if(MSVC)
|
|||
set(CMAKE_CXX_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
|
||||
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
set(CMAKE_CXX_FLAGS_DEBUG_INIT "-g4")
|
||||
endif()
|
||||
|
|
|
@ -6,3 +6,8 @@ add_executable(CDemo
|
|||
|
||||
target_link_libraries(CDemo
|
||||
slvs)
|
||||
|
||||
if(EMSCRIPTEN)
|
||||
set_target_properties(CDemo PROPERTIES
|
||||
LINK_FLAGS "-s TOTAL_MEMORY=134217728")
|
||||
endif()
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 4e643b6d3178e0ea2a093b7e14fe621631a91e4b
|
||||
Subproject commit f819dbb4e4813fab464aee16770f39f11476bfea
|
|
@ -1,69 +1,97 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/TingPing/flatpak-manifest-schema/master/flatpak-manifest.schema",
|
||||
"app-id": "com.solvespace.SolveSpace",
|
||||
"runtime": "org.freedesktop.Platform",
|
||||
"runtime-version": "20.08",
|
||||
"runtime-version": "21.08",
|
||||
"sdk": "org.freedesktop.Sdk",
|
||||
"finish-args": [
|
||||
/* Access to display server and OpenGL */
|
||||
"--device=dri",
|
||||
"--share=ipc",
|
||||
"--socket=fallback-x11",
|
||||
"--socket=wayland",
|
||||
"--device=dri",
|
||||
/* Access to save files */
|
||||
"--filesystem=home"
|
||||
"--socket=wayland"
|
||||
],
|
||||
"cleanup": [
|
||||
"/include",
|
||||
"/lib/*/include",
|
||||
"*.a",
|
||||
"*.la",
|
||||
"*.m4",
|
||||
"/lib/libslvs*.so*",
|
||||
"/lib/libglibmm_generate_extra_defs*.so*",
|
||||
"/share/pkgconfig",
|
||||
"*.pc",
|
||||
"/share/man",
|
||||
"/share/doc",
|
||||
"/lib/cmake",
|
||||
"/lib/pkgconfig",
|
||||
"/share/aclocal",
|
||||
/* mm-common junk */
|
||||
"/bin/mm-common-prepare",
|
||||
"/share/mm-common"
|
||||
"/share/pkgconfig",
|
||||
"*.la"
|
||||
],
|
||||
"command": "solvespace",
|
||||
"modules": [
|
||||
{
|
||||
"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",
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://download.gnome.org/sources/glibmm/2.64/glibmm-2.64.5.tar.xz",
|
||||
"sha256": "508fc86e2c9141198aa16c225b16fd6b911917c0d3817602652844d0973ea386"
|
||||
"url": "https://download.gnome.org/sources/mm-common/1.0/mm-common-1.0.4.tar.xz",
|
||||
"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": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "http://ftp.gnome.org/pub/GNOME/sources/cairomm/1.12/cairomm-1.12.0.tar.xz",
|
||||
"sha256": "a54ada8394a86182525c0762e6f50db6b9212a2109280d13ec6a0b29bfd1afe6"
|
||||
"url": "https://download.gnome.org/sources/cairomm/1.12/cairomm-1.12.0.tar.xz",
|
||||
"sha256": "a54ada8394a86182525c0762e6f50db6b9212a2109280d13ec6a0b29bfd1afe6",
|
||||
"x-checker-data": {
|
||||
"type": "gnome",
|
||||
"name": "cairomm",
|
||||
"stable-only": true,
|
||||
"versions": {
|
||||
"<": "1.16.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"cleanup": [
|
||||
"/lib/cairomm-*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"sources": [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://download.gnome.org/sources/gtkmm/3.24/gtkmm-3.24.4.tar.xz",
|
||||
"sha256": "9beb71c3e90cfcfb790396b51e3f5e7169966751efd4f3ef9697114be3be6743"
|
||||
"url": "https://download.gnome.org/sources/pangomm/2.46/pangomm-2.46.2.tar.xz",
|
||||
"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",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"builddir": true,
|
||||
"config-opts": [
|
||||
"-DBUILD_STATIC_LIBS=OFF",
|
||||
"-DENABLE_THREADING=ON"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
/* 0.15-nodoc doesn't build */
|
||||
"type": "archive",
|
||||
"url": "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.13.1-nodoc.tar.gz",
|
||||
"sha256": "94a26340c0785fcff4f46ff38609cf84ebcd670df0c8efd75d039cc951d80132"
|
||||
"url": "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.16.tar.gz",
|
||||
"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": [
|
||||
{
|
||||
"type": "dir",
|
||||
"path": "../.."
|
||||
}
|
||||
],
|
||||
"buildsystem": "cmake",
|
||||
"builddir": true,
|
||||
"config-opts": [
|
||||
"-DFLATPAK=ON",
|
||||
"-DENABLE_CLI=OFF",
|
||||
"-DENABLE_TESTS=OFF"
|
||||
"cleanup": [
|
||||
"/lib/libslvs*.so*"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: solvespace
|
||||
base: core20
|
||||
base: core22
|
||||
summary: Parametric 2d/3d CAD
|
||||
adopt-info: solvespace
|
||||
description: |
|
||||
|
@ -15,6 +15,7 @@ description: |
|
|||
confinement: strict
|
||||
license: GPL-3.0
|
||||
compression: lzo
|
||||
grade: stable
|
||||
|
||||
layout:
|
||||
/usr/share/solvespace:
|
||||
|
@ -24,11 +25,11 @@ apps:
|
|||
solvespace:
|
||||
command: usr/bin/solvespace
|
||||
desktop: solvespace.desktop
|
||||
extensions: [gnome-3-38]
|
||||
extensions: [gnome]
|
||||
plugs: [opengl, unity7, home, removable-media, gsettings, network]
|
||||
cli:
|
||||
command: usr/bin/solvespace-cli
|
||||
extensions: [gnome-3-38]
|
||||
extensions: [gnome]
|
||||
plugs: [home, removable-media, network]
|
||||
|
||||
parts:
|
||||
|
@ -37,16 +38,14 @@ parts:
|
|||
source: ./solvespace-snap-src
|
||||
source-type: local
|
||||
override-pull: |
|
||||
snapcraftctl pull
|
||||
craftctl default
|
||||
git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen
|
||||
override-build: |
|
||||
snapcraftctl build
|
||||
craftctl default
|
||||
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)"
|
||||
snapcraftctl set-version "$version"
|
||||
git describe --exact-match HEAD && grade="stable" || grade="devel"
|
||||
snapcraftctl set-grade "$grade"
|
||||
craftctl set version="$version"
|
||||
cmake-parameters:
|
||||
- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
- -DCMAKE_BUILD_TYPE=Release
|
||||
|
@ -54,8 +53,6 @@ parts:
|
|||
- -DSNAP=ON
|
||||
- -DENABLE_OPENMP=ON
|
||||
- -DENABLE_LTO=ON
|
||||
build-snaps:
|
||||
- gnome-3-38-2004-sdk
|
||||
build-packages:
|
||||
- zlib1g-dev
|
||||
- libpng-dev
|
||||
|
@ -67,6 +64,7 @@ parts:
|
|||
- libspnav-dev
|
||||
- git
|
||||
- g++
|
||||
- libc6-dev
|
||||
stage-packages:
|
||||
- libspnav0
|
||||
- libsigc++-2.0-0v5
|
||||
|
@ -74,14 +72,14 @@ parts:
|
|||
cleanup:
|
||||
after: [solvespace]
|
||||
plugin: nil
|
||||
build-snaps: [gnome-3-38-2004]
|
||||
build-snaps: [gnome-42-2204]
|
||||
override-prime: |
|
||||
set -eux
|
||||
for snap in "gnome-3-38-2004"; 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/{}" \;
|
||||
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 "$CRAFT_PRIME/{}" "$CRAFT_PRIME/usr/{}" \;
|
||||
done
|
||||
for cruft in bug lintian man; do
|
||||
rm -rf $SNAPCRAFT_PRIME/usr/share/$cruft
|
||||
rm -rf $CRAFT_PRIME/usr/share/$cruft
|
||||
done
|
||||
find $SNAPCRAFT_PRIME/usr/share/doc/ -type f -not -name 'copyright' -delete
|
||||
find $SNAPCRAFT_PRIME/usr/share -type d -empty -delete
|
||||
find $CRAFT_PRIME/usr/share/doc/ -type f -not -name 'copyright' -delete
|
||||
find $CRAFT_PRIME/usr/share -type d -empty -delete
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# First, set up registration functions for the kinds of resources we handle.
|
||||
set(resource_root ${CMAKE_CURRENT_SOURCE_DIR}/)
|
||||
set(resource_list)
|
||||
set(resource_names)
|
||||
if(WIN32)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/win32/versioninfo.rc.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc)
|
||||
|
@ -83,6 +84,23 @@ elseif(APPLE)
|
|||
DEPENDS ${source}
|
||||
VERBATIM)
|
||||
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
|
||||
include(GNUInstallDirs)
|
||||
|
||||
|
@ -112,6 +130,7 @@ function(add_resources)
|
|||
foreach(name ${ARGN})
|
||||
add_resource(${name})
|
||||
set(resource_list "${resource_list}" PARENT_SCOPE)
|
||||
set(resource_names "${resource_names}" PARENT_SCOPE)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
|
@ -262,6 +281,7 @@ add_resources(
|
|||
icons/text-window/shaded.png
|
||||
icons/text-window/workplane.png
|
||||
locales.txt
|
||||
locales/cs_CZ.po
|
||||
locales/de_DE.po
|
||||
locales/en_US.po
|
||||
locales/fr_FR.po
|
||||
|
@ -270,6 +290,7 @@ add_resources(
|
|||
locales/tr_TR.po
|
||||
locales/ru_RU.po
|
||||
locales/zh_CN.po
|
||||
locales/ja_JP.po
|
||||
fonts/unifont.hex.gz
|
||||
fonts/private/0-check-false.png
|
||||
fonts/private/1-check-true.png
|
||||
|
@ -304,4 +325,6 @@ add_custom_target(resources
|
|||
DEPENDS ${resource_list})
|
||||
if(WIN32)
|
||||
set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file})
|
||||
elseif(EMSCRIPTEN)
|
||||
set_property(TARGET resources PROPERTY NAMES ${resource_names})
|
||||
endif()
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
SolveSpace is a free (GPLv3) parametric 3d CAD tool. Applications include:
|
||||
</p>
|
||||
<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 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>
|
||||
|
@ -31,6 +31,34 @@
|
|||
<url type="bugtracker">https://github.com/solvespace/solvespace/issues</url>
|
||||
|
||||
<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>
|
||||
<mediatype>application/x-solvespace</mediatype>
|
||||
</provides>
|
||||
|
@ -38,6 +66,19 @@
|
|||
<content_rating type="oars-1.0" />
|
||||
|
||||
<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">
|
||||
<description>
|
||||
<p>Major new stable release. Includes new intersection boolean operation,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Version=1.0
|
||||
Name=SolveSpace
|
||||
Comment=A parametric 2d/3d CAD
|
||||
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace
|
||||
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f
|
||||
MimeType=application/x-solvespace
|
||||
Icon=com.solvespace.SolveSpace
|
||||
Type=Application
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Version=1.0
|
||||
Name=SolveSpace
|
||||
Comment=A parametric 2d/3d CAD
|
||||
Exec=solvespace
|
||||
Exec=solvespace %f
|
||||
MimeType=application/x-solvespace
|
||||
Icon=${SNAP}/meta/icons/hicolor/scalable/apps/snap.solvespace.svg
|
||||
Type=Application
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Version=1.0
|
||||
Name=SolveSpace
|
||||
Comment=A parametric 2d/3d CAD
|
||||
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace
|
||||
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f
|
||||
MimeType=application/x-solvespace
|
||||
Icon=solvespace
|
||||
Type=Application
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# 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.
|
||||
cs-CZ,1029,Česky
|
||||
de-DE,0407,Deutsch
|
||||
en-US,0409,English (US)
|
||||
fr-FR,040C,Français
|
||||
|
@ -8,3 +9,4 @@ ru-RU,0419,Русский
|
|||
tr-TR,041F,Türkçe
|
||||
uk-UA,0422,Українська
|
||||
zh-CN,0804,简体中文
|
||||
ja-JP,0411,日本語
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,15 +7,15 @@ msgstr ""
|
|||
"Project-Id-Version: SolveSpace 3.0\n"
|
||||
"Report-Msgid-Bugs-To: whitequark@whitequark.org\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"
|
||||
"Language-Team: none\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Zanata 4.5.0\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||
"X-Generator: Poedit 2.4.2\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: clipboard.cpp:309
|
||||
msgid ""
|
||||
|
@ -26,7 +26,7 @@ msgstr ""
|
|||
"Ausschneiden, Einfügen und Kopieren sind nur in einer Arbeitsebene "
|
||||
"zulässig.\n"
|
||||
"\n"
|
||||
"Aktivieren Sie eine mit Skizze -> In Arbeitsebene"
|
||||
"Aktivieren Sie eine mit \"Skizze -> In Arbeitsebene\"."
|
||||
|
||||
#: clipboard.cpp:326
|
||||
msgid "Clipboard is empty; nothing to paste."
|
||||
|
@ -172,12 +172,12 @@ msgstr "Längenverhältnis"
|
|||
#: constraint.cpp:25
|
||||
msgctxt "constr-name"
|
||||
msgid "arc-arc-length-ratio"
|
||||
msgstr ""
|
||||
msgstr "Bogen-Bogen-Längenverhältnis"
|
||||
|
||||
#: constraint.cpp:26
|
||||
msgctxt "constr-name"
|
||||
msgid "arc-line-length-ratio"
|
||||
msgstr ""
|
||||
msgstr "Bogen-Linien-Längenverhältnis"
|
||||
|
||||
#: constraint.cpp:27
|
||||
msgctxt "constr-name"
|
||||
|
@ -187,12 +187,12 @@ msgstr "Längendifferenz"
|
|||
#: constraint.cpp:28
|
||||
msgctxt "constr-name"
|
||||
msgid "arc-arc-len-difference"
|
||||
msgstr ""
|
||||
msgstr "Bogen-Bogen-Längendifferenz"
|
||||
|
||||
#: constraint.cpp:29
|
||||
msgctxt "constr-name"
|
||||
msgid "arc-line-len-difference"
|
||||
msgstr ""
|
||||
msgstr "Bogen-Linien-Längendifferenz"
|
||||
|
||||
#: constraint.cpp:30
|
||||
msgctxt "constr-name"
|
||||
|
@ -306,7 +306,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Die Bogentangente und das Liniensegment müssen einen gemeinsamen Endpunkt "
|
||||
"haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die "
|
||||
"Tangente einschränken. -> Sc"
|
||||
"Tangente einschränken."
|
||||
|
||||
#: constraint.cpp:163
|
||||
msgid ""
|
||||
|
@ -315,7 +315,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Die Kurventangente und das Liniensegment müssen einen gemeinsamen Endpunkt "
|
||||
"haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die "
|
||||
"Tangente einschränken. -> Sc"
|
||||
"Tangente einschränken."
|
||||
|
||||
#: constraint.cpp:189
|
||||
msgid ""
|
||||
|
@ -323,7 +323,7 @@ msgid ""
|
|||
"before constraining tangent."
|
||||
msgstr ""
|
||||
"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
|
||||
msgid ""
|
||||
|
@ -408,6 +408,12 @@ msgid ""
|
|||
" * two arcs\n"
|
||||
" * one arc and one line segment\n"
|
||||
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
|
||||
msgid ""
|
||||
|
@ -418,6 +424,12 @@ msgid ""
|
|||
" * two arcs\n"
|
||||
" * one arc and one line segment\n"
|
||||
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
|
||||
msgid ""
|
||||
|
@ -584,7 +596,7 @@ msgid ""
|
|||
"2d View to export bare lines and curves."
|
||||
msgstr ""
|
||||
"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
|
||||
msgid ""
|
||||
|
@ -699,7 +711,7 @@ msgstr "&Neu"
|
|||
|
||||
#: graphicswin.cpp:43
|
||||
msgid "&Open..."
|
||||
msgstr "&Öffnen"
|
||||
msgstr "&Öffnen..."
|
||||
|
||||
#: graphicswin.cpp:44
|
||||
msgid "Open &Recent"
|
||||
|
@ -727,7 +739,7 @@ msgstr "Exportiere 2D-Auswahl…"
|
|||
|
||||
#: graphicswin.cpp:51
|
||||
msgid "Export 3d &Wireframe..."
|
||||
msgstr "Exportiere 3D-Drahtgittermodell"
|
||||
msgstr "Exportiere 3D-Drahtgittermodell..."
|
||||
|
||||
#: graphicswin.cpp:52
|
||||
msgid "Export Triangle &Mesh..."
|
||||
|
@ -859,7 +871,7 @@ msgstr "Perspektivische Projektion"
|
|||
|
||||
#: graphicswin.cpp:97
|
||||
msgid "Show E&xploded View"
|
||||
msgstr ""
|
||||
msgstr "Zeige e&xplodierte Ansicht"
|
||||
|
||||
#: graphicswin.cpp:98
|
||||
msgid "Dimension &Units"
|
||||
|
@ -879,7 +891,7 @@ msgstr "Maße in Zoll"
|
|||
|
||||
#: graphicswin.cpp:102
|
||||
msgid "Dimensions in &Feet and Inches"
|
||||
msgstr ""
|
||||
msgstr "Maße in &Fuß und Inch"
|
||||
|
||||
#: graphicswin.cpp:104
|
||||
msgid "Show &Toolbar"
|
||||
|
@ -931,7 +943,7 @@ msgstr "D&rehen"
|
|||
|
||||
#: graphicswin.cpp:121
|
||||
msgid "Link / Assemble..."
|
||||
msgstr "Verknüpfen / Zusammensetzen"
|
||||
msgstr "Verknüpfen / Zusammensetzen..."
|
||||
|
||||
#: graphicswin.cpp:122
|
||||
msgid "Link Recent"
|
||||
|
@ -1047,11 +1059,11 @@ msgstr "Gleicher Abstand / Radius / Winkel"
|
|||
|
||||
#: graphicswin.cpp:158
|
||||
msgid "Length / Arc Ra&tio"
|
||||
msgstr ""
|
||||
msgstr "Länge / Bogen Verhäl&tnis"
|
||||
|
||||
#: graphicswin.cpp:159
|
||||
msgid "Length / Arc Diff&erence"
|
||||
msgstr ""
|
||||
msgstr "Länge / Bogen Diff&erenz"
|
||||
|
||||
#: graphicswin.cpp:160
|
||||
msgid "At &Midpoint"
|
||||
|
@ -1119,7 +1131,7 @@ msgstr "Punkt nachzeichnen"
|
|||
|
||||
#: graphicswin.cpp:180
|
||||
msgid "&Stop Tracing..."
|
||||
msgstr "Nachzeichnen beenden"
|
||||
msgstr "Nachzeichnen beenden..."
|
||||
|
||||
#: graphicswin.cpp:181
|
||||
msgid "Step &Dimension..."
|
||||
|
@ -1139,7 +1151,7 @@ msgstr "&Website / Anleitung"
|
|||
|
||||
#: graphicswin.cpp:186
|
||||
msgid "&Go to GitHub commit"
|
||||
msgstr ""
|
||||
msgstr "&Gehe zu GitHub commit"
|
||||
|
||||
#: graphicswin.cpp:188
|
||||
msgid "&About"
|
||||
|
@ -1300,6 +1312,12 @@ msgid ""
|
|||
" * a point and a normal (through the point, orthogonal to the normal)\n"
|
||||
" * a workplane (copy of the workplane)\n"
|
||||
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
|
||||
msgid ""
|
||||
|
@ -1307,7 +1325,7 @@ msgid ""
|
|||
"will be extruded normal to the workplane."
|
||||
msgstr ""
|
||||
"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
|
||||
msgctxt "group-name"
|
||||
|
@ -1434,7 +1452,7 @@ msgstr "Kante mit Länge Null!"
|
|||
|
||||
#: importmesh.cpp:136
|
||||
msgid "Text-formated STL files are not currently supported"
|
||||
msgstr ""
|
||||
msgstr "Text-formatierte STL Dateien werden aktuell nicht unterstützt"
|
||||
|
||||
#: modify.cpp:252
|
||||
msgid "Must be sketching in workplane to create tangent arc."
|
||||
|
@ -1447,7 +1465,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Um eine Bogentangente zu erstellen, wählen Sie einen Punkt, in dem sich zwei "
|
||||
"nicht-Konstruktionslinien oder -kreise in dieser Gruppe und Arbeitsebene "
|
||||
"treffen. "
|
||||
"treffen."
|
||||
|
||||
#: modify.cpp:386
|
||||
msgid ""
|
||||
|
@ -1646,7 +1664,7 @@ msgstr "SolveSpace-Modelle"
|
|||
#: platform/gui.cpp:89
|
||||
msgctxt "file-type"
|
||||
msgid "ALL"
|
||||
msgstr ""
|
||||
msgstr "ALLE"
|
||||
|
||||
#: platform/gui.cpp:91
|
||||
msgctxt "file-type"
|
||||
|
@ -1656,7 +1674,7 @@ msgstr "IDF Leiterplatte"
|
|||
#: platform/gui.cpp:92
|
||||
msgctxt "file-type"
|
||||
msgid "STL triangle mesh"
|
||||
msgstr ""
|
||||
msgstr "STL-Dreiecks-Netz"
|
||||
|
||||
#: platform/gui.cpp:96
|
||||
msgctxt "file-type"
|
||||
|
@ -2072,7 +2090,7 @@ msgstr ""
|
|||
|
||||
#: style.cpp:735
|
||||
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
|
||||
msgid "Can't repeat fewer than 1 time."
|
||||
|
@ -2084,7 +2102,7 @@ msgstr "Nicht mehr als 999 Wiederholungen möglich."
|
|||
|
||||
#: textscreens.cpp:820
|
||||
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
|
||||
msgid "Opacity must be between zero and one."
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -8,8 +8,8 @@ msgstr ""
|
|||
"Project-Id-Version: SolveSpace 3.0\n"
|
||||
"Report-Msgid-Bugs-To: whitequark@whitequark.org\n"
|
||||
"POT-Creation-Date: 2022-02-01 16:24+0200\n"
|
||||
"PO-Revision-Date: 2021-10-04 15:33+0300\n"
|
||||
"Last-Translator: Olesya Gerasimenko <translation-team@basealt.ru>\n"
|
||||
"PO-Revision-Date: 2022-11-05 19:37+0200\n"
|
||||
"Last-Translator: ruevs Olesya Gerasimenko <translation-team@basealt.ru>\n"
|
||||
"Language-Team: Basealt Translation Team\n"
|
||||
"Language: ru_RU\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -936,7 +936,7 @@ msgstr "Тело В&ращения"
|
|||
|
||||
#: graphicswin.cpp:119
|
||||
msgid "Re&volve"
|
||||
msgstr "Тело В&ращения"
|
||||
msgstr "Тело В&ращения на угол"
|
||||
|
||||
#: graphicswin.cpp:121
|
||||
msgid "Link / Assemble..."
|
||||
|
@ -1350,9 +1350,9 @@ msgstr ""
|
|||
"Группа может быть создана, используя в качестве выделения следующие "
|
||||
"примитивы:\n"
|
||||
"\n"
|
||||
" * точку и отрезок / координатный базис (нормаль) (тело вращения вокруг "
|
||||
" * точку и отрезок / координатный базис (нормаль) (вращение вокруг "
|
||||
"оси, проходящей через точку и параллельной отрезку / нормали)\n"
|
||||
" * отрезок (тело вращения вокруг оси, проходящей через отрезок)\n"
|
||||
" * отрезок (вращение вокруг оси, проходящей через отрезок)\n"
|
||||
"\n"
|
||||
|
||||
#: group.cpp:201
|
||||
|
@ -1363,7 +1363,7 @@ msgstr "тело-вращения"
|
|||
#: group.cpp:206
|
||||
msgid "Revolve operation can only be applied to planar sketches."
|
||||
msgstr ""
|
||||
"Операция создания тела вращения может быть применена только к плоским "
|
||||
"Операция создания тела вращения на угол может быть применена только к плоским "
|
||||
"эскизам."
|
||||
|
||||
#: group.cpp:217
|
||||
|
@ -1374,19 +1374,19 @@ msgid ""
|
|||
"to line / normal, through point)\n"
|
||||
" * a line segment (revolved about line segment)\n"
|
||||
msgstr ""
|
||||
"Неправильное выделение для создания группы тела вращения. \n"
|
||||
"Неправильное выделение для создания группы тела вращения на угол. \n"
|
||||
"Группа может быть создана, используя в качестве выделения следующие "
|
||||
"примитивы:\n"
|
||||
"\n"
|
||||
" * точку и отрезок / координатный базис (нормаль) (тело вращения вокруг "
|
||||
" * точку и отрезок / координатный базис (нормаль) (вращение вокруг "
|
||||
"оси, проходящей через точку и параллельной отрезку / нормали)\n"
|
||||
" * отрезок (тело вращения вокруг оси, проходящей через отрезок)\n"
|
||||
" * отрезок (вращение вокруг оси, проходящей через отрезок)\n"
|
||||
"\n"
|
||||
|
||||
#: group.cpp:229
|
||||
msgctxt "group-name"
|
||||
msgid "revolve"
|
||||
msgstr "тело-вращения"
|
||||
msgstr "тело-вращения-на-угол"
|
||||
|
||||
#: group.cpp:234
|
||||
msgid "Helix operation can only be applied to planar sketches."
|
||||
|
@ -2219,7 +2219,7 @@ msgstr "Создать группу выдавливания текущего э
|
|||
|
||||
#: toolbar.cpp:70
|
||||
msgid "New group rotating active sketch"
|
||||
msgstr "Создать группу вращения текущего эскиза"
|
||||
msgstr "Создать группу тела вращения текущего эскиза"
|
||||
|
||||
#: toolbar.cpp:72
|
||||
msgid "New group helix from active sketch"
|
||||
|
@ -2227,7 +2227,7 @@ msgstr "Создать группу тела выдавливания по ви
|
|||
|
||||
#: toolbar.cpp:74
|
||||
msgid "New group revolve active sketch"
|
||||
msgstr "Создать группу тела вращения из текущего эскиза"
|
||||
msgstr "Создать группу тела вращения на угол из текущего эскиза"
|
||||
|
||||
#: toolbar.cpp:76
|
||||
msgid "New group step and repeat rotating"
|
||||
|
|
|
@ -82,7 +82,9 @@ target_compile_definitions(slvs
|
|||
PRIVATE -DLIBRARY)
|
||||
|
||||
target_include_directories(slvs
|
||||
PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
PUBLIC
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${EIGEN3_INCLUDE_DIRS})
|
||||
|
||||
target_link_libraries(slvs PRIVATE slvs_deps)
|
||||
|
||||
|
@ -101,7 +103,8 @@ endif()
|
|||
set(every_platform_SOURCES
|
||||
platform/guiwin.cpp
|
||||
platform/guigtk.cpp
|
||||
platform/guimac.mm)
|
||||
platform/guimac.mm
|
||||
platform/guihtml.cpp)
|
||||
|
||||
# solvespace library
|
||||
|
||||
|
@ -166,6 +169,7 @@ add_library(solvespace-core STATIC
|
|||
srf/merge.cpp
|
||||
srf/ratpoly.cpp
|
||||
srf/raycast.cpp
|
||||
srf/shell.cpp
|
||||
srf/surface.cpp
|
||||
srf/surfinter.cpp
|
||||
srf/triangulate.cpp)
|
||||
|
@ -300,6 +304,56 @@ if(ENABLE_GUI)
|
|||
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES"
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.solvespace"
|
||||
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()
|
||||
target_sources(solvespace PRIVATE
|
||||
platform/guigtk.cpp)
|
||||
|
@ -335,7 +389,8 @@ target_compile_definitions(solvespace-headless
|
|||
PRIVATE 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
|
||||
PRIVATE
|
||||
|
@ -363,7 +418,7 @@ endif()
|
|||
|
||||
# solvespace unix package
|
||||
|
||||
if(NOT (WIN32 OR APPLE))
|
||||
if(NOT (WIN32 OR APPLE OR EMSCRIPTEN))
|
||||
if(ENABLE_GUI)
|
||||
install(TARGETS solvespace
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
|
|
@ -721,7 +721,11 @@ void Constraint::MenuConstrain(Command id) {
|
|||
}
|
||||
|
||||
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.entityA = gs.vector[0];
|
||||
c.entityB = gs.vector[1];
|
||||
|
@ -765,6 +769,7 @@ void Constraint::MenuConstrain(Command id) {
|
|||
} else {
|
||||
Error(_("Bad selection for parallel / tangent constraint. This "
|
||||
"constraint can apply to:\n\n"
|
||||
" * two faces\n"
|
||||
" * two line segments (parallel)\n"
|
||||
" * a line segment and a normal (parallel)\n"
|
||||
" * two normals (parallel)\n"
|
||||
|
@ -776,13 +781,18 @@ void Constraint::MenuConstrain(Command id) {
|
|||
break;
|
||||
|
||||
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.entityA = gs.vector[0];
|
||||
c.entityB = gs.vector[1];
|
||||
} else {
|
||||
Error(_("Bad selection for perpendicular constraint. This "
|
||||
"constraint can apply to:\n\n"
|
||||
" * two faces\n"
|
||||
" * two line segments\n"
|
||||
" * a line segment and a normal\n"
|
||||
" * two normals\n"));
|
||||
|
|
|
@ -89,6 +89,7 @@ void TextWindow::DescribeSelection() {
|
|||
case Entity::Type::POINT_N_ROT_TRANS:
|
||||
case Entity::Type::POINT_N_COPY:
|
||||
case Entity::Type::POINT_N_ROT_AA:
|
||||
case Entity::Type::POINT_N_ROT_AXIS_TRANS:
|
||||
p = e->PointGetNum();
|
||||
Printf(false, "%FtPOINT%E at " PT_AS_STR, COSTR(e, p));
|
||||
break;
|
||||
|
@ -171,6 +172,7 @@ void TextWindow::DescribeSelection() {
|
|||
double r = e->CircleGetRadiusNum();
|
||||
Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).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;
|
||||
}
|
||||
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_AA:
|
||||
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");
|
||||
p = e->FaceGetNormalNum();
|
||||
Printf(true, " normal = " PT_AS_NUM, CO(p));
|
||||
|
@ -424,14 +428,19 @@ void TextWindow::DescribeSelection() {
|
|||
double d = (p1.Minus(p0)).Dot(n0);
|
||||
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) {
|
||||
Constraint *c = SK.GetConstraint(gs.constraint[0]);
|
||||
const std::string &desc = c->DescriptionString().c_str();
|
||||
|
||||
if(c->type == Constraint::Type::COMMENT) {
|
||||
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()) {
|
||||
if(c->reference) {
|
||||
Printf(false, "%FtREFERENCE%E %s", desc.c_str());
|
||||
|
|
|
@ -26,6 +26,9 @@ bool EntityBase::HasVector() const {
|
|||
}
|
||||
|
||||
ExprVector EntityBase::VectorGetExprsInWorkplane(hEntity wrkpl) const {
|
||||
if(IsFace()) {
|
||||
return FaceGetNormalExprs();
|
||||
}
|
||||
switch(type) {
|
||||
case Type::LINE_SEGMENT:
|
||||
return (SK.GetEntity(point[0])->PointGetExprsInWorkplane(wrkpl)).Minus(
|
||||
|
@ -62,6 +65,9 @@ ExprVector EntityBase::VectorGetExprs() const {
|
|||
}
|
||||
|
||||
Vector EntityBase::VectorGetNum() const {
|
||||
if(IsFace()) {
|
||||
return FaceGetNormalNum();
|
||||
}
|
||||
switch(type) {
|
||||
case Type::LINE_SEGMENT:
|
||||
return (SK.GetEntity(point[0])->PointGetNum()).Minus(
|
||||
|
@ -79,6 +85,9 @@ Vector EntityBase::VectorGetNum() const {
|
|||
}
|
||||
|
||||
Vector EntityBase::VectorGetRefPoint() const {
|
||||
if(IsFace()) {
|
||||
return FaceGetPointNum();
|
||||
}
|
||||
switch(type) {
|
||||
case Type::LINE_SEGMENT:
|
||||
return ((SK.GetEntity(point[0])->PointGetNum()).Plus(
|
||||
|
|
|
@ -918,7 +918,7 @@ try_again:
|
|||
switch(LocateImportedFile(linkFileRelative, canCancel)) {
|
||||
case Platform::MessageDialog::Response::YES: {
|
||||
Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
|
||||
dialog->AddFilters(Platform::SolveSpaceModelFileFilters);
|
||||
dialog->AddFilters(Platform::SolveSpaceLinkFileFilters);
|
||||
dialog->ThawChoices(settings, "LinkSketch");
|
||||
dialog->SuggestFilename(linkFileRelative);
|
||||
if(dialog->RunModal()) {
|
||||
|
|
|
@ -433,6 +433,11 @@ void GraphicsWindow::Init() {
|
|||
// a canvas.
|
||||
window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4);
|
||||
window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT);
|
||||
window->onContextLost = [&] {
|
||||
canvas = NULL;
|
||||
persistentCanvas = NULL;
|
||||
persistentDirty = true;
|
||||
};
|
||||
window->onRender = std::bind(&GraphicsWindow::Paint, this);
|
||||
window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1);
|
||||
window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1);
|
||||
|
@ -712,16 +717,47 @@ double GraphicsWindow::ZoomToFit(const Camera &camera,
|
|||
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) {
|
||||
switch(id) {
|
||||
case Command::ZOOM_IN:
|
||||
SS.GW.scale *= 1.2;
|
||||
SS.ScheduleShowTW();
|
||||
SS.GW.ZoomToMouse(1);
|
||||
break;
|
||||
|
||||
case Command::ZOOM_OUT:
|
||||
SS.GW.scale /= 1.2;
|
||||
SS.ScheduleShowTW();
|
||||
SS.GW.ZoomToMouse(-1);
|
||||
break;
|
||||
|
||||
case Command::ZOOM_TO_FIT:
|
||||
|
@ -781,13 +817,18 @@ void GraphicsWindow::MenuView(Command id) {
|
|||
Quaternion quatf = quat0;
|
||||
double dmin = 1e10;
|
||||
|
||||
// There are 24 possible views; 3*2*2*2
|
||||
int i, j, negi, negj;
|
||||
for(i = 0; i < 3; i++) {
|
||||
for(j = 0; j < 3; j++) {
|
||||
// There are 24 possible views (3*2*2*2), if all are
|
||||
// allowed. If the user is in turn-table mode, the
|
||||
// isometric view must have the z-axis facing up, leaving
|
||||
// 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;
|
||||
for(negi = 0; negi < 2; negi++) {
|
||||
for(negj = 0; negj < 2; negj++) {
|
||||
if(require_turntable && (j!=2)) continue;
|
||||
for(int negi = 0; negi < 2; negi++) {
|
||||
for(int negj = 0; negj < 2; negj++) {
|
||||
Vector ou = ortho[i], ov = ortho[j];
|
||||
if(negi) ou = ou.ScaledBy(-1);
|
||||
if(negj) ov = ov.ScaledBy(-1);
|
||||
|
|
|
@ -914,7 +914,7 @@ bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) {
|
|||
break;
|
||||
|
||||
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;
|
||||
|
||||
case MouseEvent::Type::LEAVE:
|
||||
|
@ -1478,17 +1478,10 @@ void GraphicsWindow::EditControlDone(const std::string &s) {
|
|||
}
|
||||
}
|
||||
|
||||
void GraphicsWindow::MouseScroll(double x, double y, double delta) {
|
||||
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).
|
||||
void GraphicsWindow::MouseScroll(double zoomMultiplyer) {
|
||||
// To support smooth scrolling where scroll wheel events come in increments
|
||||
// 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
|
||||
// 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
|
||||
|
@ -1496,21 +1489,7 @@ void GraphicsWindow::MouseScroll(double x, double y, double delta) {
|
|||
// while
|
||||
// scale * a * b != scale * (a+b)
|
||||
// So this constant is ln(1.2) = 0.1823216 to make the default zoom 1.2x
|
||||
scale *= exp(0.1823216 * delta);
|
||||
|
||||
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();
|
||||
ZoomToMouse(zoomMultiplyer);
|
||||
}
|
||||
|
||||
void GraphicsWindow::MouseLeave() {
|
||||
|
|
|
@ -221,6 +221,7 @@ public:
|
|||
std::function<bool(KeyboardEvent)> onKeyboardEvent;
|
||||
std::function<void(std::string)> onEditingDone;
|
||||
std::function<void(double)> onScrollbarAdjusted;
|
||||
std::function<void()> onContextLost;
|
||||
std::function<void()> onRender;
|
||||
|
||||
virtual ~Window() = default;
|
||||
|
@ -229,7 +230,7 @@ public:
|
|||
virtual double GetPixelDensity() = 0;
|
||||
// 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.
|
||||
virtual int GetDevicePixelRatio() = 0;
|
||||
virtual double GetDevicePixelRatio() = 0;
|
||||
// Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio.
|
||||
virtual double GetDeviceFontScale() {
|
||||
return GetPixelDensity() / GetDevicePixelRatio() / 96.0;
|
||||
|
|
|
@ -889,7 +889,7 @@ public:
|
|||
return gtkWindow.get_screen()->get_resolution();
|
||||
}
|
||||
|
||||
int GetDevicePixelRatio() override {
|
||||
double GetDevicePixelRatio() override {
|
||||
return gtkWindow.get_scale_factor();
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -372,7 +372,8 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
double rotationGestureCurrent;
|
||||
Point2d trackpadPositionShift;
|
||||
bool inTrackpadScrollGesture;
|
||||
int numTouches;
|
||||
int activeTrackpadTouches;
|
||||
bool scrollFromTrackpadTouch;
|
||||
Platform::Window::Kind kind;
|
||||
}
|
||||
|
||||
|
@ -398,7 +399,8 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
editor.action = @selector(didEdit:);
|
||||
|
||||
inTrackpadScrollGesture = false;
|
||||
numTouches = 0;
|
||||
activeTrackpadTouches = 0;
|
||||
scrollFromTrackpadTouch = false;
|
||||
self.acceptsTouchEvents = YES;
|
||||
kind = aKind;
|
||||
if(kind == Platform::Window::Kind::TOPLEVEL) {
|
||||
|
@ -576,9 +578,16 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
using Platform::MouseEvent;
|
||||
|
||||
MouseEvent event = [self convertMouseEvent:nsEvent];
|
||||
// Check for number of touches to exclude single-finger scrolling on Magic Mouse
|
||||
bool isTrackpadEvent = numTouches >= 2 && nsEvent.subtype == NSEventSubtypeTabletPoint;
|
||||
if(isTrackpadEvent && kind == Platform::Window::Kind::TOPLEVEL) {
|
||||
if(nsEvent.phase == NSEventPhaseBegan) {
|
||||
// If this scroll began on trackpad then touchesBeganWithEvent was called prior to this
|
||||
// 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
|
||||
// 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
|
||||
|
@ -632,20 +641,15 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
|
|||
}
|
||||
|
||||
- (void)touchesBeganWithEvent:(NSEvent *)event {
|
||||
numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count;
|
||||
[super touchesBeganWithEvent:event];
|
||||
}
|
||||
- (void)touchesMovedWithEvent:(NSEvent *)event {
|
||||
numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count;
|
||||
[super touchesMovedWithEvent:event];
|
||||
activeTrackpadTouches++;
|
||||
}
|
||||
|
||||
- (void)touchesEndedWithEvent:(NSEvent *)event {
|
||||
numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count;
|
||||
[super touchesEndedWithEvent:event];
|
||||
activeTrackpadTouches--;
|
||||
}
|
||||
|
||||
- (void)touchesCancelledWithEvent:(NSEvent *)event {
|
||||
numTouches = 0;
|
||||
[super touchesCancelledWithEvent:event];
|
||||
activeTrackpadTouches--;
|
||||
}
|
||||
|
||||
- (void)mouseExited:(NSEvent *)nsEvent {
|
||||
|
@ -983,10 +987,10 @@ public:
|
|||
return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f;
|
||||
}
|
||||
|
||||
int GetDevicePixelRatio() override {
|
||||
double GetDevicePixelRatio() override {
|
||||
NSSize unitSize = { 1.0f, 0.0f };
|
||||
unitSize = [ssView convertSizeToBacking:unitSize];
|
||||
return (int)unitSize.width;
|
||||
return unitSize.width;
|
||||
}
|
||||
|
||||
bool IsVisible() override {
|
||||
|
|
|
@ -793,7 +793,7 @@ public:
|
|||
break;
|
||||
|
||||
case WM_SIZING: {
|
||||
int pixelRatio = window->GetDevicePixelRatio();
|
||||
double pixelRatio = window->GetDevicePixelRatio();
|
||||
|
||||
RECT rcw, rcc;
|
||||
sscheck(GetWindowRect(window->hWindow, &rcw));
|
||||
|
@ -806,10 +806,10 @@ public:
|
|||
int adjHeight = rc->bottom - rc->top;
|
||||
|
||||
adjWidth -= nonClientWidth;
|
||||
adjWidth = max(window->minWidth * pixelRatio, adjWidth);
|
||||
adjWidth = max((int)(window->minWidth * pixelRatio), adjWidth);
|
||||
adjWidth += nonClientWidth;
|
||||
adjHeight -= nonClientHeight;
|
||||
adjHeight = max(window->minHeight * pixelRatio, adjHeight);
|
||||
adjHeight = max((int)(window->minHeight * pixelRatio), adjHeight);
|
||||
adjHeight += nonClientHeight;
|
||||
switch(wParam) {
|
||||
case WMSZ_RIGHT:
|
||||
|
@ -868,7 +868,7 @@ public:
|
|||
case WM_MOUSEMOVE:
|
||||
case WM_MOUSEWHEEL:
|
||||
case WM_MOUSELEAVE: {
|
||||
int pixelRatio = window->GetDevicePixelRatio();
|
||||
double pixelRatio = window->GetDevicePixelRatio();
|
||||
|
||||
MouseEvent event = {};
|
||||
event.x = GET_X_LPARAM(lParam) / pixelRatio;
|
||||
|
@ -941,7 +941,7 @@ public:
|
|||
event.y = pt.y / pixelRatio;
|
||||
|
||||
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;
|
||||
|
||||
case WM_MOUSELEAVE:
|
||||
|
@ -1109,10 +1109,10 @@ public:
|
|||
return (double)dpi;
|
||||
}
|
||||
|
||||
int GetDevicePixelRatio() override {
|
||||
double GetDevicePixelRatio() override {
|
||||
UINT dpi;
|
||||
sscheck(dpi = ssGetDpiForWindow(hWindow));
|
||||
return dpi / USER_DEFAULT_SCREEN_DPI;
|
||||
return (double)dpi / USER_DEFAULT_SCREEN_DPI;
|
||||
}
|
||||
|
||||
bool IsVisible() override {
|
||||
|
@ -1177,7 +1177,7 @@ public:
|
|||
}
|
||||
|
||||
void GetContentSize(double *width, double *height) override {
|
||||
int pixelRatio = GetDevicePixelRatio();
|
||||
double pixelRatio = GetDevicePixelRatio();
|
||||
|
||||
RECT rc;
|
||||
sscheck(GetClientRect(hWindow, &rc));
|
||||
|
@ -1189,15 +1189,15 @@ public:
|
|||
minWidth = (int)width;
|
||||
minHeight = (int)height;
|
||||
|
||||
int pixelRatio = GetDevicePixelRatio();
|
||||
double pixelRatio = GetDevicePixelRatio();
|
||||
|
||||
RECT rc;
|
||||
sscheck(GetClientRect(hWindow, &rc));
|
||||
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) {
|
||||
rc.bottom = rc.top + minHeight * pixelRatio;
|
||||
rc.bottom = rc.top + (LONG)(minHeight * pixelRatio);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1270,7 +1270,7 @@ public:
|
|||
tooltipText = newText;
|
||||
|
||||
if(!newText.empty()) {
|
||||
int pixelRatio = GetDevicePixelRatio();
|
||||
double pixelRatio = GetDevicePixelRatio();
|
||||
RECT toolRect;
|
||||
toolRect.left = (int)(x * pixelRatio);
|
||||
toolRect.top = (int)(y * pixelRatio);
|
||||
|
@ -1301,9 +1301,9 @@ public:
|
|||
bool isMonospace, const std::string &text) override {
|
||||
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,
|
||||
DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial");
|
||||
if(hFont == NULL) {
|
||||
|
@ -1324,12 +1324,12 @@ public:
|
|||
sscheck(ReleaseDC(hEditor, hDc));
|
||||
|
||||
RECT rc;
|
||||
rc.left = (LONG)x * pixelRatio;
|
||||
rc.top = (LONG)y * pixelRatio - tm.tmAscent;
|
||||
rc.left = (LONG)(x * pixelRatio);
|
||||
rc.top = (LONG)(y * pixelRatio) - tm.tmAscent;
|
||||
// Add one extra char width to avoid scrolling.
|
||||
rc.right = (LONG)x * pixelRatio +
|
||||
std::max((LONG)minWidth * pixelRatio, ts.cx + tm.tmAveCharWidth);
|
||||
rc.bottom = (LONG)y * pixelRatio + tm.tmDescent;
|
||||
rc.right = (LONG)(x * pixelRatio) +
|
||||
std::max((LONG)(minWidth * pixelRatio), ts.cx + tm.tmAveCharWidth);
|
||||
rc.bottom = (LONG)(y * pixelRatio) + tm.tmDescent;
|
||||
sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE,
|
||||
ssGetDpiForWindow(hWindow)));
|
||||
|
||||
|
@ -1608,7 +1608,7 @@ public:
|
|||
|
||||
void AddFilter(std::string name, std::vector<std::string> extensions) override {
|
||||
std::string desc, patterns;
|
||||
for(auto extension : extensions) {
|
||||
for(auto &extension : extensions) {
|
||||
std::string pattern = "*." + extension;
|
||||
if(!desc.empty()) desc += ", ";
|
||||
desc += pattern;
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
|
@ -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%);
|
||||
}
|
|
@ -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;
|
|
@ -516,6 +516,12 @@ static Platform::Path ResourcePath(const std::string &name) {
|
|||
return path;
|
||||
}
|
||||
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
|
||||
static Platform::Path ResourcePath(const std::string &name) {
|
||||
return Path::From("res/" + name);
|
||||
}
|
||||
|
||||
#elif !defined(WIN32)
|
||||
|
||||
# if defined(__linux__)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
#ifndef SOLVESPACE_GL3SHADER_H
|
||||
#define SOLVESPACE_GL3SHADER_H
|
||||
|
||||
#if defined(WIN32)
|
||||
#if defined(WIN32) || defined(__EMSCRIPTEN__)
|
||||
# define GL_APICALL /*static linkage*/
|
||||
# define GL_GLEXT_PROTOTYPES
|
||||
# include <GLES2/gl2.h>
|
||||
|
|
|
@ -125,12 +125,8 @@ void SolveSpaceUI::Init() {
|
|||
SetLocale(locale);
|
||||
}
|
||||
|
||||
generateAllTimer = Platform::CreateTimer();
|
||||
generateAllTimer->onTimeout = std::bind(&SolveSpaceUI::GenerateAll, &SS, Generate::DIRTY,
|
||||
/*andFindFree=*/false, /*genForBBox=*/false);
|
||||
|
||||
showTWTimer = Platform::CreateTimer();
|
||||
showTWTimer->onTimeout = std::bind(&TextWindow::Show, &TW);
|
||||
refreshTimer = Platform::CreateTimer();
|
||||
refreshTimer->onTimeout = std::bind(&SolveSpaceUI::Refresh, &SS);
|
||||
|
||||
autosaveTimer = Platform::CreateTimer();
|
||||
autosaveTimer->onTimeout = std::bind(&SolveSpaceUI::Autosave, &SS);
|
||||
|
@ -302,12 +298,26 @@ void SolveSpaceUI::Exit() {
|
|||
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() {
|
||||
generateAllTimer->RunAfterProcessingEvents();
|
||||
scheduledGenerateAll = true;
|
||||
refreshTimer->RunAfterProcessingEvents();
|
||||
}
|
||||
|
||||
void SolveSpaceUI::ScheduleShowTW() {
|
||||
showTWTimer->RunAfterProcessingEvents();
|
||||
scheduledShowTW = true;
|
||||
refreshTimer->RunAfterProcessingEvents();
|
||||
}
|
||||
|
||||
void SolveSpaceUI::ScheduleAutosave() {
|
||||
|
@ -550,12 +560,18 @@ bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
|
|||
|
||||
if(saveAs || saveFile.IsEmpty()) {
|
||||
Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window);
|
||||
// FIXME(emscripten):
|
||||
dbp("Calling AddFilter()...");
|
||||
dialog->AddFilter(C_("file-type", "SolveSpace models"), { SKETCH_EXT });
|
||||
dbp("Calling ThawChoices()...");
|
||||
dialog->ThawChoices(settings, "Sketch");
|
||||
if(!newSaveFile.IsEmpty()) {
|
||||
dbp("Calling SetFilename()...");
|
||||
dialog->SetFilename(newSaveFile);
|
||||
}
|
||||
dbp("Calling RunModal()...");
|
||||
if(dialog->RunModal()) {
|
||||
dbp("Calling FreezeChoices()...");
|
||||
dialog->FreezeChoices(settings, "Sketch");
|
||||
newSaveFile = dialog->GetFilename();
|
||||
} else {
|
||||
|
@ -568,6 +584,9 @@ bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
|
|||
RemoveAutosave();
|
||||
saveFile = newSaveFile;
|
||||
unsaved = false;
|
||||
if (this->OnSaveFinished) {
|
||||
this->OnSaveFinished(newSaveFile, saveAs, false);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -579,7 +598,11 @@ void SolveSpaceUI::Autosave()
|
|||
ScheduleAutosave();
|
||||
|
||||
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()) {
|
||||
dialog->FreezeChoices(settings, "ExportImage");
|
||||
SS.ExportAsPngTo(dialog->GetFilename());
|
||||
if (SS.OnSaveFinished) {
|
||||
SS.OnSaveFinished(dialog->GetFilename(), false, false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -704,6 +730,9 @@ void SolveSpaceUI::MenuFile(Command id) {
|
|||
}
|
||||
|
||||
SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false);
|
||||
if (SS.OnSaveFinished) {
|
||||
SS.OnSaveFinished(dialog->GetFilename(), false, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -716,6 +745,9 @@ void SolveSpaceUI::MenuFile(Command id) {
|
|||
dialog->FreezeChoices(settings, "ExportWireframe");
|
||||
|
||||
SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true);
|
||||
if (SS.OnSaveFinished) {
|
||||
SS.OnSaveFinished(dialog->GetFilename(), false, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -728,6 +760,9 @@ void SolveSpaceUI::MenuFile(Command id) {
|
|||
dialog->FreezeChoices(settings, "ExportSection");
|
||||
|
||||
SS.ExportSectionTo(dialog->GetFilename());
|
||||
if (SS.OnSaveFinished) {
|
||||
SS.OnSaveFinished(dialog->GetFilename(), false, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -740,6 +775,10 @@ void SolveSpaceUI::MenuFile(Command id) {
|
|||
dialog->FreezeChoices(settings, "ExportMesh");
|
||||
|
||||
SS.ExportMeshTo(dialog->GetFilename());
|
||||
if (SS.OnSaveFinished) {
|
||||
SS.OnSaveFinished(dialog->GetFilename(), false, false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -753,6 +792,9 @@ void SolveSpaceUI::MenuFile(Command id) {
|
|||
|
||||
StepFileWriter sfw = {};
|
||||
sfw.ExportSurfacesTo(dialog->GetFilename());
|
||||
if (SS.OnSaveFinished) {
|
||||
SS.OnSaveFinished(dialog->GetFilename(), false, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -683,6 +683,7 @@ public:
|
|||
void NewFile();
|
||||
bool SaveToFile(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);
|
||||
void UpgradeLegacyData();
|
||||
bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le,
|
||||
|
@ -793,9 +794,11 @@ public:
|
|||
// the sketch!
|
||||
bool allConsistent;
|
||||
|
||||
Platform::TimerRef showTWTimer;
|
||||
Platform::TimerRef generateAllTimer;
|
||||
bool scheduledGenerateAll;
|
||||
bool scheduledShowTW;
|
||||
Platform::TimerRef refreshTimer;
|
||||
Platform::TimerRef autosaveTimer;
|
||||
void Refresh();
|
||||
void ScheduleShowTW();
|
||||
void ScheduleGenerateAll();
|
||||
void ScheduleAutosave();
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -489,608 +489,4 @@ void SSurface::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();
|
||||
}
|
||||
|
|
3
src/ui.h
3
src/ui.h
|
@ -622,6 +622,7 @@ public:
|
|||
void HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin,
|
||||
double *wmin, bool usePerspective,
|
||||
const Camera &camera);
|
||||
void ZoomToMouse(double delta);
|
||||
void LoopOverPoints(const std::vector<Entity *> &entities,
|
||||
const std::vector<Constraint *> &constraints,
|
||||
const std::vector<hEntity> &faces,
|
||||
|
@ -842,7 +843,7 @@ public:
|
|||
void MouseLeftDoubleClick(double x, double y);
|
||||
void MouseMiddleOrRightDown(double x, double y);
|
||||
void MouseRightUp(double x, double y);
|
||||
void MouseScroll(double x, double y, double delta);
|
||||
void MouseScroll(double delta);
|
||||
void MouseLeave();
|
||||
bool KeyboardEvent(Platform::KeyboardEvent event);
|
||||
void EditControlDone(const std::string &s);
|
||||
|
|
|
@ -76,6 +76,9 @@ target_link_libraries(solvespace-testsuite
|
|||
solvespace-headless
|
||||
${COVERAGE_LIBRARY})
|
||||
|
||||
target_include_directories(solvespace-testsuite
|
||||
PRIVATE
|
||||
${EIGEN3_INCLUDE_DIRS})
|
||||
add_dependencies(solvespace-testsuite
|
||||
resources)
|
||||
|
||||
|
|
Loading…
Reference in New Issue