Merge branch 'master' into python

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

View File

@ -49,4 +49,4 @@ else
if [ $(uname -m) = "$2" ]; then
make -j$(sysctl -n hw.logicalcpu) test_solvespace
fi
fi
fi

View File

@ -72,12 +72,12 @@ do
echo "Checking progress..."
progress=$(xcrun altool --notarization-info "${notarize_uuid}" -u "${MACOS_APPSTORE_USERNAME}" -p "${MACOS_APPSTORE_APP_PASSWORD}" 2>&1)
# echo "${progress}"
if [ $? -ne 0 ] || [[ "${progress}" =~ "Invalid" ]] ; then
echo "Error with notarization. Exiting"
break
fi
if [[ "${progress}" =~ "success" ]]; then
success=1
break
@ -88,4 +88,4 @@ do
done
# staple
xcrun stapler staple "${dmg}"
xcrun stapler staple "${dmg}"

View File

@ -69,7 +69,7 @@ jobs:
with:
name: windows
path: build/bin/RelWithDebInfo/solvespace.exe
build_release_windows_openmp:
needs: [test_ubuntu, test_windows, test_macos]
name: Build Release Windows (OpenMP)
@ -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]

View File

@ -21,7 +21,12 @@ 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}"

View File

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

View File

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

View File

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

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

View File

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

75
cmake/FindCairo.cmake Normal file
View File

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

View File

@ -0,0 +1,20 @@
set(EMSCRIPTEN 1)
set(CMAKE_C_OUTPUT_EXTENSION ".o")
set(CMAKE_CXX_OUTPUT_EXTENSION ".o")
set(CMAKE_EXECUTABLE_SUFFIX ".html")
set(CMAKE_SIZEOF_VOID_P 4)
set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE)
# FIXME(emscripten): Suppress non-c-typedef-for-linkage warnings in solvespace.h
add_compile_options(-Wno-non-c-typedef-for-linkage)
# Enable optimization. Workaround for "too many locals" error when runs on browser.
if(CMAKE_BUILD_TYPE STREQUAL Release)
add_compile_options(-O2)
else()
add_compile_options(-O1)
endif()

View File

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

View File

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

View File

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

View File

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

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

View File

@ -1,69 +1,97 @@
{
"$schema": "https://raw.githubusercontent.com/TingPing/flatpak-manifest-schema/master/flatpak-manifest.schema",
"app-id": "com.solvespace.SolveSpace",
"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*"
]
}
]

View File

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

View File

@ -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)
@ -111,7 +129,8 @@ endif()
function(add_resources)
foreach(name ${ARGN})
add_resource(${name})
set(resource_list "${resource_list}" PARENT_SCOPE)
set(resource_list "${resource_list}" PARENT_SCOPE)
set(resource_names "${resource_names}" PARENT_SCOPE)
endforeach()
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()

View File

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

View File

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

View File

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

View File

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

View File

@ -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,日本語

2281
res/locales/cs_CZ.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,15 @@ msgstr ""
"Project-Id-Version: SolveSpace 3.0\n"
"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

2138
res/locales/ja_JP.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: SolveSpace 3.0\n"
"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"

View File

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

View File

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

View File

@ -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;
@ -167,10 +168,11 @@ void TextWindow::DescribeSelection() {
case Entity::Type::CIRCLE: {
Printf(false, "%FtCIRCLE%E");
p = SK.GetEntity(e->point[0])->PointGetNum();
Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p));
Printf(true, " center = " PT_AS_STR, COSTR(SK.GetEntity(e->point[0]), p));
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(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,7 +180,9 @@ void TextWindow::DescribeSelection() {
case Entity::Type::FACE_N_ROT_TRANS:
case Entity::Type::FACE_N_ROT_AA:
case Entity::Type::FACE_N_TRANS:
Printf(false, "%FtPLANE FACE%E");
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));
p = e->FaceGetPointNum();
@ -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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1464
src/platform/guihtml.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -372,7 +372,8 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
double rotationGestureCurrent;
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 {

View File

@ -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 += nonClientWidth;
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,27 +1177,27 @@ public:
}
void GetContentSize(double *width, double *height) override {
int pixelRatio = GetDevicePixelRatio();
double pixelRatio = GetDevicePixelRatio();
RECT rc;
sscheck(GetClientRect(hWindow, &rc));
*width = (rc.right - rc.left) / pixelRatio;
*height = (rc.bottom - rc.top) / pixelRatio;
*width = (rc.right - rc.left) / pixelRatio;
*height = (rc.bottom - rc.top) / pixelRatio;
}
void SetMinContentSize(double width, double height) {
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -622,6 +622,7 @@ public:
void HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin,
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);

View File

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