diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index d6471a38..00000000 --- a/.browserslistrc +++ /dev/null @@ -1,2 +0,0 @@ -> 1% -last 2 versions diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..05082f85 --- /dev/null +++ b/.clang-format @@ -0,0 +1,8 @@ +# https://github.com/microsoft/vscode-cpptools/issues/932 +NamespaceIndentation: None +IndentWidth: 4 +AccessModifierOffset: -4 +PointerAlignment: Left + +# https://stackoverflow.com/questions/29477654/how-to-make-clang-format-add-new-line-before-opening-brace-of-a-function +BreakBeforeBraces: Stroustrup diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 761b3cdb..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - root: true, - env: { - node: true - }, - extends: [ - "plugin:vue/essential", - "eslint:recommended", - "@vue/prettier", - "@vue/typescript" - ], - rules: { - // "no-console": process.env.NODE_ENV === "production" ? "error" : "off", - // "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", - "no-console": "off", - "no-debugger": "off", - indent: "off", - allowIndentationTabs: true - }, - parserOptions: { - parser: "@typescript-eslint/parser", - ecmaVersion: 6 - } -}; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1c84865..91e214de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,45 +1,420 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Update and build application +name: Build on: push: - branches: [ master ] + branches: [main, master] pull_request: - branches: [ master ] + branches: [main, master] + workflow_dispatch: + inputs: + sentry_crash_test: + description: "After building, run --sentry-crash-test to send a real crash to Sentry (verifies symbolication)" + type: boolean + default: false jobs: - build: + # Single source of truth for the build tag used in artifact names. + # tag = v-, where the short hash is the + # SAME `git rev-parse --short HEAD` the app embeds (TEXTURELAB_BUILD_HASH), so + # a build file name matches the version shown in-app / in screenshots. + version: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.v.outputs.tag }} + steps: + - uses: actions/checkout@v4 + - name: Compute build tag + id: v + run: | + # tag = v-, where is the numeric + # project() version + optional suffix — the same string the app embeds + # as TEXTURELAB_VERSION, so file names match the in-app version. + NUM=$(grep -oP 'project\(texturelab VERSION \K[0-9.]+' src/texturelab/CMakeLists.txt) + SUFFIX=$(grep -oP 'set\(TEXTURELAB_VERSION_SUFFIX "\K[^"]*' src/texturelab/CMakeLists.txt) + echo "tag=v${NUM}${SUFFIX}-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + # Fails the build if inline widget stylesheets or hardcoded QColor literals + # creep into the UI/rendering code (they'd bypass the theme system and break + # --dev-theme hot-reload). See scripts/check-theme-hygiene.sh + UI_DESIGN_SYSTEM_PRD.md. + theme-hygiene: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check theme hygiene + run: bash scripts/check-theme-hygiene.sh - strategy: - matrix: - node-version: [12.x, 14.x] + build-linux: + needs: version + # runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - - name: Checkout source code - uses: actions/checkout@master - - - name: Setup cache - id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" - - - uses: actions/cache@master - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@master - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies and build - run: | - yarn install - yarn electron:build --publish=never + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + fetch-tags: true + + - name: Install Qt6 + uses: jurplel/install-qt-action@v4 + with: + version: "6.7.3" + modules: "qtshadertools" + dir: "${{ github.workspace }}/Qt" + cache: true + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + cmake \ + libgl1-mesa-dev \ + libxkbcommon-dev \ + libvulkan-dev \ + libxcb-cursor0 \ + libxcb-shape0-dev \ + libcurl4-openssl-dev + + - name: Configure CMake + run: cmake -B build -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DSENTRY_BACKEND=crashpad -DTEXTURELAB_SENTRY_DSN="${{ secrets.SENTRY_DSN }}" + + - name: Build + run: cmake --build build --target texturelab --parallel $(nproc) + + - name: Upload debug symbols to Sentry + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + run: | + curl -sL https://sentry.io/get-cli/ | bash + # Upload the FULL, unstripped binary. It contains debug info AND the + # .eh_frame unwind info (CFI) that Sentry needs to walk a minidump's + # stack. objcopy --only-keep-debug drops the unwind info, which left + # crashes unsymbolicated ("debug information files are missing"). + sentry-cli debug-files upload --include-sources \ + build/src/texturelab/texturelab + # Strip the shipped copy to shrink the AppImage. The GNU build-id + # (== Sentry Debug ID) survives stripping, so the DIF we just + # uploaded still matches the binary that ships and crashes. + strip build/src/texturelab/texturelab + + - name: Sentry crash test (manual) + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.sentry_crash_test == 'true' }} + run: | + export QT_QPA_PLATFORM=offscreen + # Crashes before the main window opens; crashpad_handler (copied next to + # the binary at build time) uploads the minidump via the built-in DSN. + build/src/texturelab/texturelab --sentry-crash-test || true + echo "waiting for crashpad to upload the minidump..." + sleep 25 + + - name: Install LinuxDeploy + uses: miurahr/install-linuxdeploy-action@v1 + with: + plugins: qt appimage + + - name: Create AppImage + env: + APPIMAGE_EXTRACT_AND_RUN: 1 + DEPLOY_STDCXX: 1 + run: | + export QMAKE=${{ github.workspace }}/Qt/Qt/6.7.3/gcc_64/bin/qmake + export PATH=${{ github.workspace }}/Qt/Qt/6.7.3/gcc_64/bin:$PATH + export LD_LIBRARY_PATH=${{ github.workspace }}/Qt/Qt/6.7.3/gcc_64/lib:$LD_LIBRARY_PATH + + # linuxdeploy's qt plugin deploys *every* sqldriver plugin it finds and + # hard-fails when one has an unresolvable dependency. Qt ships + # libqsqlmimer.so, whose libmimerapi.so is a proprietary Mimer SQL + # client that isn't on the runner ("ERROR: Could not find dependency: + # libmimerapi.so"). We only ever open QSQLITE (src/catalog/database.cpp), + # so keep libqsqlite.so and drop the rest — that also stops us shipping + # the GPL libmysqlclient the MySQL driver drags in. + QT_SQLDRIVERS=${{ github.workspace }}/Qt/Qt/6.7.3/gcc_64/plugins/sqldrivers + find "$QT_SQLDRIVERS" -name 'libqsql*.so' ! -name 'libqsqlite.so' -delete + + # Desktop entry + icon (name must match the entry's Icon= key) + cp packaging/linux/texturelab.desktop texturelab.desktop + cp src/icons/logo.png texturelab.png + + # Bundle crashpad_handler alongside the main binary + mkdir -p AppDir/usr/bin + cp build/src/texturelab/crashpad_handler AppDir/usr/bin/ || \ + cp build/_deps/sentry-build/crashpad_build/handler/crashpad_handler AppDir/usr/bin/ + + linuxdeploy-x86_64.AppImage \ + --appdir AppDir \ + --executable build/src/texturelab/texturelab \ + --desktop-file texturelab.desktop \ + --icon-file texturelab.png \ + --plugin qt \ + --output appimage + + - name: Rename Linux artifact + run: | + APP=$(ls *.AppImage | head -1) + mv "$APP" "texturelab-linux-${{ needs.version.outputs.tag }}.AppImage" + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: texturelab-linux-${{ needs.version.outputs.tag }} + path: "texturelab-linux-${{ needs.version.outputs.tag }}.AppImage" + + build-windows: + needs: version + runs-on: windows-2022 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + fetch-tags: true + + - name: Install Qt6 + uses: jurplel/install-qt-action@v4 + with: + version: "6.7.3" + arch: "win64_msvc2019_64" + modules: "qtshadertools" + dir: "${{ github.workspace }}/Qt" + cache: true + + - name: Configure CMake + run: cmake -B build -G "Visual Studio 17 2022" -A x64 -DSENTRY_BACKEND=crashpad -DTEXTURELAB_SENTRY_DSN="${{ secrets.SENTRY_DSN }}" + + - name: Build + run: cmake --build build --target texturelab --config Release --parallel + + - name: Upload debug symbols to Sentry + shell: pwsh + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + run: | + Invoke-WebRequest -Uri "https://github.com/getsentry/sentry-cli/releases/latest/download/sentry-cli-Windows-x86_64.exe" -OutFile "sentry-cli.exe" + $exe = "build\src\texturelab\Release\texturelab.exe" + $pdb = "build\src\texturelab\Release\texturelab.pdb" + if (-not (Test-Path $pdb)) { Write-Error "texturelab.pdb not found — build produced no debug info; Sentry cannot symbolicate."; exit 1 } + # Log the Debug IDs. The exe's Debug ID (from its CodeView record) MUST + # be non-null and match the pdb, or crash minidumps stay unsymbolicated. + Write-Host "== exe Debug ID =="; .\sentry-cli.exe debug-files check $exe + Write-Host "== pdb Debug ID =="; .\sentry-cli.exe debug-files check $pdb + .\sentry-cli.exe debug-files upload --include-sources "build\src\texturelab\Release\" + + - name: Deploy Qt dependencies + shell: pwsh + run: | + New-Item -ItemType Directory -Path deploy + Copy-Item "build\src\texturelab\Release\texturelab.exe" deploy\ + & "${{ github.workspace }}\Qt\Qt\6.7.3\msvc2019_64\bin\windeployqt.exe" "deploy\texturelab.exe" --release --no-translations + $handler = @( + "build\src\texturelab\Release\crashpad_handler.exe", + "build\src\texturelab\crashpad_handler.exe", + "build\_deps\sentry-build\crashpad_build\handler\Release\crashpad_handler.exe" + ) | Where-Object { Test-Path $_ } | Select-Object -First 1 + if ($handler) { Copy-Item $handler deploy\ } else { Write-Warning "crashpad_handler.exe not found, skipping" } + + - name: Sentry crash test (manual) + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.sentry_crash_test == 'true' }} + shell: pwsh + run: | + $env:QT_QPA_PLATFORM = "offscreen" + # Run the deployed bundle (Qt DLLs + crashpad_handler.exe alongside the + # exe). Crashes before the main window; crashpad uploads the minidump + # via the built-in DSN. Debug IDs match the PDB uploaded above. + $p = Start-Process -FilePath "deploy\texturelab.exe" -ArgumentList "--sentry-crash-test" -PassThru + if (-not $p.WaitForExit(60000)) { $p.Kill(); Write-Warning "timed out waiting for crash" } + else { Write-Host "app exited with code $($p.ExitCode) (crash expected)" } + Write-Host "waiting for crashpad to upload the minidump..." + Start-Sleep -Seconds 25 + + # Rename after the crash-test step above (which runs deploy\texturelab.exe) + # so the exe ships as texturelab-win-v-.exe alongside its DLLs. + - name: Rename Windows executable + shell: pwsh + run: | + Rename-Item "deploy\texturelab.exe" "texturelab-win-${{ needs.version.outputs.tag }}.exe" + + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: texturelab-win-${{ needs.version.outputs.tag }} + path: deploy/ + + build-macos: + needs: version + # Qt 6.7.x still references AGL.framework, which modern macOS SDKs (Xcode 16+) + # removed — so we pin Xcode 15 (macOS 14 SDK, AGL present) on macos-14. Build + # universal x86_64+arm64 so it runs on both Intel and Apple Silicon Macs. + runs-on: macos-14 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + fetch-tags: true + + - name: Select Xcode 15 (macOS 14 SDK still ships AGL.framework) + run: | + XC=$(ls -d /Applications/Xcode_15*.app 2>/dev/null | sort -V | tail -1) + if [ -z "$XC" ]; then echo "No Xcode 15.x found on runner"; ls -d /Applications/Xcode_*.app; exit 1; fi + echo "Using $XC" + sudo xcode-select -s "$XC/Contents/Developer" + xcodebuild -version + + - name: Install Qt6 + uses: jurplel/install-qt-action@v4 + with: + version: "6.7.3" + modules: "qtshadertools" + dir: "${{ github.workspace }}/Qt" + cache: true + + - name: Configure CMake + run: | + # Crashpad's CMake locates the Mach mig defs at + # ${CMAKE_OSX_SYSROOT}/usr/include/mach/exc.defs. If CMAKE_OSX_SYSROOT + # is empty it resolves to /usr/include/mach (absent on modern macOS) + # and configure fails. Point it explicitly at the SDK, which ships them. + SDKROOT=$(xcrun --show-sdk-path) + echo "Using SDK: $SDKROOT" + cmake -B build -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DCMAKE_OSX_SYSROOT="$SDKROOT" -DTEXTURELAB_SENTRY_DSN="${{ secrets.SENTRY_DSN }}" + + - name: Build + run: cmake --build build --target texturelab --parallel $(sysctl -n hw.ncpu) + + - name: Generate dSYM and upload to Sentry + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + run: | + dsymutil build/src/texturelab/texturelab.app/Contents/MacOS/texturelab \ + -o texturelab.dSYM + strip build/src/texturelab/texturelab.app/Contents/MacOS/texturelab + curl -sL https://sentry.io/get-cli/ | bash + sentry-cli debug-files upload --include-sources texturelab.dSYM + + - name: Sentry crash test (manual) + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.sentry_crash_test == 'true' }} + run: | + export QT_QPA_PLATFORM=offscreen + # Crashpad (crashpad_handler bundled next to the binary) uploads the + # minidump out-of-process, symbolicated by the dSYM uploaded above. + build/src/texturelab/texturelab.app/Contents/MacOS/texturelab --sentry-crash-test || true + echo "waiting for crashpad to upload the minidump..." + sleep 25 + + - name: Upload macOS artifact + uses: actions/upload-artifact@v4 + with: + name: texturelab-mac-${{ needs.version.outputs.tag }} + path: build/src/texturelab/texturelab.app + + deploy: + needs: [version, build-linux, build-windows, build-macos] + if: always() + runs-on: ubuntu-latest + env: + TAG: ${{ needs.version.outputs.tag }} + steps: + - name: Download artifacts + if: ${{ needs.build-linux.result == 'success' || needs.build-windows.result == 'success' || needs.build-macos.result == 'success' }} + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Zip and upload to S3 + if: ${{ needs.build-linux.result == 'success' || needs.build-windows.result == 'success' || needs.build-macos.result == 'success' }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: us-east-2 + run: | + # Artifacts already arrive as texturelab--/ with the + # primary file (exe/AppImage) renamed. Here we just zip each bundle + # under the same texturelab--.zip name. + cd artifacts + + # Linux — single AppImage. + if [ -d "texturelab-linux-${TAG}" ]; then + zip -j "../texturelab-linux-${TAG}.zip" "texturelab-linux-${TAG}"/*.AppImage + fi + + # Windows — exe + sibling Qt DLLs + crashpad_handler. + if [ -d "texturelab-win-${TAG}" ]; then + (cd "texturelab-win-${TAG}" && zip -r "../../texturelab-win-${TAG}.zip" .) + fi + + # macOS — re-wrap the uploaded bundle contents as a proper .app, zip it. + if [ -d "texturelab-mac-${TAG}" ]; then + mkdir -p "wrap/texturelab.app" + cp -R "texturelab-mac-${TAG}/." "wrap/texturelab.app/" + (cd wrap && zip -r "../../texturelab-mac-${TAG}.zip" texturelab.app) + rm -rf wrap + fi + + cd .. + for f in texturelab-linux-${TAG}.zip texturelab-win-${TAG}.zip texturelab-mac-${TAG}.zip; do + if [ -f "$f" ]; then aws s3 cp "$f" s3://texturelab-nightlies/; fi + done + + - name: Post to Discord + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + BRANCH: ${{ github.ref_name }} + RUN_ID: ${{ github.run_id }} + REPO: ${{ github.repository }} + LINUX_RESULT: ${{ needs.build-linux.result }} + WINDOWS_RESULT: ${{ needs.build-windows.result }} + MACOS_RESULT: ${{ needs.build-macos.result }} + run: | + RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID" + S3_BASE="https://texturelab-nightlies.s3.us-east-2.amazonaws.com" + + status_emoji() { + case "$1" in + success) echo "✅" ;; + failure) echo "❌" ;; + cancelled) echo "⏹️" ;; + *) echo "⚠️" ;; + esac + } + + download_line() { + local result="$1" label="$2" url="$3" + if [[ "$result" == "success" ]]; then + echo "$(status_emoji "$result") **$label** — [Download]($url)" + else + echo "$(status_emoji "$result") **$label** — build $result" + fi + } + + if [[ "$LINUX_RESULT" == "success" && "$WINDOWS_RESULT" == "success" && "$MACOS_RESULT" == "success" ]]; then + TITLE="Build succeeded — $BRANCH @ $TAG" + COLOR=3066993 + else + TITLE="Build failed — $BRANCH @ $TAG" + COLOR=15158332 + fi + + DESCRIPTION="$(download_line "$LINUX_RESULT" "Linux" "$S3_BASE/texturelab-linux-$TAG.zip")\n$(download_line "$WINDOWS_RESULT" "Windows" "$S3_BASE/texturelab-win-$TAG.zip")\n$(download_line "$MACOS_RESULT" "macOS" "$S3_BASE/texturelab-mac-$TAG.zip")\n\n[View run]($RUN_URL)" + + curl -s -X POST "$DISCORD_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "{ + \"embeds\": [{ + \"title\": \"$TITLE\", + \"description\": \"$DESCRIPTION\", + \"color\": $COLOR, + \"url\": \"$RUN_URL\" + }] + }" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..fefd94b2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,406 @@ +name: Release + +# Official release builds. Pushing a tag like v0.4.0 (or v0.4.0-beta) builds all +# three platforms, verifies and packages them, and creates a DRAFT GitHub +# Release with the files attached. Review the draft, then publish it and upload +# the same files to itch.io etc. Nightly/CI builds stay in build.yml. +# +# Optional signing: each platform signs only when its secrets exist, otherwise +# it ships unsigned (Gatekeeper / SmartScreen warnings). +# macOS: APPLE_CERT_P12 (base64 Developer ID Application .p12), +# APPLE_CERT_PASSWORD, and for notarization APPLE_ID, APPLE_TEAM_ID, +# APPLE_APP_PASSWORD (app-specific password) +# Windows: Azure Artifact Signing (formerly Trusted Signing) — AZURE_TENANT_ID, AZURE_CLIENT_ID, +# AZURE_CLIENT_SECRET, AZURE_SIGNING_ENDPOINT, +# AZURE_SIGNING_ACCOUNT, AZURE_SIGNING_PROFILE + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Existing tag to (re)build, e.g. v0.4.0" + required: true + +permissions: + contents: read + +# Never run two releases of the same tag at once. +concurrency: + group: release-${{ github.event.inputs.tag || github.ref_name }} + cancel-in-progress: false + +env: + QT_VERSION: "6.7.3" + TAG: ${{ github.event.inputs.tag || github.ref_name }} + +jobs: + # Refuses to build unless the tag, the CMake version and the release config + # all agree, so a mistagged or misconfigured build never gets packaged. + preflight: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.v.outputs.tag }} + prerelease: ${{ steps.v.outputs.prerelease }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG }} + + - name: Check tag matches the app version + id: v + run: | + NUM=$(grep -oP 'project\(texturelab VERSION \K[0-9.]+' src/texturelab/CMakeLists.txt) + SUFFIX=$(grep -oP 'set\(TEXTURELAB_VERSION_SUFFIX "\K[^"]*' src/texturelab/CMakeLists.txt) + EXPECTED="v${NUM}${SUFFIX}" + if [ "$TAG" != "$EXPECTED" ]; then + echo "::error::Tag '$TAG' does not match the app version '$EXPECTED' (project() VERSION + TEXTURELAB_VERSION_SUFFIX in src/texturelab/CMakeLists.txt)." + exit 1 + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + # Any suffix (-beta, -rc1, ...) marks the GitHub Release as a prerelease. + if [ -n "$SUFFIX" ]; then echo "prerelease=true"; else echo "prerelease=false"; fi >> "$GITHUB_OUTPUT" + + - name: Check update checker points at production + run: | + if ! grep -qE '^constexpr const char\* kApiBase = "https://texturelab.io";' src/texturelab/update/updatechecker.cpp; then + echo "::error::kApiBase in src/texturelab/update/updatechecker.cpp is not https://texturelab.io" + exit 1 + fi + + - name: Check release secrets are set + env: + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + run: | + # A release without crash reporting or uploaded symbols is worse than + # no release, so these are required (unlike in nightly builds). + [ -n "$SENTRY_DSN" ] || { echo "::error::SENTRY_DSN secret is not set"; exit 1; } + [ -n "$SENTRY_AUTH_TOKEN" ] || { echo "::error::SENTRY_AUTH_TOKEN secret is not set"; exit 1; } + + - name: Check theme hygiene + run: bash scripts/check-theme-hygiene.sh + + build-linux: + needs: preflight + # Oldest supported runner: the AppImage only runs on distros with a glibc at + # least as new as the one it was built against. + runs-on: ubuntu-22.04 + env: + TAG: ${{ needs.preflight.outputs.tag }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG }} + submodules: recursive + fetch-depth: 0 + + - name: Install Qt6 + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + modules: "qtshadertools" + dir: "${{ github.workspace }}/Qt" + cache: true + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake libgl1-mesa-dev \ + libxkbcommon-dev libvulkan-dev libxcb-cursor0 libxcb-shape0-dev \ + libcurl4-openssl-dev xvfb + + - name: Configure + run: cmake -B build -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DSENTRY_BACKEND=crashpad -DTEXTURELAB_SENTRY_DSN="${{ secrets.SENTRY_DSN }}" + + - name: Build + run: cmake --build build --target texturelab --parallel $(nproc) + + - name: Upload debug symbols to Sentry and strip + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + run: | + set -e + curl -sL https://sentry.io/get-cli/ | bash + # Upload the full unstripped binary (debug info + .eh_frame CFI); see build.yml. + sentry-cli debug-files upload --include-sources build/src/texturelab/texturelab + # Keep an unstripped copy for offline symbolication. + mkdir -p symbols && cp build/src/texturelab/texturelab symbols/ + # The GNU build-id survives stripping, so the uploaded symbols still match. + strip build/src/texturelab/texturelab + + - name: Install LinuxDeploy + uses: miurahr/install-linuxdeploy-action@v1 + with: + plugins: qt appimage + + - name: Create AppImage + env: + QTDIR: ${{ github.workspace }}/Qt/Qt/${{ env.QT_VERSION }}/gcc_64 + run: scripts/package-linux.sh "$TAG" + + - name: Smoke test AppImage + env: + APPIMAGE_EXTRACT_AND_RUN: 1 + run: | + OUT=$(xvfb-run -a "dist/texturelab-linux-${TAG}-x86_64.AppImage" --version) + echo "$OUT" + echo "$OUT" | grep -q "texturelab ${TAG#v}+" || { echo "::error::unexpected --version output"; exit 1; } + + - uses: actions/upload-artifact@v4 + with: + name: release-linux + path: dist/ + if-no-files-found: error + + - uses: actions/upload-artifact@v4 + with: + name: symbols-linux-${{ env.TAG }} + path: symbols/ + retention-days: 90 + + build-windows: + needs: preflight + runs-on: windows-2022 + env: + TAG: ${{ needs.preflight.outputs.tag }} + HAS_WIN_SIGNING: ${{ secrets.AZURE_CLIENT_ID != '' }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG }} + submodules: recursive + fetch-depth: 0 + + - name: Install Qt6 + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + arch: "win64_msvc2019_64" + modules: "qtshadertools" + dir: "${{ github.workspace }}/Qt" + cache: true + + - name: Configure + run: cmake -B build -G "Visual Studio 17 2022" -A x64 -DSENTRY_BACKEND=crashpad -DTEXTURELAB_SENTRY_DSN="${{ secrets.SENTRY_DSN }}" + + - name: Build + run: cmake --build build --target texturelab --config Release --parallel + + - name: Upload debug symbols to Sentry + shell: pwsh + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + run: | + $ErrorActionPreference = "Stop" + Invoke-WebRequest -Uri "https://github.com/getsentry/sentry-cli/releases/latest/download/sentry-cli-Windows-x86_64.exe" -OutFile "sentry-cli.exe" + $out = "build\src\texturelab\Release" + if (-not (Test-Path "$out\texturelab.pdb")) { Write-Error "texturelab.pdb not found; Sentry cannot symbolicate." } + .\sentry-cli.exe debug-files check "$out\texturelab.exe" + .\sentry-cli.exe debug-files upload --include-sources "$out\" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + New-Item -ItemType Directory -Force symbols | Out-Null + Copy-Item "$out\texturelab.exe", "$out\texturelab.pdb" symbols\ + + - name: Deploy Qt dependencies + shell: pwsh + env: + QTDIR: ${{ github.workspace }}\Qt\Qt\${{ env.QT_VERSION }}\msvc2019_64 + run: scripts/package-windows.ps1 -Tag $env:TAG -Stage Deploy + + # Authenticode signing doesn't touch the CodeView record, so the Debug IDs + # uploaded to Sentry above still match the signed binaries. + - name: Sign executables + if: env.HAS_WIN_SIGNING == 'true' + uses: azure/artifact-signing-action@v2 + env: + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + with: + endpoint: ${{ secrets.AZURE_SIGNING_ENDPOINT }} + signing-account-name: ${{ secrets.AZURE_SIGNING_ACCOUNT }} + certificate-profile-name: ${{ secrets.AZURE_SIGNING_PROFILE }} + files: | + ${{ github.workspace }}\dist\texturelab-win-${{ env.TAG }}\texturelab.exe + ${{ github.workspace }}\dist\texturelab-win-${{ env.TAG }}\crashpad_handler.exe + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + + - name: Smoke test + shell: pwsh + run: | + # GUI-subsystem exe: redirect stdout to a file to read the --version line. + $p = Start-Process "dist\texturelab-win-$env:TAG\texturelab.exe" -ArgumentList "--version" ` + -RedirectStandardOutput version.txt -PassThru -Wait + Get-Content version.txt + if ($p.ExitCode -ne 0) { Write-Error "texturelab.exe --version exited with $($p.ExitCode)" } + + - name: Zip + shell: pwsh + run: scripts/package-windows.ps1 -Tag $env:TAG -Stage Zip + + - uses: actions/upload-artifact@v4 + with: + name: release-windows + path: dist/*.zip + if-no-files-found: error + + - uses: actions/upload-artifact@v4 + with: + name: symbols-windows-${{ env.TAG }} + path: symbols/ + retention-days: 90 + + build-macos: + needs: preflight + # Xcode 15 on macos-14: Qt 6.7 still needs AGL.framework, gone from newer SDKs. + runs-on: macos-14 + env: + TAG: ${{ needs.preflight.outputs.tag }} + HAS_MAC_SIGNING: ${{ secrets.APPLE_CERT_P12 != '' }} + HAS_MAC_NOTARIZE: ${{ secrets.APPLE_CERT_P12 != '' && secrets.APPLE_ID != '' }} + APP: build/src/texturelab/texturelab.app + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ env.TAG }} + submodules: recursive + fetch-depth: 0 + + - name: Select Xcode 15 + run: | + XC=$(ls -d /Applications/Xcode_15*.app 2>/dev/null | sort -V | tail -1) + [ -n "$XC" ] || { echo "No Xcode 15.x on runner"; ls -d /Applications/Xcode_*.app; exit 1; } + sudo xcode-select -s "$XC/Contents/Developer" + xcodebuild -version + + - name: Install Qt6 + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + modules: "qtshadertools" + dir: "${{ github.workspace }}/Qt" + cache: true + + - name: Configure + run: | + # Crashpad needs CMAKE_OSX_SYSROOT set to find the Mach .defs; see build.yml. + cmake -B build -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" \ + -DCMAKE_OSX_SYSROOT="$(xcrun --show-sdk-path)" \ + -DTEXTURELAB_SENTRY_DSN="${{ secrets.SENTRY_DSN }}" + + - name: Build + run: cmake --build build --target texturelab --parallel $(sysctl -n hw.ncpu) + + - name: Generate dSYM, upload to Sentry, strip + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + run: | + set -e + mkdir -p symbols + dsymutil "$APP/Contents/MacOS/texturelab" -o symbols/texturelab.dSYM + strip "$APP/Contents/MacOS/texturelab" + curl -sL https://sentry.io/get-cli/ | bash + sentry-cli debug-files upload --include-sources symbols/texturelab.dSYM + + - name: Import signing certificate + if: env.HAS_MAC_SIGNING == 'true' + env: + APPLE_CERT_P12: ${{ secrets.APPLE_CERT_P12 }} + APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} + run: | + KC="$RUNNER_TEMP/signing.keychain-db" + KC_PW=$(uuidgen) + security create-keychain -p "$KC_PW" "$KC" + security set-keychain-settings -lut 21600 "$KC" + security unlock-keychain -p "$KC_PW" "$KC" + echo "$APPLE_CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12" + security import "$RUNNER_TEMP/cert.p12" -P "$APPLE_CERT_PASSWORD" -A -t cert -f pkcs12 -k "$KC" + rm "$RUNNER_TEMP/cert.p12" + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PW" "$KC" + security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') + ID=$(security find-identity -v -p codesigning "$KC" | grep -o '"Developer ID Application:[^"]*"' | head -1 | tr -d '"') + [ -n "$ID" ] || { echo "::error::No 'Developer ID Application' identity in APPLE_CERT_P12"; exit 1; } + echo "MAC_SIGN_ID=$ID" >> "$GITHUB_ENV" + + - name: Bundle Qt, sign and create DMG + env: + QTDIR: ${{ github.workspace }}/Qt/Qt/${{ env.QT_VERSION }}/macos + run: scripts/package-macos.sh "$TAG" + + - name: Smoke test + run: | + OUT=$("$APP/Contents/MacOS/texturelab" --version) + echo "$OUT" + echo "$OUT" | grep -q "texturelab ${TAG#v}+" || { echo "::error::unexpected --version output"; exit 1; } + + - name: Notarize and staple + if: env.HAS_MAC_NOTARIZE == 'true' + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} + run: | + DMG="dist/texturelab-mac-${TAG}-universal.dmg" + xcrun notarytool submit "$DMG" --apple-id "$APPLE_ID" --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_APP_PASSWORD" --wait --timeout 30m + xcrun stapler staple "$DMG" + xcrun stapler validate "$DMG" + + - uses: actions/upload-artifact@v4 + with: + name: release-macos + path: dist/*.dmg + if-no-files-found: error + + - uses: actions/upload-artifact@v4 + with: + name: symbols-macos-${{ env.TAG }} + path: symbols/ + retention-days: 90 + + # Runs only when all three platforms succeeded: no partial releases. + draft-release: + needs: [preflight, build-linux, build-windows, build-macos] + runs-on: ubuntu-latest + permissions: + contents: write + env: + TAG: ${{ needs.preflight.outputs.tag }} + PRERELEASE: ${{ needs.preflight.outputs.prerelease }} + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + steps: + - uses: actions/download-artifact@v4 + with: + pattern: release-* + merge-multiple: true + path: dist + + - name: Checksums + run: | + cd dist + sha256sum * > SHA256SUMS.txt + cat SHA256SUMS.txt + + - name: Create or update draft release + run: | + if gh release view "$TAG" >/dev/null 2>&1; then + # Re-run: refresh the assets on the existing release, keep its notes. + gh release upload "$TAG" dist/* --clobber + else + FLAGS=(--draft --verify-tag --generate-notes --title "TextureLab ${TAG}") + [ "$PRERELEASE" = "true" ] && FLAGS+=(--prerelease) + gh release create "$TAG" dist/* "${FLAGS[@]}" + fi + gh release view "$TAG" --json url,isDraft,assets --jq '{url, isDraft, assets: [.assets[].name]}' diff --git a/.gitignore b/.gitignore index 025b65c7..06a5cb00 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,17 @@ -.DS_Store -node_modules -/dist +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps -# local env files -.env.local -.env.*.local +build/ +build-sentry-test/ -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -#Electron-builder output -/dist_electron -/build/icons \ No newline at end of file +# Secrets — Sentry auth token, org/project slugs +.env \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index ee94bc5e..eb6f2ac9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "public/assets"] path = public/assets url = https://github.com/njbrown/texturelabdata.git +[submodule "src/ads"] + path = src/ads + url = https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System.git diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index c9590876..00000000 --- a/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "useTabs": true -} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..c6c7c054 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.10) + +project(qtcompleteapp VERSION 0.1 LANGUAGES CXX) + +# sentry-native (Crashpad backend for out-of-process crash capture) +include(FetchContent) +FetchContent_Declare( + sentry + GIT_REPOSITORY https://github.com/getsentry/sentry-native.git + GIT_TAG 0.7.20 +) +set(SENTRY_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +if(WIN32) + set(SENTRY_BACKEND "crashpad" CACHE STRING "" FORCE) + set(SENTRY_TRANSPORT "winhttp" CACHE STRING "" FORCE) +elseif(APPLE) + # Crashpad: out-of-process minidumps with full thread stacks (symbolicated via + # the uploaded dSYM), same as Win/Linux. The inproc backend delivered crashes + # but with no stack frames on Apple Silicon, so reports were unsymbolicatable. + set(SENTRY_BACKEND "crashpad" CACHE STRING "" FORCE) + set(SENTRY_TRANSPORT "curl" CACHE STRING "" FORCE) +else() + set(SENTRY_BACKEND "crashpad" CACHE STRING "" FORCE) + set(SENTRY_TRANSPORT "curl" CACHE STRING "" FORCE) +endif() +FetchContent_MakeAvailable(sentry) + +# Find Qt6 with GuiPrivate before adding subdirectories that need it +find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED) + +# GuiPrivate is needed for ADS on Qt6 (for qpa/qplatformnativeinterface.h) +if(Qt6_VERSION VERSION_GREATER_EQUAL "6.0.0") + find_package(Qt6 COMPONENTS GuiPrivate QUIET) +endif() + +# Advanced Docking System (build as static library) +set(BUILD_STATIC ON CACHE BOOL "Build ADS as static library" FORCE) +set(BUILD_EXAMPLES OFF CACHE BOOL "Don't build ADS examples" FORCE) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/ads) +# set_target_properties(qtadvanceddocking-qt6 PROPERTIES BUILD_STATIC TRUE) + +# Theme / design-token system (single source of truth for colors, QSS, palette) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/theme) + +# Launcher data layer (index.db + thumbs.db). Headless, no widgets. +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/catalog) + +# Node Graph +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/nodegraph) + +# 3D Viewer +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/viewer3d) + +# 3D Viewer +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/colorpicker) + +# Main App +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/src/texturelab) + +# Unit tests (QtTest). Off in release builds; enable with -DTEXTURELAB_BUILD_TESTS=ON. +option(TEXTURELAB_BUILD_TESTS "Build the unit test suite" ON) +if(TEXTURELAB_BUILD_TESTS) + find_package(Qt6 COMPONENTS Test QUIET) + if(Qt6Test_FOUND) + enable_testing() + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/tests) + else() + message(STATUS "Qt6 Test module not found — skipping unit tests") + endif() +endif() + diff --git a/README.md b/README.md index 608398aa..71cd178b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ - -

TextureLab

@@ -17,23 +15,58 @@ ![Screenshot](https://user-images.githubusercontent.com/1708550/123368911-4ceb9f00-d542-11eb-87b5-b0fc3ea3cc3d.png) # NOTE! + Texturelab will soon be converted to a qt project. All issues will be addressed after the conversion is complete. ## Building +Prerequisites + +``` +install Qt 6 and required dependencies + +Note: Linux needs libmesa: +https://doc.qt.io/qt-6/linux.html + +sudo apt install build-essential libgl1-mesa-dev libxkbcommon-dev libvulkan-dev libcurl4-openssl-dev + +Note: libcurl4-openssl-dev is required by sentry-native (crash reporting). +Without it, CMake fails with "CURL: Required feature AsynchDNS is not found". + +``` + Building is done with `yarn`. Install it [here](https://classic.yarnpkg.com/en/docs/install) if you havent already. ``` git clone https://github.com/njbrown/texturelab.git - cd texturelab +git submodule update --init --recursive +``` + +Ensure Qt6 is added to your CMAKE_PREFIX_PATH env + +generate build files: + +``` +cmake -G "Unix Makefiles" +``` -# if you want to pull down assets (textures and node icons) -git submodule update --init +build -yarn install -yarn electron:serve ``` +make texturelab +``` + +## Windows Setup +If you dont want to or have MSVC installed, you can use g++ via MSYS2 +``` +winget install MSYS2.MSYS2 +``` +Then in MSYS2 UCRT64 terminal: +``` +pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-ninja mingw-w64-ucrt-x86_64-cmake +``` +Add `C:\msys64\ucrt64\bin` to your PATH ## Feedback diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index f57da986..00000000 --- a/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: ["@vue/app"] -}; diff --git a/gh-build.sh b/gh-build.sh new file mode 100755 index 00000000..8b79f09e --- /dev/null +++ b/gh-build.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +source .env + +# Force gh to use the keyring login (which has repo scope). gh prefers the +# GH_TOKEN / GITHUB_TOKEN env vars over keyring auth, and the token exported +# from the shell only has read:packages scope, which 403s on workflow dispatch. +unset GH_TOKEN GITHUB_TOKEN + +REPO="njbrown/texturelab" +WORKFLOW="build.yml" +BRANCH="${1:-$(git rev-parse --abbrev-ref HEAD)}" + +echo "Triggering build for branch: $BRANCH" +gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$BRANCH" + +echo "Waiting for run to start..." +sleep 3 + +RUN_ID=$(gh run list --repo "$REPO" --workflow "$WORKFLOW" --branch "$BRANCH" --limit 1 --json databaseId -q '.[0].databaseId') + +echo "Run ID: $RUN_ID" +echo "https://github.com/$REPO/actions/runs/$RUN_ID" + +if [[ "${2:-}" == "--watch" || "${1:-}" == "--watch" ]]; then + gh run watch "$RUN_ID" --repo "$REPO" +fi diff --git a/gh-release.sh b/gh-release.sh new file mode 100755 index 00000000..e10ec602 --- /dev/null +++ b/gh-release.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# Kicks off an official release build (.github/workflows/release.yml), which +# ends in a DRAFT GitHub Release you review and publish yourself. +# +# ./gh-release.sh # tag taken from the version in CMakeLists.txt +# ./gh-release.sh v0.4.0-beta # explicit tag +# ./gh-release.sh --watch # follow the run until it finishes +# +# If the tag doesn't exist on the remote yet it offers to create and push it, +# which is what starts the workflow. If it already exists, the run is dispatched +# against that tag instead (for re-running a failed release). +set -euo pipefail + +if [ -f .env ]; then source .env; fi + +# Force gh to use the keyring login (which has repo scope). gh prefers the +# GH_TOKEN / GITHUB_TOKEN env vars over keyring auth, and the token exported +# from the shell only has read:packages scope, which 403s on workflow dispatch. +unset GH_TOKEN GITHUB_TOKEN + +REPO="njbrown/texturelab" +WORKFLOW="release.yml" + +WATCH=false +TAG="" +for arg in "$@"; do + case "$arg" in + --watch) WATCH=true ;; + *) TAG="$arg" ;; + esac +done + +# Default tag = the version the app will report, which is what release.yml's +# preflight job insists the tag matches. +if [ -z "$TAG" ]; then + NUM=$(grep -oP 'project\(texturelab VERSION \K[0-9.]+' src/texturelab/CMakeLists.txt) + SUFFIX=$(grep -oP 'set\(TEXTURELAB_VERSION_SUFFIX "\K[^"]*' src/texturelab/CMakeLists.txt) + TAG="v${NUM}${SUFFIX}" +fi + +echo "Release tag: $TAG" + +if git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then + echo "Tag already on origin — dispatching a re-run against it." + gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$TAG" -f tag="$TAG" +else + BRANCH=$(git rev-parse --abbrev-ref HEAD) + SHA=$(git rev-parse --short HEAD) + echo + echo "Tag $TAG does not exist on origin. It would be created at $BRANCH ($SHA)" + echo "and pushed, which starts the release build." + read -r -p "Create and push $TAG? [y/N] " answer + [[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 1; } + + # Annotated tag: release.yml checks out the tag, and --verify-tag on the + # draft release requires it to exist on the remote. + git tag -a "$TAG" -m "TextureLab $TAG" + git push origin "$TAG" +fi + +echo "Waiting for run to start..." +sleep 5 + +RUN_ID=$(gh run list --repo "$REPO" --workflow "$WORKFLOW" --limit 1 --json databaseId -q '.[0].databaseId') +RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID" +echo "Run ID: $RUN_ID" +echo "$RUN_URL" + +if [ "$WATCH" = true ]; then + # Non-zero exit here means the release build failed; the draft is not created. + gh run watch "$RUN_ID" --repo "$REPO" --exit-status || { + echo "Release build failed — see $RUN_URL" + exit 1 + } + echo + echo "Draft release:" + gh release view "$TAG" --repo "$REPO" --json url,isDraft,assets \ + --jq '"\(.url)\ndraft: \(.isDraft)\nassets:\n" + (.assets | map(" " + .name) | join("\n"))' + echo + echo "Review it, then publish with:" + echo " gh release edit $TAG --repo $REPO --draft=false" +fi diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index a31425cf..00000000 --- a/jest.config.js +++ /dev/null @@ -1,27 +0,0 @@ -module.exports = { - moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"], - transform: { - "^.+\\.vue$": "vue-jest", - ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": - "jest-transform-stub", - "^.+\\.tsx?$": "ts-jest" - }, - transformIgnorePatterns: ["/node_modules/"], - moduleNameMapper: { - "^@/(.*)$": "/src/$1" - }, - snapshotSerializers: ["jest-serializer-vue"], - testMatch: [ - "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" - ], - testURL: "http://localhost/", - watchPlugins: [ - "jest-watch-typeahead/filename", - "jest-watch-typeahead/testname" - ], - globals: { - "ts-jest": { - babelConfig: true - } - } -}; diff --git a/package.json b/package.json deleted file mode 100644 index b954adc2..00000000 --- a/package.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "name": "texturelab", - "version": "0.3.4", - "displayVersion": "0.3.4", - "private": false, - "scripts": { - "serve": "vue-cli-service serve", - "build": "vue-cli-service build", - "lint": "vue-cli-service lint", - "preelectron:build": "yarn buildicons", - "electron:build": "vue-cli-service electron:build", - "electron:serve": "vue-cli-service electron:serve", - "start": "vue-cli-service electron:serve", - "postinstall": "electron-builder install-app-deps", - "postuninstall": "electron-builder install-app-deps", - "test:unit": "vue-cli-service test:unit", - "buildicons": "electron-icon-builder --input=./public/icon.png --output ./build" - }, - "main": "background.js", - "dependencies": { - "@electron/remote": "^1.2.0", - "@petamoriken/float16": "^3.4.7", - "@sentry/electron": "^3.0.0", - "@types/adm-zip": "^0.4.34", - "@types/node": "^16.9.2", - "@types/pngjs": "^6.0.1", - "@types/sharp": "^0.29.2", - "adm-zip": "^0.4.16", - "boxicons": "^2.0.5", - "core-js": "^2.6.5", - "custom-electron-titlebar": "^3.1.0", - "dedent-js": "^1.0.1", - "electron-settings": "^4.0.2", - "element-resize-detector": "^1.1.15", - "fast-png": "^5.0.4", - "jquery": "^3.5.0", - "pngjs": "^6.0.0", - "sharp": "^0.29.1", - "three": "^0.118.3", - "three-orbitcontrols-ts": "git+https://git@github.com/nicolaspanel/three-orbitcontrols-ts.git", - "typeface-open-sans": "^0.0.75", - "upng-js": "^2.1.0", - "vue": "^2.6.10", - "vue-class-component": "^7.0.2", - "vue-color": "^2.7.1", - "vue-final-modal": "^2.4.1", - "vue-golden-layout": "^1.6.0", - "vue-property-decorator": "^8.1.0", - "vue-router": "^3.0.3", - "vue-toast-notification": "^0.6.2", - "vuex": "^3.0.1" - }, - "devDependencies": { - "@sentry/webpack-plugin": "^1.16.0", - "@types/jest": "^23.1.4", - "@types/jquery": "^3.3.30", - "@types/three": "^0.103.2", - "@typescript-eslint/eslint-plugin": "^2.33.0", - "@typescript-eslint/parser": "^2.33.0", - "@vue/cli-plugin-babel": "^3.8.0", - "@vue/cli-plugin-eslint": "^3.8.0", - "@vue/cli-plugin-typescript": "^3.8.0", - "@vue/cli-plugin-unit-jest": "^3.8.0", - "@vue/cli-service": "^3.8.0", - "@vue/eslint-config-prettier": "^4.0.1", - "@vue/eslint-config-typescript": "^4.0.0", - "@vue/test-utils": "1.0.0-beta.29", - "babel-core": "7.0.0-bridge.0", - "babel-eslint": "^10.0.1", - "electron": "^13.0.0", - "electron-devtools-installer": "^3.2.0", - "electron-icon-builder": "^2.0.1", - "eslint": "^5.16.0", - "eslint-plugin-vue": "^5.0.0", - "ts-jest": "^23.0.0", - "typescript": "^3.9.5", - "vue-cli-plugin-electron-builder": "^1.4.0", - "vue-template-compiler": "^2.6.10" - } -} diff --git a/packaging/linux/texturelab.desktop b/packaging/linux/texturelab.desktop new file mode 100644 index 00000000..8dd71c4d --- /dev/null +++ b/packaging/linux/texturelab.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=TextureLab +GenericName=Procedural Texture Editor +Comment=Procedural texture authoring +Exec=texturelab +Icon=texturelab +Terminal=false +Categories=Graphics; +StartupWMClass=texturelab diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 30803047..00000000 --- a/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - plugins: { - autoprefixer: {} - } -}; diff --git a/public/assets b/public/assets index eed449f3..0aeea6c2 160000 --- a/public/assets +++ b/public/assets @@ -1 +1 @@ -Subproject commit eed449f3f9abe8f17ae354ab4cb9932272c7811b +Subproject commit 0aeea6c2358099025e1a19eeeac01f62420bc6fb diff --git a/public/css/scrollbar.css b/public/css/scrollbar.css deleted file mode 100644 index 9d3e8573..00000000 --- a/public/css/scrollbar.css +++ /dev/null @@ -1,33 +0,0 @@ -::-webkit-scrollbar { - width: 6px; - height: 6px; -} -::-webkit-scrollbar-button { - width: 0px; - height: 0px; -} -::-webkit-scrollbar-thumb { - background: #e1e1e1; - border: 0px none #ffffff; - border-radius: 50px; -} -::-webkit-scrollbar-thumb:hover { - background: #ffffff; -} -::-webkit-scrollbar-thumb:active { - background: #a7a7a7; -} -::-webkit-scrollbar-track { - background: #666666; - border: 0px none #ffffff; - border-radius: 50px; -} -::-webkit-scrollbar-track:hover { - background: #666666; -} -::-webkit-scrollbar-track:active { - background: #333333; -} -::-webkit-scrollbar-corner { - background: transparent; -} diff --git a/public/icon.png b/public/icon.png deleted file mode 100644 index 68017cde..00000000 Binary files a/public/icon.png and /dev/null differ diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 2df7731a..00000000 --- a/public/index.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - TextureLab v0.3.4 - - - - -
- - - diff --git a/resources/icons/ads/close-button-disabled.svg b/resources/icons/ads/close-button-disabled.svg new file mode 100644 index 00000000..fb0cb58c --- /dev/null +++ b/resources/icons/ads/close-button-disabled.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Jemis Mali + + + + + image/svg+xml + + + + + + diff --git a/resources/icons/ads/close-button.svg b/resources/icons/ads/close-button.svg new file mode 100644 index 00000000..6ebbf382 --- /dev/null +++ b/resources/icons/ads/close-button.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Jemis Mali + + + + + image/svg+xml + + + + + + diff --git a/resources/icons/ads/detach-button-disabled.svg b/resources/icons/ads/detach-button-disabled.svg new file mode 100644 index 00000000..b94b0c3a --- /dev/null +++ b/resources/icons/ads/detach-button-disabled.svg @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jemis Mali + + + + + image/svg+xml + + + + + + + + diff --git a/resources/icons/ads/detach-button.svg b/resources/icons/ads/detach-button.svg new file mode 100644 index 00000000..a1b6241c --- /dev/null +++ b/resources/icons/ads/detach-button.svg @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jemis Mali + + + + + image/svg+xml + + + + + + + diff --git a/resources/icons/ads/maximize-button.svg b/resources/icons/ads/maximize-button.svg new file mode 100644 index 00000000..4dc165eb --- /dev/null +++ b/resources/icons/ads/maximize-button.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Jemis Mali + + + + + image/svg+xml + + + + + + diff --git a/resources/icons/ads/minimize-button-focused.svg b/resources/icons/ads/minimize-button-focused.svg new file mode 100644 index 00000000..c473a133 --- /dev/null +++ b/resources/icons/ads/minimize-button-focused.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/icons/ads/restore-button.svg b/resources/icons/ads/restore-button.svg new file mode 100644 index 00000000..d6fd5059 --- /dev/null +++ b/resources/icons/ads/restore-button.svg @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Jemis Mali + + + + + image/svg+xml + + + + + + diff --git a/resources/icons/ads/tabs-menu-button.svg b/resources/icons/ads/tabs-menu-button.svg new file mode 100644 index 00000000..d5e2e2b4 --- /dev/null +++ b/resources/icons/ads/tabs-menu-button.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Jemis Mali + + + + + image/svg+xml + + + + + + diff --git a/resources/icons/ads/vs-pin-button-disabled.svg b/resources/icons/ads/vs-pin-button-disabled.svg new file mode 100644 index 00000000..a00d2b50 --- /dev/null +++ b/resources/icons/ads/vs-pin-button-disabled.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/icons/ads/vs-pin-button-pinned-focused.svg b/resources/icons/ads/vs-pin-button-pinned-focused.svg new file mode 100644 index 00000000..ac05edb9 --- /dev/null +++ b/resources/icons/ads/vs-pin-button-pinned-focused.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/icons/ads/vs-pin-button.svg b/resources/icons/ads/vs-pin-button.svg new file mode 100644 index 00000000..f9e36d01 --- /dev/null +++ b/resources/icons/ads/vs-pin-button.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/qss/ads.qss.in b/resources/qss/ads.qss.in new file mode 100644 index 00000000..78ccc3bf --- /dev/null +++ b/resources/qss/ads.qss.in @@ -0,0 +1,107 @@ +/* + * Dock-system (Qt Advanced Docking System) color overrides. + * + * IMPORTANT: this is APPENDED to ADS's own default stylesheet by MainWindow + * (see mainwindow.cpp), not used on its own. ADS's default carries button icons, + * layout metrics, and auto-hide geometry -- we keep all of that and only override + * colors/borders here. Rules must match or exceed the default's selector + * specificity to win; since we're appended, equal specificity is enough. + * + * ADS's default is palette()-driven, so much already follows our theme (e.g. + * highlight == accent). These overrides replace the muddy derived-palette bits + * (inactive tab text = palette(dark), active-tab gradient) with crisp tokens. + * + * Hot-reloads with --dev-theme (edit + save; MainWindow re-applies on themeChanged). + */ + +/* ---- containers & areas ---- */ +/* Container is black so the only thing between panels is a single strong line + (the splitter), not a grey gutter -> no "double line" around panels. */ +ads--CDockContainerWidget { background: {{border.strong}}; } +/* Kill the default 1px splitter padding that otherwise shows a grey gap. */ +ads--CDockContainerWidget > QSplitter { padding: 0; } +ads--CDockAreaWidget { background: {{bg.panel}}; border: none; } +ads--CDockWidget { background: {{bg.panel}}; border: none; } + +/* Title bar = the strip holding the tabs + area buttons. Darker than the panel + so the active tab (panel-colored) reads as raised out of a near-black header. */ +ads--CDockAreaTitleBar { background: {{bg.elevated}}; border: none; } + +/* ---- dock widget tabs ---- */ +/* Inactive tabs sit in the dark header but stay a step lighter than it so they + don't blend in (header = bg.elevated, inactive tab = gray.800, active = bg.panel). */ +ads--CDockWidgetTab { + background: {{gray.800}}; + border: none; + border-right: 1px solid {{border.subtle}}; + padding: 5px 14px; +} +/* Active tab is distinguished by its panel-colored background (no top indicator). */ +ads--CDockWidgetTab[activeTab="true"] { + background: {{bg.panel}}; +} +ads--CDockWidgetTab QLabel { color: {{text.disabled}}; } +ads--CDockWidgetTab[activeTab="true"] QLabel { color: {{text.primary}}; } +ads--CDockWidgetTab:hover QLabel { color: {{text.secondary}}; } + +/* ---- splitters / resize handles ---- */ +/* Strong (black) gutters between panels. NOTE: the gutter THICKNESS is the + splitter handleWidth, which QSplitter ignores from QSS (both ::handle width + and qproperty-handleWidth) — so it's set in C++ (MainWindow, via setHandleWidth + on dockAreaCreated). This rule only colors the gutter. */ +/* Match ADS's own selector specificity (it uses the CDockContainerWidget + descendant form), else its palette(dark) grey wins over our black. */ +ads--CDockContainerWidget ads--CDockSplitter::handle { background: {{border.strong}}; } +ads--CDockContainerWidget ads--CDockSplitter::handle:hover { background: {{accent}}; } +ads--CResizeHandle { background: {{border.strong}}; } + +/* ---- title-bar & tab buttons ---- */ +ads--CTitleBarButton { + background: transparent; + border: none; + border-radius: {{radius.sm}}px; + padding: 2px; +} +ads--CTitleBarButton:hover { background: {{ctrl.hover}}; } +ads--CTitleBarButton:pressed { background: {{ctrl.pressed}}; } + +#tabCloseButton:hover { background: {{ctrl.hover}}; border: 1px solid {{border.subtle}}; } +#tabCloseButton:pressed { background: {{ctrl.pressed}}; } + +/* ---- floating docks ---- */ +ads--CFloatingWidgetTitleBar { background: {{bg.window}}; } +#floatingTitleCloseButton:hover, #floatingTitleMaximizeButton:hover { background: {{ctrl.hover}}; } + +/* ---- auto-hide side panels ---- */ +ads--CAutoHideSideBar { background: {{bg.elevated}}; } +ads--CAutoHideDockContainer { background: {{bg.panel}}; } +#autoHideTitleLabel { color: {{text.secondary}}; } + +/* ---- white icons ---- (override ADS's black default SVGs; see resources/icons/ads/, + generated white/gray copies. Static URLs, no token substitution.) */ +#tabCloseButton { + qproperty-icon: url(:/adsicons/close-button.svg), + url(:/adsicons/close-button-disabled.svg) disabled; +} +#dockAreaCloseButton { + qproperty-icon: url(:/adsicons/close-button.svg), + url(:/adsicons/close-button-disabled.svg) disabled; +} +#tabsMenuButton { qproperty-icon: url(:/adsicons/tabs-menu-button.svg); } +#detachGroupButton { + qproperty-icon: url(:/adsicons/detach-button.svg), + url(:/adsicons/detach-button-disabled.svg) disabled; +} +#floatingTitleCloseButton { qproperty-icon: url(:/adsicons/close-button.svg); } +#dockAreaMinimizeButton { qproperty-icon: url(:/adsicons/minimize-button-focused.svg); } +#dockAreaAutoHideButton { + qproperty-icon: url(:/adsicons/vs-pin-button.svg), + url(:/adsicons/vs-pin-button-disabled.svg) disabled; +} +ads--CAutoHideDockContainer #dockAreaAutoHideButton { + qproperty-icon: url(:/adsicons/vs-pin-button-pinned-focused.svg); +} +ads--CFloatingWidgetTitleBar { + qproperty-maximizeIcon: url(:/adsicons/maximize-button.svg); + qproperty-normalIcon: url(:/adsicons/restore-button.svg); +} diff --git a/resources/qss/app.qss.in b/resources/qss/app.qss.in new file mode 100644 index 00000000..eebe4d2f --- /dev/null +++ b/resources/qss/app.qss.in @@ -0,0 +1,515 @@ +/* + * TextureLab application stylesheet (template). + * + * Double-brace placeholders are substituted from the active theme JSON by + * QssBuilder at load time -- a placeholder naming the color token bg.panel + * becomes #353535, radius.sm becomes 3, font.ui.family becomes the UI font + * family, and so on. (This comment avoids writing a literal placeholder so the + * substituter has nothing to flag.) + * + * PHASE 1 -- app chrome: menus, toolbars, status bar, and the common controls + * that appear across every panel (buttons, inputs, combos, checks, sliders, + * scrollbars, tooltips, splitters). Panel-specific styling lands in later + * phases (see UI_DESIGN_SYSTEM_PRD.md section 6). + * + * Edit + save this file to see changes live (no rebuild): hot-reload is ON by + * default in Debug builds (also `--dev-theme` in any build; `--no-dev-theme` to + * disable). See the hot-reload section in UI_DESIGN_SYSTEM_PRD.md. + */ + +/* ============================================================= base ======= */ + +QWidget { + font-family: {{font.ui.family}}; + font-size: {{font.ui.size}}px; + color: {{text.primary}}; +} + +QToolTip { + background: {{bg.elevated}}; + color: {{text.primary}}; + border: 1px solid {{border.subtle}}; + padding: 3px 6px; +} + +/* ======================================================= menu bar ========= */ + +QMenuBar { + background: {{bg.panel}}; + border: none; + padding: 2px 4px; +} +QMenuBar::item { + background: transparent; + padding: 4px 10px; + border-radius: {{radius.sm}}px; +} +QMenuBar::item:selected { background: {{ctrl.hover}}; } +QMenuBar::item:pressed { background: {{selection}}; color: {{text.primary}}; } + +QMenu { + background: {{bg.elevated}}; + border: 1px solid {{border.subtle}}; + padding: 4px; +} +QMenu::item { + padding: 5px 24px 5px 22px; + border-radius: {{radius.sm}}px; +} +QMenu::item:selected { background: {{selection}}; color: {{text.primary}}; } +QMenu::item:disabled { color: {{text.disabled}}; } +QMenu::separator { + height: 1px; + background: {{border.subtle}}; + margin: 4px 8px; +} +QMenu::icon { padding-left: 6px; } + +/* ======================================================= tool bar ========= */ + +QToolBar { + background: {{bg.panel}}; + border: none; + border-bottom: 1px solid {{border.subtle}}; + padding: 3px; + spacing: {{space.sm}}px; +} +QToolBar::separator { + width: 1px; + background: {{border.subtle}}; + margin: 4px 4px; +} +/* Main window toolbar: strong (black) top border matching the ADS dock gutters. + Extra right padding so the Export button isn't flush against the window edge. */ +#MainToolbar { + border-top: 1px solid {{border.strong}}; + padding-right: 10px; +} +/* Hover/pressed on the main toolbar buttons (undo/redo/export): a lighter fill, + no colored outline. */ +#MainToolbar QToolButton:hover { background: {{ctrl.hover}}; } +#MainToolbar QToolButton:pressed { background: {{ctrl.pressed}}; } +QToolButton { + background: transparent; + border: 1px solid transparent; + border-radius: {{radius.sm}}px; + padding: 4px 6px; +} +QToolButton:hover { background: {{ctrl.hover}}; } +QToolButton:pressed { background: {{ctrl.pressed}}; } +QToolButton:checked { background: {{selection}}; border-color: {{accent.press}}; } +/* Disabled: the global QWidget color rule forces text.primary even when + disabled (QSS color doesn't auto-dim), so dim it back explicitly. */ +QToolButton:disabled { color: {{text.disabled}}; } +QToolButton::menu-indicator { image: none; } + +/* ====================================================== status bar ======== */ + +QStatusBar { + background: {{bg.panel}}; + border-top: 1px solid {{border.subtle}}; +} +QStatusBar::item { border: none; } +QStatusBar QLabel { background: transparent; } + +#StatusVersionLabel { + color: {{text.disabled}}; + padding: 0 6px; +} + +/* ========================================================= buttons ======== */ + +QPushButton { + background: {{ctrl.bg}}; + border: 1px solid {{border.input}}; + border-radius: {{radius.sm}}px; + padding: 4px 12px; + min-height: 18px; +} +QPushButton:hover { background: {{ctrl.hover}}; } +QPushButton:pressed { background: {{ctrl.pressed}}; } +QPushButton:disabled { color: {{text.disabled}}; border-color: {{border.subtle}}; } +QPushButton:default { border-color: {{accent}}; } + +QPushButton[variant="primary"] { + background: {{accent}}; + border: 1px solid {{accent.press}}; + color: {{text.primary}}; +} +QPushButton[variant="primary"]:hover { background: {{accent.hover}}; } +QPushButton[variant="primary"]:pressed { background: {{accent.press}}; } + +/* ==================================================== text inputs ========= */ + +QLineEdit, QPlainTextEdit, QTextEdit, QSpinBox, QDoubleSpinBox { + background: {{bg.input}}; + border: 1px solid {{border.input}}; + border-radius: {{radius.sm}}px; + padding: 3px 6px; + selection-background-color: {{selection}}; + selection-color: {{text.primary}}; +} +QLineEdit:focus, QPlainTextEdit:focus, QTextEdit:focus, +QSpinBox:focus, QDoubleSpinBox:focus { + border-color: {{accent}}; +} +QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled { + color: {{text.disabled}}; +} + +QSpinBox::up-button, QDoubleSpinBox::up-button, +QSpinBox::down-button, QDoubleSpinBox::down-button { + width: 14px; + background: transparent; + border: none; +} +QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover, +QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover { + background: {{ctrl.hover}}; +} +/* Styling the up/down buttons drops the native arrows, so supply them. */ +QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { + image: url(:/icons/chevron-up.svg); + width: 9px; + height: 9px; +} +QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { + image: url(:/icons/chevron-down.svg); + width: 9px; + height: 9px; +} + +/* ==================================================== combo boxes ========= */ + +QComboBox { + background: {{ctrl.bg}}; + border: 1px solid {{border.input}}; + border-radius: {{radius.sm}}px; + padding: 3px 6px; + min-height: 18px; +} +QComboBox:hover { background: {{ctrl.hover}}; } +QComboBox:focus { border-color: {{accent}}; } +QComboBox::drop-down { + subcontrol-origin: padding; + subcontrol-position: center right; + width: 18px; + border: none; +} +/* Styling ::drop-down drops the native arrow, so supply our own chevron. */ +QComboBox::down-arrow { + image: url(:/icons/chevron-down.svg); + width: 12px; + height: 12px; +} +QComboBox QAbstractItemView { + background: {{bg.elevated}}; + border: 1px solid {{border.subtle}}; + selection-background-color: {{selection}}; + selection-color: {{text.primary}}; + outline: none; +} + +/* ============================================= checks & radios ============ */ + +QCheckBox, QRadioButton { spacing: 6px; background: transparent; } +QCheckBox::indicator, QRadioButton::indicator { + width: 15px; + height: 15px; + background: {{bg.input}}; + border: 1px solid {{border.input}}; +} +QCheckBox::indicator { border-radius: {{radius.sm}}px; } +QRadioButton::indicator { border-radius: 8px; } +QCheckBox::indicator:hover, QRadioButton::indicator:hover { border-color: {{accent}}; } +QCheckBox::indicator:checked, QRadioButton::indicator:checked { + background: {{ctrl.checked}}; + border-color: {{accent.press}}; +} + +/* ========================================================= sliders ======== */ + +QSlider::groove:horizontal { + height: 4px; + background: {{bg.input}}; + border-radius: 2px; +} +QSlider::sub-page:horizontal { background: {{accent}}; border-radius: 2px; } +QSlider::handle:horizontal { + background: {{text.secondary}}; + width: 12px; + height: 12px; + margin: -5px 0; + border-radius: 6px; +} +QSlider::handle:horizontal:hover { background: {{text.primary}}; } + +/* ======================================================= scrollbars ======= */ + +QScrollBar:vertical { + background: transparent; + width: 12px; + margin: 0; +} +QScrollBar:horizontal { + background: transparent; + height: 12px; + margin: 0; +} +QScrollBar::handle:vertical, QScrollBar::handle:horizontal { + background: {{gray.600}}; + border-radius: 4px; + border: 2px solid transparent; + background-clip: padding; +} +QScrollBar::handle:vertical { min-height: 28px; } +QScrollBar::handle:horizontal { min-width: 28px; } +QScrollBar::handle:hover { background: {{gray.550}}; } +QScrollBar::add-line, QScrollBar::sub-line { height: 0; width: 0; background: none; } +QScrollBar::add-page, QScrollBar::sub-page { background: none; } + +/* ========================================================= tab bar ======== */ + +QTabBar::tab { + background: {{bg.window}}; + color: {{text.secondary}}; + padding: 5px 12px; + border: none; +} +QTabBar::tab:selected { + background: {{bg.panel}}; + color: {{text.primary}}; + border-bottom: 2px solid {{accent}}; +} +QTabBar::tab:hover:!selected { color: {{text.primary}}; } + +/* ========================================================= splitter ======= */ + +QSplitter::handle { background: {{border.subtle}}; } +QSplitter::handle:horizontal { width: 1px; } +QSplitter::handle:vertical { height: 1px; } +QSplitter::handle:hover { background: {{accent}}; } + +/* ======================================================= progress ========= */ + +QProgressBar { + background: {{bg.input}}; + border: none; + border-radius: {{radius.sm}}px; + text-align: center; + color: {{text.primary}}; + max-height: 14px; +} +QProgressBar::chunk { background: {{accent}}; border-radius: {{radius.sm}}px; } + +/* ============================================= properties / inspector ===== */ + +/* Collapsible section header (AccordionWidget). + NOTE: min-height MUST be positive. With min-height:0 the flat header button + collapses to ~0px inside PropertiesWidget's dense QVBoxLayout, clipping the + title to invisibility (it survives in a looser layout, which masked it). The + original inline style set no min-height; keep an explicit one here since the + global QPushButton rule's 18px is what we're overriding. */ +#AccordionHeader { + background: {{gray.800}}; + color: {{text.secondary}}; + font-weight: bold; + text-align: left; + padding: 5px 8px; + border: none; + border-top: 1px solid {{border.subtle}}; + border-bottom: 1px solid {{border.subtle}}; + min-height: 20px; +} +#AccordionHeader:hover { background: {{gray.700}}; } + +/* "Frame" / "Comment" section titles in the properties panel */ +#PropSectionTitle { + font-weight: bold; + margin-bottom: 4px; +} + +/* Curve editor readout (In/Out values while dragging) */ +#CurveReadout { + color: {{text.disabled}}; + font-size: 10px; +} + +/* Image property placeholder / preview */ +#ImagePreview { + background: {{bg.input}}; + border: 1px solid {{border.input}}; + border-radius: {{radius.sm}}px; + color: {{text.disabled}}; +} + +/* Compact buttons (e.g. curve "Reset") */ +QPushButton[size="small"] { + font-size: 10px; + padding: 2px 6px; + min-height: 0; +} + +/* ================================================== viewport overlays ===== */ + +/* Compact 2D-view toolbar (save/copy/tile/recenter) */ +#View2DToolbar { padding: 1px; spacing: 1px; } +#View2DToolbar QToolButton { padding: 2px; } + +/* 2D view "Texture copied" toast (bottom-center, fades in/out) */ +#ViewToast { + background: {{bg.elevated}}; + color: {{text.primary}}; + border: 1px solid {{border.subtle}}; + padding: 10px 20px; + border-radius: {{radius.md}}px; +} + +/* ================================================= popups & dialogs ======= */ + +/* Floating node-search popup (Blender-style quick add). Frameless top-level, so + a crisp border reads as elevation; no radius (avoids frameless-corner artifacts). */ +#NodeSearchPopup { + background: {{bg.elevated}}; + border: 1px solid {{border.strong}}; +} + +/* Export dialog destination field + help text */ +#ExportDestination { + background: {{bg.input}}; + border: 1px solid {{border.input}}; + border-radius: {{radius.sm}}px; + padding: 5px; +} +#ExportDestination[empty="true"] { color: {{text.disabled}}; } +#ExportHelp { color: {{text.disabled}}; } + +/* Crash-report consent dialog. Reads as one block of prose, so the only + hierarchy is the title's size and the way the secondary text steps back. */ +#ConsentDialog { background: {{bg.window}}; } +#ConsentTitle { color: {{text.primary}}; font-size: 17px; font-weight: bold; } +#ConsentBody { color: {{text.secondary}}; font-size: 13px; } +#ConsentFactLead { color: {{text.primary}}; font-size: 12px; font-weight: bold; } +#ConsentFact { color: {{text.secondary}}; font-size: 12px; } +#ConsentNote { color: {{text.disabled}}; font-size: 11px; } +#ConsentSeparator { color: {{border.subtle}}; } + +/* About dialog typography */ +#AboutTitle { color: {{text.primary}}; font-size: 26px; font-weight: bold; } +#AboutTag { color: {{text.secondary}}; font-size: 12px; } +#AboutVersion { color: {{text.secondary}}; font-size: 13px; } +#AboutDesc { color: {{text.secondary}}; font-size: 13px; } +#AboutLink { font-size: 12px; } +#AboutCopyright { color: {{text.disabled}}; font-size: 11px; } +#AboutSeparator { color: {{border.subtle}}; } + +/* ==================================================== library panel ======= */ + +/* Library version indicator; turns warn-colored when the library is outdated */ +#LibraryVersionLabel { color: {{text.secondary}}; } +#LibraryVersionLabel[outdated="true"] { color: {{warn}}; } + +/* Library search: a bit larger/taller than the default input */ +#LibrarySearch { + font-size: 13px; + padding: 4px 6px; + min-height: 24px; +} + +/* Node-thumbnail grid: transparent card that gets an accent ring on hover/select */ +#LibraryList { + background: {{bg.base}}; + border: none; +} +#LibraryList::item { + border: 1px solid transparent; + border-radius: {{radius.sm}}px; + margin-left: 6px; + padding: 2px; +} +#LibraryList::item:hover { border: 1px solid {{accent}}; } +#LibraryList::item:selected { + border: 1px solid {{accent}}; + background: {{ctrl.hover}}; + color: {{text.primary}}; +} + +/* ==================================================== launcher =========== */ + +/* Project-manager window. Two thin bars with the card grid between them; the + cards themselves are painted by TextureCardDelegate, not styled here. */ + +#launcherTopBar, +#launcherActionBar { + background: {{bg.panel}}; +} +#launcherTopBar { border-bottom: 1px solid {{border.subtle}}; } +#launcherActionBar { border-top: 1px solid {{border.subtle}}; } + +#launcherGrid { + background: {{bg.elevated}}; + border: none; +} +/* The delegate owns the whole card, so the view must not draw its own + selection fill underneath it. */ +#launcherGrid::item, +#launcherGrid::item:hover, +#launcherGrid::item:selected { + background: transparent; + border: none; +} + +/* All / Recents / Starred, and the grid/list toggle — segmented controls that + fill when active rather than carrying an underline. Grey rather than accent + on purpose: the material thumbnails are meant to be the only saturated thing + in the window (see LAUNCHER_PRD.md §3.5). */ +#launcherFilterTab { + background: transparent; + border: none; + border-radius: {{radius.sm}}px; + color: {{text.secondary}}; + padding: 4px 10px; + font-size: 13px; +} +#launcherFilterTab:hover { + color: {{text.primary}}; + background: {{ctrl.bg}}; +} +#launcherFilterTab:checked { + color: {{text.primary}}; + background: {{ctrl.hover}}; +} + +/* First-run overlay: the message, plus a "New Texture" button on the genuine + empty state. It floats over the grid, so it must not paint a panel of its + own. */ +#launcherEmptyPanel { + background: transparent; +} +#launcherEmptyLabel { + background: transparent; + color: {{text.secondary}}; + font-size: 14px; +} + +/* Home button in the status bar — reopens the launcher */ +#StatusHomeButton { + background: transparent; + border: none; + color: {{text.secondary}}; + padding: 0px 6px; + font-size: 14px; +} +#StatusHomeButton:hover { color: {{text.primary}}; } + +/* Update notice — hidden until a newer release exists, so it only ever appears + when it has something to say. Accent-tinted rather than shouty. */ +#launcherUpdateButton { + background: {{accent}}; + border: none; + border-radius: {{radius.sm}}px; + color: {{white}}; + padding: 4px 10px; + font-size: 12px; +} +#launcherUpdateButton:hover { background: {{accent.hover}}; } diff --git a/resources/theme.qrc b/resources/theme.qrc new file mode 100644 index 00000000..d7f39548 --- /dev/null +++ b/resources/theme.qrc @@ -0,0 +1,22 @@ + + + themes/dark.json + + + qss/app.qss.in + qss/ads.qss.in + + + icons/ads/close-button.svg + icons/ads/close-button-disabled.svg + icons/ads/tabs-menu-button.svg + icons/ads/detach-button.svg + icons/ads/detach-button-disabled.svg + icons/ads/minimize-button-focused.svg + icons/ads/maximize-button.svg + icons/ads/restore-button.svg + icons/ads/vs-pin-button.svg + icons/ads/vs-pin-button-disabled.svg + icons/ads/vs-pin-button-pinned-focused.svg + + diff --git a/resources/themes/dark.json b/resources/themes/dark.json new file mode 100644 index 00000000..65d0642d --- /dev/null +++ b/resources/themes/dark.json @@ -0,0 +1,139 @@ +{ + "meta": { + "name": "TextureLab Dark", + "base": "dark" + }, + "color": { + "gray.900": "#191919", + "gray.850": "#232323", + "gray.800": "#2B2B2B", + "gray.750": "#2E2E2E", + "gray.700": "#353535", + "gray.650": "#404040", + "gray.600": "#505050", + "gray.550": "#5C5C5C", + "gray.400": "#7F7F7F", + "gray.300": "#787878", + "gray.200": "#C8C8C8", + "white": "#FFFFFF", + "black": "#000000", + "accent": "#2A82DA", + "accent.hover": "#3D93E8", + "accent.press": "#2069B8", + "warn": "#E5A54B", + "danger": "#E5484D", + "ok": "#3DAF6E", + "bg.window": "@gray.700", + "bg.panel": "@gray.700", + "bg.base": "@gray.850", + "bg.elevated": "@gray.900", + "bg.input": "@gray.750", + "border.subtle": "@gray.900", + "border.strong": "@black", + "border.input": "@gray.600", + "text.primary": "@white", + "text.secondary": "@gray.200", + "text.disabled": "@gray.400", + "selection": "@accent", + "ctrl.bg": "@gray.650", + "ctrl.hover": "@gray.600", + "ctrl.pressed": "@gray.550", + "ctrl.checked": "@accent", + "node.bg": "#0A0A0A", + "node.border": "@black", + "node.border.hover": "@gray.300", + "node.border.select": "@gray.200", + "node.title": "@white", + "node.channel": "#C8FFC8", + "socket.fill": "#AAAAAA", + "wire": "#AAAAAA", + "wire.dragging": "#969696", + "wire.selected": "@accent", + "grid.bg": "@gray.700", + "grid.fine": "#3C3C3C", + "grid.coarse": "@gray.900", + "checker.a": "#C0C0C0", + "checker.b": "#808080", + "frame.select": "@warn", + "comment.fill": "@white", + "comment.text": "#F0F0F0", + "view2d.bg": "#212121", + "view3d.clear": "#1A1A1A", + "view3d.grid": "@gray.600", + "curve.bg": "@bg.elevated", + "curve.grid": "@gray.850", + "curve.identity": "@gray.800", + "curve.line": "@text.secondary", + "curve.anchor": "@text.disabled", + "curve.anchor.hover": "@text.primary", + "curve.anchor.select": "@accent", + "curve.handle.line": "@gray.600", + "curve.handle.dot": "@text.disabled", + "curve.handle.hover": "@text.secondary", + "curve.handle.corner": "@warn", + "launcher.card": "@gray.800", + "launcher.card.hover": "@gray.700", + "launcher.card.border": "@gray.900", + "launcher.thumb.bg": "@gray.850", + "launcher.star": "@warn", + "launcher.badge": "@warn", + "launcher.pip.off": "@gray.650", + "launcher.pip.on": "@gray.200", + "launcher.open.dot": "@ok" + }, + "palette": { + "window": "@bg.window", + "windowText": "@text.primary", + "base": "@bg.base", + "alternateBase": "@bg.window", + "toolTipBase": "@gray.900", + "toolTipText": "@text.primary", + "text": "@text.primary", + "button": "@bg.window", + "buttonText": "@text.primary", + "brightText": "@danger", + "link": "@accent", + "highlight": "@accent", + "highlightedText": "@black", + "disabled.windowText": "@text.disabled", + "disabled.text": "@text.disabled", + "disabled.buttonText": "@text.disabled", + "disabled.highlightedText": "@text.disabled", + "disabled.highlight": "@gray.600" + }, + "radius": { + "sm": 3, + "md": 5, + "lg": 8, + "pill": 999 + }, + "space": { + "xs": 2, + "sm": 4, + "md": 8, + "lg": 12, + "xl": 16 + }, + "motion": { + "fast": 120, + "base": 180, + "slow": 260 + }, + "font": { + "ui": { + "family": "Segoe UI, Inter, sans-serif", + "size": 12, + "weight": 400 + }, + "mono": { + "family": "Consolas, JetBrains Mono, monospace", + "size": 12, + "weight": 400 + }, + "title": { + "family": "Segoe UI, Inter, sans-serif", + "size": 13, + "weight": 600 + } + } +} diff --git a/scripts/check-theme-hygiene.sh b/scripts/check-theme-hygiene.sh new file mode 100755 index 00000000..395ab681 --- /dev/null +++ b/scripts/check-theme-hygiene.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# Theme hygiene gate. +# +# The theme system (src/theme/ + resources/themes/*.json + resources/qss/*.qss.in) +# is the single source of truth for the app's look. This check fails the build if +# new *inline widget stylesheets* or *hardcoded QColor literals* creep into the +# UI / rendering code, which would bypass the theme and break --dev-theme +# hot-reload. +# +# Legitimate exceptions carry a "// theme-exempt: " marker on the same +# line (e.g. dynamic color-DATA swatches, or the one call that applies the +# composed theme sheet to the ADS dock manager). See UI_DESIGN_SYSTEM_PRD.md. +# +# Out of scope by design: src/theme (the system), src/ads (vendored submodule), +# src/colorpicker (a color-DATA widget lib), src/texturelab/libraries (node +# default *values*, not UI styling). + +set -uo pipefail +cd "$(dirname "$0")/.." + +# UI + rendering code that must stay theme-driven. +SCOPE=( + "src/texturelab/widgets" + "src/texturelab/mainwindow.cpp" + "src/nodegraph/graph" +) + +status=0 + +# Drop matches that live on a commented-out line (content after "file:line:" +# starts with //) and any line carrying the theme-exempt marker. +drop_noise() { grep -vE ':[0-9]+:[[:space:]]*//' | grep -v 'theme-exempt'; } + +# 1) Inline widget stylesheets -> belong in resources/qss/app.qss.in. +hits=$(grep -rnE '(->|\.)setStyleSheet\(' "${SCOPE[@]}" --include=*.cpp 2>/dev/null \ + | drop_noise) +if [ -n "$hits" ]; then + echo "FAIL: inline setStyleSheet() in UI code." + echo " Move the rule to resources/qss/app.qss.in and target it by objectName," + echo " or add '// theme-exempt: ' if it is genuinely dynamic data." + echo "$hits" | sed 's/^/ /' + echo + status=1 +fi + +# 2) Hardcoded numeric QColor literals in paint code -> use a token. +hits=$(grep -rnE 'QColor\((0x)?[0-9]' "${SCOPE[@]}" --include=*.cpp 2>/dev/null \ + | drop_noise) +if [ -n "$hits" ]; then + echo "FAIL: hardcoded QColor(...) literal in rendering code." + echo " Add a token to resources/themes/dark.json + src/theme/tokens.h and read it" + echo " via ntColor()/ThemeManager::instance().theme().color(), or mark // theme-exempt." + echo "$hits" | sed 's/^/ /' + echo + status=1 +fi + +if [ "$status" -eq 0 ]; then + echo "theme hygiene: OK — no un-exempted inline stylesheets or hardcoded colors in UI code." +fi +exit $status diff --git a/scripts/package-linux.sh b/scripts/package-linux.sh new file mode 100755 index 00000000..f0d1bb6c --- /dev/null +++ b/scripts/package-linux.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Packages an already-built texturelab into an AppImage in dist/. +# Used by .github/workflows/release.yml; runnable locally for the same result. +# +# scripts/package-linux.sh [build-dir] +# e.g. scripts/package-linux.sh v0.4.0-beta build +# +# Requires linuxdeploy + its qt plugin on PATH (override with $LINUXDEPLOY). +# Qt is found via qmake on PATH, or $QTDIR. +set -euo pipefail + +TAG=${1:?usage: package-linux.sh [build-dir]} +BUILD=${2:-build} +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$ROOT" + +BIN="$BUILD/src/texturelab/texturelab" +[ -x "$BIN" ] || { echo "error: $BIN not found — build first" >&2; exit 1; } + +LINUXDEPLOY=${LINUXDEPLOY:-linuxdeploy-x86_64.AppImage} +command -v "$LINUXDEPLOY" >/dev/null || { echo "error: $LINUXDEPLOY not on PATH" >&2; exit 1; } + +# Qt: prefer an explicit $QTDIR, else derive it from qmake's location. +if [ -z "${QTDIR:-}" ]; then + QMAKE_BIN=$(command -v qmake6 || command -v qmake) || { + echo "error: set QTDIR or put qmake on PATH" >&2; exit 1; } + QTDIR=$(dirname "$(dirname "$QMAKE_BIN")") +fi +export QMAKE="$QTDIR/bin/qmake" +export PATH="$QTDIR/bin:$PATH" +export LD_LIBRARY_PATH="$QTDIR/lib:${LD_LIBRARY_PATH:-}" + +# linuxdeploy's qt plugin deploys *every* sqldriver it finds and hard-fails when +# one has an unresolvable dependency — Qt's libqsqlmimer.so needs a proprietary +# Mimer client that isn't installed. We only ever open QSQLITE +# (src/catalog/database.cpp), so drop the rest; that also avoids shipping the +# GPL libmysqlclient the MySQL driver drags in. +if [ -w "$QTDIR/plugins/sqldrivers" ]; then + find "$QTDIR/plugins/sqldrivers" -name 'libqsql*.so' ! -name 'libqsqlite.so' -delete +fi + +APPDIR="$BUILD/AppDir" +STAGE="$BUILD/appimage-stage" +rm -rf "$APPDIR" "$STAGE" +mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/doc/texturelab" "$STAGE" dist + +# crashpad_handler ships next to the binary; without it crashes go unreported. +HANDLER=$(ls "$BUILD/src/texturelab/crashpad_handler" \ + "$BUILD/_deps/sentry-build/crashpad_build/handler/crashpad_handler" \ + 2>/dev/null | head -1) || true +[ -n "$HANDLER" ] || { echo "error: crashpad_handler not found in $BUILD" >&2; exit 1; } +cp "$HANDLER" "$APPDIR/usr/bin/" +cp LICENSE "$APPDIR/usr/share/doc/texturelab/" + +# The icon's basename must match the desktop entry's Icon= key (texturelab). +cp packaging/linux/texturelab.desktop "$STAGE/texturelab.desktop" +cp src/icons/logo.png "$STAGE/texturelab.png" + +OUT="dist/texturelab-linux-${TAG}-x86_64.AppImage" +rm -f "$OUT" +# The appimage plugin writes to $OUTPUT, so we never have to guess the name. +APPIMAGE_EXTRACT_AND_RUN=${APPIMAGE_EXTRACT_AND_RUN:-1} \ +DEPLOY_STDCXX=${DEPLOY_STDCXX:-1} \ +OUTPUT="$OUT" \ +"$LINUXDEPLOY" \ + --appdir "$APPDIR" \ + --executable "$BIN" \ + --desktop-file "$STAGE/texturelab.desktop" \ + --icon-file "$STAGE/texturelab.png" \ + --plugin qt \ + --output appimage + +chmod +x "$OUT" +echo "packaged: $OUT" diff --git a/scripts/package-macos.sh b/scripts/package-macos.sh new file mode 100755 index 00000000..cec1ea67 --- /dev/null +++ b/scripts/package-macos.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# +# Bundles Qt into the built texturelab.app, optionally signs it, and produces a +# DMG in dist/. Used by .github/workflows/release.yml; runnable locally too. +# +# scripts/package-macos.sh [build-dir] +# e.g. scripts/package-macos.sh v0.4.0-beta build +# +# Signing is optional: set MAC_SIGN_ID to a "Developer ID Application: ..." +# identity to sign the bundle and the DMG. Notarization is a separate step +# (see the workflow) and must run after this script. +# macdeployqt is found via qmake on PATH, or $QTDIR / $MACDEPLOYQT. +set -euo pipefail + +TAG=${1:?usage: package-macos.sh [build-dir]} +BUILD=${2:-build} +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$ROOT" + +APP="$BUILD/src/texturelab/texturelab.app" +[ -d "$APP" ] || { echo "error: $APP not found — build first" >&2; exit 1; } + +if [ -z "${MACDEPLOYQT:-}" ]; then + if [ -n "${QTDIR:-}" ]; then + MACDEPLOYQT="$QTDIR/bin/macdeployqt" + else + QMAKE_BIN=$(command -v qmake6 || command -v qmake) || { + echo "error: set QTDIR/MACDEPLOYQT or put qmake on PATH" >&2; exit 1; } + MACDEPLOYQT="$(dirname "$QMAKE_BIN")/macdeployqt" + fi +fi +[ -x "$MACDEPLOYQT" ] || { echo "error: macdeployqt not found at $MACDEPLOYQT" >&2; exit 1; } + +# crashpad_handler is copied next to the binary at build time; it must end up +# inside the bundle or crash reporting silently does nothing. +[ -f "$APP/Contents/MacOS/crashpad_handler" ] || { + echo "error: crashpad_handler missing from the bundle" >&2; exit 1; } + +"$MACDEPLOYQT" "$APP" + +# Ship only the SQLite driver, matching the Linux package. +find "$APP/Contents/PlugIns/sqldrivers" -name 'libqsql*.dylib' \ + ! -name 'libqsqlite.dylib' -delete 2>/dev/null || true +cp LICENSE "$APP/Contents/Resources/" + +if [ -n "${MAC_SIGN_ID:-}" ]; then + # Hardened runtime + a secure timestamp are required for notarization. + # --deep also covers the bundled Qt frameworks, plugins and crashpad_handler. + codesign --force --deep --options runtime --timestamp --sign "$MAC_SIGN_ID" "$APP" + codesign --verify --deep --strict --verbose=2 "$APP" +else + echo "warning: MAC_SIGN_ID not set — building an UNSIGNED app (Gatekeeper will block it)" >&2 +fi + +STAGE="$BUILD/dmg-stage" +rm -rf "$STAGE" +mkdir -p "$STAGE" dist +cp -R "$APP" "$STAGE/" +ln -s /Applications "$STAGE/Applications" # drag-to-install layout + +OUT="dist/texturelab-mac-${TAG}-universal.dmg" +rm -f "$OUT" +hdiutil create -volname "TextureLab" -srcfolder "$STAGE" -format UDZO -ov "$OUT" +if [ -n "${MAC_SIGN_ID:-}" ]; then + codesign --force --timestamp --sign "$MAC_SIGN_ID" "$OUT" +fi + +echo "packaged: $OUT" diff --git a/scripts/package-windows.ps1 b/scripts/package-windows.ps1 new file mode 100644 index 00000000..cae1955b --- /dev/null +++ b/scripts/package-windows.ps1 @@ -0,0 +1,74 @@ +<# +.SYNOPSIS + Packages an already-built texturelab.exe into a redistributable folder and + zip. Used by .github/workflows/release.yml; runnable locally for the same + result. + +.DESCRIPTION + Two stages, because code signing has to happen after the files are gathered + but before they are zipped: + + scripts/package-windows.ps1 -Tag v0.4.0-beta -Stage Deploy # dist/texturelab-win-/ + /*.exe here> + scripts/package-windows.ps1 -Tag v0.4.0-beta -Stage Zip # dist/texturelab-win--x64.zip + + -Stage Both (the default) runs both, which is what you want locally. + windeployqt is found on PATH, or via $env:QTDIR, or -WindeployQt. +#> +param( + [Parameter(Mandatory = $true)][string]$Tag, + [ValidateSet("Deploy", "Zip", "Both")][string]$Stage = "Both", + [string]$BuildDir = "build", + [string]$Config = "Release", + [string]$WindeployQt +) + +$ErrorActionPreference = "Stop" +Set-Location (Join-Path $PSScriptRoot "..") + +$dist = "dist\texturelab-win-$Tag" +$zip = "dist\texturelab-win-$Tag-x64.zip" + +if ($Stage -eq "Deploy" -or $Stage -eq "Both") { + $exe = "$BuildDir\src\texturelab\$Config\texturelab.exe" + if (-not (Test-Path $exe)) { throw "$exe not found - build first" } + + if (-not $WindeployQt) { + $WindeployQt = if ($env:QTDIR) { "$env:QTDIR\bin\windeployqt.exe" } + else { (Get-Command windeployqt.exe -ErrorAction SilentlyContinue).Source } + } + if (-not $WindeployQt -or -not (Test-Path $WindeployQt)) { + throw "windeployqt.exe not found - set `$env:QTDIR or pass -WindeployQt" + } + + if (Test-Path $dist) { Remove-Item $dist -Recurse -Force } + New-Item -ItemType Directory -Force $dist | Out-Null + Copy-Item $exe $dist + + & $WindeployQt "$dist\texturelab.exe" --release --no-translations + if ($LASTEXITCODE -ne 0) { throw "windeployqt failed ($LASTEXITCODE)" } + + # Ship only the SQLite driver, matching the Linux and macOS packages. + Get-ChildItem "$dist\sqldrivers" -Filter *.dll -ErrorAction SilentlyContinue | + Where-Object { $_.Name -ne "qsqlite.dll" } | Remove-Item + + # crashpad_handler.exe must sit next to the exe or crashes go unreported. + $handler = @( + "$BuildDir\src\texturelab\$Config\crashpad_handler.exe", + "$BuildDir\src\texturelab\crashpad_handler.exe", + "$BuildDir\_deps\sentry-build\crashpad_build\handler\$Config\crashpad_handler.exe" + ) | Where-Object { Test-Path $_ } | Select-Object -First 1 + if (-not $handler) { throw "crashpad_handler.exe not found in $BuildDir" } + Copy-Item $handler $dist + + Copy-Item LICENSE "$dist\LICENSE.txt" + Write-Host "deployed: $dist" +} + +if ($Stage -eq "Zip" -or $Stage -eq "Both") { + if (-not (Test-Path $dist)) { throw "$dist not found - run -Stage Deploy first" } + if (Test-Path $zip) { Remove-Item $zip -Force } + # Zip the folder itself so it extracts into texturelab-win-\. + Compress-Archive -Path $dist -DestinationPath $zip + Write-Host "packaged: $zip" +} diff --git a/scripts/sentry-local-test.sh b/scripts/sentry-local-test.sh new file mode 100755 index 00000000..47239b14 --- /dev/null +++ b/scripts/sentry-local-test.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Local end-to-end test for Sentry crash symbolication (Linux). +# +# Mirrors what CI does, then triggers a real crash so you can confirm the +# stack trace is readable in sentry.io. +# +# Requires: sentry-cli on PATH, and these env vars: +# SENTRY_AUTH_TOKEN – an auth token with project:write / project:releases +# SENTRY_ORG – your Sentry org slug +# SENTRY_PROJECT – your Sentry project slug +# +# Usage: +# SENTRY_AUTH_TOKEN=xxx SENTRY_ORG=xxx SENTRY_PROJECT=xxx \ +# ./scripts/sentry-local-test.sh [build-dir] +set -euo pipefail + +BUILD_DIR="${1:-build-sentry-test}" +BIN="$BUILD_DIR/src/texturelab/texturelab" + +: "${SENTRY_AUTH_TOKEN:?set SENTRY_AUTH_TOKEN}" +: "${SENTRY_ORG:?set SENTRY_ORG}" +: "${SENTRY_PROJECT:?set SENTRY_PROJECT}" + +if [ ! -f "$BIN" ]; then + echo "!! $BIN not found. Build first:" + echo " cmake --build $BUILD_DIR --target texturelab --parallel \$(nproc)" + exit 1 +fi + +echo "== 1) Inspecting DIF of the freshly built binary ==" +sentry-cli debug-files check "$BIN" + +echo +echo "== 2) Uploading FULL unstripped binary (debug + unwind + sources) ==" +sentry-cli debug-files upload --include-sources "$BIN" + +echo +echo "== 3) Stripping the shipped copy (build-id / Debug ID is preserved) ==" +strip "$BIN" +sentry-cli debug-files check "$BIN" # should still show a matching Debug ID + +echo +echo "== 4) Triggering a deliberate crash so Crashpad uploads a minidump ==" +# Wipe any stale crash DB so we know the minidump is from this run. +rm -rf "$HOME/.local/share/texturelab/texturelab/sentry" 2>/dev/null || true +set +e +"$BIN" --sentry-crash-test +echo " app exited with code $? (a crash is expected)" +set -e + +echo +echo "== Done ==" +echo "Crashpad uploads the minidump in the background. Open your Sentry project:" +echo " https://$SENTRY_ORG.sentry.io/issues/" +echo "You should see a new crash whose stack trace includes 'sentryCrashTest'" +echo "and 'main' with file/line info. If the frames are symbolicated, the fix works." diff --git a/src/App.vue b/src/App.vue deleted file mode 100644 index 610f8f88..00000000 --- a/src/App.vue +++ /dev/null @@ -1,1429 +0,0 @@ - - - - - - - - diff --git a/src/ads b/src/ads new file mode 160000 index 00000000..25834084 --- /dev/null +++ b/src/ads @@ -0,0 +1 @@ +Subproject commit 258340842056271ec207f9b112ac5efc45d9b5f5 diff --git a/src/assets/logo.png b/src/assets/logo.png deleted file mode 100644 index f3d2503f..00000000 Binary files a/src/assets/logo.png and /dev/null differ diff --git a/src/background.ts b/src/background.ts deleted file mode 100644 index 49777279..00000000 --- a/src/background.ts +++ /dev/null @@ -1,127 +0,0 @@ -"use strict"; - -import { app, protocol, BrowserWindow, ipcMain } from "electron"; -import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; -import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"; -import { setupMenu } from "./menu"; -import * as Sentry from "@sentry/electron"; - -// todo: fix later -// https://docs.sentry.io/platforms/javascript/guides/electron/ -// if (process.env.VUE_APP_SENTRY_DNS) { -// Sentry.init({ dsn: process.env.VUE_APP_SENTRY_DNS }); -// } - -// seems as tho this needs to be called else the app breaks -require("@electron/remote/main").initialize(); - -const isDevelopment = process.env.NODE_ENV !== "production"; - -app.commandLine.appendSwitch("--disable-seccomp-filter-sandbox"); - -// Keep a global reference of the window object, if you don't, the window will -// be closed automatically when the JavaScript object is garbage collected. -let win: BrowserWindow | null; - -// Scheme must be registered before the app is ready -protocol.registerSchemesAsPrivileged([ - { scheme: "app", privileges: { secure: true, standard: true } } -]); -console.log("registered schemes"); - -function createWindow() { - // Create the browser window. - win = new BrowserWindow({ - width: 1280, - height: 720, - frame: false, - webPreferences: { - nodeIntegration: true, - webSecurity: false, - contextIsolation: false, - enableRemoteModule: true - } - }); - win.maximize(); - - setupMenu(); - - if (process.env.WEBPACK_DEV_SERVER_URL) { - // Load the url of the dev server if in development mode - win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string); - //if (!process.env.IS_TEST) win.webContents.openDevTools(); - } else { - createProtocol("app"); - // Load the index.html when not in development - console.log("loading page"); - win.loadURL("app://./index.html"); - } - - win.on("closed", () => { - win = null; - }); -} - -// Quit when all windows are closed. -app.on("window-all-closed", () => { - // On macOS it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== "darwin") { - app.quit(); - } -}); - -app.on("activate", () => { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (win === null) { - createWindow(); - } -}); - -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.on("ready", async () => { - if (isDevelopment && !process.env.IS_TEST) { - // Install Vue Devtools - try { - await installExtension({ - id: "ljjemllljcmogpfapbkkighbhhppjdbg", //Vue Devtools beta - electron: ">=1.2.1" - }); - } catch (e) { - console.error("Vue Devtools failed to install:", e.toString()); - } - } - - // needed for image files to work - // https://github.com/electron/electron/issues/23393 - protocol.registerFileProtocol("image", (request, callback) => { - const url = request.url.replace("image://", ""); - - try { - return callback(url); - } catch (error) { - console.error(error); - return callback(""); - } - }); - - createWindow(); -}); - -// Exit cleanly on request from parent process in development mode. -if (isDevelopment) { - if (process.platform === "win32") { - process.on("message", data => { - if (data === "graceful-exit") { - app.quit(); - } - }); - } else { - process.on("SIGTERM", () => { - app.quit(); - }); - } -} diff --git a/src/catalog/CMakeLists.txt b/src/catalog/CMakeLists.txt new file mode 100644 index 00000000..d9a9a9e2 --- /dev/null +++ b/src/catalog/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.10) + +# The launcher's data layer: index.db (what textures exist) and thumbs.db +# (their cached previews). See LAUNCHER_PRD.md. +# +# Deliberately free of any dependency on the app's node graph or on Qt Widgets, +# so it builds and tests headless. The mapping between TextureChannel and this +# library's ChannelBit lives at the app boundary, not here. + +project(catalog LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Sets QT_VERSION_MAJOR, matching how the other subdirectories resolve Qt. +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Sql) + +add_library(catalog STATIC + database.h + database.cpp + catalogindex.h + catalogindex.cpp + thumbnailcache.h + thumbnailcache.cpp + texturerecord.h +) + +target_include_directories(catalog PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# Qt6::Sql pulls in the bundled QSQLITE driver plugin. The deploy tooling +# (linuxdeploy --plugin qt, windeployqt, macdeployqt) copies sqldrivers/ +# automatically for any target that links it. +target_link_libraries(catalog PUBLIC + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Sql +) + +set_target_properties(catalog PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/src/catalog/catalogindex.cpp b/src/catalog/catalogindex.cpp new file mode 100644 index 00000000..dab6f8b0 --- /dev/null +++ b/src/catalog/catalogindex.cpp @@ -0,0 +1,662 @@ +#include "catalogindex.h" + +#include +#include +#include +#include + +namespace catalog { + +namespace { + +// Column order shared by every SELECT, so readRow() can stay a single function. +const char* const kColumns = + "id, path, name, file_size, file_mtime, width, height, " + "node_count, lib_version, channels, created_at, last_opened, last_saved, " + "starred, missing_since"; + +// Escapes the LIKE metacharacters so a search for "50%" doesn't match +// everything. Paired with ESCAPE '\' in the query. +QString escapeLike(const QString& term) +{ + QString out = term; + out.replace(QLatin1Char('\\'), QLatin1String("\\\\")); + out.replace(QLatin1Char('%'), QLatin1String("\\%")); + out.replace(QLatin1Char('_'), QLatin1String("\\_")); + return out; +} + +QVariant nullIfZero(qint64 value) +{ + return value == 0 ? QVariant() : QVariant(value); +} + +} // namespace + +CatalogIndex::CatalogIndex() = default; + +CatalogIndex::~CatalogIndex() +{ + close(); +} + +bool CatalogIndex::open(const QString& path) +{ + close(); + + // index.db holds no blobs and is read far more than written, so the + // defaults are right; only thumbs.db needs page_size/auto_vacuum tuning. + Database::Options options; + options.walMode = true; + options.foreignKeys = true; + + if (!db.open(path, options)) + return false; + + const bool fresh = db.scalar( + QStringLiteral("SELECT count(*) FROM sqlite_master WHERE type='table'")) + == 0; + + if (fresh) { + if (!createSchema()) { + close(); + return false; + } + currentVersion = SchemaVersion; + return true; + } + + currentVersion = readSchemaVersion(); + + if (currentVersion > SchemaVersion) { + // Written by a newer build. Migrating backwards is guesswork and this + // file cannot be regenerated by rescanning, so reopen read-only and let + // the launcher show what it can. + qWarning("catalog: index schema v%d is newer than this build (v%d); opening read-only", + currentVersion, SchemaVersion); + db.close(); + + Database::Options ro = options; + ro.readOnly = true; + if (!db.open(path, ro)) + return false; + + readOnly = true; + return true; + } + + if (currentVersion < SchemaVersion && !migrate(currentVersion)) { + close(); + return false; + } + + return true; +} + +void CatalogIndex::close() +{ + db.close(); + readOnly = false; + currentVersion = 0; +} + +bool CatalogIndex::createSchema() +{ + QStringList statements; + + statements << QStringLiteral(R"( + CREATE TABLE texture ( + id INTEGER PRIMARY KEY, + path TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + file_size INTEGER NOT NULL DEFAULT 0, + file_mtime INTEGER NOT NULL DEFAULT 0, + width INTEGER, + height INTEGER, + node_count INTEGER, + lib_version TEXT, + channels INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL, + last_opened INTEGER, + last_saved INTEGER, + starred INTEGER NOT NULL DEFAULT 0, + missing_since INTEGER + ) + )"); + + statements << QStringLiteral(R"( + CREATE TABLE tag ( + texture_id INTEGER NOT NULL REFERENCES texture(id) ON DELETE CASCADE, + tag TEXT NOT NULL, + PRIMARY KEY (texture_id, tag) + ) WITHOUT ROWID + )"); + + statements << QStringLiteral( + "CREATE INDEX ix_recent ON texture(last_opened DESC) WHERE missing_since IS NULL"); + statements << QStringLiteral("CREATE INDEX ix_name ON texture(name)"); + + statements << QStringLiteral("CREATE TABLE meta (k TEXT PRIMARY KEY, v TEXT)"); + statements << QStringLiteral("INSERT INTO meta (k, v) VALUES ('schema_version', '%1')") + .arg(SchemaVersion); + + return db.execBatch(statements); +} + +int CatalogIndex::readSchemaVersion() +{ + QSqlQuery query = db.prepare(QStringLiteral("SELECT v FROM meta WHERE k = 'schema_version'")); + if (!query.exec() || !query.next()) + return 0; + + return query.value(0).toInt(); +} + +bool CatalogIndex::writeSchemaVersion(int version) +{ + QSqlQuery query = db.prepare(QStringLiteral( + "INSERT INTO meta (k, v) VALUES ('schema_version', ?) " + "ON CONFLICT(k) DO UPDATE SET v = excluded.v")); + query.addBindValue(QString::number(version)); + if (!query.exec()) { + db.setError(query.lastError().text()); + return false; + } + currentVersion = version; + return true; +} + +bool CatalogIndex::migrate(int fromVersion) +{ + // No migrations yet — v1 is the first shipped schema. When one is needed, + // add a step here per version, each inside its own transaction, and bump + // SchemaVersion. Steps must be additive: this file is the only copy of the + // user's history. + Q_UNUSED(fromVersion); + return writeSchemaVersion(SchemaVersion); +} + +// --- write points --------------------------------------------------------- + +bool CatalogIndex::recordOpened(TextureRecord& rec, qint64 whenMs) +{ + return upsert(rec, Stamp::Opened, whenMs); +} + +bool CatalogIndex::recordSaved(TextureRecord& rec, qint64 whenMs) +{ + return upsert(rec, Stamp::Saved, whenMs); +} + +bool CatalogIndex::upsert(TextureRecord& rec, Stamp stamp, qint64 whenMs) +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return false; + } + if (rec.path.isEmpty()) { + db.setError(QStringLiteral("cannot record a texture with no path")); + return false; + } + + if (rec.name.isEmpty()) + rec.name = QFileInfo(rec.path).completeBaseName(); + + if (stamp == Stamp::Opened) + rec.lastOpened = whenMs; + else + rec.lastSaved = whenMs; + + // created_at is preserved on conflict; every other column reflects what we + // just learned from the document. starred and missing_since are handled + // separately: starring is a user action this call knows nothing about, and + // touching a file necessarily means it isn't missing. + QSqlQuery query = db.prepare(QStringLiteral(R"( + INSERT INTO texture (path, name, file_size, file_mtime, + width, height, node_count, lib_version, channels, + created_at, last_opened, last_saved, starred, missing_since) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL) + ON CONFLICT(path) DO UPDATE SET + name = excluded.name, + file_size = excluded.file_size, + file_mtime = excluded.file_mtime, + width = excluded.width, + height = excluded.height, + node_count = excluded.node_count, + lib_version = excluded.lib_version, + channels = excluded.channels, + last_opened = max(coalesce(texture.last_opened, 0), + coalesce(excluded.last_opened, 0)), + last_saved = max(coalesce(texture.last_saved, 0), + coalesce(excluded.last_saved, 0)), + missing_since = NULL + )")); + + query.addBindValue(rec.path); + query.addBindValue(rec.name); + query.addBindValue(rec.fileSize); + query.addBindValue(rec.fileMtime); + query.addBindValue(rec.width); + query.addBindValue(rec.height); + query.addBindValue(rec.nodeCount); + query.addBindValue(rec.libVersion.isEmpty() ? QVariant() : QVariant(rec.libVersion)); + query.addBindValue(rec.channels); + query.addBindValue(rec.createdAt != 0 ? rec.createdAt : whenMs); + query.addBindValue(nullIfZero(rec.lastOpened)); + query.addBindValue(nullIfZero(rec.lastSaved)); + + if (!query.exec()) { + db.setError(query.lastError().text()); + qWarning("catalog: upsert failed for %s: %s", qPrintable(rec.path), + qPrintable(db.lastError())); + return false; + } + + // max() above can leave the in-memory record behind the stored row, and the + // id is unknown on the update path, so read back rather than guess. + const TextureRecord stored = byPath(rec.path); + if (!stored.isValid()) { + db.setError(QStringLiteral("row vanished immediately after upsert")); + return false; + } + rec = stored; + return true; +} + +bool CatalogIndex::setStarred(qint64 id, bool starred) +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return false; + } + + QSqlQuery query = db.prepare(QStringLiteral("UPDATE texture SET starred = ? WHERE id = ?")); + query.addBindValue(starred ? 1 : 0); + query.addBindValue(id); + if (!query.exec()) { + db.setError(query.lastError().text()); + return false; + } + return query.numRowsAffected() > 0; +} + +bool CatalogIndex::remove(qint64 id) +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return false; + } + + QSqlQuery query = db.prepare(QStringLiteral("DELETE FROM texture WHERE id = ?")); + query.addBindValue(id); + if (!query.exec()) { + db.setError(query.lastError().text()); + return false; + } + return query.numRowsAffected() > 0; +} + +int CatalogIndex::removeAllMissing() +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return 0; + } + + QSqlQuery query = db.prepare( + QStringLiteral("DELETE FROM texture WHERE missing_since IS NOT NULL")); + if (!query.exec()) { + db.setError(query.lastError().text()); + return 0; + } + return query.numRowsAffected(); +} + +bool CatalogIndex::clearRecents() +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return false; + } + + return db.exec(QStringLiteral("UPDATE texture SET last_opened = NULL")); +} + +// --- reconciliation ------------------------------------------------------- + +QVector CatalogIndex::allRecords() const +{ + QVector records; + + QSqlQuery query = const_cast(db).prepare( + QStringLiteral("SELECT %1 FROM texture").arg(QLatin1String(kColumns))); + if (!query.exec()) + return records; + + while (query.next()) + records.append(readRow(query)); + + return records; +} + +bool CatalogIndex::markMissing(qint64 id, qint64 whenMs) +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return false; + } + + // Only stamp the first time, so the dimmed card can say how long it's been + // gone rather than resetting on every launcher open. + QSqlQuery query = db.prepare(QStringLiteral( + "UPDATE texture SET missing_since = ? WHERE id = ? AND missing_since IS NULL")); + query.addBindValue(whenMs); + query.addBindValue(id); + if (!query.exec()) { + db.setError(query.lastError().text()); + return false; + } + return true; +} + +bool CatalogIndex::markPresent(qint64 id, qint64 fileSize, qint64 fileMtime) +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return false; + } + + QSqlQuery query = db.prepare(QStringLiteral( + "UPDATE texture SET missing_since = NULL, file_size = ?, file_mtime = ? WHERE id = ?")); + query.addBindValue(fileSize); + query.addBindValue(fileMtime); + query.addBindValue(id); + if (!query.exec()) { + db.setError(query.lastError().text()); + return false; + } + return true; +} + +qint64 CatalogIndex::relocate(qint64 id, const QString& newPath, qint64 fileSize, + qint64 fileMtime) +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return -1; + } + if (newPath.isEmpty()) + return -1; + + const TextureRecord moving = byId(id); + if (!moving.isValid()) + return -1; + + Transaction tx(db); + if (!tx.isActive()) + return -1; + + const TextureRecord existing = byPath(newPath); + + // Already indexed under the new path — usually because the user opened it + // there before getting round to fixing the stale card. Fold the old row's + // user data into it rather than failing on the UNIQUE constraint. + if (existing.isValid() && existing.id != id) { + QSqlQuery merge = db.prepare(QStringLiteral( + "UPDATE texture SET " + " starred = max(starred, ?), " + " created_at = min(created_at, ?), " + " last_opened = max(coalesce(last_opened, 0), ?), " + " last_saved = max(coalesce(last_saved, 0), ?), " + " missing_since = NULL " + "WHERE id = ?")); + merge.addBindValue(moving.starred ? 1 : 0); + merge.addBindValue(moving.createdAt != 0 ? moving.createdAt : existing.createdAt); + merge.addBindValue(moving.lastOpened); + merge.addBindValue(moving.lastSaved); + merge.addBindValue(existing.id); + if (!merge.exec()) { + db.setError(merge.lastError().text()); + return -1; + } + + QSqlQuery moveTags = db.prepare( + QStringLiteral("INSERT OR IGNORE INTO tag (texture_id, tag) " + "SELECT ?, tag FROM tag WHERE texture_id = ?")); + moveTags.addBindValue(existing.id); + moveTags.addBindValue(id); + if (!moveTags.exec()) { + db.setError(moveTags.lastError().text()); + return -1; + } + + QSqlQuery drop = db.prepare(QStringLiteral("DELETE FROM texture WHERE id = ?")); + drop.addBindValue(id); + if (!drop.exec()) { + db.setError(drop.lastError().text()); + return -1; + } + + return tx.commit() ? existing.id : -1; + } + + QSqlQuery query = db.prepare(QStringLiteral( + "UPDATE texture SET path = ?, name = ?, file_size = ?, file_mtime = ?, " + "missing_since = NULL WHERE id = ?")); + query.addBindValue(newPath); + query.addBindValue(QFileInfo(newPath).completeBaseName()); + query.addBindValue(fileSize); + query.addBindValue(fileMtime); + query.addBindValue(id); + + if (!query.exec()) { + db.setError(query.lastError().text()); + return -1; + } + + return tx.commit() ? id : -1; +} + +// --- reads ---------------------------------------------------------------- + +QString CatalogIndex::whereClause(const Query& query) +{ + QStringList clauses; + + switch (query.filter) { + case Filter::All: + break; + case Filter::Recents: + // Matches the ix_recent partial index. Recents is "jump back in", so a + // file that isn't there isn't useful; All and Starred still show it. + clauses << QStringLiteral("last_opened IS NOT NULL") + << QStringLiteral("missing_since IS NULL"); + break; + case Filter::Starred: + clauses << QStringLiteral("starred = 1"); + break; + } + + if (!query.search.isEmpty()) { + clauses << QStringLiteral( + R"((name LIKE :search ESCAPE '\' OR EXISTS ( + SELECT 1 FROM tag WHERE tag.texture_id = texture.id + AND tag.tag LIKE :search ESCAPE '\')))"); + } + + if (clauses.isEmpty()) + return QString(); + + return QStringLiteral(" WHERE ") + clauses.join(QStringLiteral(" AND ")); +} + +QString CatalogIndex::orderByClause(const Query& query) +{ + const QString direction = query.ascending ? QStringLiteral("ASC") : QStringLiteral("DESC"); + + switch (query.sort) { + case SortKey::Name: + return QStringLiteral(" ORDER BY name COLLATE NOCASE %1, id %1").arg(direction); + case SortKey::Size: + return QStringLiteral(" ORDER BY file_size %1, id %1").arg(direction); + case SortKey::Opened: + // Spelled out rather than using NULLS LAST so the ordering doesn't + // depend on which SQLite version Qt happens to bundle. Never-opened + // rows sort last either way. + return QStringLiteral(" ORDER BY (last_opened IS NULL) ASC, last_opened %1, id %1") + .arg(direction); + case SortKey::Modified: + break; + } + return QStringLiteral(" ORDER BY file_mtime %1, id %1").arg(direction); +} + +QVector CatalogIndex::list(const Query& query) const +{ + QVector records; + + QString sql = QStringLiteral("SELECT %1 FROM texture").arg(QLatin1String(kColumns)); + sql += whereClause(query); + sql += orderByClause(query); + + if (query.limit >= 0) + sql += QStringLiteral(" LIMIT :limit OFFSET :offset"); + + QSqlQuery q = const_cast(db).prepare(sql); + if (!query.search.isEmpty()) + q.bindValue(QStringLiteral(":search"), + QStringLiteral("%%%1%%").arg(escapeLike(query.search))); + if (query.limit >= 0) { + q.bindValue(QStringLiteral(":limit"), query.limit); + q.bindValue(QStringLiteral(":offset"), query.offset); + } + + if (!q.exec()) { + const_cast(db).setError(q.lastError().text()); + qWarning("catalog: list failed: %s", qPrintable(q.lastError().text())); + return records; + } + + while (q.next()) + records.append(readRow(q)); + + return records; +} + +int CatalogIndex::count(const Query& query) const +{ + QString sql = QStringLiteral("SELECT count(*) FROM texture"); + sql += whereClause(query); + + QSqlQuery q = const_cast(db).prepare(sql); + if (!query.search.isEmpty()) + q.bindValue(QStringLiteral(":search"), + QStringLiteral("%%%1%%").arg(escapeLike(query.search))); + + if (!q.exec() || !q.next()) + return 0; + + return q.value(0).toInt(); +} + +TextureRecord CatalogIndex::readRow(const QSqlQuery& query) +{ + TextureRecord rec; + rec.id = query.value(0).toLongLong(); + rec.path = query.value(1).toString(); + rec.name = query.value(2).toString(); + rec.fileSize = query.value(3).toLongLong(); + rec.fileMtime = query.value(4).toLongLong(); + rec.width = query.value(5).toInt(); + rec.height = query.value(6).toInt(); + rec.nodeCount = query.value(7).toInt(); + rec.libVersion = query.value(8).toString(); + rec.channels = query.value(9).toInt(); + rec.createdAt = query.value(10).toLongLong(); + rec.lastOpened = query.value(11).isNull() ? 0 : query.value(11).toLongLong(); + rec.lastSaved = query.value(12).isNull() ? 0 : query.value(12).toLongLong(); + rec.starred = query.value(13).toBool(); + rec.missingSince = query.value(14).isNull() ? 0 : query.value(14).toLongLong(); + return rec; +} + +TextureRecord CatalogIndex::byId(qint64 id) const +{ + QSqlQuery query = const_cast(db).prepare( + QStringLiteral("SELECT %1 FROM texture WHERE id = ?").arg(QLatin1String(kColumns))); + query.addBindValue(id); + + if (!query.exec() || !query.next()) + return TextureRecord(); + + return readRow(query); +} + +TextureRecord CatalogIndex::byPath(const QString& path) const +{ + QSqlQuery query = const_cast(db).prepare( + QStringLiteral("SELECT %1 FROM texture WHERE path = ?").arg(QLatin1String(kColumns))); + query.addBindValue(path); + + if (!query.exec() || !query.next()) + return TextureRecord(); + + return readRow(query); +} + +// --- tags ----------------------------------------------------------------- + +QStringList CatalogIndex::tags(qint64 id) const +{ + QStringList result; + + QSqlQuery query = const_cast(db).prepare( + QStringLiteral("SELECT tag FROM tag WHERE texture_id = ? ORDER BY tag")); + query.addBindValue(id); + + if (!query.exec()) + return result; + + while (query.next()) + result << query.value(0).toString(); + + return result; +} + +bool CatalogIndex::addTag(qint64 id, const QString& tag) +{ + if (readOnly || tag.isEmpty()) { + db.setError(QStringLiteral("index is read-only or tag is empty")); + return false; + } + + QSqlQuery query = db.prepare( + QStringLiteral("INSERT OR IGNORE INTO tag (texture_id, tag) VALUES (?, ?)")); + query.addBindValue(id); + query.addBindValue(tag); + if (!query.exec()) { + db.setError(query.lastError().text()); + return false; + } + return true; +} + +bool CatalogIndex::removeTag(qint64 id, const QString& tag) +{ + if (readOnly) { + db.setError(QStringLiteral("index is read-only")); + return false; + } + + QSqlQuery query = db.prepare( + QStringLiteral("DELETE FROM tag WHERE texture_id = ? AND tag = ?")); + query.addBindValue(id); + query.addBindValue(tag); + if (!query.exec()) { + db.setError(query.lastError().text()); + return false; + } + return query.numRowsAffected() > 0; +} + +} // namespace catalog diff --git a/src/catalog/catalogindex.h b/src/catalog/catalogindex.h new file mode 100644 index 00000000..8c343e0d --- /dev/null +++ b/src/catalog/catalogindex.h @@ -0,0 +1,131 @@ +#pragma once + +#include "database.h" +#include "texturerecord.h" + +#include +#include + +namespace catalog { + +// Repository over index.db — the durable record of every texture the app has +// created, opened, or saved. +// +// There is no filesystem scanner. Rows appear here only because the user did +// something, which means this file is the *only* record of what the launcher +// knows; losing it loses history even though every .texture file is still on +// disk. Two consequences are baked into this class: nothing is ever deleted +// implicitly (missing files are flagged, not removed), and a database written +// by a newer build is opened read-only rather than migrated speculatively. +class CatalogIndex { +public: + // Bump when the schema changes, and add a step to migrate(). + static constexpr int SchemaVersion = 1; + + CatalogIndex(); + ~CatalogIndex(); + + CatalogIndex(const CatalogIndex&) = delete; + CatalogIndex& operator=(const CatalogIndex&) = delete; + + // Creates the schema if the file is new, migrates it if it's older, and + // falls back to read-only if it's newer than this build understands. + bool open(const QString& path); + void close(); + + bool isOpen() const { return db.isOpen(); } + + // True when the file was written by a newer build. Every mutating call + // fails in this state; the launcher should still show what it can. + bool isReadOnly() const { return readOnly; } + + int schemaVersion() const { return currentVersion; } + QString lastError() const { return db.lastError(); } + Database& database() { return db; } + + // --- write points (see LAUNCHER_PRD.md §6.1) ------------------------- + + // Upserts by path and stamps last_opened. On success `rec.id` is filled in. + // + // Paths are identity here. A texture moved on disk becomes a new row at its + // new path, and the old one stays behind, flagged missing until the user + // removes it — no attempt is made to recognize the two as the same file. + // Detecting that needs a content hash or an mtime heuristic, and neither + // earns its keep for how often textures actually move. + bool recordOpened(TextureRecord& rec, qint64 whenMs); + + // Upserts by path and stamps last_saved. + bool recordSaved(TextureRecord& rec, qint64 whenMs); + + bool setStarred(qint64 id, bool starred); + + // Forgets a texture. Never touches the file on disk. + bool remove(qint64 id); + int removeAllMissing(); + + // Clears last_opened everywhere, emptying the Recents view. Keeps the rows, + // their stars, and their tags — "clear recents" is about history, not about + // discarding what the user has collected. + bool clearRecents(); + + // --- reconciliation (see LAUNCHER_PRD.md §6.2) ----------------------- + + // Every row, cheapest form, for the stat() pass on launcher open. + QVector allRecords() const; + + bool markMissing(qint64 id, qint64 whenMs); + + // Records that a file is present with this size/mtime, clearing any missing + // flag. Metadata is refreshed on next open, not here — this pass must not + // parse files. + // + // A caller that sees size or mtime differ from the stored row knows the + // file was edited outside the app, and should drop that texture's cached + // thumbnail before calling this. That comparison is the only change + // detection in the system. + bool markPresent(qint64 id, qint64 fileSize, qint64 fileMtime); + + // Re-points a row at a new path — the user found a file that had gone + // missing. This is the deliberate counterpart to not detecting moves + // automatically (§6.2): the launcher can't guess, but the user can tell it. + // + // If another row already occupies `newPath`, the two are merged: the + // survivor keeps that path and inherits stars, tags, and the earlier + // created_at, and the relocated row is deleted. Returns the surviving row + // id, or -1 on failure. + qint64 relocate(qint64 id, const QString& newPath, qint64 fileSize, qint64 fileMtime); + + // --- reads ------------------------------------------------------------ + + QVector list(const Query& query) const; + int count(const Query& query) const; + + TextureRecord byId(qint64 id) const; + TextureRecord byPath(const QString& path) const; + + // --- tags ------------------------------------------------------------- + + QStringList tags(qint64 id) const; + bool addTag(qint64 id, const QString& tag); + bool removeTag(qint64 id, const QString& tag); + +private: + enum class Stamp { Opened, Saved }; + + bool createSchema(); + bool migrate(int fromVersion); + int readSchemaVersion(); + bool writeSchemaVersion(int version); + + bool upsert(TextureRecord& rec, Stamp stamp, qint64 whenMs); + + static TextureRecord readRow(const class QSqlQuery& query); + static QString orderByClause(const Query& query); + static QString whereClause(const Query& query); + + Database db; + bool readOnly = false; + int currentVersion = 0; +}; + +} // namespace catalog diff --git a/src/catalog/database.cpp b/src/catalog/database.cpp new file mode 100644 index 00000000..0172c846 --- /dev/null +++ b/src/catalog/database.cpp @@ -0,0 +1,279 @@ +#include "database.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace catalog { + +namespace { + +// Qt keys connections by name in a process-wide registry, so every Database +// instance needs its own. The thread id is in the name purely to make a +// cross-thread misuse obvious in a debugger. +QString makeConnectionName() +{ + static QAtomicInteger counter(0); + return QStringLiteral("texturelab_catalog_%1_%2") + .arg(reinterpret_cast(QThread::currentThreadId()), 0, 16) + .arg(counter.fetchAndAddRelaxed(1)); +} + +bool isMemoryPath(const QString& path) +{ + return path == QLatin1String(":memory:") || path.startsWith(QLatin1String("file::memory:")); +} + +// Guards addDatabase/removeDatabase only. Qt's connection registry is a +// process-wide map, and the reconciliation pass opens its own connections from +// a worker thread while the GUI thread holds its own. Individual connections +// stay thread-confined; this just keeps two threads from mutating the registry +// at the same moment. +QMutex& registryMutex() +{ + static QMutex mutex; + return mutex; +} + +} // namespace + +Database::Database() = default; + +Database::~Database() +{ + close(); +} + +bool Database::open(const QString& path, const Options& options) +{ + close(); + + connectionName = makeConnectionName(); + dbPath = path; + + // Every QSqlDatabase copy must be destroyed before removeDatabase() runs, + // or Qt warns that the connection is still in use and leaves it registered. + // That includes the failure paths below, hence the scoping. + bool openFailed = false; + { + QMutexLocker lock(®istryMutex()); + QSqlDatabase conn = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connectionName); + conn.setDatabaseName(path); + + QStringList connectOptions; + connectOptions << QStringLiteral("QSQLITE_BUSY_TIMEOUT=%1").arg(options.busyTimeoutMs); + if (options.readOnly) + connectOptions << QStringLiteral("QSQLITE_OPEN_READONLY"); + conn.setConnectOptions(connectOptions.join(QLatin1Char(';'))); + + if (!conn.open()) { + errorText = conn.lastError().text(); + openFailed = true; + } + } + + if (openFailed) { + qWarning("catalog: could not open %s: %s", qPrintable(path), qPrintable(errorText)); + { + QMutexLocker lock(®istryMutex()); + QSqlDatabase::removeDatabase(connectionName); + } + connectionName.clear(); + return false; + } + + opened = true; + + if (!applyPragmas(options)) { + close(); + return false; + } + + return true; +} + +void Database::close() +{ + if (!connectionName.isEmpty()) { + if (transactionActive) + rollbackTransaction(); + { + QSqlDatabase db = QSqlDatabase::database(connectionName, false); + if (db.isValid() && db.isOpen()) + db.close(); + } + // The QSqlDatabase copy above must be out of scope before + // removeDatabase, or Qt warns about a connection still in use. + { + QMutexLocker lock(®istryMutex()); + QSqlDatabase::removeDatabase(connectionName); + } + connectionName.clear(); + } + opened = false; + transactionActive = false; + dbPath.clear(); +} + +bool Database::isOpen() const +{ + return opened && handle().isOpen(); +} + +QSqlDatabase Database::handle() const +{ + return QSqlDatabase::database(connectionName, false); +} + +bool Database::applyPragmas(const Options& options) +{ + const bool memory = isMemoryPath(dbPath); + + // Order matters and is not negotiable. page_size and auto_vacuum can only + // take effect on a database with no pages yet, and journal_mode=WAL writes + // a page — so both must precede it. Getting this backwards fails silently: + // the PRAGMA reports success and the setting simply doesn't apply. + if (!options.readOnly && !memory) { + if (options.pageSize > 0 + && !exec(QStringLiteral("PRAGMA page_size = %1").arg(options.pageSize))) + return false; + + if (options.incrementalAutoVacuum && !exec(QStringLiteral("PRAGMA auto_vacuum = INCREMENTAL"))) + return false; + + if (options.walMode) { + if (!exec(QStringLiteral("PRAGMA journal_mode = WAL"))) + return false; + if (!exec(QStringLiteral("PRAGMA synchronous = NORMAL"))) + return false; + } + } + + if (options.foreignKeys && !exec(QStringLiteral("PRAGMA foreign_keys = ON"))) + return false; + + return true; +} + +bool Database::exec(const QString& sql) +{ + QSqlQuery query(handle()); + if (!query.exec(sql)) { + errorText = query.lastError().text(); + qWarning("catalog: query failed: %s [%s]", qPrintable(errorText), qPrintable(sql)); + return false; + } + return true; +} + +bool Database::execBatch(const QStringList& statements) +{ + Transaction tx(*this); + if (!tx.isActive()) + return false; + + for (const QString& sql : statements) { + if (!exec(sql)) + return false; + } + + return tx.commit(); +} + +qint64 Database::scalar(const QString& sql, qint64 fallback) +{ + QSqlQuery query(handle()); + if (!query.exec(sql) || !query.next()) + return fallback; + + const QVariant value = query.value(0); + return value.isNull() ? fallback : value.toLongLong(); +} + +QSqlQuery Database::prepare(const QString& sql) +{ + QSqlQuery query(handle()); + if (!query.prepare(sql)) { + errorText = query.lastError().text(); + qWarning("catalog: prepare failed: %s [%s]", qPrintable(errorText), qPrintable(sql)); + } + return query; +} + +bool Database::beginTransaction() +{ + if (transactionActive) { + errorText = QStringLiteral("transaction already active"); + return false; + } + // BEGIN IMMEDIATE takes the write lock up front. The default deferred + // transaction takes it at the first write, which under WAL can fail with + // SQLITE_BUSY partway through a batch that already read data — the classic + // "upgrade deadlock" that busy_timeout cannot resolve. + if (!exec(QStringLiteral("BEGIN IMMEDIATE"))) + return false; + + transactionActive = true; + return true; +} + +bool Database::commitTransaction() +{ + if (!transactionActive) { + errorText = QStringLiteral("no transaction to commit"); + return false; + } + const bool ok = exec(QStringLiteral("COMMIT")); + transactionActive = false; + return ok; +} + +bool Database::rollbackTransaction() +{ + if (!transactionActive) + return false; + + const bool ok = exec(QStringLiteral("ROLLBACK")); + transactionActive = false; + return ok; +} + +Transaction::Transaction(Database& database) : db(&database) +{ + if (db->inTransaction()) { + qWarning("catalog: nested transaction requested; inner scope is inert"); + return; + } + active = db->beginTransaction(); +} + +Transaction::~Transaction() +{ + if (active) + rollback(); +} + +bool Transaction::commit() +{ + if (!active) + return false; + + active = false; + return db->commitTransaction(); +} + +void Transaction::rollback() +{ + if (!active) + return; + + active = false; + db->rollbackTransaction(); +} + +} // namespace catalog diff --git a/src/catalog/database.h b/src/catalog/database.h new file mode 100644 index 00000000..d760bcb5 --- /dev/null +++ b/src/catalog/database.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include + +class QSqlQuery; + +namespace catalog { + +// A single SQLite connection, opened through Qt's bundled QSQLITE driver. +// +// One connection per thread, never shared: QSqlDatabase handles are tied to the +// thread that opened them, and WAL only makes concurrent *connections* safe, not +// concurrent use of one handle. Each Database instance registers its own +// uniquely-named Qt connection, so several can coexist over the same file. +class Database { +public: + struct Options { + // Applied before any other PRAGMA and before any DDL. SQLite can only + // honor these on an empty database file: page_size otherwise needs a + // full VACUUM to take effect, and auto_vacuum cannot be raised from + // NONE at all without one. 0 leaves the driver default in place. + int pageSize = 0; + bool incrementalAutoVacuum = false; + + bool walMode = true; + bool foreignKeys = true; + int busyTimeoutMs = 5000; + bool readOnly = false; + }; + + Database(); + ~Database(); + + Database(const Database&) = delete; + Database& operator=(const Database&) = delete; + + // `path` may be ":memory:" for a private in-memory database, which is what + // the tests use. Options that only apply to a file (page_size, WAL) are + // skipped in that case rather than failing. + bool open(const QString& path, const Options& options); + + // Spelled as an overload rather than a defaulted argument: a default + // argument of `Options()` would need the nested struct's member + // initializers before the enclosing class is complete. + bool open(const QString& path) { return open(path, Options()); } + void close(); + + bool isOpen() const; + QString path() const { return dbPath; } + QSqlDatabase handle() const; + + // Runs a statement that takes no parameters and returns no rows. On failure + // the driver's message is recorded in lastError() and logged. + bool exec(const QString& sql); + + // Runs several statements in one transaction, stopping at the first + // failure. Used for schema creation. + bool execBatch(const QStringList& statements); + + // Single-value query; returns `fallback` on any failure or empty result. + qint64 scalar(const QString& sql, qint64 fallback = 0); + + // Prepared statement bound to this connection. Always check isValid() on + // the returned query — a prepare failure is reported here, not at exec(). + QSqlQuery prepare(const QString& sql); + + bool beginTransaction(); + bool commitTransaction(); + bool rollbackTransaction(); + bool inTransaction() const { return transactionActive; } + + QString lastError() const { return errorText; } + void setError(const QString& text) { errorText = text; } + +private: + bool applyPragmas(const Options& options); + + QString connectionName; + QString dbPath; + QString errorText; + bool opened = false; + bool transactionActive = false; +}; + +// RAII transaction. Rolls back on destruction unless commit() succeeded, so an +// early return or a failed step can never leave a half-written batch behind. +// +// Does not nest: constructing one while another is active is a programming +// error, and the inner instance becomes inert rather than committing the outer +// transaction out from under it. +class Transaction { +public: + explicit Transaction(Database& db); + ~Transaction(); + + Transaction(const Transaction&) = delete; + Transaction& operator=(const Transaction&) = delete; + + bool isActive() const { return active; } + bool commit(); + void rollback(); + +private: + Database* db = nullptr; + bool active = false; +}; + +} // namespace catalog diff --git a/src/catalog/texturerecord.h b/src/catalog/texturerecord.h new file mode 100644 index 00000000..bcf360ad --- /dev/null +++ b/src/catalog/texturerecord.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include + +namespace catalog { + +// Output channel bits, mirroring TextureChannel in src/texturelab/models.h. +// +// Deliberately redeclared instead of including models.h: the catalog library +// must not depend on the app's node graph, so that it stays testable on its own +// and so that a change to the graph model can't quietly alter what's already +// stored in the index. The mapping between the two lives at the app boundary +// (Phase 2), and the ordinals below must not be renumbered — they're persisted. +enum ChannelBit : int { + ChannelNone = 0, + ChannelAlbedo = 1 << 1, + ChannelNormal = 1 << 2, + ChannelMetalness = 1 << 3, + ChannelRoughness = 1 << 4, + ChannelHeight = 1 << 5, + ChannelAlpha = 1 << 6, + ChannelAO = 1 << 7, +}; + +// One row of the texture table. +// +// Timestamps are Unix milliseconds. 0 means "unset" and is written to the +// database as NULL — the partial index on recents and every `IS NULL` filter +// depend on that distinction, so don't start storing a real 0. +// +// There is deliberately no content hash. Change detection is (file_size, +// file_mtime) from stat(), which is what actually decides whether a file was +// edited outside the app; a hash would only have been a cache key, and the +// cache keys on `id` instead. +struct TextureRecord { + qint64 id = -1; + QString path; + QString name; + qint64 fileSize = 0; + qint64 fileMtime = 0; + int width = 0; + int height = 0; + int nodeCount = 0; + QString libVersion; + int channels = ChannelNone; + qint64 createdAt = 0; + qint64 lastOpened = 0; + qint64 lastSaved = 0; + bool starred = false; + qint64 missingSince = 0; + + bool isValid() const { return id >= 0; } + bool isMissing() const { return missingSince != 0; } + bool hasChannel(ChannelBit bit) const { return (channels & bit) != 0; } +}; + +// Which set of rows a query covers. +enum class Filter { + All, // everything, including missing files (they render dimmed) + Recents, // opened at least once, excluding missing — "jump back in" + Starred, // starred, including missing +}; + +enum class SortKey { + Modified, // file_mtime — what the card's relative time shows + Opened, // last_opened + Name, + Size, +}; + +struct Query { + Filter filter = Filter::All; + SortKey sort = SortKey::Modified; + bool ascending = false; + + // Case-insensitive substring match against name and tags. Empty disables + // the filter entirely rather than matching everything, so the common path + // doesn't pay for the tag subquery. + QString search; + + // limit < 0 means unbounded. The grid pages, so it normally doesn't. + int limit = -1; + int offset = 0; +}; + +} // namespace catalog diff --git a/src/catalog/thumbnailcache.cpp b/src/catalog/thumbnailcache.cpp new file mode 100644 index 00000000..b62a7b1b --- /dev/null +++ b/src/catalog/thumbnailcache.cpp @@ -0,0 +1,328 @@ +#include "thumbnailcache.h" + +#include +#include +#include +#include +#include + +namespace catalog { + +namespace { + +const char* sourceToText(ThumbSource source) +{ + return source == ThumbSource::Save ? "save" : "open"; +} + +Database::Options cacheOptions() +{ + Database::Options options; + // 8 KiB pages suit rows that are mostly a JPEG blob — fewer overflow pages + // per image than the 4 KiB default. Both this and auto_vacuum only take + // effect on an empty file, which is why the cache is recreated rather than + // migrated when anything is wrong with it. + options.pageSize = 8192; + options.incrementalAutoVacuum = true; + options.walMode = true; + options.foreignKeys = false; + return options; +} + +} // namespace + +ThumbnailCache::ThumbnailCache() = default; + +ThumbnailCache::~ThumbnailCache() +{ + close(); +} + +bool ThumbnailCache::open(const QString& path) +{ + close(); + + if (!db.open(path, cacheOptions())) + return recreate(path); + + const bool fresh = db.scalar( + QStringLiteral("SELECT count(*) FROM sqlite_master WHERE type='table'")) + == 0; + + if (fresh) + return createSchema(); + + if (readSchemaVersion() != SchemaVersion) { + qInfo("catalog: thumbnail cache schema mismatch; rebuilding %s", qPrintable(path)); + return recreate(path); + } + + return true; +} + +bool ThumbnailCache::recreate(const QString& path) +{ + db.close(); + + // A cache has no history worth saving, so anything unexpected — a corrupt + // file, an unreadable one, a schema from another build — is resolved by + // starting over. Delete the WAL sidecars too, or SQLite will try to replay + // them into the new file. + QFile::remove(path); + QFile::remove(path + QStringLiteral("-wal")); + QFile::remove(path + QStringLiteral("-shm")); + + if (!db.open(path, cacheOptions())) + return false; + + return createSchema(); +} + +bool ThumbnailCache::createSchema() +{ + QStringList statements; + + statements << QStringLiteral(R"( + CREATE TABLE thumb ( + texture_id INTEGER NOT NULL, + mesh TEXT NOT NULL DEFAULT 'default', + hdri TEXT NOT NULL DEFAULT 'default', + size INTEGER NOT NULL, + format TEXT NOT NULL DEFAULT 'jpg', + source TEXT NOT NULL, + bytes BLOB NOT NULL, + created_at INTEGER NOT NULL, + last_used INTEGER NOT NULL, + PRIMARY KEY (texture_id, mesh, hdri, size) + ) WITHOUT ROWID + )"); + + statements << QStringLiteral("CREATE INDEX ix_evict ON thumb(last_used)"); + statements << QStringLiteral("CREATE TABLE meta (k TEXT PRIMARY KEY, v TEXT)"); + statements << QStringLiteral("INSERT INTO meta (k, v) VALUES ('schema_version', '%1')") + .arg(SchemaVersion); + + return db.execBatch(statements); +} + +int ThumbnailCache::readSchemaVersion() +{ + QSqlQuery query = db.prepare(QStringLiteral("SELECT v FROM meta WHERE k = 'schema_version'")); + if (!query.exec() || !query.next()) + return 0; + + return query.value(0).toInt(); +} + +void ThumbnailCache::close() +{ + db.close(); +} + +bool ThumbnailCache::put(const ThumbKey& key, const QByteArray& bytes, ThumbSource source, + qint64 whenMs) +{ + Entry entry; + entry.key = key; + entry.bytes = bytes; + entry.source = source; + return putBatch({entry}, whenMs); +} + +bool ThumbnailCache::putBatch(const QVector& entries, qint64 whenMs) +{ + if (entries.isEmpty()) + return true; + + Transaction tx(db); + if (!tx.isActive()) + return false; + + for (const Entry& entry : entries) { + if (!entry.key.isValid() || entry.bytes.isEmpty()) { + db.setError(QStringLiteral("refusing to cache an empty or unkeyed thumbnail")); + return false; + } + + // created_at is preserved on conflict so the row keeps its original + // provenance; last_used moves forward because we just produced it. + QSqlQuery query = db.prepare(QStringLiteral(R"( + INSERT INTO thumb (texture_id, mesh, hdri, size, format, source, + bytes, created_at, last_used) + VALUES (?, ?, ?, ?, 'jpg', ?, ?, ?, ?) + ON CONFLICT(texture_id, mesh, hdri, size) DO UPDATE SET + source = excluded.source, + bytes = excluded.bytes, + last_used = excluded.last_used + )")); + + query.addBindValue(entry.key.textureId); + query.addBindValue(entry.key.mesh); + query.addBindValue(entry.key.hdri); + query.addBindValue(entry.key.size); + query.addBindValue(QString::fromLatin1(sourceToText(entry.source))); + query.addBindValue(entry.bytes); + query.addBindValue(whenMs); + query.addBindValue(whenMs); + + if (!query.exec()) { + db.setError(query.lastError().text()); + qWarning("catalog: thumbnail write failed: %s", qPrintable(db.lastError())); + return false; + } + } + + return tx.commit(); +} + +QByteArray ThumbnailCache::get(const ThumbKey& key) const +{ + if (!key.isValid()) + return QByteArray(); + + QSqlQuery query = const_cast(db).prepare(QStringLiteral( + "SELECT bytes FROM thumb WHERE texture_id = ? AND mesh = ? AND hdri = ? AND size = ?")); + query.addBindValue(key.textureId); + query.addBindValue(key.mesh); + query.addBindValue(key.hdri); + query.addBindValue(key.size); + + if (!query.exec() || !query.next()) + return QByteArray(); + + return query.value(0).toByteArray(); +} + +bool ThumbnailCache::contains(const ThumbKey& key) const +{ + if (!key.isValid()) + return false; + + QSqlQuery query = const_cast(db).prepare(QStringLiteral( + "SELECT 1 FROM thumb WHERE texture_id = ? AND mesh = ? AND hdri = ? AND size = ?")); + query.addBindValue(key.textureId); + query.addBindValue(key.mesh); + query.addBindValue(key.hdri); + query.addBindValue(key.size); + + return query.exec() && query.next(); +} + +bool ThumbnailCache::touch(const QVector& keys, qint64 whenMs) +{ + if (keys.isEmpty()) + return true; + + Transaction tx(db); + if (!tx.isActive()) + return false; + + for (const ThumbKey& key : keys) { + QSqlQuery query = db.prepare(QStringLiteral( + "UPDATE thumb SET last_used = ? " + "WHERE texture_id = ? AND mesh = ? AND hdri = ? AND size = ?")); + query.addBindValue(whenMs); + query.addBindValue(key.textureId); + query.addBindValue(key.mesh); + query.addBindValue(key.hdri); + query.addBindValue(key.size); + + if (!query.exec()) { + db.setError(query.lastError().text()); + return false; + } + } + + return tx.commit(); +} + +int ThumbnailCache::removeTexture(qint64 textureId) +{ + if (textureId < 0) + return 0; + + QSqlQuery query = db.prepare(QStringLiteral("DELETE FROM thumb WHERE texture_id = ?")); + query.addBindValue(textureId); + + if (!query.exec()) { + db.setError(query.lastError().text()); + return 0; + } + return query.numRowsAffected(); +} + +qint64 ThumbnailCache::totalBytes() const +{ + return const_cast(db).scalar( + QStringLiteral("SELECT coalesce(sum(length(bytes)), 0) FROM thumb")); +} + +int ThumbnailCache::rowCount() const +{ + return static_cast( + const_cast(db).scalar(QStringLiteral("SELECT count(*) FROM thumb"))); +} + +int ThumbnailCache::evictTo(qint64 budgetBytes) +{ + qint64 total = totalBytes(); + if (total <= budgetBytes) + return 0; + + // Walk oldest-first, deleting until we're under budget. Done in one + // transaction so a crash mid-eviction can't leave a partially reaped cache + // — not that it would matter much, but the free-page accounting below + // assumes the deletes actually landed. + QVector doomed; + qint64 freed = 0; + + { + QSqlQuery scan = db.prepare(QStringLiteral( + "SELECT texture_id, mesh, hdri, size, length(bytes) FROM thumb " + "ORDER BY last_used ASC")); + if (!scan.exec()) { + db.setError(scan.lastError().text()); + return 0; + } + + while (scan.next() && (total - freed) > budgetBytes) { + doomed << scan.value(0) << scan.value(1) << scan.value(2) << scan.value(3); + freed += scan.value(4).toLongLong(); + } + } + + if (doomed.isEmpty()) + return 0; + + Transaction tx(db); + if (!tx.isActive()) + return 0; + + int deleted = 0; + for (int i = 0; i + 3 < doomed.size(); i += 4) { + QSqlQuery query = db.prepare(QStringLiteral( + "DELETE FROM thumb WHERE texture_id = ? AND mesh = ? AND hdri = ? AND size = ?")); + query.addBindValue(doomed[i]); + query.addBindValue(doomed[i + 1]); + query.addBindValue(doomed[i + 2]); + query.addBindValue(doomed[i + 3]); + + if (!query.exec()) { + db.setError(query.lastError().text()); + return 0; + } + deleted += query.numRowsAffected(); + } + + if (!tx.commit()) + return 0; + + // Hand the freed pages back to the filesystem. Incremental rather than a + // full VACUUM so this stays bounded; auto_vacuum was set to INCREMENTAL at + // creation precisely so this call works at all. + db.exec(QStringLiteral("PRAGMA incremental_vacuum")); + + return deleted; +} + +} // namespace catalog diff --git a/src/catalog/thumbnailcache.h b/src/catalog/thumbnailcache.h new file mode 100644 index 00000000..c11814bb --- /dev/null +++ b/src/catalog/thumbnailcache.h @@ -0,0 +1,109 @@ +#pragma once + +#include "database.h" + +#include +#include +#include + +namespace catalog { + +// Identifies one cached image. +// +// Keyed by the index's texture id. An earlier draft keyed on a hash of the +// file's contents so that copies shared a thumbnail and edits invalidated +// themselves — but change detection turned out to be (file_size, file_mtime) +// from stat() either way, and the hash was only ever the key. Dropping it +// removed a dependency and a column for the price of one extra render the +// first time you open a copied file. +// +// The consequence is that this cache is coupled to index.db's row ids: delete +// the index and these rows are orphaned. That's acceptable precisely because +// the cache is disposable — clear it alongside. +struct ThumbKey { + qint64 textureId = -1; + QString mesh = QStringLiteral("default"); + QString hdri = QStringLiteral("default"); + int size = 256; + + bool isValid() const { return textureId >= 0 && size > 0; } +}; + +// Where a cached image came from. Lets a better capture supersede a cheaper one +// instead of the cache locking in whatever was written first. +enum class ThumbSource { + Save, // captured from the 3D viewport at save time — the good one + Open, // captured on open, for a file indexed before this feature existed +}; + +// Repository over thumbs.db — a pure cache. +// +// Deleting this file at any time must be harmless: it is rebuilt as the user +// saves and opens textures. That's why it lives apart from index.db, which +// cannot be rebuilt at all (see LAUNCHER_PRD.md §1.1), and why a schema +// mismatch here is handled by deleting the file rather than migrating it. +class ThumbnailCache { +public: + static constexpr int SchemaVersion = 1; + + // Roughly one 512px and one 256px JPEG per texture, so this is generous. + static constexpr qint64 DefaultBudgetBytes = 512LL * 1024 * 1024; + + ThumbnailCache(); + ~ThumbnailCache(); + + ThumbnailCache(const ThumbnailCache&) = delete; + ThumbnailCache& operator=(const ThumbnailCache&) = delete; + + // Recreates the file from scratch if it's missing, corrupt, or written to a + // different schema version. Only returns false if even that fails. + bool open(const QString& path); + void close(); + + bool isOpen() const { return db.isOpen(); } + QString lastError() const { return db.lastError(); } + + bool put(const ThumbKey& key, const QByteArray& bytes, ThumbSource source, qint64 whenMs); + + // Writes many images in one transaction. One transaction per thumbnail + // means one fsync per thumbnail, which is what makes a bulk write crawl. + struct Entry { + ThumbKey key; + QByteArray bytes; + ThumbSource source = ThumbSource::Save; + }; + bool putBatch(const QVector& entries, qint64 whenMs); + + // Returns an empty QByteArray on a miss. Deliberately does not update + // last_used: this runs during scroll, and a write per painted card is + // exactly what the "no disk writes on the GUI thread" rule forbids. Call + // touch() later with what was actually used. + QByteArray get(const ThumbKey& key) const; + + bool contains(const ThumbKey& key) const; + + // Batched last_used bookkeeping, flushed on idle or close. Day-granularity + // timestamps are plenty for an LRU whose eviction budget is half a gigabyte. + bool touch(const QVector& keys, qint64 whenMs); + + // Drops every variant of one texture. Called when reconciliation sees a + // file's size or mtime change — the cached image no longer shows what's on + // disk — and when a texture is removed from the launcher. + int removeTexture(qint64 textureId); + + qint64 totalBytes() const; + int rowCount() const; + + // Deletes least-recently-used rows until the cache fits in `budgetBytes`, + // then returns pages to the filesystem. Returns the number of rows deleted. + int evictTo(qint64 budgetBytes = DefaultBudgetBytes); + +private: + bool createSchema(); + bool recreate(const QString& path); + int readSchemaVersion(); + + Database db; +}; + +} // namespace catalog diff --git a/src/colorpicker/CMakeLists.txt b/src/colorpicker/CMakeLists.txt new file mode 100644 index 00000000..5b240291 --- /dev/null +++ b/src/colorpicker/CMakeLists.txt @@ -0,0 +1,113 @@ + +cmake_minimum_required(VERSION 3.10) + + +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets REQUIRED) +if (UNIX AND NOT APPLE) + include_directories(${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS}) +endif() + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(SRCS + colorpicker.cpp + gradient.cpp + gradientpicker.cpp + widgets.cpp +) +set(HEADERS + colorpicker.h + gradient.h + gradientpicker.h + widgets.h +) + +# library +add_library(colorpicker STATIC ${SRCS} ${HEADERS}) +target_link_libraries(colorpicker PRIVATE Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) + + +set_target_properties(colorpicker PROPERTIES + AUTOMOC ON + AUTORCC ON + CXX_EXTENSIONS OFF + EXPORT_NAME "Color Picker" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +if(QT_VERSION_MAJOR STREQUAL "5") + set_target_properties(colorpicker PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON) +elseif(QT_VERSION_MAJOR STREQUAL "6") + set_target_properties(colorpicker PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) +endif() + +# EXE for testing purposes +add_executable(colorpicker_app + ${HEADERS} + ${SRCS} + main.cpp + ) +target_link_libraries(colorpicker_app PRIVATE Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) + + +set_target_properties(colorpicker_app PROPERTIES + AUTOMOC ON + AUTORCC ON + CXX_EXTENSIONS OFF + EXPORT_NAME "Color Picker App" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +if(QT_VERSION_MAJOR STREQUAL "5") + set_target_properties(colorpicker_app PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON) +elseif(QT_VERSION_MAJOR STREQUAL "6") + set_target_properties(colorpicker_app PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) +endif() + +# EXE for testing purposes +add_executable(gradientpicker_app + ${HEADERS} + ${SRCS} + gradient_app.cpp + ) +target_link_libraries(gradientpicker_app PRIVATE Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) + + +set_target_properties(gradientpicker_app PROPERTIES + AUTOMOC ON + AUTORCC ON + CXX_EXTENSIONS OFF + EXPORT_NAME "Gradient Example App" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +if(QT_VERSION_MAJOR STREQUAL "5") + set_target_properties(gradientpicker_app PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON) +elseif(QT_VERSION_MAJOR STREQUAL "6") + set_target_properties(gradientpicker_app PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) +endif() \ No newline at end of file diff --git a/src/colorpicker/colorpicker.cpp b/src/colorpicker/colorpicker.cpp new file mode 100644 index 00000000..07da5c23 --- /dev/null +++ b/src/colorpicker/colorpicker.cpp @@ -0,0 +1,124 @@ +#include "colorpicker.h" +#include "./widgets.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ColorPicker::ColorPicker() +{ + // Frameless tool window instead of Qt::Popup: Qt::Popup does an X11 + // keyboard/pointer grab to detect outside clicks, which also blocks + // global WM shortcuts (e.g. PrintScreen) while it's open. Outside + // clicks are instead detected manually via the app-wide event filter + // below, which doesn't require any grab. + setWindowFlags(Qt::Tool | Qt::FramelessWindowHint + | Qt::WindowStaysOnTopHint); + qApp->installEventFilter(this); + + svBox = new SVBox(); + hueSlider = new HueSlider(); + alphaSlider = new AlphaSlider(); + + svBox->setColor(QColor(255, 150, 0, 255)); + originalColor = QColor(255, 150, 0, 255); + alphaSlider->setColor(originalColor); + + // connect widget signals to emit color changes + connect(svBox, &SVBox::onSVChanged, this, [this](float s, float v) { + QColor color = currentColor(); + alphaSlider->setColor(color); + emit onColorChanged(color); + }); + connect(hueSlider, &HueSlider::onHueChanged, this, [this](float h) { + QColor color = currentColor(); + color.setHsvF(h, color.saturationF(), color.valueF(), color.alphaF()); + svBox->setColor(color); + alphaSlider->setColor(color); + emit onColorChanged(color); + }); + connect(alphaSlider, &AlphaSlider::onAlphaChanged, this, + [this](float a) { emit onColorChanged(currentColor()); }); + + // add layout + auto vlayout = new QVBoxLayout(this); + vlayout->addWidget(svBox); + vlayout->addWidget(hueSlider); + vlayout->addWidget(alphaSlider); + + this->setLayout(vlayout); + + // this->setBaseSize(400, 500); + this->resize(400, 300); +} + +void ColorPicker::setColor(const QColor& color) +{ + originalColor = color; + svBox->setColor(color); + hueSlider->setColor(color); + alphaSlider->setColor(color); +} + +QColor ColorPicker::currentColor() const +{ + // SVBox tracks hue/sat/value only; alpha lives on the alpha slider + QColor color = svBox->getColor(); + color.setAlphaF(alphaSlider->getAlpha()); + return color; +} + +void ColorPicker::cancel() +{ + // revert to the color the dialog was opened with + svBox->setColor(originalColor); + hueSlider->setColor(originalColor); + alphaSlider->setColor(originalColor); + emit onColorChanged(originalColor); + reject(); +} + +void ColorPicker::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Escape) { + cancel(); + return; + } + + event->ignore(); + + // QDialog::keyPressEvent(event); +} + +void ColorPicker::hideEvent(QHideEvent* event) +{ + QDialog::hideEvent(event); + emit onClosed(); +} + +void ColorPicker::showEvent(QShowEvent* event) +{ + QDialog::showEvent(event); + // Tool windows aren't always given keyboard focus by the window + // manager on their own, unlike Qt::Popup; claim it explicitly so + // Escape reaches us. + raise(); + activateWindow(); +} + +bool ColorPicker::eventFilter(QObject* watched, QEvent* event) +{ + if (event->type() == QEvent::MouseButtonPress) { + auto widget = qobject_cast(watched); + if (widget && widget != this && !this->isAncestorOf(widget)) { + close(); + } + } + + return QDialog::eventFilter(watched, event); +} diff --git a/src/colorpicker/colorpicker.h b/src/colorpicker/colorpicker.h new file mode 100644 index 00000000..0e6f4691 --- /dev/null +++ b/src/colorpicker/colorpicker.h @@ -0,0 +1,45 @@ +#pragma once +#include + +class SVBox; +class HueSlider; +class AlphaSlider; +class QKeyEvent; +class QHideEvent; +class QShowEvent; +class QPainter; +class QRect; + +// Fills rect with a light gray/white checkerboard, used behind colors that +// have transparency. +void drawCheckerboard(QPainter& painter, const QRect& rect, int cellSize = 6); + +class ColorPicker : public QDialog { + Q_OBJECT +public: + ColorPicker(); + + void setColor(const QColor& color); + +signals: + void onColorChanged(const QColor& color); + void onClosed(); + +protected: + void keyPressEvent(QKeyEvent* event) override; + void hideEvent(QHideEvent* event) override; + void showEvent(QShowEvent* event) override; + bool eventFilter(QObject* watched, QEvent* event) override; + +private: + void initUI(); + void colorChangedByEditor(QColor color); + void colorChangedByUI(QColor color); + void cancel(); + QColor currentColor() const; + + SVBox* svBox; + HueSlider* hueSlider; + AlphaSlider* alphaSlider; + QColor originalColor; +}; \ No newline at end of file diff --git a/src/colorpicker/gradient.cpp b/src/colorpicker/gradient.cpp new file mode 100644 index 00000000..a5f2ac77 --- /dev/null +++ b/src/colorpicker/gradient.cpp @@ -0,0 +1,64 @@ +#include "gradient.h" + +void Gradient::sort() +{ + std::sort(points.begin(), points.end(), + [](const GradientPoint& a, const GradientPoint& b) { + return a.position < b.position; + }); +} + +QColor Gradient::sample(float position) const +{ + // Default color is black + QColor color = Qt::black; + + if (points.size() == 0) { + return color; + } + + if (points.size() == 1) { + return points[0].color; + } + + // Find surrounding points + int leftIdx = -1, rightIdx = -1; + for (int i = 0; i < points.size(); ++i) { + if (points[i].position <= position) { + leftIdx = i; + } + if (points[i].position >= position && rightIdx == -1) { + rightIdx = i; + break; + } + } + + if (leftIdx >= 0 && rightIdx >= 0 && leftIdx != rightIdx) { + // Interpolate between the two colors + float t = (position - points[leftIdx].position) / + (points[rightIdx].position - points[leftIdx].position); + QColor leftColor = points[leftIdx].color; + QColor rightColor = points[rightIdx].color; + + color = QColor( + leftColor.red() + t * (rightColor.red() - leftColor.red()), + leftColor.green() + t * (rightColor.green() - leftColor.green()), + leftColor.blue() + t * (rightColor.blue() - leftColor.blue())); + } + else if (leftIdx >= 0) { + color = points[leftIdx].color; + } + else if (rightIdx >= 0) { + color = points[rightIdx].color; + } + + return color; +} + +Gradient Gradient::defaultGradient() +{ + Gradient gradient; + gradient.addPoint(GradientPoint(0.0, QColor(0, 0, 0, 255))); + gradient.addPoint(GradientPoint(1.0, QColor(255, 255, 255, 255))); + return gradient; +} \ No newline at end of file diff --git a/src/colorpicker/gradient.h b/src/colorpicker/gradient.h new file mode 100644 index 00000000..9dd39b25 --- /dev/null +++ b/src/colorpicker/gradient.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +class GradientPoint { +public: + float position; + QColor color; + + GradientPoint() : position(0.0f), color(Qt::black) {} + GradientPoint(float position, const QColor& color) + : position(position), color(color) + { + } +}; + +class Gradient { +public: + QVector points; + + Gradient() {} + Gradient(const QVector& points) : points(points) {} + Gradient(const Gradient& other) : points(other.points) {} + + void addPoint(GradientPoint point) { points.append(point); } + void sort(); + QColor sample(float position) const; + + static Gradient defaultGradient(); +}; + +Q_DECLARE_METATYPE(Gradient) +Q_DECLARE_METATYPE(GradientPoint) \ No newline at end of file diff --git a/src/colorpicker/gradient_app.cpp b/src/colorpicker/gradient_app.cpp new file mode 100644 index 00000000..fffa4212 --- /dev/null +++ b/src/colorpicker/gradient_app.cpp @@ -0,0 +1,14 @@ +#include "gradientpicker.h" +#include + +int main(int argc, char* argv[]) +{ + QApplication a(argc, argv); + GradientPickerDialog dialog; + + // dialog.resize(800, 600); + // dialog.setColor(QColor(50, 125, 125)); + dialog.show(); + + return a.exec(); +} diff --git a/src/colorpicker/gradientpicker.cpp b/src/colorpicker/gradientpicker.cpp new file mode 100644 index 00000000..20550da0 --- /dev/null +++ b/src/colorpicker/gradientpicker.cpp @@ -0,0 +1,432 @@ +#include "gradientpicker.h" +#include "gradient.h" +#include "widgets.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// GradientControlPoint implementation +GradientControlPoint::GradientControlPoint(int index, qreal x, qreal y, + qreal size, const QColor& color) + : QGraphicsObject(), index(index), size(size), color(color) +{ + setPos(x, y); + setAcceptedMouseButtons(Qt::NoButton); + // setCacheMode(QGraphicsItem::NoCache); +} + +QRectF GradientControlPoint::boundingRect() const +{ + return QRectF(0, 0, size, size); +} + +void GradientControlPoint::paint(QPainter* painter, + const QStyleOptionGraphicsItem* option, + QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + // Draw upward-pointing triangle + qreal centerX = size / 2.0; + qreal triangleHeight = size * 0.5; + qreal triangleWidth = size; + + QPolygonF triangle; + triangle << QPointF(centerX, -3) // Top point + << QPointF(centerX - triangleWidth / 2, + triangleHeight) // Bottom left + << QPointF(centerX + triangleWidth / 2, + triangleHeight); // Bottom right + + painter->setBrush(QBrush(Qt::black)); + painter->setPen(Qt::NoPen); + painter->drawPolygon(triangle); + + painter->setBrush(QBrush(color)); + painter->setPen(QPen(Qt::black, 2)); + painter->drawEllipse(boundingRect()); +} + +// GradientSlider implementation +GradientSlider::GradientSlider() : QGraphicsView() { this->initUI(); } + +void GradientSlider::setGradient(const Gradient& gradient) +{ + this->gradient = gradient; + updateGradientDisplay(); + selectFirstPoint(); +} + +void GradientSlider::setPointColor(const QColor& color) +{ + if (selectedPointIndex >= 0 && + selectedPointIndex < gradient.points.size() && + selectedPointIndex < controlPoints.size()) { + gradient.points[selectedPointIndex].color = color; + + // Update control point color + controlPoints[selectedPointIndex]->color = color; + controlPoints[selectedPointIndex]->update(); + + // Update gradient display + QLinearGradient linearGrad(10, 0, sliderWidth + 10, 0); + for (const auto& point : gradient.points) { + linearGrad.setColorAt(point.position, point.color); + } + gradientRect->setBrush(QBrush(linearGrad)); + + emitGradientUpdate(); + } +} + +void GradientSlider::selectFirstPoint() +{ + if (gradient.points.size() > 0) { + selectedPointIndex = 0; + emit onActivePointChanged(0, gradient.points[0].color); + } +} + +void GradientSlider::initUI() +{ + setMinimumSize(200, 60); + setFixedHeight(60); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + scene = new QGraphicsScene(this); + setScene(scene); + + // Update slider width based on initial size + sliderWidth = qMax(200.0, width() - 40.0); + scene->setSceneRect(0, 0, width() - 20, 40); + + // Create gradient display rectangle + gradientRect = + new QGraphicsRectItem(10, sliderY, sliderWidth, sliderHeight); + gradientRect->setPen(QPen(Qt::black, 1)); + scene->addItem(gradientRect); + + // Initialize with default gradient + gradient = Gradient::defaultGradient(); + updateGradientDisplay(); + // selectFirstPoint(); +} + +void GradientSlider::updateGradientDisplay() +{ + // Clear existing control points + for (auto* point : controlPoints) { + scene->removeItem(point); + delete point; + } + controlPoints.clear(); + + // Create linear gradient for display + QLinearGradient linearGrad(10, 0, sliderWidth + 10, 0); + for (const auto& point : gradient.points) { + linearGrad.setColorAt(point.position, point.color); + } + + gradientRect->setBrush(QBrush(linearGrad)); + + // Create control points + for (int i = 0; i < gradient.points.size(); ++i) { + qreal x = xFromPosition(gradient.points[i].position); + GradientControlPoint* controlPoint = new GradientControlPoint( + i, x - pointSize / 2, sliderY + sliderHeight, pointSize, + gradient.points[i].color); + controlPoints.append(controlPoint); + scene->addItem(controlPoint); + } +} + +void GradientSlider::addControlPoint(float position, const QColor& color) +{ + gradient.addPoint(GradientPoint(position, color)); + updateGradientDisplay(); + emitGradientUpdate(); +} + +void GradientSlider::removeControlPoint(int index) +{ + if (index >= 0 && index < gradient.points.size()) { + gradient.points.remove(index); + updateGradientDisplay(); + emitGradientUpdate(); + selectedPointIndex = -1; + } +} + +void GradientSlider::updateControlPointPositions() +{ + for (int i = 0; i < controlPoints.size() && i < gradient.points.size(); + ++i) { + qreal x = controlPoints[i]->pos().x() + 5; // Center of control point + gradient.points[i].position = positionFromX(x); + } + updateGradientDisplay(); + emitGradientUpdate(); +} + +void GradientSlider::emitGradientUpdate() +{ + Gradient sortedGradient(gradient); + sortedGradient.sort(); + emit onGradientChanged(sortedGradient); +} + +float GradientSlider::positionFromX(qreal x) +{ + return qBound(0.0, (x - 10.0) / sliderWidth, 1.0); +} + +qreal GradientSlider::xFromPosition(float position) +{ + return 10.0 + position * sliderWidth; +} + +void GradientSlider::mousePressEvent(QMouseEvent* event) +{ + QPointF scenePos = mapToScene(event->pos()); + + // Check if clicked on a control point + QGraphicsItem* item = scene->itemAt(scenePos, transform()); + GradientControlPoint* controlPoint = + dynamic_cast(item); + + const int SLIDER_BOTTOM_MARGIN = 15; + + if (event->button() == Qt::LeftButton) { + if (controlPoint) { + // Select and start dragging the control point + selectedPointIndex = controlPoint->index; + draggingPointIndex = controlPoint->index; + dragStartPos = scenePos; + if (selectedPointIndex >= 0 && + selectedPointIndex < gradient.points.size()) { + emit onActivePointChanged( + selectedPointIndex, + gradient.points[selectedPointIndex].color); + } + event->accept(); + return; + } + else if (scenePos.y() >= sliderY && + scenePos.y() <= + sliderY + sliderHeight + SLIDER_BOTTOM_MARGIN && + scenePos.x() >= 10 && scenePos.x() <= sliderWidth + 10) { + // Add new control point + float position = positionFromX(scenePos.x()); + + // Sample color at this position + QColor color = gradient.sample(position); + + addControlPoint(position, color); + + // Select the newly added control point and prepare for dragging + selectedPointIndex = gradient.points.size() - 1; + draggingPointIndex = selectedPointIndex; + dragStartPos = scenePos; + + emit onActivePointChanged(selectedPointIndex, color); + + event->accept(); + return; + } + } + else if (event->button() == Qt::MiddleButton) { + if (controlPoint) { + // Delete the control point + removeControlPoint(controlPoint->index); + event->accept(); + return; + } + } + + QGraphicsView::mousePressEvent(event); +} + +void GradientSlider::mouseMoveEvent(QMouseEvent* event) +{ + if (draggingPointIndex >= 0 && draggingPointIndex < controlPoints.size()) { + QPointF scenePos = mapToScene(event->pos()); + + // Constrain to horizontal movement within bounds + qreal minX = 10; + qreal maxX = sliderWidth + 10; + qreal newX = qBound(minX, scenePos.x(), maxX); + // qDebug() << "Dragging to X:" << newX; + + // Update control point position + controlPoints[draggingPointIndex]->setPos(newX - pointSize / 2, + sliderY + sliderHeight); + + controlPoints[draggingPointIndex]->update(); + // qDebug() << "Control point scenePos:" + // << controlPoints[draggingPointIndex]->scenePos() + // << "pos:" << controlPoints[draggingPointIndex]->pos(); + + // Update gradient point position + gradient.points[draggingPointIndex].position = positionFromX(newX); + + // Update gradient display (without sorting to maintain indices) + QLinearGradient linearGrad(10, 0, sliderWidth + 10, 0); + for (const auto& point : gradient.points) { + linearGrad.setColorAt(point.position, point.color); + } + gradientRect->setBrush(QBrush(linearGrad)); + + // Emit change during drag + emitGradientUpdate(); + + event->accept(); + } + else { + QGraphicsView::mouseMoveEvent(event); + } +} + +void GradientSlider::mouseReleaseEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton && draggingPointIndex >= 0) { + draggingPointIndex = -1; + updateGradientDisplay(); + emitGradientUpdate(); + } + QGraphicsView::mouseReleaseEvent(event); +} + +void GradientSlider::resizeEvent(QResizeEvent* event) +{ + QGraphicsView::resizeEvent(event); + + // Update slider width based on new size + sliderWidth = qMax(200.0, width() - 40.0); + scene->setSceneRect(0, 0, width() - 20, 40); + + // Update gradient rectangle size + if (gradientRect) { + gradientRect->setRect(10, sliderY, sliderWidth, sliderHeight); + updateGradientDisplay(); + } +} + +GradientPickerDialog::GradientPickerDialog() { this->initUI(); } + +void GradientPickerDialog::setGradient(const Gradient& gradient) +{ + currentGradient = gradient; + this->gradientSlider->setGradient(gradient); + + // Enable and update color pickers with first point + if (gradient.points.size() > 0) { + onControlPointSelected(0, gradient.points[0].color); + } +} + +void GradientPickerDialog::initUI() +{ + setWindowTitle("Gradient Picker"); + setMinimumSize(400, 290); + resize(460, 290); + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(5, 5, 5, 5); + + // Create and add gradient slider + gradientSlider = new GradientSlider(); + layout->addWidget(gradientSlider); + + // Add SVBox and HueSlider + svBox = new SVBox(); + svBox->setEnabled(false); + svBox->setMaximumHeight(150); // 60% reduction from default 300px + svBox->setMinimumHeight(150); // 60% reduction from default 300px + svBox->setFixedHeight(150); + layout->addWidget(svBox); + + hueSlider = new HueSlider(); + hueSlider->setEnabled(false); + hueSlider->setFixedHeight(30); + layout->addWidget(hueSlider); + + // Add OK and Cancel buttons + auto buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + + setLayout(layout); + + // Connect signals + connect(gradientSlider, &GradientSlider::onGradientChanged, this, + [this](const Gradient& gradient) { + currentGradient = gradient; + emit onGradientChanged(gradient); + }); + connect(gradientSlider, &GradientSlider::onActivePointChanged, this, + &GradientPickerDialog::onControlPointSelected); + connect(svBox, &SVBox::onSVChanged, this, + &GradientPickerDialog::onSVChanged); + connect(hueSlider, &HueSlider::onHueChanged, this, + &GradientPickerDialog::onHueChanged); + connect(buttonBox, &QDialogButtonBox::accepted, this, + &GradientPickerDialog::onAccepted); + connect(buttonBox, &QDialogButtonBox::rejected, this, + &GradientPickerDialog::onRejected); + + setGradient(Gradient::defaultGradient()); +} + +void GradientPickerDialog::onControlPointSelected(int index, + const QColor& color) +{ + Q_UNUSED(index); + updatingFromControlPoint = true; + svBox->setEnabled(true); + svBox->setColor(color); + hueSlider->setEnabled(true); + hueSlider->setColor(color); + updatingFromControlPoint = false; +} + +void GradientPickerDialog::onSVChanged(float saturation, float value) +{ + if (updatingFromControlPoint) + return; + + QColor currentColor = svBox->getColor(); + gradientSlider->setPointColor(currentColor); +} + +void GradientPickerDialog::onHueChanged(float hue) +{ + if (updatingFromControlPoint) + return; + + QColor currentColor = svBox->getColor(); + currentColor.setHsvF(hue, currentColor.saturationF(), + currentColor.valueF()); + svBox->setColor(currentColor); + gradientSlider->setPointColor(currentColor); +} + +void GradientPickerDialog::onAccepted() +{ + // Create a sorted clone of the gradient + Gradient sortedGradient(currentGradient); + sortedGradient.sort(); + + emit onGradientAccepted(sortedGradient); + accept(); +} + +void GradientPickerDialog::onRejected() { reject(); } \ No newline at end of file diff --git a/src/colorpicker/gradientpicker.h b/src/colorpicker/gradientpicker.h new file mode 100644 index 00000000..ef0e5caf --- /dev/null +++ b/src/colorpicker/gradientpicker.h @@ -0,0 +1,97 @@ +#pragma once +#include "gradient.h" +#include +#include +#include +#include +#include +#include +#include + +class SVBox; +class HueSlider; + +class GradientControlPoint : public QGraphicsObject { + Q_OBJECT +public: + int index; + qreal size; + QColor color; + + GradientControlPoint(int index, qreal x, qreal y, qreal size, + const QColor& color); + + QRectF boundingRect() const override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget = nullptr) override; +}; + +class GradientSlider : public QGraphicsView { + Q_OBJECT + + Gradient gradient; + QGraphicsScene* scene; + QGraphicsRectItem* gradientRect; + QVector controlPoints; + int selectedPointIndex = -1; + int draggingPointIndex = -1; + QPointF dragStartPos; + +public: + qreal sliderWidth = 380; + qreal sliderHeight = 30; + qreal sliderY = 0; + static constexpr qreal pointSize = 12; + + GradientSlider(); + void setGradient(const Gradient& gradient); + Gradient getGradient() const { return gradient; } + void setPointColor(const QColor& color); + void selectFirstPoint(); + +private: + void initUI(); + void updateGradientDisplay(); + void addControlPoint(float position, const QColor& color); + void removeControlPoint(int index); + void updateControlPointPositions(); + void emitGradientUpdate(); + float positionFromX(qreal x); + qreal xFromPosition(float position); + +protected: + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + +signals: + void onGradientChanged(const Gradient& gradient); + void onActivePointChanged(int index, const QColor& color); +}; + +class GradientPickerDialog : public QDialog { + Q_OBJECT + + GradientSlider* gradientSlider = nullptr; + SVBox* svBox = nullptr; + HueSlider* hueSlider = nullptr; + bool updatingFromControlPoint = false; + Gradient currentGradient; + +public: + GradientPickerDialog(); + void setGradient(const Gradient& gradient); + +signals: + void onGradientAccepted(const Gradient& gradient); + void onGradientChanged(const Gradient& gradient); + +private: + void initUI(); + void onControlPointSelected(int index, const QColor& color); + void onSVChanged(float saturation, float value); + void onHueChanged(float hue); + void onAccepted(); + void onRejected(); +}; \ No newline at end of file diff --git a/src/colorpicker/main.cpp b/src/colorpicker/main.cpp new file mode 100644 index 00000000..b4d330a7 --- /dev/null +++ b/src/colorpicker/main.cpp @@ -0,0 +1,14 @@ +#include "colorpicker.h" +#include + +int main(int argc, char* argv[]) +{ + QApplication a(argc, argv); + ColorPicker dialog; + + // dialog.resize(800, 600); + dialog.setColor(QColor(50, 125, 125)); + dialog.show(); + + return a.exec(); +} diff --git a/src/colorpicker/widgets.cpp b/src/colorpicker/widgets.cpp new file mode 100644 index 00000000..b4849cad --- /dev/null +++ b/src/colorpicker/widgets.cpp @@ -0,0 +1,295 @@ +#include "widgets.h" +#include "colorpicker.h" +#include +#include +#include +#include +#include +#include +#include + +// https://github.com/yjg30737/pyqt-color-picker/blob/main/pyqt_color_picker/colorSquareWidget.py + +SVBox::SVBox() +{ + // this->setFixedSize(400, 300); + // this->setMinimumHeight(150); + + auto layout = new QGridLayout(this); + colorWidget = new QWidget(); + + blackWidget = new QWidget(); + // https://github.com/yjg30737/pyqt-color-picker/blob/main/pyqt_color_picker/style/black_overlay.css + QString blackStyle = "background-color: qlineargradient(spread:pad, x1:0, " + "y1:0, x2:0, y2:1, stop:0 rgba(0, 0, 0, 0)," + "stop:1 rgba(0, 0, 0, 255));" + "width:100%;" + "border-radius: 5px;"; + blackWidget->setStyleSheet(blackStyle); + + blackWidget->installEventFilter(this); + blackWidget->setMouseTracking(true); + + selector = new QWidget(blackWidget); + selector->setGeometry(qFloor(selectorDiameter / 2) * -1, + qFloor(selectorDiameter / 2) * -1, selectorDiameter, + selectorDiameter); + + selector->setStyleSheet("background-color: none;" + "border: 2px solid white;" + "border-radius: 5px;"); + + // connect(blackWidget, &QWidget::mouseMoveEvent, []() { + + // }); + + // you can stack widgets on top of each other in a grid + + layout->addWidget(colorWidget, 0, 0, 1, 1); + layout->addWidget(blackWidget, 0, 0, 1, 1); + layout->setContentsMargins(0, 0, 0, 0); + this->setLayout(layout); + + this->setColor(QColor(255, 0, 0)); +} + +void SVBox::setColor(const QColor& color) +{ + this->color = color; + h = std::clamp(color.hueF(), 0.0f, 1.0f); + s = color.saturationF(); + v = color.valueF(); + + auto hue = h * 100; + QString style = "background-color: qlineargradient(x1:1, x2:0, " + "stop:0 hsl(%1%,100%,50%)," + "stop:1 #fff);" + "border-radius: 5px;"; + auto formatted = style.arg(hue); + + // qDebug() << formatted; + colorWidget->setStyleSheet(formatted); + + // Update selector position based on saturation and value + int x = s * this->width() - selectorDiameter / 2; + int y = (1.0f - v) * this->height() - selectorDiameter / 2; + selector->move(x, y); + + this->selector->update(); + this->update(); + colorWidget->update(); +} + +QColor SVBox::getColor() const { return QColor::fromHsvF(h, s, v); } + +bool SVBox::eventFilter(QObject* object, QEvent* event) +{ + + if (object == this->blackWidget) { + auto mouseEvent = (QMouseEvent*)event; + // handle appropriate event + switch (event->type()) { + case QEvent::MouseButtonPress: { + if (mouseEvent->button() == Qt::LeftButton) { + dragging = true; + moveSelector(mouseEvent); + } + } break; + case QEvent::MouseButtonRelease: { + if (mouseEvent->button() == Qt::LeftButton) { + dragging = false; + } + } break; + case QEvent::MouseMove: { + if (dragging) { + // + moveSelector(mouseEvent); + } + } break; + } + } + + // false means it should be send to target also. as in , we dont remove + // it. if you return true , you will take the event and widget never + // sees it so be carefull with that. + return false; +} + +void SVBox::moveSelector(QMouseEvent* evt) +{ + auto pos = evt->pos(); + + pos.setX(std::clamp(pos.x(), 0, this->width())); + pos.setY(std::clamp(pos.y(), 0, this->height())); + + this->selector->move(pos - + QPoint(selectorDiameter / 2, selectorDiameter / 2)); + + // calculate hsl + s = (this->selector->pos().x() + (selectorDiameter / 2.0f)) / this->width(); + + v = std::abs(((this->selector->pos().y() + (selectorDiameter / 2.0f)) / + this->height()) - + 1.0f); + + // qDebug() << s << " " << v << "\n"; + emit onSVChanged(s, v); +} + +// https://github.com/mortalis13/Qt-Color-Picker-Qt/blob/master/Widgets/ColorWidgets/hselector.cpp + +HueSlider::HueSlider() +{ + selectorDrawn = false; + hue = 0; + // this->setFixedSize(400, 20); + setFixedHeight(20); + + this->setStyleSheet("border-radius: 5px;"); +} +void HueSlider::setHue(float hue) +{ + this->hue = hue; + update(); +} +void HueSlider::setColor(const QColor& color) +{ + this->color = color; + this->hue = std::clamp(color.hueF(), 0.0f, 1.0f); + update(); +} + +void HueSlider::paintEvent(QPaintEvent* event) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + float barHeight = height(); + float barWidth = width(); + + if (!selectorDrawn) { + // if (true) { + selectorPixmap = QPixmap(barWidth, barHeight); + QPainter huePainter(&selectorPixmap); + + QPointF p1(0, 0); + QPointF p2(width(), 0); + // QPointF p1(0, 0); + // QPointF p2(100, 100); + QLinearGradient grad(p1, p2); + // QLinearGradient grad(p1, p2); + + float ratio = 1.0 / 360.0f; + + QColor gradientColor; + for (qreal hs = 0; hs < 1.0; hs += ratio) { + gradientColor.setHsvF(hs, 1.0f, 1.0f); + grad.setColorAt(hs, gradientColor); + } + + // grad.setColorAt(0.0, Qt::red); + // grad.setColorAt(0.5, Qt::green); + // grad.setColorAt(1.0, Qt::blue); + + huePainter.setPen(Qt::NoPen); + huePainter.setBrush(QBrush(grad)); + huePainter.drawRect(0, 0, barWidth, barHeight); + + selectorDrawn = true; + } + + // selectorPixmap.save("./selector.png"); + painter.drawPixmap(0, 0, selectorPixmap); + + // draw selector + painter.setPen(QPen(Qt::black, 2)); + painter.setBrush(Qt::white); + const QPointF point(hue * width(), height() / 2); + painter.drawEllipse(point, 5, 5); +} + +void HueSlider::resizeEvent(QResizeEvent* event) { selectorDrawn = false; } + +void HueSlider::mousePressEvent(QMouseEvent* event) +{ + hue = event->pos().x() / (float)width(); + update(); + emit onHueChanged(hue); +} + +void HueSlider::mouseMoveEvent(QMouseEvent* event) +{ + hue = event->pos().x() / (float)width(); + hue = std::clamp(hue, 0.0f, 1.0f); + update(); + emit onHueChanged(hue); +} + +void drawCheckerboard(QPainter& painter, const QRect& rect, int cellSize) +{ + painter.save(); + painter.setClipRect(rect, Qt::IntersectClip); + painter.fillRect(rect, QColor(204, 204, 204)); + for (int y = rect.top(); y < rect.bottom() + 1; y += cellSize) { + for (int x = rect.left(); x < rect.right() + 1; x += cellSize) { + int col = (x - rect.left()) / cellSize; + int row = (y - rect.top()) / cellSize; + if ((col + row) % 2) + painter.fillRect(x, y, cellSize, cellSize, + QColor(255, 255, 255)); + } + } + painter.restore(); +} + +AlphaSlider::AlphaSlider() +{ + alpha = 1.0f; + color = QColor(255, 0, 0); + setFixedHeight(20); +} + +void AlphaSlider::setColor(const QColor& color) +{ + this->color = color; + this->alpha = color.alphaF(); + update(); +} + +void AlphaSlider::paintEvent(QPaintEvent* event) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + drawCheckerboard(painter, rect(), 5); + + QColor transparent = color; + transparent.setAlphaF(0.0f); + QColor opaque = color; + opaque.setAlphaF(1.0f); + + QLinearGradient grad(QPointF(0, 0), QPointF(width(), 0)); + grad.setColorAt(0.0, transparent); + grad.setColorAt(1.0, opaque); + painter.fillRect(rect(), QBrush(grad)); + + // draw selector + painter.setPen(QPen(Qt::black, 2)); + painter.setBrush(Qt::white); + const QPointF point(alpha * width(), height() / 2); + painter.drawEllipse(point, 5, 5); +} + +void AlphaSlider::mousePressEvent(QMouseEvent* event) +{ + alpha = std::clamp(event->pos().x() / (float)width(), 0.0f, 1.0f); + update(); + emit onAlphaChanged(alpha); +} + +void AlphaSlider::mouseMoveEvent(QMouseEvent* event) +{ + alpha = std::clamp(event->pos().x() / (float)width(), 0.0f, 1.0f); + update(); + emit onAlphaChanged(alpha); +} diff --git a/src/colorpicker/widgets.h b/src/colorpicker/widgets.h new file mode 100644 index 00000000..fa5012b9 --- /dev/null +++ b/src/colorpicker/widgets.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +class SVBox; +class HueSlider; +class AlphaSlider; + +// https://github.com/yjg30737/pyqt-color-picker/blob/main/pyqt_color_picker/colorSquareWidget.py +class SVBox : public QWidget { + Q_OBJECT + + QColor color; + + QWidget* colorWidget; + // black overlay + QWidget* blackWidget; + + QWidget* selector; + int selectorDiameter = 10; + bool dragging = false; + + float h, s, v; + +public: + SVBox(); + void setColor(const QColor& color); + QColor getColor() const; + bool eventFilter(QObject* object, QEvent* event); + void moveSelector(QMouseEvent* evt); + +signals: + void onSVChanged(float saturation, float value); +}; + +// https://github.com/mortalis13/Qt-Color-Picker-Qt/blob/master/Widgets/ColorWidgets/hselector.cpp +class HueSlider : public QWidget { + Q_OBJECT + + float hue; + bool selectorDrawn; + QColor color; + QPixmap selectorPixmap; + +public: + HueSlider(); + void setHue(float hue); + void setColor(const QColor& color); + +protected: + void paintEvent(QPaintEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; +signals: + void onHueChanged(float hue); +}; + +// Horizontal slider over a checkerboard, fading from transparent to the +// current color at full opacity. +class AlphaSlider : public QWidget { + Q_OBJECT + + float alpha; + QColor color; + +public: + AlphaSlider(); + void setColor(const QColor& color); + float getAlpha() const { return alpha; } + +protected: + void paintEvent(QPaintEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; +signals: + void onAlphaChanged(float alpha); +}; diff --git a/src/components/Accordion.vue b/src/components/Accordion.vue deleted file mode 100644 index f0a7ac37..00000000 --- a/src/components/Accordion.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - - - diff --git a/src/components/ColorPicker.vue b/src/components/ColorPicker.vue deleted file mode 100644 index 387cc650..00000000 --- a/src/components/ColorPicker.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - - - diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index 8fd7679d..00000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - diff --git a/src/components/LibraryMenu.vue b/src/components/LibraryMenu.vue deleted file mode 100644 index 29591ec5..00000000 --- a/src/components/LibraryMenu.vue +++ /dev/null @@ -1,399 +0,0 @@ - - - - - diff --git a/src/components/ProgressView.vue b/src/components/ProgressView.vue deleted file mode 100644 index c2e9decd..00000000 --- a/src/components/ProgressView.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - - - diff --git a/src/components/properties/BoolProp.vue b/src/components/properties/BoolProp.vue deleted file mode 100644 index b6706607..00000000 --- a/src/components/properties/BoolProp.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - - - diff --git a/src/components/properties/ColorProp.vue b/src/components/properties/ColorProp.vue deleted file mode 100644 index 861052e3..00000000 --- a/src/components/properties/ColorProp.vue +++ /dev/null @@ -1,122 +0,0 @@ -, - - - - - diff --git a/src/components/properties/EnumProp.vue b/src/components/properties/EnumProp.vue deleted file mode 100644 index 1797abe1..00000000 --- a/src/components/properties/EnumProp.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - - - diff --git a/src/components/properties/FloatProp.vue b/src/components/properties/FloatProp.vue deleted file mode 100644 index 6a8ca8c1..00000000 --- a/src/components/properties/FloatProp.vue +++ /dev/null @@ -1,226 +0,0 @@ - - - - - diff --git a/src/components/properties/GradientProp.vue b/src/components/properties/GradientProp.vue deleted file mode 100644 index 66afed29..00000000 --- a/src/components/properties/GradientProp.vue +++ /dev/null @@ -1,522 +0,0 @@ - - - - - diff --git a/src/components/properties/ImageProp.vue b/src/components/properties/ImageProp.vue deleted file mode 100644 index 1639e02a..00000000 --- a/src/components/properties/ImageProp.vue +++ /dev/null @@ -1,241 +0,0 @@ - - - - - diff --git a/src/components/properties/RandomSeedProp.vue b/src/components/properties/RandomSeedProp.vue deleted file mode 100644 index f833560f..00000000 --- a/src/components/properties/RandomSeedProp.vue +++ /dev/null @@ -1,198 +0,0 @@ - - - - - diff --git a/src/components/properties/StringProp.vue b/src/components/properties/StringProp.vue deleted file mode 100644 index 5663d390..00000000 --- a/src/components/properties/StringProp.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - - - diff --git a/src/components/properties/TextureChannelProp.vue b/src/components/properties/TextureChannelProp.vue deleted file mode 100644 index b627aecb..00000000 --- a/src/components/properties/TextureChannelProp.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - - - diff --git a/src/components/properties/ipropertyui.ts b/src/components/properties/ipropertyui.ts deleted file mode 100644 index d37fe32c..00000000 --- a/src/components/properties/ipropertyui.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface IProperyUi { - refresh(); -} - -export class PropertyChangeComplete { - public propName: string; - public oldValue: any; - public newValue: any; -} diff --git a/src/components/variables/BoolVar.vue b/src/components/variables/BoolVar.vue deleted file mode 100644 index ef95130d..00000000 --- a/src/components/variables/BoolVar.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/src/components/variables/ColorVar.vue b/src/components/variables/ColorVar.vue deleted file mode 100644 index 14766338..00000000 --- a/src/components/variables/ColorVar.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/src/components/variables/EnumVar.vue b/src/components/variables/EnumVar.vue deleted file mode 100644 index 97047e59..00000000 --- a/src/components/variables/EnumVar.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - diff --git a/src/components/variables/FloatVar.vue b/src/components/variables/FloatVar.vue deleted file mode 100644 index dbb038b5..00000000 --- a/src/components/variables/FloatVar.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - diff --git a/src/export.ts b/src/export.ts deleted file mode 100644 index f30735c9..00000000 --- a/src/export.ts +++ /dev/null @@ -1,375 +0,0 @@ -import jimp from "jimp"; -import sharp from "sharp"; -import { Editor } from "./lib/editor"; -import { BitDepth, encode } from "fast-png"; -import fs from "fs"; -import { PNG } from "pngjs"; -import { - Float16Array, - isFloat16Array, - getFloat16, - setFloat16, - hfround -} from "@petamoriken/float16"; -import { - TextureComponents, - TextureDataConverter -} from "./lib/designer/texturedataconverter"; -import { TextureDataType } from "./lib/designer/gl"; -import { DesignerNode } from "./lib/designer/designernode"; -import { Designer } from "./lib/designer"; -import AdmZip from "adm-zip"; -import path from "path"; -import electron from "electron"; - -export enum ImageFileType { - Png = "png", - Jpg = "jpg", - Tga = "tga", - Tif = "tif" -} - -export enum ColorSpace { - Linear = "linear", - sRGB = "srgb" -} - -export enum OutputType { - Folder, - Zip, - UnityPackage -} - -export enum TextureTransform { - InvertX, - InvertY, - InvertZ, - LinearizeRoughness -} - -export class ExportTextureSettings { - channelName: string = ""; // albedo, roughness, normal, etc.. - colorSpace: ColorSpace = ColorSpace.Linear; - fileType: ImageFileType = ImageFileType.Png; - transforms: TextureTransform[] = []; -} - -// can have multiple settings and presets -export class ExportSettings { - name: string = ""; - filePattern: string = "{project}_{file}"; - // textureSettings: Map = new Map< - // string, - // ExportTextureSettings - // >(); - // enabled: boolean = true; - outputType: OutputType = OutputType.Zip; - outputPath: string = ""; -} - -export class Exporter { - async export(editor: Editor, settings: ExportSettings) { - const designer = editor.designer; - const conv = new TextureDataConverter(editor.designer.gl); - // sample config - - // assume the settings have all the nodes listen already - // settings is the source of truth - console.log(editor.textureChannels); - - const exportNodes = this.gatherExportNodes(designer, editor); - console.log(exportNodes); - - let files: Map = new Map(); - - for (const node of exportNodes) { - const channelNode = node.node; - - // const pixelData = channelNode.getPixelData(); - // const pixelData = conv.getData( - // channelNode.node.tex, - // designer.width, - // designer.height, - // TextureDataType.Uint16, - // TextureComponents.RGBA, - // true - // ); - - const pixelData = conv.getData( - channelNode.tex, - designer.width, - designer.height, - node.dataType, - node.components, - true - ); - - // too expensive - // convertRange(pixelData); - - // console.log(pixelData); - // console.log(floatData); - // for (let i = 0; i < 20; i++) { - // console.log(floatData[i]); - // } - - // const exportPath = - // "C:/Users/Nicolas Brown/Desktop/export-" + node.name + ".png"; - // console.log("exporting to: " + exportPath); - - const fileData = exportNodeAsPng( - node, - pixelData, - designer.width, - designer.height - ); - - const projectName = settings.name - .replace(" ", "_") - .replace(/[^0-9a-zA-Z_]/g, ""); - - const fileName = interpolateString(settings.filePattern, { - project: projectName, - name: node.name - }); - - // files.set(node.name + ".png", fileData); - files.set(fileName + ".png", fileData); - - // write to file if folder mode - // fs.writeFile(exportPath, bytes, function(err) { - // if (err) alert("Error exporting texture: " + err); - // }); - - // write to zip otherwise - - // const imgCanvas = editor.getChannelCanvasImage(channelName); - // if (imgCanvas) { - // const imgData = imgCanvas.canvas.toDataURL("image/png"); - - // //todo: get raw image data from gpu in floating point - // } - } - - if (settings.outputType == OutputType.Folder) { - exportFilesToFolder(files, settings.outputPath); - } else { - exportFilesToZip(files, settings.outputPath); - electron.remote.shell.showItemInFolder(settings.outputPath); - } - - // export them - - // const image = await jimp.read(""); - } - - calculateFileName(fileName: string, project: string): string { - return null; - } - - // returns all nodes ready for export with their data - // todo: use display channel if no name property specified - gatherExportNodes(designer: Designer, editor: Editor) { - // gather all export nodes - // let exportNodes = []; - - let nodes = designer.nodes.filter(x => x.typeName === "output"); - - let exportNodes: ExportNode[] = []; - - for (const node of nodes) { - let exportNode = new ExportNode(); - - exportNode.node = node; - for (const prop of node.properties) { - if (prop.name === "name") { - exportNode.name = prop.getValue(); - } - if (prop.name === "precision") { - const precision = prop.getValue() as number; - - // 0 -> 8bits - // 1 -> 16bits - if (precision === 0) exportNode.dataType = TextureDataType.Uint8; - if (precision === 1) exportNode.dataType = TextureDataType.Uint16; - } - if (prop.name === "components") { - const components = prop.getValue() as number; - - // 0 -> RGBA - // 1 -> RGB - // 2 -> R - // 3 -> G - // 4 -> B - // 5 -> A - if (components === 0) exportNode.components = TextureComponents.RGBA; - if (components === 1) exportNode.components = TextureComponents.RGB; - if (components === 2) exportNode.components = TextureComponents.R; - if (components === 3) exportNode.components = TextureComponents.G; - if (components === 4) exportNode.components = TextureComponents.B; - if (components === 5) exportNode.components = TextureComponents.A; - } - } - - if (!exportNode.name || exportNode.name === "") { - exportNode.name = getChannelNameForNode(exportNode.node, editor); - } - - exportNodes.push(exportNode); - } - - return exportNodes; - } -} - -function getChannelNameForNode(node: DesignerNode, editor: Editor) { - const channels = editor.textureChannels; - // slow. - // create dictionary with nodeId as key instead - for (const key of channels.keys()) { - if (channels.get(key) === node) return key; - } - - return ""; -} - -function exportNodeAsPng( - node: ExportNode, - pixelData: ArrayBuffer, - width: number, - height: number -): ArrayBuffer { - try { - let bitDepth: BitDepth = 8; - if (node.dataType === TextureDataType.Uint16) bitDepth = 16; - - let channels: number = 4; - // if (node.components === TextureComponents.RGBA) channels = 4; - if (node.components === TextureComponents.RGB) channels = 3; - if ( - node.components === TextureComponents.R || - node.components === TextureComponents.G || - node.components === TextureComponents.B || - node.components === TextureComponents.A - ) - channels = 1; - - // FAST-PNG - const bytes = encode({ - data: - bitDepth == 8 ? new Uint8Array(pixelData) : new Uint16Array(pixelData), - width: width, - height: height, - depth: bitDepth, - channels: channels - }); - - return bytes.buffer; - - // fs.writeFile(exportPath, bytes, function(err) { - // if (err) alert("Error exporting texture: " + err); - // }); - - // await sharp(pixelData, { - // raw: { - // width: editor.designer.width, - // height: editor.designer.height, - // channels: 4 - // } - // }) - // // .pipelineColourspace("rgb") - // // .toColorspace("rgb") - // .png({}) - // .toFile(exportPath); - - // PNGJS - // https://github.com/lukeapage/pngjs/blob/master/examples/16bit_write.js - // let png = new PNG({ - // width: editor.designer.width, - // height: editor.designer.height, - // inputColorType: 6, - // colorType: 6, - // bitDepth: 16, - // inputHasAlpha: true - // }); - - // png.data = Buffer.from(pixelData); - // png.gamma = 1; - // png.pack().pipe(fs.createWriteStream(exportPath)); - } catch (error) { - console.log("error saving file"); - console.log(error); - } -} - -class ExportNode { - node: DesignerNode; - components: TextureComponents = TextureComponents.RGBA; - dataType: TextureDataType = TextureDataType.Uint8; - name: string = ""; -} - -function exportFilesToFolder( - files: Map, - folderPath: string -) { - for (let fileName of files.keys()) { - console.log(fileName); - const exportPath = path.join(folderPath, fileName); - const bytes = files.get(fileName); - - fs.writeFile(exportPath, new Uint8Array(bytes), function(err) { - if (err) alert("Error exporting texture: " + err); - }); - } -} -function exportFilesToZip( - files: Map, - zipFilePath: string -) { - const zip = new AdmZip(); - - for (let fileName of files.keys()) { - const bytes = files.get(fileName); - console.log(bytes); - zip.addFile(fileName, Buffer.from(bytes)); - } - - zip.writeZip(zipFilePath); -} - -function convertRange(data: Uint16Array) { - const UINT_MAX = 65535.0; - const VALUE_MAX = 15360.0; - - const floatData = new Float16Array(data.buffer); - for (let i = 0; i < data.length; i++) { - // if (i < 10) console.log(floatData[i] * UINT_MAX); - data[i] = floatData[i] * UINT_MAX; - } -} - -function interpolateString(text: string, exportData: object) { - if (!text) return ""; - - // run regex on text and replace values from the string - text = text.replace( - /\${([a-z]+)}/g, - (wholeMatch: string, path: any): string => { - // console.log(path); - const data = exportData[path]; - if (!data) return ""; - - return data; - } - ); - - // console.log(text); - - return text; -} - -// class PixelDataConverter -// { -// public static rgbaTo -// } diff --git a/src/iapp.ts b/src/iapp.ts deleted file mode 100644 index 6f463340..00000000 --- a/src/iapp.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IApp { - randomSeed: number; -} diff --git a/src/icons/chevron-down.svg b/src/icons/chevron-down.svg new file mode 100644 index 00000000..6a16f720 --- /dev/null +++ b/src/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/chevron-up.svg b/src/icons/chevron-up.svg new file mode 100644 index 00000000..f56d0b50 --- /dev/null +++ b/src/icons/chevron-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/copy.svg b/src/icons/copy.svg new file mode 100644 index 00000000..daed7d43 --- /dev/null +++ b/src/icons/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/crosshair.svg b/src/icons/crosshair.svg new file mode 100644 index 00000000..016b5794 --- /dev/null +++ b/src/icons/crosshair.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/icons/download.svg b/src/icons/download.svg new file mode 100644 index 00000000..0b25b2e5 --- /dev/null +++ b/src/icons/download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/export.svg b/src/icons/export.svg new file mode 100644 index 00000000..9effdc40 --- /dev/null +++ b/src/icons/export.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/grid.svg b/src/icons/grid.svg new file mode 100644 index 00000000..9c5bb650 --- /dev/null +++ b/src/icons/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/icons/logo.icns b/src/icons/logo.icns new file mode 100644 index 00000000..4d298a28 Binary files /dev/null and b/src/icons/logo.icns differ diff --git a/src/icons/logo.ico b/src/icons/logo.ico new file mode 100644 index 00000000..fb06fcf2 Binary files /dev/null and b/src/icons/logo.ico differ diff --git a/src/icons/logo.png b/src/icons/logo.png new file mode 100644 index 00000000..f479bc31 Binary files /dev/null and b/src/icons/logo.png differ diff --git a/src/icons/logo.svg b/src/icons/logo.svg new file mode 100644 index 00000000..f16c1c9e --- /dev/null +++ b/src/icons/logo.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/icons/redo.svg b/src/icons/redo.svg new file mode 100644 index 00000000..501cd955 --- /dev/null +++ b/src/icons/redo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/save.svg b/src/icons/save.svg new file mode 100644 index 00000000..d0952227 --- /dev/null +++ b/src/icons/save.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/undo.svg b/src/icons/undo.svg new file mode 100644 index 00000000..1a562277 --- /dev/null +++ b/src/icons/undo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/lib/actions/addconnectionaction.ts b/src/lib/actions/addconnectionaction.ts deleted file mode 100644 index 65ad4cb8..00000000 --- a/src/lib/actions/addconnectionaction.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2 } from "../scene/view"; -import { ConnectionGraphicsItem } from "../scene/connectiongraphicsitem"; -import { NodeScene } from "../scene"; - -export class AddConnectionAction extends Action { - scene: NodeScene; - con: ConnectionGraphicsItem; - - constructor(scene: NodeScene, con: ConnectionGraphicsItem) { - super(); - - this.scene = scene; - this.con = con; - } - - undo() { - this.scene.removeConnection(this.con); - } - - redo() { - this.scene.addConnection(this.con); - } -} diff --git a/src/lib/actions/additemsaction.ts b/src/lib/actions/additemsaction.ts deleted file mode 100644 index 4223ef1f..00000000 --- a/src/lib/actions/additemsaction.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2 } from "../scene/view"; -import { ConnectionGraphicsItem } from "../scene/connectiongraphicsitem"; -import { NodeScene } from "../scene"; -import { FrameGraphicsItem } from "../scene/framegraphicsitem"; -import { CommentGraphicsItem } from "../scene/commentgraphicsitem"; -import { NodeGraphicsItem } from "../scene/nodegraphicsitem"; -import { NavigationGraphicsItem } from "../scene/navigationgraphicsitem"; -import { DesignerNode } from "../designer/designernode"; -import { Designer } from "../designer"; - -// used when items are added to the scene -// also used for paste events -export class AddItemsAction extends Action { - scene: NodeScene; - designer: Designer; - frames: FrameGraphicsItem[]; - comments: CommentGraphicsItem[]; - navs: NavigationGraphicsItem[]; - cons: ConnectionGraphicsItem[]; - nodes: NodeGraphicsItem[]; - dnodes: DesignerNode[]; - - constructor( - scene: NodeScene, - designer: Designer, - frames: FrameGraphicsItem[], - comments: CommentGraphicsItem[], - navs: NavigationGraphicsItem[], - cons: ConnectionGraphicsItem[], - nodes: NodeGraphicsItem[], - dnodes: DesignerNode[] - ) { - super(); - - this.scene = scene; - this.designer = designer; - this.frames = frames; - this.comments = comments; - this.navs = navs; - this.cons = cons; - this.nodes = nodes; - this.dnodes = dnodes; - } - - undo() { - for (const frame of this.frames) { - this.scene.removeFrame(frame); - } - - for (const comment of this.comments) { - this.scene.removeComment(comment); - } - - for (const nav of this.navs) { - this.scene.removeNavigation(nav); - } - - // for (let dnode of this.dnodes) { - // this.designer.addNode(dnode, false); - // } - - // relying on callbacks to add the connection - // in designer - for (const con of this.cons) { - this.scene.removeConnection(con); - } - - // also relying callbacks to have the node deleted in designer - // note: texture channel gets removed here if assigned - // since texture channels are not assigned upon a node being - // added to a scene then there is no need to attempt to - // reassign any - for (const node of this.nodes) { - this.scene.deleteNode(node); - } - } - - redo() { - for (const frame of this.frames) { - this.scene.addFrame(frame); - } - - for (const comment of this.comments) { - this.scene.addComment(comment); - } - - for (const nav of this.navs) { - this.scene.addNavigation(nav); - } - - for (const node of this.nodes) { - this.scene.addNode(node); - } - - for (const dnode of this.dnodes) { - this.designer.addNode(dnode, false); - } - - // relying on callbacks to add the connection - // in designer - for (const con of this.cons) { - this.scene.addConnection(con); - } - } -} diff --git a/src/lib/actions/changetexturechannelaction.ts b/src/lib/actions/changetexturechannelaction.ts deleted file mode 100644 index 57c128df..00000000 --- a/src/lib/actions/changetexturechannelaction.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2, Rect } from "../scene/view"; -import { FrameGraphicsItem } from "../scene/framegraphicsitem"; -import { Editor } from "../editor"; - -/* -FROM - -node1 --> channel1 - -ndoe2 --> channel2 - -OR - -node1 --> channel1 - - --> channel2 - - -TO - -node1 \ channel1 - \ -node2 \-> channel2 - -OR - -node1 \ channel1 - \ - \-> channel2 - -NOTE: -node2 might be null -if channel2 is null then we just remove the channel -*/ -export class ChangeTextureChannelAction extends Action { - ui: () => void; - editor: Editor; - node1: string; - node2: string; - channel1: string; - channel2: string; - - constructor( - ui: () => void, - editor: Editor, - node1: string, // active node - channel1: string, // current channel - node2: string, // new channel - channel2: string // node being displaced from new channel - ) { - super(); - - this.ui = ui; - this.editor = editor; - this.channel1 = channel1; - this.channel2 = channel2; - this.node1 = node1; - this.node2 = node2; - } - - undo() { - if (this.channel1) { - this.editor.assignNodeToTextureChannel(this.node1, this.channel1); - } else { - // node didnt have a channel to begin with - this.editor.clearTextureChannel(this.node1); - } - - if (this.channel2 != null && this.node2 != null) { - // reset displaced node - this.editor.assignNodeToTextureChannel(this.node2, this.channel2); - } - - if (this.ui) this.ui(); - } - - redo() { - if (this.channel2 == null) { - // clear channel - this.editor.clearTextureChannel(this.node1); - } else { - this.editor.assignNodeToTextureChannel(this.node1, this.channel2); - } - - if (this.ui) this.ui(); - } -} diff --git a/src/lib/actions/moveItemsaction.ts b/src/lib/actions/moveItemsaction.ts deleted file mode 100644 index f8f4647b..00000000 --- a/src/lib/actions/moveItemsaction.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2 } from "../scene/view"; - -export class MoveItemsAction extends Action { - items: GraphicsItem[]; - oldPosList: Vector2[]; - newPosList: Vector2[]; - - constructor( - items: GraphicsItem[], - oldPosList: Vector2[], - newPosList: Vector2[] - ) { - super(); - - this.items = items; - this.oldPosList = oldPosList; - this.newPosList = newPosList; - } - - undo() { - for (let i = 0; i < this.items.length; i++) { - const pos = this.oldPosList[i]; - this.items[i].setPos(pos.x, pos.y); - } - } - - redo() { - for (let i = 0; i < this.items.length; i++) { - const pos = this.newPosList[i]; - this.items[i].setPos(pos.x, pos.y); - } - } -} diff --git a/src/lib/actions/propertychangeaction.ts b/src/lib/actions/propertychangeaction.ts deleted file mode 100644 index 562bfb0e..00000000 --- a/src/lib/actions/propertychangeaction.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2, Rect } from "../scene/view"; -import { FrameGraphicsItem } from "../scene/framegraphicsitem"; -import { IPropertyHolder } from "../designer/properties"; -import { IProperyUi } from "@/components/properties/ipropertyui"; -import App from "@/App.vue"; - -export class PropertyChangeAction extends Action { - ui: () => void; - propHolder: IPropertyHolder; - propName: string; - oldValue: any; - newValue: any; - - constructor( - ui: () => void, - propName: string, - propHolder: IPropertyHolder, - oldValue: any, - newValue: any - ) { - super(); - - this.ui = ui; - this.propName = propName; - this.propHolder = propHolder; - this.oldValue = oldValue; - this.newValue = newValue; - } - - undo() { - this.propHolder.setProperty(this.propName, this.oldValue); - //App.instance.$refs.properties.$forceUpdate(); - //this.ui.refresh(); - if (this.ui) this.ui(); - } - - redo() { - this.propHolder.setProperty(this.propName, this.newValue); - //this.ui.refresh(); - if (this.ui) this.ui(); - } -} diff --git a/src/lib/actions/removeconnectionaction.ts b/src/lib/actions/removeconnectionaction.ts deleted file mode 100644 index b22077b3..00000000 --- a/src/lib/actions/removeconnectionaction.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2 } from "../scene/view"; -import { ConnectionGraphicsItem } from "../scene/connectiongraphicsitem"; -import { NodeScene } from "../scene"; - -export class RemoveConnectionAction extends Action { - scene: NodeScene; - con: ConnectionGraphicsItem; - - constructor(scene: NodeScene, con: ConnectionGraphicsItem) { - super(); - - this.scene = scene; - this.con = con; - } - - undo() { - this.scene.addConnection(this.con); - } - - redo() { - this.scene.removeConnection(this.con); - } -} diff --git a/src/lib/actions/removeitemsaction.ts b/src/lib/actions/removeitemsaction.ts deleted file mode 100644 index 39289729..00000000 --- a/src/lib/actions/removeitemsaction.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2 } from "../scene/view"; -import { ConnectionGraphicsItem } from "../scene/connectiongraphicsitem"; -import { NodeScene } from "../scene"; -import { FrameGraphicsItem } from "../scene/framegraphicsitem"; -import { CommentGraphicsItem } from "../scene/commentgraphicsitem"; -import { NodeGraphicsItem } from "../scene/nodegraphicsitem"; -import { NavigationGraphicsItem } from "../scene/navigationgraphicsitem"; -import { DesignerNode } from "../designer/designernode"; -import { Designer } from "../designer"; -import { Editor } from "../editor"; - -// used when items are added to the scene -// also used for paste events -export class RemoveItemsAction extends Action { - editor: Editor; - scene: NodeScene; - designer: Designer; - frames: FrameGraphicsItem[]; - comments: CommentGraphicsItem[]; - navs: NavigationGraphicsItem[]; - cons: ConnectionGraphicsItem[]; - nodes: NodeGraphicsItem[]; - dnodes: DesignerNode[]; - textureChannels: Map; - - constructor( - editor: Editor, - scene: NodeScene, - designer: Designer, - frames: FrameGraphicsItem[], - comments: CommentGraphicsItem[], - navs: NavigationGraphicsItem[], - cons: ConnectionGraphicsItem[], - nodes: NodeGraphicsItem[], - dnodes: DesignerNode[] - ) { - super(); - - this.editor = editor; - this.scene = scene; - this.designer = designer; - this.frames = frames; - this.comments = comments; - this.navs = navs; - this.cons = cons; - this.nodes = nodes; - this.dnodes = dnodes; - - this.textureChannels = new Map(); - - for (const node of nodes) { - if (node.textureChannel != null) { - this.textureChannels.set(node.id, node.textureChannel); - } - } - } - - undo() { - for (const frame of this.frames) { - this.scene.addFrame(frame); - } - - for (const comment of this.comments) { - this.scene.addComment(comment); - } - - for (const nav of this.navs) { - this.scene.addNavigation(nav); - } - - for (const node of this.nodes) { - this.scene.addNode(node); - } - - for (const dnode of this.dnodes) { - this.designer.addNode(dnode, false); - } - - // assign texture channels - for (const [nodeId, channel] of this.textureChannels) { - this.editor.assignNodeToTextureChannel(nodeId, channel); - } - - // relying on callbacks to add the connection - // in designer - for (const con of this.cons) { - this.scene.addConnection(con); - } - } - - redo() { - for (const frame of this.frames) { - this.scene.removeFrame(frame); - } - - for (const comment of this.comments) { - this.scene.removeComment(comment); - } - - for (const nav of this.navs) { - this.scene.removeNavigation(nav); - } - - // for (let dnode of this.dnodes) { - // this.designer.addNode(dnode, false); - // } - - // relying on callbacks to add the connection - // in designer - for (const con of this.cons) { - this.scene.removeConnection(con); - } - - // also relying callbacks to have the node deleted in designer - // note: texture channel gets removed here if assigned - // since texture channels are not assigned upon a node being - // added to a scene then there is no need to attempt to - // reassign any - for (const node of this.nodes) { - this.scene.deleteNode(node); - } - } -} diff --git a/src/lib/actions/resizeframeaction.ts b/src/lib/actions/resizeframeaction.ts deleted file mode 100644 index fb880143..00000000 --- a/src/lib/actions/resizeframeaction.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2, Rect } from "../scene/view"; -import { FrameGraphicsItem } from "../scene/framegraphicsitem"; - -export class ResizeFrameAction extends Action { - frame: FrameGraphicsItem; - oldRect: Rect; - newRect: Rect; - - constructor(frame: FrameGraphicsItem, oldRect: Rect, newRect: Rect) { - super(); - - this.frame = frame; - this.oldRect = oldRect; - this.newRect = newRect; - } - - undo() { - this.frame.setFrameRect(this.oldRect); - } - - redo() { - this.frame.setFrameRect(this.newRect); - } -} diff --git a/src/lib/actions/setglobalrandomseedaction.ts b/src/lib/actions/setglobalrandomseedaction.ts deleted file mode 100644 index 09395701..00000000 --- a/src/lib/actions/setglobalrandomseedaction.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2, Rect } from "../scene/view"; -import { FrameGraphicsItem } from "../scene/framegraphicsitem"; -import { Editor } from "../editor"; -import { IApp } from "@/iapp"; - -export class SetGlobalRandomSeedAction extends Action { - editor: Editor; - oldSeed: number; - newSeed: number; - app: IApp; - - constructor(app: IApp, editor: Editor, oldSeed: number, newSeed: number) { - super(); - - this.app = app; - this.editor = editor; - this.oldSeed = oldSeed; - this.newSeed = newSeed; - } - - undo() { - this.app.randomSeed = this.oldSeed; - this.editor.designer.setRandomSeed(this.oldSeed); - } - - redo() { - this.app.randomSeed = this.newSeed; - this.editor.designer.setRandomSeed(this.newSeed); - } -} diff --git a/src/lib/actions/setnoderandomseedaction.ts b/src/lib/actions/setnoderandomseedaction.ts deleted file mode 100644 index c4762cf2..00000000 --- a/src/lib/actions/setnoderandomseedaction.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Action } from "../undostack"; -import { DesignerNode } from "../designer/designernode"; - -export class SetNodeRandomSeedAction extends Action { - ui: () => void; - node: DesignerNode; - oldSeed: number; - newSeed: number; - - constructor( - ui: () => void, - node: DesignerNode, - oldSeed: number, - newSeed: number - ) { - super(); - - this.ui = ui; - this.node = node; - this.oldSeed = oldSeed; - this.newSeed = newSeed; - } - - undo() { - this.node.setRandomSeed(this.oldSeed); - if (this.ui) this.ui(); - } - - redo() { - this.node.setRandomSeed(this.newSeed); - if (this.ui) this.ui(); - } -} diff --git a/src/lib/actions/switchconnectionaction.ts b/src/lib/actions/switchconnectionaction.ts deleted file mode 100644 index b3b6c5ed..00000000 --- a/src/lib/actions/switchconnectionaction.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Action } from "../undostack"; -import { GraphicsItem } from "../scene/graphicsitem"; -import { Vector2 } from "../scene/view"; -import { ConnectionGraphicsItem } from "../scene/connectiongraphicsitem"; -import { NodeScene } from "../scene"; - -export enum ConnectionSwitchAction { - Add = "add", - Remove = "delete" -} - -export class SwitchConnectionAction extends Action { - scene: NodeScene; - cons: ConnectionGraphicsItem[]; - actions: ConnectionSwitchAction[]; //'add' or 'remove' - - constructor( - scene: NodeScene, - cons: ConnectionGraphicsItem[], - actions: ConnectionSwitchAction[] - ) { - super(); - - this.scene = scene; - this.cons = cons; - this.actions = actions; - } - - undo() { - for (let i = this.cons.length - 1; i >= 0; i--) { - if (this.actions[i] == ConnectionSwitchAction.Add) { - this.scene.removeConnection(this.cons[i]); - } else if (this.actions[i] == ConnectionSwitchAction.Remove) { - this.scene.addConnection(this.cons[i]); - } else { - throw "Invalid connection undo-redo action"; - } - } - } - - redo() { - for (let i = 0; i < this.cons.length; i++) { - if (this.actions[i] == ConnectionSwitchAction.Add) { - this.scene.addConnection(this.cons[i]); - } else if (this.actions[i] == ConnectionSwitchAction.Remove) { - this.scene.removeConnection(this.cons[i]); - } else { - throw "Invalid connection undo-redo action"; - } - } - } -} diff --git a/src/lib/clipboard.ts b/src/lib/clipboard.ts deleted file mode 100644 index 6903d9c2..00000000 --- a/src/lib/clipboard.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { GraphicsItem } from "./scene/graphicsitem"; -import { NodeScene } from "./scene"; -import { DesignerLibrary } from "./designer/library"; -import { FrameGraphicsItem } from "./scene/framegraphicsitem"; -import { Color } from "./designer/color"; -import { CommentGraphicsItem } from "./scene/commentgraphicsitem"; -import { NavigationGraphicsItem } from "./scene/navigationgraphicsitem"; -import { Designer } from "./designer"; -import { DesignerNode } from "./designer/designernode"; -import { NodeGraphicsItem } from "./scene/nodegraphicsitem"; -import { DesignerNodeConn } from "./designer/designerconnection"; -import { SocketType } from "./scene/socketgraphicsitem"; -import { ConnectionGraphicsItem } from "./scene/connectiongraphicsitem"; -import { Guid } from "./utils"; -import { AddItemsAction } from "./actions/additemsaction"; -import { UndoStack } from "./undostack"; -import { Rect, Vector2 } from "./scene/view"; - -export class ItemClipboard { - public static copyItems( - designer: Designer, - library: DesignerLibrary, - scene: NodeScene, - clipboard: DataTransfer - ) { - clipboard.clearData(); - const items = scene.selectedItems; - if (items.length == 0) { - // empty clipboard - clipboard.setData("text/nodes", ""); - } - - const data = { - nodes: [], - connections: [], - comments: [], - frames: [], - navigations: [], - libraryVersion: "" - }; - - // NODES AND CONNECTIONS - const nodeList: NodeGraphicsItem[] = []; - items.forEach(i => { - // check if this works with obfuscated code - if (i instanceof NodeGraphicsItem) nodeList.push(i); - }); - - data.nodes = this.getNodes(designer, nodeList); - data.connections = this.getConnections(data.nodes, designer, nodeList); - - // FRAMES - const frames = []; - for (const item of items) { - if (!(item instanceof FrameGraphicsItem)) continue; - const frame = item; - - const n: any = {}; - n["x"] = frame.left; - n["y"] = frame.top; - n["width"] = frame.getWidth(); - n["height"] = frame.getHeight(); - - n["title"] = frame.title; - n["showTitle"] = frame.showTitle; - n["description"] = frame.description; - n["color"] = frame.color.toHex(); - - frames.push(n); - } - data.frames = frames; - - // COMMENTS - const comments = []; - for (const item of items) { - if (!(item instanceof CommentGraphicsItem)) continue; - const comment = item; - - const n: any = {}; - n["x"] = comment.left; - n["y"] = comment.top; - - n["text"] = comment.text; - n["color"] = comment.color.toHex(); - - comments.push(n); - } - data.comments = comments; - - // NAVIGATIONS - const navs = []; - for (const item of items) { - if (!(item instanceof NavigationGraphicsItem)) continue; - const nav = item; - - const n: any = {}; - n["x"] = nav.left; - n["y"] = nav.top; - - navs.push(n); - } - data.navigations = navs; - - // let data = scene.save(); // do to items - data.libraryVersion = library.getVersionName(); - - const json = JSON.stringify(data); - - clipboard.setData("json/nodes", json); - } - - public static pasteItems( - designer: Designer, - library: DesignerLibrary, - scene: NodeScene, - clipboard: DataTransfer - ) { - const json = clipboard.getData("json/nodes"); - //console.log(json); - if (json == null || json == "") return; - - const data = JSON.parse(json); - if (!data) return; - - const frames: FrameGraphicsItem[] = []; - const comments: CommentGraphicsItem[] = []; - const navs: NavigationGraphicsItem[] = []; - const cons: ConnectionGraphicsItem[] = []; - const nodes: NodeGraphicsItem[] = []; - const dnodes: DesignerNode[] = []; - - // for selecting pasted items - const focusItems: GraphicsItem[] = []; - - // FRAMES - if (data.frames) { - for (const d of data.frames) { - const frame = new FrameGraphicsItem(scene.view); - frame.setPos(d.x, d.y); - frame.setSize(d.width, d.height); - - frame.setTitle(d.title); - frame.setShowTitle(d.showTitle); - frame.setDescription(d.description); - frame.color = Color.parse(d.color); - - scene.addFrame(frame); - frames.push(frame); - focusItems.push(frame); - } - } - - // COMMENTS - if (data.comments) { - for (const d of data.comments) { - const comment = new CommentGraphicsItem(scene.view); - comment.setPos(d.x, d.y); - comment.setText(d.text); - comment.color = Color.parse(d.color); - - scene.addComment(comment); - comments.push(comment); - focusItems.push(comment); - } - } - - // NAVIGATION - if (data.navigations) { - for (const d of data.navigations) { - const nav = new NavigationGraphicsItem(); - nav.setPos(d.x, d.y); - scene.addNavigation(nav); - navs.push(nav); - focusItems.push(nav); - } - } - - //NODES AND CONNECTIONS - - // old : new - const nodeIdMap = {}; - // add them to designer then add them to scene - for (const n of data.nodes) { - const dNode = library.create(n.typeName); - - // add to designer - designer.addNode(dNode); - nodeIdMap[n.id] = dNode.id; - - // assign properties - for (const propName in n.properties) { - dNode.setProperty(propName, n.properties[propName]); - } - - // create scene version - const node = new NodeGraphicsItem(dNode.title); - for (const input of dNode.getInputs()) { - node.addSocket(input, input, SocketType.In); - } - node.addSocket("output", "output", SocketType.Out); - node.id = dNode.id; - scene.addNode(node); - focusItems.push(node); - - // generate thumbnail - const thumb = designer.generateImageFromNode(dNode); - node.setThumbnail(thumb); - - node.setCenter(n.x, n.y); - - nodes.push(node); - dnodes.push(dNode); - } - // console.log(nodeIdMap); - - // console.log(scene.nodes); - - // add connections - for (const c of data.connections) { - // map to ids of new nodes - const leftId = nodeIdMap[c.leftNodeId]; - const rightId = nodeIdMap[c.rightNodeId]; - - // create connection - const con = new ConnectionGraphicsItem(); - con.id = Guid.newGuid(); // brand new connection - - // get nodes - const leftNode = scene.getNodeById(leftId); - const rightNode = scene.getNodeById(rightId); - - // get sockets - con.socketA = leftNode.getOutSocketByName(c.leftNodeOutput); - con.socketB = rightNode.getInSocketByName(c.rightNodeInput); - - // callback triggers the creation in designer - scene.addConnection(con); - - cons.push(con); - } - - if ( - frames.length != 0 || - comments.length != 0 || - navs.length != 0 || - cons.length != 0 || - nodes.length != 0 || - dnodes.length != 0 - ) { - // gather bounding box and center items to screen - if (focusItems.length > 0) { - const rect = this.getItemsBounds(focusItems); - const center = scene.view.sceneCenter; - - // find diff, then offset each object by that diff - //let diff = Vector2.subtract(center, rect.center); - for (const item of focusItems) { - const offsetFromRect = Vector2.subtract(item.getPos(), rect.center); - const newPos = Vector2.add(center, offsetFromRect); - item.setPos(newPos.x, newPos.y); - //item.move(diff.x, diff.y); - } - } - - // add undo-redo - const action = new AddItemsAction( - scene, - designer, - frames, - comments, - navs, - cons, - nodes, - dnodes - ); - UndoStack.current.push(action); - - // make items selected - scene.setSelectedItems(focusItems, true); - } - } - - // merge designer and scene node in one - // scene node only has x and y values - static getNodes( - designer: Designer, - items: NodeGraphicsItem[] - ): Array { - const dnodes = []; - items.forEach(i => { - const node = designer.getNodeById(i.id); - - const n = {}; - n["id"] = node.id; - n["typeName"] = node.typeName; - n["exportName"] = node.exportName; - //n["inputs"] = node.inputs;// not needed imo - - const props = {}; - for (const prop of node.properties) { - props[prop.name] = prop.getValue(); - } - n["properties"] = props; - - n["x"] = i.centerX(); - n["y"] = i.centerY(); - - dnodes.push(n); - }); - - return dnodes; - } - - static getConnections( - nodeList: Array, - designer: Designer, - items: NodeGraphicsItem[] - ): Array { - const conns = []; - - // we're searching for connections with both left and right socket - // in our selection pool - designer.conns.forEach(con => { - if ( - ItemClipboard.getNodeById(con.leftNode.id, nodeList) && - ItemClipboard.getNodeById(con.rightNode.id, nodeList) - ) { - const c = {}; - c["id"] = con.id; - c["leftNodeId"] = con.leftNode.id; - c["leftNodeOutput"] = con.leftNodeOutput; - c["rightNodeId"] = con.rightNode.id; - c["rightNodeInput"] = con.rightNodeInput; - - conns.push(c); - } - }); - - return conns; - } - - static getNodeById(id: string, nodeList: Array): object { - for (const node of nodeList) if (node["id"] == id) return node; - - return null; - } - - static getItemsBounds(items: GraphicsItem[]): Rect { - const rect: Rect = items[0].getRect(); - for (const item of items) { - const r = item.getRect(); - rect.expandByRect(r); - } - - return rect; - } -} diff --git a/src/lib/designer.ts b/src/lib/designer.ts deleted file mode 100644 index 6d6cbd37..00000000 --- a/src/lib/designer.ts +++ /dev/null @@ -1,945 +0,0 @@ -import { DesignerNode, NodeInput } from "./designer/designernode"; -import { DesignerNodeConn } from "./designer/designerconnection"; -import { DesignerLibrary } from "./designer/library"; -import { Guid } from "./utils"; -import { ImageCanvas } from "./designer/imagecanvas"; -import { Image } from "./designer/image"; -import { buildShaderProgram } from "./designer/gl"; -import { - IntProperty, - FloatProperty, - BoolProperty, - EnumProperty, - ColorProperty, - Property, - PropertyType -} from "./designer/properties"; -import { Color } from "./designer/color"; -import { - DesignerVariable, - DesignerVariableType, - DesignerNodePropertyMap -} from "./designer/designervariable"; - -export class NodeRenderContext { - canvas: HTMLCanvasElement; - gl: WebGLRenderingContext; - fbo: WebGLFramebuffer; - inputs: NodeInput[]; - - textureWidth: number; - textureHeight: number; - - randomSeed: number; -} - -// keeps track of query object that tracks -// node's processing time -// https://www.khronos.org/registry/webgl/extensions/EXT_disjoint_timer_query_webgl2/ -export class NodeRenderTimer { - query: WebGLQuery; - ms: number; - node: DesignerNode; -} - -export enum UpdateMode { - Immediate = "immediate", - Staggered = "staggered" -} - -export class Designer { - canvas: HTMLCanvasElement; - gl: WebGL2RenderingContext; - public texCoordBuffer: WebGLBuffer; - public posBuffer: WebGLBuffer; - vertexShaderSource: string; - fbo: WebGLFramebuffer; - thumbnailProgram: WebGLProgram; - - updateMode: UpdateMode; - - randomSeed: number; - width: number; - height: number; - - nodes: DesignerNode[]; - conns: DesignerNodeConn[]; - - // list of nodes yet to be designed - updateList: DesignerNode[]; - - library: DesignerLibrary; - - //variables - variables: DesignerVariable[]; - - renderContext: NodeRenderContext; - - renderTimers: NodeRenderTimer[]; - queryExt: any; - - // callbacks - onthumbnailgenerated: (DesignerNode, HTMLImageElement) => void; - - // called everytime a node's texture gets updated - // listeners can use this update their CanvasTextures - // by rendering the node's texture with renderNodeTextureToCanvas(node, imageCanvas) - onnodetextureupdated: (DesignerNode, number) => void; - - onnodetimeupdated: (DesignerNode, number) => void; - - public constructor() { - this.width = 1024; - this.height = 1024; - this.randomSeed = 32; - - this.updateMode = UpdateMode.Staggered; - - this.canvas = document.createElement("canvas"); - //document.body.appendChild(this.canvas); - this.canvas.width = this.width; - this.canvas.height = this.height; - this.gl = this.canvas.getContext("webgl2"); - - let result = this.gl.getExtension("EXT_disjoint_timer_query_webgl2"); - if (result) console.log("TIMER QUERY SUPPORTED", result); - else console.log("TIMER QUERY NOT SUPPORTED", result); - this.queryExt = result; - - // floating point textures - result = this.gl.getExtension("EXT_color_buffer_float"); - if (!result) console.log("COLOR BUFFER FLOAT NOT SUPPORTED", result); - - result = this.gl.getExtension("OES_texture_float_linear"); - if (!result) console.log("TEXTURE FLOAT LINEAR NOT SUPPORTED", result); - - result = this.gl.getExtension("EXT_texture_norm16"); - if (!result) console.log("TEXTURE NORM16 NOT SUPPORTED", result); - - this.renderContext = new NodeRenderContext(); - - this.nodes = []; - this.conns = []; - - this.updateList = []; - this.variables = []; - - this.renderTimers = []; - - this.init(); - } - - // evaludate rendertimers - // discards ones that arent ready - // this function should be called at the beginning - // of a render cycle - public calculateNodeProcessingTimes(): NodeRenderTimer[] { - const gl = this.gl; - - let completeTimers: NodeRenderTimer[] = []; - for (let timer of this.renderTimers) { - var available = gl.getQueryParameter( - timer.query, - gl.QUERY_RESULT_AVAILABLE - ); - var disjoint = gl.getParameter(this.queryExt.GPU_DISJOINT_EXT as GLenum); - - if (available && !disjoint) { - // See how much time the rendering of the object took in nanoseconds. - var timeElapsed = gl.getQueryParameter(timer.query, gl.QUERY_RESULT); - timer.ms = timeElapsed / (1000 * 1000); - completeTimers.push(timer); - } - } - - // decided not to empty this each frame since some operations might - // take long, however, something should be done so that dangling - // timers dont stay in the list - //this.renderTimers = []; - - return completeTimers; - } - - public setTextureSize(width: number, height: number) { - //todo: is resizing the canvas even necessary? - this.width = width; - this.height = height; - this.canvas.width = this.width; - this.canvas.height = this.height; - - for (const node of this.nodes) { - node.createTexture(); - this.requestUpdate(node); - } - } - - public randomizeSeed() { - this.setRandomSeed(Math.floor(Math.random() * 256)); - } - - public setRandomSeed(newSeed: number) { - this.randomSeed = newSeed; - // invalidate all nodes - this.invalidateAllNodes(); - } - - public getRandomSeed(): number { - return this.randomSeed; - } - - init() { - this.createVertexBuffers(); - this.createFBO(); - this.createThumbmailProgram(); - } - - updateImmediate() { - this.updateRenderTimers(); - - let updateQuota = 10000000000000; - // fetch random node from update list (having all in sockets that have been updated) and update it - // todo: do only on per update loop - while (this.updateList.length != 0) { - for (const node of this.updateList) { - if (this.haveAllUpdatedLeftNodes(node)) { - // update this node's texture and thumbnail - - // remove from list - this.updateList.splice(this.updateList.indexOf(node), 1); - - // a note about this: - // technically all the subsequent nodes should be updated here, so this.updateList - // wont be touched in this function - // so we avoid messing up our loop since the length of this.updateList wont change - this.generateImageFromNode(node); - - node.needsUpdate = false; - //break;// one per update loop - - updateQuota--; - if (updateQuota < 0) break; - } - } - } - } - - updateStaggered() { - this.updateRenderTimers(); - - const batch: DesignerNode[] = []; - - for (const node of this.updateList) { - if (this.haveAllUpdatedLeftNodes(node)) { - batch.push(node); - } - } - - for (const node of batch) { - // remove from list - batch.splice(batch.indexOf(node), 1); - this.updateList.splice(this.updateList.indexOf(node), 1); - - this.generateImageFromNode(node, false); - - node.needsUpdate = false; - } - } - - get processedNodes() { - return this.nodes.length - this.updateList.length; - } - - get totalNodes() { - return this.nodes.length; - } - - setUpdateMode(updateMode: UpdateMode) { - this.updateMode = updateMode; - } - - update() { - if (this.updateMode == UpdateMode.Immediate) this.updateImmediate(); - else this.updateStaggered(); - } - - updateRenderTimers() { - if (!this.onnodetimeupdated) return; - - let timers = this.calculateNodeProcessingTimes(); - for (let timer of timers) { - this.onnodetimeupdated(timer.node, timer.ms); - } - } - - // checks if all input nodes have needsUpdate set to false - haveAllUpdatedLeftNodes(node: DesignerNode): boolean { - for (const con of this.conns) { - // get connections to this node - if (con.rightNode == node) { - if (con.leftNode.needsUpdate == true) { - // found a node that needs update itself - return false; - } - } - } - - return true; - } - - // adds node to update list - // add subsequent (output) nodes in tree to update list a well, recursively - requestUpdate(node: DesignerNode) { - if (this.updateList.indexOf(node) == -1) { - // not yet in the list, add to list and add dependent nodes - node.needsUpdate = true; // just in case... - this.updateList.push(node); - } - - // add all right connections - for (const con of this.conns) { - if (con.leftNode == node) { - this.requestUpdate(con.rightNode); - } - } - } - - public invalidateAllNodes() { - for (const node of this.nodes) { - this.requestUpdate(node); - } - } - - setLibrary(lib: DesignerLibrary) { - this.library = lib; - } - - // creates node and adds it to scene - createNode(name: string): DesignerNode { - const node = this.library.create(name); - - this.addNode(node); - return node; - } - - createFBO() { - const gl = this.gl; - - this.fbo = gl.createFramebuffer(); - - // gotta create at least a renderbuffer - } - - createVertexBuffers() { - const gl = this.gl; - //var texCoordLocation = gl.getAttribLocation(program, "a_texCoord"); - - // provide texture coordinates for the rectangle. - this.texCoordBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 1.0 - ]), - gl.STATIC_DRAW - ); - //gl.enableVertexAttribArray(texCoordLocation); - //gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); - - this.posBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - -1.0, - -1.0, - 0.0, - 1.0, - -1.0, - 0.0, - -1.0, - 1.0, - 0.0, - -1.0, - 1.0, - 0.0, - 1.0, - -1.0, - 0.0, - 1.0, - 1.0, - 0.0 - ]), - gl.STATIC_DRAW - ); - - gl.bindBuffer(gl.ARRAY_BUFFER, null); - } - - // adds node to scene - // adds it to this.updateList - // if `init` is set to false, node will not be initialized - // useful for undo-redo where node is re-added - addNode(node: DesignerNode, init = true) { - this.nodes.push(node); - if (init) { - node.gl = this.gl; - node.designer = this; - node._init(); - } - this.requestUpdate(node); - } - - getNodeById(nodeId: string): DesignerNode { - for (const node of this.nodes) { - if (node.id == nodeId) return node; - } - return null; - } - - addConnection( - leftNode: DesignerNode, - rightNode: DesignerNode, - rightIndex: string - ) { - const con = new DesignerNodeConn(); - con.leftNode = leftNode; - - con.rightNode = rightNode; - con.rightNodeInput = rightIndex; - - this.conns.push(con); - - // right node needs to be updated - this.requestUpdate(rightNode); - - return con; - } - - removeConnection( - leftNode: DesignerNode, - rightNode: DesignerNode, - rightIndex: string - ) { - for (const con of this.conns) { - if ( - con.leftNode == leftNode && - con.rightNode == rightNode && - con.rightNodeInput == rightIndex - ) { - // right node needs to be updated - this.requestUpdate(rightNode); - - // found our connection, remove - this.conns.splice(this.conns.indexOf(con), 1); - //console.log("removed connection in designer"); - //console.log(con); - - return con; - } - } - - return null; - } - - // todo: double check connections just in case - removeNode(nodeId: string): DesignerNode { - const node = this.getNodeById(nodeId); - if (!node) { - return null; - } - - this.nodes.splice(this.nodes.indexOf(node), 1); - - // it's safe here to pluck this node right out of the update queue - // the connections would have been already removed, triggering - // updates for the previously neighbor nodes already - while (this.updateList.indexOf(node) !== -1) - this.updateList.splice(this.updateList.indexOf(node), 1); - } - - generateImage(name: string): HTMLImageElement { - const node: DesignerNode = this.getNodeByName(name); - return this.generateImageFromNode(node); - } - - // this function generates the image of the node given its input nodes - // if the input nodes arent updated then it will update them - // for every node updated in this function, it emits onthumbnailgenerated(node, thumbnail) - // it returns a thumbnail (an html image) - - generateImageFromNode( - node: DesignerNode, - processInputs: boolean = true - ): HTMLImageElement { - const inputs: NodeInput[] = this.getNodeInputs(node); - - if (processInputs) { - // process input nodes - for (const input of inputs) { - if (input.node.needsUpdate) { - this.generateImageFromNode(input.node); - - // remove from update list since thumbnail has now been generated - input.node.needsUpdate = false; - this.updateList.splice(this.updateList.indexOf(input.node), 1); - } - } - } - - const gl = this.gl; - - let context = this.renderContext; - context.gl = gl; - context.canvas = this.canvas; - context.inputs = inputs; - context.randomSeed = this.randomSeed; - context.fbo = this.fbo; - context.textureWidth = this.width; - context.textureHeight = this.height; - - let dtInMs = 0; - if (node.isCpu()) { - let startTime = Date.now(); - - node.render(context); - - let endTime = Date.now(); - dtInMs = endTime - startTime; - } else { - let query = gl.createQuery(); - gl.beginQuery(this.queryExt.TIME_ELAPSED_EXT, query); - - node.render(context); - gl.endQuery(this.queryExt.TIME_ELAPSED_EXT); - - // create timer - let timer = new NodeRenderTimer(); - timer.node = node; - timer.query = query; - timer.ms = 0; - - this.renderTimers.push(timer); - - dtInMs = -1; - } - - if (this.onnodetextureupdated) { - this.onnodetextureupdated(node, dtInMs); - } - - const thumb = this.generateThumbnailFromNode(node); - if (this.onthumbnailgenerated) { - this.onthumbnailgenerated(node, thumb); - } - - return thumb; - } - - // renders node's texture to an image object - // ensure the node is updated before calling this function - // this function doesnt try to update child nodes - generateThumbnailFromNode(node: DesignerNode) { - const gl = this.gl; - - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - // bind shader - gl.useProgram(this.thumbnailProgram); - - // bind mesh - const posLoc = gl.getAttribLocation(this.thumbnailProgram, "a_pos"); - const texCoordLoc = gl.getAttribLocation( - this.thumbnailProgram, - "a_texCoord" - ); - - // provide texture coordinates for the rectangle. - gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer); - gl.enableVertexAttribArray(posLoc); - gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0); - - gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer); - gl.enableVertexAttribArray(texCoordLoc); - gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); - - // send texture - gl.uniform1i(gl.getUniformLocation(this.thumbnailProgram, "tex"), 0); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, node.tex); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - - // cleanup - gl.disableVertexAttribArray(posLoc); - gl.disableVertexAttribArray(texCoordLoc); - - //var img:HTMLImageElement = document.createElement("image"); - //var img:HTMLImageElement = new Image(this.width, this.height); - //img.src = this.canvas.toDataURL("image/png"); - - // note: this called right after clears the image for some reason - //gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - //return img; - return null; - } - - // render's node's texture then draws it on the given canvas - // used as an alternative to move textures since toDataUrl is - // so computationally expensive - copyNodeTextureToImageCanvas(node: DesignerNode, canvas: ImageCanvas) { - const gl = this.gl; - - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - // bind shader - gl.useProgram(this.thumbnailProgram); - - // bind mesh - const posLoc = gl.getAttribLocation(this.thumbnailProgram, "a_pos"); - const texCoordLoc = gl.getAttribLocation( - this.thumbnailProgram, - "a_texCoord" - ); - - // provide texture coordinates for the rectangle. - gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer); - gl.enableVertexAttribArray(posLoc); - gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0); - - gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer); - gl.enableVertexAttribArray(texCoordLoc); - gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); - - // send texture - gl.uniform1i(gl.getUniformLocation(this.thumbnailProgram, "tex"), 0); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, node.tex); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - - // cleanup - gl.disableVertexAttribArray(posLoc); - gl.disableVertexAttribArray(texCoordLoc); - - // force rendering to be complete - //gl.flush(); - - canvas.copyFromCanvas(this.canvas, true); - } - - createThumbmailProgram() { - const prog = buildShaderProgram( - this.gl, - `precision mediump float; - - attribute vec3 a_pos; - attribute vec2 a_texCoord; - - // the texCoords passed in from the vertex shader. - varying vec2 v_texCoord; - - void main() { - gl_Position = vec4(a_pos,1.0); - v_texCoord = a_texCoord; - }`, - `precision mediump float; - varying vec2 v_texCoord; - uniform sampler2D tex; - - vec4 process(vec2 uv); - - void main() { - gl_FragColor = texture2D(tex,v_texCoord); - }` - ); - - this.thumbnailProgram = prog; - } - - getNodeByName(exportName: string): DesignerNode { - for (const node of this.nodes) { - if (node.exportName == exportName) return node; - } - - return null; - } - - getNodeInputs(node: DesignerNode): NodeInput[] { - const inputs: NodeInput[] = []; - - for (const con of this.conns) { - if (con.rightNode == node) { - const input = new NodeInput(); - input.name = con.rightNodeInput; - input.node = con.leftNode; - inputs.push(input); - } - } - - return inputs; - } - - public addVariable( - name: string, - displayName: string, - varType: DesignerVariableType - ): DesignerVariable { - //todo: throw exception if variable already exists? - - const variable = new DesignerVariable(); - variable.type = varType; - variable.id = Guid.newGuid(); - - switch (varType) { - case DesignerVariableType.Int: - variable.property = new IntProperty(name, displayName, 0); - break; - case DesignerVariableType.Float: - variable.property = new FloatProperty(name, displayName, 0); - break; - case DesignerVariableType.Bool: - variable.property = new BoolProperty(name, displayName, false); - break; - case DesignerVariableType.Enum: - variable.property = new EnumProperty(name, displayName, []); - break; - case DesignerVariableType.Color: - variable.property = new ColorProperty(name, displayName, new Color()); - break; - } - - this.variables.push(variable); - return variable; - } - - // todo: keep reference inside node's property - public mapNodePropertyToVariable( - varName: string, - node: DesignerNode, - nodePropName: string - ) { - const variable = this.findVariable(varName); - if (variable == null) return; //todo: throw exception? - - const map = new DesignerNodePropertyMap(); - map.node = node; - map.propertyName = nodePropName; - - variable.nodes.push(map); - } - - //todo: remove property map - - public setVariable(name: string, value: any) { - const variable = this.findVariable(name); - if (variable) { - //todo: throw exception for invalid types being set - variable.property.setValue(value); - - //update each node's variables - for (const nodeMap of variable.nodes) { - //if (nodeMap.node.hasProperty(nodeMap.propertyName))// just incase - nodeMap.node.setProperty(nodeMap.propertyName, value); - } - } else { - // throw exception? - } - } - - public findVariable(name: string) { - for (const variable of this.variables) - if (variable.property.name == name) return variable; - return null; - } - - public hasVariable(name: string): boolean { - for (const variable of this.variables) - if (variable.property.name == name) return true; - return false; - } - - public variableCount(): number { - return this.variables.length; - } - - public save(): any { - const nodes = []; - for (const node of this.nodes) { - const n = {}; - n["id"] = node.id; - n["typeName"] = node.typeName; - n["exportName"] = node.exportName; - - n["randomSeed"] = node.randomSeed; - //n["inputs"] = node.inputs;// not needed imo - - const props = {}; - for (const prop of node.properties) { - if (prop.type == PropertyType.Image) { - // serialize texture as png - let imageData = (prop.getValue() as Image).serialize(); - props[prop.name] = imageData; - } else { - props[prop.name] = prop.getValue(); - } - } - n["properties"] = props; - - nodes.push(n); - } - - const connections = []; - for (const con of this.conns) { - const c = {}; - c["id"] = con.id; - c["leftNodeId"] = con.leftNode.id; - c["leftNodeOutput"] = con.leftNodeOutput; - c["rightNodeId"] = con.rightNode.id; - c["rightNodeInput"] = con.rightNodeInput; - - connections.push(c); - } - - const variables = []; - for (const dvar of this.variables) { - const v = {}; - v["id"] = dvar.id; - v["type"] = dvar.type; - v["property"] = dvar.property; - - const nodeIds = []; - for (const n of dvar.nodes) { - nodeIds.push({ - nodeId: n.node.id, - name: n.propertyName - }); - } - v["linkedProperties"] = nodeIds; - variables.push(v); - // console.log(v); - } - - const data = {}; - data["nodes"] = nodes; - data["connections"] = connections; - data["variables"] = variables; - return data; - } - - static load(data: any, lib: DesignerLibrary): Designer { - // console.log(data); - const d = new Designer(); - const nodes = data["nodes"]; - for (const node of nodes) { - const n = lib.create(node["typeName"]); - n.exportName = node["exportName"]; - n.id = node["id"]; - - n.randomSeed = node["randomSeed"] || 0; - - // add node to it's properties will be initialized - // todo: separate setting properties and inputs from setting shader in node - d.addNode(n); - - // add properties - const properties = node["properties"]; - for (const prop in properties) { - n.setProperty(prop, properties[prop]); - } - } - - const connections = data["connections"]; - for (const con of connections) { - //var c = d.addConnection() - const left = d.getNodeById(con.leftNodeId); - const right = d.getNodeById(con.rightNodeId); - - // todo: support left index - d.addConnection(left, right, con.rightNodeInput); - } - /* - for(let dvar of this.variables) { - var v = {}; - v["id"] = dvar.id; - v["type"] = dvar.type; - v["property"] = dvar.property; - - var nodeIds = new Array(); - for(let n of dvar.nodes) { - nodeIds.push({ - nodeId:n.node.id, - name:n.propertyName - }); - } - v["linkedProperties"] = nodeIds; - variables.push(v); - console.log(v); - } - */ - if (data.variables) { - const variables = data.variables; - for (const v of variables) { - //this.addVariable(v.name, v.displayName, ) - - const dvar = d.addVariable( - v.property.name, - v.property.displayName, - v.type - ); - dvar.id = v.id; - // copy values over to the property - switch (dvar.type) { - case DesignerVariableType.Float: - (dvar.property).copyValuesFrom( - v.property - ); - break; - - case DesignerVariableType.Int: - (dvar.property).copyValuesFrom( - v.property - ); - break; - - case DesignerVariableType.Bool: - (dvar.property).copyValuesFrom( - v.property - ); - break; - - case DesignerVariableType.Enum: - (dvar.property).copyValuesFrom( - v.property - ); - break; - - case DesignerVariableType.Color: - (dvar.property).copyValuesFrom( - v.property - ); - break; - } - - // link properties - for (const lp of (v).linkedProperties) { - const node = d.getNodeById(lp.nodeId); - d.mapNodePropertyToVariable(v.property.name, node, lp.name); - } - } - } - - return d; - } -} diff --git a/src/lib/designer/color.ts b/src/lib/designer/color.ts deleted file mode 100644 index 8b3a1c21..00000000 --- a/src/lib/designer/color.ts +++ /dev/null @@ -1,52 +0,0 @@ -// values range from 0 to 1 -export class Color { - public r = 0.0; - public g = 0.0; - public b = 0.0; - public a = 1.0; - - public constructor(r = 0.0, g = 0.0, b = 0.0, a = 1.0) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } - public clone(): Color { - return new Color(this.r, this.g, this.b, this.a); - } - - public copy(col: Color) { - this.r = col.r; - this.g = col.g; - this.b = col.b; - this.a = col.a; - } - - public lerp(to: Color, t: number) { - this.r = this.r * t + to.r * (1.0 - t); - this.g = this.g * t + to.g * (1.0 - t); - this.b = this.b * t + to.b * (1.0 - t); - this.a = this.a * t + to.a * (1.0 - t); - } - - public static parse(hex: string): Color { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - if (result) { - const c = new Color(); - c.r = parseInt(result[1], 16) / 255; - c.g = parseInt(result[2], 16) / 255; - c.b = parseInt(result[3], 16) / 255; - return c; - } else { - return new Color(); - } - } - - public toHex(): string { - //https://stackoverflow.com/questions/596467/how-do-i-convert-a-float-number-to-a-whole-number-in-javascript - const r = ~~(this.r * 255); - const g = ~~(this.g * 255); - const b = ~~(this.b * 255); - return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); - } -} diff --git a/src/lib/designer/designerconnection.ts b/src/lib/designer/designerconnection.ts deleted file mode 100644 index 33ba9b10..00000000 --- a/src/lib/designer/designerconnection.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { DesignerNode } from "./designernode"; -import { Guid } from "../utils"; - -export class DesignerNodeConn { - public id: string = Guid.newGuid(); - - leftNode: DesignerNode; - leftNodeOutput = ""; // if null, use first output - - rightNode: DesignerNode; - rightNodeInput: string; -} diff --git a/src/lib/designer/designernode.ts b/src/lib/designer/designernode.ts deleted file mode 100644 index b398b3b7..00000000 --- a/src/lib/designer/designernode.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { Guid } from "../utils"; -import { Designer, NodeRenderContext } from "../designer"; -import { - Property, - FloatProperty, - IntProperty, - BoolProperty, - EnumProperty, - ColorProperty, - StringProperty, - GradientProperty, - IPropertyHolder, - ImageProperty, - PropertyType, - PropertyGroup -} from "./properties"; -import { buildShaderProgram } from "./gl"; -import { Color } from "./color"; -import { Gradient } from "./gradient"; -import { Image } from "./image"; - -export class NodeInput { - public node: DesignerNode; - public name: string; -} - -export class DesignerNode implements IPropertyHolder { - public id: string = Guid.newGuid(); - public title: string; - public typeName: string; // added when node is created from library - - public gl: WebGLRenderingContext; - tex: WebGLTexture; - public designer: Designer; - exportName: string; - - randomSeed: number = 0; - - inputs: string[] = []; - properties: Property[] = []; - propertyGroups: PropertyGroup[] = []; - - // tells scene to update the texture next frame - needsUpdate = true; - - // callbacks - onthumbnailgenerated: (DesignerNode, HTMLImageElement) => void; - - // an update is requested when: - // a property is changed - // a new connection is made - // a connection is removedscientist - // - // all output connected nodes are invalidated as well - private requestUpdate() { - this.designer.requestUpdate(this); - } - - getPixelData(): Uint16Array { - return null; - } - - getTextureData(): ArrayBuffer { - return null; - } - - setRandomSeed(seed: number) { - this.randomSeed = seed; - this.requestUpdate(); - } - - public render(context: NodeRenderContext) {} - - public isCpu() { - return true; - } - - public getInputs(): string[] { - return this.inputs; - } - - protected addInput(name: string) { - this.inputs.push(name); - } - - public setProperty(name: string, value: any) { - const prop = this.properties.find(x => { - return x.name == name; - }); - - if (prop) { - if (prop.type == PropertyType.Image) { - prop.setValue(value, () => { - this.requestUpdate(); - }); - } else { - prop.setValue(value); - this.requestUpdate(); - } - } - - // for (let prop of this.properties) { - // console.log("prop iter"); - // console.log(prop); - // console.log(prop.name == name); - // if (prop.name == name) { - // prop.setValue(value); - // this.requestUpdate(); - // } - // } - } - - public _init() { - //this.inputs = new Array(); - //this.properties = new Array(); - this.createTexture(); - - this.init(); - } - - protected init() {} - - // creates opengl texture for this node - // gets the height from the scene - // if the texture is already created, delete it and recreate it - createTexture() { - const gl = this.gl as WebGL2RenderingContext; - - if (this.tex) { - gl.deleteTexture(this.tex); - this.tex = null; - } - - const tex = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, tex); - - const level = 0; - const internalFormat = gl.RGBA16F; - const border = 0; - const format = gl.RGBA; - const type = gl.FLOAT; - const data = null; - gl.texImage2D( - gl.TEXTURE_2D, - level, - internalFormat, - this.designer.width, - this.designer.height, - border, - format, - type, - data - ); - - // set the filtering so we don't need mips - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - - gl.bindTexture(gl.TEXTURE_2D, null); - - this.tex = tex; - } - - createGroup(name: string): PropertyGroup { - const group = new PropertyGroup(); - group.name = name; - this.propertyGroups.push(group); - - return group; - } - - // PROPERTY FUNCTIONS - addIntProperty( - id: string, - displayName: string, - defaultVal = 1, - minVal = 1, - maxVal = 100, - increment = 1 - ): IntProperty { - const prop = new IntProperty(id, displayName, defaultVal); - prop.minValue = minVal; - prop.maxValue = maxVal; - prop.step = increment; - - this.properties.push(prop); - return prop; - } - - addFloatProperty( - id: string, - displayName: string, - defaultVal = 1, - minVal = 1, - maxVal = 100, - increment = 1 - ): FloatProperty { - const prop = new FloatProperty(id, displayName, defaultVal); - prop.minValue = minVal; - prop.maxValue = maxVal; - prop.step = increment; - - this.properties.push(prop); - return prop; - } - - addBoolProperty( - id: string, - displayName: string, - defaultVal = false - ): BoolProperty { - const prop = new BoolProperty(id, displayName, defaultVal); - - this.properties.push(prop); - return prop; - } - - addEnumProperty( - id: string, - displayName: string, - defaultVal: string[] = [] - ): EnumProperty { - const prop = new EnumProperty(id, displayName, defaultVal); - - this.properties.push(prop); - return prop; - } - - addColorProperty( - id: string, - displayName: string, - defaultVal: Color - ): ColorProperty { - const prop = new ColorProperty(id, displayName, defaultVal); - - this.properties.push(prop); - return prop; - } - - addStringProperty( - id: string, - displayName: string, - defaultVal = "" - ): StringProperty { - const prop = new StringProperty(id, displayName, defaultVal); - - this.properties.push(prop); - return prop; - } - - addGradientProperty( - id: string, - displayName: string, - defaultVal: Gradient - ): GradientProperty { - const prop = new GradientProperty(id, displayName, defaultVal); - - this.properties.push(prop); - return prop; - } - - addImageProperty( - id: string, - displayName: string, - defaultVal: Image - ): ImageProperty { - const prop = new ImageProperty(id, displayName, defaultVal); - - this.properties.push(prop); - return prop; - } -} diff --git a/src/lib/designer/designervariable.ts b/src/lib/designer/designervariable.ts deleted file mode 100644 index f91b1b85..00000000 --- a/src/lib/designer/designervariable.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DesignerNode } from "./designernode"; -import { Property } from "./properties"; - -export enum DesignerVariableType { - None = 0, - Float = 1, - Int = 2, - Bool = 3, - Enum = 4, - Color = 5 - //Gradient -} - -export class DesignerNodePropertyMap { - public node: DesignerNode; - public propertyName: string; -} - -export class DesignerVariable { - id: string; - - type: DesignerVariableType; - // used to keep the value in bounds - property: Property; - - nodes: DesignerNodePropertyMap[] = []; -} diff --git a/src/lib/designer/gl.ts b/src/lib/designer/gl.ts deleted file mode 100644 index a827060c..00000000 --- a/src/lib/designer/gl.ts +++ /dev/null @@ -1,202 +0,0 @@ -export function createTexture( - gl: WebGLRenderingContext, - width: number, - height: number -): WebGLTexture { - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - - // Set the parameters so we can render any size image. - //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - - // Upload the image into the texture. - gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, null); - - return texture; -} - -export enum TextureDataType { - Uint8, - Uint16, - Float16, - Float32 -} - -// assumes all textures are RGBA -export function createTextureWithType( - gl: WebGL2RenderingContext, - dataType: TextureDataType, - width: number, - height: number -): WebGLTexture { - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - - let ext = gl.getExtension("EXT_texture_norm16"); - - // format of data being passed to GPU - // let internalFormat: number = gl.RGBA; - // let type: number = gl.UNSIGNED_BYTE; - - // // format and components the texture should actually store - // let format: number = gl.RGBA8; - - // webgl has a guide for the combination of these - // properties(internalFormat, format, type) - // https://www.khronos.org/files/webgl20-reference-guide.pdf - - // Upload the image into the texture. - switch (dataType) { - case TextureDataType.Uint8: - // gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, null); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA8, - width, - height, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - null - ); - break; - case TextureDataType.Uint16: - // gl.texImage2D( - // gl.TEXTURE_2D, - // 0, - // gl.RGBA16UI, - // width, - // height, - // 0, - // gl.RGBA, - // gl.UNSIGNED_SHORT, - // null - // ); - - gl.texImage2D( - gl.TEXTURE_2D, - 0, - ext.RGBA16_EXT, - width, - height, - 0, - gl.RGBA, - gl.UNSIGNED_SHORT, - null - ); - break; - case TextureDataType.Float16: - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA16F, - width, - height, - 0, - gl.RGBA, - gl.FLOAT, - null - ); - break; - case TextureDataType.Float32: - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA32F, - width, - height, - 0, - gl.RGBA, - gl.FLOAT, - null - ); - break; - } - - // gl.texImage2D( - // gl.TEXTURE_2D, - // 0, - // internalFormat, - // width, - // height, - // 0, - // format, - // type, - // null - // ); - - // Set the parameters so we can render any size image. - //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - //gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - - return texture; -} - -export function getShaderSource(id) { - const shaderScript = document.getElementById(id); - if (!shaderScript) { - return null; - } - - let str = ""; - let k = shaderScript.firstChild; - while (k) { - if (k.nodeType == 3) { - str += k.textContent; - } - k = k.nextSibling; - } - - return str; -} - -export function compileShader(gl, source, shaderType) { - const shader = gl.createShader(shaderType); - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - alert(gl.getShaderInfoLog(shader)); - return null; - } - - return shader; -} - -export function buildShaderProgram(gl, vertSource, fragSource) { - const vertexShader = compileShader(gl, vertSource, gl.VERTEX_SHADER); - const fragmentShader = compileShader(gl, fragSource, gl.FRAGMENT_SHADER); - - const shaderProgram = gl.createProgram(); - gl.attachShader(shaderProgram, vertexShader); - gl.attachShader(shaderProgram, fragmentShader); - gl.linkProgram(shaderProgram); - - if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { - alert("Could not initialise shaders"); - } - - gl.useProgram(shaderProgram); - /* - shaderProgram.samplerLoc = gl.getUniformLocation(shaderProgram, "tex"); - - shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition"); - gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute); - - shaderProgram.vertexUVAttribute = gl.getAttribLocation(shaderProgram, "aVertexUV"); - gl.enableVertexAttribArray(shaderProgram.vertexUVAttribute); - */ - - return shaderProgram; -} diff --git a/src/lib/designer/gpudesignernode.ts b/src/lib/designer/gpudesignernode.ts deleted file mode 100644 index 79f9c676..00000000 --- a/src/lib/designer/gpudesignernode.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { Guid } from "../utils"; -import { Designer, NodeRenderContext } from "../designer"; -import { - Property, - FloatProperty, - IntProperty, - BoolProperty, - EnumProperty, - ColorProperty, - StringProperty, - GradientProperty, - IPropertyHolder -} from "./properties"; -import { buildShaderProgram } from "./gl"; -import { Color } from "./color"; -import { Gradient } from "./gradient"; -import { DesignerNode } from "./designernode"; - -export class GpuDesignerNode extends DesignerNode { - // tex: WebGLTexture; - readFbo: WebGLFramebuffer; - //program:WebGLShader; - source: string; // shader code - shaderProgram: WebGLProgram; - - public isCpu() { - return false; - } - - // https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels - // returns copy of data - getPixelData(): Uint16Array { - const gl = this.gl as WebGL2RenderingContext; - const width = this.designer.width; - const height = this.designer.height; - const data = new Uint16Array(width * height * 4); - - gl.bindFramebuffer(gl.FRAMEBUFFER, this.readFbo); - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - this.tex, - 0 - ); - if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) { - gl.readPixels(0, 0, width, height, gl.RGBA, gl.HALF_FLOAT, data); - // todo: check for errors in this operation - // gl.readPixels( - // 0, - // 0, - // this.designer.width, - // this.designer.height, - // gl.RGBA, - // gl.HALF_FLOAT, - // data - // ); - } else { - alert("getPixelData: unable to read from FBO"); - } - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - - return data; - } - - public render(context: NodeRenderContext) { - const inputs = context.inputs; - const gl = this.gl; - - gl.bindFramebuffer(gl.FRAMEBUFFER, context.fbo); - gl.activeTexture(gl.TEXTURE0); - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - this.tex, - 0 - ); - - gl.viewport(0, 0, context.textureWidth, context.textureHeight); - - // bind texture to fbo - //gl.clearColor(0,0,1,1); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - // bind shader - gl.useProgram(this.shaderProgram); - - // clear all inputs - for (const input of this.inputs) { - gl.activeTexture(gl.TEXTURE0 + texIndex); - gl.bindTexture(gl.TEXTURE_2D, null); - - gl.uniform1i(gl.getUniformLocation(this.shaderProgram, input), 0); - - gl.uniform1i( - gl.getUniformLocation(this.shaderProgram, input + "_connected"), - 0 - ); - } - - // pass inputs for rendering - var texIndex = 0; - for (const input of inputs) { - gl.activeTexture(gl.TEXTURE0 + texIndex); - gl.bindTexture(gl.TEXTURE_2D, input.node.tex); - gl.uniform1i( - gl.getUniformLocation(this.shaderProgram, input.name), - texIndex - ); - gl.uniform1i( - gl.getUniformLocation(this.shaderProgram, input.name + "_connected"), - 1 - ); - //console.log("bound texture " + texIndex); - texIndex++; - } - - // pass seed - gl.uniform1f( - gl.getUniformLocation(this.shaderProgram, "_seed"), - this.randomSeed + this.designer.getRandomSeed() - ); - - // texture size - gl.uniform2f( - gl.getUniformLocation(this.shaderProgram, "_textureSize"), - this.designer.width, - this.designer.height - ); - - // pass properties - for (const prop of this.properties) { - if (prop instanceof FloatProperty) { - gl.uniform1f( - gl.getUniformLocation(this.shaderProgram, "prop_" + prop.name), - (prop as FloatProperty).value - ); - } - if (prop instanceof IntProperty) { - gl.uniform1i( - gl.getUniformLocation(this.shaderProgram, "prop_" + prop.name), - (prop as IntProperty).value - ); - } - if (prop instanceof BoolProperty) { - gl.uniform1i( - gl.getUniformLocation(this.shaderProgram, "prop_" + prop.name), - (prop as BoolProperty).value == false ? 0 : 1 - ); - } - if (prop instanceof EnumProperty) { - gl.uniform1i( - gl.getUniformLocation(this.shaderProgram, "prop_" + prop.name), - (prop as EnumProperty).index - ); - } - if (prop instanceof ColorProperty) { - const col = (prop as ColorProperty).value; - //console.log("color: ", col); - gl.uniform4f( - gl.getUniformLocation(this.shaderProgram, "prop_" + prop.name), - col.r, - col.g, - col.b, - col.a - ); - } - if (prop instanceof GradientProperty) { - const gradient = (prop as GradientProperty).value; - - gl.uniform1i( - gl.getUniformLocation( - this.shaderProgram, - "prop_" + prop.name + ".numPoints" - ), - gradient.points.length - ); - - for (let i = 0; i < gradient.points.length; i++) { - const point = gradient.points[i]; - const col = point.color; - - gl.uniform3f( - gl.getUniformLocation( - this.shaderProgram, - "prop_" + prop.name + ".colors[" + i + "]" - ), - col.r, - col.g, - col.b - ); - - gl.uniform1f( - gl.getUniformLocation( - this.shaderProgram, - "prop_" + prop.name + ".positions[" + i + "]" - ), - point.t - ); - } - } - } - - // bind mesh - const posLoc = gl.getAttribLocation(this.shaderProgram, "a_pos"); - const texCoordLoc = gl.getAttribLocation(this.shaderProgram, "a_texCoord"); - - // provide texture coordinates for the rectangle. - gl.bindBuffer(gl.ARRAY_BUFFER, this.designer.posBuffer); - gl.enableVertexAttribArray(posLoc); - gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0); - - gl.bindBuffer(gl.ARRAY_BUFFER, this.designer.texCoordBuffer); - gl.enableVertexAttribArray(texCoordLoc); - gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - - gl.disableVertexAttribArray(posLoc); - gl.disableVertexAttribArray(texCoordLoc); - - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - } - - public _init() { - //this.inputs = new Array(); - //this.properties = new Array(); - - this.readFbo = this.gl.createFramebuffer(); - this.createTexture(); - - this.init(); - } - - // #source gets appended to fragment shader - buildShader(source: string) { - const vertSource = `#version 300 es - precision highp float; - - in vec3 a_pos; - in vec2 a_texCoord; - - // the texCoords passed in from the vertex shader. - out vec2 v_texCoord; - - void main() { - gl_Position = vec4(a_pos,1.0); - v_texCoord = a_texCoord; - }`; - - let fragSource = `#version 300 es - precision highp float; - in vec2 v_texCoord; - - #define GRADIENT_MAX_POINTS 32 - - vec4 process(vec2 uv); - void initRandom(); - - uniform vec2 _textureSize; - - out vec4 fragColor; - - void main() { - initRandom(); - vec4 result = process(v_texCoord); - fragColor = clamp(result, 0.0, 1.0); - } - - `; - - fragSource = - fragSource + - this.createRandomLib() + - this.createGradientLib() + - this.createCodeForInputs() + - this.createCodeForProps() + - "#line 0\n" + - source; - - this.shaderProgram = buildShaderProgram(this.gl, vertSource, fragSource); - } - - createRandomLibOld(): string { - // float _seed = `+this.designer.getRandomSeed().toFixed(1)+`; - const code = ` - // this offsets the random start (should be a uniform) - uniform float _seed; - // this is the starting number for the rng - // (should be set from the uv coordinates so it's unique per pixel) - vec2 _randomStart; - - float _rand(vec2 co){ - return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); - } - - //todo: test variance! - vec2 _rand2(vec2 co){ - return vec2(_rand(co), _rand(co + vec2(0.0001, 0.0001))); - } - - float randomFloat(int index) - { - return _rand(_randomStart + vec2(_seed) + vec2(index)); - } - - float randomVec2(int index) - { - return _rand(_randomStart + vec2(_seed) + vec2(index)); - } - - float randomFloat(int index, float start, float end) - { - float r = _rand(_randomStart + vec2(_seed) + vec2(index)); - return start + r*(end-start); - } - - int randomInt(int index, int start, int end) - { - float r = _rand(_randomStart + vec2(_seed) + vec2(index)); - return start + int(r*float(end-start)); - } - - bool randomBool(int index) - { - return _rand(_randomStart + vec2(_seed) + vec2(index)) > 0.5; - } - - void initRandom() - { - _randomStart = v_texCoord; - } - `; - - return code; - } - - createRandomLib(): string { - // float _seed = `+this.designer.getRandomSeed().toFixed(1)+`; - const code = ` - // this offsets the random start (should be a uniform) - uniform float _seed; - // this is the starting number for the rng - // (should be set from the uv coordinates so it's unique per pixel) - vec2 _randomStart; - - // gives a much better distribution at 1 - #define RANDOM_ITERATIONS 1 - - #define HASHSCALE1 443.8975 - #define HASHSCALE3 vec3(443.897, 441.423, 437.195) - #define HASHSCALE4 vec4(443.897, 441.423, 437.195, 444.129) - - // 1 out, 2 in... - float hash12(vec2 p) - { - vec3 p3 = fract(vec3(p.xyx) * HASHSCALE1); - p3 += dot(p3, p3.yzx + 19.19); - return fract((p3.x + p3.y) * p3.z); - } - - /// 2 out, 2 in... - vec2 hash22(vec2 p) - { - vec3 p3 = fract(vec3(p.xyx) * HASHSCALE3); - p3 += dot(p3, p3.yzx+19.19); - return fract((p3.xx+p3.yz)*p3.zy); - - } - - - float _rand(vec2 uv) - { - float a = 0.0; - for (int t = 0; t < RANDOM_ITERATIONS; t++) - { - float v = float(t+1)*.152; - // 0.005 is a good value - vec2 pos = (uv * v); - a += hash12(pos); - } - - return a/float(RANDOM_ITERATIONS); - } - - vec2 _rand2(vec2 uv) - { - vec2 a = vec2(0.0); - for (int t = 0; t < RANDOM_ITERATIONS; t++) - { - float v = float(t+1)*.152; - // 0.005 is a good value - vec2 pos = (uv * v); - a += hash22(pos); - } - - return a/float(RANDOM_ITERATIONS); - } - - float randomFloat(int index) - { - return _rand(_randomStart + vec2(_seed) + vec2(index)); - } - - float randomVec2(int index) - { - return _rand(_randomStart + vec2(_seed) + vec2(index)); - } - - float randomFloat(int index, float start, float end) - { - float r = _rand(_randomStart + vec2(_seed) + vec2(index)); - return start + r*(end-start); - } - - int randomInt(int index, int start, int end) - { - float r = _rand(_randomStart + vec2(_seed) + vec2(index)); - return start + int(r*float(end-start)); - } - - bool randomBool(int index) - { - return _rand(_randomStart + vec2(_seed) + vec2(index)) > 0.5; - } - - void initRandom() - { - _randomStart = v_texCoord; - } - `; - - return code; - } - - createGradientLib(): string { - // float _seed = `+this.designer.getRandomSeed().toFixed(1)+`; - const code = ` - struct Gradient { - vec3 colors[GRADIENT_MAX_POINTS]; - float positions[GRADIENT_MAX_POINTS]; - int numPoints; - }; - - // assumes points are sorted - vec3 sampleGradient(vec3 colors[GRADIENT_MAX_POINTS], float positions[GRADIENT_MAX_POINTS], int numPoints, float t) - { - if (numPoints == 0) - return vec3(1,0,0); - - if (numPoints == 1) - return colors[0]; - - // here at least two points are available - if (t <= positions[0]) - return colors[0]; - - int last = numPoints - 1; - if (t >= positions[last]) - return colors[last]; - - // find two points in-between and lerp - - for(int i = 0; i < numPoints-1;i++) { - if (positions[i+1] > t) { - vec3 colorA = colors[i]; - vec3 colorB = colors[i+1]; - - float t1 = positions[i]; - float t2 = positions[i+1]; - - float lerpPos = (t - t1)/(t2 - t1); - return mix(colorA, colorB, lerpPos); - - } - - } - - return vec3(0,0,0); - } - - vec3 sampleGradient(Gradient gradient, float t) - { - return sampleGradient(gradient.colors, gradient.positions, gradient.numPoints, t); - } - `; - - return code; - } - - createCodeForInputs() { - let code = ""; - - for (const input of this.inputs) { - code += "uniform sampler2D " + input + ";\n"; - code += "uniform bool " + input + "_connected;\n"; - } - - return code; - } - - createCodeForProps() { - let code = ""; - - //console.log(this.properties); - //console.log(typeof FloatProperty); - - for (const prop of this.properties) { - //code += "uniform sampler2D " + input + ";\n"; - if (prop instanceof FloatProperty) { - code += "uniform float prop_" + prop.name + ";\n"; - } - if (prop instanceof IntProperty) { - code += "uniform int prop_" + prop.name + ";\n"; - } - if (prop instanceof BoolProperty) { - code += "uniform bool prop_" + prop.name + ";\n"; - } - if (prop instanceof EnumProperty) { - code += "uniform int prop_" + prop.name + ";\n"; - } - if (prop instanceof ColorProperty) { - code += "uniform vec4 prop_" + prop.name + ";\n"; - } - if (prop instanceof GradientProperty) { - // code += "uniform struct prop_" + prop.name + " {\n"; - // code += "vec3 colors[GRADIENT_MAX_POINTS];\n"; - // code += "vec3 positions[GRADIENT_MAX_POINTS];\n"; - // code += "int numPoints;\n"; - // code += "};"; - - code += "uniform Gradient prop_" + prop.name + ";\n"; - } - } - - code += "\n"; - - return code; - } -} diff --git a/src/lib/designer/gradient.ts b/src/lib/designer/gradient.ts deleted file mode 100644 index 9a80e412..00000000 --- a/src/lib/designer/gradient.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Color } from "./color"; - -export class GradientPoint { - // position on gradient - t: number; - - // color of point - color: Color; -} - -export class Gradient { - points: GradientPoint[]; - - constructor() { - this.points = []; - } - - addPoint(t: number, color: Color): GradientPoint { - const point = new GradientPoint(); - point.t = t; - point.color = color; - - this.points.push(point); - this.sort(); - - return point; - } - - removePoint(point: GradientPoint) { - this.points.splice(this.points.indexOf(point), 1); - } - - clear() { - this.points = []; - } - - sort() { - this.points.sort(function(a: GradientPoint, b: GradientPoint) { - return a.t - b.t; - }); - } - - sample(t: number): Color { - if (this.points.length == 0) return new Color(); - if (this.points.length == 1) return this.points[0].color.clone(); - - // here at least two points are available - if (t < this.points[0].t) return this.points[0].color.clone(); - - const last = this.points.length - 1; - if (t > this.points[last].t) return this.points[last].color.clone(); - - // find two points and lerp - for (let i = 0; i < this.points.length - 1; i++) { - if (this.points[i + 1].t > t) { - const p1 = this.points[i]; - const p2 = this.points[i + 1]; - - const lerpPos = (t - p1.t) / (p2.t - p1.t); - const color = new Color(); - color.copy(p1.color); - color.lerp(p2.color, lerpPos); - - return color; - } - } - - // should never get to this point - return new Color(); - } - - clone() { - const grad = new Gradient(); - grad.clear(); - - for (const p of this.points) { - grad.addPoint(p.t, p.color.clone()); - } - - return grad; - } - - public static parse(obj: any) { - const gradient = new Gradient(); - for (const p of obj.points) { - const t = p.t; - const color = new Color(); - color.copy(p.color); - - gradient.addPoint(t, color); - } - - return gradient; - } - - public static default() { - const gradient = new Gradient(); - gradient.addPoint(0, new Color(0, 0, 0, 1.0)); - gradient.addPoint(1, new Color(1, 1, 1, 1.0)); - - return gradient; - } -} diff --git a/src/lib/designer/image.ts b/src/lib/designer/image.ts deleted file mode 100644 index e2173129..00000000 --- a/src/lib/designer/image.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { NativeImage } from "electron"; - -enum ImageSource { - Inline = "inline", // in .texture file - Embedded = "embedded" // in .texlab package as a seperate file -} - -export class Image { - canvas: HTMLCanvasElement; - path: string; - src: string; - type: ImageSource; - - constructor( - path: string, - canvasSource: CanvasImageSource, - width: number, - height: number - ) { - this.init(path, canvasSource, width, height); - } - - private init( - path: string, - canvasSource: CanvasImageSource, - width: number, - height: number - ) { - if (canvasSource != null) { - let canvas = document.createElement("canvas") as HTMLCanvasElement; - canvas.width = width; - canvas.height = height; - - let ctx = canvas.getContext("2d"); - ctx.drawImage(canvasSource, 0, 0); - - this.canvas = canvas; - } - - this.path = path; - } - - get isEmpty() { - return this.canvas == null; - } - - static empty(): Image { - return new Image(null, null, 0, 0); - } - - static load(path: string): Image { - let nativeImage = NativeImage.createFromPath(path); - let img: HTMLImageElement = document.createElement( - "image" - ) as HTMLImageElement; - - img.src = nativeImage.toDataURL(); - - return new Image(path, img, img.width, img.height); - } - - clone(): Image { - return new Image(this.path, this.canvas, this.width, this.height); - } - - get width() { - return this.canvas.width; - } - - get height() { - return this.canvas.height; - } - - serialize() { - return { - type: ImageSource.Inline, - src: this.canvas.toDataURL("image/png") - }; - } - - // handle async - deserialize(obj: any, completeCallback: () => void = null) { - console.log(obj); - - // note: only works with inline images right now - if (obj.type !== ImageSource.Inline) return; - - let img: HTMLImageElement = document.createElement( - "img" - ) as HTMLImageElement; - - img.onload = () => { - console.log("image loaded"); - console.log(img); - console.log(img.width); - console.log(img.height); - - // // flip image - // let canvas = document.createElement("canvas"); - // canvas.width = img.width; - // canvas.height = img.height; - - // let ctx = canvas.getContext("2d"); - // ctx.save(); - // ctx.clearRect(0, 0, canvas.width, canvas.height); - // //ctx.translate(0, img.height); - // //ctx.scale(1, -1); - // ctx.drawImage(img, 0, 0, img.width, img.height); - // ctx.restore(); - - this.init("", img, img.width, img.height); - - completeCallback(); - }; - - img.src = obj.src; - } -} diff --git a/src/lib/designer/imagecanvas.ts b/src/lib/designer/imagecanvas.ts deleted file mode 100644 index ac88fa8d..00000000 --- a/src/lib/designer/imagecanvas.ts +++ /dev/null @@ -1,79 +0,0 @@ -export class ImageCanvas { - canvas: HTMLCanvasElement; - context: CanvasRenderingContext2D; - - constructor(width = 1024, height = 1024) { - this.canvas = document.createElement("canvas"); - this.canvas.width = width; - this.canvas.height = height; - this.context = this.canvas.getContext("2d"); - } - - // copies image from source - // scales the image to fit dest canvas - copyFromCanvas(src: HTMLCanvasElement, resize = false) { - if (resize) { - if (this.canvas.width != src.width) this.canvas.width = src.width; - if (this.canvas.height != src.height) this.canvas.height = src.height; - } - //console.log("copying from " + src.width + " to " + this.canvas.width); - this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.context.drawImage(src, 0, 0, this.canvas.width, this.canvas.height); - } - - copyFromImageCanvas(imageCanvas: ImageCanvas) { - this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.context.drawImage( - imageCanvas.canvas, - 0, - 0, - this.canvas.width, - this.canvas.height - ); - } - - resize(width: number, height: number) { - this.canvas.width = width; - this.canvas.height = height; - } - - width(): number { - return this.canvas.width; - } - - height(): number { - return this.canvas.height; - } - - toImage(): HTMLImageElement { - const img: HTMLImageElement = ( - document.createElement("image") - ); - //var img:HTMLImageElement = new Image(this.width, this.height); - img.src = this.canvas.toDataURL("image/png"); - - return img; - } - - createTexture(gl: WebGLRenderingContext): WebGLTexture { - const texture = gl.createTexture(); - gl.bindTexture(gl.TEXTURE_2D, texture); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - - // Upload the image into the texture. - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - this.canvas - ); - - return texture; - } -} diff --git a/src/lib/designer/library.ts b/src/lib/designer/library.ts deleted file mode 100644 index d9ad8c15..00000000 --- a/src/lib/designer/library.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { DesignerNode } from "./designernode"; - -export class DesignerNodeFactory { - public name: string; - public displayName: string; - public create: () => DesignerNode; -} - -// holds list of node factories -export class DesignerLibrary { - versionName: string; - nodes = []; - - // https://www.snip2code.com/Snippet/685188/Create-instance-of-generic-type-on-TypeS - public addNode( - name: string, - displayName: string, - type: { new (): T } - ) { - const factory = new DesignerNodeFactory(); - factory.name = name; - factory.displayName = displayName; - factory.create = (): DesignerNode => { - return new type(); - }; - - //this.nodes.push(factory); - this.nodes[name] = factory; - } - - getVersionName() { - return this.versionName; - } - - public create(name: string): DesignerNode { - //if (this.nodes.indexOf(name) == -1) - // return null; - - const node = this.nodes[name].create(); - node.typeName = name; - - return node; - } -} diff --git a/src/lib/designer/properties.ts b/src/lib/designer/properties.ts deleted file mode 100644 index 375af7b9..00000000 --- a/src/lib/designer/properties.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { Color } from "./color"; -import { Gradient } from "./gradient"; -import { Image } from "./image"; - -// for use in code after build -export enum PropertyType { - Float = "float", - Int = "int", - Bool = "bool", - Color = "color", - Enum = "enum", - String = "string", - Gradient = "gradient", - Image = "image" -} - -export class PropertyGroup { - name: string = ""; - properties: Property[] = []; - collapsed: boolean = true; - - add(prop: Property) { - this.properties.push(prop); - prop.group = this; - } -} - -export class Property { - public name: string; - public displayName: string; - public type: string; - - public group: PropertyGroup; - - // to be overriden - public getValue(): any { - return null; - } - - public setValue(val: any, completeCallback: () => void = null) {} - - public clone(): Property { - return null; - } -} - -export interface IPropertyHolder { - properties: Property[]; - propertyGroups: PropertyGroup[]; - setProperty(name: string, value: any); -} - -export class FloatProperty extends Property { - value: number; - minValue = 0; - maxValue = 1; - step = 1; - public constructor( - name: string, - displayName: string, - value: number, - step = 1 - ) { - super(); - this.name = name; - this.displayName = displayName; - this.value = value; - this.step = step; - this.type = PropertyType.Float; - } - - public getValue(): any { - return this.value; - } - - public setValue(val: any) { - // todo: validate - this.value = val; - } - - public clone(): Property { - const prop = new FloatProperty( - this.name, - this.displayName, - this.value, - this.step - ); - prop.minValue = this.minValue; - prop.maxValue = this.maxValue; - - return prop; - } - - public copyValuesFrom(prop: FloatProperty) { - this.minValue = prop.minValue; - this.maxValue = prop.maxValue; - this.value = prop.value; - this.step = prop.step; - } -} - -export class IntProperty extends Property { - value: number; - minValue = 0; - maxValue = 100; - step = 1; - public constructor( - name: string, - displayName: string, - value: number, - step = 1 - ) { - super(); - this.name = name; - this.displayName = displayName; - this.value = value; - this.step = step; - this.type = PropertyType.Int; - } - - public getValue(): any { - return this.value; - } - - public setValue(val: any) { - // todo: validate - this.value = val; - } - - public clone(): Property { - const prop = new IntProperty( - this.name, - this.displayName, - this.value, - this.step - ); - prop.minValue = this.minValue; - prop.maxValue = this.maxValue; - - return prop; - } - - public copyValuesFrom(prop: IntProperty) { - this.minValue = prop.minValue; - this.maxValue = prop.maxValue; - this.value = prop.value; - this.step = prop.step; - } -} - -export class BoolProperty extends Property { - value: boolean; - public constructor(name: string, displayName: string, value: boolean) { - super(); - this.name = name; - this.displayName = displayName; - this.value = value; - this.type = PropertyType.Bool; - } - - public getValue(): any { - return this.value; - } - - public setValue(val: any) { - // todo: validate - this.value = val; - } - - public clone(): Property { - const prop = new BoolProperty(this.name, this.displayName, this.value); - - return prop; - } - - public copyValuesFrom(prop: BoolProperty) { - this.value = prop.value; - } -} - -export class EnumProperty extends Property { - values: string[]; - index = 0; - public constructor(name: string, displayName: string, values: string[]) { - super(); - this.name = name; - this.displayName = displayName; - this.values = values; - this.type = PropertyType.Enum; - } - - public getValues(): string[] { - return this.values; - } - - public getValue(): any { - return this.index; - } - - public setValue(val: any) { - // todo: validate - this.index = val; - } - - public clone(): Property { - const prop = new EnumProperty( - this.name, - this.displayName, - this.values.slice(0) - ); - prop.index = this.index; - - return prop; - } - - public copyValuesFrom(prop: EnumProperty) { - this.values = prop.values; - this.index = prop.index; - } -} - -export class ColorProperty extends Property { - value: Color; - public constructor(name: string, displayName: string, value: Color) { - super(); - this.name = name; - this.displayName = displayName; - this.value = value; - this.type = PropertyType.Color; - } - - public getValue(): any { - return this.value; - } - - public setValue(val: any) { - // todo: validate - //console.log("got color: " + val); - if (val instanceof Color) this.value = val; - else if (typeof val == "string") this.value = Color.parse(val); - else if (typeof val == "object") { - //console.log("setting value", val); - const value = new Color(); - value.r = val.r || 0; - value.g = val.g || 0; - value.b = val.b || 0; - value.a = val.a || 1.0; - - this.value = value; - } - } - - public clone(): Property { - const prop = new ColorProperty(this.name, this.displayName, this.value); - - return prop; - } - - public copyValuesFrom(prop: ColorProperty) { - this.setValue(prop.value); - } -} - -export class StringProperty extends Property { - value: string; - isMultiline: boolean; - public constructor( - name: string, - displayName: string, - value = "", - isMultiline = false - ) { - super(); - this.name = name; - this.displayName = displayName; - this.value = value; - this.type = PropertyType.String; - this.isMultiline = isMultiline; - } - - public getValue(): any { - return this.value; - } - - public setValue(val: any) { - // todo: validate - this.value = val; - } - - public clone(): Property { - const prop = new StringProperty(this.name, this.displayName, this.value); - - return prop; - } - - public copyValuesFrom(prop: StringProperty) { - this.value = prop.value; - } -} - -export class GradientProperty extends Property { - value: Gradient; - public constructor(name: string, displayName: string, value: Gradient) { - super(); - this.name = name; - this.displayName = displayName; - this.value = value; - this.type = PropertyType.Gradient; - } - - public getValue(): any { - return this.value; - } - - public setValue(val: any) { - //console.log("setting gradient value"); - this.value = Gradient.parse(val); - } - - public clone(): Property { - const prop = new GradientProperty( - this.name, - this.displayName, - this.value.clone() - ); - - return prop; - } - - public copyValuesFrom(prop: GradientProperty) { - //console.log("copy value from gradient"); - this.setValue(prop.value.clone()); - } -} - -export class ImageProperty extends Property { - value: Image; - public constructor(name: string, displayName: string, value: Image = null) { - super(); - this.name = name; - this.displayName = displayName; - this.value = value; - this.type = PropertyType.Image; - } - - public getValue(): any { - return this.value; - } - - // todo: pass callback for when value update is complete - public setValue(val: any, completeCallback: () => void = null) { - // todo: validate - if (val instanceof Image) { - this.value = val; - completeCallback(); - } else { - let image = Image.empty(); - image.deserialize(val, completeCallback); - this.value = image; - } - } - - public clone(): Property { - const prop = new ImageProperty( - this.name, - this.displayName, - this.value.clone() - ); - - return prop; - } - - public copyValuesFrom(prop: ImageProperty) { - this.value = prop.value.clone(); - } -} diff --git a/src/lib/designer/texturedataconverter.ts b/src/lib/designer/texturedataconverter.ts deleted file mode 100644 index b8d1d1a2..00000000 --- a/src/lib/designer/texturedataconverter.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { - compileShader, - buildShaderProgram, - createTextureWithType, - TextureDataType -} from "./gl"; - -export enum TextureComponents { - RGBA = "RGBA", - RGB = "RGB", - R = "R", - G = "G", - B = "B", - A = "A" -} - -// to be used mainly to extract pixels from textures -// to the needed format for export -export class TextureDataConverter { - gl: WebGL2RenderingContext; - readFbo: WebGLFramebuffer; - shaderProgram: WebGLProgram; - texture: WebGLTexture; - - posBuffer: WebGLBuffer; - texCoordBuffer: WebGLBuffer; - - constructor(gl: WebGL2RenderingContext) { - this.gl = gl; - this.readFbo = this.gl.createFramebuffer(); - this.createVertexBuffers(); - this.buildShader(); - } - - getData( - sourceTexture: WebGLTexture, - width: number, - height: number, - dataType: TextureDataType, - components: TextureComponents = TextureComponents.RGBA, - flipY: boolean = false - ): ArrayBuffer { - const gl = this.gl; - - // create textures based on specified data type - this.texture = createTextureWithType(this.gl, dataType, width, height); - - // set fbo and bind newly created texture - gl.bindFramebuffer(gl.FRAMEBUFFER, this.readFbo); - gl.activeTexture(gl.TEXTURE0); - - // NOTE: texture must be in the RGBA format - // spec doesn't mandate any other format being supported - // https://www.html5gamedevs.com/topic/38191-how-to-use-webgl2-rgb-texture-format/ - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - this.texture, - 0 - ); - - gl.viewport(0, 0, width, height); - gl.disable(gl.DEPTH_TEST); - gl.clearColor(0, 0, 0, 1); - gl.clear(gl.COLOR_BUFFER_BIT); - - // bind shader - gl.useProgram(this.shaderProgram); - - // clear all inputs - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, sourceTexture); - - gl.uniform1i(gl.getUniformLocation(this.shaderProgram, "tex"), 0); - - gl.uniform1i( - gl.getUniformLocation(this.shaderProgram, "flipY"), - flipY ? 1 : 0 - ); - - // determine which channels the source comes from - // 0 - all - // 1 - red - // 2 - green - // 3 - blue - // 4 - alpha - let outputChannel: number = 0; - - switch (components) { - case TextureComponents.R: - outputChannel = 1; - break; - case TextureComponents.G: - outputChannel = 2; - break; - case TextureComponents.B: - outputChannel = 3; - break; - case TextureComponents.A: - outputChannel = 4; - break; - } - - gl.uniform1i( - gl.getUniformLocation(this.shaderProgram, "channel"), - outputChannel - ); - - // render texure to fbo - const posLoc = gl.getAttribLocation(this.shaderProgram, "a_pos"); - const texCoordLoc = gl.getAttribLocation(this.shaderProgram, "a_texCoord"); - - // provide texture coordinates for the rectangle. - gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer); - gl.enableVertexAttribArray(posLoc); - gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0); - - gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer); - gl.enableVertexAttribArray(texCoordLoc); - gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - - gl.disableVertexAttribArray(posLoc); - gl.disableVertexAttribArray(texCoordLoc); - - // read back pixels - - let readDataType = gl.UNSIGNED_BYTE; - let arrayBufferView: ArrayBufferView = null; - let arrayBuffer: ArrayBuffer = null; - - let format: number = gl.RGBA; - let numComponents: number = 4; - - // commented this out since the format has to be - // RGBA. Any other format and WebGL throws an error - // switch (components) { - // case TextureComponents.RGBA: - // numComponents = 4; - // format = gl.RGBA; - // break; - // case TextureComponents.RGB: - // numComponents = 3; - // format = gl.RGB; - // break; - // case TextureComponents.R: - // case TextureComponents.G: - // case TextureComponents.B: - // case TextureComponents.A: - // numComponents = 1; - // format = gl.RED; - // break; - // } - - switch (dataType) { - case TextureDataType.Uint8: - readDataType = gl.UNSIGNED_BYTE; - arrayBuffer = new ArrayBuffer(width * height * numComponents * 1); - arrayBufferView = new Uint8Array(arrayBuffer); - break; - case TextureDataType.Uint16: - readDataType = gl.UNSIGNED_SHORT; - arrayBuffer = new ArrayBuffer(width * height * numComponents * 2); - arrayBufferView = new Uint16Array(arrayBuffer); - break; - case TextureDataType.Float16: - readDataType = gl.HALF_FLOAT; - arrayBuffer = new ArrayBuffer(width * height * numComponents * 2); - arrayBufferView = new Uint16Array(arrayBuffer); - break; - case TextureDataType.Float32: - readDataType = gl.FLOAT; - arrayBuffer = new ArrayBuffer(width * height * numComponents * 4); - arrayBufferView = new Float32Array(arrayBuffer); - break; - } - - if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) { - gl.readPixels(0, 0, width, height, format, readDataType, arrayBufferView); - - // todo: check for errors in this operation - // gl.readPixels( - // 0, - // 0, - // this.designer.width, - // this.designer.height, - // gl.RGBA, - // gl.HALF_FLOAT, - // data - // ); - } else { - alert("getPixelData: unable to read from FBO"); - } - - // cleanup - gl.enable(gl.DEPTH_TEST); - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - - // convert buffer to required format - // currently only supports uint8 and uint16 - - if (dataType === TextureDataType.Uint8) { - if (components === TextureComponents.RGB) { - arrayBuffer = rgbaToRgbUint8(arrayBuffer, width, height); - } else if ( - components === TextureComponents.R || - components === TextureComponents.G || - components === TextureComponents.B || - components === TextureComponents.A - ) { - arrayBuffer = rgbaToRUint8(arrayBuffer, width, height); - } - } else if (dataType === TextureDataType.Uint16) { - if (components === TextureComponents.RGB) { - arrayBuffer = rgbaToRgbUint16(arrayBuffer, width, height); - } - if ( - components === TextureComponents.R || - components === TextureComponents.G || - components === TextureComponents.B || - components === TextureComponents.A - ) { - arrayBuffer = rgbaToRUint16(arrayBuffer, width, height); - } - } - - return arrayBuffer; - } - - createVertexBuffers() { - const gl = this.gl; - //var texCoordLocation = gl.getAttribLocation(program, "a_texCoord"); - - // provide texture coordinates for the rectangle. - this.texCoordBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 1.0 - ]), - gl.STATIC_DRAW - ); - //gl.enableVertexAttribArray(texCoordLocation); - //gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0); - - this.posBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - -1.0, - -1.0, - 0.0, - 1.0, - -1.0, - 0.0, - -1.0, - 1.0, - 0.0, - -1.0, - 1.0, - 0.0, - 1.0, - -1.0, - 0.0, - 1.0, - 1.0, - 0.0 - ]), - gl.STATIC_DRAW - ); - - gl.bindBuffer(gl.ARRAY_BUFFER, null); - } - - buildShader() { - const vertSource = `#version 300 es - precision highp float; - - in vec3 a_pos; - in vec2 a_texCoord; - - // the texCoords passed in from the vertex shader. - out vec2 v_texCoord; - - void main() { - gl_Position = vec4(a_pos,1.0); - v_texCoord = a_texCoord; - }`; - - // https://stackoverflow.com/questions/51101023/render-to-16bits-unsigned-integer-2d-texture-in-webgl2 - // https://stackoverflow.com/questions/27509285/how-to-render-to-a-unsigned-integer-format - let fragSource = `#version 300 es - precision highp float; - in vec2 v_texCoord; - - uniform sampler2D tex; - // uniform vec2 _textureSize; - uniform bool flipY; - - uniform int channel; - - // out uvec4 fragColor; - out vec4 fragColor; - - void main() { - vec2 texCoords = vec2(v_texCoord.x, flipY? 1.0 - v_texCoord.y:v_texCoord.y); - vec4 result = texture(tex, texCoords); - - // determine which channels to extract if any - switch(channel){ - // 0 - do nothing - - // red - case 1: - result = vec4(result.r); - break; - // green - case 2: - result = vec4(result.g); - break; - // blue - case 3: - result = vec4(result.b); - break; - // alpha - case 4: - result = vec4(result.a); - break; - } - - fragColor = result; - // fragColor = vec4(1.0, 0.0, 0.0, 1.0); - } - - `; - - this.shaderProgram = buildShaderProgram(this.gl, vertSource, fragSource); - } -} - -function rgbaToRgbUint8( - buffer: ArrayBuffer, - width: number, - height: number -): ArrayBuffer { - const totalPixels = width * height; - const numComponents = 3; - const inputArray: Uint8Array = new Uint8Array(buffer); - const outputArray: Uint8Array = new Uint8Array(totalPixels * numComponents); - - for (let i = 0; i < totalPixels; i++) { - outputArray[i * numComponents + 0] = inputArray[i * 4 + 0]; - outputArray[i * numComponents + 1] = inputArray[i * 4 + 1]; - outputArray[i * numComponents + 2] = inputArray[i * 4 + 2]; - } - - return outputArray.buffer; -} - -function rgbaToRUint8( - buffer: ArrayBuffer, - width: number, - height: number -): ArrayBuffer { - const totalPixels = width * height; - const numComponents = 1; - const inputArray: Uint8Array = new Uint8Array(buffer); - const outputArray: Uint8Array = new Uint8Array(totalPixels * numComponents); - - for (let i = 0; i < totalPixels; i++) { - outputArray[i * numComponents] = inputArray[i * 4]; - } - - return outputArray.buffer; -} - -function rgbaToRgbUint16( - buffer: ArrayBuffer, - width: number, - height: number -): ArrayBuffer { - const totalPixels = width * height; - const numComponents = 3; - const inputArray: Uint16Array = new Uint16Array(buffer); - const outputArray: Uint16Array = new Uint16Array(totalPixels * numComponents); - - for (let i = 0; i < totalPixels; i++) { - outputArray[i * numComponents + 0] = inputArray[i * 4 + 0]; - outputArray[i * numComponents + 1] = inputArray[i * 4 + 1]; - outputArray[i * numComponents + 2] = inputArray[i * 4 + 2]; - } - - return outputArray.buffer; -} - -function rgbaToRUint16( - buffer: ArrayBuffer, - width: number, - height: number -): ArrayBuffer { - const totalPixels = width * height; - const numComponents = 1; - const inputArray: Uint16Array = new Uint16Array(buffer); - const outputArray: Uint16Array = new Uint16Array(totalPixels * numComponents); - - for (let i = 0; i < totalPixels; i++) { - outputArray[i * numComponents] = inputArray[i * 4]; - } - - return outputArray.buffer; -} diff --git a/src/lib/editor.ts b/src/lib/editor.ts deleted file mode 100644 index 22541d6c..00000000 --- a/src/lib/editor.ts +++ /dev/null @@ -1,886 +0,0 @@ -//import * as scene from "./scene"; -import { Designer } from "./designer"; -import { DesignerNode } from "./designer/designernode"; -import { Property, PropertyType } from "./designer/properties"; -import { Guid } from "./utils"; -import { DesignerVariableType } from "./designer/designervariable"; -import { DesignerLibrary } from "./designer/library"; -import { NodeScene } from "./scene"; -import { ConnectionGraphicsItem } from "./scene/connectiongraphicsitem"; -import { NodeGraphicsItem } from "./scene/nodegraphicsitem"; -import { SocketType } from "./scene/socketgraphicsitem"; -import { ImageCanvas } from "./designer/imagecanvas"; - -import { - createLibrary, - createLibrary as createV1Library -} from "@/lib/library/libraryv1"; -import { createLibrary as createV2Library } from "@/lib/library/libraryv2"; -import { Color } from "./designer/color"; -import { CommentGraphicsItem } from "./scene/commentgraphicsitem"; -import { FrameGraphicsItem } from "./scene/framegraphicsitem"; -import { NavigationGraphicsItem } from "./scene/navigationgraphicsitem"; -import { ItemClipboard } from "./clipboard"; -import { UndoStack } from "./undostack"; -import { RemoveItemsAction } from "./actions/removeitemsaction"; - -function hexToRgb(hex) { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result - ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } - : null; -} - -function rgbToHex(r, g, b) { - return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); -} - -// stores the IDs for the display nodes -export class DisplayNodes { - public albedoNode: string; - public normalNode: string; - public roughnessNode: string; - public heightNode: string; - public metallicNode: string; - /* - public albedoCanvas : ImageCanvas = new ImageCanvas(); - public normalCanvas : ImageCanvas = new ImageCanvas(); - public roughnessCanvas : ImageCanvas = new ImageCanvas(); - public heightCanvas : ImageCanvas = new ImageCanvas(); - public metallicCanvas : ImageCanvas = new ImageCanvas(); - - resize(width:number, height:number) - { - this.albedoCanvas.resize(width, height); - this.normalCanvas.resize(width, height); - this.roughnessCanvas.resize(width, height); - this.heightCanvas.resize(width, height); - this.metallicCanvas.resize(width, height); - } - */ -} - -export enum DisplayChannel { - Albedo, - Metallic, - Roughness, - Normal, - Height -} - -export class Editor { - static active: Editor; - - canvas: HTMLCanvasElement; - - library: DesignerLibrary; - graph: NodeScene; - designer: Designer; - selectedDesignerNode: DesignerNode; - - undoStack: UndoStack; - - preview2D: HTMLCanvasElement; - preview2DCtx: CanvasRenderingContext2D; - - scene3D: any; // todo: set a type - - // propGen: PropertyGenerator; - // varGen: VariableGenerator; - displayNodes: DisplayNodes; - - onnodeselected?: (item: DesignerNode) => void; - oncommentselected?: (item: CommentGraphicsItem) => void; - onframeselected?: (item: FrameGraphicsItem) => void; - onnavigationselected?: (item: NavigationGraphicsItem) => void; - onpreviewnode?: (item: DesignerNode, image: HTMLCanvasElement) => void; - onlibrarymenu?: () => void; - - textureChannels: Map = new Map(); - ontexturechannelcleared?: ( - imageCanvas: ImageCanvas, - channelName: string - ) => void; - ontexturechannelassigned?: ( - imageCanvas: ImageCanvas, - channelName: string - ) => void; - ontexturechannelupdated?: ( - imageCanvas: ImageCanvas, - channelName: string - ) => void; - onnodetextureupdated?: (node: DesignerNode, timeInMs: number) => void; - - constructor() { - this.displayNodes = new DisplayNodes(); - this.selectedDesignerNode = null; - this.undoStack = new UndoStack(); - } - - getImageWidth() { - return this.designer.width; - } - - getImageHeight() { - return this.designer.height; - } - - assignNodeToTextureChannel(nodeId: string, channelName: string) { - // only one node can be assigned to a channel - if ( - this.textureChannels.has(channelName) && - this.textureChannels.get(channelName) - ) { - // remove label from node view - const oldNode = this.textureChannels.get(channelName); - const nodeView = this.graph.getNodeById(oldNode.id); - nodeView.clearTextureChannel(); - //this.textureChannels[channelName] = null; - this.textureChannels.delete(channelName); - - if (this.ontexturechannelcleared) { - this.ontexturechannelcleared(null, channelName); - } - } - - const nodeView = this.graph.getNodeById(nodeId); - nodeView.setTextureChannel(channelName); - - const newNode = this.designer.getNodeById(nodeId); - this.textureChannels.set(channelName, newNode); - - // notify 3d view - if (this.ontexturechannelcleared) { - this.ontexturechannelassigned(nodeView.imageCanvas, channelName); - } - } - - clearTextureChannel(nodeId: string) { - // eval which channel has this node assigned - for (const channelName of this.textureChannels.keys()) { - const node = this.textureChannels.get(channelName); - - if (node.id == nodeId) { - const oldNode = this.textureChannels.get(channelName); - const nodeView = this.graph.getNodeById(oldNode.id); - - // if this function is called when a node is deleted - // nodeView will be null - if (nodeView) nodeView.clearTextureChannel(); - - this.textureChannels.delete(channelName); - - if (this.ontexturechannelcleared) { - this.ontexturechannelcleared(null, channelName); - } - } - } - - // only one node can be assigned to a channel - // if (this.textureChannels.hasOwnProperty(channelName)) { - // // remove label from node view - // let oldNode = this.textureChannels[channelName] as DesignerNode; - // let nodeView = this.graph.getNodeById(oldNode.id); - // nodeView.clearTextureChannel(); - // this.textureChannels[channelName] = null; - - // if (this.ontexturechannelcleared) { - // this.ontexturechannelcleared(oldNode, channelName); - // } - // } - } - - hasTextureChannel(channelName: string) { - return this.textureChannels.has(channelName); - } - - clearTextureChannels() { - for (const channelName of this.textureChannels.keys()) { - const node = this.textureChannels.get(channelName); - - this.clearTextureChannel(node.id); - } - } - - getChannelCanvasImage(channelName: string) { - if (this.hasTextureChannel(channelName)) { - //console.log(this.textureChannels[channelName]); - const dnodeId = this.textureChannels.get(channelName).id; - const nodeView = this.graph.getNodeById(dnodeId); - //console.log(nodeView) - //console.log(this.graph) - return nodeView.imageCanvas; - } - - return null; - } - - /* - constructor(canvas:HTMLCanvasElement, preview2D:HTMLCanvasElement, propHolder : HTMLElement, varHolder : HTMLElement, scene3D:any) - { - this.canvas = canvas; - - this.displayNodes = new DisplayNodes(); - - this.preview2D = preview2D; - this.preview2DCtx = preview2D.getContext("2d"); - - this.scene3D = scene3D; - this.selectedDesignerNode = null; - - this.propGen = new PropertyGenerator(this, propHolder); - this.varGen = new VariableGenerator(this, varHolder); - - - //this.setDesigner(new Designer()); - //this.setScene(new NodeScene(canvas)); - } - */ - - undo() { - this.undoStack.undo(); - } - - redo() { - this.undoStack.redo(); - } - - // creates new texture - // requires canvas to be already set - createNewTexture() { - this.clearTextureChannels(); - - this.library = createV2Library(); - this.setDesigner(new Designer()); - this.setScene(new NodeScene(this.canvas)); - - this.setupDefaultScene(); - } - - setupDefaultScene() { - const offset = 100; - const spacing = 150; - - // albedo - let node = this.library.create("output"); - let nodeView = this.addNode(node, 0, 0); - // figure out why this doesnt work before adding addNode: - node.setProperty("name", "albedo"); - node.setProperty("color", new Color(1, 1, 1, 1)); - node.setProperty("components", 0); - nodeView.setCenter(800, offset + spacing * 0); - this.assignNodeToTextureChannel(nodeView.id, "albedo"); - - // normal - node = this.library.create("output"); - nodeView = this.addNode(node, 0, 0); - node.setProperty("name", "normal"); - node.setProperty("color", new Color(0.5, 0.5, 1, 1)); - node.setProperty("components", 1); - nodeView.setCenter(800, offset + spacing * 1); - this.assignNodeToTextureChannel(nodeView.id, "normal"); - const normalId = node.id; - - // normal map - node = this.library.create("normalmap"); - nodeView = this.addNode(node, 0, 0); - nodeView.setCenter(600, offset + spacing * 1); - - this.graph.createConnection(node.id, normalId, 0); - - // roughness - node = this.library.create("output"); - nodeView = this.addNode(node, 0, 0); - node.setProperty("name", "roughness"); - node.setProperty("color", new Color(0.5, 0.5, 0.5, 1)); - node.setProperty("components", 2); - node.setProperty("precision", 1); - nodeView.setCenter(800, offset + spacing * 2); - this.assignNodeToTextureChannel(nodeView.id, "roughness"); - - // metalness - node = this.library.create("output"); - nodeView = this.addNode(node, 0, 0); - node.setProperty("name", "metalness"); - node.setProperty("color", new Color(0, 0, 0, 1)); - node.setProperty("components", 2); - node.setProperty("precision", 1); - nodeView.setCenter(800, offset + spacing * 3); - this.assignNodeToTextureChannel(nodeView.id, "metalness"); - - // height - node = this.library.create("output"); - nodeView = this.addNode(node, 0, 0); - node.setProperty("name", "height"); - node.setProperty("color", new Color(0, 0, 0, 1)); - node.setProperty("components", 2); - node.setProperty("precision", 1); - nodeView.setCenter(800, offset + spacing * 4); - this.assignNodeToTextureChannel(nodeView.id, "height"); - - // refresh everything - this.designer.invalidateAllNodes(); - } - - set2DPreview(preview2D: HTMLCanvasElement) { - this.preview2D = preview2D; - this.preview2DCtx = preview2D.getContext("2d"); - } - - setSceneCanvas(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.setScene(new NodeScene(canvas)); - } - - resizeScene(width: number, height: number) { - this.canvas.width = width; - this.canvas.height = height; - } - - set3DScene(scene3D: any) { - this.scene3D = scene3D; - } - - setDesigner(designer: Designer) { - this.designer = designer; - const self = this; - - designer.onnodetextureupdated = function(dnode, dtInMs) { - const graphNode = self.graph.getNodeById(dnode.id); - if (!graphNode) return; // node could have been deleted - - graphNode.processingTime = dtInMs; - - self.designer.copyNodeTextureToImageCanvas(dnode, graphNode.imageCanvas); - - if (self.onpreviewnode) { - if (dnode == self.selectedDesignerNode) - self.onpreviewnode(dnode, graphNode.imageCanvas.canvas); - } - - if (self.onnodetextureupdated) self.onnodetextureupdated(dnode, dtInMs); - - if (self.ontexturechannelupdated && graphNode.textureChannel) { - self.ontexturechannelupdated( - graphNode.imageCanvas, - graphNode.textureChannel - ); - } - // if(node == self.selectedDesignerNode) { - // requestAnimationFrame(function(){ - // self.preview2DCtx.clearRect(0,0,self.preview2D.width, self.preview2D.height); - // self.preview2DCtx.drawImage(graphNode.imageCanvas.canvas, - // 0,0, - // self.preview2D.width, self.preview2D.height); - // }); - // } - - self.updateDisplayNode(graphNode); - }; - - designer.onnodetimeupdated = function(dnode, dtInMs) { - const graphNode = self.graph.getNodeById(dnode.id); - if (!graphNode) return; // node could have been deleted - - graphNode.processingTime = dtInMs; - }; - - /* - designer.onthumbnailgenerated = function(node, thumb) { - console.log(self.selectedDesignerNode); - console.log("onthumbnailgenerated generated for: "+node.title); - // refresh right node image - var graphNode = self.graph.getNodeById(node.id); - graphNode.setThumbnail(thumb); - self.updateDisplayNode(graphNode); - - if(node == self.selectedDesignerNode) { - requestAnimationFrame(function(){ - self.preview2DCtx.clearRect(0,0,self.preview2D.width, self.preview2D.height); - self.preview2DCtx.drawImage(thumb, - 0,0, - self.preview2D.width, self.preview2D.height); - }); - - - } - } - */ - - //if (this.varGen) this.varGen.setDesigner(designer); - //this.propGen.setDesigner(designer); - } - - setScene(scene: NodeScene) { - // cleanup previous graph - if (this.graph) this.graph.dispose(); - - // this.undoStack = new UndoStack(); - this.undoStack.clear(); - - // this shouldnt be here - // assignment of current undo redo stack should be controlled - // by App - // UndoStack.current = this.undoStack; - - this.graph = scene; - - const self = this; - this.graph.onconnectioncreated = function(con: ConnectionGraphicsItem) { - // get node from graph - const leftNode = con.socketA.node; - const rightNode = con.socketB.node; - - // get node from designer and connect them - const leftDNode = self.designer.getNodeById(leftNode.id); - const rightDNode = self.designer.getNodeById(rightNode.id); - - // make connection - // switch from `title` to `name` - self.designer.addConnection(leftDNode, rightDNode, con.socketB.title); - - // refresh right node image - //var thumb = self.designer.generateImageFromNode(rightDNode); - //rightNode.setThumbnail(thumb); - }; - - this.graph.onconnectiondestroyed = function(con: ConnectionGraphicsItem) { - // get node from graph - const leftNode = con.socketA.node; - const rightNode = con.socketB.node; - - // get node from designer and connect them - const leftDNode = self.designer.getNodeById(leftNode.id); - const rightDNode = self.designer.getNodeById(rightNode.id); - - // remove connection - // switch from `title` to `name` - self.designer.removeConnection(leftDNode, rightDNode, con.socketB.title); - - // clear right node image - rightNode.setThumbnail(null); - }; - - this.graph.onnodeselected = function(node: NodeGraphicsItem) { - if (node != null) { - var dnode = self.designer.getNodeById(node.id); - self.selectedDesignerNode = dnode; - //console.log(dnode); - - if (self.preview2DCtx) { - self.preview2DCtx.drawImage( - node.imageCanvas.canvas, - 0, - 0, - self.preview2D.width, - self.preview2D.height - ); - } - - // todo: move to double click - if (self.onpreviewnode) { - self.onpreviewnode(dnode, node.imageCanvas.canvas); - } - - //console.log(this.scene3D); - if (self.scene3D) { - //console.log("setting height texture"); - //self.scene3D.setHeightTexture(node.thumbnail); - self.updateDisplayNode(node); - } - } - - if (self.onnodeselected) self.onnodeselected(dnode); - }; - - this.graph.oncommentselected = function(item: CommentGraphicsItem) { - if (self.oncommentselected) self.oncommentselected(item); - }; - - this.graph.onframeselected = function(item: FrameGraphicsItem) { - if (self.onframeselected) self.onframeselected(item); - }; - - this.graph.onnavigationselected = function(item: NavigationGraphicsItem) { - if (self.onnavigationselected) self.onnavigationselected(item); - }; - - this.graph.onnodedeleted = function(node: NodeGraphicsItem) { - // remove node from channels - //console.log(self); - self.clearTextureChannel(node.id); - - self.designer.removeNode(node.id); - - if (self.onpreviewnode) { - self.onpreviewnode(null, null); - } - }; - - this.graph.onitemsdeleting = function( - frames: FrameGraphicsItem[], - comments: CommentGraphicsItem[], - navs: NavigationGraphicsItem[], - cons: ConnectionGraphicsItem[], - nodes: NodeGraphicsItem[] - ) { - const dnodes: DesignerNode[] = []; - for (const node of nodes) { - const dnode = self.designer.getNodeById(node.id); - - // should never happen! - if (dnode == null) throw "Node with id " + dnode.id + " doesnt exist!!"; - - dnodes.push(dnode); - } - - const action = new RemoveItemsAction( - self, - self.graph, - self.designer, - frames, - comments, - navs, - cons, - nodes, - dnodes - ); - UndoStack.current.push(action); - }; - - this.graph.onitemsdeleted = function( - frames: FrameGraphicsItem[], - comments: CommentGraphicsItem[], - navs: NavigationGraphicsItem[], - cons: ConnectionGraphicsItem[], - nodes: NodeGraphicsItem[] - ) { - if (self.onpreviewnode) { - self.onpreviewnode(null, null); - } - }; - - this.graph.oncopy = function(evt: ClipboardEvent) { - self.executeCopy(evt); - }; - - this.graph.oncut = function(evt: ClipboardEvent) { - self.executeCut(evt); - }; - - this.graph.onpaste = function(evt: ClipboardEvent) { - self.executePaste(evt); - }; - - this.graph.onlibrarymenu = function() { - if (self.onlibrarymenu != null) { - self.onlibrarymenu(); - } - }; - - // property changes - /* - this.propGen.onnodepropertychanged = function(dnode:DesignerNode, prop:Property) { - //var node = self.graph.getNodeById(node.id); - //self.graph.refreshNode() - - // todo: do this properly - var thumb = self.designer.generateImageFromNode(dnode); - var node = self.graph.getNodeById(dnode.id); - node.thumbnail = thumb; - - //console.log(node.thumbnail); - requestAnimationFrame(function(){ - self.preview2DCtx.clearRect(0,0,self.preview2D.width, self.preview2D.height); - self.preview2DCtx.drawImage(thumb, - 0,0, - self.preview2D.width, self.preview2D.height); - }); - - // just a stest - //self.scene3D.setHeightTexture(node.thumbnail); - self.updateDisplayNode(node); - } - */ - } - - executeCopy(evt: ClipboardEvent) { - ItemClipboard.copyItems( - this.designer, - this.library, - this.graph, - evt.clipboardData - ); - } - - executeCut(evt: ClipboardEvent) { - ItemClipboard.copyItems( - this.designer, - this.library, - this.graph, - evt.clipboardData - ); - } - - executePaste(evt: ClipboardEvent) { - ItemClipboard.pasteItems( - this.designer, - this.library, - this.graph, - evt.clipboardData - ); - } - - // adds node - // x and y are screen space - addNode(dNode: DesignerNode, screenX = 0, screenY = 0): NodeGraphicsItem { - // must add to designer first - this.designer.addNode(dNode); - - // create node from designer - const node = new NodeGraphicsItem(dNode.title); - for (const input of dNode.getInputs()) { - node.addSocket(input, input, SocketType.In); - } - node.addSocket("output", "output", SocketType.Out); - this.graph.addNode(node); - node.id = dNode.id; - - // generate thumbnail - const thumb = this.designer.generateImageFromNode(dNode); - node.setThumbnail(thumb); - - const pos = this.graph.view.canvasToSceneXY(screenX, screenY); - node.setCenter(pos.x, pos.y); - - return node; - } - - createComment(): CommentGraphicsItem { - const comment = new CommentGraphicsItem(this.graph.view); - const pos = this.graph.view.sceneCenter; - comment.setCenter(pos.x, pos.y); - - this.graph.addComment(comment); - - return comment; - } - - createFrame(): FrameGraphicsItem { - const frame = new FrameGraphicsItem(this.graph.view); - const pos = this.graph.view.sceneCenter; - frame.setCenter(pos.x, pos.y); - - this.graph.addFrame(frame); - - return frame; - } - - createNavigation(): NavigationGraphicsItem { - const nav = new NavigationGraphicsItem(); - const pos = this.graph.view.sceneCenter; - nav.setCenter(pos.x, pos.y); - - this.graph.addNavigation(nav); - - return nav; - } - - // DISPLAY NODE FUNCTIONS - - // updates appropriate image if set - updateDisplayNode(node: NodeGraphicsItem) { - if (!this.scene3D) return; - - //console.log(node.id); - //console.log(this.displayNodes.normalNode); - - // TODO: create custom CanvasImage that resizes with - // the texture size. NodeGraphicsItem's CanvasImage is fixed - // to 1024x1024. Another option is to give each DesignerNode a - // CanvasImage that updates when its texture updates then pass - // it to NodeGraphicsitem. That way it gets used one place and - // gets updated everywhere else all at once. - if (node.id == this.displayNodes.albedoNode) { - //this.scene3D.setAlbedoTexture(node.thumbnail); - this.scene3D.setAlbedoCanvasTexture(node.imageCanvas.canvas); - } - - if (node.id == this.displayNodes.metallicNode) { - //this.scene3D.setMetallicTexture(node.thumbnail); - this.scene3D.setMetallicCanvasTexture(node.imageCanvas.canvas); - } - - if (node.id == this.displayNodes.normalNode) { - //this.scene3D.setNormalTexture(node.thumbnail); - this.scene3D.setNormalCanvasTexture(node.imageCanvas.canvas); - } - - if (node.id == this.displayNodes.roughnessNode) { - //this.scene3D.setRoughnessTexture(node.thumbnail); - this.scene3D.setRoughnessCanvasTexture(node.imageCanvas.canvas); - } - - if (node.id == this.displayNodes.heightNode) { - //this.scene3D.setHeightTexture(node.thumbnail); - this.scene3D.setHeightCanvasTexture(node.imageCanvas.canvas); - } - } - - setDisplayChannelNode(channel: DisplayChannel, nodeId: string) { - const node = this.graph.getNodeById(nodeId); - if (channel == DisplayChannel.Albedo) { - this.displayNodes.albedoNode = nodeId; - } - if (channel == DisplayChannel.Metallic) { - this.displayNodes.metallicNode = nodeId; - } - if (channel == DisplayChannel.Normal) { - this.displayNodes.normalNode = nodeId; - } - if (channel == DisplayChannel.Roughness) { - this.displayNodes.roughnessNode = nodeId; - } - if (channel == DisplayChannel.Height) { - this.displayNodes.heightNode = nodeId; - } - - this.updateDisplayNode(node); - } - - exposeVariable(node: DesignerNode, prop: Property, varDisplayName: string) { - // create new variable - const varName = Guid.newGuid(); - const dvar = this.designer.addVariable( - varName, - varDisplayName, - this.evalDesignerVariableType(prop) - ); - // copy over important props - // todo:make more elegant - dvar.property = prop.clone(); - dvar.property.name = varName; - dvar.property.displayName = varDisplayName; - - // add it to scene and bind prop - this.designer.mapNodePropertyToVariable(varName, node, prop.name); - - // copy property props - - // refresh var ui - // this.varGen.refreshUi(); - } - - evalDesignerVariableType(prop: Property): DesignerVariableType { - if (prop.type == PropertyType.Float) { - return DesignerVariableType.Float; - } else if (prop.type == PropertyType.Int) { - return DesignerVariableType.Int; - } else if (prop.type == PropertyType.Bool) { - return DesignerVariableType.Bool; - } else if (prop.type == PropertyType.Enum) { - return DesignerVariableType.Enum; - } else if (prop.type == PropertyType.Color) { - return DesignerVariableType.Color; - } else { - console.log("error: invalid property type for variable", prop); - } - - return null; - } - - update() { - if (this.designer) this.designer.update(); - } - - draw() { - if (this.graph) this.graph.draw(); - } - - load(data: any) { - // clear texture channels - this.clearTextureChannels(); - - let library; - if (!data["libraryVersion"]) { - library = createV2Library(); - } else { - let libVer = data["libraryVersion"]; - if (libVer === "v0") library = createLibrary(); - if (libVer === "v1") - // silently load v1 textures as v2 - library = createV2Library(); - else if (libVer === "v2") library = createV2Library(); - else - console.error( - `Invalid library version ${libVer}, project might not load properly` - ); - } - // load scene - const d = Designer.load(data, library); - // copy over relevant props - if (this.designer) d.setUpdateMode(this.designer.updateMode); - - // load graph - const g = NodeScene.load(d, data["scene"], this.canvas); - - //todo: properly destroy existing graph - - //this.designer = d; - //this.graph = g; - this.library = library; - this.setDesigner(d); - this.setScene(g); - - // assign each node to it's texture channel - // it's expected at this point that the 3d preview should already - // have the texturechannel callbacks assigned - - // load editor data - if (data["editor"] != null) { - const e = data["editor"]; - // console.log("loading editor data"); - // console.log(e.displayNodes); - - // this.displayNodes.albedoNode = e.displayNodes.albedoNode; - // this.displayNodes.metallicNode = e.displayNodes.metallicNode; - // this.displayNodes.normalNode = e.displayNodes.normalNode; - // this.displayNodes.roughnessNode = e.displayNodes.roughnessNode; - // this.displayNodes.heightNode = e.displayNodes.heightNode; - - for (const channelName in e.textureChannels) { - if (!e.textureChannels.hasOwnProperty(channelName)) continue; - // console.log(e); - const node = this.graph.getNodeById(e.textureChannels[channelName]); - if (node) this.assignNodeToTextureChannel(node.id, channelName); - } - - //this.textureChannels = e.textureChannels || {}; - //console.log(this.textureChannels) - } - } - - save(): any { - const data = this.designer.save(); - data["scene"] = this.graph.save(); - - const textureChannels = {}; - for (const channelName of this.textureChannels.keys()) { - textureChannels[channelName] = this.textureChannels.get(channelName).id; - } - - data["editor"] = { - //displayNodes: this.displayNodes, - textureChannels: textureChannels - }; - - data["libraryVersion"] = this.library.getVersionName(); - //data["libraryVersion"] = "v1"; - - return data; - } -} diff --git a/src/lib/export/unityexporter.js b/src/lib/export/unityexporter.js deleted file mode 100644 index f8de194f..00000000 --- a/src/lib/export/unityexporter.js +++ /dev/null @@ -1,657 +0,0 @@ -/* -NOTES: -- _BumpMap is the normal map if then _NORMALMAP keyword is uses - the normal map needs to be inverted to work in unity -- _METALLICGLOSSMAP keyword means the gloss map is in the alpha channel of the metallic map - the gloss map is the inverse of the roughness map - both metallic and gloss maps have to be combined into a single texture - generate metallic of 0 if no metallic map is specified but a glossmap is -- _ParallaxMap is the height map and keyword _PARALLAXMAP is needed to enable it -*/ -import JSZip from "jszip"; - -const keyWords = "_METALLICGLOSSMAP _NORMALMAP"; - -const materialTemplate = `%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!21 &2100000 -Material: - serializedVersion: 6 - m_ObjectHideFlags: 0 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_Name: {{name}} - m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0} - m_ShaderKeywords: {{keywords}} - m_LightmapFlags: 4 - m_EnableInstancingVariants: 0 - m_DoubleSidedGI: 0 - m_CustomRenderQueue: -1 - stringTagMap: {} - disabledShaderPasses: [] - m_SavedProperties: - serializedVersion: 3 - m_TexEnvs: - - _BumpMap: - {{normal_tex}} - - _DetailAlbedoMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailMask: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailNormalMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _EmissionMap: - m_Texture: {fileID: 0} - m_Scale: {x: 4, y: 4} - m_Offset: {x: 0, y: 0} - - _MainTex: - {{albedo_tex}} - - _MetallicGlossMap: - {{metallicgloss_tex}} - - _OcclusionMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _ParallaxMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - m_Floats: - - _BumpScale: 1 - - _Cutoff: 0.5 - - _DetailNormalMapScale: 1 - - _DstBlend: 0 - - _GlossMapScale: 1 - - _Glossiness: 0.5 - - _GlossyReflections: 1 - - _Metallic: 0 - - _Mode: 0 - - _OcclusionStrength: 1 - - _Parallax: 0.02 - - _SmoothnessTextureChannel: 0 - - _SpecularHighlights: 1 - - _SrcBlend: 1 - - _UVSec: 0 - - _ZWrite: 1 - m_Colors: - - _Color: {r: 1, g: 1, b: 1, a: 1} - - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} -`; - -const materialMetaTemplate = `fileFormatVersion: 2 -guid: {{material_guid}} -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 2100000 - userData: - assetBundleName: - assetBundleVariant: -`; - -const textureMetaTemplate = `fileFormatVersion: 2 -guid: {{texture_guid}} -TextureImporter: - fileIDToRecycleName: {} - externalObjects: {} - serializedVersion: 5 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - isReadable: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: -1 - aniso: 16 - mipBias: -1 - wrapU: -1 - wrapV: -1 - wrapW: -1 - nPOTScale: 1 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationDetail: -1 - textureType: {{type}} - textureShape: 1 - singleChannelComponent: 0 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - platformSettings: - - serializedVersion: 2 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: Android - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: WebGL - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - physicsShape: [] - bones: [] - spriteID: - vertices: [] - indices: - edges: [] - weights: [] - spritePackingTag: - userData: - assetBundleName: - assetBundleVariant: -`; - -const TextureType = { - Default: 0, - NormalMap: 1 -}; - -const matNullTexTemp = `m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0}`; - -const matTexTemp = `m_Texture: {fileID: 2800000, guid: {{guid}}, type: 3} - m_Scale: {x: {{repeatx}}, y: {{repeaty}}} - m_Offset: {x: 0, y: 0}`; - -const exporter = { - canvas: null, - gl: null, - posBuffer: null, - texCoordBuffer: null, - - // shaders - metalGlossProgram: null, - normalProgram: null -}; - -export function unityExport(editor, materialName) { - const zip = new JSZip(); - - // todo : filter name, might have illegal characters - //var materialName = saveData.name; - - const matData = { - name: materialName, - keywords: "", - albedo_tex: matNullTexTemp, - metallicgloss_tex: matNullTexTemp, - normal_tex: matNullTexTemp, - height_tex: matNullTexTemp - }; - - // write texture first - if (editor.hasTextureChannel("albedo")) { - const albedoGuid = newGuid(); - zip.file( - albedoGuid + "/asset", - canvasToBase64(editor.getChannelCanvasImage("albedo").canvas), - { base64: true } - ); - zip.file( - albedoGuid + "/asset.meta", - template(textureMetaTemplate, { - texture_guid: albedoGuid, - type: TextureType.Default - }) - ); - zip.file( - albedoGuid + "/pathname", - "Assets/" + materialName + "/Albedo.png" - ); - - matData.albedo_tex = template(matTexTemp, { - guid: albedoGuid, - repeatx: 1, - repeaty: 1 - }); - } - - if (editor.hasTextureChannel("normal")) { - const guid = newGuid(); - - const normalCanvas = editor.getChannelCanvasImage("normal"); - exporter.canvas.width = normalCanvas.width(); - exporter.canvas.height = normalCanvas.height(); - - fixNormalMap(exporter, normalCanvas.createTexture(exporter.gl)); - zip.file(guid + "/asset", canvasToBase64(exporter.canvas), { - base64: true - }); - zip.file( - guid + "/asset.meta", - template(textureMetaTemplate, { - texture_guid: guid, - type: TextureType.NormalMap - }) - ); - zip.file(guid + "/pathname", "Assets/" + materialName + "/Normal.png"); - - matData.normal_tex = template(matTexTemp, { - guid: guid, - repeatx: 1, - repeaty: 1 - }); - } - - if ( - editor.hasTextureChannel("metalness") || - editor.hasTextureChannel("roughness") - ) { - const guid = newGuid(); - - //var normalCanvas = viewer.normalCanvas; - // resize canvas - // todo: figure out how to derive canvas size - exporter.canvas.width = editor.getImageWidth(); - exporter.canvas.height = editor.getImageHeight(); - - let mTex = null; - if (editor.hasTextureChannel("metalness")) - mTex = editor - .getChannelCanvasImage("metalness") - .createTexture(exporter.gl); - - let rTex = null; - if (editor.hasTextureChannel("roughness")) - rTex = editor - .getChannelCanvasImage("roughness") - .createTexture(exporter.gl); - - generateMetallicGloss(exporter, mTex, rTex); - zip.file(guid + "/asset", canvasToBase64(exporter.canvas), { - base64: true - }); - zip.file( - guid + "/asset.meta", - template(textureMetaTemplate, { - texture_guid: guid, - type: TextureType.Default - }) - ); - zip.file( - guid + "/pathname", - "Assets/" + materialName + "/MetallicGloss.png" - ); - - matData.metallicgloss_tex = template(matTexTemp, { - guid: guid, - repeatx: 1, - repeaty: 1 - }); - } - - // NOTE: Unity has the metallic and glossy fields in one texture (glossiness being the alpha channel) - // glosiness is the inverse of the roughness map - - // write material last - const matGuid = newGuid(); - /* - zip.file(matGuid+"/asset",template(materialTemplate,{ - keywords:"", - albedo_guid:albedoGuid - })); - */ - zip.file(matGuid + "/asset", template(materialTemplate, matData)); - zip.file( - matGuid + "/asset.meta", - template(materialMetaTemplate, { material_guid: matGuid }) - ); - zip.file( - matGuid + "/pathname", - "Assets/" + materialName + "/" + materialName + ".mat" - ); - //zip.file(matGuid+"/asset","");//preview - zip.file(matGuid + "/asset", template(materialTemplate, matData)); - zip.file( - matGuid + "/asset.meta", - template(materialMetaTemplate, { material_guid: matGuid }) - ); - zip.file( - matGuid + "/pathname", - "Assets/" + materialName + "/" + materialName + ".mat" - ); - - return zip.generateAsync({ type: "nodebuffer" }); -} - -function newGuid() { - return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, function(c) { - const r = (Math.random() * 16) | 0, - v = c == "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} - -function template(str, data) { - let res = str; - for (const name in data) { - res = res.replace("{{" + name + "}}", data[name]); - } - - return res; -} - -// merges metallic and inverted roughness map -// uses a metallic of 0 if no metallic map is provided -// sets glosiness to 0 by default -function generateMetallicGloss(exporter, mTex, rTex) { - renderToImage(exporter, exporter.metalGlossProgram, [ - { name: "u_metallicMap", tex: mTex }, - { name: "u_roughnessMap", tex: rTex } - ]); -} - -// inverts normal map -function fixNormalMap(exporter, tex) { - renderToImage(exporter, exporter.normalProgram, [ - { name: "u_normalMap", tex: tex } - ]); -} - -const DEFAULT_VERT = `precision mediump float; - - attribute vec3 a_pos; - attribute vec2 a_texCoord; - - // the texCoords passed in from the vertex shader. - varying vec2 v_texCoord; - - void main() { - gl_Position = vec4(a_pos,1.0); - v_texCoord = a_texCoord; - }`; - -const NORMAl_FRAG = `precision mediump float; - varying vec2 v_texCoord; - uniform sampler2D u_normalMap; - - void main() { - vec4 norm = texture2D(u_normalMap,vec2(v_texCoord.x, 1.0 - v_texCoord.y)); - norm.z = 1.0 - norm.z; - gl_FragColor = norm; - }`; - -const METALLICGLOSS_FRAG = `precision mediump float; - varying vec2 v_texCoord; - uniform sampler2D u_metallicMap; - uniform bool u_metallicMapEnabled; - uniform sampler2D u_roughnessMap; - uniform bool u_roughnessMapEnabled; - - void main() { - vec2 texCoord = vec2(v_texCoord.x, 1.0 - v_texCoord.y); - float metal = 0.0; - if (u_metallicMapEnabled) - metal = texture2D(u_metallicMap,texCoord).r; - - - float gloss = 0.0; - if (u_roughnessMapEnabled) - gloss = 1.0 - texture2D(u_roughnessMap,texCoord).r; - - gl_FragColor = vec4(vec3(metal), gloss); - }`; - -// creates canvas and context -// creates shaders for converting the the textures -function initGLAndResources(exporter) { - const canvas = document.createElement("canvas"); - const gl = canvas.getContext("webgl"); - - exporter.canvas = canvas; - exporter.gl = gl; - - exporter.metalGlossProgram = buildShaderProgram( - gl, - DEFAULT_VERT, - METALLICGLOSS_FRAG - ); - exporter.normalProgram = buildShaderProgram(gl, DEFAULT_VERT, NORMAl_FRAG); - - createVertexBuffers(exporter); -} - -function getShaderSource(id) { - const shaderScript = document.getElementById(id); - if (!shaderScript) { - return null; - } - - let str = ""; - let k = shaderScript.firstChild; - while (k) { - if (k.nodeType == 3) { - str += k.textContent; - } - k = k.nextSibling; - } - - return str; -} - -function compileShader(gl, source, shaderType) { - const shader = gl.createShader(shaderType); - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - alert(gl.getShaderInfoLog(shader)); - return null; - } - - return shader; -} - -function buildShaderProgram(gl, vertSource, fragSource) { - const vertexShader = compileShader(gl, vertSource, gl.VERTEX_SHADER); - const fragmentShader = compileShader(gl, fragSource, gl.FRAGMENT_SHADER); - - const shaderProgram = gl.createProgram(); - gl.attachShader(shaderProgram, vertexShader); - gl.attachShader(shaderProgram, fragmentShader); - gl.linkProgram(shaderProgram); - - if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { - alert("Could not initialise shaders"); - } - - gl.useProgram(shaderProgram); - - return shaderProgram; -} - -//var texCoordBuffer = new WebGLBuffer; -//var posBuffer = new WebGLBuffer; - -// render quad using shader and texture inputs -// returns HtmlImageElement -function renderToImage(exporter, program, inputs) { - const gl = exporter.gl; - - gl.viewport(0, 0, exporter.canvas.width, exporter.canvas.height); - - // bind texture to fbo - //gl.clearColor(0,0,1,1); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - // bind shader - gl.useProgram(program); - - // pass textures - let texIndex = 0; - for (const i in inputs) { - const input = inputs[i]; - if (input.tex) { - gl.activeTexture(gl.TEXTURE0 + texIndex); - gl.bindTexture(gl.TEXTURE_2D, input.tex); - gl.uniform1i(gl.getUniformLocation(program, input.name), texIndex); - gl.uniform1i(gl.getUniformLocation(program, input.name + "Enabled"), 1); - } else { - gl.uniform1i(gl.getUniformLocation(program, input.name + "Enabled"), 0); - } - texIndex++; - } - - // bind mesh - const posLoc = gl.getAttribLocation(program, "a_pos"); - const texCoordLoc = gl.getAttribLocation(program, "a_texCoord"); - - // provide texture coordinates for the rectangle. - gl.bindBuffer(gl.ARRAY_BUFFER, exporter.posBuffer); - gl.enableVertexAttribArray(posLoc); - gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0); - - gl.bindBuffer(gl.ARRAY_BUFFER, exporter.texCoordBuffer); - gl.enableVertexAttribArray(texCoordLoc); - gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - - gl.disableVertexAttribArray(posLoc); - gl.disableVertexAttribArray(texCoordLoc); -} - -function createVertexBuffers(exporter) { - const gl = exporter.gl; - - // provide texture coordinates for the rectangle. - const texCoordBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 1.0 - ]), - gl.STATIC_DRAW - ); - - const posBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - -1.0, - -1.0, - 0.0, - 1.0, - -1.0, - 0.0, - -1.0, - 1.0, - 0.0, - -1.0, - 1.0, - 0.0, - 1.0, - -1.0, - 0.0, - 1.0, - 1.0, - 0.0 - ]), - gl.STATIC_DRAW - ); - - gl.bindBuffer(gl.ARRAY_BUFFER, null); - - exporter.texCoordBuffer = texCoordBuffer; - exporter.posBuffer = posBuffer; -} - -function canvasToBase64(canvas) { - let data = canvas.toDataURL(); - // todo: maybe script header? - // https://code-examples.net/en/q/6f412f - data = data.replace(/^data:image\/(png|jpg);base64,/, ""); - return data; -} - -// todo: cleanup textures!! - -// initialize -initGLAndResources(exporter); diff --git a/src/lib/export/unityzipexporter.js b/src/lib/export/unityzipexporter.js deleted file mode 100644 index 8565ab4e..00000000 --- a/src/lib/export/unityzipexporter.js +++ /dev/null @@ -1,338 +0,0 @@ -/* -NOTES: -Unity's mettalic and gloss maps are in one texture. -*/ -import AdmZip from "adm-zip"; -import electron from "electron"; - -const TextureType = { - Default: 0, - NormalMap: 1 -}; - -const exporter = { - canvas: null, - gl: null, - posBuffer: null, - texCoordBuffer: null, - - // shaders - metalGlossProgram: null, - normalProgram: null -}; - -export async function unityZipExport(editor, materialName) { - const zip = new AdmZip(); - - // write albedo first - if (editor.hasTextureChannel("albedo")) { - zip.addFile( - "albedo.png", - canvasToBuffer(editor.getChannelCanvasImage("albedo").canvas) - ); - } - - if (editor.hasTextureChannel("normal")) { - const normalCanvas = editor.getChannelCanvasImage("normal"); - exporter.canvas.width = normalCanvas.width(); - exporter.canvas.height = normalCanvas.height(); - - fixNormalMap(exporter, normalCanvas.createTexture(exporter.gl)); - - zip.addFile("normal.png", canvasToBuffer(exporter.canvas)); - } - - if ( - editor.hasTextureChannel("metalness") || - editor.hasTextureChannel("roughness") - ) { - exporter.canvas.width = editor.getImageWidth(); - exporter.canvas.height = editor.getImageHeight(); - - let mTex = null; - if (editor.hasTextureChannel("metalness")) - mTex = editor - .getChannelCanvasImage("metalness") - .createTexture(exporter.gl); - - let rTex = null; - if (editor.hasTextureChannel("roughness")) - rTex = editor - .getChannelCanvasImage("roughness") - .createTexture(exporter.gl); - - generateMetallicGloss(exporter, mTex, rTex); - - zip.addFile("metallic_gloss.png", canvasToBuffer(exporter.canvas)); - } - - if (editor.hasTextureChannel("height")) { - zip.addFile( - "height.png", - canvasToBuffer(editor.getChannelCanvasImage("height").canvas) - ); - } - - return zip; -} - -// merges metallic and inverted roughness map -// uses a metallic of 0 if no metallic map is provided -// sets glosiness to 0 by default -function generateMetallicGloss(exporter, mTex, rTex) { - renderToImage(exporter, exporter.metalGlossProgram, [ - { name: "u_metallicMap", tex: mTex }, - { name: "u_roughnessMap", tex: rTex } - ]); -} - -// inverts normal map -function fixNormalMap(exporter, tex) { - renderToImage(exporter, exporter.normalProgram, [ - { name: "u_normalMap", tex: tex } - ]); -} - -const DEFAULT_VERT = `precision mediump float; - - attribute vec3 a_pos; - attribute vec2 a_texCoord; - - // the texCoords passed in from the vertex shader. - varying vec2 v_texCoord; - - void main() { - gl_Position = vec4(a_pos,1.0); - v_texCoord = a_texCoord; - }`; - -const NORMAl_FRAG = `precision mediump float; - varying vec2 v_texCoord; - uniform sampler2D u_normalMap; - - void main() { - vec4 norm = texture2D(u_normalMap,vec2(v_texCoord.x, 1.0 - v_texCoord.y)); - norm.z = 1.0 - norm.z; - gl_FragColor = norm; - }`; - -const METALLICGLOSS_FRAG = `precision mediump float; - varying vec2 v_texCoord; - uniform sampler2D u_metallicMap; - uniform bool u_metallicMapEnabled; - uniform sampler2D u_roughnessMap; - uniform bool u_roughnessMapEnabled; - - void main() { - vec2 texCoord = vec2(v_texCoord.x, 1.0 - v_texCoord.y); - float metal = 0.0; - if (u_metallicMapEnabled) - metal = texture2D(u_metallicMap,texCoord).r; - - - float gloss = 0.0; - if (u_roughnessMapEnabled) - gloss = 1.0 - texture2D(u_roughnessMap,texCoord).r; - - gl_FragColor = vec4(vec3(metal), gloss); - }`; - -// creates canvas and context -// creates shaders for converting the the textures -function initGLAndResources(exporter) { - const canvas = document.createElement("canvas"); - const gl = canvas.getContext("webgl"); - - exporter.canvas = canvas; - exporter.gl = gl; - - exporter.metalGlossProgram = buildShaderProgram( - gl, - DEFAULT_VERT, - METALLICGLOSS_FRAG - ); - exporter.normalProgram = buildShaderProgram(gl, DEFAULT_VERT, NORMAl_FRAG); - - createVertexBuffers(exporter); -} - -function getShaderSource(id) { - const shaderScript = document.getElementById(id); - if (!shaderScript) { - return null; - } - - let str = ""; - let k = shaderScript.firstChild; - while (k) { - if (k.nodeType == 3) { - str += k.textContent; - } - k = k.nextSibling; - } - - return str; -} - -function compileShader(gl, source, shaderType) { - const shader = gl.createShader(shaderType); - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - alert(gl.getShaderInfoLog(shader)); - return null; - } - - return shader; -} - -function buildShaderProgram(gl, vertSource, fragSource) { - const vertexShader = compileShader(gl, vertSource, gl.VERTEX_SHADER); - const fragmentShader = compileShader(gl, fragSource, gl.FRAGMENT_SHADER); - - const shaderProgram = gl.createProgram(); - gl.attachShader(shaderProgram, vertexShader); - gl.attachShader(shaderProgram, fragmentShader); - gl.linkProgram(shaderProgram); - - if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { - alert("Could not initialise shaders"); - } - - gl.useProgram(shaderProgram); - - return shaderProgram; -} - -//var texCoordBuffer = new WebGLBuffer; -//var posBuffer = new WebGLBuffer; - -// render quad using shader and texture inputs -// returns HtmlImageElement -function renderToImage(exporter, program, inputs) { - const gl = exporter.gl; - - gl.viewport(0, 0, exporter.canvas.width, exporter.canvas.height); - - // bind texture to fbo - //gl.clearColor(0,0,1,1); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - // bind shader - gl.useProgram(program); - - // pass textures - let texIndex = 0; - for (const i in inputs) { - const input = inputs[i]; - if (input.tex) { - gl.activeTexture(gl.TEXTURE0 + texIndex); - gl.bindTexture(gl.TEXTURE_2D, input.tex); - gl.uniform1i(gl.getUniformLocation(program, input.name), texIndex); - gl.uniform1i(gl.getUniformLocation(program, input.name + "Enabled"), 1); - } else { - gl.uniform1i(gl.getUniformLocation(program, input.name + "Enabled"), 0); - } - texIndex++; - } - - // bind mesh - const posLoc = gl.getAttribLocation(program, "a_pos"); - const texCoordLoc = gl.getAttribLocation(program, "a_texCoord"); - - // provide texture coordinates for the rectangle. - gl.bindBuffer(gl.ARRAY_BUFFER, exporter.posBuffer); - gl.enableVertexAttribArray(posLoc); - gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0); - - gl.bindBuffer(gl.ARRAY_BUFFER, exporter.texCoordBuffer); - gl.enableVertexAttribArray(texCoordLoc); - gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - - gl.disableVertexAttribArray(posLoc); - gl.disableVertexAttribArray(texCoordLoc); -} - -function createVertexBuffers(exporter) { - const gl = exporter.gl; - - // provide texture coordinates for the rectangle. - const texCoordBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 1.0 - ]), - gl.STATIC_DRAW - ); - - const posBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - -1.0, - -1.0, - 0.0, - 1.0, - -1.0, - 0.0, - -1.0, - 1.0, - 0.0, - -1.0, - 1.0, - 0.0, - 1.0, - -1.0, - 0.0, - 1.0, - 1.0, - 0.0 - ]), - gl.STATIC_DRAW - ); - - gl.bindBuffer(gl.ARRAY_BUFFER, null); - - exporter.texCoordBuffer = texCoordBuffer; - exporter.posBuffer = posBuffer; -} - -function canvasToBase64(canvas) { - let data = canvas.toDataURL(); - // todo: maybe script header? - // https://code-examples.net/en/q/6f412f - data = data.replace(/^data:image\/(png|jpg);base64,/, ""); - return data; -} - -function canvasToBuffer(canvas) { - // https://github.com/mattdesl/electron-canvas-to-buffer/blob/master/index.js - const url = canvas.toDataURL("image/png", 1); - const nativeImage = electron.nativeImage.createFromDataURL(url); - const buffer = nativeImage.toPNG(); - - return buffer; -} - -// todo: cleanup textures!! - -// initialize -initGLAndResources(exporter); diff --git a/src/lib/export/zipexporter.js b/src/lib/export/zipexporter.js deleted file mode 100644 index f605202e..00000000 --- a/src/lib/export/zipexporter.js +++ /dev/null @@ -1,302 +0,0 @@ -import AdmZip from "adm-zip"; -import electron from "electron"; - -const TextureType = { - Default: 0, - NormalMap: 1 -}; - -const exporter = { - canvas: null, - gl: null, - posBuffer: null, - texCoordBuffer: null, - - // shaders - metalGlossProgram: null, - normalProgram: null -}; - -export async function zipExport(editor, materialName) { - const zip = new AdmZip(); - - // write albedo first - // if (editor.hasTextureChannel("albedo")) { - // zip.addFile( - // "albedo.png", - // canvasToBuffer(editor.getChannelCanvasImage("albedo").canvas) - // ); - // } - - for (const channel in editor.textureChannels) { - if (editor.textureChannels.hasOwnProperty(channel)) { - zip.addFile( - channel + ".png", - canvasToBuffer(editor.getChannelCanvasImage(channel).canvas) - ); - } - } - - return zip; -} - -// merges metallic and inverted roughness map -// uses a metallic of 0 if no metallic map is provided -// sets glosiness to 0 by default -function generateMetallicGloss(exporter, mTex, rTex) { - renderToImage(exporter, exporter.metalGlossProgram, [ - { name: "u_metallicMap", tex: mTex }, - { name: "u_roughnessMap", tex: rTex } - ]); -} - -// inverts normal map -function fixNormalMap(exporter, tex) { - renderToImage(exporter, exporter.normalProgram, [ - { name: "u_normalMap", tex: tex } - ]); -} - -const DEFAULT_VERT = `precision mediump float; - - attribute vec3 a_pos; - attribute vec2 a_texCoord; - - // the texCoords passed in from the vertex shader. - varying vec2 v_texCoord; - - void main() { - gl_Position = vec4(a_pos,1.0); - v_texCoord = a_texCoord; - }`; - -const NORMAl_FRAG = `precision mediump float; - varying vec2 v_texCoord; - uniform sampler2D u_normalMap; - - void main() { - vec4 norm = texture2D(u_normalMap,vec2(v_texCoord.x, 1.0 - v_texCoord.y)); - norm.z = 1.0 - norm.z; - gl_FragColor = norm; - }`; - -const METALLICGLOSS_FRAG = `precision mediump float; - varying vec2 v_texCoord; - uniform sampler2D u_metallicMap; - uniform bool u_metallicMapEnabled; - uniform sampler2D u_roughnessMap; - uniform bool u_roughnessMapEnabled; - - void main() { - vec2 texCoord = vec2(v_texCoord.x, 1.0 - v_texCoord.y); - float metal = 0.0; - if (u_metallicMapEnabled) - metal = texture2D(u_metallicMap,texCoord).r; - - - float gloss = 0.0; - if (u_roughnessMapEnabled) - gloss = 1.0 - texture2D(u_roughnessMap,texCoord).r; - - gl_FragColor = vec4(vec3(metal), gloss); - }`; - -// creates canvas and context -// creates shaders for converting the the textures -function initGLAndResources(exporter) { - const canvas = document.createElement("canvas"); - const gl = canvas.getContext("webgl"); - - exporter.canvas = canvas; - exporter.gl = gl; - - exporter.metalGlossProgram = buildShaderProgram( - gl, - DEFAULT_VERT, - METALLICGLOSS_FRAG - ); - exporter.normalProgram = buildShaderProgram(gl, DEFAULT_VERT, NORMAl_FRAG); - - createVertexBuffers(exporter); -} - -function getShaderSource(id) { - const shaderScript = document.getElementById(id); - if (!shaderScript) { - return null; - } - - let str = ""; - let k = shaderScript.firstChild; - while (k) { - if (k.nodeType == 3) { - str += k.textContent; - } - k = k.nextSibling; - } - - return str; -} - -function compileShader(gl, source, shaderType) { - const shader = gl.createShader(shaderType); - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - alert(gl.getShaderInfoLog(shader)); - return null; - } - - return shader; -} - -function buildShaderProgram(gl, vertSource, fragSource) { - const vertexShader = compileShader(gl, vertSource, gl.VERTEX_SHADER); - const fragmentShader = compileShader(gl, fragSource, gl.FRAGMENT_SHADER); - - const shaderProgram = gl.createProgram(); - gl.attachShader(shaderProgram, vertexShader); - gl.attachShader(shaderProgram, fragmentShader); - gl.linkProgram(shaderProgram); - - if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { - alert("Could not initialise shaders"); - } - - gl.useProgram(shaderProgram); - - return shaderProgram; -} - -//var texCoordBuffer = new WebGLBuffer; -//var posBuffer = new WebGLBuffer; - -// render quad using shader and texture inputs -// returns HtmlImageElement -function renderToImage(exporter, program, inputs) { - const gl = exporter.gl; - - gl.viewport(0, 0, exporter.canvas.width, exporter.canvas.height); - - // bind texture to fbo - //gl.clearColor(0,0,1,1); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - // bind shader - gl.useProgram(program); - - // pass textures - let texIndex = 0; - for (const i in inputs) { - const input = inputs[i]; - if (input.tex) { - gl.activeTexture(gl.TEXTURE0 + texIndex); - gl.bindTexture(gl.TEXTURE_2D, input.tex); - gl.uniform1i(gl.getUniformLocation(program, input.name), texIndex); - gl.uniform1i(gl.getUniformLocation(program, input.name + "Enabled"), 1); - } else { - gl.uniform1i(gl.getUniformLocation(program, input.name + "Enabled"), 0); - } - texIndex++; - } - - // bind mesh - const posLoc = gl.getAttribLocation(program, "a_pos"); - const texCoordLoc = gl.getAttribLocation(program, "a_texCoord"); - - // provide texture coordinates for the rectangle. - gl.bindBuffer(gl.ARRAY_BUFFER, exporter.posBuffer); - gl.enableVertexAttribArray(posLoc); - gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 0, 0); - - gl.bindBuffer(gl.ARRAY_BUFFER, exporter.texCoordBuffer); - gl.enableVertexAttribArray(texCoordLoc); - gl.vertexAttribPointer(texCoordLoc, 2, gl.FLOAT, false, 0, 0); - - gl.drawArrays(gl.TRIANGLES, 0, 6); - - gl.disableVertexAttribArray(posLoc); - gl.disableVertexAttribArray(texCoordLoc); -} - -function createVertexBuffers(exporter) { - const gl = exporter.gl; - - // provide texture coordinates for the rectangle. - const texCoordBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 1.0 - ]), - gl.STATIC_DRAW - ); - - const posBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - new Float32Array([ - -1.0, - -1.0, - 0.0, - 1.0, - -1.0, - 0.0, - -1.0, - 1.0, - 0.0, - -1.0, - 1.0, - 0.0, - 1.0, - -1.0, - 0.0, - 1.0, - 1.0, - 0.0 - ]), - gl.STATIC_DRAW - ); - - gl.bindBuffer(gl.ARRAY_BUFFER, null); - - exporter.texCoordBuffer = texCoordBuffer; - exporter.posBuffer = posBuffer; -} - -function canvasToBase64(canvas) { - let data = canvas.toDataURL(); - // todo: maybe script header? - // https://code-examples.net/en/q/6f412f - data = data.replace(/^data:image\/(png|jpg);base64,/, ""); - return data; -} - -function canvasToBuffer(canvas) { - // https://github.com/mattdesl/electron-canvas-to-buffer/blob/master/index.js - const url = canvas.toDataURL("image/png", 1); - const nativeImage = electron.nativeImage.createFromDataURL(url); - const buffer = nativeImage.toPNG(); - - return buffer; -} - -// todo: cleanup textures!! - -// initialize -initGLAndResources(exporter); diff --git a/src/lib/geometry/cylinder.ts b/src/lib/geometry/cylinder.ts deleted file mode 100644 index 9b10b364..00000000 --- a/src/lib/geometry/cylinder.ts +++ /dev/null @@ -1,218 +0,0 @@ -// snatched from three.js - -/** - * @author mrdoob / http://mrdoob.com/ - * @author Mugen87 / https://github.com/Mugen87 - * @author njbrown / https://twitter.com/njbrown - */ - -import { Geometry } from "three"; -import { BufferGeometry } from "three"; -import { Float32BufferAttribute } from "three"; -import { Vector3 } from "three"; -import { Vector2 } from "three"; - -class CylinderGeometry extends Geometry { - parameters = {}; - - constructor( - radiusTop?, - radiusBottom?, - height?, - radialSegments?, - heightSegments?, - openEnded?, - thetaStart?, - thetaLength? - ) { - super(); - - this.type = "CylinderGeometry"; - - this.parameters = { - radiusTop: radiusTop, - radiusBottom: radiusBottom, - height: height, - radialSegments: radialSegments, - heightSegments: heightSegments, - openEnded: openEnded, - thetaStart: thetaStart, - thetaLength: thetaLength - }; - - this.fromBufferGeometry( - new CylinderBufferGeometry( - radiusTop, - radiusBottom, - height, - radialSegments, - heightSegments, - openEnded, - thetaStart, - thetaLength - ) - ); - this.mergeVertices(); - } -} - -class CylinderBufferGeometry extends BufferGeometry { - parameters = {}; - constructor( - radiusTop?, - radiusBottom?, - height?, - radialSegments?, - heightSegments?, - openEnded?, - thetaStart?, - thetaLength? - ) { - super(); - - this.type = "CylinderBufferGeometry"; - - this.parameters = { - radiusTop: radiusTop, - radiusBottom: radiusBottom, - height: height, - radialSegments: radialSegments, - heightSegments: heightSegments, - openEnded: openEnded, - thetaStart: thetaStart, - thetaLength: thetaLength - }; - - const scope = this; - - radiusTop = radiusTop !== undefined ? radiusTop : 1; - radiusBottom = radiusBottom !== undefined ? radiusBottom : 1; - height = height || 1; - - radialSegments = Math.floor(radialSegments) || 8; - heightSegments = Math.floor(heightSegments) || 1; - - openEnded = openEnded !== undefined ? openEnded : false; - thetaStart = thetaStart !== undefined ? thetaStart : 0.0; - thetaLength = thetaLength !== undefined ? thetaLength : Math.PI * 2; - - // buffers - - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - - // helper variables - - let index = 0; - const indexArray = []; - const halfHeight = height / 2; - let groupStart = 0; - - // generate geometry - - generateTorso(); - - // open-ended by default - // if (openEnded === false) { - // if (radiusTop > 0) generateCap(true); - // if (radiusBottom > 0) generateCap(false); - // } - - // build geometry - - this.setIndex(indices); - this.addAttribute("position", new Float32BufferAttribute(vertices, 3)); - this.addAttribute("normal", new Float32BufferAttribute(normals, 3)); - this.addAttribute("uv", new Float32BufferAttribute(uvs, 2)); - - function generateTorso() { - let x, y; - const normal = new Vector3(); - const vertex = new Vector3(); - - let groupCount = 0; - - // this will be used to calculate the normal - const slope = (radiusBottom - radiusTop) / height; - - // generate vertices, normals and uvs - - for (y = 0; y <= heightSegments; y++) { - const indexRow = []; - - const v = y / heightSegments; - - // calculate the radius of the current row - - const radius = v * (radiusBottom - radiusTop) + radiusTop; - - for (x = 0; x <= radialSegments; x++) { - const u = x / radialSegments; - - const theta = u * thetaLength + thetaStart; - - const sinTheta = Math.sin(theta); - const cosTheta = Math.cos(theta); - - // vertex - - vertex.x = radius * sinTheta; - vertex.y = -v * height + halfHeight; - vertex.z = radius * cosTheta; - vertices.push(vertex.x, vertex.y, vertex.z); - - // normal - - normal.set(sinTheta, slope, cosTheta).normalize(); - normals.push(normal.x, normal.y, normal.z); - - // uv - - uvs.push(u * 4, 1 - v); - - // save index of vertex in respective row - - indexRow.push(index++); - } - - // now save vertices of the row in our index array - - indexArray.push(indexRow); - } - - // generate indices - - for (x = 0; x < radialSegments; x++) { - for (y = 0; y < heightSegments; y++) { - // we use the index array to access the correct indices - - const a = indexArray[y][x]; - const b = indexArray[y + 1][x]; - const c = indexArray[y + 1][x + 1]; - const d = indexArray[y][x + 1]; - - // faces - - indices.push(a, b, d); - indices.push(b, c, d); - - // update group counter - - groupCount += 6; - } - } - - // add a group to the geometry. this will ensure multi material support - - scope.addGroup(groupStart, groupCount, 0); - - // calculate new start value for groups - - groupStart += groupCount; - } - } -} - -export { CylinderGeometry, CylinderBufferGeometry }; diff --git a/src/lib/geometry/plane.ts b/src/lib/geometry/plane.ts deleted file mode 100644 index a88b03f0..00000000 --- a/src/lib/geometry/plane.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @author mrdoob / http://mrdoob.com/ - * @author Mugen87 / https://github.com/Mugen87 - * @author njbrown / https://twitter.com/njbrown - */ - -import { Geometry } from "three"; -import { BufferGeometry } from "three"; -import { Float32BufferAttribute } from "three"; -import { Vector3 } from "three"; -import { Vector2 } from "three"; - -// PlaneGeometry (X, Z) - -class PlaneGeometry extends Geometry { - parameters = {}; - - constructor(width, height, widthSegments, heightSegments) { - super(); - this.type = "PlaneGeometry"; - - this.parameters = { - width: width, - height: height, - widthSegments: widthSegments, - heightSegments: heightSegments - }; - - this.fromBufferGeometry( - new PlaneBufferGeometry(width, height, widthSegments, heightSegments) - ); - this.mergeVertices(); - } -} - -// PlaneBufferGeometry - -class PlaneBufferGeometry extends BufferGeometry { - parameters = {}; - - constructor(width, height, widthSegments, heightSegments) { - super(); - - this.type = "PlaneBufferGeometry"; - - this.parameters = { - width: width, - height: height, - widthSegments: widthSegments, - heightSegments: heightSegments - }; - - width = width || 1; - height = height || 1; - - const width_half = width / 2; - const height_half = height / 2; - - const gridX = Math.floor(widthSegments) || 1; - const gridY = Math.floor(heightSegments) || 1; - - const gridX1 = gridX + 1; - const gridY1 = gridY + 1; - - const segment_width = width / gridX; - const segment_height = height / gridY; - - // buffers - - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - - // generate vertices, normals and uvs - - for (let iy = 0; iy < gridY1; iy++) { - const y = iy * segment_height - height_half; - - for (let ix = 0; ix < gridX1; ix++) { - const x = ix * segment_width - width_half; - - vertices.push(x, 0, y); - - normals.push(0, 1, 0); - - uvs.push(ix / gridX); - uvs.push(1 - iy / gridY); - } - } - - // indices - - for (let iy = 0; iy < gridY; iy++) { - for (let ix = 0; ix < gridX; ix++) { - const a = ix + gridX1 * iy; - const b = ix + gridX1 * (iy + 1); - const c = ix + 1 + gridX1 * (iy + 1); - const d = ix + 1 + gridX1 * iy; - - // faces - - indices.push(a, b, d); - indices.push(b, c, d); - } - } - - // build geometry - - this.setIndex(indices); - this.setAttribute("position", new Float32BufferAttribute(vertices, 3)); - this.setAttribute("normal", new Float32BufferAttribute(normals, 3)); - this.setAttribute("uv", new Float32BufferAttribute(uvs, 2)); - } -} - -export { PlaneGeometry, PlaneBufferGeometry }; diff --git a/src/lib/geometry/sphere.ts b/src/lib/geometry/sphere.ts deleted file mode 100644 index fb57f9b8..00000000 --- a/src/lib/geometry/sphere.ts +++ /dev/null @@ -1,183 +0,0 @@ -// snatched from threejs -/** - * @author mrdoob / http://mrdoob.com/ - * @author benaadams / https://twitter.com/ben_a_adams - * @author Mugen87 / https://github.com/Mugen87 - * @author njbrown / https://twitter.com/njbrown - */ - -import { Geometry } from "three"; -import { BufferGeometry } from "three"; -import { Float32BufferAttribute } from "three"; -import { Vector3 } from "three"; - -class SphereGeometry extends Geometry { - parameters = {}; - constructor( - radius?, - widthSegments?, - heightSegments?, - phiStart?, - phiLength?, - thetaStart?, - thetaLength? - ) { - super(); - - this.type = "SphereGeometry"; - - this.parameters = { - radius: radius, - widthSegments: widthSegments, - heightSegments: heightSegments, - phiStart: phiStart, - phiLength: phiLength, - thetaStart: thetaStart, - thetaLength: thetaLength - }; - - this.fromBufferGeometry( - new SphereBufferGeometry( - radius, - widthSegments, - heightSegments, - phiStart, - phiLength, - thetaStart, - thetaLength - ) - ); - //this.mergeVertices(); - } -} - -// SphereBufferGeometry - -class SphereBufferGeometry extends BufferGeometry { - parameters = {}; - constructor( - radius, - widthSegments, - heightSegments, - phiStart, - phiLength, - thetaStart, - thetaLength - ) { - super(); - - this.type = "SphereBufferGeometry"; - - this.parameters = { - radius: radius, - widthSegments: widthSegments, - heightSegments: heightSegments, - phiStart: phiStart, - phiLength: phiLength, - thetaStart: thetaStart, - thetaLength: thetaLength - }; - - radius = radius || 1; - - widthSegments = Math.max(3, Math.floor(widthSegments) || 8); - heightSegments = Math.max(2, Math.floor(heightSegments) || 6); - - phiStart = phiStart !== undefined ? phiStart : 0; - phiLength = phiLength !== undefined ? phiLength : Math.PI * 2; - - thetaStart = thetaStart !== undefined ? thetaStart : 0; - thetaLength = thetaLength !== undefined ? thetaLength : Math.PI; - - const thetaEnd = Math.min(thetaStart + thetaLength, Math.PI); - - let ix, iy; - - let index = 0; - const grid = []; - - const vertex = new Vector3(); - const normal = new Vector3(); - - // buffers - - const indices = []; - const vertices = []; - const normals = []; - const uvs = []; - - // generate vertices, normals and uvs - - for (iy = 0; iy <= heightSegments; iy++) { - const verticesRow = []; - - const v = iy / heightSegments; - - // special case for the poles - - let uOffset = 0; - - if (iy == 0 && thetaStart == 0) { - uOffset = 0.5 / widthSegments; - } else if (iy == heightSegments && thetaEnd == Math.PI) { - uOffset = -0.5 / widthSegments; - } - - for (ix = 0; ix <= widthSegments; ix++) { - const u = ix / widthSegments; - - // vertex - - vertex.x = - -radius * - Math.cos(phiStart + u * phiLength) * - Math.sin(thetaStart + v * thetaLength); - vertex.y = radius * Math.cos(thetaStart + v * thetaLength); - vertex.z = - radius * - Math.sin(phiStart + u * phiLength) * - Math.sin(thetaStart + v * thetaLength); - - vertices.push(vertex.x, vertex.y, vertex.z); - - // normal - - normal.copy(vertex).normalize(); - normals.push(normal.x, normal.y, normal.z); - - // uv - - uvs.push((u + uOffset) * 2, 1 - v); - - verticesRow.push(index++); - } - - grid.push(verticesRow); - } - - // indices - - for (iy = 0; iy < heightSegments; iy++) { - for (ix = 0; ix < widthSegments; ix++) { - const a = grid[iy][ix + 1]; - const b = grid[iy][ix]; - const c = grid[iy + 1][ix]; - const d = grid[iy + 1][ix + 1]; - - if (iy !== 0 || thetaStart > 0) indices.push(a, b, d); - if (iy !== heightSegments - 1 || thetaEnd < Math.PI) - indices.push(b, c, d); - } - } - - // build geometry - - this.setIndex(indices); - this.addAttribute("position", new Float32BufferAttribute(vertices, 3)); - this.addAttribute("normal", new Float32BufferAttribute(normals, 3)); - this.addAttribute("uv", new Float32BufferAttribute(uvs, 2)); - this.addAttribute("uv2", new Float32BufferAttribute(uvs, 2)); - } -} - -export { SphereGeometry, SphereBufferGeometry }; diff --git a/src/lib/library/libraryv0.ts b/src/lib/library/libraryv0.ts deleted file mode 100644 index 0b334bf8..00000000 --- a/src/lib/library/libraryv0.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - TestShapeNode, - TestGradientNode, - TestSimplexNode, - TestWorleyNode, - TestMultiplyNode, - TestInvertNode, - TestNormalNode, - TestBrickNode, - TestWarpNode -} from "./v0/nodes"; - -import { DesignerLibrary } from "../designer/library"; - -export function createLibrary() { - const lib = new DesignerLibrary(); - lib.versionName = "v0"; - lib.addNode("shape", "Shape", TestShapeNode); - lib.addNode("gradient", "Gradient", TestGradientNode); - lib.addNode("multiply", "Multiply", TestMultiplyNode); - lib.addNode("invert", "Invert", TestInvertNode); - lib.addNode("worley", "Worley", TestWorleyNode); - lib.addNode("normal", "Normal", TestNormalNode); - lib.addNode("brick", "Brick", TestBrickNode); - lib.addNode("warp", "Warp", TestWarpNode); - lib.addNode("simplex", "Simpled Noise", TestSimplexNode); - - return lib; -} diff --git a/src/lib/library/libraryv1.ts b/src/lib/library/libraryv1.ts deleted file mode 100644 index 7e771d6a..00000000 --- a/src/lib/library/libraryv1.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { BrickGeneratorNode } from "./v1/brickgenerator"; -import { NormalMapNode, BetterWarpNode } from "./v1/normalmap"; -import { CellNode } from "./v1/cellnode"; -import { SolidCellNode } from "./v1/solidcell"; -import { LineCellNode } from "./v1/linecell"; -import { PolygonNode, CircleNode } from "./v1/shapes"; -import { BlendNode } from "./v1/blend"; -import { InvertNode } from "./v1/invert"; -import { WarpNode } from "./v1/warp"; -import { ColorNode, ColorizeNode } from "./v1/color"; -import { SimplexNoiseNode } from "./v1/simplexnoise"; -import { MaskNode } from "./v1/mask"; -import { Transform2DNode } from "./v1/transform2d"; -import { MapRangeNode } from "./v1/maprange"; -import { SplatNode } from "./v1/splat"; -import { Perlin3DNode } from "./v1/perlin3d"; -import { HexagonNode } from "./v1/hexagon"; -import { FractalNoiseNode } from "./v1/fractalnoise"; -import { TileNode } from "./v1/tile"; -import { ThresholdNode } from "./v1/threshold"; -import { HeightShiftNode } from "./v1/heightshift"; -import { CheckerBoardNode } from "./v1/checkerboard"; -import { DirectionalWarpNode } from "./v1/directionalwarp"; -import { MirrorNode } from "./v1/mirror"; -import { BrightnessContrastNode } from "./v1/brightnesscontrast"; -import { WaveNode } from "./v1/wave"; -import { CopyNode } from "./v1/copy"; -import { GradientNode, TriGradientNode } from "./v1/gradient"; -import { DesignerLibrary } from "../designer/library"; -import { OutputNode } from "./v1/output"; -import { GradientMapNode } from "./v1/gradientmap"; - -export function createLibrary() { - const lib = new DesignerLibrary(); - lib.versionName = "v1"; - lib.addNode("brickgenerator", "Brick Generator", BrickGeneratorNode); - lib.addNode("normalmap", "Normal Map", NormalMapNode); - lib.addNode("cell", "Cell", CellNode); - lib.addNode("solidcell", "Solid Cell", SolidCellNode); - lib.addNode("linecell", "Line Cell", LineCellNode); - lib.addNode("circle", "Circle", CircleNode); - lib.addNode("polygon", "Polygon", PolygonNode); - lib.addNode("blend", "Blend", BlendNode); - lib.addNode("invert", "Invert", InvertNode); - lib.addNode("warp", "Warp", WarpNode); - lib.addNode("color", "Color", ColorNode); - lib.addNode("colorize", "Colorize", ColorizeNode); - lib.addNode("simplexnoise", "Simplex Noise", SimplexNoiseNode); - lib.addNode("mask", "Mask", MaskNode); - lib.addNode("transform2d", "Transform2D", Transform2DNode); - lib.addNode("maprange", "Map Range", MapRangeNode); - lib.addNode("splat", "Splat", SplatNode); - lib.addNode("perlin3d", "Perlin 3D", Perlin3DNode); - lib.addNode("hexagon", "Hexagon", HexagonNode); - lib.addNode("fractalnoise", "Fractal Noise", FractalNoiseNode); - lib.addNode("tile", "Tile", TileNode); - lib.addNode("threshold", "Threshold", ThresholdNode); - lib.addNode("heightshift", "Height Shift", HeightShiftNode); - lib.addNode("checkerboard", "CheckerBoard", CheckerBoardNode); - lib.addNode("directionalwarp", "Directional Warp", DirectionalWarpNode); - lib.addNode("mirror", "Mirror", MirrorNode); - lib.addNode( - "brightnesscontrast", - "Brightness Contrast", - BrightnessContrastNode - ); - lib.addNode("wave", "Wave", WaveNode); - lib.addNode("copy", "Copy", CopyNode); - lib.addNode("gradient", "Gradient", GradientNode); - lib.addNode("trigradient", "TriGradient", TriGradientNode); - lib.addNode("output", "Output", OutputNode); - lib.addNode("gradientmap", "Gradient Map", GradientMapNode); - // lib.addNode("betterwarp", "Better Warp", BetterWarpNode); - - return lib; -} diff --git a/src/lib/library/libraryv2.ts b/src/lib/library/libraryv2.ts deleted file mode 100644 index dae2dbc7..00000000 --- a/src/lib/library/libraryv2.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { BrickGeneratorNode } from "./v1/brickgenerator"; -import { NormalMapV2 } from "./v2/normalmapv2"; -import { CellNode } from "./v1/cellnode"; -import { SolidCellNode } from "./v1/solidcell"; -import { LineCellNode } from "./v1/linecell"; -import { PolygonNode, CircleNode } from "./v1/shapes"; -import { BlendNode } from "./v1/blend"; -import { InvertNode } from "./v1/invert"; -import { WarpNodeV2 } from "./v2/warpv2"; -import { ColorNode, ColorizeNode } from "./v1/color"; -import { SimplexNoiseNode } from "./v1/simplexnoise"; -import { MaskNode } from "./v1/mask"; -import { Transform2DNodeV2 } from "./v2/transform2dv2"; -import { MapRangeNode } from "./v1/maprange"; -import { Perlin3DNode } from "./v1/perlin3d"; -import { HexagonNode } from "./v1/hexagon"; -import { FractalNoiseNode } from "./v1/fractalnoise"; -import { TileNode } from "./v1/tile"; -import { ThresholdNode } from "./v1/threshold"; -import { HeightShiftNode } from "./v1/heightshift"; -import { CheckerBoardNode } from "./v1/checkerboard"; -import { DirectionalWarpNode } from "./v1/directionalwarp"; -import { MirrorNode } from "./v1/mirror"; -import { BrightnessContrastNode } from "./v1/brightnesscontrast"; -import { WaveNode } from "./v1/wave"; -import { CopyNode } from "./v1/copy"; -import { GradientNode, TriGradientNode } from "./v1/gradient"; -import { DesignerLibrary } from "../designer/library"; -import { OutputNode } from "./v1/output"; -import { GradientMapNode } from "./v1/gradientmap"; -import { SplatNodeV2 } from "./v2/splat"; -import { BlurV2 } from "./v2/blur"; -import { AdvanceSplatterV2 } from "./v2/advancesplatter"; -import { SlopeBlur } from "./v2/slopeblur"; -import { TileSampler } from "./v2/tilesampler"; -import { GradientDynamic } from "./v2/gradientdynamic"; -import { Clamp } from "./v2/clamp"; -import { Pow } from "./v2/pow"; -import { Quantize } from "./v2/quantize"; -import { HistogramShift } from "./v2/histogramshift"; -import { HistogramScan } from "./v2/histogramscan"; -import { HistogramSelect } from "./v2/histogramselect"; -import { Skew } from "./v2/skew"; -import { Bevel } from "./v2/bevel"; -import { FloodFill } from "./v2/floodfill"; -import { FloodFillToColor } from "./v2/floodfilltocolor"; -import { FloodFillToBBox } from "./v2/floodfilltobbox"; -import { FloodFillToRandomColor } from "./v2/floodfilltorandomcolor"; -import { FloodFillToRandomIntensity } from "./v2/floodfilltorandomintensity"; -import { FloodFillSampler } from "./v2/floodfillsampler"; -import { FloodFillToGradient } from "./v2/floodfilltogradient"; -import { StripesNode } from "./v2/stripes"; -import { RgbaMerge } from "./v2/rgbamerge"; -import { RgbaShuffle } from "./v2/rgbashuffle"; -import { ExtractChannel } from "./v2/extractchannel"; -import { Grayscale } from "./v2/grayscale"; -import { InvertNormal } from "./v2/invertnormal"; -import { CapsuleNode } from "./v2/capsule"; -import { CartesianToPolar } from "./v2/cartesiantopolar"; -import { PolarToCartesian } from "./v2/polartocartesian"; -import { CombineNormals } from "./v2/combinenormals"; -import { SoftFlower } from "./v2/softflower"; -import { Star } from "./v2/star"; -import { HslExtract } from "./v2/hslextract"; -import { Hsl } from "./v2/hsl"; -import { DirectionalBlur } from "./v2/directionalblur"; -import { AnisotropicBlur } from "./v2/anisotropicblur"; -import { CircularSplatter } from "./v2/circularsplatter"; -import { ValueNoise } from "./v2/valuenoise"; -import { ValueNoiseFractalSum } from "./v2/valuenoisefractalsum"; -import { GradientNoise } from "./v2/gradientnoise"; -import { GradientNoiseFractalSum } from "./v2/gradientnoisefractalsum"; -import { SimplexNoiseV2 } from "./v2/simplexnoise"; -import { DirectionalWarpNodeV2 } from "./v2/directionalwarpv2"; -import { ImageNode } from "./v2/imagenode"; - -export function createLibrary() { - const lib = new DesignerLibrary(); - lib.versionName = "v2"; - lib.addNode("brickgenerator", "Brick Generator", BrickGeneratorNode); - lib.addNode("normalmap", "Normal Map", NormalMapV2); - lib.addNode("cell", "Cell", CellNode); - lib.addNode("solidcell", "Solid Cell", SolidCellNode); - lib.addNode("linecell", "Line Cell", LineCellNode); - lib.addNode("circle", "Circle", CircleNode); - lib.addNode("polygon", "Polygon", PolygonNode); - lib.addNode("blend", "Blend", BlendNode); - lib.addNode("invert", "Invert", InvertNode); - lib.addNode("warp", "Warp", WarpNodeV2); - lib.addNode("color", "Color", ColorNode); - lib.addNode("colorize", "Colorize", ColorizeNode); - lib.addNode("simplexnoise", "Simplex Noise", SimplexNoiseNode); - lib.addNode("mask", "Mask", MaskNode); - lib.addNode("transform2d", "Transform2D", Transform2DNodeV2); - lib.addNode("maprange", "Map Range", MapRangeNode); - lib.addNode("splat", "Splat", SplatNodeV2); - lib.addNode("perlin3d", "Perlin 3D", Perlin3DNode); - lib.addNode("hexagon", "Hexagon", HexagonNode); - lib.addNode("fractalnoise", "Fractal Noise", FractalNoiseNode); - lib.addNode("tile", "Tile", TileNode); - lib.addNode("threshold", "Threshold", ThresholdNode); - lib.addNode("heightshift", "Height Shift", HeightShiftNode); - lib.addNode("checkerboard", "CheckerBoard", CheckerBoardNode); - lib.addNode("directionalwarp", "Directional Warp", DirectionalWarpNodeV2); - lib.addNode("mirror", "Mirror", MirrorNode); - lib.addNode( - "brightnesscontrast", - "Brightness Contrast", - BrightnessContrastNode - ); - lib.addNode("wave", "Wave", WaveNode); - lib.addNode("copy", "Copy", CopyNode); - lib.addNode("gradient", "Gradient", GradientNode); - lib.addNode("trigradient", "TriGradient", TriGradientNode); - lib.addNode("output", "Output", OutputNode); - lib.addNode("gradientmap", "Gradient Map", GradientMapNode); - lib.addNode("blurv2", "Blur", BlurV2); - lib.addNode("slopeblur", "Slope Blur", SlopeBlur); - lib.addNode("advancesplatter", "Advance Splatter", AdvanceSplatterV2); - lib.addNode("tilesampler", "Tile Sampler", TileSampler); - lib.addNode("gradientdynamic", "Gradient Dynamic", GradientDynamic); - lib.addNode("clamp", "Clamp", Clamp); - lib.addNode("pow", "Pow", Pow); - lib.addNode("quantize", "Quantize", Quantize); - lib.addNode("histogramshift", "Histogram Shift", HistogramShift); - lib.addNode("histogramscan", "Histogram Scan", HistogramScan); - lib.addNode("histogramselect", "Histogram Select", HistogramSelect); - lib.addNode("skew", "Skew", Skew); - lib.addNode("bevel", "Bevel", Bevel); - lib.addNode("floodfill", "Flood Fill", FloodFill); - lib.addNode("floodfilltocolor", "Flood Fill To Color", FloodFillToColor); - lib.addNode("floodfilltobbox", "Flood Fill To BBox", FloodFillToBBox); - lib.addNode( - "floodfilltorandomcolor", - "Flood Fill To Random Color", - FloodFillToRandomColor - ); - lib.addNode( - "floodfilltorandomintensity", - "Flood Fill To Random Intensity", - FloodFillToRandomIntensity - ); - lib.addNode("floodfillsampler", "Flood Fill Sampler", FloodFillSampler); - lib.addNode( - "floodfilltogradient", - "Flood Fill To Gradient", - FloodFillToGradient - ); - lib.addNode("stripes", "Stripes", StripesNode); - lib.addNode("rgbamerge", "RGBA Merge", RgbaMerge); - lib.addNode("rgbashuffle", "RGBA Shuffle", RgbaShuffle); - lib.addNode("extractchannel", "Extract Channel", ExtractChannel); - lib.addNode("grayscale", "Grayscale", Grayscale); - lib.addNode("invertnormal", "Invert Normal", InvertNormal); - lib.addNode("capsule", "Capsule", CapsuleNode); - lib.addNode("cartesiantopolar", "Cartesian to Polar", CartesianToPolar); - lib.addNode("polartocartesian", "Polar to Cartesian", PolarToCartesian); - lib.addNode("combinenormals", "Combine Normals", CombineNormals); - lib.addNode("softflower", "Soft Flower", SoftFlower); - lib.addNode("star", "Star", Star); - lib.addNode("hslextract", "HSL Extract", HslExtract); - lib.addNode("hsl", "HSL", Hsl); - lib.addNode("directionalblur", "Directional Blur", DirectionalBlur); - lib.addNode("anisotropic", "Anisotropic Blur", AnisotropicBlur); - lib.addNode("circularsplatter", "Circular Splatter", CircularSplatter); - lib.addNode("valuenoise", "Value Noise", ValueNoise); - lib.addNode("gradientnoise", "Gradient Noise", GradientNoise); - lib.addNode( - "gradientnoisefractalsum", - "Gradient Noise Fractal Sum", - GradientNoiseFractalSum - ); - lib.addNode( - "valuenoisefractalsum", - "Value Noise Fractal Sum", - ValueNoiseFractalSum - ); - lib.addNode( - "valuenoisefractalsum", - "Value Noise Fractal Sum", - ValueNoiseFractalSum - ); - lib.addNode("image", "Image", ImageNode); - //lib.addNode("simplexnoise", "Simplex Noise", SimplexNoiseV2); - // lib.addNode("betterwarp", "Better Warp", BetterWarpNode); - - return lib; -} diff --git a/src/lib/library/v0/nodes.ts b/src/lib/library/v0/nodes.ts deleted file mode 100644 index 99288807..00000000 --- a/src/lib/library/v0/nodes.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class TestGradientNode extends GpuDesignerNode { - public init() { - this.title = "Gradient Node"; - - const source = ` - vec4 process(vec2 uv) - { - return vec4(uv.x, 0, uv.y, 1); - } - `; - - this.buildShader(source); - } -} - -export class TestInvertNode extends GpuDesignerNode { - public init() { - this.title = "Invert"; - - this.addInput("color"); - - const source = ` - vec4 process(vec2 uv) - { - vec4 col = vec4(1.0) - texture(color,uv); - col.a = 1.0; - return col; - } - `; - - this.buildShader(source); - } -} - -export class TestMultiplyNode extends GpuDesignerNode { - public init() { - this.title = "Multiply"; - - this.addInput("colorA"); - this.addInput("colorB"); - - const source = ` - vec4 process(vec2 uv) - { - vec4 colA = texture(colorA,uv); - vec4 colB = texture(colorB,uv); - return colA * colB; - } - `; - - this.buildShader(source); - } -} - -export class TestShapeNode extends GpuDesignerNode { - public init() { - this.title = "Shape"; - //this.exportName = "result"; - - const source = ` - #define PI 3.14159265359 - #define TWO_PI 6.28318530718 - - vec4 process(vec2 uv) - { - vec3 color = vec3(0.0); - float d = 0.0; - - float scale = 0.5; - vec2 st = (uv - vec2(0.5,0.5)); - int N = 8; - - // Angle and radius from the current pixel - float a = atan(st.x,st.y)+PI; - float r = TWO_PI/float(N); - - // Shaping function that modulate the distance - d = cos(floor(.9+a/r)*r-a)*length(st) * (1.0 / scale); - - color = vec3(1.0-smoothstep(.5,.500001,d)); - - return vec4(color,1.0); - } - `; - - this.buildShader(source); - } -} - -export class TestWorleyNode extends GpuDesignerNode { - public init() { - this.title = "Worley"; - //this.exportName = "result"; - - this.addFloatProperty("scale", "Cell Scale", 5, 1, 20); - - const source = ` - vec3 mod289(vec3 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; - } - - vec2 mod289(vec2 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; - } - - // Modulo 7 without a division - vec3 mod7(vec3 x) { - return x - floor(x * (1.0 / 7.0)) * 7.0; - } - - // Permutation polynomial: (34x^2 + x) mod 289 - vec3 permute(vec3 x) { - return mod289((34.0 * x + 1.0) * x); - } - - // Cellular noise, returning F1 and F2 in a vec2. - // Standard 3x3 search window for good F1 and F2 values - vec2 cellular(vec2 P) { - #define K 0.142857142857 // 1/7 - #define Ko 0.428571428571 // 3/7 - #define jitter 1.0 // Less gives more regular pattern - vec2 Pi = mod289(floor(P)); - vec2 Pf = fract(P); - vec3 oi = vec3(-1.0, 0.0, 1.0); - vec3 of = vec3(-0.5, 0.5, 1.5); - vec3 px = permute(Pi.x + oi); - vec3 p = permute(px.x + Pi.y + oi); // p11, p12, p13 - vec3 ox = fract(p*K) - Ko; - vec3 oy = mod7(floor(p*K))*K - Ko; - vec3 dx = Pf.x + 0.5 + jitter*ox; - vec3 dy = Pf.y - of + jitter*oy; - vec3 d1 = dx * dx + dy * dy; // d11, d12 and d13, squared - p = permute(px.y + Pi.y + oi); // p21, p22, p23 - ox = fract(p*K) - Ko; - oy = mod7(floor(p*K))*K - Ko; - dx = Pf.x - 0.5 + jitter*ox; - dy = Pf.y - of + jitter*oy; - vec3 d2 = dx * dx + dy * dy; // d21, d22 and d23, squared - p = permute(px.z + Pi.y + oi); // p31, p32, p33 - ox = fract(p*K) - Ko; - oy = mod7(floor(p*K))*K - Ko; - dx = Pf.x - 1.5 + jitter*ox; - dy = Pf.y - of + jitter*oy; - vec3 d3 = dx * dx + dy * dy; // d31, d32 and d33, squared - // Sort out the two smallest distances (F1, F2) - vec3 d1a = min(d1, d2); - d2 = max(d1, d2); // Swap to keep candidates for F2 - d2 = min(d2, d3); // neither F1 nor F2 are now in d3 - d1 = min(d1a, d2); // F1 is now in d1 - d2 = max(d1a, d2); // Swap to keep candidates for F2 - d1.xy = (d1.x < d1.y) ? d1.xy : d1.yx; // Swap if smaller - d1.xz = (d1.x < d1.z) ? d1.xz : d1.zx; // F1 is in d1.x - d1.yz = min(d1.yz, d2.yz); // F2 is now not in d2.yz - d1.y = min(d1.y, d1.z); // nor in d1.z - d1.y = min(d1.y, d2.x); // F2 is in d1.y, we're done. - return sqrt(d1.xy); - } - - vec4 process(vec2 uv) - { - float scale = 7.0; - //vec2 cel = cellular(uv*vec2(scale)); - vec2 cel = cellular(uv*vec2(prop_scale)); - return vec4(vec3(cel.y - cel.x),1.0); - } - `; - - this.buildShader(source); - } -} - -// http://www.smart-page.net/smartnormal/js/SmartNormalMapFilter.js -export class TestNormalNode extends GpuDesignerNode { - public init() { - this.title = "Normal Map"; - //this.exportName = "result"; - this.addInput("height"); - - this.addFloatProperty("strength", "Strength", 1, 0, 10, 0.01); - - const source = ` - vec4 process(vec2 uv) - { - //vec2 size = textureSize(height); - vec2 size = vec2(1024,1024); // quick hack for now - vec2 step = vec2(1.0,1.0)/size; - - // center point - float d0 = abs(texture(height, uv + vec2(0.0, 0.0)).r); - - // sample horizontally - float d1 = abs(texture(height, uv + vec2(step.x, 0.0)).r); - float d2 = abs(texture(height, uv + vec2(-step.x, 0.0)).r); - - // sample vertically - float d3 = abs(texture(height, uv + vec2(0.0, step.y)).r); - float d4 = abs(texture(height, uv + vec2(0.0, -step.y)).r); - - // find diff horizontally and average - float dx = ((d2 - d0) + (d0 - d1)) * 0.5; - - // find diff vertically and average - float dy = ((d4 - d0) + (d0 - d3)) * 0.5; - - // calculate normal - //float strength = 0.05; - //float strength = (10.0-prop_strength) * 0.01; - vec3 normal = normalize(vec3(dx * prop_strength, dy * prop_strength, 1.0)); - vec3 final = normal.xyz * 0.5 + 0.5; // bring to 0.0 - 1.0 range - - return vec4(final, 1.0); - } - `; - - this.buildShader(source); - } -} - -export class TestWarpNode extends GpuDesignerNode { - public init() { - this.title = "Warp"; - - this.addInput("inputImage"); - this.addInput("height"); - - this.addFloatProperty("strength", "Strength", 1, 0, 100, 0.01); - - // calculates normal, then warps uv by it - const source = ` - vec4 process(vec2 uv) - { - //vec2 size = textureSize(height); - vec2 size = vec2(1024,1024); // quick hack for now - vec2 step = vec2(1.0,1.0)/size; - - // center point - float d0 = abs(texture(height, uv + vec2(0.0, 0.0)).r); - - // sample horizontally - float d1 = abs(texture(height, uv + vec2(step.x, 0.0)).r); - float d2 = abs(texture(height, uv + vec2(-step.x, 0.0)).r); - - // sample vertically - float d3 = abs(texture(height, uv + vec2(0.0, step.y)).r); - float d4 = abs(texture(height, uv + vec2(0.0, -step.y)).r); - - // find diff horizontally and average - float dx = ((d2 - d0) + (d0 - d1)) * 0.5; - - // find diff vertically and average - float dy = ((d4 - d0) + (d0 - d3)) * 0.5; - - // calculate normal - //float strength = 0.05; - //float strength = (10.0-prop_strength) * 0.01; - vec2 offset = vec2(dx, dy); - - vec4 color = texture(inputImage, uv + offset * prop_strength); - - return color; - } - `; - - this.buildShader(source); - } -} - -// https://thebookofshaders.com/11/ -export class TestSimplexNode extends GpuDesignerNode { - public init() { - this.title = "Simplex Noise"; - - this.addFloatProperty("scale", "Scale", 100, 1, 1000, 0.01); - - const source = ` - float random (in vec2 st) { - return fract(sin(dot(st.xy, - vec2(12.9898,78.233))) - * 43758.5453123); - } - - float noise (in vec2 st) { - vec2 i = floor(st); - vec2 f = fract(st); - - // Four corners in 2D of a tile - float a = random(i); - float b = random(i + vec2(1.0, 0.0)); - float c = random(i + vec2(0.0, 1.0)); - float d = random(i + vec2(1.0, 1.0)); - - // Smooth Interpolation - - // Cubic Hermine Curve. Same as SmoothStep() - vec2 u = f*f*(3.0-2.0*f); - // u = smoothstep(0.,1.,f); - - // Mix 4 coorners porcentages - return mix(a, b, u.x) + - (c - a)* u.y * (1.0 - u.x) + - (d - b) * u.x * u.y; - } - - vec2 skew (vec2 st) { - vec2 r = vec2(0.0); - r.x = 1.1547*st.x; - r.y = st.y+0.5*r.x; - return r; - } - - vec3 simplexGrid (vec2 st) { - vec3 xyz = vec3(0.0); - - vec2 p = fract(skew(st)); - if (p.x > p.y) { - xyz.xy = 1.0-vec2(p.x,p.y-p.x); - xyz.z = p.y; - } else { - xyz.yz = 1.0-vec2(p.x-p.y,p.y); - xyz.x = p.x; - } - - return fract(xyz); - } - - vec4 process(vec2 uv) - { - vec3 color = vec3(noise(uv * prop_scale)); - - return vec4(color,1.0); - } - `; - - this.buildShader(source); - } -} - -export class TestBrickNode extends GpuDesignerNode { - public init() { - this.title = "Brick"; - - this.addIntProperty("widthX", "Width X", 3, 0, 10, 1); - this.addIntProperty("widthY", "Width Y", 6, 0, 10, 1); - - this.addFloatProperty("holeX", "Hole X", 2, 1, 10, 0.01); - this.addFloatProperty("holeY", "Hole Y", 2, 1, 10, 0.01); - - const source = ` - vec4 process(vec2 uv) - { - float u = uv.x; - float v = uv.y; - - float widthX = float(prop_widthX); - float widthY = float(prop_widthY); - float shiftX = 0.5; - float shiftRandWeight = 0.0; - //float holeX = 0.03; - float holeX = prop_holeX / 100.0; - //float holeY = 0.03; - float holeY = prop_holeY / 100.0; - //float smooth = 0.05; - float smooth = 0.01; - - float normX = widthX; - float normY = widthY; - float uu = u * normX; - float vv = v * normY; - - float y = vv - floor(vv); - - if (vv >= normY) vv -= normY; - if (uu >= normX) uu -= normX; - - float by = floor(vv); - - if ((vv * 0.5 - floor(vv * 0.5)) >= 0.5) - uu += shiftX; - - if (uu >= normX) uu -= normX; - - float bx = floor(uu); - - float x = uu - floor(uu); - - float val = 1.0; - - bool inside = true; - if (holeX > 0.0) inside = inside && (x > (holeX * normX)); - if (holeY > 0.0) inside = inside && (y > (holeY * normY)); - - if (inside) { - float dist = min(min(x -holeX*normX, 1.0 - x)/normX, min(y - holeY*normY, 1.0 - y)/normY); - dist *= min(normX, normY); - - if (dist < smooth) { - val = val * dist/smooth; - } - } else val = 0.0; - - vec4 color; - color.r = val; - color.g = val; - color.b = val; - color.a = 1.0; - - return color; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/brickgenerator.ts b/src/lib/library/v1/brickgenerator.ts deleted file mode 100644 index 2c90d8e4..00000000 --- a/src/lib/library/v1/brickgenerator.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class BrickGeneratorNode extends GpuDesignerNode { - public init() { - this.title = "Brick Generator"; - - this.addFloatProperty("rows", "Rows", 6, 1, 20, 1); - this.addFloatProperty("columns", "Columns", 6, 1, 20, 1); - - this.addFloatProperty("offset", "Offset", 0.5, 0, 1, 0.1); - - // brick size - const sizeProps = this.createGroup("Size"); - sizeProps.collapsed = false; - sizeProps.add( - this.addFloatProperty("brickWidth", "Brick Width", 0.9, 0, 1, 0.01) - ); - sizeProps.add( - this.addFloatProperty("brickHeight", "Brick Height", 0.9, 0, 1, 0.01) - ); - - // height - const heightProps = this.createGroup("Height"); - heightProps.collapsed = false; - heightProps.add( - this.addFloatProperty("heightMin", "Height Min", 0.0, 0, 1, 0.05) - ); - heightProps.add( - this.addFloatProperty("heightMax", "Height Max", 1.0, 0, 1, 0.05) - ); - heightProps.add( - this.addFloatProperty("heightBalance", "Height Balance", 1.0, 0, 1, 0.05) - ); - heightProps.add( - this.addFloatProperty("heightVariance", "Height Variance", 0, 0, 1, 0.05) - ); - - const source = ` - //vec2 brickSize = vec2(prop_brickWidth, prop_brickHeight); - //vec2 tileSize = vec2(prop_rows, prop_columns); - - // float shiftX = 0.5; - // float shiftY = 0.0; - - // offset for alternating rows - //float offset = prop_offset; - - // HEIGHT FUNCTIONS - float calculateHeight(vec2 brickId) - { - // height - float heightMin = prop_heightMin; - float heightMax = prop_heightMax; - float heightBalance= prop_heightBalance; // threshold that decides whether to use height variance or not - float heightVariance = prop_heightVariance; // multiplies the heightMax-heightMin range - - - // check whether or not there should be a height range in the first place - float balRand = _rand(vec2(_seed) + brickId * vec2(0.01)); - - // if balRand is less than heightBalance it means it qualifies for a random - // height. This way if heightBalance is 0 then we only use the min luminance - if( balRand > heightBalance) { - return 1.0; - } - - // calculate height variance - // need to offset brickId to give new random result - float randVariance = _rand(vec2(_seed) + (brickId + vec2(1) ) * vec2(0.01)); - randVariance *= heightVariance; - - float range = (heightMax - heightMin); - float height = heightMax - - range * randVariance; - - return height; - } - - // slope - // float slopeX;// slope x direction - // float slopeY;// slope y direction - // float slopeBalance;// threshold that determines whether or not to use slope - // float slopeVariation;// decreases the range of the slope - - vec2 is_brick(vec2 pos) - { - vec2 brickSize = vec2(prop_brickWidth, prop_brickHeight); - - vec2 edgeSize = (vec2(1.0) - brickSize) * vec2(0.5); - vec2 brick = vec2(0.0); - - if (pos.x > edgeSize.x && pos.x < (1.0 - edgeSize.x)) - brick.x = 1.0; - - if (pos.y > edgeSize.y && pos.y < (1.0 - edgeSize.y)) - brick.y = 1.0; - - return brick; - } - - vec4 process(vec2 uv) - { - vec2 tileSize = vec2(prop_columns, prop_rows); - float offset = prop_offset; - - //vec2 pos = uv * vec2(5); - vec2 pos = uv * tileSize; - - float xOffset = 0.0; - if (fract(pos.y * 0.5) > 0.5) { - xOffset = offset; - } - pos.x += xOffset; - - // a brick's id would be floor(pos) - // this gives us its origin - // this can act as a random seed for the entire brick - vec2 brickId = floor(pos);// - vec2(xOffset, 0); - - // wrap around x - if (brickId.x > tileSize.x-1.0) - brickId.x = 0.0; - - //float lum = _rand(vec2(_seed) + brickId * vec2(0.01)); - //float lum = randomFloat(0); - float lum = calculateHeight(brickId); - pos = fract(pos); - - //vec2 isBrick = step(pos,vec2(0.95,0.9)); - vec2 isBrick =is_brick(pos); - - - return vec4(vec3(isBrick.x * isBrick.y * lum),1.0); - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/brightnesscontrast.ts b/src/lib/library/v1/brightnesscontrast.ts deleted file mode 100644 index 4173b1c6..00000000 --- a/src/lib/library/v1/brightnesscontrast.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -// https://github.com/evanw/glfx.js/blob/master/src/filters/adjust/brightnesscontrast.js -export class BrightnessContrastNode extends GpuDesignerNode { - public init() { - this.title = "Brightness Contrast"; - - this.addInput("image"); - - this.addFloatProperty("contrast", "Contrast", 0.0, -1, 1, 0.1); - this.addFloatProperty("brightness", "Brightness", 0.0, -1, 1, 0.1); - const source = ` - vec4 process(vec2 uv) - { - vec4 col = texture(image, uv); - - col.rgb += prop_brightness; - if (prop_contrast > 0.0) - col.rgb = (col.rgb - 0.5) / (1.0 - prop_contrast) + 0.5; - else - col.rgb = (col.rgb - 0.5) * (1.0 + prop_contrast) + 0.5; - - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/checkerboard.ts b/src/lib/library/v1/checkerboard.ts deleted file mode 100644 index 8254f498..00000000 --- a/src/lib/library/v1/checkerboard.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; - -// https://stackoverflow.com/questions/4694608/glsl-checkerboard-pattern -export class CheckerBoardNode extends GpuDesignerNode { - public init() { - this.title = "CheckerBoard"; - - this.addFloatProperty("rows", "Rows", 2, 1, 20, 1); - this.addFloatProperty("columns", "Columns", 2, 1, 20, 1); - - this.addColorProperty("color", "Color", new Color()); - - const source = ` - vec4 process(vec2 uv) - { - if ((mod(prop_columns*uv.x, 1.0) < 0.5) ^^ (mod(prop_rows*uv.y, 1.0) < 0.5)) - { - return vec4(prop_color.rgb, 1.0); - } - else - { - return vec4(1.0, 1.0, 1.0, 1.0); - } - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/color.ts b/src/lib/library/v1/color.ts deleted file mode 100644 index 4f96ba13..00000000 --- a/src/lib/library/v1/color.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; - -export class ColorizeNode extends GpuDesignerNode { - public init() { - this.title = "Colorize"; - - this.addInput("image"); - - this.addColorProperty("color", "Color", new Color()); - - const source = ` - vec4 process(vec2 uv) - { - return texture(image,uv) * prop_color; - } - `; - - this.buildShader(source); - } -} - -export class ColorNode extends GpuDesignerNode { - public init() { - this.title = "Color"; - - this.addColorProperty("color", "Color", new Color()); - - const source = ` - vec4 process(vec2 uv) - { - return prop_color; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/copy.ts b/src/lib/library/v1/copy.ts deleted file mode 100644 index 8af68e78..00000000 --- a/src/lib/library/v1/copy.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class CopyNode extends GpuDesignerNode { - public init() { - this.title = "Copy"; - - this.addInput("image"); - this.addStringProperty("name", "Name"); - - const source = ` - vec4 process(vec2 uv) - { - vec4 col = texture(image, uv); - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/directionalwarp.ts b/src/lib/library/v1/directionalwarp.ts deleted file mode 100644 index d14151d7..00000000 --- a/src/lib/library/v1/directionalwarp.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class DirectionalWarpNode extends GpuDesignerNode { - public init() { - this.title = "Directional Warp"; - - this.addInput("inputImage"); - this.addInput("height"); - - this.addFloatProperty("intensity", "Intensity", 0.1, -0.5, 0.5, 0.01); - this.addFloatProperty("angle", "Angle", 0.85, 0.0, 3.142, 0.01); - - // calculates normal, then warps uv by it - const source = ` - vec4 process(vec2 uv) - { - vec2 step = vec2(1.0,1.0)/_textureSize; - - vec2 dir = normalize(vec2(cos(prop_angle), sin(prop_angle))); - - // center point - float dist = abs(texture(height, uv).r) - 0.5; - - vec4 color = texture(inputImage, uv + dir * prop_intensity * dist); - - return color; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/gradient.ts b/src/lib/library/v1/gradient.ts deleted file mode 100644 index ed93afc0..00000000 --- a/src/lib/library/v1/gradient.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; - -export class GradientNode extends GpuDesignerNode { - public init() { - this.title = "Gradient"; - - const white = new Color(); - white.r = 1; - white.g = 1; - white.b = 1; - - this.addColorProperty("colorA", "Color A", Color.parse("#000000")); - this.addFloatProperty("posA", "Position A", 0, 0, 1, 0.01); - this.addColorProperty("colorB", "Color B", white); - this.addFloatProperty("posB", "Position B", 1, 0, 1, 0.01); - - this.addEnumProperty("mode", "Gradient Direction", [ - "Left To Right", - "Right To Left", - "Top To Bottom", - "Bottom To Top" - ]); - - const source = ` - #define POINTS_MAX 32 - - // assumes points are sorted - vec3 calcGradient(float t, vec3 colors[POINTS_MAX], float positions[POINTS_MAX], int numPoints) - { - if (numPoints == 0) - return vec3(1,0,0); - - if (numPoints == 1) - return colors[0]; - - // here at least two points are available - if (t < positions[0]) - return colors[0]; - - int last = numPoints - 1; - if (t > positions[last]) - return colors[last]; - - // find two points in-between and lerp - - for(int i = 0; i < numPoints-1;i++) { - if (positions[i+1] > t) { - vec3 colorA = colors[i]; - vec3 colorB = colors[i+1]; - - float t1 = positions[i]; - float t2 = positions[i+1]; - - float lerpPos = (t - t1)/(t2 - t1); - return mix(colorA, colorB, lerpPos); - - } - - } - - return vec3(0,0,0); - } - - - vec4 process(vec2 uv) - { - float t = 0.0; - - // left to right - if (prop_mode == 0) - t = uv.x; - // right to left - else if (prop_mode == 1) - t = 1.0 - uv.x; - // top to bottom - else if (prop_mode == 2) - t = 1.0 - uv.y; - // bottom to top - else if (prop_mode == 3) - t = uv.y; - - - vec3 colors[POINTS_MAX]; - colors[0] = prop_colorA.rgb; - colors[1] = prop_colorB.rgb; - float positions[POINTS_MAX]; - positions[0] = prop_posA; - positions[1] = prop_posB; - - - vec3 col = calcGradient(t, colors, positions, 2); - - return vec4(col,1.0); - } - `; - - this.buildShader(source); - } -} - -export class TriGradientNode extends GpuDesignerNode { - public init() { - this.title = "TriGradient"; - - const white = new Color(); - white.r = 1; - white.g = 1; - white.b = 1; - - this.addColorProperty("colorA", "Color A", Color.parse("#000000")); - this.addFloatProperty("posA", "Position A", 0, 0, 1, 0.01); - - this.addColorProperty("colorB", "Color B", white); - this.addFloatProperty("posB", "Position B", 0.5, 0, 1, 0.01); - - this.addColorProperty("colorC", "Color C", Color.parse("#000000")); - this.addFloatProperty("posC", "Position C", 1, 0, 1, 0.01); - - this.addEnumProperty("mode", "Gradient Direction", [ - "Left To Right", - "Right To Left", - "Top To Bottom", - "Bottom To Top" - ]); - - const source = ` - #define POINTS_MAX 32 - - // assumes points are sorted - vec3 calcGradient(float t, vec3 colors[POINTS_MAX], float positions[POINTS_MAX], int numPoints) - { - if (numPoints == 0) - return vec3(1,0,0); - - if (numPoints == 1) - return colors[0]; - - // here at least two points are available - if (t < positions[0]) - return colors[0]; - - int last = numPoints - 1; - if (t > positions[last]) - return colors[last]; - - // find two points in-between and lerp - - for(int i = 0; i < numPoints-1;i++) { - if (positions[i+1] > t) { - vec3 colorA = colors[i]; - vec3 colorB = colors[i+1]; - - float t1 = positions[i]; - float t2 = positions[i+1]; - - float lerpPos = (t - t1)/(t2 - t1); - return mix(colorA, colorB, lerpPos); - - } - - } - - return vec3(0,0,0); - } - - - vec4 process(vec2 uv) - { - float t = 0.0; - - // left to right - if (prop_mode == 0) - t = uv.x; - // right to left - else if (prop_mode == 1) - t = 1.0 - uv.x; - // top to bottom - else if (prop_mode == 2) - t = 1.0 - uv.y; - // bottom to top - else if (prop_mode == 3) - t = uv.y; - - - vec3 colors[POINTS_MAX]; - colors[0] = prop_colorA.rgb; - colors[1] = prop_colorB.rgb; - colors[2] = prop_colorC.rgb; - float positions[POINTS_MAX]; - positions[0] = prop_posA; - positions[1] = prop_posB; - positions[2] = prop_posC; - - - vec3 col = calcGradient(t, colors, positions, 3); - - return vec4(col,1.0); - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/gradientmap.ts b/src/lib/library/v1/gradientmap.ts deleted file mode 100644 index a3d9f627..00000000 --- a/src/lib/library/v1/gradientmap.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; - -export class GradientMapNode extends GpuDesignerNode { - public init() { - this.title = "GradientMap"; - - this.addInput("inputImage"); - - this.addGradientProperty("gradient", "Gradient", Gradient.default()); - - const source = ` - float grayscale(vec3 col) - { - return (col.r + col.g + col.b) / 3.0; - } - - vec4 process(vec2 uv) - { - // grayscale input color - float t = grayscale(texture(inputImage, uv).rgb); - vec3 col = sampleGradient(prop_gradient, t); - - return vec4(col, 1.0); - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/heightshift.ts b/src/lib/library/v1/heightshift.ts deleted file mode 100644 index ecdc0cca..00000000 --- a/src/lib/library/v1/heightshift.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class HeightShiftNode extends GpuDesignerNode { - public init() { - this.title = "Height Shift"; - - this.addInput("image"); - - this.addFloatProperty("shift", "Shift", 0.0, -1.0, 1.0, 0.01); - - const source = ` - vec4 process(vec2 uv) - { - vec4 a = texture(image, uv); - - return a + vec4(vec3(prop_shift), 0.0); - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/invert.ts b/src/lib/library/v1/invert.ts deleted file mode 100644 index 2e298d44..00000000 --- a/src/lib/library/v1/invert.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class InvertNode extends GpuDesignerNode { - public init() { - this.title = "Invert"; - - this.addInput("color"); - - const source = ` - vec4 process(vec2 uv) - { - vec4 col = vec4(1.0) - texture(color,uv); - col.a = 1.0; - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/maprange.ts b/src/lib/library/v1/maprange.ts deleted file mode 100644 index b5b0577a..00000000 --- a/src/lib/library/v1/maprange.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class MapRangeNode extends GpuDesignerNode { - public init() { - this.title = "Map Range"; - - this.addInput("color"); - this.addFloatProperty("in_min", "Input Minimum", 0, 0, 1.0, 0.01); - this.addFloatProperty("in_max", "Input Maximum", 1, 0, 1.0, 0.01); - this.addFloatProperty("out_min", "Output Minimum", 0, 0, 1.0, 0.01); - this.addFloatProperty("out_max", "Output Maximum", 1, 0, 1.0, 0.01); - - const source = ` - vec4 process(vec2 uv) - { - vec4 col = texture(color,uv); - - // color range coming in - float inDiff = prop_in_max - prop_in_min; - col = (col-prop_in_min) / inDiff; - - - float outDiff = prop_out_max - prop_out_min; - col.rgb = prop_out_min + col.rgb * vec3(outDiff); - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/normalmap.ts b/src/lib/library/v1/normalmap.ts deleted file mode 100644 index 8f0b3b59..00000000 --- a/src/lib/library/v1/normalmap.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -// http://www.smart-page.net/smartnormal/js/SmartNormalMapFilter.js -export class NormalMapNode extends GpuDesignerNode { - public init() { - this.title = "Normal Map"; - //this.exportName = "result"; - this.addInput("height"); - - this.addFloatProperty("strength", "Strength", 0.02, -0.02, 0.02, 0.00001); - - const source = ` - vec4 process(vec2 uv) - { - //vec2 size = textureSize(height); - //vec2 size = vec2(1024,1024); // quick hack for now - vec2 step = vec2(1.0,1.0)/_textureSize; - - // center point - float d0 = abs(texture(height, uv + vec2(0.0, 0.0)).r) * prop_strength / 2.0; - - // sample horizontally - float d1 = abs(texture(height, uv + vec2(step.x, 0.0)).r) * prop_strength / 2.0; - float d2 = abs(texture(height, uv + vec2(-step.x, 0.0)).r) * prop_strength / 2.0; - - // sample vertically - float d3 = abs(texture(height, uv + vec2(0.0, step.y)).r) * prop_strength / 2.0; - float d4 = abs(texture(height, uv + vec2(0.0, -step.y)).r) * prop_strength / 2.0; - - // find diff horizontally and average - float dx = ((d2 - d0) + (d0 - d1)) * 0.5; - - // find diff vertically and average - float dy = ((d4 - d0) + (d0 - d3)) * 0.5; - - // calculate normal - //vec3 normal = normalize(vec3(dx * prop_strength, dy * prop_strength, 1.0)); - //vec3 normal = normalize(vec3(dx, dy, 1.0)); - //vec3 final = normal.xyz * 0.5 + 0.5; // bring to 0.0 - 1.0 range - - vec3 dvx = vec3(step.x, 0.0 , d1-d0); - vec3 dvy = vec3(0.0 , step.y, d3-d0); - vec3 normal = normalize(cross(dvx, dvy)); - vec3 final = normal.xyz * 0.5 + 0.5; - - return vec4(final, 1.0); - } - `; - - this.buildShader(source); - } -} - -export class BetterWarpNode extends GpuDesignerNode { - public init() { - this.title = "Better Warp Node"; - //this.exportName = "result"; - this.addInput("image"); - this.addInput("height"); - - this.addFloatProperty("strength", "Strength", 0.001, -0.02, 0.02, 0.00001); - this.addFloatProperty("spread", "Spread", 0.1, 0, 1, 0.01); - - const source = ` - vec4 process(vec2 uv) - { - //vec2 size = textureSize(height); - //vec2 size = vec2(1024,1024); // quick hack for now - vec2 step = vec2(1.0,1.0)/_textureSize; - - // center point - float d0 = abs(texture(height, uv + vec2(0.0, 0.0)).r) * prop_strength / 2.0; - - // sample horizontally - float d1 = abs(texture(height, uv + vec2(step.x, 0.0)).r) * prop_strength / 2.0; - float d2 = abs(texture(height, uv + vec2(-step.x, 0.0)).r) * prop_strength / 2.0; - - // sample vertically - float d3 = abs(texture(height, uv + vec2(0.0, step.y)).r) * prop_strength / 2.0; - float d4 = abs(texture(height, uv + vec2(0.0, -step.y)).r) * prop_strength / 2.0; - - // find diff horizontally and average - float dx = ((d2 - d0) + (d0 - d1)) * 0.5; - - // find diff vertically and average - float dy = ((d4 - d0) + (d0 - d3)) * 0.5; - - // calculate normal - //vec3 normal = normalize(vec3(dx * prop_strength, dy * prop_strength, 1.0)); - //vec3 normal = normalize(vec3(dx, dy, 1.0)); - //vec3 final = normal.xyz * 0.5 + 0.5; // bring to 0.0 - 1.0 range - - vec3 dvx = vec3(step.x, 0.0 , d1-d0); - vec3 dvy = vec3(0.0 , step.y, d3-d0); - vec3 normal = normalize(cross(dvx, dvy)); - //vec3 final = normal.xyz * 0.5 + 0.5; - - float intensity = (1.0 - texture(height, uv).r) * prop_spread; - vec3 colorOut = texture(image, uv + vec2(-normal.x * intensity, -normal.y * intensity) ).rgb; - - return vec4(colorOut, 1.0); - //return vec4(vec3(normal.y), 1.0); - //return vec4(vec3(dot(normal, vec3(0,0,1))), 1.0); - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/output.ts b/src/lib/library/v1/output.ts deleted file mode 100644 index 8a630805..00000000 --- a/src/lib/library/v1/output.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; - -export class OutputNode extends GpuDesignerNode { - public init() { - this.title = "Output"; - - this.addInput("image"); - this.addStringProperty("name", "Output Name"); - this.addEnumProperty("components", "Components", [ - "RGBA", - "RGB", - "R", - "G", - "B", - "A" - ]); - - this.addEnumProperty("precision", "Precision", [ - "8 Bits Per Component", - "16 Bits Per Component" - ]); - this.addColorProperty("color", "Default Color", new Color()); - - const source = ` - vec4 process(vec2 uv) - { - vec4 col; - if (image_connected) { - col = vec4(0,1,0,1); - col = texture(image, uv); - // return texture(image, uv); - } else { - col = prop_color; - // return prop_color; - } - - if(prop_components == 2) col.rgb = vec3(col.r); - if(prop_components == 3) col.rgb = vec3(col.g); - if(prop_components == 4) col.rgb = vec3(col.b); - if(prop_components == 5) col.rgb = vec3(col.a); - - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/perlin3d.ts b/src/lib/library/v1/perlin3d.ts deleted file mode 100644 index cb08f4f7..00000000 --- a/src/lib/library/v1/perlin3d.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class Perlin3DNode extends GpuDesignerNode { - public init() { - this.title = "Perlin 3D"; - - const source = ` - vec3 mod289(vec3 x) -{ - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 mod289(vec4 x) -{ - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 permute(vec4 x) -{ - return mod289(((x*34.0)+1.0)*x); -} - -vec4 taylorInvSqrt(vec4 r) -{ - return 1.79284291400159 - 0.85373472095314 * r; -} - -vec3 fade(vec3 t) { - return t*t*t*(t*(t*6.0-15.0)+10.0); -} - -// Classic Perlin noise -float cnoise(vec3 P) -{ - vec3 Pi0 = floor(P); // Integer part for indexing - vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 - Pi0 = mod289(Pi0); - Pi1 = mod289(Pi1); - vec3 Pf0 = fract(P); // Fractional part for interpolation - vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 - vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); - vec4 iy = vec4(Pi0.yy, Pi1.yy); - vec4 iz0 = Pi0.zzzz; - vec4 iz1 = Pi1.zzzz; - - vec4 ixy = permute(permute(ix) + iy); - vec4 ixy0 = permute(ixy + iz0); - vec4 ixy1 = permute(ixy + iz1); - - vec4 gx0 = ixy0 * (1.0 / 7.0); - vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; - gx0 = fract(gx0); - vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); - vec4 sz0 = step(gz0, vec4(0.0)); - gx0 -= sz0 * (step(0.0, gx0) - 0.5); - gy0 -= sz0 * (step(0.0, gy0) - 0.5); - - vec4 gx1 = ixy1 * (1.0 / 7.0); - vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; - gx1 = fract(gx1); - vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); - vec4 sz1 = step(gz1, vec4(0.0)); - gx1 -= sz1 * (step(0.0, gx1) - 0.5); - gy1 -= sz1 * (step(0.0, gy1) - 0.5); - - vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); - vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); - vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); - vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); - vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); - vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); - vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); - vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); - - vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); - g000 *= norm0.x; - g010 *= norm0.y; - g100 *= norm0.z; - g110 *= norm0.w; - vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); - g001 *= norm1.x; - g011 *= norm1.y; - g101 *= norm1.z; - g111 *= norm1.w; - - float n000 = dot(g000, Pf0); - float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); - float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); - float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); - float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); - float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); - float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); - float n111 = dot(g111, Pf1); - - vec3 fade_xyz = fade(Pf0); - vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); - vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); - float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); - return 2.2 * n_xyz; -} - -// Classic Perlin noise, periodic variant -float pnoise(vec3 P, vec3 rep) -{ - vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period - vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period - Pi0 = mod289(Pi0); - Pi1 = mod289(Pi1); - vec3 Pf0 = fract(P); // Fractional part for interpolation - vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 - vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); - vec4 iy = vec4(Pi0.yy, Pi1.yy); - vec4 iz0 = Pi0.zzzz; - vec4 iz1 = Pi1.zzzz; - - vec4 ixy = permute(permute(ix) + iy); - vec4 ixy0 = permute(ixy + iz0); - vec4 ixy1 = permute(ixy + iz1); - - vec4 gx0 = ixy0 * (1.0 / 7.0); - vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; - gx0 = fract(gx0); - vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); - vec4 sz0 = step(gz0, vec4(0.0)); - gx0 -= sz0 * (step(0.0, gx0) - 0.5); - gy0 -= sz0 * (step(0.0, gy0) - 0.5); - - vec4 gx1 = ixy1 * (1.0 / 7.0); - vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; - gx1 = fract(gx1); - vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); - vec4 sz1 = step(gz1, vec4(0.0)); - gx1 -= sz1 * (step(0.0, gx1) - 0.5); - gy1 -= sz1 * (step(0.0, gy1) - 0.5); - - vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); - vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); - vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); - vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); - vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); - vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); - vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); - vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); - - vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); - g000 *= norm0.x; - g010 *= norm0.y; - g100 *= norm0.z; - g110 *= norm0.w; - vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); - g001 *= norm1.x; - g011 *= norm1.y; - g101 *= norm1.z; - g111 *= norm1.w; - - float n000 = dot(g000, Pf0); - float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); - float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); - float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); - float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); - float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); - float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); - float n111 = dot(g111, Pf1); - - vec3 fade_xyz = fade(Pf0); - vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); - vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); - float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); - return 2.2 * n_xyz; -} - -vec4 process(vec2 uv) -{ - float c = pnoise(vec3(uv * 10.0,10.0), vec3(10.0,10.0,10.0)); - - return vec4(vec3(c), 1.0); -} - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/threshold.ts b/src/lib/library/v1/threshold.ts deleted file mode 100644 index 7df1af5d..00000000 --- a/src/lib/library/v1/threshold.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class ThresholdNode extends GpuDesignerNode { - public init() { - this.title = "Threshold"; - - this.addInput("image"); - - this.addFloatProperty("threshold", "Threshold", 0.0, 0.0, 1.0, 0.01); - this.addBoolProperty("invert", "Invert", true); - - const source = ` - vec4 process(vec2 uv) - { - vec4 a = texture(image, uv); - - if (prop_invert) - a.rgb = step(1.0 - prop_threshold, a.rgb); - else - a.rgb = step(prop_threshold, a.rgb); - - return a; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/warp.ts b/src/lib/library/v1/warp.ts deleted file mode 100644 index ad97002d..00000000 --- a/src/lib/library/v1/warp.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class WarpNode extends GpuDesignerNode { - public init() { - this.title = "Warp"; - - this.addInput("inputImage"); - this.addInput("height"); - - this.addFloatProperty("intensity", "Intensity", 0.1, -1.0, 1.0, 0.01); - - // calculates normal, then warps uv by it - const source = ` - vec4 process(vec2 uv) - { - vec2 step = vec2(1.0,1.0)/_textureSize; - vec4 warpCol = texture(height, uv); - float warp = (warpCol.r + warpCol.g + warpCol.b) / 3.0; - - vec4 color = texture(inputImage, uv + (vec2(warp) - 0.5) * vec2(1.0, -1.0) * prop_intensity); - - return color; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v1/wave.ts b/src/lib/library/v1/wave.ts deleted file mode 100644 index 4e3f00a0..00000000 --- a/src/lib/library/v1/wave.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class WaveNode extends GpuDesignerNode { - public init() { - this.title = "Wave"; - - this.addIntProperty("xfrequency", "X Frequency", 1, 0, 20, 1); - this.addIntProperty("yfrequency", "Y Frequency", 0, 0, 20, 1); - this.addFloatProperty("phase", "Phase Offset", 0.0, 0.0, 3.142, 0.01); - this.addFloatProperty("amp", "Amplitude", 0.5, 0.0, 1.0, 0.01); - - // calculates normal, then warps uv by it - const source = ` - vec4 process(vec2 uv) - { - float fx = uv.x * 3.142 * 2.0 * float(prop_xfrequency); - float fy = uv.y * 3.142 * 2.0 * float(prop_yfrequency); - float wave = sin(fx + fy + prop_phase) * prop_amp; - - // bring wave to range 0...1 - wave = wave * 0.5 + 0.5; - - return vec4(vec3(wave), 1.0); - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/bevel.ts b/src/lib/library/v2/bevel.ts deleted file mode 100644 index 22ee3ae9..00000000 --- a/src/lib/library/v2/bevel.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { NodeRenderContext } from "@/lib/designer"; -import { TextureDataType } from "@/lib/designer/gl"; -import { FloatProperty } from "@/lib/designer/properties"; -import { TextureDataConverter } from "@/lib/designer/texturedataconverter"; -import { DesignerNode } from "../../designer/designernode"; - -// https://xjavascript.com/view/639466/read-pixels-in-webgltexture-rendering-webgl-to-texture -export class Bevel extends DesignerNode { - f: Float64Array; - z: Float64Array; - v: Uint16Array; - grid: Float64Array; - gridInner: Float64Array; - gridOuter: Float64Array; - - // working pixels - resultPixels: Float32Array; - readPixels: Float32Array; - - readFbo: WebGLFramebuffer; - - distanceProp: FloatProperty; - - width: number; - height: number; - - converter: TextureDataConverter; - - public init() { - this.title = "Bevel"; - - this.addInput("image"); - - this.distanceProp = this.addFloatProperty( - "distance", - "Distance", - 50.0, - 0.0, - 100.0, - 0.01 - ); - - this.initArrays(); - - // create framebuffer for reading pixels from input texture - this.readFbo = this.gl.createFramebuffer(); - this.converter = new TextureDataConverter( - this.gl as WebGL2RenderingContext - ); - } - - private initArrays() { - const width = this.designer.width; - const height = this.designer.height; - this.width = width; - this.height = height; - - let size = Math.max(width, height); - - // make it span 3 textures wide to get wrapping - this.f = new Float64Array(size * 3); - this.z = new Float64Array(size * 3 + 1); - this.v = new Uint16Array(size * 3); - - // convert image to float64 array - let gridSize = width * height; - this.grid = new Float64Array(gridSize); - this.gridOuter = new Float64Array(gridSize); - this.gridInner = new Float64Array(gridSize); - this.resultPixels = new Float32Array(width * height * 4); - this.readPixels = new Float32Array(width * height * 4); - } - - public render(context: NodeRenderContext) { - const inputs = context.inputs; - - if (inputs.length == 0) return; - - let inputTexture = inputs[0].node.tex; - - if ( - this.designer.width != this.width || - this.designer.height != this.height - ) { - // this.width = this.designer.width; - // this.height = this.designer.height; - - this.initArrays(); - } - - const width = this.designer.width; - const height = this.designer.height; - const gridOuter = this.gridOuter; - const gridInner = this.gridInner; - const grid = this.grid; - - let gridSize = width * height; - let gl = context.gl as WebGL2RenderingContext; - - // gl.bindFramebuffer(gl.FRAMEBUFFER, this.readFbo); - // gl.framebufferTexture2D( - // gl.FRAMEBUFFER, - // gl.COLOR_ATTACHMENT0, - // gl.TEXTURE_2D, - // inputTexture, - // 0 - // ); - // if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE) { - // gl.readPixels( - // 0, - // 0, - // width, - // height, - // gl.RGBA, - // gl.HALF_FLOAT, - // this.readPixels - // ); - // } else { - // alert("Bevel: unable to read from FBO"); - // } - // gl.bindFramebuffer(gl.FRAMEBUFFER, null); - - this.readPixels = new Float32Array( - this.converter.getData( - inputTexture, - width, - height, - TextureDataType.Float32 - ) - ); - - // max value from each color component - const VALUE_MAX = 15360.0; - let pixels = this.readPixels; - - // copy data over from canvas data to grid - for (let i = 0; i < gridSize; i++) { - // let col = 0; - // col += VALUE_MAX - pixels[i * 4 + 0]; - // col += VALUE_MAX - pixels[i * 4 + 1]; - // col += VALUE_MAX - pixels[i * 4 + 2]; - // let a = (col * 0.3333) / VALUE_MAX; - - // just use the red component - let a = pixels[i * 4 + 0]; - - //let a = 1.0 - (col * 0.3333) / 255; // invert - this.gridOuter[i] = - a === 1 ? 0 : a === 0 ? INF : Math.pow(Math.max(0, 0.5 - a), 2); - this.gridInner[i] = - a === 1 ? INF : a === 0 ? 0 : Math.pow(Math.max(0, a - 0.5), 2); - } - - edt(this.gridOuter, width, height, this.f, this.v, this.z); - edt(this.gridInner, width, height, this.f, this.v, this.z); - - let min = 1.0; - let max = 0; - let radius = this.distanceProp.getValue(); - let offset = 0.25; - - for (let i = 0; i < gridSize; i++) { - // let col = grid[i]; - let d = Math.sqrt(gridOuter[i]) - Math.sqrt(gridInner[i]); - //var col = 255 - 255 * (d / 50 + 0.25); - var col = VALUE_MAX - VALUE_MAX * (d / radius + offset); - // clamp - col = Math.max(0, Math.min(VALUE_MAX, col)); - - min = Math.min(min, col); - max = Math.max(max, col); - grid[i] = col; - } - - let range = max - min; - let scale = 1.0 / range; - // console.log(min, max, range, scale); - - let resultPixels = this.resultPixels; - for (let i = 0; i < gridSize; i++) { - //let col = (grid[i] - min) * scale; - let col = 1.0 - (grid[i] - min) * scale; // de-invert - - resultPixels[i * 4 + 0] = col; - resultPixels[i * 4 + 1] = col; - resultPixels[i * 4 + 2] = col; - resultPixels[i * 4 + 3] = 1.0; - } - - gl.bindTexture(gl.TEXTURE_2D, this.tex); - - const level = 0; - const internalFormat = gl.RGBA16F; - const border = 0; - const format = gl.RGBA; - const type = gl.FLOAT; - const data = resultPixels; - gl.texImage2D( - gl.TEXTURE_2D, - level, - internalFormat, - this.designer.width, - this.designer.height, - border, - format, - type, - data - ); - - // set the filtering so we don't need mips - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - - gl.bindTexture(gl.TEXTURE_2D, null); - } -} - -// todo: find better algorithm -// https://github.com/parmanoir/Meijster-distance -// https://github.com/scijs/distance-transform - -// https://github.com/mapbox/tiny-sdf/blob/master/index.js#L60 -// https://observablehq.com/@mourner/fast-distance-transform - -var INF = 1e20; - -// 2D Euclidean squared distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/papers/dt-final.pdf -function edt( - data: Float64Array, - width: number, - height: number, - f: Float64Array, - v: Uint16Array, - z: Float64Array -) { - for (let x = 0; x < width; x++) edt1d(data, x, width, height, f, v, z); - for (let y = 0; y < height; y++) edt1d(data, y * width, 1, width, f, v, z); -} - -// 1D squared distance transform -function edt1d( - grid: Float64Array, - offset: number, - stride: number, - length: number, - f: Float64Array, - v: Uint16Array, - z: Float64Array -) { - let q, k, s, r; - v[0] = 0; - z[0] = -INF; - z[1] = INF; - - // load line in array for convenient access - // do it three times - for (q = 0; q < length; q++) f[q] = grid[offset + q * stride]; - for (q = 0; q < length; q++) f[q + length] = grid[offset + q * stride]; - for (q = 0; q < length; q++) - f[q + length + length] = grid[offset + q * stride]; - - for (q = 1, k = 0, s = 0; q < length * 3; q++) { - do { - r = v[k]; - s = (f[q] - f[r] + q * q - r * r) / (q - r) / 2; - // todo: wrap k in z[k] and make --k > -length - } while (s <= z[k] && --k > -1); - - k++; - v[k] = q; - z[k] = s; - z[k + 1] = INF; - } - - // only cope over mid section - // |----|----|----| - // ^ - for (q = length, k = 0; q < length + length; q++) { - while (z[k + 1] < q) k++; - r = v[k]; - grid[offset + (q - length) * stride] = f[r] + (q - r) * (q - r); - } -} diff --git a/src/lib/library/v2/cartesiantopolar.ts b/src/lib/library/v2/cartesiantopolar.ts deleted file mode 100644 index cb7fcef1..00000000 --- a/src/lib/library/v2/cartesiantopolar.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; - -// https://www.mathsisfun.com/polar-cartesian-coordinates.html -export class CartesianToPolar extends GpuDesignerNode { - public init() { - this.title = "Cartesian To Polar"; - - this.addInput("image"); - - this.addFloatProperty("yscale", "Y Scale", 1.0, 0, 4.0, 0.01); - this.addIntProperty("xtile", "X Tile", 2, 0, 5, 1); - this.addFloatProperty("angle_offset", "Angle Offset", 0.0, 0, 360, 1.0); - // this.addBoolProperty("clamp", "Clamp", false); - - const source = ` - const float PI = 3.142; - vec4 process(vec2 uv) - { - vec2 dir = uv - vec2(0.5, 0.5); - float y = length(dir) * 2.0 * prop_yscale; - - float x = atan(dir.y, dir.x); - x = (x + PI) / (PI * 2.0); // bring to range 0..1 - x += (prop_angle_offset / 360.0); - x = x * float(prop_xtile); - - vec4 col = texture(image, vec2(x, y)); - - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/clamp.ts b/src/lib/library/v2/clamp.ts deleted file mode 100644 index 66ac6b80..00000000 --- a/src/lib/library/v2/clamp.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; - -export class Clamp extends GpuDesignerNode { - public init() { - this.title = "Clamp"; - - this.addInput("inputImage"); - - this.addFloatProperty("min", "Minimum", 0, 0, 1.0, 0.01); - this.addFloatProperty("max", "Maximum", 1, 0, 1.0, 0.01); - - this.addBoolProperty("clamp_r", "Clamp R Chanel", true); - this.addBoolProperty("clamp_g", "Clamp G Chanel", true); - this.addBoolProperty("clamp_b", "Clamp B Chanel", true); - this.addBoolProperty("clamp_a", "Clamp A Chanel", false); - - const source = ` - - vec4 process(vec2 uv) - { - // grayscale input color - vec4 col = texture(inputImage,uv); - - if (prop_clamp_r) col.r = clamp(col.r, prop_min, prop_max); - if (prop_clamp_g) col.g = clamp(col.g, prop_min, prop_max); - if (prop_clamp_b) col.b = clamp(col.b, prop_min, prop_max); - if (prop_clamp_a) col.a = clamp(col.a, prop_min, prop_max); - - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/directionalwarpv2.ts b/src/lib/library/v2/directionalwarpv2.ts deleted file mode 100644 index c41a68ce..00000000 --- a/src/lib/library/v2/directionalwarpv2.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class DirectionalWarpNodeV2 extends GpuDesignerNode { - public init() { - this.title = "Directional Warp"; - - this.addInput("inputImage"); - this.addInput("height"); - - this.addFloatProperty("intensity", "Intensity", 0.1, -0.5, 0.5, 0.01); - this.addFloatProperty("angle", "Angle", 0, 0, 360, 1); - - // calculates normal, then warps uv by it - const source = ` - vec4 process(vec2 uv) - { - vec2 step = vec2(1.0,1.0)/_textureSize; - - vec2 dir = normalize(vec2(cos(radians(prop_angle)), sin(radians(prop_angle)))); - - // center point - float dist = abs(texture(height, uv).r) - 0.5; - - vec4 color = texture(inputImage, uv + dir * prop_intensity * dist); - - return color; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/floodfill.ts b/src/lib/library/v2/floodfill.ts deleted file mode 100644 index a6de3485..00000000 --- a/src/lib/library/v2/floodfill.ts +++ /dev/null @@ -1,395 +0,0 @@ -import { NodeRenderContext } from "@/lib/designer"; -import { FloatProperty } from "@/lib/designer/properties"; -import { DesignerNode } from "../../designer/designernode"; - -const VALUE_MAX = 15360.0; -const ONE_OVER_VALUE_MAX = 1.0 / VALUE_MAX; - -// https://xjavascript.com/view/639466/read-pixels-in-webgltexture-rendering-webgl-to-texture -// NOTE: THIS CREATES A HALF FLOAT TEXTURE, NOT UNSIGNED BYTE -// more accuracy is needed for the output -export class FloodFill extends DesignerNode { - // working pixels - pixels: Uint16Array; - readFbo: WebGLFramebuffer; - gen: FloodFillGenerator; - - // stores the size to detect resolution change - width: number; - height: number; - - public init() { - this.title = "Flood Fill"; - - this.addInput("image"); - - const width = this.designer.width; - const height = this.designer.height; - this.width = width; - this.height = height; - - this.pixels = new Uint16Array(width * height * 4); - - // create framebuffer for reading pixels from input texture - this.readFbo = this.gl.createFramebuffer(); - - this.gen = new FloodFillGenerator(); - this.gen.init(width, height); - } - - public render(context: NodeRenderContext) { - const inputs = context.inputs; - - if (inputs.length == 0) return; - - let inputTexture = inputs[0].node.tex; - - if ( - this.designer.width != this.width || - this.designer.height != this.height - ) { - this.width = this.designer.width; - this.height = this.designer.height; - - this.pixels = new Uint16Array(this.width * this.height * 4); - - this.gen = new FloodFillGenerator(); - this.gen.init(this.width, this.height); - } - - const width = this.designer.width; - const height = this.designer.height; - - let gridSize = width * height; - let gl = context.gl as WebGL2RenderingContext; - var pixels = this.pixels; - - gl.bindFramebuffer(gl.FRAMEBUFFER, this.readFbo); - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - inputTexture, - 0 - ); - if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) == gl.FRAMEBUFFER_COMPLETE) { - gl.readPixels(0, 0, width, height, gl.RGBA, gl.HALF_FLOAT, pixels); - } else { - alert("Bevel: unable to read from FBO"); - } - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - - this.gen.process(pixels); - - gl.bindTexture(gl.TEXTURE_2D, this.tex); - - // https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glTexImage2D.xhtml - const level = 0; - const internalFormat = gl.RGBA16F; - const border = 0; - const format = gl.RGBA; - const type = gl.FLOAT; - // const data = pixels; - gl.texImage2D( - gl.TEXTURE_2D, - level, - internalFormat, - this.designer.width, - this.designer.height, - border, - format, - type, - this.gen.results - ); - - // set the filtering so we don't need mips - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - - gl.bindTexture(gl.TEXTURE_2D, null); - - // console.log(this.gen.results); - } -} - -class Vector { - x: number = 0; - y: number = 0; - - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } -} - -class FloodFillPixel { - localX: number = 0; - localY: number = 0; - - globalX: number = 0; - globalY: number = 0; - - constructor( - localX: number, - localY: number, - globalX: number, - globalY: number - ) { - this.localX = localX; - this.localY = localY; - - this.globalX = globalX; - this.globalY = globalY; - } -} - -class Box { - left: number = 0; - top: number = 0; - right: number = 0; - bottom: number = 0; - - // coordinates of captured pixels - pixels: FloodFillPixel[] = []; - - public expand(x: number, y: number) { - //console.log(this); - this.left = Math.min(this.left, x); - this.top = Math.min(this.top, y); - - this.right = Math.max(this.right, x); - this.bottom = Math.max(this.bottom, y); - - //console.log(x + " , " + y); - } - - public negativeInfinity() { - this.left = Number.POSITIVE_INFINITY; - this.top = Number.POSITIVE_INFINITY; - - this.right = Number.NEGATIVE_INFINITY; - this.bottom = Number.NEGATIVE_INFINITY; - } - - get width() { - return this.right - this.left; - } - - get height() { - return this.bottom - this.top; - } - - get x() { - return this.left; - } - - get y() { - return this.top; - } -} - -class FloodFillGenerator { - rects: Box[] = []; - - visited: Array = []; - width: number; - height: number; - results: Float32Array; - - constructor() {} - - init(width: number, height: number) { - this.width = width; - this.height = height; - - let visited = this.visited; - visited.length = width * height; - - this.results = new Float32Array(width * height * 4); - } - - process(pixels: Uint16Array) { - let threshold = 0.1; - - // allocate image of same size to track visited pixels - let visited = this.visited; - for (let i = 0; i < visited.length; i++) visited[i] = false; - - let width = this.width; - let height = this.height; - - this.rects = []; - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - if (visited[y * width + x] == true) continue; - - this.captureIsland(x, y, pixels, width, height, visited); - } - } - - this.renderOutput(this.results); - } - - captureIsland( - x: number, - y: number, - pixels: Uint16Array, - width: number, - height: number, - visited: Array - ) { - let rect: Box = new Box(); - rect.negativeInfinity(); - - // this queue contains coords in a global space - // i.e. values can be negative or larger than the - // texture size - let queue: Vector[] = [new Vector(x, y)]; - while (queue.length > 0) { - let globalPixel = queue.shift(); - // should never happen - if (!globalPixel) continue; - - let pixel = this.mapPixelToLocal(globalPixel, width, height); - - // skip if already visited - if (visited[pixel.y * width + pixel.x] == true) continue; - - visited[pixel.y * width + pixel.x] = true; - - // 0.1 or less denotes a dark pixel - // skip this - let intensity = this.getIntensity( - pixel.x, - pixel.y, - pixels, - width, - height - ); - if (intensity < 0.1) { - continue; - } - - // ok it's a white enough pixel, add it's neighbors to the queue - // and expand the rect - rect.expand(globalPixel.x, globalPixel.y); - rect.pixels.push( - new FloodFillPixel(pixel.x, pixel.y, globalPixel.x, globalPixel.y) - ); - - queue.push(new Vector(globalPixel.x + 1, globalPixel.y + 0)); - queue.push(new Vector(globalPixel.x - 1, globalPixel.y + 0)); - queue.push(new Vector(globalPixel.x + 0, globalPixel.y + 1)); - queue.push(new Vector(globalPixel.x + 0, globalPixel.y - 1)); - } - - if (rect.width <= 0 || rect.height <= 0) return; - - this.rects.push(rect); - } - - mapPixelToLocal(globalPixel: Vector, width: number, height: number): Vector { - return new Vector( - wrapAround(globalPixel.x, width), - wrapAround(globalPixel.y, height) - ); - } - - getIntensity( - x: number, - y: number, - data: Uint16Array, - width: number, - height: number - ): number { - let col = 0; - col += data[4 * (width * y + x) + 0]; - col += data[4 * (width * y + x) + 1]; - col += data[4 * (width * y + x) + 2]; - - // average map to 0.0 - 1.0 - let intensity = col * (1.0 / 3.0) * ONE_OVER_VALUE_MAX; - - return intensity; - } - - renderRects(canvas: HTMLCanvasElement) { - let ctx = canvas.getContext("2d"); - if (!ctx) return; - - ctx.strokeStyle = "red"; - ctx.lineWidth = 1; - - for (let rect of this.rects) { - ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); - } - } - - renderOutput(results: Float32Array) { - let width = this.width; - let height = this.height; - - // clear results first - for (let i = 0; i < results.length; i++) results[i] = 0; - - for (let rect of this.rects) { - let sx = rect.width * (1.0 / width); // * 255; - let sy = rect.height * (1.0 / height); // * 255; - - for (let pixel of rect.pixels) { - let u = (pixel.globalX - rect.left) / rect.width; // * 255; - let v = (pixel.globalY - rect.top) / rect.height; // * 255; - - setColorAtPixel( - results, - width, - height, - pixel.localX, - pixel.localY, - u, - v, - sx, - sy - ); - //setColorAtPixel(data, pixel.x, pixel.y, 255, 0, 0, 255); - } - } - } -} - -// https://stackoverflow.com/questions/14325750/using-mod-to-wrap-around -// upperBound is never touchec -function wrapAround(value: number, upperBound: number) { - return (value + upperBound - 1) % upperBound; -} - -// https://codepen.io/Geeyoam/pen/vLGZzG -function getColorAtPixel(imageData: ImageData, x: number, y: number) { - const { width, data } = imageData; - - return { - r: data[4 * (width * y + x) + 0], - g: data[4 * (width * y + x) + 1], - b: data[4 * (width * y + x) + 2], - a: data[4 * (width * y + x) + 3] - }; -} - -function setColorAtPixel( - data: Float32Array, - width: number, - height: number, - x: number, - y: number, - r: number, - g: number, - b: number, - a: number -) { - data[4 * (width * y + x) + 0] = r; - data[4 * (width * y + x) + 1] = g; - data[4 * (width * y + x) + 2] = b; - data[4 * (width * y + x) + 3] = a; -} diff --git a/src/lib/library/v2/gradientdynamic.ts b/src/lib/library/v2/gradientdynamic.ts deleted file mode 100644 index e28a51b5..00000000 --- a/src/lib/library/v2/gradientdynamic.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class GradientDynamic extends GpuDesignerNode { - public init() { - this.title = "Gradient Dynamic"; - - this.addInput("image"); - this.addInput("gradient"); - - this.addFloatProperty("position", "Position", 0, 0, 1, 0.01); - - this.addEnumProperty("orientation", "Gradient Direction", [ - "Vertical", - "Horizontal" - ]); - - const source = ` - vec4 process(vec2 uv) - { - float t = 0.0; - - float intensity = texture(image, uv).r; - - vec4 col = vec4(0); - if (prop_orientation == 0) - col = texture(gradient, vec2(prop_position, intensity)); - else - col = texture(gradient, vec2(intensity, prop_position)); - - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/grayscale.ts b/src/lib/library/v2/grayscale.ts deleted file mode 100644 index ff269822..00000000 --- a/src/lib/library/v2/grayscale.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; - -export class Grayscale extends GpuDesignerNode { - public init() { - this.title = "Grayscale"; - - this.addInput("image"); - - const source = ` - vec4 process(vec2 uv) - { - // grayscale input color - vec4 col = texture(image, uv); - - col.rgb = vec3((col.r + col.g + col.b) * 0.3333333); - - return col; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/histogramshift.ts b/src/lib/library/v2/histogramshift.ts deleted file mode 100644 index 83f4101a..00000000 --- a/src/lib/library/v2/histogramshift.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class HistogramShift extends GpuDesignerNode { - public init() { - this.title = "Histogram Shift"; - - this.addInput("image"); - - this.addFloatProperty("position", "Position", 0.0, 0.0, 1.0, 0.01); - - const source = ` - vec4 process(vec2 uv) - { - vec4 a = texture(image, uv); - - // shows artifacts for pixels at 1 when position is at 0 - a.rgb = mod(a.rgb + vec3(prop_position), vec3(1.0)); - - return a; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/imagenode.ts b/src/lib/library/v2/imagenode.ts deleted file mode 100644 index 47506e4a..00000000 --- a/src/lib/library/v2/imagenode.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Image } from "@/lib/designer/image"; -import { DesignerNode } from "@/lib/designer/designernode"; -import { NodeRenderContext } from "@/lib/designer"; -import { ImageProperty } from "@/lib/designer/properties"; - -export class ImageNode extends DesignerNode { - canvas: HTMLCanvasElement; - imageProp: ImageProperty; - public init() { - this.title = "Image"; - - this.canvas = document.createElement("canvas") as HTMLCanvasElement; - this.canvas.width = this.designer.width; - this.canvas.height = this.designer.height; - - let ctx = this.canvas.getContext("2d"); - ctx.fillStyle = "rgb(0,255,0)"; - ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - - this.imageProp = this.addImageProperty("image", "Image", Image.empty()); - } - - public render(context: NodeRenderContext) { - let gl = context.gl as WebGL2RenderingContext; - - this.canvas.width = this.designer.width; - this.canvas.height = this.designer.height; - console.log(this.designer.width); - console.log(this.designer.height); - - let ctx = this.canvas.getContext("2d"); - - // ctx.fillStyle = "rgb(0,255,0)"; - // ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - - ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - if (this.imageProp.value && !this.imageProp.value.isEmpty) { - ctx.drawImage( - this.imageProp.value.canvas, - 0, - 0, - this.canvas.width, - this.canvas.height - ); - console.log("drawing image"); - } - - gl.bindTexture(gl.TEXTURE_2D, this.tex); - - //todo: this throws an error, manually convert to float texture - const level = 0; - const internalFormat = gl.RGBA16F; - const border = 0; - const format = gl.RGBA; - const type = gl.UNSIGNED_BYTE; - const data = this.canvas; - gl.texImage2D( - gl.TEXTURE_2D, - level, - internalFormat, - this.designer.width, - this.designer.height, - border, - format, - type, - data - ); - - // set the filtering so we don't need mips - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - - gl.bindTexture(gl.TEXTURE_2D, null); - } -} diff --git a/src/lib/library/v2/invertnormal.ts b/src/lib/library/v2/invertnormal.ts deleted file mode 100644 index f9bae0dc..00000000 --- a/src/lib/library/v2/invertnormal.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; - -export class InvertNormal extends GpuDesignerNode { - public init() { - this.title = "Invert Normal"; - - this.addInput("inputImage"); - - this.addBoolProperty("invertRed", "Invert Red", false); - this.addBoolProperty("invertGreen", "Invert Green", true); - this.addBoolProperty("invertBlue", "Invert Blue", false); - this.addBoolProperty("invertAlpha", "Invert Alpha", false); - - const source = ` - - vec4 process(vec2 uv) - { - // grayscale input color - vec4 result = texture(inputImage,uv); - - if(prop_invertRed) result.r = 1.0 - result.r; - if(prop_invertGreen) result.g = 1.0 - result.g; - if(prop_invertBlue) result.b = 1.0 - result.b; - if(prop_invertAlpha) result.a = 1.0 - result.a; - - return result; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/normalmapv2.ts b/src/lib/library/v2/normalmapv2.ts deleted file mode 100644 index 2f32a7b5..00000000 --- a/src/lib/library/v2/normalmapv2.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -// http://www.smart-page.net/smartnormal/js/SmartNormalMapFilter.js -export class NormalMapV2 extends GpuDesignerNode { - public init() { - this.title = "Normal Map"; - //this.exportName = "result"; - this.addInput("height"); - - // this.addFloatProperty("strength", "Strength", 0.02, -0.02, 0.02, 0.00001); - this.addFloatProperty("strength", "Strength", 1, -2.0, 2.0, 0.05); - this.addIntProperty("range", "Range", 1, 1, 20, 1); - this.addBoolProperty("res_ind", "Resolution Independent", false); - - const source = ` - vec4 process(vec2 uv) - { - vec2 step = (vec2(1.0, 1.0) / _textureSize) * float(prop_range); - if (prop_res_ind) - step = (vec2(1.0, 1.0) / 1024.0) * float(prop_range); - float strength = prop_strength * 0.1; - - // center point - float d0 = abs(texture(height, uv + vec2(0.0, 0.0)).r) * strength / 2.0; - - // sample horizontally - float d1 = abs(texture(height, uv + vec2(step.x, 0.0)).r) * strength / 2.0; - float d2 = abs(texture(height, uv + vec2(-step.x, 0.0)).r) * strength / 2.0; - - // sample vertically - float d3 = abs(texture(height, uv + vec2(0.0, step.y)).r) * strength / 2.0; - float d4 = abs(texture(height, uv + vec2(0.0, -step.y)).r) * strength / 2.0; - - // find diff horizontally and average - float dx = ((d2 - d0) + (d0 - d1)) * 0.5; - - // find diff vertically and average - float dy = ((d4 - d0) + (d0 - d3)) * 0.5; - - vec3 dvx = vec3(step.x, 0.0 , d1-d0); - vec3 dvy = vec3(0.0 , step.y, d3-d0); - vec3 normal = normalize(cross(dvx, dvy)); - vec3 final = normal.xyz * 0.5 + 0.5; - - return vec4(final, 1.0); - } - `; - - this.buildShader(source); - } -} - -export class BetterWarpNode extends GpuDesignerNode { - public init() { - this.title = "Better Warp Node"; - //this.exportName = "result"; - this.addInput("image"); - this.addInput("height"); - - this.addFloatProperty("strength", "Strength", 0.001, -0.02, 0.02, 0.00001); - this.addFloatProperty("spread", "Spread", 0.1, 0, 1, 0.01); - - const source = ` - vec4 process(vec2 uv) - { - //vec2 size = textureSize(height); - //vec2 size = vec2(1024,1024); // quick hack for now - vec2 step = vec2(1.0,1.0)/_textureSize; - - // center point - float d0 = abs(texture(height, uv + vec2(0.0, 0.0)).r) * prop_strength / 2.0; - - // sample horizontally - float d1 = abs(texture(height, uv + vec2(step.x, 0.0)).r) * prop_strength / 2.0; - float d2 = abs(texture(height, uv + vec2(-step.x, 0.0)).r) * prop_strength / 2.0; - - // sample vertically - float d3 = abs(texture(height, uv + vec2(0.0, step.y)).r) * prop_strength / 2.0; - float d4 = abs(texture(height, uv + vec2(0.0, -step.y)).r) * prop_strength / 2.0; - - // find diff horizontally and average - float dx = ((d2 - d0) + (d0 - d1)) * 0.5; - - // find diff vertically and average - float dy = ((d4 - d0) + (d0 - d3)) * 0.5; - - // calculate normal - //vec3 normal = normalize(vec3(dx * prop_strength, dy * prop_strength, 1.0)); - //vec3 normal = normalize(vec3(dx, dy, 1.0)); - //vec3 final = normal.xyz * 0.5 + 0.5; // bring to 0.0 - 1.0 range - - vec3 dvx = vec3(step.x, 0.0 , d1-d0); - vec3 dvy = vec3(0.0 , step.y, d3-d0); - vec3 normal = normalize(cross(dvx, dvy)); - //vec3 final = normal.xyz * 0.5 + 0.5; - - float intensity = (1.0 - texture(height, uv).r) * prop_spread; - vec3 colorOut = texture(image, uv + vec2(-normal.x * intensity, -normal.y * intensity) ).rgb; - - return vec4(colorOut, 1.0); - //return vec4(vec3(normal.y), 1.0); - //return vec4(vec3(dot(normal, vec3(0,0,1))), 1.0); - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/pow.ts b/src/lib/library/v2/pow.ts deleted file mode 100644 index c262792b..00000000 --- a/src/lib/library/v2/pow.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class Pow extends GpuDesignerNode { - public init() { - this.title = "Pow"; - - this.addInput("image"); - - this.addFloatProperty("exponent", "Exponent", 1.0, 0.0, 10.0, 0.01); - - const source = ` - vec4 process(vec2 uv) - { - vec4 a = texture(image, uv); - a.rgb = pow(a.rgb, vec3(prop_exponent)); - - return a; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/quantize.ts b/src/lib/library/v2/quantize.ts deleted file mode 100644 index dfc1ae88..00000000 --- a/src/lib/library/v2/quantize.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class Quantize extends GpuDesignerNode { - public init() { - this.title = "Quantize"; - - this.addInput("image"); - - this.addIntProperty("steps", "Steps", 12, 2, 20, 1); - - const source = ` - vec4 process(vec2 uv) - { - vec4 a = texture(image, uv); - a.rgb = floor(a.rgb * vec3(float(prop_steps))) / vec3(float(prop_steps)); - - return a; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/rgbamerge.ts b/src/lib/library/v2/rgbamerge.ts deleted file mode 100644 index 9b6ffca9..00000000 --- a/src/lib/library/v2/rgbamerge.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; - -export class RgbaMerge extends GpuDesignerNode { - public init() { - this.title = "RGBA Merge"; - - this.addInput("redSource"); - this.addInput("greenSource"); - this.addInput("blueSource"); - this.addInput("alphaSource"); - - // Red - let prop = this.addEnumProperty("redSource", "Red Source", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); - prop.setValue(0); - - // Green - prop = this.addEnumProperty("greenSource", "Green Source", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); - prop.setValue(1); - - // Blue - prop = this.addEnumProperty("blueSource", "Blue Source", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); - prop.setValue(2); - - // Alpha - prop = this.addEnumProperty("alphaSource", "Alpha Source", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); - prop.setValue(3); - - const source = ` - - float getChannel(vec4 inputData, int mode) - { - if (mode == 0) return inputData.r; - if (mode == 1) return inputData.g; - if (mode == 2) return inputData.b; - if (mode == 3) return inputData.a; - if (mode == 4) { - return (inputData.r + inputData.g + inputData.b) * 0.3333333; - } - - return 0.0; - } - - vec4 process(vec2 uv) - { - // grayscale input color - vec4 r = texture(redSource,uv); - vec4 g = texture(greenSource,uv); - vec4 b = texture(blueSource,uv); - vec4 a = texture(alphaSource,uv); - - vec4 result = vec4(0, 0, 0, 1); - if (redSource_connected) - result.r = getChannel(r, prop_redSource); - if (greenSource_connected) - result.g = getChannel(g, prop_greenSource); - if (blueSource_connected) - result.b = getChannel(b, prop_blueSource); - if (alphaSource_connected) - result.a = getChannel(a, prop_alphaSource); - - return result; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/rgbashuffle.ts b/src/lib/library/v2/rgbashuffle.ts deleted file mode 100644 index ec60f44b..00000000 --- a/src/lib/library/v2/rgbashuffle.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; - -export class RgbaShuffle extends GpuDesignerNode { - public init() { - this.title = "RGBA Shuffle"; - - this.addInput("inputImage"); - - // Red - let prop = this.addEnumProperty("redSource", "Red Source", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); - prop.setValue(0); - - // Green - prop = this.addEnumProperty("greenSource", "Green Source", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); - prop.setValue(1); - - // Blue - prop = this.addEnumProperty("blueSource", "Blue Source", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); - prop.setValue(2); - - // Alpha - prop = this.addEnumProperty("alphaSource", "Alpha Source", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); - prop.setValue(3); - - const source = ` - - float getChannel(vec4 inputData, int mode) - { - if (mode == 0) return inputData.r; - if (mode == 1) return inputData.g; - if (mode == 2) return inputData.b; - if (mode == 3) return inputData.a; - if (mode == 4) { - return (inputData.r + inputData.g + inputData.b) * 0.3333333; - } - - return 0.0; - } - - vec4 process(vec2 uv) - { - // grayscale input color - vec4 col = texture(inputImage,uv); - - vec4 result = vec4(0); - result.r = getChannel(col, prop_redSource); - result.g = getChannel(col, prop_greenSource); - result.b = getChannel(col, prop_blueSource); - result.a = getChannel(col, prop_alphaSource); - - return result; - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v2/stripes.ts b/src/lib/library/v2/stripes.ts deleted file mode 100644 index 83f14ce1..00000000 --- a/src/lib/library/v2/stripes.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class StripesNode extends GpuDesignerNode { - public init() { - this.title = "Stripes"; - - this.addIntProperty("stripes", "Stripes", 10, 0, 30, 1); - this.addIntProperty("shift", "Shift", 0, 0, 30, 1); - this.addFloatProperty("width", "Width", 0.5, 0.0, 1.0, 0.01); - - // calculates normal, then warps uv by it - const source = ` - vec4 process(vec2 uv) - { - float fx = uv.x * 3.142 * 2.0 * float(prop_stripes); - float fy = uv.y * 3.142 * 2.0 * float(prop_shift); - float wave = sin(fx + fy); - - // bring wave to range 0...1 - wave = wave * 0.5 + 0.5; - wave = step(prop_width, wave); - - return vec4(vec3(wave), 1.0); - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/library/v3/crop.ts b/src/lib/library/v3/crop.ts deleted file mode 100644 index 271419d6..00000000 --- a/src/lib/library/v3/crop.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Color } from "@/lib/designer/color"; -import { GpuDesignerNode } from "@/lib/designer/gpudesignernode"; - -export class Crop extends GpuDesignerNode { - public init() { - this.title = "Crop"; - - this.addInput("image"); - - this.addFloatProperty("min_x", "X Start", 0.0, 0, 1, 0.01); - this.addFloatProperty("max_x", "X End", 1.0, 0, 1, 0.01); - this.addFloatProperty("min_y", "Y Start", 0.0, 0, 1, 0.01); - this.addFloatProperty("max_y", "Y End", 1.0, 0, 1, 0.01); - - this.addColorProperty("bg", "Background Color", new Color()); - - this.addBoolProperty("clamp", "Clamp", false); - this.addBoolProperty("clamp_x", "Clamp X", false); - this.addBoolProperty("clamp_y", "Clamp Y", false); - - const source = ` - - bool is_fg(float val, float min_val, float max_val, bool clamp) - { - if (min_val <= max_val) { - return min_val <= val && val <= max_val; - } else { - if (clamp) return false; - - return val <= max_val || min_val <= val; - } - } - - vec4 process(vec2 uv) - { - if (is_fg(uv.x, prop_min_x, prop_max_x, prop_clamp || prop_clamp_x) && is_fg(uv.y, prop_min_y, prop_max_y, prop_clamp || prop_clamp_y)) - { - return texture(image, uv); - } else { - return vec4(prop_bg.rgb, 1.0); - } - } - `; - - this.buildShader(source); - } -} diff --git a/src/lib/project.ts b/src/lib/project.ts deleted file mode 100644 index 6b79f27e..00000000 --- a/src/lib/project.ts +++ /dev/null @@ -1,36 +0,0 @@ -import fs from "fs"; - -interface ExportSettings { - destination: string; - pattern: string; -} - -export class Project { - name: string = null; - path: string = null; - - data: object = null; - - getExportSettings() {} -} - -export class ProjectManager { - static load(path: string): Project { - const project = new Project(); - - project.path = path; - - const fileName = path.replace(/^.*[\\/]/, ""); - project.name = fileName.substr(0, fileName.lastIndexOf(".")) || fileName; - - // project.name = path.replace(/^.*[\\/]/, ""); - project.data = JSON.parse(fs.readFileSync(path).toString()); - return project; - } - - static save(path: string, project: Project) { - // console.log(project.data); - // console.log(JSON.stringify(project.data)); - fs.writeFileSync(path, JSON.stringify(project.data)); - } -} diff --git a/src/lib/scene.ts b/src/lib/scene.ts deleted file mode 100644 index f79ff6bd..00000000 --- a/src/lib/scene.ts +++ /dev/null @@ -1,1203 +0,0 @@ -import { ImageCanvas } from "./designer/imagecanvas"; -import { Designer } from "./designer"; -import { - NodeGraphicsItem, - NodeGraphicsItemRenderState -} from "./scene/nodegraphicsitem"; -import { ConnectionGraphicsItem } from "./scene/connectiongraphicsitem"; -import { SocketGraphicsItem, SocketType } from "./scene/socketgraphicsitem"; -import { - GraphicsItem, - MouseMoveEvent, - MouseDownEvent, - MouseUpEvent, - MouseOverEvent -} from "./scene/graphicsitem"; -import { SceneView } from "./scene/view"; -import { FrameGraphicsItem } from "./scene/framegraphicsitem"; -import { CommentGraphicsItem } from "./scene/commentgraphicsitem"; -import { NavigationGraphicsItem } from "./scene/navigationgraphicsitem"; -import { SelectionGraphicsItem } from "./scene/selectiongraphicsitem"; -import { Color } from "./designer/color"; -import { ItemClipboard } from "./clipboard"; - -enum DragMode { - None, - Selection, - Nodes, - Socket, - Frame, - Pin, - Comment -} - -class Selection { - nodes: NodeGraphicsItem[]; - - public clear() { - this.nodes = []; - } -} -class Rect { - x: number; - y: number; - width: number; - height: number; - - Rect(x = 0, y = 0, width = 1, height = 1) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } -} - -export class NodeScene { - canvas: HTMLCanvasElement; - context!: CanvasRenderingContext2D; - contextExtra: any; - hasFocus: boolean; - - frames: FrameGraphicsItem[]; - comments: CommentGraphicsItem[]; - navigations: NavigationGraphicsItem[]; - nodes: NodeGraphicsItem[]; - conns: ConnectionGraphicsItem[]; - selection: SelectionGraphicsItem; - - dragMode: DragMode; - selectionRect: Rect; - draggedNode?: NodeGraphicsItem; - //selectedNode: NodeGraphicsItem; - hitSocket?: SocketGraphicsItem; - hitConnection?: ConnectionGraphicsItem; - //selectedItem: GraphicsItem; - selectedItems: GraphicsItem[]; - hitItem: GraphicsItem; - - // callbacks - onconnectioncreated?: (item: ConnectionGraphicsItem) => void; - onconnectiondestroyed?: (item: ConnectionGraphicsItem) => void; - // passes null if no node is selected - onnodeselected?: (item: NodeGraphicsItem) => void; - oncommentselected?: (item: CommentGraphicsItem) => void; - onframeselected?: (item: FrameGraphicsItem) => void; - onnavigationselected?: (item: NavigationGraphicsItem) => void; - - onnodedeleted?: (item: NodeGraphicsItem) => void; - - // called right before items get deleted - // ideal for undo/redo - onitemsdeleting?: ( - frames: FrameGraphicsItem[], - comments: CommentGraphicsItem[], - navs: NavigationGraphicsItem[], - cons: ConnectionGraphicsItem[], - nodes: NodeGraphicsItem[] - ) => void; - - // called after items are deleted - onitemsdeleted?: ( - frames: FrameGraphicsItem[], - comments: CommentGraphicsItem[], - navs: NavigationGraphicsItem[], - cons: ConnectionGraphicsItem[], - nodes: NodeGraphicsItem[] - ) => void; - - oncopy?: (evt: ClipboardEvent) => void; - oncut?: (evt: ClipboardEvent) => void; - onpaste?: (evt: ClipboardEvent) => void; - - onlibrarymenu?: () => void; - - view: SceneView; - - // listeners for cleanup - _mouseMove: (evt: MouseEvent) => void; - _mouseDown: (evt: MouseEvent) => void; - _mouseUp: (evt: MouseEvent) => void; - _mouseClick: (evt: MouseEvent) => void; - _keyDown: (evt: KeyboardEvent) => void; - _contextMenu: (evt: MouseEvent) => void; - _copyEvent: (evt: ClipboardEvent) => void; - _cutEvent: (evt: ClipboardEvent) => void; - _pasteEvent: (evt: ClipboardEvent) => void; - copyElement: HTMLInputElement; - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.context = this.canvas.getContext("2d"); - this.view = new SceneView(canvas); - this.hasFocus = false; - this.contextExtra = this.context; - this.frames = []; - this.comments = []; - this.nodes = []; - this.conns = []; - this.navigations = []; - this.dragMode = null; - this.selectionRect = new Rect(); - //this.selectedItem = null; - this.selectedItems = []; - this.hitItem = null; - - // add sample frames - // let frame = new FrameGraphicsItem(this.view); - // frame.setSize(500, 300); - // frame.scene = this; - // this.frames.push(frame); - - // let comment = new CommentGraphicsItem(this.view); - // comment.scene = this; - // //comment.setText("Hello World"); - // comment.setText("This\nis\na\nmultiline\nmessage"); - // comment.setCenter(200, 500); - // this.comments.push(comment); - - // let nav = new NavigationGraphicsItem(); - // nav.scene = this; - // nav.setLabel("Test Navigation"); - // this.navigations.push(nav); - - // bind event listeners - const self = this; - this._mouseMove = function(evt: MouseEvent) { - self.onMouseMove(evt); - }; - canvas.addEventListener("mousemove", this._mouseMove); - - self._mouseDown = function(evt: MouseEvent) { - self.onMouseDown(evt); - }; - canvas.addEventListener("mousedown", self._mouseDown); - - self._mouseUp = function(evt: MouseEvent) { - self.onMouseUp(evt); - - if (evt.target == canvas) { - self.hasFocus = true; - - // focus copy element - self.copyElement.focus(); - self.copyElement.select(); - } else { - self.hasFocus = false; - } - }; - canvas.addEventListener("mouseup", self._mouseUp); - - // self._mouseClick = function(evt: MouseEvent) { - // if (evt.target == canvas) { - // self.hasFocus = true; - // } else { - // self.hasFocus = false; - // } - // }; - // window.addEventListener("click", self._mouseClick); - - self._keyDown = function(evt: KeyboardEvent) { - if ( - evt.key == "Delete" && - self.hasFocus && - self.selectedItems.length != 0 - ) { - //self.deleteNode(self.selectedNode); - self.deleteItems(self.selectedItems); - } - - if ( - evt.key == " " && - // self.hasFocus && - self.view.isMouseOverCanvas() - ) { - if (self.onlibrarymenu != null && self.hitItem == null) { - self.onlibrarymenu(); - } - } - - //console.log(evt.key.length); - }; - window.addEventListener("keydown", self._keyDown, true); - // canvas.addEventListener("mousewheel", function(evt: WheelEvent) { - // self.onMouseScroll(evt); - // }); - self._contextMenu = function(evt: MouseEvent) { - evt.preventDefault(); - }; - canvas.addEventListener("contextmenu", self._contextMenu); - - this._copyEvent = evt => { - if (self.hasFocus && evt.target == self.copyElement) { - // alert("copying selection"); - evt.preventDefault(); - - self.onCopy(evt); - } - }; - document.addEventListener("copy", this._copyEvent); - - this._cutEvent = evt => { - if (self.hasFocus && evt.target == self.copyElement) { - // alert("cutting selection"); - evt.preventDefault(); - - self.onCut(evt); - self.deleteItems(this.selectedItems); - } - }; - document.addEventListener("cut", this._cutEvent); - - this._pasteEvent = evt => { - if (self.hasFocus && evt.target == self.copyElement) { - // alert("pasting selection"); - // console.log(evt.target); - // console.log(evt.clipboardData); - evt.preventDefault(); - self.copyElement.value = " "; - - self.onPaste(evt); - } - }; - document.addEventListener("paste", this._pasteEvent); - - this.copyElement = document.createElement("input"); - self.copyElement.value = " "; - //self.copyElement.style.display = "none"; - self.copyElement.style.opacity = "0"; - self.copyElement.style.width = "1px"; - self.copyElement.style.height = "1px"; - document.body.appendChild(this.copyElement); - //this.copyElement.addEventListener("copy", this._copyEvent); - // note: console.log(this.copyElement) to see in DOM - // golden layout conveniently hides it - } - - dispose() { - // alert("disposed!"); - this.canvas.removeEventListener("mousemove", this._mouseMove); - this.canvas.removeEventListener("mousedown", this._mouseDown); - this.canvas.removeEventListener("mouseup", this._mouseUp); - window.removeEventListener("click", this._mouseClick); - window.removeEventListener("keydown", this._keyDown, true); - this.canvas.removeEventListener("contextmenu", this._contextMenu); - document.removeEventListener("copy", this._copyEvent); - document.removeEventListener("paste", this._pasteEvent); - // this.copyElement.removeEventListener("copy", this._copyEvent); - // this.copyElement.removeEventListener("paste", this._copyEvent); - - this.canvas = null; - this.context = null; - this.view = null; - this.contextExtra = null; - this.frames = []; - this.comments = []; - this.nodes = []; - this.conns = []; - this.navigations = []; - this.selectionRect = null; - this.selectedItems = []; - this.hitItem = null; - } - - setSelectedItems(items: GraphicsItem[], createSelection = false) { - this.selectedItems = items; - - // create actual selection object to encapsulate items - if (createSelection == true) { - const sel: SelectionGraphicsItem = new SelectionGraphicsItem( - this, - this.view - ); - sel.setHitItems(items); - this.selection = sel; - } - } - - // no callbacks are made here - addNode(item: NodeGraphicsItem) { - this.nodes.push(item); - - // needed for sockets to get scene instance - item.setScene(this); - } - - addComment(item: CommentGraphicsItem) { - item.setScene(this); - this.comments.push(item); - } - - removeComment(item: CommentGraphicsItem) { - //todo: remove from selection - const i = this.comments.indexOf(item); - if (i !== -1) this.comments.splice(i, 1); - } - - addFrame(item: FrameGraphicsItem) { - item.setScene(this); - this.frames.push(item); - } - - removeFrame(item: FrameGraphicsItem) { - //todo: remove from selection - const i = this.frames.indexOf(item); - if (i !== -1) this.frames.splice(i, 1); - } - - addNavigation(nav: NavigationGraphicsItem) { - nav.setScene(this); - this.navigations.push(nav); - } - - removeNavigation(item: NavigationGraphicsItem) { - //todo: remove from selection - const i = this.navigations.indexOf(item); - if (i !== -1) this.navigations.splice(i, 1); - } - - deleteNode(item: NodeGraphicsItem) { - // delete connections - const conns = this.conns; - for (let i = this.conns.length - 1; i >= 0; i--) { - const con = this.conns[i]; - if ( - (con.socketA && con.socketA.node.id == item.id) || - (con.socketB && con.socketB.node.id == item.id) - ) { - this.removeConnection(con); - } - } - - // remove node from list - this.nodes.splice(this.nodes.indexOf(item), 1); - - // if node is selected (which it most likely is), clear it from selection - // this.selectedNode = null; - - // emit deselection - if (this.onnodeselected) this.onnodeselected(null); - - // emit remove event - if (this.onnodedeleted) this.onnodedeleted(item); - } - - // called by delete or cut event - deleteItems(items: GraphicsItem[]) { - // 1 - put items in buckets - const frames: FrameGraphicsItem[] = []; - const comments: CommentGraphicsItem[] = []; - const navs: NavigationGraphicsItem[] = []; - const cons: ConnectionGraphicsItem[] = []; - const nodes: NodeGraphicsItem[] = []; - - for (const item of items) { - if (item instanceof FrameGraphicsItem) { - frames.push(item); - } - if (item instanceof CommentGraphicsItem) { - comments.push(item); - } - if (item instanceof NavigationGraphicsItem) { - navs.push(item); - } - if (item instanceof NodeGraphicsItem) { - nodes.push(item); - } - } - - // if nothing was deleted then return - if ( - frames.length == 0 && - comments.length == 0 && - navs.length == 0 && - nodes.length == 0 - ) - return; - - // 2 - gather affected connections - const conDict = new Map(); - for (const node of nodes) { - // add all connections to map - for (const sock of node.sockets) { - for (const con of sock.conns) { - conDict.set(con.id, con); - } - } - } - for (const [key, con] of conDict) cons.push(con); - - // 3 - actual deletion - if (this.onitemsdeleting) { - this.onitemsdeleting(frames, comments, navs, cons, nodes); - } - - for (const frame of frames) this.removeFrame(frame); - for (const comment of comments) this.removeComment(comment); - for (const nav of navs) this.removeNavigation(nav); - for (const node of nodes) this.deleteNode(node); - - // 4 - callback - if (this.onitemsdeleted) { - this.onitemsdeleted(frames, comments, navs, cons, nodes); - } - } - - getNodeById(id: string): NodeGraphicsItem { - for (const node of this.nodes) { - if (node.id == id) return node; - } - return null; - } - - /** - * Checks if adding a hypothetical connection, the graph would remain to be a directed acyclic graph (DAG). - * - * @param leftNode input node of the new connection - * @param rightNode output node of the new connection - * @returns - */ - remainsDAG(leftNode: NodeGraphicsItem, rightNode: NodeGraphicsItem) { - let checked = new Set(); - checked.add(leftNode); - - let expanding = [rightNode]; - while (expanding.length > 0) { - let nodeA = expanding.pop(); - - for (let socket of nodeA.getOutSockets()) { - for (let conn of socket.conns) { - let nodeB = conn.socketB.node; - - // if (checked.has(nodeB)) { - // if left node is hit, this means making - // the connection will cause a cycle - if (nodeB === leftNode) { - return false; - } else { - expanding.push(nodeB); - checked.add(nodeA); - } - } - } - } - - return true; - } - - addConnection(con: ConnectionGraphicsItem) { - this.conns.push(con); - - // link the sockets - con.socketA.addConnection(con); - con.socketB.addConnection(con); - - // callback - if (this.onconnectioncreated) this.onconnectioncreated(con); - } - - createConnection(leftId: string, rightId: string, rightIndex = 0) { - const con = new ConnectionGraphicsItem(); - - // get nodes - const leftNode = this.getNodeById(leftId); - const rightNode = this.getNodeById(rightId); - - // get sockets - con.socketA = leftNode.sockets.find(x => x.socketType == SocketType.Out); - con.socketB = rightNode.sockets[rightIndex]; - - this.addConnection(con); - } - - removeConnection(con: ConnectionGraphicsItem) { - this.conns.splice(this.conns.indexOf(con), 1); - //con.socketA.con = null; - //con.socketB.con = null; - con.socketA.removeConnection(con); - con.socketB.removeConnection(con); - - // callback - if (this.onconnectiondestroyed) this.onconnectiondestroyed(con); - } - - // if the user click drags on a socket then it's making a connection - drawActiveConnection() { - const mouse = this.view.getMouseSceneSpace(); - const mouseX = mouse.x; - const mouseY = mouse.y; - - const ctx = this.context; - if (this.hitSocket) { - ctx.beginPath(); - ctx.strokeStyle = "rgb(200, 200, 200)"; - ctx.lineWidth = 4; - ctx.moveTo(this.hitSocket.centerX(), this.hitSocket.centerY()); - - if (this.hitSocket.socketType == SocketType.Out) { - ctx.bezierCurveTo( - this.hitSocket.centerX() + 60, - this.hitSocket.centerY(), // control point 1 - mouseX - 60, - mouseY, - mouseX, - mouseY - ); - } else { - ctx.bezierCurveTo( - this.hitSocket.centerX() - 60, - this.hitSocket.centerY(), // control point 1 - mouseX + 60, - mouseY, - mouseX, - mouseY - ); - } - - ctx.setLineDash([5, 3]); - ctx.stroke(); - ctx.setLineDash([]); - - ctx.beginPath(); - ctx.fillStyle = "rgb(200, 200, 200)"; - const radius = 6; - ctx.arc(mouseX, mouseY, radius, 0, 2 * Math.PI); - ctx.fill(); - } - } - - clearAndDrawGrid() { - //this.context.scale(2,2); - // this.context.fillStyle = "rgb(120, 120, 120)"; - // var topCorner = this.view.canvasToSceneXY(0, 0); - // var bottomCorner = this.view.canvasToSceneXY( - // this.canvas.clientWidth, - // this.canvas.clientHeight - // ); - // this.context.fillRect( - // topCorner.x, - // topCorner.y, - // bottomCorner.x - topCorner.x, - // bottomCorner.y - topCorner.y - // ); - //this.context.fillRect(0,0,this.canvas.width, this.canvas.height); - - // todo: draw grid - - this.view.clear(this.context, "#4A5050"); - this.view.setViewMatrix(this.context); - this.view.drawGrid(this.context, 33.33333, "#4E5454", 1); - this.view.drawGrid(this.context, 100, "#464C4C", 3); - } - - draw() { - this.clearAndDrawGrid(); - - // draw frames - for (const frame of this.frames) frame.draw(this.context); - - // draw comments - for (const comment of this.comments) comment.draw(this.context); - - // draw connections - for (const con of this.conns) { - if (con == this.hitConnection) continue; - con.draw(this.context); - } - - if (this.hitSocket) { - this.drawActiveConnection(); - } - - // draw nodes - const mouse = this.view.getMouseSceneSpace(); - const mouseX = mouse.x; - const mouseY = mouse.y; - const nodeState: NodeGraphicsItemRenderState = { - hovered: false, // mouse over - selected: false // selected node - }; - for (const item of this.nodes) { - // check for selection ( only do this when not dragging anything ) - //if (item == this.selectedNode) nodeState.selected = true; - //else nodeState.selected = false; - - // check for hover - if (item.isPointInside(mouseX, mouseY) && this.hitSocket == null) - nodeState.hovered = true; - else nodeState.hovered = false; - - item.draw(this.context, nodeState); - } - - for (const nav of this.navigations) nav.draw(this.context); - - if (this.selection) this.selection.draw(this.context); - - if (this.selectedItems.length > 0) { - this.drawSelectedItems(this.selectedItems, this.context); - } - } - - drawSelectedItems(items: GraphicsItem[], ctx: CanvasRenderingContext2D) { - for (const item of items) { - ctx.beginPath(); - ctx.lineWidth = 3; - ctx.strokeStyle = "rgba(255, 255, 255)"; - //this.roundRect(ctx, this.x, this.y, width, height, 1); - // ctx.rect(item.left, item.top, item.getWidth(), item.getHeight()); - const rect = item.getRect(); - rect.expand(15); - ctx.rect(rect.left, rect.top, rect.width, rect.height); - - ctx.stroke(); - - ctx.fillStyle = "rgba(255, 255, 255, 0.1)"; - ctx.rect(rect.left, rect.top, rect.width, rect.height); - ctx.fill(); - } - } - - onCopy(evt: ClipboardEvent) { - // todo: copy selected items to clipboard - //ItemClipboard.copyItems(this, evt.clipboardData); - if (this.oncopy) this.oncopy(evt); - } - - onCut(evt: ClipboardEvent) { - // todo: copy selected items to clipboard - //ItemClipboard.copyItems(this, evt.clipboardData); - if (this.oncut) this.oncut(evt); - } - - onPaste(evt: ClipboardEvent) { - // todo: paste items from clipboard - //ItemClipboard.pasteItems(this, evt.clipboardData); - if (this.onpaste) this.onpaste(evt); - } - - // mouse events - onMouseDown(evt: MouseEvent) { - // console.log("mouse dwn!"); - //todo: look at double event calling - const pos = this.getScenePos(evt); - const mouseX = pos.x; - const mouseY = pos.y; - - if (evt.button == 0) { - const hitItem = this.getHitItem(mouseX, mouseY); - const mouseEvent = new MouseDownEvent(); - mouseEvent.globalX = pos.x; - mouseEvent.globalY = pos.y; - mouseEvent.shiftKey = evt.shiftKey; - mouseEvent.altKey = evt.altKey; - mouseEvent.ctrlKey = evt.ctrlKey; - - if (hitItem != null) { - mouseEvent.localX = hitItem.left - pos.x; - mouseEvent.localY = hitItem.top - pos.y; - - hitItem.mouseDown(mouseEvent); - if (mouseEvent.isAccepted) { - this.hitItem = hitItem; - - //console.log(hitItem); - if (hitItem instanceof NodeGraphicsItem) { - const hitNode = hitItem; - //move node to stop of stack - this.moveNodeToTop(hitNode); - - if (this.onnodeselected) { - if (hitNode) this.onnodeselected(hitNode); - else this.onnodeselected(hitNode); - } - } - - //todo: look at double event calling for comments - if (hitItem instanceof CommentGraphicsItem) { - const hitComment = hitItem; - - if (this.oncommentselected) { - if (hitComment) this.oncommentselected(hitComment); - else this.oncommentselected(hitComment); - } - } - - if (hitItem instanceof FrameGraphicsItem) { - const hit = hitItem; - - if (this.onframeselected) { - if (hit) this.onframeselected(hit); - else this.onframeselected(hit); - } - } - - if (hitItem instanceof NavigationGraphicsItem) { - const hit = hitItem; - - if (this.onnavigationselected) { - if (hit) this.onnavigationselected(hit); - else this.onnavigationselected(hit); - } - } - - // selection graphics item can never be *selected* - if ( - !(hitItem instanceof SelectionGraphicsItem) && - !(hitItem instanceof SocketGraphicsItem) - ) - this.selectedItems = [hitItem]; - } - } else { - const hitItem = new SelectionGraphicsItem(this, this.view); - mouseEvent.localX = hitItem.left - pos.x; - mouseEvent.localY = hitItem.top - pos.y; - hitItem.mouseDown(mouseEvent); - - this.selection = hitItem; - this.hitItem = hitItem; - //console.log(hitItem); - } - - // check for a hit socket first - // let hitSock: SocketGraphicsItem = this.getHitSocket(mouseX, mouseY); - - // if (hitSock) { - // // if socket is an in socket with a connection, make hitsocket the connected out socket - // if ( - // hitSock.socketType == SocketType.In && - // hitSock.hasConnections() - // ) { - // this.hitSocket = hitSock.getConnection(0).socketA; // insockets should only have one connection - // // store connection for removal as well - // this.hitConnection = hitSock.getConnection(0); - // } else this.hitSocket = hitSock; - // } else { - // // if there isnt a hit socket then check for a hit node - // let hitNode: NodeGraphicsItem = this.getHitNode(mouseX, mouseY); - - // if (hitNode) { - // //move node to stop of stack - // this.moveNodeToTop(hitNode); - - // // todo: do this properly on mouse release - // this.selectedNode = hitNode; - // } else { - // this.selectedNode = null; - // } - - // this.draggedNode = hitNode; - // if (this.onnodeselected) { - // if (hitNode) this.onnodeselected(hitNode); - // else this.onnodeselected(hitNode); - // } - // } - } - } - - // https://stackoverflow.com/questions/5306680/move-an-array-element-from-one-array-position-to-another - moveNodeToTop(node: NodeGraphicsItem) { - const index = this.nodes.indexOf(node); - if (index === -1) { - console.log("Attempting to push node that doesnt exist in node list"); - } - this.nodes.splice(index, 1); - this.nodes.push(node); - } - - onMouseUp(evt: MouseEvent) { - const pos = this.getScenePos(evt); - const mouseX = pos.x; - const mouseY = pos.y; - - if (evt.button == 0) { - if (this.hitItem != null) { - const hitItem = this.hitItem; - - const mouseEvent = new MouseUpEvent(); - mouseEvent.globalX = pos.x; - mouseEvent.globalY = pos.y; - mouseEvent.localX = hitItem.left - pos.x; - mouseEvent.localY = hitItem.top - pos.y; - mouseEvent.shiftKey = evt.shiftKey; - mouseEvent.altKey = evt.altKey; - mouseEvent.ctrlKey = evt.ctrlKey; - - hitItem.mouseUp(mouseEvent); - - this.hitItem = null; - } - } - - // if (evt.button == 0) { - // if (this.hitSocket) { - // // remove previous connection - // // this block creates a new connection regardless of the outcome - // if (this.hitConnection) { - // this.removeConnection(this.hitConnection); - // this.hitConnection = null; - // } - - // let closeSock: SocketGraphicsItem = this.getHitSocket( - // mouseX, - // mouseY - // ); - - // if ( - // closeSock && - // closeSock != this.hitSocket && - // closeSock.socketType != this.hitSocket.socketType && - // closeSock.node != this.hitSocket.node - // ) { - // // close socket - // var con: ConnectionGraphicsItem = new ConnectionGraphicsItem(); - // // out socket should be on the left, socketA - // if (this.hitSocket.socketType == SocketType.Out) { - // // out socket - // con.socketA = this.hitSocket; - // con.socketB = closeSock; - - // // close sock is an inSocket which means it should only have one connection - // // remove current connection from inSocket - // if (closeSock.hasConnections()) - // this.removeConnection(closeSock.getConnection(0)); - // } else { - // // in socket - // con.socketA = closeSock; - // con.socketB = this.hitSocket; - // } - - // // link connection - // //con.socketA.con = con; - // //con.socketB.con = con; - - // this.addConnection(con); - // } else if (!closeSock) { - // // delete connection if hit node is an insock - // // if we're here it means one of 2 things: - // // 1: a new connection failed to form - // // 2: we're breaking a previously formed connection, which can only be done - // // by dragging from an insock that already has a connection - - // if (this.hitSocket.socketType == SocketType.Out) { - // /* - // if (this.hitSocket.hasConnections()) { - // // remove connection - // //let con = this.hitSocket.con; - // this.removeConnection(this.hitSocket.getConnectionFrom(this.hitSocket)); - // } - // */ - - // if (this.hitConnection) - // this.removeConnection(this.hitConnection); - // } - // } - // } - - // this.draggedNode = null; - // this.hitSocket = null; - // this.hitConnection = null; - // } - } - - onMouseMove(evt: MouseEvent) { - const pos = this.getScenePos(evt); - - if (this.hitItem) { - const mouseEvent = new MouseMoveEvent(); - mouseEvent.globalX = pos.x; - mouseEvent.globalY = pos.y; - - mouseEvent.localX = this.hitItem.left - pos.x; - mouseEvent.localY = this.hitItem.top - pos.y; - - mouseEvent.shiftKey = evt.shiftKey; - mouseEvent.altKey = evt.altKey; - mouseEvent.ctrlKey = evt.ctrlKey; - - const drag = this.view.getMouseDeltaSceneSpace(); - mouseEvent.deltaX = drag.x; - mouseEvent.deltaY = drag.y; - - this.hitItem.mouseMove(mouseEvent); - } else { - // do mouse over - const hitItem = this.getHitItem(pos.x, pos.y); - if (hitItem) { - const mouseEvent = new MouseOverEvent(); - mouseEvent.globalX = pos.x; - mouseEvent.globalY = pos.y; - - mouseEvent.localX = hitItem.left - pos.x; - mouseEvent.localY = hitItem.top - pos.y; - - mouseEvent.shiftKey = evt.shiftKey; - mouseEvent.altKey = evt.altKey; - mouseEvent.ctrlKey = evt.ctrlKey; - - hitItem.mouseOver(mouseEvent); - } else { - // reset pointer - this.view.canvas.style.cursor = "default"; - } - } - - // // handle dragged socket - // if (this.hitSocket) { - // } - - // // handle dragged node - // if (this.draggedNode != null) { - // //var diff = this.view.canvasToSceneXY(evt.movementX, evt.movementY); - // //console.log("move: ",evt.movementX,evt.movementY); - // //this.draggedNode.move(evt.movementX, evt.movementY); - - // // view keeps track of dragging - // let drag = this.view.getMouseDeltaSceneSpace(); - // this.draggedNode.move(drag.x, drag.y); - // } - } - - // hit detection - // x and y are scene space - getHitNode(x: number, y: number): NodeGraphicsItem { - // for (let node of this.nodes) { - for (let index = this.nodes.length - 1; index >= 0; index--) { - const node = this.nodes[index]; - if (node.isPointInside(x, y)) return node; - } - - return null; - } - - getHitSocket(x: number, y: number): SocketGraphicsItem { - for (const node of this.nodes) { - for (const sock of node.sockets) { - if (sock.isPointInside(x, y)) return sock; - } - } - - return null; - } - - // gets item over mouse x and y - // obeys precedence - getHitItem(x: number, y: number): GraphicsItem { - const hitItem = this._getHitItem(x, y); - - // if item is in selection then return whole selection - if ( - hitItem != null && - this.isItemSelected(hitItem) && - this.selection != null - ) { - if (this.selection.isPointInside(x, y)) return this.selection; - } - - return hitItem; - } - - _getHitItem(x: number, y: number): GraphicsItem { - // 1) navigation pins - for (let index = this.navigations.length - 1; index >= 0; index--) { - const nav = this.navigations[index]; - - if (nav.isPointInside(x, y)) return nav; - } - - // 2) nodes and their sockets - for (let index = this.nodes.length - 1; index >= 0; index--) { - const node = this.nodes[index]; - - for (const sock of node.sockets) { - if (sock.isPointInside(x, y)) return sock; - } - - if (node.isPointInside(x, y)) return node; - } - - // 3) comments - for (let index = this.comments.length - 1; index >= 0; index--) { - const comment = this.comments[index]; - - if (comment.isPointInside(x, y)) return comment; - } - - // 4) frame - for (let index = this.frames.length - 1; index >= 0; index--) { - const frame = this.frames[index]; - - if (frame.isPointInside(x, y)) return frame; - } - - return null; - } - - isItemSelected(hitItem): boolean { - // todo: use dictionary - for (const item of this.selectedItems) if (item == hitItem) return true; - return false; - } - - // UTILITY - - // returns the scene pos from the mouse event - getScenePos(evt: MouseEvent) { - const canvasPos = _getMousePos(this.canvas, evt); - return this.view.canvasToSceneXY(canvasPos.x, canvasPos.y); - } - - // SAVE/LOAD - - // only save position data to associative array - save(): any { - const data: any = {}; - - // NODES - const nodes = {}; - for (const node of this.nodes) { - const n: any = {}; - n["id"] = node.id; - n["x"] = node.centerX(); - n["y"] = node.centerY(); - - nodes[node.id] = n; - } - data["nodes"] = nodes; - - // FRAMES - const frames = []; - for (const frame of this.frames) { - const n: any = {}; - n["x"] = frame.left; - n["y"] = frame.top; - n["width"] = frame.getWidth(); - n["height"] = frame.getHeight(); - - n["title"] = frame.title; - n["showTitle"] = frame.showTitle; - n["description"] = frame.description; - n["color"] = frame.color.toHex(); - - frames.push(n); - } - data["frames"] = frames; - - // COMMENTS - const comments = []; - for (const comment of this.comments) { - const n: any = {}; - n["x"] = comment.left; - n["y"] = comment.top; - - n["text"] = comment.text; - n["color"] = comment.color.toHex(); - - comments.push(n); - } - data["comments"] = comments; - - // NAVIGATIONS - const navs = []; - for (const nav of this.navigations) { - const n: any = {}; - n["x"] = nav.left; - n["y"] = nav.top; - - navs.push(n); - } - data["navigations"] = navs; - - return data; - } - - static load( - designer: Designer, - data: any, - canvas: HTMLCanvasElement - ): NodeScene { - const s = new NodeScene(canvas); - - // add nodes one by one - for (const dNode of designer.nodes) { - // create node from designer - const node = new NodeGraphicsItem(dNode.title); - for (const input of dNode.getInputs()) { - node.addSocket(input, input, SocketType.In); - } - node.addSocket("output", "output", SocketType.Out); - s.addNode(node); - node.id = dNode.id; - - // get position - const x = data["nodes"][node.id].x; - const y = data["nodes"][node.id].y; - node.setCenter(x, y); - } - - // add connection one by one - for (const dcon of designer.conns) { - const con = new ConnectionGraphicsItem(); - con.id = dcon.id; - - // get nodes - const leftNode = s.getNodeById(dcon.leftNode.id); - const rightNode = s.getNodeById(dcon.rightNode.id); - - // get sockets - con.socketA = leftNode.getOutSocketByName(dcon.leftNodeOutput); - con.socketB = rightNode.getInSocketByName(dcon.rightNodeInput); - - s.addConnection(con); - } - - //todo: integrity checks - // FRAMES - if (data.frames) { - for (const d of data.frames) { - const frame = new FrameGraphicsItem(s.view); - frame.setPos(d.x, d.y); - frame.setSize(d.width, d.height); - - frame.setTitle(d.title); - frame.setShowTitle(d.showTitle); - frame.setDescription(d.description); - frame.color = Color.parse(d.color); - - s.addFrame(frame); - } - } - - // COMMENTS - if (data.comments) { - for (const d of data.comments) { - const comment = new CommentGraphicsItem(s.view); - comment.setPos(d.x, d.y); - comment.setText(d.text); - comment.color = Color.parse(d.color); - - s.addComment(comment); - } - } - - // NAVIGATION - if (data.navigations) { - for (const d of data.navigations) { - const nav = new NavigationGraphicsItem(); - nav.setPos(d.x, d.y); - s.addNavigation(nav); - } - } - - return s; - } -} - -// https://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/ -// https://stackoverflow.com/questions/17130395/real-mouse-position-in-canvas -function _getMousePos(canvas, evt) { - const rect = canvas.getBoundingClientRect(); - return { - x: evt.clientX - rect.left, - y: evt.clientY - rect.top - }; -} diff --git a/src/lib/scene/commentgraphicsitem.ts b/src/lib/scene/commentgraphicsitem.ts deleted file mode 100644 index 116d1457..00000000 --- a/src/lib/scene/commentgraphicsitem.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { SocketGraphicsItem } from "./socketgraphicsitem"; -import { - GraphicsItem, - MouseDownEvent, - MouseMoveEvent, - MouseUpEvent -} from "./graphicsitem"; -import { SceneView, Vector2 } from "./view"; -import { Color } from "../designer/color"; -import { - IPropertyHolder, - Property, - PropertyGroup, - StringProperty -} from "../designer/properties"; -import { MoveItemsAction } from "../actions/moveItemsaction"; -import { UndoStack } from "../undostack"; - -// https://stackoverflow.com/questions/5026961/html5-canvas-ctx-filltext-wont-do-line-breaks -export class CommentGraphicsItem extends GraphicsItem - implements IPropertyHolder { - text: string; - textProp: StringProperty; - view: SceneView; - color: Color; - - padding: number; - fontHeight: number; - - hit: boolean; - dragged: boolean; - dragStartPos: Vector2; - - constructor(view: SceneView) { - super(); - this.text = ""; - this.view = view; - this.color = new Color(0.9, 0.9, 0.9); - - this.hit = false; - this.dragged = false; - - this.padding = 5; - this.fontHeight = 20; - - this.textProp = new StringProperty("comment", "Comment", "Comment.", true); - this.properties.push(this.textProp); - - this.setText("comment"); - } - propertyGroups: PropertyGroup[]; - - properties: Property[] = []; - setProperty(name: string, value: any) { - const prop = this.properties.find(x => { - return x.name == name; - }); - - // if (prop) { - // prop.setValue(value); - // } - - if (name == "comment") { - this.setText(value); - } - } - - setText(text: string) { - this.text = text; - this.textProp.setValue(text); - const fontHeight = this.fontHeight; - - const ctx = this.view.context; - - ctx.lineWidth = 1; - ctx.font = fontHeight + "px 'Open Sans'"; - const size = ctx.measureText(this.text); - - let maxWidth = 0; - const lines = this.text.split("\n"); - // console.log(lines); - // console.log(ctx); - // console.log(ctx.font); - for (let i = 0; i < lines.length; ++i) { - const size = ctx.measureText(lines[i]); - //console.log("INITIAL WITH: " + size.width); - maxWidth = Math.max(maxWidth, size.width); - } - - // somewhat inaccurate here for some reason - // maybe some bug in html5 canvas - // recalculate in draw function - this.width = maxWidth + this.padding * 2; - this.height = lines.length * fontHeight + this.padding * 2; - } - - private buildColor(color: Color, alpha: number) { - const col = - "rgba(" + - color.r * 255 + - "," + - color.g * 255 + - "," + - color.b * 255 + - "," + - alpha + - ")"; - - return col; - } - - draw(ctx: CanvasRenderingContext2D, renderData: any = null) { - const fontHeight = this.fontHeight; - ctx.font = fontHeight + "px 'Open Sans'"; - ctx.fillStyle = "rgb(240, 240, 240)"; - - // recalc rect - let maxWidth = 0; - //console.log(this.text); - const lines = this.text.split("\n"); - for (var i = 0; i < lines.length; ++i) { - const size = ctx.measureText(lines[i]); - maxWidth = Math.max(maxWidth, size.width); - } - - this.width = maxWidth + this.padding * 2; - this.height = lines.length * fontHeight + this.padding * 2; - - // -------------------------------------------------------- - - const width = this.width; - const height = this.height; - - // stroke bounding rect - ctx.beginPath(); - ctx.lineWidth = 1; - ctx.strokeStyle = this.buildColor(this.color, 0.5); - this.roundRect(ctx, this.x, this.y, width, height, 1); - ctx.stroke(); - - // inner area - ctx.beginPath(); - ctx.lineWidth = 1; - ctx.fillStyle = this.buildColor(this.color, 0.1); - this.roundRect(ctx, this.x, this.y, width, height, 1); - ctx.fill(); - - // multiline text - ctx.fillStyle = "rgb(240, 240, 240)"; - const textX = this.x + this.padding; - let textY = this.y + fontHeight; - - const lineHeight = fontHeight; - //var lines = this.text.split("\n"); - ctx.font = fontHeight + "px 'Open Sans'"; - ctx.textAlign = "left"; - ctx.lineWidth = 1; - - //console.log(ctx.font); - for (let i = 0; i < lines.length; ++i) { - ctx.fillText(lines[i], textX, textY); - textY += lineHeight; - const size = ctx.measureText(lines[i]); - //console.log("RENDER WITH: " + size.width); - } - } - - // MOUSE EVENTS - public mouseDown(evt: MouseDownEvent) { - this.hit = true; - this.dragged = false; - this.dragStartPos = new Vector2(this.x, this.y); - //console.log(this.text); - } - - public mouseMove(evt: MouseMoveEvent) { - if (this.hit) { - // movement - this.move(evt.deltaX, evt.deltaY); - this.dragged = true; - } - } - - public mouseUp(evt: MouseUpEvent) { - this.hit = false; - - // add undo/redo - const newPos = new Vector2(this.x, this.y); - - if (this.dragged) { - const action = new MoveItemsAction( - [this], - [this.dragStartPos.clone()], - [newPos] - ); - - UndoStack.current.push(action); - } - - this.dragged = false; - } -} diff --git a/src/lib/scene/connectiongraphicsitem.ts b/src/lib/scene/connectiongraphicsitem.ts deleted file mode 100644 index 3055c469..00000000 --- a/src/lib/scene/connectiongraphicsitem.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { SocketGraphicsItem } from "./socketgraphicsitem"; -import { GraphicsItem } from "./graphicsitem"; - -export class ConnectionGraphicsItem extends GraphicsItem { - id!: string; - public socketA!: SocketGraphicsItem; - public socketB!: SocketGraphicsItem; - - draw(ctx: CanvasRenderingContext2D, renderData: any = null) { - ctx.beginPath(); - ctx.strokeStyle = "rgb(200, 200, 200)"; - ctx.lineWidth = 4; - ctx.moveTo(this.socketA.centerX(), this.socketA.centerY()); - ctx.bezierCurveTo( - this.socketA.centerX() + 60, - this.socketA.centerY(), // control point 1 - this.socketB.centerX() - 60, - this.socketB.centerY(), - this.socketB.centerX(), - this.socketB.centerY() - ); - ctx.stroke(); - } -} diff --git a/src/lib/scene/framegraphicsitem.ts b/src/lib/scene/framegraphicsitem.ts deleted file mode 100644 index 2a86f9f4..00000000 --- a/src/lib/scene/framegraphicsitem.ts +++ /dev/null @@ -1,585 +0,0 @@ -import { SocketGraphicsItem } from "./socketgraphicsitem"; -import { - GraphicsItem, - MouseDownEvent, - MouseMoveEvent, - MouseUpEvent, - MouseOverEvent -} from "./graphicsitem"; -import { SceneView, Vector2, Rect } from "./view"; -import { Color } from "../designer/color"; -import { NodeGraphicsItem } from "./nodegraphicsitem"; -import { - IPropertyHolder, - Property, - StringProperty, - BoolProperty, - PropertyGroup -} from "../designer/properties"; -import { MoveItemsAction } from "../actions/moveItemsaction"; -import { UndoStack } from "../undostack"; -import { ResizeFrameAction } from "../actions/resizeframeaction"; - -enum XResizeDir { - None, - Left, - Right -} - -enum YResizeDir { - None, - Top, - Bottom -} - -enum DragMode { - None, - HandleTop, - Resize -} - -export class FrameRegion { - rect: Rect = null; - dragMode: DragMode = DragMode.None; - xResizeDir: XResizeDir = XResizeDir.None; - yResizeDir: YResizeDir = YResizeDir.None; - cursor: string = null; -} - -// https://developer.mozilla.org/en-US/docs/Web/CSS/cursor -export class FrameGraphicsItem extends GraphicsItem implements IPropertyHolder { - title: string; - description: string; - showTitle: boolean; - view: SceneView; - color: Color; - hit: boolean; - dragged: boolean; - dragStartPos: Vector2; - dragStartRect: Rect; - - xResize: XResizeDir; - yResize: YResizeDir; - dragMode: DragMode; - - // display properties - handleSize: number; - - // the radius of the resize handle - resizeHandleSize: number; - - nodes: NodeGraphicsItem[]; - - titleProp: StringProperty; - showTitleProp: BoolProperty; - descrProp: StringProperty; - - public constructor(view: SceneView) { - super(); - this.title = "Frame"; - this.description = ""; - this.showTitle = true; - this.view = view; - this.color = new Color(0.1, 0, 0.2); - this.hit = false; - this.dragged = true; - - this.xResize = XResizeDir.None; - this.yResize = YResizeDir.None; - this.dragMode = DragMode.None; - - this.handleSize = 30; - this.resizeHandleSize = 20; - - this.setSize(500, 300); - - this.nodes = []; - - this.titleProp = new StringProperty("title", "Title", "Frame"); - this.showTitleProp = new BoolProperty("showtitle", "Show Title", true); - this.descrProp = new StringProperty("description", "Description", "", true); - this.properties.push(this.titleProp); - this.properties.push(this.showTitleProp); - this.properties.push(this.descrProp); - } - propertyGroups: PropertyGroup[]; - properties: Property[] = []; - setProperty(name: string, value: any) { - if (name == "title") { - this.setTitle(value); - } else if (name == "showtitle") { - this.setShowTitle(value); - } else if (name == "description") { - this.setDescription(value); - } - } - - setSize(w: number, h: number) { - this.width = w; - this.height = h; - } - - setTitle(text: string) { - this.title = text; - this.titleProp.setValue(text); - } - - setShowTitle(shouldShow: boolean) { - this.showTitle = shouldShow; - this.showTitleProp.setValue(shouldShow); - } - - setDescription(text: string) { - this.description = text; - this.descrProp.setValue(text); - } - - private buildColor(color: Color, alpha: number) { - const col = - "rgba(" + - color.r * 255 + - "," + - color.g * 255 + - "," + - color.b * 255 + - "," + - alpha + - ")"; - //console.log(col); - return col; - } - - public setPos(x: number, y: number) { - // find diff - const diff = new Vector2(x - this.x, y - this.y); - super.setPos(x, y); - - // do a move - // for (let node of this.nodes) { - // node.move(diff.x, diff.y); - // } - } - - public setFrameRect(rect: Rect) { - this.x = rect.x; - this.y = rect.y; - this.width = rect.width; - this.height = rect.height; - } - - draw(ctx: CanvasRenderingContext2D, renderData: any = null) { - // outer frame - ctx.beginPath(); - ctx.lineWidth = 1; - //ctx.strokeStyle = "rgb(100, 0, 0)"; - ctx.strokeStyle = this.buildColor(this.color, 1); - this.roundRect(ctx, this.x, this.y, this.width, this.height, 1); - ctx.stroke(); - - // handle - const handleSize = this.handleSize; - ctx.beginPath(); - ctx.lineWidth = 1; - //ctx.fillStyle = "rgba(100, 0, 0, 0.5)"; - ctx.fillStyle = this.buildColor(this.color, 0.5); - this.roundRect(ctx, this.x, this.y, this.width, handleSize, 1); - ctx.fill(); - - ctx.beginPath(); - ctx.lineWidth = 1; - //ctx.strokeStyle = "rgb(100, 0, 0, 0.8)"; - ctx.strokeStyle = this.buildColor(this.color, 0.8); - this.roundRect(ctx, this.x, this.y, this.width, handleSize, 1); - ctx.stroke(); - - // body - ctx.beginPath(); - ctx.lineWidth = 2; - //ctx.fillStyle = "rgba(100, 0, 0, 0.2)"; - ctx.fillStyle = this.buildColor(this.color, 0.2); - this.roundRect( - ctx, - this.x, - this.y + handleSize, - this.width, - this.height - handleSize, - 1 - ); - ctx.fill(); - - // title - if (this.showTitle == true) { - ctx.beginPath(); - - const fontSize = 18; // * this.view.zoomFactor; - - ctx.save(); - //ctx.scale(1.0 / this.view.zoomFactor, 1.0 / this.view.zoomFactor); - ctx.setTransform(1, 0, 0, 1, this.view.offset.x, this.view.offset.y); - - //ctx.font = fontSize + "px 'Open Sans'"; - ctx.font = "30px 'Open Sans'"; - ctx.fillStyle = "rgb(240, 240, 240)"; - //let size = ctx.measureText(this.textureChannel.toUpperCase()); - const textX = this.x; - const textY = this.y; - //ctx.fillText("Hello World", textX, textY - 5); - ctx.fillText( - this.title, - textX * this.view.zoomFactor, - (textY - 5) * this.view.zoomFactor - ); - - ctx.restore(); - } - - // debug draw frames - // if (false) { - // const regions = this.getFrameRegions(); - // for (const region of regions) { - // const rect = region.rect; - // ctx.beginPath(); - // ctx.lineWidth = 1; - // ctx.strokeStyle = "rgb(100, 0, 0, 0.8)"; - // ctx.rect(rect.x, rect.y, rect.width, rect.height); - // ctx.stroke(); - // } - // } - } - - public isPointInside(px: number, py: number): boolean { - const regions = this.getFrameRegions(); - for (const region of regions) { - if (region.rect.isPointInside(px, py)) { - return true; - } - } - - // top handle - if ( - px >= this.x && - px <= this.x + this.width && - py >= this.y && - py <= this.y + this.handleSize - ) - return true; - return false; - } - - // MOUSE EVENTS - public mouseDown(evt: MouseDownEvent) { - this.hit = true; - this.dragged = false; - - const px = evt.globalX; - const py = evt.globalY; - - let hitRegion: FrameRegion = null; - const regions = this.getFrameRegions(); - for (const region of regions) { - if (region.rect.isPointInside(px, py)) { - this.dragMode = region.dragMode; - this.xResize = region.xResizeDir; - this.yResize = region.yResizeDir; - - this.dragStartRect = this.getRect(); - hitRegion = region; - break; - } - } - - // topbar - if (hitRegion == null) { - if ( - px >= this.x && - px <= this.x + this.width && - py >= this.y && - py <= this.y + this.handleSize - ) { - this.dragMode = DragMode.HandleTop; - this.xResize = XResizeDir.None; - this.yResize = YResizeDir.None; - - this.dragStartPos = new Vector2(this.x, this.y); - - // capture nodes if alt key isnt pressed - if (!evt.altKey) this.nodes = this.getHoveredNodes(); - - // set cursor - this.view.canvas.style.cursor = "grabbing"; - } - } - } - - public mouseOver(evt: MouseOverEvent) { - const px = evt.globalX; - const py = evt.globalY; - - const hitRegion: FrameRegion = null; - const regions = this.getFrameRegions(); - for (const region of regions) { - if (region.rect.isPointInside(px, py)) { - this.view.canvas.style.cursor = region.cursor; - return; - } - } - - // topbar - if (hitRegion == null) { - if ( - px >= this.x && - px <= this.x + this.width && - py >= this.y && - py <= this.y + this.handleSize - ) { - this.view.canvas.style.cursor = "grab"; - } - } - } - - // return all scene's nodes in this frame - getHoveredNodes(): NodeGraphicsItem[] { - const nodes: NodeGraphicsItem[] = []; - for (const node of this.scene.nodes) { - // node must be entirely inside frame - if ( - node.left >= this.left && - node.right <= this.right && - node.top >= this.top && - node.bottom <= this.bottom - ) { - nodes.push(node); - } - } - - return nodes; - } - - public mouseMove(evt: MouseMoveEvent) { - if (this.hit) { - // movement - if (this.dragMode == DragMode.HandleTop) { - this.move(evt.deltaX, evt.deltaY); - - // move nodes - for (const node of this.nodes) { - node.move(evt.deltaX, evt.deltaY); - } - } - - //todo: clamp size - if (this.dragMode == DragMode.Resize) { - if (this.xResize == XResizeDir.Left) { - this.left += evt.deltaX; - this.width -= evt.deltaX; - } - if (this.xResize == XResizeDir.Right) { - this.width += evt.deltaX; - } - if (this.yResize == YResizeDir.Top) { - this.top += evt.deltaY; - this.height -= evt.deltaY; - } - if (this.yResize == YResizeDir.Bottom) { - this.height += evt.deltaY; - } - - // clamp - this.height = Math.max( - this.height, - this.handleSize + this.resizeHandleSize - ); - - this.width = Math.max(this.width, this.resizeHandleSize * 2); - } - - this.dragged = true; - } - } - - public mouseUp(evt: MouseUpEvent) { - // add undo/redo action - if (this.dragged) { - if (this.dragMode == DragMode.HandleTop) { - const newPos = new Vector2(this.x, this.y); - const items: GraphicsItem[] = [this]; - const oldPosList: Vector2[] = [this.dragStartPos.clone()]; - const newPosList: Vector2[] = [newPos]; - - // reverse diff: new pos to old pos - const diff = new Vector2( - this.dragStartPos.x - newPos.x, - this.dragStartPos.y - newPos.y - ); - - // add all captured nodes - for (const node of this.nodes) { - items.push(node); - // new pos is the current pos - const itemNewPos = new Vector2(node.left, node.top); - // old pos is current pos plus reverse diff - const itemOldPos = Vector2.add(itemNewPos, diff); - - newPosList.push(itemNewPos); - oldPosList.push(itemOldPos); - } - - const action = new MoveItemsAction(items, oldPosList, newPosList); - UndoStack.current.push(action); - } else if (this.dragMode == DragMode.Resize) { - const action = new ResizeFrameAction( - this, - this.dragStartRect.clone(), - this.getRect().clone() - ); - UndoStack.current.push(action); - } - } - - this.hit = false; - this.dragMode = DragMode.None; - this.nodes = []; - - // reset cursor - this.view.canvas.style.cursor = "default"; - } - - getFrameRegions(): FrameRegion[] { - const regions: FrameRegion[] = []; - const frameRect = this.getRect(); - let rect: Rect = null; - let region: FrameRegion = null; - - // CORNERS - - // bottom-right - region = new FrameRegion(); - rect = new Rect(); - rect.x = frameRect.right - this.resizeHandleSize; - rect.y = frameRect.bottom - this.resizeHandleSize; - rect.width = this.resizeHandleSize; - rect.height = this.resizeHandleSize; - region.rect = rect; - region.dragMode = DragMode.Resize; - region.xResizeDir = XResizeDir.Right; - region.yResizeDir = YResizeDir.Bottom; - region.cursor = "nwse-resize"; - regions.push(region); - - // bottom-left - region = new FrameRegion(); - rect = new Rect(); - rect.x = frameRect.left; - rect.y = frameRect.bottom - this.resizeHandleSize; - rect.width = this.resizeHandleSize; - rect.height = this.resizeHandleSize; - region.rect = rect; - region.dragMode = DragMode.Resize; - region.xResizeDir = XResizeDir.Left; - region.yResizeDir = YResizeDir.Bottom; - region.cursor = "nesw-resize"; - regions.push(region); - - // top-left - region = new FrameRegion(); - rect = new Rect(); - rect.x = frameRect.left; - rect.y = frameRect.top; - rect.width = this.resizeHandleSize; - rect.height = this.resizeHandleSize; - region.rect = rect; - region.dragMode = DragMode.Resize; - region.xResizeDir = XResizeDir.Left; - region.yResizeDir = YResizeDir.Top; - region.cursor = "nw-resize"; - regions.push(region); - - // top-right - region = new FrameRegion(); - rect = new Rect(); - rect.x = frameRect.right - this.resizeHandleSize; - rect.y = frameRect.top; - rect.width = this.resizeHandleSize; - rect.height = this.resizeHandleSize; - region.rect = rect; - region.dragMode = DragMode.Resize; - region.xResizeDir = XResizeDir.Right; - region.yResizeDir = YResizeDir.Top; - region.cursor = "ne-resize"; - regions.push(region); - - // SIDES - - // left - region = new FrameRegion(); - rect = new Rect(); - rect.x = frameRect.left; - rect.y = frameRect.top; - rect.width = this.resizeHandleSize; - rect.height = frameRect.height; - region.rect = rect; - region.dragMode = DragMode.Resize; - region.xResizeDir = XResizeDir.Left; - region.yResizeDir = YResizeDir.None; - region.cursor = "w-resize"; - regions.push(region); - - // right - region = new FrameRegion(); - rect = new Rect(); - rect.x = frameRect.right - this.resizeHandleSize; - rect.y = frameRect.top; - rect.width = this.resizeHandleSize; - rect.height = frameRect.height; - region.rect = rect; - region.dragMode = DragMode.Resize; - region.xResizeDir = XResizeDir.Right; - region.yResizeDir = YResizeDir.None; - region.cursor = "e-resize"; - regions.push(region); - - // top - region = new FrameRegion(); - rect = new Rect(); - rect.x = frameRect.left; - rect.y = frameRect.top; - rect.width = frameRect.width; - rect.height = this.resizeHandleSize * 0.5; // top is thinner - region.rect = rect; - region.dragMode = DragMode.Resize; - region.xResizeDir = XResizeDir.None; - region.yResizeDir = YResizeDir.Top; - region.cursor = "n-resize"; - regions.push(region); - - // bottom - region = new FrameRegion(); - rect = new Rect(); - rect.x = frameRect.left; - rect.y = frameRect.bottom - this.resizeHandleSize; - rect.width = frameRect.width; - rect.height = this.resizeHandleSize; - region.rect = rect; - region.dragMode = DragMode.Resize; - region.xResizeDir = XResizeDir.None; - region.yResizeDir = YResizeDir.Bottom; - region.cursor = "s-resize"; - regions.push(region); - - // this is handles separately - // TOPBAR - // region = new FrameRegion(); - // rect = new Rect(); - // rect.x = frameRect.right - this.resizeHandleSize; - // rect.y = frameRect.bottom - this.resizeHandleSize; - // rect.width = this.resizeHandleSize; - // rect.height = this.resizeHandleSize; - // region.rect = rect; - // region.dragMode = DragMode.HandleTop; - // region.xResizeDir = XResizeDir.None; - // region.yResizeDir = YResizeDir.None; - // regions.push(region); - - return regions; - } -} diff --git a/src/lib/scene/graphicsitem.ts b/src/lib/scene/graphicsitem.ts deleted file mode 100644 index 5f7fd0b9..00000000 --- a/src/lib/scene/graphicsitem.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { NodeScene } from "../scene"; -import { Rect, Vector2 } from "./view"; - -export class MouseEvent { - // scene space - globalX: number; - globalY: number; - - localX: number; - localY: number; - - mouseButton: number; - - // modifiers - shiftKey = false; - altKey = false; - ctrlKey = false; - - // default is accepted - private accepted = true; - public accept() { - this.accepted = true; - } - - public reject() { - this.accepted = false; - } - - public get isAccepted() { - return this.accepted; - } - - public get isRejected() { - return !this.accepted; - } -} - -export class MouseDownEvent extends MouseEvent {} -export class MouseMoveEvent extends MouseEvent { - deltaX: number; - deltaY: number; -} -export class MouseUpEvent extends MouseEvent {} -export class MouseOverEvent extends MouseEvent {} - -export class HoverEvent extends MouseEvent {} - -export class GraphicsItem { - scene!: NodeScene; - protected visible = true; - - protected x = 0; - protected y = 0; - protected width: number; - protected height: number; - - public constructor() { - //this.scene = scene; - //scene.addItem(this); - this.width = 1; - this.height = 1; - } - - // sets top-left - public setPos(x: number, y: number) { - this.x = x; - this.y = y; - } - - public getPos() { - return new Vector2(this.x, this.y); - } - - public setSize(w: number, h: number) { - this.width = w; - this.height = h; - } - - public setScene(scene: NodeScene) { - this.scene = scene; - } - - public isPointInside(px: number, py: number): boolean { - if ( - px >= this.x && - px <= this.x + this.width && - py >= this.y && - py <= this.y + this.height - ) - return true; - return false; - } - - public get left() { - return this.x; - } - - public set left(value) { - this.x = value; - } - - public get top() { - return this.y; - } - - public set top(value) { - this.y = value; - } - - public get right() { - return this.x + this.width; - } - - public get bottom() { - return this.y + this.height; - } - - public intersectsRect(other: Rect) { - if (this.left > other.right) return false; - if (this.right < other.left) return false; - if (this.bottom < other.top) return false; - if (this.top > other.bottom) return false; - - return true; - } - - public intersects(other: GraphicsItem) { - if (this.left > other.right) return false; - if (this.right < other.left) return false; - if (this.bottom < other.top) return false; - if (this.top > other.bottom) return false; - - return true; - } - - public getRect(): Rect { - const rect = new Rect(); - rect.x = this.x; - rect.y = this.y; - rect.width = this.width; - rect.height = this.height; - - return rect; - } - - public setCenter(x: number, y: number) { - this.x = x - this.width / 2; - this.y = y - this.height / 2; - } - - public centerX(): number { - return this.x + this.width / 2; - } - - public centerY(): number { - return this.y + this.height / 2; - } - - public getWidth(): number { - return this.width; - } - - public getHeight(): number { - return this.height; - } - - public move(dx: number, dy: number) { - this.x += dx; - this.y += dy; - } - - // UTILITIES - // https://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas - roundRect(ctx: CanvasRenderingContext2D, x, y, w, h, r) { - if (w < 2 * r) r = w / 2; - if (h < 2 * r) r = h / 2; - ctx.beginPath(); - ctx.moveTo(x + r, y); - ctx.arcTo(x + w, y, x + w, y + h, r); - ctx.arcTo(x + w, y + h, x, y + h, r); - ctx.arcTo(x, y + h, x, y, r); - ctx.arcTo(x, y, x + w, y, r); - ctx.closePath(); - //ctx.stroke(); - } - - // to be overriden - public draw(ctx: CanvasRenderingContext2D, renderData: any = null) {} - - // MOUSE EVENTS - - // STANDARD MOUSE EVENTS - public mouseDown(evt: MouseDownEvent) {} - public mouseMove(evt: MouseMoveEvent) {} - public mouseUp(evt: MouseUpEvent) {} - - // called every frame the mouse is over this object - public mouseOver(evt: MouseOverEvent) {} -} diff --git a/src/lib/scene/navigationgraphicsitem.ts b/src/lib/scene/navigationgraphicsitem.ts deleted file mode 100644 index 1c9e2129..00000000 --- a/src/lib/scene/navigationgraphicsitem.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { SocketGraphicsItem } from "./socketgraphicsitem"; -import { - GraphicsItem, - MouseDownEvent, - MouseMoveEvent, - MouseUpEvent -} from "./graphicsitem"; -import { Vector2 } from "./view"; -import { MoveItemsAction } from "../actions/moveItemsaction"; -import { UndoStack } from "../undostack"; - -export class NavigationGraphicsItem extends GraphicsItem { - id!: string; - public socketA!: SocketGraphicsItem; - public socketB!: SocketGraphicsItem; - - label: string; - hit: boolean; - dragged: boolean; - dragStartPos: Vector2; - - constructor() { - super(); - this.label = ""; - - this.hit = false; - this.dragged = false; - - this.width = 10; - this.height = 10; - } - - setLabel(label: string) { - this.label = label; - } - - draw(ctx: CanvasRenderingContext2D, renderData: any = null) { - ctx.beginPath(); - ctx.strokeStyle = "rgb(200, 200, 200)"; - ctx.lineWidth = 4; - ctx.arc(this.centerX(), this.centerY(), this.width, 0, Math.PI * 2); - ctx.fill(); - ctx.strokeStyle = "rgb(200, 0, 0)"; - ctx.stroke(); - - ctx.fillText(this.label, this.x + 5, this.y); - } - - // MOUSE EVENTS - public mouseDown(evt: MouseDownEvent) { - this.hit = true; - this.dragged = false; - this.dragStartPos = new Vector2(this.x, this.y); - } - - public mouseMove(evt: MouseMoveEvent) { - if (this.hit) { - // movement - this.move(evt.deltaX, evt.deltaY); - this.dragged = true; - } - } - - public mouseUp(evt: MouseUpEvent) { - this.hit = false; - - // add undo/redo - const newPos = new Vector2(this.x, this.y); - - if (this.dragged) { - const action = new MoveItemsAction( - [this], - [this.dragStartPos.clone()], - [newPos] - ); - - UndoStack.current.push(action); - } - - this.dragged = false; - } -} diff --git a/src/lib/scene/nodegraphicsitem.ts b/src/lib/scene/nodegraphicsitem.ts deleted file mode 100644 index 5b308241..00000000 --- a/src/lib/scene/nodegraphicsitem.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { SocketGraphicsItem, SocketType } from "./socketgraphicsitem"; -import { ImageCanvas } from "../designer/imagecanvas"; -import { - GraphicsItem, - MouseDownEvent, - MouseMoveEvent, - MouseUpEvent -} from "./graphicsitem"; -import { NodeScene } from "../scene"; -import { Vector2 } from "./view"; -import { MoveItemsAction } from "../actions/moveItemsaction"; -import { UndoStack } from "../undostack"; - -export class NodeGraphicsItemRenderState { - hovered = false; - selected = false; -} - -export class NodeGraphicsItem extends GraphicsItem { - id!: string; - sockets: SocketGraphicsItem[] = []; - public title: string; - thumbnail!: HTMLImageElement; - imageCanvas: ImageCanvas; - - processingTime: number; - - hit: boolean; - - // albedo, normal, height, etc... - textureChannel: string; - - dragStartPos: Vector2; - - constructor(title: string) { - super(); - this.width = 100; - this.height = 100; - this.title = title; - this.imageCanvas = new ImageCanvas(); - this.processingTime = 0; - this.hit = false; - } - - public setScene(scene: NodeScene) { - this.scene = scene; - - for (const sock of this.sockets) sock.setScene(scene); - } - - public setTextureChannel(name: string) { - this.textureChannel = name; - } - - public clearTextureChannel() { - this.textureChannel = null; - } - - public setThumbnail(thumbnail: HTMLImageElement) { - this.thumbnail = thumbnail; - } - - public move(dx: number, dy: number) { - this.x += dx; - this.y += dy; - for (const sock of this.sockets) { - sock.move(dx, dy); - } - } - - draw(ctx: CanvasRenderingContext2D, renderData: any) { - const renderState = renderData; - - // border - if (renderState.selected) { - ctx.strokeStyle = "rgb(255, 255, 255)"; - ctx.beginPath(); - ctx.lineWidth = 8; - //ctx.rect(this.x, this.y, this.width, this.height); - this.roundRect(ctx, this.x, this.y, this.width, this.height, 2); - ctx.stroke(); - } - - // background - ctx.beginPath(); - ctx.fillStyle = "rgb(0, 0, 0)"; - ctx.rect(this.x, this.y, this.width, this.height); - ctx.fill(); - - // thumbnail if any - if (this.thumbnail) { - //ctx.drawImage(this.thumbnail,this.x, this.y, this.width, this.height); - } - - ctx.drawImage( - this.imageCanvas.canvas, - this.x, - this.y, - this.width, - this.height - ); - - // title - if (!renderState.hovered) { - ctx.beginPath(); - ctx.fillStyle = "rgb(0,0,0)"; - ctx.rect(this.x, this.y, this.width, 20); - ctx.fill(); - - ctx.beginPath(); - //ctx.font = "14px monospace"; - ctx.font = "bold 9px 'Open Sans'"; - ctx.fillStyle = "rgb(255,255,255)"; - const size = ctx.measureText(this.title); - const textX = this.centerX() - size.width / 2; - const textY = this.y + 14; - ctx.fillText(this.title, textX, textY); - } - - // DRAW SHAPE - ctx.beginPath(); - ctx.lineWidth = 4; - // if (renderState.selected) ctx.strokeStyle = "rgb(255, 255, 255)"; - // else ctx.strokeStyle = "rgb(0, 0, 0)"; - ctx.strokeStyle = "rgb(0, 0, 0)"; - //ctx.rect(this.x, this.y, this.width, this.height); - this.roundRect(ctx, this.x, this.y, this.width, this.height, 2); - ctx.stroke(); - - for (const sock of this.sockets) { - sock.draw(ctx, renderState); - } - - // processing time - ctx.beginPath(); - let procTime = "calculating.."; - if (this.processingTime >= 0) procTime = this.processingTime + "ms"; - - ctx.font = "bold 9px 'Open Sans'"; - ctx.fillStyle = "rgb(255,255,255)"; - const size = ctx.measureText(procTime); - const textX = this.centerX() - size.width / 2; - const textY = this.y + this.height + 14; - ctx.fillText(procTime, textX, textY); - - // texture channel - if (this.textureChannel) { - ctx.beginPath(); - //ctx.font = "14px monospace"; - ctx.font = "12px 'Open Sans'"; - ctx.fillStyle = "rgb(200, 255, 200)"; - const size = ctx.measureText(this.textureChannel.toUpperCase()); - const textX = this.centerX() - size.width / 2; - //const textY = this.y + this.height + 14; - - const textY = this.y - 12; - ctx.fillText(this.textureChannel.toUpperCase(), textX, textY); - } - } - - public setPos(x: number, y: number) { - super.setPos(x, y); - this.sortSockets(); - } - - public setCenter(x: number, y: number) { - super.setCenter(x, y); - this.sortSockets(); - } - - public sortSockets() { - let socks = this.getInSockets(); - - // top and bottom padding for sockets - const pad = socks.length < 5 ? 10 : 0; - - // sort in sockets - let incr = (this.height - pad * 2) / socks.length; - let mid = incr / 2.0; - let i = 0; - for (const sock of socks) { - const y = pad + i * incr + mid; - const x = this.x; - sock.setCenter(x, this.y + y); - i++; - } - - // sort out sockets - socks = this.getOutSockets(); - incr = (this.height - pad * 2) / socks.length; - mid = incr / 2.0; - i = 0; - for (const sock of socks) { - const y = pad + i * incr + mid; - const x = this.x + this.width; - sock.setCenter(x, this.y + y); - i++; - } - } - - getInSockets() { - const array: SocketGraphicsItem[] = []; - for (const sock of this.sockets) { - if (sock.socketType == SocketType.In) array.push(sock); - } - - return array; - } - - getInSocketByName(name: string): SocketGraphicsItem { - for (const sock of this.sockets) { - if (sock.socketType == SocketType.In) - if (sock.title == name) - //todo: separate title from name - return sock; - } - - return null; - } - - getOutSockets() { - const array: SocketGraphicsItem[] = []; - for (const sock of this.sockets) { - if (sock.socketType == SocketType.Out) array.push(sock); - } - - return array; - } - - getOutSocketByName(name: string): SocketGraphicsItem { - // blank or empty name means first out socket - if (!name) { - const socks = this.getOutSockets(); - if (socks.length > 0) return socks[0]; - else { - console.log( - "[warning] attempting to get output socket from node with no output sockets" - ); - return null; - } - } - - for (const sock of this.sockets) { - if (sock.socketType == SocketType.Out) - if (sock.title == name) - //todo: separate title from name - return sock; - } - - return null; - } - - // adds socket to node - public addSocket(name: string, id: string, type: SocketType) { - const sock = new SocketGraphicsItem(); - sock.id = id; - sock.title = name; - sock.node = this; - sock.socketType = type; - this.sockets.push(sock); - - this.sortSockets(); - } - - // MOUSE EVENTS - public mouseDown(evt: MouseDownEvent) { - this.hit = true; - this.dragStartPos = new Vector2(this.x, this.y); - } - - public mouseMove(evt: MouseMoveEvent) { - if (this.hit) { - // movement - this.move(evt.deltaX, evt.deltaY); - } - } - - public mouseUp(evt: MouseUpEvent) { - this.hit = false; - - // add undo/redo - const newPos = new Vector2(this.x, this.y); - - if (newPos.x != this.dragStartPos.x || newPos.y != this.dragStartPos.y) { - const action = new MoveItemsAction( - [this], - [this.dragStartPos.clone()], - [newPos] - ); - - UndoStack.current.push(action); - } - } -} diff --git a/src/lib/scene/selectiongraphicsitem.ts b/src/lib/scene/selectiongraphicsitem.ts deleted file mode 100644 index 88a74abf..00000000 --- a/src/lib/scene/selectiongraphicsitem.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { SocketGraphicsItem } from "./socketgraphicsitem"; -import { - GraphicsItem, - MouseDownEvent, - MouseMoveEvent, - MouseUpEvent -} from "./graphicsitem"; -import { SceneView, Vector2, Rect } from "./view"; -import { Color } from "../designer/color"; -import { - IPropertyHolder, - Property, - StringProperty -} from "../designer/properties"; -import { NodeScene } from "../scene"; -import { MoveItemsAction } from "../actions/moveItemsaction"; -import { UndoStack } from "../undostack"; -import { FrameGraphicsItem } from "./framegraphicsitem"; - -// https://stackoverflow.com/questions/5026961/html5-canvas-ctx-filltext-wont-do-line-breaks -export class SelectionGraphicsItem extends GraphicsItem { - view: SceneView; - color: Color; - - padding: number; - fontHeight: number; - - tooSmallSize: number; - - hit: boolean; - dragged: boolean; - items: GraphicsItem[]; - draggableItems: GraphicsItem[]; // list with items and frame's items for dragging - itemsDragStartPos: Vector2[]; - - constructor(scene: NodeScene, view: SceneView) { - super(); - this.scene = scene; - this.view = view; - this.color = new Color(0.9, 0.9, 0.9); - this.items = []; - this.itemsDragStartPos = []; - - this.hit = false; - this.dragged = false; - - this.padding = 5; - this.fontHeight = 20; - - this.tooSmallSize = 5; - } - - public isPointInside(px: number, py: number): boolean { - //todo: loop through child items rect to see if a hit is made - for (const item of this.items) { - if ( - px >= item.left && - px <= item.left + item.getWidth() && - py >= item.top && - py <= item.top + item.getHeight() - ) - return true; - } - - return false; - } - - draw(ctx: CanvasRenderingContext2D, renderData: any = null) { - // should only display if hit or has items - if (this.hit == false && this.items.length == 0) return; - - const width = this.width; - const height = this.height; - - if (this.items.length == 0) { - if (!this.isTooSmall()) { - // stroke bounding rect - ctx.beginPath(); - ctx.lineWidth = 3; - ctx.strokeStyle = "rgb(250, 250, 250)"; - //this.roundRect(ctx, this.x, this.y, width, height, 1); - ctx.rect(this.x, this.y, width, height); - - ctx.setLineDash([5, 3]); - ctx.stroke(); - ctx.setLineDash([]); - - // if hit, then mouse is being dragged, these items are temporary - const items = this.getHitItems(); - this.drawSelectedItems(items, ctx); - } - } else { - //this.drawSelectedItems(this.items, ctx); - } - } - - drawSelectedItems(items: GraphicsItem[], ctx: CanvasRenderingContext2D) { - for (const item of items) { - ctx.beginPath(); - ctx.lineWidth = 2; - ctx.strokeStyle = "rgba(250, 250, 250)"; - //this.roundRect(ctx, this.x, this.y, width, height, 1); - // ctx.rect(item.left, item.top, item.getWidth(), item.getHeight()); - const rect = item.getRect(); - rect.expand(15); - ctx.rect(rect.left, rect.top, rect.width, rect.height); - - ctx.stroke(); - - ctx.fillStyle = "rgba(255, 255, 255, 0.1)"; - ctx.rect(rect.left, rect.top, rect.width, rect.height); - ctx.fill(); - } - ctx.setLineDash([]); - } - - /** - * If user drags left of up, the width or height becomes negative - * This returns a rect with positive dimensions - */ - getPositiveRect(): Rect { - const rect = new Rect(); - rect.x = this.x; - rect.y = this.y; - - rect.width = Math.abs(this.width); - rect.height = Math.abs(this.height); - - if (this.width < 0) rect.x += this.width; - if (this.height < 0) rect.y += this.height; - - return rect; - } - - // for manually setting the hit items - setHitItems(items: GraphicsItem[]) { - this.items = items; - this.draggableItems = this.getDraggableHitItems(this.items); - } - - getHitItems(): GraphicsItem[] { - const items: GraphicsItem[] = []; - const rect = this.getPositiveRect(); - - for (const node of this.scene.nodes) { - if (node.intersectsRect(rect)) { - items.push(node); - } - } - - for (const item of this.scene.comments) { - if (item.intersectsRect(rect)) { - items.push(item); - } - } - - // Frames are treated differently - // check if any of the sides were hit - // by the selection box for a valid selection - for (const item of this.scene.frames) { - const frame = item; - const regions = frame.getFrameRegions(); - for (const region of regions) { - if (region.rect.intersects(rect)) { - items.push(item); - break; - } - } - } - - for (const item of this.scene.navigations) { - if (item.intersectsRect(rect)) { - items.push(item); - } - } - - return items; - } - - // todo: ultra slow! - // does frame * scenenodes check per frame - getDraggableHitItems(hitItems: GraphicsItem[]): GraphicsItem[] { - const items: Set = new Set(); - - hitItems.forEach((i: GraphicsItem) => { - items.add(i); - - if (i instanceof FrameGraphicsItem) { - const captured = (i).getHoveredNodes(); - captured.forEach(c => items.add(c)); - } - }); - - const results: GraphicsItem[] = []; - items.forEach((i: GraphicsItem) => results.push(i)); - - return results; - } - - // MOUSE EVENTS - - // This graphics item has two phases - // the first phase is about selecting items - // the second phase is about dragging - // after the first phase, items will not be empty - // if there is a mouse event and items is not empty - // then its in drag mode - public mouseDown(evt: MouseDownEvent) { - this.hit = true; - this.dragged = false; - - if (this.items.length > 0) { - this.captureDragStarts(); - } else { - this.x = evt.globalX; - this.y = evt.globalY; - this.scene.setSelectedItems([]); - } - } - - public mouseMove(evt: MouseMoveEvent) { - if (this.items.length > 0) { - for (const item of this.draggableItems) { - item.move(evt.deltaX, evt.deltaY); - } - this.dragged = true; - } else if (this.hit) { - // movement - this.width += evt.deltaX; - this.height += evt.deltaY; - } - } - - public mouseUp(evt: MouseUpEvent) { - this.hit = false; - if (this.items.length == 0 && !this.isTooSmall()) { - this.items = this.getHitItems(); - this.scene.setSelectedItems(this.items); - this.draggableItems = this.getDraggableHitItems(this.items); - } else { - //todo: check for movement - if (this.dragged) this.createUndoAction(); - } - - this.dragged = false; - } - - // UNDO-REDO - captureDragStarts() { - this.itemsDragStartPos = []; - for (const item of this.draggableItems) { - const pos = new Vector2(item.left, item.top); - this.itemsDragStartPos.push(pos); - } - } - - isTooSmall(): boolean { - return ( - Math.abs(this.width) < this.tooSmallSize || - Math.abs(this.height) < this.tooSmallSize - ); - } - - createUndoAction() { - const newPosList = []; - for (const item of this.draggableItems) { - const pos = new Vector2(item.left, item.top); - newPosList.push(pos); - } - - const action = new MoveItemsAction( - this.draggableItems, - this.itemsDragStartPos, - newPosList - ); - - UndoStack.current.push(action); - } -} diff --git a/src/lib/scene/socketgraphicsitem.ts b/src/lib/scene/socketgraphicsitem.ts deleted file mode 100644 index 56fe288b..00000000 --- a/src/lib/scene/socketgraphicsitem.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { - GraphicsItem, - MouseDownEvent, - MouseMoveEvent, - MouseUpEvent -} from "./graphicsitem"; -import { - NodeGraphicsItem, - NodeGraphicsItemRenderState -} from "./nodegraphicsitem"; -import { ConnectionGraphicsItem } from "./connectiongraphicsitem"; -import { AddConnectionAction } from "../actions/addconnectionaction"; -import { UndoStack } from "../undostack"; -import { RemoveConnectionAction } from "../actions/removeconnectionaction"; -import { - ConnectionSwitchAction, - SwitchConnectionAction -} from "../actions/switchconnectionaction"; - -export enum SocketType { - In, - Out -} - -export class SocketGraphicsItem extends GraphicsItem { - public id!: string; - public title!: string; - public node!: NodeGraphicsItem; - public socketType!: SocketType; - radius = 8; - - // only in sockets store the connection - // since outsockets can have multiple connections - //public con:ConnectionGraphicsItem; - conns: ConnectionGraphicsItem[] = []; - - hit: boolean; - hitSocket: SocketGraphicsItem; - hitConnection: ConnectionGraphicsItem; // for removal - mouseDragX: number; - mouseDragY: number; - - addConnection(con: ConnectionGraphicsItem) { - this.conns.push(con); - } - removeConnection(con: ConnectionGraphicsItem) { - this.conns.splice(this.conns.indexOf(con), 1); - } - getConnection(index: number): ConnectionGraphicsItem { - return this.conns[index]; - } - - // retruns a connection where the outSocket == socketA - // returns null if no result is found - getConnectionFrom(socketA: SocketGraphicsItem) { - for (const con of this.conns) { - if (con.socketA == socketA) return con; - } - - return null; - } - - // retruns a connection where the inSocket == socketB - // returns null if no result is found - getConnectionTo(socketB: SocketGraphicsItem) { - for (const con of this.conns) { - if (con.socketB == socketB) return con; - } - - return null; - } - - hasConnections() { - return this.conns.length > 0; - } - - constructor() { - super(); - this.width = this.radius * 2; - this.height = this.radius * 2; - - this.hit = false; - this.hitSocket = null; - } - - draw(ctx: CanvasRenderingContext2D, renderData: any = null) { - const renderState = renderData; - - //console.log(this.width); - ctx.lineWidth = 3; - //ctx.rect(this.x, this.y, this.width, this.height); - ctx.beginPath(); - ctx.fillStyle = "rgb(150,150,150)"; - ctx.arc(this.centerX(), this.centerY(), this.radius, 0, 2 * Math.PI); - ctx.fill(); - - // border - ctx.beginPath(); - ctx.arc(this.centerX(), this.centerY(), this.radius, 0, 2 * Math.PI); - ctx.strokeStyle = "rgb(0, 0, 0)"; - ctx.stroke(); - - // draw inner dot if connected - if (this.hasConnections()) { - //console.log("con"); - ctx.beginPath(); - ctx.fillStyle = "rgb(100,100,100)"; - ctx.arc(this.centerX(), this.centerY(), this.radius / 3, 0, 2 * Math.PI); - ctx.fill(); - } - - this.drawActiveConnection(ctx); - - // draw text - if (renderState.hovered) { - ctx.fillStyle = "rgb(150,150,150)"; - ctx.font = "9px 'Open Sans'"; - if (this.socketType == SocketType.Out) { - const w = ctx.measureText(this.title).width; - ctx.fillText(this.title, this.x + this.width + 4, this.y + 12); - } else { - const w = ctx.measureText(this.title).width; - ctx.fillText(this.title, this.x - 4 - w, this.y + 12); - } - } - } - - drawActiveConnection(ctx: CanvasRenderingContext2D) { - if (this.hitSocket) { - const mouseX = this.mouseDragX; - const mouseY = this.mouseDragY; - - ctx.beginPath(); - ctx.strokeStyle = "rgb(200, 200, 200)"; - ctx.lineWidth = 4; - ctx.moveTo(this.hitSocket.centerX(), this.hitSocket.centerY()); - - if (this.hitSocket.socketType == SocketType.Out) { - ctx.bezierCurveTo( - this.hitSocket.centerX() + 60, - this.hitSocket.centerY(), // control point 1 - mouseX - 60, - mouseY, - mouseX, - mouseY - ); - } else { - ctx.bezierCurveTo( - this.hitSocket.centerX() - 60, - this.hitSocket.centerY(), // control point 1 - mouseX + 60, - mouseY, - mouseX, - mouseY - ); - } - - ctx.setLineDash([5, 3]); - ctx.stroke(); - ctx.setLineDash([]); - } - } - - // MOUSE EVENTS - public mouseDown(evt: MouseDownEvent) { - this.hit = true; - this.hitSocket = null; - this.mouseDragX = evt.globalX; - this.mouseDragY = evt.globalY; - - // if socket is an in socket with a connection, make hitsocket the connected out socket - if (this.socketType == SocketType.In && this.hasConnections()) { - this.hitSocket = this.getConnection(0).socketA; // insockets should only have one connection - // store connection for removal as well - this.hitConnection = this.getConnection(0); - // console.log("hit connection"); - // console.log(this.hitConnection); - } else this.hitSocket = this; - } - - public mouseMove(evt: MouseMoveEvent) { - if (this.hit) { - // movement - //this.move(evt.deltaX, evt.deltaY); - this.mouseDragX = evt.globalX; - this.mouseDragY = evt.globalY; - } - } - - public mouseUp(evt: MouseUpEvent) { - const mouseX = evt.globalX; - const mouseY = evt.globalY; - - // for undo-redo - const movedCons: ConnectionGraphicsItem[] = []; - const actions: ConnectionSwitchAction[] = []; - - if (this.hitSocket) { - // if potential cycle in graph is found, cancel connection action quietly - const closeSock: SocketGraphicsItem = this.scene.getHitSocket( - mouseX, - mouseY - ); - - // check if a cyclical connection is about to be added, - // this can only happen when forming a new connection so no need to check - // for other conditions (existing connection being removed, etc...) - // - // this means one socket has to be SocketType.Out and the other should be SocketType.In - let remainsDAG = false; - - if (closeSock) { - if ( - this.hitSocket.socketType == SocketType.Out && - closeSock.socketType == SocketType.In - ) - remainsDAG = - this.scene.remainsDAG(this.hitSocket.node, closeSock.node) || - remainsDAG; - else if ( - this.hitSocket.socketType == SocketType.In && - closeSock.socketType == SocketType.Out - ) - remainsDAG = - this.scene.remainsDAG(closeSock.node, this.hitSocket.node) || - remainsDAG; - - if (!remainsDAG) { - this.hit = false; - this.hitSocket = null; - this.hitConnection = null; - - return; - } - } - - // remove previous connection - // this block creates a new connection regardless of the outcome - if (this.hitConnection) { - this.scene.removeConnection(this.hitConnection); - - // let action = new RemoveConnectionAction( - // this.scene, - // this.hitConnection - // ); - // UndoStack.current.push(action); - - movedCons.push(this.hitConnection); - actions.push(ConnectionSwitchAction.Remove); - - this.hitConnection = null; - } - - if ( - closeSock && - closeSock != this.hitSocket && - closeSock.socketType != this.hitSocket.socketType && - closeSock.node != this.hitSocket.node - ) { - // close socket - const con: ConnectionGraphicsItem = new ConnectionGraphicsItem(); - // out socket should be on the left, socketA - if (this.hitSocket.socketType == SocketType.Out) { - // out socket - con.socketA = this.hitSocket; - con.socketB = closeSock; - - // close sock is an inSocket which means it should only have one connection - // remove current connection from inSocket - if (closeSock.hasConnections()) { - const removeCon = closeSock.getConnection(0); - this.scene.removeConnection(removeCon); - movedCons.push(removeCon); - actions.push(ConnectionSwitchAction.Remove); - - // let action = new RemoveConnectionAction( - // this.scene, - // removeCon - // ); - // UndoStack.current.push(action); - } - } else { - // in socket - con.socketA = closeSock; - con.socketB = this.hitSocket; - } - - // link connection - //con.socketA.con = con; - //con.socketB.con = con; - - this.scene.addConnection(con); - movedCons.push(con); - actions.push(ConnectionSwitchAction.Add); - - // let action = new AddConnectionAction(this.scene, con); - // UndoStack.current.push(action); - } else if (!closeSock) { - // delete connection if hit node is an insock - // if we're here it means one of 2 things: - // 1: a new connection failed to form - // 2: we're breaking a previously formed connection, which can only be done - // by dragging from an insock that already has a connection - - if (this.hitSocket.socketType == SocketType.Out) { - /* - if (this.hitSocket.hasConnections()) { - // remove connection - //let con = this.hitSocket.con; - this.removeConnection(this.hitSocket.getConnectionFrom(this.hitSocket)); - } - */ - - if (this.hitConnection) { - this.scene.removeConnection(this.hitConnection); - - movedCons.push(this.hitConnection); - actions.push(ConnectionSwitchAction.Remove); - // let action = new RemoveConnectionAction( - // this.scene, - // this.hitConnection - // ); - // UndoStack.current.push(action); - } - } - } - } - - this.hit = false; - this.hitSocket = null; - this.hitConnection = null; - - // undo-redo - const action = new SwitchConnectionAction(this.scene, movedCons, actions); - UndoStack.current.push(action); - } -} diff --git a/src/lib/scene/view.ts b/src/lib/scene/view.ts deleted file mode 100644 index 7211d589..00000000 --- a/src/lib/scene/view.ts +++ /dev/null @@ -1,385 +0,0 @@ -// https://github.com/freegroup/draw2d/blob/master/src/Canvas.js - -//https://github.com/jgraph/mxgraph/blob/master/javascript/src/js/view/mxGraph.js#L7810 - -// https://bitbucket.org/nclsbrwn/texturedesigner/src/master/src/Designer/scene.ts?mode=edit&spa=0&at=master&fileviewer=file-view-default - -// https://bitbucket.org/nclsbrwn/texturedesigner/src/master/ - -// https://stackoverflow.com/questions/45528111/javascript-canvas-map-style-point-zooming/45528455#45528455 - -// get local mouse position -function _getMousePos(canvas, evt) { - const rect = canvas.getBoundingClientRect(); - return new Vector2(evt.clientX - rect.left, evt.clientY - rect.top); -} - -export class Vector2 { - x: number; - y: number; - - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } - - clone(): Vector2 { - return new Vector2(this.x, this.y); - } - - static add(a: Vector2, b: Vector2): Vector2 { - return new Vector2(a.x + b.x, a.y + b.y); - } - - static subtract(a: Vector2, b: Vector2): Vector2 { - return new Vector2(a.x - b.x, a.y - b.y); - } -} - -export class Rect { - public x = 0; - public y = 0; - public width: number; - public height: number; - - color: string; - - public constructor() { - //this.scene = scene; - //scene.addItem(this); - this.width = 1; - this.height = 1; - this.color = "rgb(255, 50, 50)"; - } - - public setSize(w: number, h: number) { - this.width = w; - this.height = h; - } - - public isPointInside(px: number, py: number): boolean { - if ( - px >= this.x && - px <= this.x + this.width && - py >= this.y && - py <= this.y + this.height - ) - return true; - return false; - } - - public setCenter(x: number, y: number) { - this.x = x - this.width / 2; - this.y = y - this.height / 2; - } - - public centerX(): number { - return this.x + this.width / 2; - } - - public centerY(): number { - return this.y + this.height / 2; - } - - public move(dx: number, dy: number) { - this.x += dx; - this.y += dy; - } - - public get left() { - return this.x; - } - - public get top() { - return this.y; - } - - public get right() { - return this.x + this.width; - } - - public get bottom() { - return this.y + this.height; - } - - public get center() { - return new Vector2(this.centerX(), this.centerY()); - } - - public intersects(other: Rect) { - if (this.left > other.right) return false; - if (this.right < other.left) return false; - if (this.bottom < other.top) return false; - if (this.top > other.bottom) return false; - - return true; - } - - public expand(uniformSize: number) { - const halfSize = uniformSize * 0.5; - - // assume it's a rect with a positive area - this.x -= halfSize; - this.y -= halfSize; - this.width += halfSize * 2; - this.height += halfSize * 2; - } - - public expandByRect(rect: Rect) { - // assume it's a rect with a positive area - this.x = Math.min(this.x, rect.x); - this.y = Math.min(this.y, rect.y); - this.width = Math.max(this.width, rect.width); - this.height = Math.max(this.height, rect.height); - } - - clone(): Rect { - const rect = new Rect(); - rect.x = this.x; - rect.y = this.y; - rect.width = this.width; - rect.height = this.height; - - return rect; - } -} - -/* - This class handles panning and zooming of scene - Tracks mouse movement, position and clicks - Also converts from scene space to screen space and - vice versa. -*/ -export class SceneView { - canvas: HTMLCanvasElement; - context: CanvasRenderingContext2D; - - // screen/canvas space - globalMousePos: Vector2; - - mousePos: Vector2; - prevMousePos: Vector2; - mouseDownPos: Vector2; // pos of last mouse down - mouseDragDiff: Vector2; // mouse drag diff - - zoomFactor: number; - offset: Vector2; - - panning: boolean; - panStart: SVGPoint; - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.context = this.canvas.getContext("2d"); - - const self = this; - canvas.addEventListener("mousemove", function(evt: MouseEvent) { - self.onMouseMove(evt); - }); - canvas.addEventListener("mousedown", function(evt: MouseEvent) { - self.onMouseDown(evt); - }); - canvas.addEventListener("mouseup", function(evt: MouseEvent) { - self.onMouseUp(evt); - }); - canvas.addEventListener("mouseout", function(evt: MouseEvent) { - self.onMouseOut(evt); - }); - canvas.addEventListener("mousewheel", function(evt: WheelEvent) { - self.onMouseScroll(evt); - }); - canvas.addEventListener("contextmenu", function(evt: MouseEvent) { - evt.preventDefault(); - }); - - // todo: do document mouse move event callback too - document.addEventListener("mousemove", function(evt: MouseEvent) { - self.onGlobalMouseMove(evt); - }); - - this.zoomFactor = 1; - this.offset = new Vector2(0, 0); - - this.mousePos = new Vector2(0, 0); - this.globalMousePos = new Vector2(0, 0); - } - - getAbsPos() { - return new Vector2(this.canvas.offsetLeft, this.canvas.offsetTop); - } - - isMouseOverCanvas() { - const rect = this.canvas.getBoundingClientRect(); - //console.log(rect); - if (this.globalMousePos.x < rect.left) return false; - if (this.globalMousePos.y < rect.top) return false; - if (this.globalMousePos.x > rect.right) return false; - if (this.globalMousePos.y > rect.bottom) return false; - - return true; - } - - onMouseDown(evt: MouseEvent) { - if (evt.button == 1 || evt.button == 2) { - this.panning = true; - - this.mouseDownPos = _getMousePos(this.canvas, evt); - } - - this.mousePos = _getMousePos(this.canvas, evt); - } - - onMouseUp(evt: MouseEvent) { - if (evt.button == 1 || evt.button == 2) { - this.panning = false; - } - } - - onMouseMove(evt: MouseEvent) { - this.prevMousePos = this.mousePos; - this.mousePos = _getMousePos(this.canvas, evt); - - if (this.panning) { - const prev = this.canvasToScene(this.prevMousePos); - const cur = this.canvasToScene(this.mousePos); - const diff = new Vector2(prev.x - cur.x, prev.y - cur.y); - this.mouseDragDiff = diff; - - const factor = this.zoomFactor; - this.offset.x -= diff.x * factor; - this.offset.y -= diff.y * factor; - } - } - - onGlobalMouseMove(evt: MouseEvent) { - this.globalMousePos = new Vector2(evt.pageX, evt.pageY); - } - - onMouseScroll(evt: WheelEvent) { - // no panning while zooming - if (this.panning) return; - - const pos = _getMousePos(this.canvas, evt); - const delta = (evt).wheelDelta > 0 ? 1.1 : 1.0 / 1.1; - - // offset from mouse pos - // find offset from previous zoom then move offset by that value - - this.zoomFactor *= delta; - this.offset.x = pos.x - (pos.x - this.offset.x) * delta; // * (factor); - this.offset.y = pos.y - (pos.y - this.offset.y) * delta; // * (factor); - - //this.zoom(pos.x, pos.y, delta); - - evt.preventDefault(); - return false; - } - - onMouseOut(evt: MouseEvent) { - // cancel panning - this.panning = false; - } - - get sceneCenter(): Vector2 { - return this.canvasToSceneXY(this.canvas.width / 2, this.canvas.height / 2); - } - - zoom(x: number, y: number, level: number) {} - - clear(context: CanvasRenderingContext2D, style = "rgb(50,50,50)") { - const ctx = context; - - ctx.setTransform(1, 0, 0, 1, 0, 0); - //ctx.fillStyle = "rgb(50,50,50)"; - ctx.fillStyle = style; - ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - } - - setViewMatrix(context: CanvasRenderingContext2D) { - context.setTransform( - this.zoomFactor, - 0, - 0, - this.zoomFactor, - this.offset.x, - this.offset.y - ); - } - - drawGrid( - ctx: CanvasRenderingContext2D, - GRID_SIZE: number, - strokeStyle: string, - lineWidth: number - ) { - // todo: convert line points to canvas space, reset context and draw them there to preserve line width - - //const GRID_SIZE = 100; - const tl = this.canvasToSceneXY(0, 0); - const br = this.canvasToSceneXY(this.canvas.width, this.canvas.height); - - //ctx.strokeStyle = "#4A5050"; - //ctx.strokeStyle = "#464C4C"; - ctx.strokeStyle = strokeStyle; - ctx.lineWidth = lineWidth; - - // vertical - const vCount = (br.x - tl.x) / GRID_SIZE + 1.0; - const xStart = tl.x - (tl.x % GRID_SIZE); - for (let i = 0; i < vCount; i++) { - ctx.beginPath(); - ctx.moveTo(xStart + i * GRID_SIZE, tl.y); - ctx.lineTo(xStart + i * GRID_SIZE, br.y); - ctx.stroke(); - } - - // horizontal - const hCount = (br.y - tl.y) / GRID_SIZE + 1.0; - const yStart = tl.y - (tl.y % GRID_SIZE); - for (let i = 0; i < hCount; i++) { - ctx.beginPath(); - ctx.moveTo(tl.x, yStart + i * GRID_SIZE); - ctx.lineTo(br.x, yStart + i * GRID_SIZE); - ctx.stroke(); - } - } - - canvasToScene(pos: Vector2): Vector2 { - return new Vector2( - (pos.x - this.offset.x) * (1.0 / this.zoomFactor), - (pos.y - this.offset.y) * (1.0 / this.zoomFactor) - ); - } - - canvasToSceneXY(x: number, y: number): Vector2 { - return new Vector2( - (x - this.offset.x) * (1.0 / this.zoomFactor), - (y - this.offset.y) * (1.0 / this.zoomFactor) - ); - } - - globalToCanvasXY(x: number, y: number): Vector2 { - const rect = this.canvas.getBoundingClientRect(); - return new Vector2(x - rect.left, y - rect.top); - } - - getMouseSceneSpace(): Vector2 { - return this.canvasToScene(this.mousePos); - } - - getMouseDeltaCanvasSpace(): Vector2 { - const prev = this.prevMousePos; - const cur = this.mousePos; - const diff = new Vector2(cur.x - prev.x, cur.y - prev.y); - - return diff; - } - - getMouseDeltaSceneSpace(): Vector2 { - const prev = this.canvasToScene(this.prevMousePos); - const cur = this.canvasToScene(this.mousePos); - const diff = new Vector2(cur.x - prev.x, cur.y - prev.y); - - return diff; - } -} diff --git a/src/lib/undostack.ts b/src/lib/undostack.ts deleted file mode 100644 index 4ae9fbc7..00000000 --- a/src/lib/undostack.ts +++ /dev/null @@ -1,88 +0,0 @@ -export class Action { - redo() {} - - undo() {} -} - -// https://gist.github.com/dsamarin/3050311 -// https://github.com/agrinko/js-undo-manager -// https://doc.qt.io/qt-5/qundostack.html#:~:text=An%20undo%20stack%20maintains%20a,createUndoAction()%20and%20createRedoAction(). -export class UndoStack { - static current: UndoStack; - - stack: Action[]; - pointer: number; - - // this tracks the last clean state - savedState: number = -1; - cleanStatusChanged: (isClean: boolean) => void = null; - - constructor() { - this.pointer = -1; - this.stack = []; - } - - push(action: Action) { - let isClean = this.isClean(); - - this.pointer += 1; - this.stack.splice(this.pointer); - - this.stack.push(action); - - if (isClean != this.isClean() && this.cleanStatusChanged) { - this.cleanStatusChanged(this.isClean()); - } - } - - undo() { - if (this.pointer < 0) return; - - let isClean = this.isClean(); - - const action = this.stack[this.pointer]; - action.undo(); - - this.pointer -= 1; - - if (isClean != this.isClean() && this.cleanStatusChanged) { - this.cleanStatusChanged(this.isClean()); - } - } - - redo() { - if (this.pointer >= this.stack.length - 1) return; - - let isClean = this.isClean(); - - this.pointer += 1; - - const action = this.stack[this.pointer]; - action.redo(); - - if (isClean != this.isClean() && this.cleanStatusChanged) { - this.cleanStatusChanged(this.isClean()); - } - } - - clear() { - let isClean = this.isClean(); - - this.pointer = -1; - this.savedState = -1; - this.stack = []; - - if (!isClean) { - this.cleanStatusChanged(true); - } - } - - setClean() { - this.savedState = this.pointer; - this.cleanStatusChanged(true); - } - - isClean() { - return this.savedState == this.pointer; - } -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts deleted file mode 100644 index 0b7899f9..00000000 --- a/src/lib/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class Guid { - static newGuid() { - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { - const r = (Math.random() * 16) | 0, - v = c == "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } -} diff --git a/src/lib/view3d.ts b/src/lib/view3d.ts deleted file mode 100644 index bee4522d..00000000 --- a/src/lib/view3d.ts +++ /dev/null @@ -1,513 +0,0 @@ -import * as THREE from "three"; -import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader"; -import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader"; -import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; -import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader.js"; -import { OrbitControls } from "three-orbitcontrols-ts"; -import { DesignerNode } from "./designer/designernode"; -import { ImageCanvas } from "./designer/imagecanvas"; -import { SphereGeometry } from "./geometry/sphere"; -import { CylinderGeometry } from "./geometry/cylinder"; -import { PlaneGeometry } from "./geometry/plane"; -import path from "path"; -import { DataTexture } from "three"; - -// https://www.bostonbiomotion.com/ -// https://blog.subvertallmedia.com/2018/06/25/three-js-imports.html -// https://areknawo.com/building-3d-2048-game-with-vue-and-three-js-setup/ -// https://github.com/nicolaspanel/three-orbitcontrols-ts/issues/1 -// https://github.com/nicolaspanel/three-orbitcontrols-ts/issues/7 -// "three-orbitcontrols-ts": "git+https://git@github.com/nicolaspanel/three-orbitcontrols-ts.git", -// https://stackoverflow.com/questions/16334505/how-to-load-obj-model-with-three-js-in-typescript?rq=1 - -// PMREM Generation -// https://github.com/mrdoob/three.js/pull/7902 -// https://discourse.threejs.org/t/are-there-any-guides-for-hdr-setups-for-novices/3932/2 -// examples: -// https://threejs.org/examples/?q=hdr#webgl_materials_envmaps_hdr -// https://github.com/mrdoob/three.js/blob/master/examples/webgl_materials_envmaps_hdr.html - -export class View3D { - private camera!: THREE.PerspectiveCamera; - private renderer!: THREE.WebGLRenderer; - private scene: THREE.Scene = new THREE.Scene(); - private material: THREE.MeshStandardMaterial = new THREE.MeshStandardMaterial( - { - //color: 0x3F51B5, - color: 0xffffff, - roughness: 0.5, - metalness: 0.0, - transparent: true, - // alphaTest: 0, - // depthFunc: THREE.AlwaysDepth, - side: THREE.FrontSide, - blending: THREE.NormalBlending - } - ); - private cubeMap: THREE.CubeTexture; - private skyPath: string = "assets/env/wide_street_01_1k.hdr"; - - private model: THREE.Object3D; - private controls: OrbitControls; - // texture repeat - private repeat = 1; - - // geometry - private sphereGeom = new SphereGeometry(0.7, 128, 128); - private cubeGeom = new THREE.BoxGeometry(); - private planeGeom = new PlaneGeometry(2, 2, 100, 100); - private cylinderGeom = new CylinderGeometry(0.5, 0.5, 1, 64, 64, true); - - setSkyPath(path: string) { - this.skyPath = path; - this.loadEnvEquirect(); - } - - setCanvas(el: HTMLCanvasElement) { - this.setupRenderer(el); - this.camera = new THREE.PerspectiveCamera( - 45, - el.width / el.height, - 0.1, - 1000 - ); - this.camera.position.z = 2.6; - this.camera.position.y = 1; - this.camera.position.x = 1; - - this.setupOrbitControls(el); - this.setupLighting(); - // this.cubeMap = this.loadEnv(); - // this.material.envMap = this.cubeMap; - this.loadEnvEquirect(); - - //const geometry = new THREE.SphereGeometry(1, 64, 64); - this.model = new THREE.Mesh(this.sphereGeom, this.material); - - // let loader = new THREE.ObjectLoader(); - // loader.load("app://./") - - this.scene.add(this.model); - - const animate = () => { - requestAnimationFrame(animate); - this.controls.update(); - this.renderer.render(this.scene, this.camera); - }; - - animate(); - } - - setupRenderer(el: HTMLCanvasElement) { - const renderer = new THREE.WebGLRenderer({ - alpha: true, - canvas: el, - preserveDrawingBuffer: true, - antialias: true - }); - renderer.setClearColor(0x000000, 0); - renderer.physicallyCorrectLights = true; - // renderer.gammaInput = false; - // renderer.gammaOutput = true; - renderer.gammaFactor = 2.2; - renderer.outputEncoding = THREE.GammaEncoding; - renderer.shadowMap.enabled = true; - renderer.shadowMap.type = THREE.PCFSoftShadowMap; - renderer.setSize(el.width, el.height); - - renderer.toneMapping = THREE.ACESFilmicToneMapping; - - this.renderer = renderer; - } - - setupLighting() { - const container = new THREE.Object3D(); - - const brightness = 2; - - let object3d = new THREE.DirectionalLight("white", 0.225 * brightness); - object3d.position.set(2.6, 1, 3); - object3d.name = "Back light"; - container.add(object3d); - - object3d = new THREE.DirectionalLight("white", 0.375 * brightness); - object3d.position.set(-2, -1, 0); - object3d.name = "Key light"; - container.add(object3d); - - object3d = new THREE.DirectionalLight("white", 0.75 * brightness); - object3d.position.set(3, 3, 2); - object3d.name = "Fill light"; - container.add(object3d); - - // this.scene.add(container); - } - - setupOrbitControls(el: HTMLElement) { - const controls = new OrbitControls(this.camera, el); - - controls.enableZoom = true; - controls.enableRotate = true; - - //controls.autoRotate = true; - controls.enablePan = true; - controls.keyPanSpeed = 7.0; - controls.enableKeys = true; - controls.target = new THREE.Vector3(0, 0, 0); - controls.mouseButtons.PAN = null; - controls.keys = { - LEFT: 0, - UP: 0, - RIGHT: 0, - BOTTOM: 0 - }; - - this.controls = controls; - } - - loadEnvironment(basePath: string) {} - - resize(width: number, height: number) { - this.camera.aspect = width / height; - this.camera.updateProjectionMatrix(); - this.renderer.setSize(width, height); - } - private _init() {} - - setTexture(imageCanvas: ImageCanvas, channelName: string) { - if (channelName == "albedo") - this.setAlbedoTexture(imageCanvas, channelName); - if (channelName == "normal") - this.setNormalTexture(imageCanvas, channelName); - if (channelName == "metalness") - this.setMetalnessTexture(imageCanvas, channelName); - if (channelName == "roughness") - this.setRoughnessTexture(imageCanvas, channelName); - if (channelName == "height") - this.setHeightTexture(imageCanvas, channelName); - if (channelName == "emission") - this.setEmissionTexture(imageCanvas, channelName); - if (channelName == "ao") this.setAoTexture(imageCanvas, channelName); - if (channelName == "alpha") this.setAlphaTexture(imageCanvas, channelName); - } - - clearTexture(channelName: string) { - if (channelName == "albedo") { - this.material.map = null; - } - if (channelName == "normal") { - this.material.normalMap = null; - } - if (channelName == "metalness") { - this.material.metalnessMap = null; - this.material.metalness = 0; - } - if (channelName == "roughness") { - this.material.roughnessMap = null; - this.material.roughness = 0.5; - } - if (channelName == "height") { - this.material.displacementMap = null; - } - if (channelName == "emission") { - this.material.emissiveMap = null; - } - if (channelName == "ao") { - this.material.aoMap = null; - } - if (channelName == "alpha") { - this.material.alphaMap = null; - } - this.material.needsUpdate = true; - } - - updateTexture(channelName: string) { - if (channelName == "albedo" && this.material.map != null) { - this.material.map.needsUpdate = true; - } - if (channelName == "normal" && this.material.normalMap != null) { - this.material.normalMap.needsUpdate = true; - } - if (channelName == "metalness" && this.material.metalnessMap != null) { - this.material.metalnessMap.needsUpdate = true; - } - if (channelName == "roughness" && this.material.roughnessMap != null) { - this.material.roughnessMap.needsUpdate = true; - } - if (channelName == "height" && this.material.displacementMap != null) { - this.material.displacementMap.needsUpdate = true; - } - if (channelName == "emission" && this.material.emissiveMap != null) { - this.material.emissiveMap.needsUpdate = true; - } - if (channelName == "ao" && this.material.aoMap != null) { - this.material.aoMap.needsUpdate = true; - } - if (channelName == "alpha" && this.material.alphaMap != null) { - this.material.alphaMap.needsUpdate = true; - } - this.material.needsUpdate = true; - } - - setAlbedoTexture(imageCanvas: ImageCanvas, channelName: string) { - const tex = new THREE.CanvasTexture(imageCanvas.canvas); - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.repeat.set(this.repeat, this.repeat); - tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); - tex.encoding = THREE.GammaEncoding; - - tex.needsUpdate = true; - this.material.map = tex; - this.material.needsUpdate = true; - } - - setNormalTexture(imageCanvas: ImageCanvas, channelName: string) { - const tex = new THREE.CanvasTexture(imageCanvas.canvas); - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.repeat.set(this.repeat, this.repeat); - tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); - - tex.needsUpdate = true; - this.material.normalMap = tex; - this.material.needsUpdate = true; - } - - setMetalnessTexture(imageCanvas: ImageCanvas, channelName: string) { - const tex = new THREE.CanvasTexture(imageCanvas.canvas); - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.repeat.set(this.repeat, this.repeat); - tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); - - tex.needsUpdate = true; - this.material.metalnessMap = tex; - this.material.metalness = 1.0; - this.material.needsUpdate = true; - } - - setRoughnessTexture(imageCanvas: ImageCanvas, channelName: string) { - const tex = new THREE.CanvasTexture(imageCanvas.canvas); - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.repeat.set(this.repeat, this.repeat); - tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); - - tex.needsUpdate = true; - this.material.roughnessMap = tex; - this.material.roughness = 1.0; - this.material.needsUpdate = true; - } - - setHeightTexture(imageCanvas: ImageCanvas, channelName: string) { - const tex = new THREE.CanvasTexture(imageCanvas.canvas); - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.repeat.set(this.repeat, this.repeat); - tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); - - tex.needsUpdate = true; - this.material.displacementMap = tex; - this.material.displacementScale = 0.1; - this.material.needsUpdate = true; - } - - setEmissionTexture(imageCanvas: ImageCanvas, channelName: string) { - console.log("setting emission texture"); - const tex = new THREE.CanvasTexture(imageCanvas.canvas); - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.repeat.set(this.repeat, this.repeat); - tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); - tex.encoding = THREE.GammaEncoding; - - tex.needsUpdate = true; - this.material.emissiveMap = tex; - this.material.emissive = new THREE.Color(1, 1, 1); - this.material.needsUpdate = true; - } - - setAoTexture(imageCanvas: ImageCanvas, channelName: string) { - const tex = new THREE.CanvasTexture(imageCanvas.canvas); - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.repeat.set(this.repeat, this.repeat); - tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); - - tex.needsUpdate = true; - this.material.aoMap = tex; - this.material.needsUpdate = true; - } - - setAlphaTexture(imageCanvas: ImageCanvas, channelName: string) { - const tex = new THREE.CanvasTexture(imageCanvas.canvas); - tex.wrapS = tex.wrapT = THREE.RepeatWrapping; - tex.repeat.set(this.repeat, this.repeat); - tex.anisotropy = this.renderer.capabilities.getMaxAnisotropy(); - - tex.needsUpdate = true; - this.material.alphaMap = tex; - this.material.needsUpdate = true; - } - - setRepeat(repeat: number) { - this.repeat = repeat; - - const mat = this.material; - if (mat.map) { - mat.map.repeat.set(repeat, repeat); - mat.map.needsUpdate = true; - } - - if (mat.normalMap) { - mat.normalMap.repeat.set(repeat, repeat); - mat.normalMap.needsUpdate = true; - } - - if (mat.metalnessMap) { - mat.metalnessMap.repeat.set(repeat, repeat); - mat.metalnessMap.needsUpdate = true; - } - - if (mat.roughnessMap) { - mat.roughnessMap.repeat.set(repeat, repeat); - mat.roughnessMap.needsUpdate = true; - } - - if (mat.displacementMap) { - mat.displacementMap.repeat.set(repeat, repeat); - mat.displacementMap.needsUpdate = true; - } - - if (mat.alphaMap) { - mat.alphaMap.repeat.set(repeat, repeat); - mat.alphaMap.needsUpdate = true; - } - - if (mat.emissiveMap) { - mat.emissiveMap.repeat.set(repeat, repeat); - mat.emissiveMap.needsUpdate = true; - } - - if (mat.aoMap) { - mat.aoMap.repeat.set(repeat, repeat); - mat.aoMap.needsUpdate = true; - } - } - - setModel(modelName: string) { - if (this.model) this.scene.remove(this.model); - - let geom: THREE.Geometry; - if (modelName == "sphere") geom = this.sphereGeom; - if (modelName == "cube") geom = this.cubeGeom; - if (modelName == "plane") geom = this.planeGeom; - if (modelName == "cylinder") geom = this.cylinderGeom; - // crash if none is valid - - this.model = new THREE.Mesh(geom, this.material); - this.scene.add(this.model); - } - - loadModel(modelPath: string) { - const ext = modelPath - .split(".") - .pop() - .toLocaleLowerCase(); - - let loader = null; - if (ext === "obj") loader = new OBJLoader(); - else if (ext === "fbx") loader = new FBXLoader(); - else return; - - loader.load(modelPath, group => { - if (this.model) this.scene.remove(this.model); - - // assign material to all children - // for (let child of group.children) - // if (child instanceof THREE.Mesh) - // (child as THREE.Mesh).material = this.material; - - group.traverse(obj => { - if (obj instanceof THREE.Mesh) - (obj as THREE.Mesh).material = this.material; - }); - - this.model = group; - this.scene.add(this.model); - }); - } - - reset() { - // clear all textures - // reset camera position - this.material = new THREE.MeshStandardMaterial({ - color: 0xffffff, - roughness: 0.5, - metalness: 0.0, - transparent: true, - // alphaTest: 0, - // depthFunc: THREE.AlwaysDepth, - side: THREE.FrontSide, - blending: THREE.NormalBlending - }); - - // this.material.envMap = this.cubeMap; - this.loadEnvEquirect(); - - (this.model as THREE.Mesh).material = this.material; - } - - setToneMappingExposure(exposure: number) { - this.renderer.toneMappingExposure = exposure; - } - - loadEnv() { - //var path = '/images/cube/Bridge2/'; - const basePath = - (process.env.NODE_ENV == "production" ? "file://" : "") + - path.join(process.env.BASE_URL, "assets/env/SwedishRoyalCastle/"); - const format = ".jpg"; - const envMap = new THREE.CubeTextureLoader().load([ - basePath + "posx" + format, - basePath + "negx" + format, - basePath + "posy" + format, - basePath + "negy" + format, - basePath + "posz" + format, - basePath + "negz" + format - ]); - - return envMap; - } - - // load equirect - - loadEnvEquirect() { - const skyPath = this.skyPath; - - const hdrPath = - (process.env.NODE_ENV == "production" ? "file://" : "") + - path.join( - process.env.BASE_URL, - // "assets/env/wide_street_01_1k.hdr" - // "assets/env/christmas/christmas_photo_studio_01_1k.hdr" - skyPath - ); - - new RGBELoader() - .setDataType(THREE.UnsignedByteType) - .load(hdrPath, (texture: DataTexture) => { - // skyPath could have changed between setting the texture and it - // actually loading, this check is there for that - if (skyPath !== this.skyPath) return; - - const pmremGenerator = new THREE.PMREMGenerator(this.renderer); - pmremGenerator.compileEquirectangularShader(); - - const evtRT = pmremGenerator.fromEquirectangular(texture); - const tex = evtRT.texture; - tex.minFilter = THREE.LinearFilter; - tex.magFilter = THREE.LinearFilter; - // tex.needsUpdate = true; - - this.material.envMap = evtRT.texture; - this.material.needsUpdate = true; - - this.scene.background = evtRT.texture; - }); - } -} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index e59833ac..00000000 --- a/src/main.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Vue from "vue"; -import vgl from "vue-golden-layout"; -//import vgl from "vue-golden-layout/src"; -import "golden-layout/src/css/goldenlayout-dark-theme.css"; -import "../public/css/scrollbar.css"; -import "./utils/inspectelement"; -import "boxicons/css/boxicons.css"; -require("typeface-open-sans"); -import * as Sentry from "@sentry/electron"; - -// toast -import VueToast from "vue-toast-notification"; -//import 'vue-toast-notification/dist/theme-default.css'; -import "vue-toast-notification/dist/theme-sugar.css"; - -// https://github.com/EmbeddedEnterprises/ng6-golden-layout/blob/master/README.md -import * as $ from "jquery"; -(window).$ = $; -(window).JQuery = $; - -// todo: fix later -// https://docs.sentry.io/platforms/javascript/guides/electron/ -// if (process.env.VUE_APP_SENTRY_DNS) { -// Sentry.init({ dsn: process.env.VUE_APP_SENTRY_DNS }); -// } - -import App from "./App.vue"; -import router from "./router"; -import store from "./store"; -import VfmPlugin from "vue-final-modal"; - -Vue.config.productionTip = false; -Vue.use(vgl); -Vue.use(VueToast); -Vue.use(VfmPlugin); - -import { Titlebar, Color } from "custom-electron-titlebar"; -const titleBar = new Titlebar({ - backgroundColor: Color.fromHex("#333333"), - icon: "./icon.png", - shadow: true -}); - -let app = new Vue({ - router, - store, - render: h => - h(App, { - props: { - titleBar: titleBar - } - }) -}); - -app.$mount("#app"); diff --git a/src/menu.ts b/src/menu.ts deleted file mode 100644 index 21a65d92..00000000 --- a/src/menu.ts +++ /dev/null @@ -1,297 +0,0 @@ -// https://www.tutorialspoint.com/electron/electron_menus.htm -// https://programmer.help/blogs/use-electron-to-customize-menus.html -// https://electronjs.org/docs/api/menu -// https://alan.fyi/renderer-menu-functions-in-electron-vue/ - -const { app, Menu, BrowserWindow } = require("electron"); -import settings from "electron-settings"; -import { Settings } from "./settings"; - -export enum MenuCommands { - FileNew = "file_new", - FileOpen = "file_open", - FileOpenRecent = "file_open_recent", - FileSave = "file_save", - FileSaveAs = "file_saveas", - FileExit = "file_exit", - - EditUndo = "edit_undo", - EditRedo = "edit_redo", - EditCut = "edit_cut", - EditCopy = "edit_copy", - EditPaste = "edit_paste", - - ExportZip = "export_zip", - ExportUnity = "export_unity", - ExportUnityZip = "export_unity_zip", - - ExamplesGoldLinesMarbleTiles = "samples_1", - ExamplesGrenade = "samples_2", - ExamplesScrews = "samples_3", - ExamplesWoodenPlanks = "samples_4", - StoneGrass = "samples_5", - Copper = "samples_6", - FoilGasket = "samples_7", - StylizedGrass = "samples_8", - Sand = "samples_9", - YellowTiles = "samples_10", - - HelpTutorials = "help_tutorials", - HelpAbout = "help_about", - HelpSubmitBug = "help_submitbug", - HelpDocumentation = "help_docs" -} - -export function setupMenu() { - //get recent files - let recentFiles = settings.getSync(Settings.RecentFiles); - if (!Array.isArray(recentFiles)) { - recentFiles = []; - } - - let recentFilesMenu = []; - recentFiles.forEach(file => { - if (typeof file !== "string") return; - - recentFilesMenu.push({ - label: file, - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.FileOpenRecent, file); - } - }); - }); - - const template = [ - { - label: "File", - submenu: [ - { - label: "New", - accelerator: "CmdOrCtrl+N", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.FileNew); - } - }, - { - label: "Open", - accelerator: "CmdOrCtrl+O", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.FileOpen); - } - }, - { - label: "Open Recent", - submenu: recentFilesMenu - }, - { - label: "Save", - accelerator: "CmdOrCtrl+S", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.FileSave); - } - }, - { - label: "Save As..", - accelerator: "CmdOrCtrl+Shift+S", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.FileSaveAs); - } - }, - { - label: "Exit", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.FileExit); - } - } - ] - }, - { - label: "Edit", - submenu: [ - { - label: "Undo", - accelerator: "CmdOrCtrl+Z", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.EditUndo); - } - }, - { - label: "Redo", - accelerator: "CmdOrCtrl+Shift+Z", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.EditRedo); - } - }, - { - label: "Cut", - accelerator: "CmdOrCtrl+X", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.EditCut); - } - }, - { - label: "Copy", - accelerator: "CmdOrCtrl+C", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.EditCopy); - } - }, - { - label: "Paste", - accelerator: "CmdOrCtrl+V", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.EditPaste); - } - } - ] - }, - // { - // label: "Export", - // submenu: [ - // { - // label: "Zip", - // click: (item, focusedWindow) => { - // focusedWindow.webContents.send(MenuCommands.ExportZip); - // } - // }, - // { - // label: "Unity Material", - // click: (item, focusedWindow) => { - // focusedWindow.webContents.send(MenuCommands.ExportUnity); - // } - // }, - // { - // label: "Unity (Zip)", - // click: (item, focusedWindow) => { - // focusedWindow.webContents.send(MenuCommands.ExportUnityZip); - // } - // } - // ] - // }, - { - label: "Examples", - submenu: [ - { - label: "GoldLinedMarbleTiles", - click: (item, focusedWindow) => { - focusedWindow.webContents.send( - MenuCommands.ExamplesGoldLinesMarbleTiles - ); - } - }, - { - label: "Stylized Grass", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.StylizedGrass); - } - }, - { - label: "StoneGrass", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.StoneGrass); - } - }, - { - label: "Sand", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.Sand); - } - }, - { - label: "Foil Gasket", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.FoilGasket); - } - }, - { - label: "Copper", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.Copper); - } - }, - { - label: "Yellow Tiles", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.YellowTiles); - } - }, - { - label: "Grenade", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.ExamplesGrenade); - } - }, - { - label: "Screws", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.ExamplesScrews); - } - }, - { - label: "WoodenPlanks", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.ExamplesWoodenPlanks); - } - } - ] - }, - { - label: "Help", - submenu: [ - { - label: "Documentation", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.HelpDocumentation); - } - }, - { - label: "About", - click: (item, focusedWindow) => { - focusedWindow.webContents.send(MenuCommands.HelpAbout); - } - } - // { - // label: "Submit Bug" - // } - ] - }, - ...(process.env.NODE_ENV !== "production" - ? [ - { - label: "Dev", - submenu: [ - { role: "reload" }, - { - role: "forcereload", - click: function(item, focusedWindow) { - if (focusedWindow) { - // After overloading, refresh and close all secondary forms - if (focusedWindow.id === 1) { - BrowserWindow.getAllWindows().forEach(function(win) { - if (win.id > 1) { - win.close(); - } - }); - } - focusedWindow.reload(); - } - } - }, - { role: "toggledevtools" }, - { type: "separator" }, - { role: "resetzoom" }, - { role: "zoomin" }, - { role: "zoomout" }, - { type: "separator" }, - { role: "togglefullscreen" } - ] - } - ] - : []) - ]; - - //console.log(template); - - const menu = Menu.buildFromTemplate(template as any); - Menu.setApplicationMenu(menu); -} diff --git a/src/nodegraph/CMakeLists.txt b/src/nodegraph/CMakeLists.txt new file mode 100644 index 00000000..5a5ccb3d --- /dev/null +++ b/src/nodegraph/CMakeLists.txt @@ -0,0 +1,96 @@ + +cmake_minimum_required(VERSION 3.10) + + +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets OpenGLWidgets REQUIRED) +find_package(OpenGL REQUIRED) +if (UNIX AND NOT APPLE) + include_directories(${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS}) +endif() + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(NODEGRAPH_SRCS + # main.cpp + nodegraph.cpp + graph/scene.cpp + graph/frame.cpp + graph/comment.cpp +) +set(NODEGRAPH_HEADERS + nodegraph.h + utils.h + graph/scene.h + graph/frame.h + graph/comment.h +) + +# library +add_library(nodegraph STATIC ${NODEGRAPH_SRCS} ${NODEGRAPH_HEADERS}) +target_link_libraries(nodegraph PRIVATE Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::OpenGLWidgets + Qt${QT_VERSION_MAJOR}::Widgets + OpenGL::GL + theme) +target_include_directories(nodegraph PRIVATE ${CMAKE_SOURCE_DIR}/src/theme) + + +set_target_properties(nodegraph PROPERTIES + AUTOMOC ON + AUTORCC ON + CXX_EXTENSIONS OFF + EXPORT_NAME "Nodegraph Library" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +if(QT_VERSION_MAJOR STREQUAL "5") + set_target_properties(nodegraph PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON) +elseif(QT_VERSION_MAJOR STREQUAL "6") + set_target_properties(nodegraph PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) +endif() + +# EXE for testing purposes +add_executable(nodegraph_app + ${NODEGRAPH_HEADERS} + ${NODEGRAPH_SRCS} + main.cpp + ) +target_link_libraries(nodegraph_app PRIVATE Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::OpenGLWidgets + Qt${QT_VERSION_MAJOR}::Widgets + theme) +target_include_directories(nodegraph_app PRIVATE ${CMAKE_SOURCE_DIR}/src/theme) + + +set_target_properties(nodegraph_app PROPERTIES + AUTOMOC ON + AUTORCC ON + CXX_EXTENSIONS OFF + EXPORT_NAME "Nodegraph App" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +if(QT_VERSION_MAJOR STREQUAL "5") + set_target_properties(nodegraph_app PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON) +elseif(QT_VERSION_MAJOR STREQUAL "6") + set_target_properties(nodegraph_app PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) +endif() + +# if(QT_VERSION_MAJOR EQUAL 6) +# qt_finalize_executable(qtapp) +# endif() \ No newline at end of file diff --git a/src/nodegraph/graph/comment.cpp b/src/nodegraph/graph/comment.cpp new file mode 100644 index 00000000..2733565b --- /dev/null +++ b/src/nodegraph/graph/comment.cpp @@ -0,0 +1,92 @@ +#include "comment.h" +#include "nodetheme.h" +#include "scene.h" +#include +#include +#include +#include +#include +#include + +namespace nodegraph { + +Comment::Comment() + : QGraphicsObject(), _id(QUuid::createUuid().toString()), _text("Comment") +{ + setFlag(QGraphicsItem::ItemIsMovable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); + // setCursor(Qt::SizeAllCursor); +} + +CommentPtr Comment::create() { return CommentPtr(new Comment()); } + +Comment::~Comment() {} + +int Comment::type() const { return (int)SceneItemType::Comment; } + +void Comment::setText(const QString& text) +{ + prepareGeometryChange(); + _text = text; + update(); +} + +QRectF Comment::calcTextRect() const +{ + QFont font("Arial", FONT_SIZE); + QFontMetrics fm(font); + + const QStringList lines = _text.split('\n'); + qreal maxWidth = 0; + for (const QString& line : lines) { + maxWidth = qMax(maxWidth, (qreal)fm.horizontalAdvance(line)); + } + + qreal w = maxWidth + PADDING * 2; + qreal h = lines.count() * fm.height() + PADDING * 2; + return QRectF(0, 0, w, h); +} + +QRectF Comment::boundingRect() const +{ + return calcTextRect().adjusted(-1, -1, 1, 1); +} + +void Comment::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHint(QPainter::Antialiasing); + painter->setRenderHint(QPainter::TextAntialiasing); + + const QRectF rect = calcTextRect(); + + // Semi-transparent fill + painter->setBrush(ntColor(Tokens::CommentFill, 30)); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(rect, 4, 4); + + // Border + QPen borderPen(ntColor(Tokens::CommentFill, isSelected() ? 220 : 140), 1.0); + painter->setPen(borderPen); + painter->setBrush(Qt::NoBrush); + painter->drawRoundedRect(rect, 4, 4); + + // Text + QFont font("Arial", FONT_SIZE); + painter->setFont(font); + painter->setPen(ntColor(Tokens::CommentText)); + + QFontMetrics fm(font); + const QStringList lines = _text.split('\n'); + qreal textY = rect.top() + PADDING + fm.ascent(); + for (const QString& line : lines) { + painter->drawText(QPointF(rect.left() + PADDING, textY), line); + textY += fm.height(); + } +} + +} // namespace nodegraph diff --git a/src/nodegraph/graph/comment.h b/src/nodegraph/graph/comment.h new file mode 100644 index 00000000..3b606c62 --- /dev/null +++ b/src/nodegraph/graph/comment.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +namespace nodegraph { + +class Comment; +typedef QSharedPointer CommentPtr; + +class Comment : public QGraphicsObject, public QEnableSharedFromThis { + Q_OBJECT + + QString _id; + QString _text; + + static constexpr qreal PADDING = 8.0; + static constexpr int FONT_SIZE = 14; + + QRectF calcTextRect() const; + +public: + explicit Comment(); + static CommentPtr create(); + + const QString& id() const { return _id; } + void setId(const QString& id) { _id = id; } + + const QString& text() const { return _text; } + void setText(const QString& text); + + QRectF boundingRect() const override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget = nullptr) override; + + virtual int type() const override; + + virtual ~Comment(); +}; + +} // namespace nodegraph diff --git a/src/nodegraph/graph/frame.cpp b/src/nodegraph/graph/frame.cpp new file mode 100644 index 00000000..0de12312 --- /dev/null +++ b/src/nodegraph/graph/frame.cpp @@ -0,0 +1,408 @@ +#include "frame.h" +#include "nodetheme.h" +#include "scene.h" +#include +#include +#include +#include +#include +#include +#include + +namespace nodegraph { + +Frame::Frame() + : QGraphicsObject(), _id(QUuid::createUuid().toString()), _title("Frame"), + _description(""), _showTitle(true), + _color(25, 0, 51) // RGB(0.1, 0, 0.2) * 255 + , + _frameRect(0, 0, 500, 300), _isHovered(false), _isDragged(false), + _dragMode(DragMode::None), _xResize(0), _yResize(0) +{ + setFlag(QGraphicsItem::ItemIsMovable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + setAcceptHoverEvents(true); + setZValue(-1000); // Behind nodes by default +} + +FramePtr Frame::create() { return FramePtr(new Frame()); } + +Frame::~Frame() {} + +int Frame::type() const { return (int)SceneItemType::Frame; } + +void Frame::setTitle(const QString& title) +{ + _title = title; + update(); +} + +void Frame::setColor(const QColor& color) +{ + _color = color; + update(); +} + +void Frame::setFrameRect(const QRectF& rect) +{ + prepareGeometryChange(); + _frameRect = rect; + update(); +} + +void Frame::setSize(qreal width, qreal height) +{ + prepareGeometryChange(); + _frameRect.setWidth(qMax(width, MIN_WIDTH)); + _frameRect.setHeight(qMax(height, MIN_HEIGHT)); + update(); +} + +void Frame::addNode(NodePtr node) +{ + if (!_nodes.contains(node)) { + _nodes.append(node); + } +} + +void Frame::removeNode(NodePtr node) { _nodes.removeAll(node); } + +void Frame::clearNodes() { _nodes.clear(); } + +QRectF Frame::boundingRect() const +{ + return _frameRect.adjusted(-5, -HANDLE_SIZE - 5, 5, 5); +} + +QVector Frame::getFrameRegions() const +{ + QVector regions; + qreal h = RESIZE_HANDLE_SIZE; + qreal w = _frameRect.width(); + qreal height = _frameRect.height(); + + // Top handle bar + regions.append( + {QRectF(_frameRect.x(), _frameRect.y() - HANDLE_SIZE, w, HANDLE_SIZE), + DragMode::HandleTop, 0, 0, Qt::SizeAllCursor}); + + // Top-left corner + regions.append({QRectF(_frameRect.x(), _frameRect.y(), h, h), + DragMode::ResizeTopLeft, -1, -1, Qt::SizeFDiagCursor}); + + // Top-right corner + regions.append({QRectF(_frameRect.x() + w - h, _frameRect.y(), h, h), + DragMode::ResizeTopRight, 1, -1, Qt::SizeBDiagCursor}); + + // Bottom-left corner + regions.append({QRectF(_frameRect.x(), _frameRect.y() + height - h, h, h), + DragMode::ResizeBottomLeft, -1, 1, Qt::SizeBDiagCursor}); + + // Bottom-right corner + regions.append( + {QRectF(_frameRect.x() + w - h, _frameRect.y() + height - h, h, h), + DragMode::ResizeBottomRight, 1, 1, Qt::SizeFDiagCursor}); + + // Top edge + regions.append({QRectF(_frameRect.x() + h, _frameRect.y(), w - 2 * h, h), + DragMode::ResizeTop, 0, -1, Qt::SizeVerCursor}); + + // Bottom edge + regions.append( + {QRectF(_frameRect.x() + h, _frameRect.y() + height - h, w - 2 * h, h), + DragMode::ResizeBottom, 0, 1, Qt::SizeVerCursor}); + + // Left edge + regions.append( + {QRectF(_frameRect.x(), _frameRect.y() + h, h, height - 2 * h), + DragMode::ResizeLeft, -1, 0, Qt::SizeHorCursor}); + + // Right edge + regions.append( + {QRectF(_frameRect.x() + w - h, _frameRect.y() + h, h, height - 2 * h), + DragMode::ResizeRight, 1, 0, Qt::SizeHorCursor}); + + return regions; +} + +DragMode Frame::getHitRegion(const QPointF& pos) const +{ + QVector regions = getFrameRegions(); + for (const ResizeRegion& region : regions) { + if (region.rect.contains(pos)) { + return region.dragMode; + } + } + return DragMode::None; +} + +Qt::CursorShape Frame::getCursorForDragMode(DragMode mode) const +{ + QVector regions = getFrameRegions(); + for (const ResizeRegion& region : regions) { + if (region.dragMode == mode) { + return region.cursor; + } + } + return Qt::ArrowCursor; +} + +QVector Frame::getNodesInFrame() const +{ + QVector nodesInFrame; + + if (!scene()) { + return nodesInFrame; + } + + // Get the frame's bounding rectangle in scene coordinates + QRectF frameSceneRect = mapRectToScene(_frameRect); + + // Get all items in the scene + QList items = scene()->items(frameSceneRect); + + // Filter for Node items that are fully or partially within the frame + for (QGraphicsItem* item : items) { + // Check if this is a Node (type() returns SceneItemType::Node) + if (item->type() == (int)SceneItemType::Node) { + Node* nodePtr = qgraphicsitem_cast(item); + if (nodePtr) { + // Get the node's bounding rect in scene coordinates + QRectF nodeBounds = nodePtr->sceneBoundingRect(); + + // Check if the node is fully contained within the frame + if (frameSceneRect.contains(nodeBounds)) { + // Find the shared pointer from the scene + Scene* scenePtr = static_cast(scene()); + if (scenePtr) { + QString nodeId = nodePtr->id(); + NodePtr sharedNode = scenePtr->getNodeById(nodeId); + if (sharedNode) { + nodesInFrame.append(sharedNode); + } + } + } + } + } + } + + return nodesInFrame; +} + +void Frame::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + painter->setRenderHint(QPainter::Antialiasing); + + // Draw top handle bar + QColor handleColor = _color.lighter(150); + QRectF handleRect(_frameRect.x(), _frameRect.y() - HANDLE_SIZE, + _frameRect.width(), HANDLE_SIZE); + painter->fillRect(handleRect, handleColor); + + // Draw title if enabled + if (_showTitle && !_title.isEmpty()) { + QFont font("Arial", 10, QFont::Bold); + painter->setFont(font); + + // shadow pass + painter->setPen(ntColor(Tokens::NodeBorder, 160)); + painter->drawText(handleRect.translated(1, 1), Qt::AlignCenter, _title); + + // text pass + painter->setPen(ntColor(Tokens::NodeTitle)); + painter->drawText(handleRect, Qt::AlignCenter, _title); + } + + // Draw frame border + QPen borderPen; + if (isSelected()) { + borderPen.setColor(ntColor(Tokens::FrameSelect)); // themed "selected" accent + borderPen.setWidth(2); + } + else { + borderPen.setColor(_color.lighter(120)); + borderPen.setWidth(1); + } + painter->setPen(borderPen); + + // Draw semi-transparent background + QColor bgColor = _color; + bgColor.setAlpha(50); // Semi-transparent + painter->setBrush(bgColor); + painter->drawRect(_frameRect); + + // Draw resize handles when selected + if (isSelected()) { + painter->setBrush(QColor(100, 100, 100, 100)); // theme-exempt: neutral resize-handle overlay + painter->setPen(Qt::NoPen); + + qreal h = RESIZE_HANDLE_SIZE; + qreal w = _frameRect.width(); + qreal height = _frameRect.height(); + + // Corner handles (small squares) + painter->drawRect(_frameRect.x(), _frameRect.y(), h, h); + painter->drawRect(_frameRect.x() + w - h, _frameRect.y(), h, h); + painter->drawRect(_frameRect.x(), _frameRect.y() + height - h, h, h); + painter->drawRect(_frameRect.x() + w - h, _frameRect.y() + height - h, + h, h); + } +} + +void Frame::mousePressEvent(QGraphicsSceneMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + QPointF pos = event->pos(); + _dragMode = getHitRegion(pos); + _isDragged = true; + _dragStartPos = event->scenePos(); + _dragStartRect = _frameRect; + + // Find resize direction + QVector regions = getFrameRegions(); + for (const ResizeRegion& region : regions) { + if (region.dragMode == _dragMode) { + _xResize = region.xResizeDir; + _yResize = region.yResizeDir; + break; + } + } + + // Capture nodes geometrically within the frame + // Skip if Alt key is pressed (allows moving frame without nodes) + if (!(event->modifiers() & Qt::AltModifier)) { + _nodeDragStartPositions.clear(); + + // Get all nodes currently within the frame's bounds + QVector nodesInBounds = getNodesInFrame(); + + // Store their starting positions + for (NodePtr node : nodesInBounds) { + if (node) { + _nodeDragStartPositions[node] = node->pos(); + } + } + } + + event->accept(); + } + + QGraphicsObject::mousePressEvent(event); +} + +void Frame::mouseMoveEvent(QGraphicsSceneMouseEvent* event) +{ + if (_isDragged && event->buttons() & Qt::LeftButton) { + QPointF delta = event->scenePos() - _dragStartPos; + + if (_dragMode == DragMode::HandleTop) { + // Move the frame and all captured nodes + setPos(pos() + delta); + + for (auto it = _nodeDragStartPositions.begin(); + it != _nodeDragStartPositions.end(); ++it) { + NodePtr node = it.key(); + if (node) { + // node->setPos(it.value() + delta); + node->moveBy(delta.x(), delta.y()); + } + } + + _dragStartPos = event->scenePos(); + } + else if (_dragMode != DragMode::None) { + // Resize mode + prepareGeometryChange(); + + QRectF newRect = _dragStartRect; + + // Handle horizontal resize + if (_xResize == -1) { // Left edge + qreal newX = _dragStartRect.x() + delta.x(); + qreal newWidth = _dragStartRect.width() - delta.x(); + if (newWidth >= MIN_WIDTH) { + newRect.setX(newX); + newRect.setWidth(newWidth); + } + } + else if (_xResize == 1) { // Right edge + qreal newWidth = _dragStartRect.width() + delta.x(); + newRect.setWidth(qMax(newWidth, MIN_WIDTH)); + } + + // Handle vertical resize + if (_yResize == -1) { // Top edge + qreal newY = _dragStartRect.y() + delta.y(); + qreal newHeight = _dragStartRect.height() - delta.y(); + if (newHeight >= MIN_HEIGHT) { + newRect.setY(newY); + newRect.setHeight(newHeight); + } + } + else if (_yResize == 1) { // Bottom edge + qreal newHeight = _dragStartRect.height() + delta.y(); + newRect.setHeight(qMax(newHeight, MIN_HEIGHT)); + } + + _frameRect = newRect; + update(); + } + + event->accept(); + return; + } + + QGraphicsObject::mouseMoveEvent(event); +} + +void Frame::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + _isDragged = false; + _dragMode = DragMode::None; + _xResize = 0; + _yResize = 0; + _nodeDragStartPositions.clear(); + setCursor(Qt::ArrowCursor); + event->accept(); + } + + QGraphicsObject::mouseReleaseEvent(event); +} + +void Frame::hoverEnterEvent(QGraphicsSceneHoverEvent* event) +{ + _isHovered = true; + update(); + QGraphicsObject::hoverEnterEvent(event); +} + +void Frame::hoverLeaveEvent(QGraphicsSceneHoverEvent* event) +{ + _isHovered = false; + setCursor(Qt::ArrowCursor); + update(); + QGraphicsObject::hoverLeaveEvent(event); +} + +void Frame::hoverMoveEvent(QGraphicsSceneHoverEvent* event) +{ + QPointF pos = event->pos(); + DragMode mode = getHitRegion(pos); + + if (mode != DragMode::None) { + Qt::CursorShape cursor = getCursorForDragMode(mode); + setCursor(cursor); + } + else { + setCursor(Qt::ArrowCursor); + } + + QGraphicsObject::hoverMoveEvent(event); +} + +} // namespace nodegraph diff --git a/src/nodegraph/graph/frame.h b/src/nodegraph/graph/frame.h new file mode 100644 index 00000000..bd16284f --- /dev/null +++ b/src/nodegraph/graph/frame.h @@ -0,0 +1,138 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nodegraph { + +class Node; +typedef QSharedPointer NodePtr; + +class Frame; +typedef QSharedPointer FramePtr; + +enum class DragMode { + None, + HandleTop, + ResizeLeft, + ResizeRight, + ResizeTop, + ResizeBottom, + ResizeTopLeft, + ResizeTopRight, + ResizeBottomLeft, + ResizeBottomRight +}; + +struct ResizeRegion { + QRectF rect; + DragMode dragMode; + int xResizeDir; // -1 for left, 1 for right, 0 for none + int yResizeDir; // -1 for top, 1 for bottom, 0 for none + Qt::CursorShape cursor; +}; + +class Frame : public QGraphicsObject, public QEnableSharedFromThis { + Q_OBJECT + +private: + QString _id; + QString _title; + QString _description; + bool _showTitle; + QColor _color; + + // Frame dimensions + QRectF _frameRect; + + // Interaction state + bool _isHovered; + bool _isDragged; + DragMode _dragMode; + int _xResize; + int _yResize; + + // Drag start state (for undo/redo) + QPointF _dragStartPos; + QRectF _dragStartRect; + + // UI dimensions + static constexpr qreal HANDLE_SIZE = 30.0; + static constexpr qreal RESIZE_HANDLE_SIZE = 20.0; + static constexpr qreal MIN_WIDTH = 100.0; + static constexpr qreal MIN_HEIGHT = 80.0; + + // Contained nodes + QVector _nodes; + QMap _nodeDragStartPositions; + + // Helper methods + QVector getFrameRegions() const; + DragMode getHitRegion(const QPointF& pos) const; + Qt::CursorShape getCursorForDragMode(DragMode mode) const; + QVector getNodesInFrame() const; + +public: + explicit Frame(); + static FramePtr create(); + + // Property getters/setters + const QString& id() const { return _id; } + void setId(const QString& id) { _id = id; } + + const QString& title() const { return _title; } + void setTitle(const QString& title); + + const QString& description() const { return _description; } + void setDescription(const QString& desc) { _description = desc; } + + bool showTitle() const { return _showTitle; } + void setShowTitle(bool show) { _showTitle = show; } + + const QColor& color() const { return _color; } + void setColor(const QColor& color); + + // Frame rectangle methods + const QRectF& frameRect() const { return _frameRect; } + void setFrameRect(const QRectF& rect); + void setSize(qreal width, qreal height); + + // Node management + void addNode(NodePtr node); + void removeNode(NodePtr node); + void clearNodes(); + const QVector& nodes() const { return _nodes; } + + // QGraphicsItem interface + QRectF boundingRect() const override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget = nullptr) override; + + virtual int type() const override; + + virtual ~Frame(); + +protected: + // Mouse event handlers + void mousePressEvent(QGraphicsSceneMouseEvent* event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; + void hoverEnterEvent(QGraphicsSceneHoverEvent* event) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent* event) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent* event) override; +}; + +} // namespace nodegraph diff --git a/src/nodegraph/graph/nodetheme.h b/src/nodegraph/graph/nodetheme.h new file mode 100644 index 00000000..678f9704 --- /dev/null +++ b/src/nodegraph/graph/nodetheme.h @@ -0,0 +1,24 @@ +#pragma once + +// Convenience accessors so the node-graph paint code (surface C: QSS can't reach +// QGraphicsItem painting) can pull colors from the shared theme with one call. +// Colors are read at paint time, so they follow theme changes / --dev-theme +// hot-reload as soon as the view repaints (see NodeGraph's themeChanged hookup). + +#include "thememanager.h" +#include "tokens.h" + +#include + +inline QColor ntColor(const char* token) +{ + return ThemeManager::instance().theme().color(token); +} + +// Same, with an explicit alpha for translucent overlays (socket labels, etc.). +inline QColor ntColor(const char* token, int alpha) +{ + QColor c = ThemeManager::instance().theme().color(token); + c.setAlpha(alpha); + return c; +} diff --git a/src/nodegraph/graph/scene.cpp b/src/nodegraph/graph/scene.cpp new file mode 100644 index 00000000..367ece96 --- /dev/null +++ b/src/nodegraph/graph/scene.cpp @@ -0,0 +1,874 @@ +#include "scene.h" +#include "comment.h" +#include "frame.h" +#include "nodetheme.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nodegraph { + +// Static member definitions for Node's OpenGL resources +QOpenGLShaderProgram* Node::shaderProgram = nullptr; +QOpenGLBuffer* Node::vbo = nullptr; +QOpenGLVertexArrayObject* Node::vao = nullptr; +bool Node::glInitialized = false; + +void Node::initializeGL() +{ + if (glInitialized) + return; + + QOpenGLContext* ctx = QOpenGLContext::currentContext(); + if (!ctx) + return; + + // Create shader program + shaderProgram = new QOpenGLShaderProgram(); + + const char* vertexShaderSource = R"( + #version 150 + in vec2 position; + in vec2 texCoord; + out vec2 vTexCoord; + uniform mat4 projectionMatrix; + void main() { + gl_Position = projectionMatrix * vec4(position, 0.0, 1.0); + vTexCoord = texCoord; + } + )"; + + const char* fragmentShaderSource = R"( + #version 150 + in vec2 vTexCoord; + out vec4 fragColor; + uniform sampler2D textureSampler; + void main() { + // 8px checkerboard in screen space + vec2 tile = floor(gl_FragCoord.xy / 8.0); + float checker = mod(tile.x + tile.y, 2.0); + vec3 bg = mix(vec3(0.753), vec3(0.502), checker); + + vec4 texColor = texture(textureSampler, vTexCoord); + fragColor = vec4(mix(bg, texColor.rgb, texColor.a), 1.0); + } + )"; + + shaderProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, + vertexShaderSource); + shaderProgram->addShaderFromSourceCode(QOpenGLShader::Fragment, + fragmentShaderSource); + shaderProgram->link(); + + // Create VAO and VBO + vao = new QOpenGLVertexArrayObject(); + vao->create(); + + vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->setUsagePattern(QOpenGLBuffer::DynamicDraw); + + glInitialized = true; +} + +void Node::cleanupGL() +{ + if (!glInitialized) + return; + + delete shaderProgram; + shaderProgram = nullptr; + + if (vbo) { + vbo->destroy(); + delete vbo; + vbo = nullptr; + } + + if (vao) { + vao->destroy(); + delete vao; + vao = nullptr; + } + + glInitialized = false; +} + +Scene::Scene() : QGraphicsScene() +{ + this->id = QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +ScenePtr Scene::create() { return ScenePtr(new Scene()); } + +void Scene::addNode(NodePtr node) +{ + this->addItem(node.data()); + nodes[node->id()] = node; +} + +ConnectionPtr Scene::connectNodes(NodePtr leftNode, QString leftOutputName, + NodePtr rightNode, QString rightInputName) +{ + auto leftPort = leftNode->getOutPortByName(leftOutputName); + auto rightPort = rightNode->getInPortByName(rightInputName); + + if (!leftPort || !rightPort) + return ConnectionPtr(nullptr); + + // create new connection item from ports + auto conn = new Connection(); + conn->startPort = leftPort; + conn->endPort = rightPort; + conn->updatePosFromPorts(); + conn->updatePathFromPositions(); + + ConnectionPtr connPtr(conn); + + // also add them to the ports + leftPort->addConnection(connPtr); + rightPort->addConnection(connPtr); + + this->addItem(conn); + + return connPtr; +} + +// .value() (not operator[]): a read-only lookup must never default-insert a +// null entry into the map, which paint/drag/label iteration would dereference. +NodePtr Scene::getNodeById(QString id) { return nodes.value(id); } + +void Scene::addFrame(FramePtr frame) +{ + this->addItem(frame.data()); + frames[frame->id()] = frame; +} + +FramePtr Scene::getFrameById(QString id) { return frames.value(id); } + +void Scene::removeFrame(FramePtr frame) +{ + frame->hide(); + this->removeItem(frame.data()); + frames.remove(frame->id()); + frame->show(); +} + +void Scene::addComment(CommentPtr comment) +{ + this->addItem(comment.data()); + comments[comment->id()] = comment; +} + +CommentPtr Scene::getCommentById(QString id) { return comments.value(id); } + +void Scene::removeComment(CommentPtr comment) +{ + comment->hide(); + this->removeItem(comment.data()); + comments.remove(comment->id()); + comment->show(); +} + +void Scene::removeNode(NodePtr node) +{ + // gather connections + QList cons; + for (auto port : node->getInPorts()) { + cons.append(port->connections); + } + + for (auto port : node->getOutPorts()) { + cons.append(port->connections); + } + + // remove connections + for (auto con : cons) { + this->removeConnection(con); + } + + // remove node + node->hide(); // fix display cache issue + this->removeItem(node.data()); + + nodes.remove(node->id()); + + // reshow here in case i forget when re-adding node for + // undo-redo + node->show(); +} + +void Scene::removeConnection(ConnectionPtr con) +{ + con->startPort->removeConnection(con); + con->endPort->removeConnection(con); + + this->removeItem(con.data()); +} + +Scene::~Scene() +{ + // remove all items manually otherwise + // smart point destructor of some items + // will cause segfault when cleaning up + auto items = this->items(); + for (auto item : items) + this->removeItem(item); +} + +Node::Node() +{ + // Generate unique ID for this node + _id = QUuid::createUuid().toString(); + + width = NODE_WIDTH; + height = NODE_HEIGHT; + isHovered = false; + showingSocketNames = false; + + // Border colors are read from tokens at paint time (see Node::paint); these + // members are kept only for any external callers. + defaultBorderColor = ntColor(Tokens::NodeBorder); + highlightBorderColor = ntColor(Tokens::NodeBorderHover); + selectedBorderColor = ntColor(Tokens::NodeBorderSelect); + + setCacheMode(QGraphicsItem::NoCache); + + setFlag(QGraphicsItem::ItemIsMovable, true); + setFlag(QGraphicsItem::ItemIsFocusable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + setFlag(QGraphicsItem::ItemSendsScenePositionChanges, true); + + setCursor(Qt::ClosedHandCursor); + + text = new QGraphicsTextItem(this); + text->setFlag(QGraphicsItem::ItemIsFocusable, false); + text->setFlag(QGraphicsItem::ItemIsSelectable, false); + + text->setPos(0, 0); + text->setTextWidth(100); + text->setDefaultTextColor(ntColor(Tokens::NodeTitle)); + text->setZValue(5); + + // center title + setName("Title"); + + text->document()->setDocumentMargin(2); + + QFont font = text->font(); + font.setWeight(QFont::Bold); + font.setPixelSize(12); + text->setFont(font); + + channelText = new QGraphicsTextItem(this); + channelText->setFlag(QGraphicsItem::ItemIsFocusable, false); + channelText->setFlag(QGraphicsItem::ItemIsSelectable, false); + channelText->setDefaultTextColor(ntColor(Tokens::NodeChannel)); + channelText->setZValue(5); + channelText->hide(); + QFont chFont = channelText->font(); + chFont.setPixelSize(12); + channelText->setFont(chFont); + + QGraphicsDropShadowEffect* effect = new QGraphicsDropShadowEffect; + effect->setBlurRadius(20); + effect->setXOffset(0); + effect->setYOffset(0); + effect->setColor(QColor(00, 00, 00, 70)); // theme-exempt: unused shadow effect (setGraphicsEffect disabled) + // setGraphicsEffect(effect); // forces node to raster remder + // maybe render to node behind this to get same effect + + setAcceptHoverEvents(true); + // setAcceptDrops(true); +} + +NodePtr Node::create() { return NodePtr(new Node()); } + +void Node::setShowSocketNames(bool show) +{ + if (showingSocketNames == show) + return; + showingSocketNames = show; + update(); +} + +void Node::setCenter(float x, float y) +{ + setPos(x - NODE_WIDTH / 2.0f, y - NODE_HEIGHT / 2.0f); +} + +QPointF Node::getCenter() const +{ + return QPointF(pos().x() + NODE_WIDTH / 2.0f, + pos().y() + NODE_HEIGHT / 2.0f); +} + +void Node::setName(QString name) +{ + this->name = name; + text->setPlainText(name); + + QTextBlockFormat format; + format.setAlignment(Qt::AlignCenter); + QTextCursor cursor = text->textCursor(); + cursor.select(QTextCursor::Document); + cursor.mergeBlockFormat(format); + cursor.clearSelection(); + text->setTextCursor(cursor); +} + +void Node::setThumbnail(const QPixmap& pixmap) +{ + this->thumbnail = pixmap; + this->update(); +} + +void Node::setChannel(QString ch) +{ + this->channel = ch; + if (ch.isEmpty()) { + channelText->hide(); + } + else { + channelText->setPlainText(ch.toUpper()); + QFontMetrics fm(channelText->font()); + int textW = fm.horizontalAdvance(ch.toUpper()); + channelText->setPos((width - textW) / 2.0, -20); + channelText->show(); + } +} + +const QVector Node::getInPorts() const { return inPorts; } + +const QVector Node::getOutPorts() const { return outPorts; } + +void Node::addInPort(QString name) +{ + PortPtr port(new Port(this)); + port->name = name; + port->portType = PortType::In; + port->node = this->sharedFromThis(); + inPorts.append(port); + + // top and bottom padding for sockets + const int pad = inPorts.count() < 5 ? 10 : 0; + + // sort in sockets + int incr = (this->height - pad * 2) / inPorts.count(); + int mid = incr / 2.0; + int i = 0; + for (auto port : inPorts) { + int y = pad + i * incr + mid; + int x = 0; + port->setCenter(x, y); + i++; + } +} + +void Node::addOutPort(QString name) +{ + PortPtr port(new Port(this)); + port->name = name; + port->portType = PortType::Out; + port->node = this->sharedFromThis(); + outPorts.append(port); + + // top and bottom padding for sockets + const int pad = outPorts.count() < 5 ? 10 : 0; + + // sort in sockets + int incr = (this->height - pad * 2) / outPorts.count(); + int mid = incr / 2.0; + int i = 0; + for (auto port : outPorts) { + int y = pad + i * incr + mid; + int x = width; + port->setCenter(x, y); + i++; + } +} + +PortPtr Node::getPortById(QString id) +{ + for (auto port : inPorts) { + if (port->id() == id) + return port; + } + + for (auto port : outPorts) { + if (port->id() == id) + return port; + } + + return PortPtr(nullptr); +} + +PortPtr Node::getInPortByName(QString name) +{ + for (auto port : inPorts) { + if (port->name == name) + return port; + } + + return PortPtr(nullptr); +} + +PortPtr Node::getOutPortByName(QString name) +{ + for (auto port : outPorts) { + if (port->name == name) + return port; + } + + return PortPtr(nullptr); +} + +QRectF Node::boundingRect() const { return QRectF(0, 0, 100, 100); } + +// void Node::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +// { +// QGraphicsObject::mouseMoveEvent(event); +// } + +void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + // if (event->button() == Qt::LeftButton) { + // emit selected(sharedFromThis()); + // } + + QGraphicsObject::mouseReleaseEvent(event); +} + +void Node::hoverEnterEvent(QGraphicsSceneHoverEvent* event) +{ + this->text->hide(); + this->isHovered = true; + QGraphicsObject::hoverEnterEvent(event); +} + +void Node::hoverLeaveEvent(QGraphicsSceneHoverEvent* event) +{ + this->text->show(); + this->isHovered = false; + QGraphicsObject::hoverLeaveEvent(event); +} + +void Node::paint(QPainter* painter, QStyleOptionGraphicsItem const* option, + QWidget* widget) +{ + const int titleHeight = 20; + const int nodeWidth = width; + const int nodeHeight = height; + const int titleRadius = 4; + const QColor titleColor(0, 0, 0); + + // // https://doc.qt.io/qt-5/qpainter.html#beginNativePainting + // https://github.com/liff-engineer/WeeklyARTS/blob/d8605aa3bfb2641d2a13621262024a1edff7b661/2018_9_4/Mixin2D%263DinQt.md + auto type = painter->paintEngine()->type(); + if (type != QPaintEngine::OpenGL && type != QPaintEngine::OpenGL2) { + qWarning() << "Paint engine needs to be OPENGL!"; + // return; + } + + auto rect = boundingRect(); + + QColor borderColor; + if (isSelected()) + borderColor = ntColor(Tokens::NodeBorderSelect); + else if (isHovered) + borderColor = ntColor(Tokens::NodeBorderHover); + else + borderColor = ntColor(Tokens::NodeBorder); + + // not really needed + // painter->setClipRect(option->exposedRect); + + // smooth rendering + // painter->setRenderHint(QPainter::Antialiasing); + // painter->setRenderHint(QPainter::TextAntialiasing); + + // title tab + // QPainterPath titlePath; + // titlePath.setFillRule(Qt::WindingFill); + // titlePath.addRect(0, 10, width, titleHeight - 10); + // titlePath.addRoundedRect(0, 0, nodeWidth, titleHeight, titleRadius, + // titleRadius); painter->fillPath(titlePath, QBrush(QColor(255, 255, + // 255))); + + // // draw text node seperator + // QPainterPath block; + // block.setFillRule(Qt::WindingFill); + // block.addRect(0, titleHeight, nodeWidth, 3); + // painter->fillPath(block, QBrush(QColor(30, 30, 30, 160))); + + // QPen pen(QColor(00, 00, 00, 250), .5); + // QPen pen(borderColor, .5); + // painter->setPen(pen); + + // background + QPainterPath bgPath; + bgPath.setFillRule(Qt::WindingFill); + bgPath.addRoundedRect(0, 0, nodeWidth, nodeHeight, titleRadius, + titleRadius); + painter->fillPath(bgPath, QBrush(ntColor(Tokens::NodeBg))); + + if (!thumbnail.isNull()) { + // Checkerboard background for alpha-transparent thumbnails. + // (Built once and cached, so it reflects the theme at first draw.) + static QPixmap checkerTile; + if (checkerTile.isNull()) { + checkerTile = QPixmap(16, 16); + checkerTile.fill(ntColor(Tokens::CheckerA)); + QPainter cp(&checkerTile); + cp.fillRect(0, 0, 8, 8, ntColor(Tokens::CheckerB)); + cp.fillRect(8, 8, 8, 8, ntColor(Tokens::CheckerB)); + } + painter->fillRect(QRect(0, 0, nodeWidth, nodeHeight), + QBrush(checkerTile)); + painter->drawPixmap(QRect(0, 0, nodeWidth, nodeHeight), thumbnail); + } + + // thumbnail as texture + if (texId != 0) { + // // https://doc.qt.io/qt-5/qpainter.html#beginNativePainting + painter->beginNativePainting(); + + // Initialize OpenGL resources if needed + initializeGL(); + + if (glInitialized && shaderProgram && vao && vbo) { + QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); + + // Get the current viewport and create orthographic projection + GLint viewport[4]; + f->glGetIntegerv(GL_VIEWPORT, viewport); + + // Create orthographic projection matrix + QTransform transform = painter->combinedTransform(); + QMatrix4x4 projectionMatrix; + projectionMatrix.ortho(0, viewport[2], viewport[3], 0, -1, 1); + + // Build vertex data - transform scene coordinates to device + // coordinates + QPointF p0 = transform.map(QPointF(0, 0)); + QPointF p1 = transform.map(QPointF(100, 0)); + QPointF p2 = transform.map(QPointF(100, 100)); + QPointF p3 = transform.map(QPointF(0, 100)); + + // Two triangles for a quad: position (x,y) + texcoord (u,v) + GLfloat vertices[] = { + // Triangle 1 + (GLfloat)p0.x(), + (GLfloat)p0.y(), + 0.0f, + 1.0f, + (GLfloat)p1.x(), + (GLfloat)p1.y(), + 1.0f, + 1.0f, + (GLfloat)p2.x(), + (GLfloat)p2.y(), + 1.0f, + 0.0f, + // Triangle 2 + (GLfloat)p0.x(), + (GLfloat)p0.y(), + 0.0f, + 1.0f, + (GLfloat)p2.x(), + (GLfloat)p2.y(), + 1.0f, + 0.0f, + (GLfloat)p3.x(), + (GLfloat)p3.y(), + 0.0f, + 0.0f, + }; + + // Setup state + f->glDisable(GL_BLEND); + f->glDisable(GL_DEPTH_TEST); + + // Bind shader + shaderProgram->bind(); + shaderProgram->setUniformValue("projectionMatrix", + projectionMatrix); + shaderProgram->setUniformValue("textureSampler", 0); + + // Bind texture + f->glActiveTexture(GL_TEXTURE0); + f->glBindTexture(GL_TEXTURE_2D, texId); + + // Setup VAO and VBO + vao->bind(); + vbo->bind(); + vbo->allocate(vertices, sizeof(vertices)); + + // Setup vertex attributes + int positionLoc = shaderProgram->attributeLocation("position"); + int texCoordLoc = shaderProgram->attributeLocation("texCoord"); + + shaderProgram->enableAttributeArray(positionLoc); + shaderProgram->enableAttributeArray(texCoordLoc); + shaderProgram->setAttributeBuffer(positionLoc, GL_FLOAT, 0, 2, + 4 * sizeof(GLfloat)); + shaderProgram->setAttributeBuffer(texCoordLoc, GL_FLOAT, + 2 * sizeof(GLfloat), 2, + 4 * sizeof(GLfloat)); + + // Draw + f->glDrawArrays(GL_TRIANGLES, 0, 6); + + // Cleanup + shaderProgram->disableAttributeArray(positionLoc); + shaderProgram->disableAttributeArray(texCoordLoc); + vbo->release(); + vao->release(); + shaderProgram->release(); + + f->glEnable(GL_BLEND); + } + + painter->endNativePainting(); + } + + // draw highlight + + // top bar for text + if (!isHovered) { + QPainterPath bgPath; + bgPath.setFillRule(Qt::WindingFill); + bgPath.addRoundedRect(0, 0, nodeWidth, 18, titleRadius, titleRadius); + painter->fillPath(bgPath, QBrush(ntColor(Tokens::NodeBorder))); + + text->paint(painter, option, widget); + } + + // draw border + painter->setPen(QPen(borderColor, 3)); + painter->drawRoundedRect(rect, titleRadius, titleRadius); + + // socket name labels — shown on hover or when cursor is nearby during drag + if (isHovered || showingSocketNames) { + painter->save(); + painter->setRenderHint(QPainter::TextAntialiasing); + + QFont labelFont = painter->font(); + labelFont.setPixelSize(10); + painter->setFont(labelFont); + + QFontMetrics fm(labelFont); + const int labelH = 14; + const int pad = 3; + const int portRadius = 7; + const int gap = 4; + + auto drawLabel = [&](const QString& labelName, QPointF portPos, + bool isIn) { + int textW = fm.horizontalAdvance(labelName); + int rectW = textW + pad * 2; + qreal x = isIn ? portPos.x() + portRadius + gap + : portPos.x() - portRadius - gap - rectW; + qreal y = portPos.y() - labelH / 2.0; + + QRectF bgRect(x, y, rectW, labelH); + painter->setPen(Qt::NoPen); + painter->setBrush(ntColor(Tokens::NodeBorder, 160)); + painter->drawRoundedRect(bgRect, 3, 3); + + painter->setPen(ntColor(Tokens::NodeTitle, 220)); + painter->drawText(bgRect, Qt::AlignCenter, labelName); + }; + + for (auto& port : inPorts) + drawLabel(port->name, port->pos(), true); + + // for (auto& port : outPorts) + // drawLabel(port->name, port->pos(), false); + + painter->restore(); + } +} + +Node::~Node() +{ + // remove ownership from scene else scene will try to + // clean it up after it's been deleted + if (this->scene()) + this->scene()->removeItem(this); +} + +QString Port::id() const { return _id; } + +Port::Port(QGraphicsObject* parent) : QGraphicsObject(parent) +{ + setCursor(Qt::ClosedHandCursor); + + // this->setFlag(QGraphicsItem::ItemIsSelectable, false); + this->setFlag(QGraphicsItem::ItemSendsScenePositionChanges); + + _radius = 7; + name = ""; + portType = PortType::In; + _id = QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +QRectF Port::boundingRect() const +{ + // return QRectF(-_radius, -_radius, _radius * 2, _radius * 2); + + // add extra space for hit testing + return QRectF(-_radius * 2, -_radius * 2, _radius * 4, _radius * 4); +} + +QRectF Port::actualRect() const +{ + return QRectF(-_radius, -_radius, _radius * 2, _radius * 2); +} + +void Port::setCenter(float x, float y) +{ + // auto rect = this->boundingRect(); + // setPos(QPointF(x - rect.x() / 2, y - rect.y() / 2)); + setPos(QPointF(x, y)); +} + +QVariant Port::itemChange(GraphicsItemChange change, const QVariant& value) +{ + if (change == ItemScenePositionHasChanged) { + for (auto con : connections) { + con->updatePosFromPorts(); + con->updatePathFromPositions(); + } + } + return value; +} + +void Port::removeConnection(ConnectionPtr con) +{ + // todo: make sure this does what it's supposed to do + connections.removeOne(con); +} + +void Port::paint(QPainter* painter, QStyleOptionGraphicsItem const* option, + QWidget* widget) +{ + auto rect = actualRect(); + + QPen pen(ntColor(Tokens::NodeBorder, 250), 1.0f); + painter->setPen(pen); + + // background + QPainterPath bgPath; + bgPath.setFillRule(Qt::WindingFill); + // bgPath.addRoundedRect(-_radius, _radius, rect.width(), rect.height(), + // rect.width() / 2, rect.height() / 2); + bgPath.addRoundedRect(rect, _radius, _radius); + painter->fillPath(bgPath, QBrush(ntColor(Tokens::SocketFill))); + + // draw border + painter->setPen(QPen(ntColor(Tokens::NodeBorder), 3)); + painter->drawRoundedRect(rect, rect.width() / 2, rect.height() / 2); +} + +Port::~Port() +{ + // remove ownership from scene else scene will try to + // clean it up after it's been deleted + if (this->scene()) + this->scene()->removeItem(this); +} + +Connection::Connection() +{ + pos1 = QPointF(0, 0); + pos2 = QPointF(0, 0); + + connectState = ConnectionState::Complete; + + auto pen = QPen(ntColor(Tokens::Wire)); + pen.setBrush(ntColor(Tokens::WireSelected)); + pen.setCapStyle(Qt::RoundCap); + pen.setWidth(lineThickness); + setPen(pen); +} + +void Connection::updatePosFromPorts() +{ + pos1 = startPort->scenePos(); + pos2 = endPort->scenePos(); +} + +void Connection::updatePathFromPositions() +{ + p = QPainterPath(); + p.moveTo(pos1); + + qreal dx = pos2.x() - pos1.x(); + qreal dy = pos2.y() - pos1.y(); + + QPointF ctr1(pos1.x() + dx * 0.5, pos1.y()); + QPointF ctr2(pos2.x() - dx * 0.5, pos2.y()); + + p.cubicTo(ctr1, ctr2, pos2); + p.setFillRule(Qt::OddEvenFill); + + setPath(p); +} + +void Connection::paint(QPainter* painter, + const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + painter->setRenderHint(QPainter::Antialiasing); + painter->save(); + + if (connectState == ConnectionState::Dragging) { + QPen pen(ntColor(Tokens::WireDragging), lineThickness); + pen.setStyle(Qt::DashLine); + pen.setDashOffset(4); + painter->setPen(pen); + painter->drawPath(p); + + painter->setPen(QPen(ntColor(Tokens::NodeBorder), 3)); + painter->setBrush(QBrush(ntColor(Tokens::WireDragging))); + painter->drawEllipse(pos1, 7, 7); + + painter->setPen(Qt::NoPen); + painter->drawEllipse(pos2, 6, 6); + } + if (connectState == ConnectionState::Complete) { + QPen pen(ntColor(Tokens::Wire), lineThickness); + painter->setPen(pen); + painter->drawPath(p); + + painter->setPen(QPen(ntColor(Tokens::NodeBorder), 3)); + painter->setBrush(QBrush(ntColor(Tokens::Wire))); + painter->drawEllipse(pos1, 7, 7); + painter->drawEllipse(pos2, 7, 7); + } + + painter->restore(); + + Q_UNUSED(option); + Q_UNUSED(widget); +} + +Connection::~Connection() +{ + // remove ownership from scene else scene will try to + // clean it up after it's been deleted + if (this->scene()) + this->scene()->removeItem(this); +} + +} // namespace nodegraph \ No newline at end of file diff --git a/src/nodegraph/graph/scene.h b/src/nodegraph/graph/scene.h new file mode 100644 index 00000000..0c20a9f2 --- /dev/null +++ b/src/nodegraph/graph/scene.h @@ -0,0 +1,259 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QGraphicsTextItem; +class QPainterPath; + +namespace nodegraph { + +class Node; +class Port; +class Connection; +class Scene; +class Frame; +class Comment; + +typedef QSharedPointer NodePtr; +typedef QSharedPointer ConnectionPtr; +typedef QSharedPointer PortPtr; +typedef QSharedPointer ScenePtr; +typedef QSharedPointer FramePtr; +typedef QSharedPointer CommentPtr; + +enum class SceneItemType : int { + Node = 1, + Port = 2, + Connection = 3, + Comment = 4, + Frame = 5 +}; + +class Scene : public QGraphicsScene, public QEnableSharedFromThis { +public: + QMap nodes; + QMap frames; + QMap comments; + QString id; + + Scene(); + static ScenePtr create(); + + void addNode(NodePtr node); + NodePtr getNodeById(QString id); + + void addFrame(FramePtr frame); + FramePtr getFrameById(QString id); + void removeFrame(FramePtr frame); + + void addComment(CommentPtr comment); + CommentPtr getCommentById(QString id); + void removeComment(CommentPtr comment); + + ConnectionPtr connectNodes(NodePtr leftNode, QString leftOutputName, + NodePtr rightNode, QString rightInputName); + + // this removes the node and associating connections + // and node from scene + void removeNode(NodePtr node); + + // removes connection and item from scene + void removeConnection(ConnectionPtr con); + + ~Scene(); +}; + +class Node : public QGraphicsObject, public QEnableSharedFromThis { + QString _id; + + static constexpr int NODE_WIDTH = 100; + static constexpr int NODE_HEIGHT = 100; + + int width; + int height; + GLuint texId = 0; + + QGraphicsTextItem* text; + QGraphicsTextItem* channelText; + QString name; + QString channel; + + QPixmap thumbnail; + + bool isHovered; + bool showingSocketNames; + // bool isSelected; + + QColor defaultBorderColor; + QColor highlightBorderColor; + QColor selectedBorderColor; + + // Modern OpenGL resources + static QOpenGLShaderProgram* shaderProgram; + static QOpenGLBuffer* vbo; + static QOpenGLVertexArrayObject* vao; + static bool glInitialized; + static void initializeGL(); + static void cleanupGL(); + +public: + QVector inPorts; + QVector outPorts; + explicit Node(); + static NodePtr create(); + + const QString id() const { return _id; } + void setId(const QString& id) { _id = id; } + + void setTextureId(GLuint id) { texId = id; } + + const QVector getInPorts() const; + const QVector getOutPorts() const; + + void setName(QString name); + void setChannel(QString ch); + void setCenter(float x, float y); + QPointF getCenter() const; + void setThumbnail(const QPixmap& pixmap); + + void setShowSocketNames(bool show); + + void addInPort(QString name); + void addOutPort(QString name); + + PortPtr getPortById(QString id); + + PortPtr getInPortByName(QString name); + PortPtr getOutPortByName(QString name); + + QRectF boundingRect() const override; + + bool hovered() const { return isHovered; }; + + virtual int type() const override { return (int)SceneItemType::Node; } + virtual ~Node(); + +protected: + void paint(QPainter* painter, QStyleOptionGraphicsItem const* option, + QWidget* widget = 0) override; + + // void + // mousePressEvent(QGraphicsSceneMouseEvent *event) override; + + // void + // mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + + void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override; + + void hoverEnterEvent(QGraphicsSceneHoverEvent* event) override; + + void hoverLeaveEvent(QGraphicsSceneHoverEvent* event) override; + + // void + // hoverMoveEvent(QGraphicsSceneHoverEvent *) override; + + // void + // mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; + + // signals: + // void selected(const NodePtr& node); +}; + +enum class PortType : int { Invalid = 0, In = 1, Out = 2 }; + +class Port : public QGraphicsObject, public QEnableSharedFromThis { + QGraphicsTextItem* text; + int _radius; + QString _id; + +public: + NodePtr node; + QVector connections; + + PortType portType; + QString name; + + QString id() const; + void setId(const QString& id) { _id = id; } + + int radius() const { return _radius; } + + void addConnection(ConnectionPtr con) { connections.append(con); } + + void removeConnection(ConnectionPtr con); + + Port(QGraphicsObject* parent); + + QRectF boundingRect() const override; + + QRectF actualRect() const; + + void setCenter(float x, float y); + + QVariant itemChange(GraphicsItemChange change, const QVariant& value); + + virtual int type() const override { return (int)SceneItemType::Port; } + virtual ~Port(); + +protected: + void paint(QPainter* painter, QStyleOptionGraphicsItem const* option, + QWidget* widget = 0) override; +}; + +enum class ConnectionState { Dragging, Complete }; + +class Connection : public QGraphicsPathItem, + public QEnableSharedFromThis { + friend class Scene; + + QString _id; + +public: + ConnectionState connectState; + + PortPtr startPort; + PortPtr endPort; + + QPointF pos1; + QPointF pos2; + + double lineThickness = 4.0; + + virtual int type() const override { return (int)SceneItemType::Connection; } + +public: + explicit Connection(); + + const QString id() const { return _id; } + void setId(const QString& id) { _id = id; } + + void updatePositions(); + + void updatePosFromPorts(); + void updatePathFromPositions(); + + // Value member (was a raw QPainterPath* that leaked on every + // updatePathFromPositions() call and was read uninitialized before the + // first update). + QPainterPath p; + + // virtual int type() const override; + void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, + QWidget* widget) override; + + virtual ~Connection(); +}; + +} // namespace nodegraph \ No newline at end of file diff --git a/src/nodegraph/main.cpp b/src/nodegraph/main.cpp new file mode 100644 index 00000000..7a50978b --- /dev/null +++ b/src/nodegraph/main.cpp @@ -0,0 +1,76 @@ +#include "graph/comment.h" +#include "graph/frame.h" +#include "graph/scene.h" +#include "nodegraph.h" +#include +#include +#include + +using namespace nodegraph; + +int main(int argc, char* argv[]) +{ + QApplication a(argc, argv); + QMainWindow w; + + auto graph = new NodeGraph(&w); + + // QStackedLayout *layout = new QStackedLayout(&w); + // layout->addWidget(graph); + + w.setCentralWidget(graph); + + auto scene = graph->scene(); + + NodePtr node(new Node()); + node->setName("Warp"); + node->addInPort("image"); + node->addInPort("height"); + node->addOutPort("image"); + scene->addNode(node); + + NodePtr outputNode(new Node()); + outputNode->setName("Floodfill"); + outputNode->addInPort("image"); + outputNode->addOutPort("result"); + outputNode->setPos(200, 150); + scene->addNode(outputNode); + + scene->connectNodes(node, "image", outputNode, "image"); + + NodePtr node2(new Node()); + node2->setName("Stress Test"); + node2->addInPort("image"); + node2->addInPort("image2"); + node2->addInPort("image3"); + node2->addInPort("image4"); + node2->addOutPort("result"); + node2->setPos(320, 0); + scene->addNode(node2); + + // Create a frame to group the first two nodes + FramePtr frame = Frame::create(); + frame->setTitle("Processing Group"); + frame->setColor(QColor(50, 20, 80)); // Purple-ish color + frame->setFrameRect(QRectF(-20, -20, 400, 250)); + frame->setPos(0, 0); + + // Add nodes to the frame so they move together + frame->addNode(node); + frame->addNode(outputNode); + + scene->addFrame(frame); + + CommentPtr comment = Comment::create(); + comment->setText("Warp node takes image\nand height map as inputs\n\nnew " + "line works too!"); + comment->setPos(420, 0); + scene->addComment(comment); + + w.resize(800, 600); + + w.show(); + w.showMaximized(); + + return a.exec(); +} diff --git a/src/nodegraph/nodegraph.cpp b/src/nodegraph/nodegraph.cpp new file mode 100644 index 00000000..aeedec09 --- /dev/null +++ b/src/nodegraph/nodegraph.cpp @@ -0,0 +1,588 @@ +#include + +#include +#include +#include + +#include +#include +// #include + +// #include +#include +#include + +#include +#include +#include + +#include "graph/comment.h" +#include "graph/frame.h" +#include "graph/nodetheme.h" +#include "graph/scene.h" +#include "nodegraph.h" + +namespace nodegraph { + +MouseButtonStates::MouseButtonStates() { reset(); } + +void MouseButtonStates::reset() +{ + left = false; + middle = false; + right = false; +} + +NodeGraph::NodeGraph(QWidget* parent) : QGraphicsView(parent) +{ + // https://doc.qt.io/qt-6.2/graphicsview.html#opengl-rendering + auto gl = new QOpenGLWidget(); + QSurfaceFormat format; + format.setSamples(4); + gl->setFormat(format); + this->setViewport(gl); + + setDragMode(QGraphicsView::RubberBandDrag); + setRenderHint(QPainter::Antialiasing); + + setBackgroundBrush(ntColor(Tokens::GridBg)); + + // Repaint (and refresh the themed background brush) whenever the theme + // changes, so the node graph follows --dev-theme hot-reloads like the rest + // of the app. drawBackground() reads grid colors from tokens at paint time. + QObject::connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this, + [this]() { + setBackgroundBrush(ntColor(Tokens::GridBg)); + if (scene()) + scene()->update(); + viewport()->update(); + }); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + + // setCacheMode(QGraphicsView::CacheBackground); + // setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); + setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + + // setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers))); + + // auto scene = new QGraphicsScene(); + // scene->addText("Hello World!"); + // setScene(scene); + + setAcceptDrops(true); + + this->setNodeGraphScene(ScenePtr(new Scene())); + mbStates.reset(); +} + +void NodeGraph::setNodeGraphScene(const ScenePtr& scene) +{ + // properly cleanup old scene + if (!!this->_scene) { + this->setScene(nullptr); + } + + this->_scene = scene; + scene->setSceneRect(-100000, -100000, 200000, 200000); + this->setScene(scene.data()); + + // handle scene's events from within the view + scene->installEventFilter(this); + + // connect to selection signal + connect(scene.data(), &QGraphicsScene::selectionChanged, + [=]() { this->handleSelectionChange(); }); +} + +void NodeGraph::wheelEvent(QWheelEvent* event) +{ + QPoint delta = event->angleDelta(); + + if (delta.y() == 0) { + event->ignore(); + return; + } + + double const d = delta.y() / std::abs(delta.y()); + + if (d > 0.0) + scaleUp(); + else + scaleDown(); +} + +void NodeGraph::scaleUp() +{ + double const step = 1.2; + double const factor = std::pow(step, 1.0); + + QTransform t = transform(); + + if (t.m11() > 2.0) + return; + + scale(factor, factor); +} + +void NodeGraph::scaleDown() +{ + double const step = 1.2; + double const factor = std::pow(step, -1.0); + + scale(factor, factor); +} + +void NodeGraph::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Delete) { + QList selectedNodes; + QList selectedFrames; + QList selectedComments; + + for (auto item : this->_scene->selectedItems()) { + if (item->type() == (int)SceneItemType::Node) + selectedNodes.append(qgraphicsitem_cast(item)->sharedFromThis()); + else if (item->type() == (int)SceneItemType::Frame) + selectedFrames.append(qgraphicsitem_cast(item)->sharedFromThis()); + else if (item->type() == (int)SceneItemType::Comment) + selectedComments.append(qgraphicsitem_cast(item)->sharedFromThis()); + } + + if (!selectedNodes.isEmpty() || !selectedFrames.isEmpty() || !selectedComments.isEmpty()) + emit deleteRequested(selectedNodes, selectedFrames, selectedComments); + } + + QGraphicsView::keyPressEvent(event); + this->invalidateScene(QRect(-1000, -1000, 1000, 1000)); +} + +void NodeGraph::keyReleaseEvent(QKeyEvent* event) +{ + switch (event->key()) { + case Qt::Key_Shift: + // setDragMode(QGraphicsView::ScrollHandDrag); + break; + + default: + break; + } + QGraphicsView::keyReleaseEvent(event); +} + +void NodeGraph::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::MiddleButton) { + _clickPos = event->pos(); + setDragMode(QGraphicsView::NoDrag); + return; + } + QGraphicsView::mousePressEvent(event); +} + +void NodeGraph::mouseMoveEvent(QMouseEvent* event) +{ + if (event->buttons() & Qt::MiddleButton) { + QPointF delta = event->pos() - _clickPos; + horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x()); + verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y()); + _clickPos = event->pos(); + return; + } + QGraphicsView::mouseMoveEvent(event); +} + +void NodeGraph::mouseReleaseEvent(QMouseEvent* event) +{ + if (event->button() == Qt::MiddleButton) { + setDragMode(QGraphicsView::RubberBandDrag); + } + QGraphicsView::mouseReleaseEvent(event); +} + +void NodeGraph::mouseDoubleClickEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + + auto scenePos = this->mapToScene(event->pos()); + + // node: stripped const from returned node + auto node = (Node*)getNodeAtScenePos(scenePos.x(), scenePos.y()); + if (node) { + emit nodeDoubleClicked(node->sharedFromThis()); + } + else { + emit nodeDoubleClicked(nullptr); + } + } + QGraphicsView::mouseDoubleClickEvent(event); +} + +void NodeGraph::drawBackground(QPainter* painter, const QRectF& r) +{ + auto type = painter->paintEngine()->type(); + if (type != QPaintEngine::OpenGL && type != QPaintEngine::OpenGL2) { + qWarning() << "background paint engine needs to be OPENGL!"; + // return; + } + + QGraphicsView::drawBackground(painter, r); + painter->setRenderHint(QPainter::Antialiasing); + + auto drawGrid = [&](double gridStep) { + QRect windowRect = rect(); + QPointF tl = mapToScene(windowRect.topLeft()); + QPointF br = mapToScene(windowRect.bottomRight()); + + double left = std::floor(tl.x() / gridStep - 0.5); + double right = std::floor(br.x() / gridStep + 1.0); + double bottom = std::floor(tl.y() / gridStep - 0.5); + double top = std::floor(br.y() / gridStep + 1.0); + + // vertical lines + for (int xi = int(left); xi <= int(right); ++xi) { + QLineF line(xi * gridStep, bottom * gridStep, xi * gridStep, + top * gridStep); + + painter->drawLine(line); + } + + // horizontal lines + for (int yi = int(bottom); yi <= int(top); ++yi) { + QLineF line(left * gridStep, yi * gridStep, right * gridStep, + yi * gridStep); + painter->drawLine(line); + } + }; + + QPen pfine(ntColor(Tokens::GridFine), 1.0); + + painter->setPen(pfine); + drawGrid(15); + + QPen p(ntColor(Tokens::GridCoarse), 1.0); + + painter->setPen(p); + drawGrid(150); +} + +void NodeGraph::showEvent(QShowEvent* event) +{ + // _scene->setSceneRect(this->rect()); + QGraphicsView::showEvent(event); +} + +bool NodeGraph::eventFilter(QObject* o, QEvent* e) +{ + QGraphicsSceneMouseEvent* me = (QGraphicsSceneMouseEvent*)e; + if (o == _scene.data()) { + + switch ((int)e->type()) { + case QEvent::GraphicsSceneMousePress: + if (this->sceneMousePressEvent(me)) + return true; + break; + case QEvent::GraphicsSceneMouseMove: + if (this->sceneMouseMoveEvent(me)) + return true; + break; + case QEvent::GraphicsSceneMouseRelease: + if (this->sceneMouseReleaseEvent(me)) + return true; + break; + + // case QEvent::GraphicsSceneDrop: + // break; + } + } + + return QObject::eventFilter(o, e); +} + +// todo: probably best to handle this in the views mousePressEvent +bool NodeGraph::sceneMousePressEvent(QGraphicsSceneMouseEvent* event) +{ + // qDebug() << "Mouse Press!"; + if (event->button() == Qt::LeftButton) + mbStates.left = true; + if (event->button() == Qt::MiddleButton) + mbStates.middle = true; + if (event->button() == Qt::RightButton) + mbStates.right = true; + + // check for hit socket if left button is pressed + if (mbStates.left) { + auto scenePos = event->scenePos(); + auto rawPort = this->getPortAtScenePos(scenePos.x(), scenePos.y()); + if (!rawPort) { + // Record node positions for move-tracking (no port drag starting) + _preDragPositions.clear(); + for (auto& node : _scene->nodes) + _preDragPositions[node->id()] = node->getCenter(); + _trackingMove = true; + } + if (rawPort) { + // auto port = rawPort->node->getPortById(rawPort->id()); + // gotta cast to get the non-const version + PortPtr port = ((Port*)rawPort)->sharedFromThis(); + + // are we modifying an existing port with a connection? + if (port->portType == PortType::In) { + // in-sockets with an active connection are the only + // ones that can be edited, and that's the case here + if (port->connections.count() > 0) { + // get the connection + auto con = port->connections[0]; + + // emit connection removal signal + emit connectionRemoved(con); + + // remove it + _scene->removeConnection(con); + + // make it activeCon + con->endPort.clear(); + con->pos1 = con->startPort->scenePos(); + con->pos2 = scenePos; + activeCon = con; + activeCon->updatePathFromPositions(); + activeCon->connectState = ConnectionState::Dragging; + + this->_scene->addItem(activeCon.data()); + setDragMode(QGraphicsView::NoDrag); + return true; + } + else { + // allow starting connection from left to right? + } + } + // start new connection + else if (port->portType != PortType::Invalid /* in or out */) { + activeCon = ConnectionPtr(new Connection()); + activeCon->startPort = port; + activeCon->connectState = ConnectionState::Dragging; + + activeCon->pos1 = port->scenePos(); + activeCon->pos2 = scenePos; + activeCon->updatePathFromPositions(); + + this->_scene->addItem(activeCon.data()); + + // prevent further clicking of other items? + // event->ignore(); + setDragMode(QGraphicsView::NoDrag); + return true; + } + else { + qDebug() << "trying to hit an Invalid socket"; + } + + this->setDragMode(QGraphicsView::NoDrag); + } + } + + return false; +} + +bool NodeGraph::sceneMouseMoveEvent(QGraphicsSceneMouseEvent* event) +{ + auto scenePos = event->scenePos(); + if (mbStates.left && !!activeCon) { + auto rawPort = this->getPortAtScenePos(scenePos.x(), scenePos.y()); + if (rawPort) { + // snap if close enough + activeCon->pos2 = rawPort->scenePos(); + } + else { + activeCon->pos2 = scenePos; + } + activeCon->updatePathFromPositions(); + + // show socket names on nodes within proximity + for (auto node : _nodesWithSocketNamesShown) + node->setShowSocketNames(false); + _nodesWithSocketNamesShown.clear(); + + for (auto& nodePtr : _scene->nodes) { + auto node = nodePtr.data(); + QPointF center = node->getCenter(); + qreal dx = center.x() - scenePos.x(); + qreal dy = center.y() - scenePos.y(); + if (dx * dx + dy * dy < SOCKET_LABEL_RADIUS * SOCKET_LABEL_RADIUS) { + node->setShowSocketNames(true); + _nodesWithSocketNamesShown.append(node); + } + } + } + + return false; +} + +bool NodeGraph::sceneMouseReleaseEvent(QGraphicsSceneMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) + mbStates.left = false; + if (event->button() == Qt::MiddleButton) + mbStates.middle = false; + if (event->button() == Qt::RightButton) + mbStates.right = false; + + auto scenePos = event->scenePos(); + + if (mbStates.left == false && !!activeCon) { + auto rawPort = this->getPortAtScenePos(scenePos.x(), scenePos.y()); + if (rawPort) { + // auto hitPort = rawPort->node->getPortById(rawPort->id()); + // gotta cast to get the non-const version + PortPtr hitPort = ((Port*)rawPort)->sharedFromThis(); + + // determine between the in and out sockets + PortPtr leftPort; + PortPtr rightPort; + + if (hitPort->portType == PortType::In) { + leftPort = activeCon->startPort; + rightPort = hitPort; + } + else { + leftPort = hitPort; + rightPort = activeCon->startPort; + } + + // check validity of the potential connection + // NOTE: there's still a chance both leftPort and rightPort are + // still the same type or the same port + + bool isConValid = true; + + if (leftPort == rightPort) { + isConValid = false; + qDebug() << "invalid port"; + } + + if (leftPort->portType == rightPort->portType) { + isConValid = false; + qDebug() << "ports are the same"; + } + + // it's okay to check this since the two prior checks + // would have invalidated same-type and same-port connections + // todo: override connection + if (rightPort->connections.count() != 0) { + isConValid = false; + qDebug() << "right port has existing connections"; + } + + // todo: check for cycle + + if (isConValid) { + // actually make connection + auto con = + _scene->connectNodes(leftPort->node, leftPort->name, + rightPort->node, rightPort->name); + + emit connectionAdded(con); + // todo: emit undo task + } + } + + // clear proximity socket labels + for (auto node : _nodesWithSocketNamesShown) + node->setShowSocketNames(false); + _nodesWithSocketNamesShown.clear(); + + // remove from scene + _scene->removeItem(activeCon.data()); + activeCon.clear(); + } + + // Emit move command if nodes changed position + if (_trackingMove && !activeCon) { + QMap newPositions; + bool moved = false; + for (auto it = _preDragPositions.begin(); it != _preDragPositions.end(); ++it) { + auto node = _scene->nodes.value(it.key()); + if (!node) + continue; + QPointF newPos = node->getCenter(); + newPositions[it.key()] = newPos; + if (newPos != it.value()) + moved = true; + } + if (moved) + emit itemsMoveFinished(_preDragPositions, newPositions); + } + _trackingMove = false; + + // important to reset drag! + this->setDragMode(QGraphicsView::RubberBandDrag); + return false; +} + +void NodeGraph::dragEnterEvent(QDragEnterEvent* evt) { evt->ignore(); } + +void NodeGraph::dragMoveEvent(QDragMoveEvent* evt) { evt->ignore(); } + +void NodeGraph::dropEvent(QDropEvent* evt) { evt->ignore(); } + +const Port* NodeGraph::getPortAtScenePos(float x, float y) const +{ + auto items = this->_scene->items(QPointF(x, y)); + // auto items = this->items(); + for (auto item : items) { + if (item && item->type() == (int)SceneItemType::Port) + return (const Port*)item; + } + + return nullptr; +} + +// get top-most node +const Node* NodeGraph::getNodeAtScenePos(float x, float y) const +{ + auto items = this->_scene->items(QPointF(x, y)); + + for (auto item : items) { + if (item && item->type() == (int)SceneItemType::Node) + return (const Node*)item; + } + + return nullptr; +} + +void NodeGraph::handleSelectionChange() +{ + auto selected = this->_scene->selectedItems(); + for (auto item : selected) { + if (item->type() == (int)SceneItemType::Node) { + // emit nulls first so downstream handlers clear before setting new selection + emit frameSelectionChanged(FramePtr(nullptr)); + emit commentSelectionChanged(CommentPtr(nullptr)); + emit nodeSelectionChanged(((Node*)item)->sharedFromThis()); + return; + } + if (item->type() == (int)SceneItemType::Frame) { + emit nodeSelectionChanged(NodePtr(nullptr)); + emit commentSelectionChanged(CommentPtr(nullptr)); + emit frameSelectionChanged(((Frame*)item)->sharedFromThis()); + return; + } + if (item->type() == (int)SceneItemType::Comment) { + emit nodeSelectionChanged(NodePtr(nullptr)); + emit frameSelectionChanged(FramePtr(nullptr)); + emit commentSelectionChanged(((Comment*)item)->sharedFromThis()); + return; + } + } + + emit nodeSelectionChanged(NodePtr(nullptr)); + emit frameSelectionChanged(FramePtr(nullptr)); + emit commentSelectionChanged(CommentPtr(nullptr)); +} + +NodeGraph::~NodeGraph() {} + +} // namespace nodegraph \ No newline at end of file diff --git a/src/nodegraph/nodegraph.h b/src/nodegraph/nodegraph.h new file mode 100644 index 00000000..c2680b19 --- /dev/null +++ b/src/nodegraph/nodegraph.h @@ -0,0 +1,143 @@ +#pragma once + +#include "graph/scene.h" +#include +#include +#include +#include + +class QWidget; +class QWheelEvent; +class QKeyEvent; +class QMouseEvent; +class QPainter; +class QWheelEvent; +class QShowEvent; + +namespace nodegraph { +class Scene; +class Node; +class Connection; +typedef QSharedPointer ScenePtr; +typedef QSharedPointer NodePtr; +typedef QSharedPointer ConnectionPtr; +class Port; + +struct MouseButtonStates { + bool left; + bool middle; + bool right; + + MouseButtonStates(); + + // reset all to false + void reset(); +}; + +/* +This class draws a lot of inspiration from NodeGraphQt +https://github.com/jchanvfx/NodeGraphQt/blob/master/NodeGraphQt/widgets/viewer.py +*/ +class NodeGraph : public QGraphicsView { + Q_OBJECT +public: + NodeGraph(QWidget* parent = nullptr); + + ScenePtr scene() const { return _scene; } + void setNodeGraphScene(const ScenePtr& scene); + + void scaleUp(); + + void scaleDown(); + + virtual ~NodeGraph(); + +protected: + void wheelEvent(QWheelEvent* event) override; + + void keyPressEvent(QKeyEvent* event) override; + + void keyReleaseEvent(QKeyEvent* event) override; + + void mousePressEvent(QMouseEvent* event) override; + + void mouseMoveEvent(QMouseEvent* event) override; + + void mouseReleaseEvent(QMouseEvent* event) override; + + void mouseDoubleClickEvent(QMouseEvent* event) override; + + void drawBackground(QPainter* painter, const QRectF& r) override; + + void showEvent(QShowEvent* event) override; + + bool eventFilter(QObject* o, QEvent* e) override; + + // events coming from the scene + // void sceneKeyPressEvent(QKeyEvent *event); + + // void sceneKeyReleaseEvent(QKeyEvent *event); + + // NOTE: these functions return true if they swallow the event + // this is because they're implemented using eventFilters and + // that's how eventFilters work in qt + bool sceneMousePressEvent(QGraphicsSceneMouseEvent* event); + + bool sceneMouseMoveEvent(QGraphicsSceneMouseEvent* event); + + bool sceneMouseReleaseEvent(QGraphicsSceneMouseEvent* event); + + // allow drag and drop + // https://stackoverflow.com/a/7210404 + // must ignore the events in nodegraph so they + // propagate to the parent widget + void dragEnterEvent(QDragEnterEvent* evt); + void dragMoveEvent(QDragMoveEvent* event); + void dropEvent(QDropEvent* event); + + const Port* getPortAtScenePos(float x, float y) const; + const Node* getNodeAtScenePos(float x, float y) const; + void handleSelectionChange(); + +private: + static constexpr float SOCKET_LABEL_RADIUS = 150.0f; + + QPointF _clickPos; + ScenePtr _scene; + MouseButtonStates mbStates; + ConnectionPtr activeCon; + + QList nodes; + QList cons; + QList _nodesWithSocketNamesShown; + + // Position tracking for move commands + bool _trackingMove = false; + QMap _preDragPositions; + +signals: + void connectionAdded(ConnectionPtr con); + void connectionRemoved(ConnectionPtr con); + void nodeAdded(NodePtr node); + void nodeRemoved(NodePtr node); + + // Emitted instead of directly deleting; GraphWidget pushes the undo command + void deleteRequested(QList nodes, + QList frames, + QList comments); + + // Emitted on mouse-release when selected nodes moved; oldPos/newPos keyed by node id + void itemsMoveFinished(QMap oldPositions, + QMap newPositions); + + // null nodeptr means no active node selected + void nodeSelectionChanged(const NodePtr& node); + void nodeDoubleClicked(const NodePtr& node); + + // null ptr means no active frame/comment selected + void frameSelectionChanged(const FramePtr& frame); + void commentSelectionChanged(const CommentPtr& comment); + + void itemsDeleted(QList nodes, QList cons); +}; +} // namespace nodegraph \ No newline at end of file diff --git a/src/nodegraph/utils.h b/src/nodegraph/utils.h new file mode 100644 index 00000000..6b9ceb66 --- /dev/null +++ b/src/nodegraph/utils.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +QString genGuid() +{ + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} \ No newline at end of file diff --git a/src/router.ts b/src/router.ts deleted file mode 100644 index 2c15b699..00000000 --- a/src/router.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Vue from "vue"; -import Router from "vue-router"; -import Home from "./views/Home.vue"; - -Vue.use(Router); - -export default new Router({ - routes: [ - { - path: "/", - name: "home", - component: Home - }, - { - path: "/about", - name: "about", - // route level code-splitting - // this generates a separate chunk (about.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => - import(/* webpackChunkName: "about" */ "./views/About.vue") - } - ] -}); diff --git a/src/settings.ts b/src/settings.ts deleted file mode 100644 index ffd0c0b0..00000000 --- a/src/settings.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum Settings { - RecentFiles = "recent_files" -} diff --git a/src/shims-tsx.d.ts b/src/shims-tsx.d.ts deleted file mode 100644 index 151d2983..00000000 --- a/src/shims-tsx.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Vue, { VNode } from "vue"; - -declare global { - namespace JSX { - // tslint:disable no-empty-interface - type Element = VNode; - // tslint:disable no-empty-interface - type ElementClass = Vue; - interface IntrinsicElements { - [elem: string]: any; - } - } -} diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts deleted file mode 100644 index 9ab7f9cd..00000000 --- a/src/shims-vue.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module "*.vue" { - import Vue from "vue"; - export default Vue; -} - -declare module "*.json" { - const value: { [key: string]: any }; - export default value; -} diff --git a/src/store.ts b/src/store.ts deleted file mode 100644 index 0a6a3f7e..00000000 --- a/src/store.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Vue from "vue"; -import Vuex from "vuex"; - -Vue.use(Vuex); - -export default new Vuex.Store({ - state: {}, - mutations: {}, - actions: {} -}); diff --git a/src/texturelab/CMakeLists.txt b/src/texturelab/CMakeLists.txt new file mode 100644 index 00000000..677eae80 --- /dev/null +++ b/src/texturelab/CMakeLists.txt @@ -0,0 +1,397 @@ +cmake_minimum_required(VERSION 3.10) + +project(texturelab VERSION 0.4.0 LANGUAGES CXX) + +# Optional pre-release suffix appended to the numeric project() version above to +# form the full app version (e.g. "0.4.0" + "-beta" = "0.4.0-beta"); leave empty +# for a final release. The full string is the in-app version / Sentry release +# (TEXTURELAB_VERSION compile def below) and is reconstructed from these same two +# lines by the build workflow (.github/workflows/build.yml) for artifact names. +set(TEXTURELAB_VERSION_SUFFIX "-beta") + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets OpenGL) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets OpenGLWidgets Network) +find_package(OpenGL REQUIRED) + +set(LIBRARYV1 + ./libraries/v1/blend.cpp + ./libraries/v1/brickgenerator.cpp + ./libraries/v1/brightnesscontrast.cpp + ./libraries/v1/cell.cpp + ./libraries/v1/checkerboard.cpp + ./libraries/v1/copy.cpp + ./libraries/v1/color.cpp + ./libraries/v1/directionalwarp.cpp + ./libraries/v1/fractalnoise.cpp + ./libraries/v1/gradient.cpp + ./libraries/v1/gradientmap.cpp + ./libraries/v1/heightshift.cpp + ./libraries/v1/hexagon.cpp + ./libraries/v1/invert.cpp + ./libraries/v1/linecell.cpp + ./libraries/v1/maprange.cpp + ./libraries/v1/mask.cpp + ./libraries/v1/mirror.cpp + ./libraries/v1/normalmap.cpp + ./libraries/v1/output.cpp + ./libraries/v1/perlin3d.cpp + ./libraries/v1/shapes.cpp + ./libraries/v1/solidcell.cpp + ./libraries/v1/splat.cpp + ./libraries/v1/threshold.cpp + ./libraries/v1/tile.cpp + ./libraries/v1/transform2d.cpp + ./libraries/v1/warp.cpp + ./libraries/v1/wave.cpp +) + +set(LIBRARYV2 + ./libraries/v2/advancesplatter.cpp + ./libraries/v2/anisotropicblur.cpp + ./libraries/v2/bevel.cpp + ./libraries/v2/blur.cpp + ./libraries/v2/capsule.cpp + ./libraries/v2/cartesiantopolar.cpp + ./libraries/v2/circularsplatter.cpp + ./libraries/v2/clamp.cpp + ./libraries/v2/combinenormals.cpp + ./libraries/v2/directionalblur.cpp + ./libraries/v2/directionalwarpv2.cpp + ./libraries/v2/extractchannel.cpp + ./libraries/v2/floodfill.cpp + ./libraries/v2/floodfillsampler.cpp + ./libraries/v2/floodfilltobbox.cpp + ./libraries/v2/floodfilltocolor.cpp + ./libraries/v2/floodfilltogradient.cpp + ./libraries/v2/floodfilltorandomcolor.cpp + ./libraries/v2/floodfilltorandomintensity.cpp + ./libraries/v2/gradientdynamic.cpp + ./libraries/v2/gradientnoise.cpp + ./libraries/v2/gradientnoisefractalsum.cpp + ./libraries/v2/grayscale.cpp + ./libraries/v2/histogramscan.cpp + ./libraries/v2/histogramselect.cpp + ./libraries/v2/histogramshift.cpp + ./libraries/v2/hsl.cpp + ./libraries/v2/hslextract.cpp + ./libraries/v2/imagenode.cpp + ./libraries/v2/invertnormal.cpp + ./libraries/v2/normalmapv2.cpp + ./libraries/v2/polartocartesian.cpp + ./libraries/v2/pow.cpp + ./libraries/v2/quantize.cpp + ./libraries/v2/rgbamerge.cpp + ./libraries/v2/rgbashuffle.cpp + ./libraries/v2/simplexnoisev2.cpp + ./libraries/v2/skew.cpp + ./libraries/v2/slopeblur.cpp + ./libraries/v2/softflower.cpp + ./libraries/v2/splatv2.cpp + ./libraries/v2/star.cpp + ./libraries/v2/stripes.cpp + ./libraries/v2/tilesampler.cpp + ./libraries/v2/transform2dv2.cpp + ./libraries/v2/valuenoise.cpp + ./libraries/v2/valuenoisefractalsum.cpp + ./libraries/v2/warpv2.cpp +) + +set(LIBRARYV3 + ./libraries/v3/ambientocclusion.cpp + ./libraries/v3/bevelv2.cpp + ./libraries/v3/curvenode.cpp + ./libraries/v3/curvature.cpp + ./libraries/v3/maskedblur.cpp + ./libraries/v3/rays.cpp + ./libraries/v3/swirl.cpp + ./libraries/v3/floodfillv2.cpp + ./libraries/v3/floodfillv2tocolor.cpp + ./libraries/v3/floodfillv2torandomcolor.cpp + ./libraries/v3/floodfillv2torandomintensity.cpp + ./libraries/v3/floodfillv2tobbox.cpp + ./libraries/v3/floodfillv2togradient.cpp + ./libraries/v3/floodfillv2sampler.cpp + # Phase 1 — Filters / Color + ./libraries/v3/blendv3.cpp + ./libraries/v3/edgedetect.cpp + ./libraries/v3/highpass.cpp + ./libraries/v3/emboss.cpp + ./libraries/v3/vibrance.cpp + ./libraries/v3/colortomask.cpp + ./libraries/v3/setalpha.cpp + ./libraries/v3/toongradient.cpp + ./libraries/v3/autolevels.cpp + # Phase 2 — Generators + ./libraries/v3/bricks2.cpp + ./libraries/v3/directionalscratches.cpp + ./libraries/v3/roughgrain.cpp + ./libraries/v3/voronoifractal.cpp + ./libraries/v3/truchet.cpp + ./libraries/v3/fbmdomainwarp.cpp + ./libraries/v3/perlinnoise.cpp + ./libraries/v3/perlinnoise3d.cpp + ./libraries/v3/cellv3.cpp + ./libraries/v3/linecellv3.cpp + ./libraries/v3/solidcellv3.cpp + # Phase 3 — Multi-pass + ./libraries/v3/blurhq.cpp + ./libraries/v3/distancetransform.cpp + ./libraries/v3/spread.cpp + ./libraries/v3/fastao.cpp + ./libraries/v3/heightblend.cpp + ./libraries/v3/atlas2x2.cpp + ./libraries/v3/makeittile.cpp + ./libraries/v3/normalmapv3.cpp +) + +set(PROJECT_SOURCES + ./main.cpp + ./telemetry.h + ./telemetry.cpp + ./systeminfo.h + ./systeminfo.cpp + ./catalogservice.h + ./catalogservice.cpp + ./launcher/launcherwindow.h + ./launcher/launcherwindow.cpp + ./launcher/texturelistmodel.h + ./launcher/texturelistmodel.cpp + ./launcher/texturecarddelegate.h + ./launcher/texturecarddelegate.cpp + ./launcher/texturerowdelegate.h + ./launcher/texturerowdelegate.cpp + ./launcher/launcherformat.h + ./launcher/launcherformat.cpp + ./update/versioncompare.h + ./update/versioncompare.cpp + ./update/updatechecker.h + ./update/updatechecker.cpp + ./mainwindow.cpp + ./mainwindow.h + ./clipboard.h + ./clipboard.cpp + ./exporter.h + ./exporter.cpp + ./utils.h + ./jsonutils.h + ./curve.h + ./curve.cpp + ./models.h + ./models.cpp + ./project.h + ./project.cpp + ./props.h + ./props.cpp + ./libraries/libv2.h + ./libraries/libv3.h + ./libraries/library.h + ./libraries/library.cpp + ./libraries/libversion.h + ./libraries/libversion.cpp + ./libraries/libraryversionmigrator.h + ./libraries/libraryversionmigrator.cpp + ./widgets/graphwidget.h + ./widgets/graphwidget.cpp + ./widgets/librarywidget.h + ./widgets/librarywidget.cpp + ./widgets/nodesearchpopup.h + ./widgets/nodesearchpopup.cpp + ./widgets/properties/propertieswidget.h + ./widgets/properties/propertieswidget.cpp + ./widgets/properties/propwidgets.h + ./widgets/properties/propwidgets.cpp + ./widgets/properties/curvepropwidget.h + ./widgets/properties/curvepropwidget.cpp + ./widgets/properties/accordionwidget.h + ./widgets/properties/accordionwidget.cpp + ./widgets/view2dwidget.h + ./widgets/view2dwidget.cpp + ./widgets/view3dwidget.h + ./widgets/view3dwidget.cpp + ./undo/undocommands.h + ./undo/undocommandids.h + ./undo/addnodecommand.h + ./undo/addnodecommand.cpp + ./undo/deleteitemscommand.h + ./undo/deleteitemscommand.cpp + ./undo/addconnectioncommand.h + ./undo/addconnectioncommand.cpp + ./undo/removeconnectioncommand.h + ./undo/removeconnectioncommand.cpp + ./undo/moveitemscommand.h + ./undo/moveitemscommand.cpp + ./undo/propertychangecommand.h + ./undo/propertychangecommand.cpp + ./undo/randomseedchangecommand.h + ./undo/randomseedchangecommand.cpp + ./undo/addframecommand.h + ./undo/addframecommand.cpp + ./undo/addcommentcommand.h + ./undo/addcommentcommand.cpp + ./undo/editframecommand.h + ./undo/editframecommand.cpp + ./undo/editcommentcommand.h + ./undo/editcommentcommand.cpp + ./undo/pastecommand.h + ./undo/pastecommand.cpp + ./undo/texturechannelassigncommand.h + ./undo/texturechannelassigncommand.cpp + ./widgets/aboutdialog.h + ./widgets/aboutdialog.cpp + ./widgets/crashconsentdialog.h + ./widgets/crashconsentdialog.cpp + ./widgets/exportdialog.h + ./widgets/exportdialog.cpp + ./graphics/texturerenderer.h + ./graphics/texturerenderer.cpp + ./graphics/noderenderer.h + ./graphics/noderenderer.cpp + ./graphics/renderworker.h + ./graphics/renderworker.cpp + ${LIBRARYV1} + ${LIBRARYV2} + ${LIBRARYV3} +) + +set(PROJECT_RESOURCES + ./assets.qrc + ${CMAKE_SOURCE_DIR}/resources/theme.qrc) + +# Platform application icons. The runtime window icon is set separately in +# main.cpp (QApplication::setWindowIcon); these give the built artifact its icon. +set(APP_ICON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../icons) +if(WIN32) + # Embedded into the .exe via a resource script. + list(APPEND PROJECT_RESOURCES ./texturelab.rc) +elseif(APPLE) + # Copied into TextureLab.app/Contents/Resources and named in Info.plist + # through MACOSX_BUNDLE_ICON_FILE below. + set(APP_ICON_MACOS ${APP_ICON_DIR}/logo.icns) + set_source_files_properties(${APP_ICON_MACOS} PROPERTIES + MACOSX_PACKAGE_LOCATION Resources) + list(APPEND PROJECT_RESOURCES ${APP_ICON_MACOS}) +endif() + +if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) + qt_add_executable(texturelab + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + ${PROJECT_RESOURCES} + ) +else() + add_executable(texturelab + ${PROJECT_SOURCES} + ${PROJECT_RESOURCES} + ) +endif() + +# note: openglwidgets is qt6 only +target_link_libraries(texturelab PRIVATE Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Network + Qt${QT_VERSION_MAJOR}::OpenGL + Qt${QT_VERSION_MAJOR}::OpenGLWidgets + OpenGL::GL + qtadvanceddocking-qt6 + theme + nodegraph + viewer3d + colorpicker + catalog + sentry + ) + +target_include_directories(texturelab PUBLIC + ../ads/src + ../theme + ../nodegraph + ../viewer3d + ../colorpicker + ) +# DSN is empty by default for local builds (Sentry init becomes a no-op) +if(NOT DEFINED TEXTURELAB_SENTRY_DSN) + set(TEXTURELAB_SENTRY_DSN "") +endif() + +target_compile_definitions(texturelab PRIVATE + TEXTURELAB_VERSION="${PROJECT_VERSION}${TEXTURELAB_VERSION_SUFFIX}" + TEXTURELAB_SENTRY_DSN="${TEXTURELAB_SENTRY_DSN}" + # Absolute path to the theme source files, so `--dev-theme` can live-reload + # colors/QSS from disk without a rebuild. Dev convenience only. + TEXTURELAB_SOURCE_RESOURCES="${CMAKE_SOURCE_DIR}/resources" + # Debug builds auto-enable theme hot-reload (no --dev-theme flag needed); + # override at runtime with --no-dev-theme. + $<$:TEXTURELAB_DEV_BUILD> +) + +# Generate version.h (with git hash) at every build, not just configure time +add_custom_target(texturelab_version + COMMAND ${CMAKE_COMMAND} + -DSOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}" + -DBINARY_DIR="${CMAKE_CURRENT_BINARY_DIR}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/GetGitHash.cmake" + BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/version.h" + COMMENT "Generating version.h" +) +add_dependencies(texturelab texturelab_version) +target_include_directories(texturelab PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + +# MSVC: deterministically emit a PDB and embed a CodeView record in the exe, in +# every config (incl. Release). Without the CodeView record the shipped exe has +# no debug_id, so Sentry cannot match any PDB to a crash minidump ("Unknown +# function" frames). Attaching to the target is reliable; injecting /Zi + /DEBUG +# via CMAKE_*_FLAGS_RELEASE on the command line was not (VS generator + Qt). +if(MSVC) + target_compile_options(texturelab PRIVATE /Zi) + target_link_options(texturelab PRIVATE /DEBUG /OPT:REF /OPT:ICF /INCREMENTAL:NO) +endif() + +# Copy crashpad_handler next to the executable so handler_path resolves at runtime +if(TARGET crashpad_handler) + add_custom_command(TARGET texturelab POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + COMMENT "Copying crashpad_handler next to texturelab" + ) +endif() + +set_target_properties(texturelab PROPERTIES + MACOSX_BUNDLE_GUI_IDENTIFIER io.texturelab.app + MACOSX_BUNDLE_ICON_FILE logo.icns + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) + + +install(TARGETS texturelab + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +# Linux executables carry no icon; desktops look it up through the .desktop +# entry's Icon= name in the hicolor theme. +if(UNIX AND NOT APPLE) + include(GNUInstallDirs) + install(FILES ${CMAKE_SOURCE_DIR}/packaging/linux/texturelab.desktop + DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) + install(FILES ${APP_ICON_DIR}/logo.svg + DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps + RENAME texturelab.svg) + install(FILES ${APP_ICON_DIR}/logo.png + DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/512x512/apps + RENAME texturelab.png) +endif() + +if(QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(texturelab) +endif() diff --git a/src/texturelab/GetGitHash.cmake b/src/texturelab/GetGitHash.cmake new file mode 100644 index 00000000..bb920925 --- /dev/null +++ b/src/texturelab/GetGitHash.cmake @@ -0,0 +1,10 @@ +execute_process( + COMMAND git -C "${SOURCE_DIR}" rev-parse --short HEAD + OUTPUT_VARIABLE TEXTURELAB_BUILD_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET +) +if(NOT TEXTURELAB_BUILD_HASH) + set(TEXTURELAB_BUILD_HASH "unknown") +endif() +configure_file("${SOURCE_DIR}/version.h.in" "${BINARY_DIR}/version.h" @ONLY) diff --git a/src/texturelab/assets.qrc b/src/texturelab/assets.qrc new file mode 100644 index 00000000..9272cdcc --- /dev/null +++ b/src/texturelab/assets.qrc @@ -0,0 +1,130 @@ + + + ../../public/assets/nodes/bevel.png + ../../public/assets/nodes/circle.png + ../../public/assets/nodes/color.png + ../../public/assets/nodes/colorize.png + ../../public/assets/nodes/blend.png + ../../public/assets/nodes/output.png + ../../public/assets/nodes/brickgenerator.png + ../../public/assets/nodes/brightnesscontrast.png + ../../public/assets/nodes/cell.png + ../../public/assets/nodes/checkerboard.png + ../../public/assets/nodes/copy.png + ../../public/assets/nodes/fractalnoise.png + ../../public/assets/nodes/directionalwarp.png + ../../public/assets/nodes/gradient.png + ../../public/assets/nodes/gradientmap.png + ../../public/assets/nodes/heightshift.png + ../../public/assets/nodes/hexagon.png + ../../public/assets/nodes/invert.png + ../../public/assets/nodes/linecell.png + ../../public/assets/nodes/maprange.png + ../../public/assets/nodes/mask.png + ../../public/assets/nodes/mirror.png + ../../public/assets/nodes/normalmap.png + ../../public/assets/nodes/output.png + ../../public/assets/nodes/perlin3d.png + + ../../public/assets/nodes/simplexnoise.png + ../../public/assets/nodes/solidcell.png + ../../public/assets/nodes/splat.png + ../../public/assets/nodes/threshold.png + ../../public/assets/nodes/tile.png + ../../public/assets/nodes/transform2d.png + ../../public/assets/nodes/warp.png + ../../public/assets/nodes/wave.png + + ../../public/assets/nodes/advancesplatter.png + ../../public/assets/nodes/anisotropicblur.png + ../../public/assets/nodes/bevel.png + ../../public/assets/nodes/blurv2.png + ../../public/assets/nodes/capsule.png + ../../public/assets/nodes/cartesiantopolar.png + ../../public/assets/nodes/circularsplatter.png + ../../public/assets/nodes/clamp.png + ../../public/assets/nodes/combinenormals.png + ../../public/assets/nodes/directionalblur.png + + ../../public/assets/nodes/extractchannel.png + ../../public/assets/nodes/floodfill.png + ../../public/assets/nodes/floodfillsampler.png + ../../public/assets/nodes/floodfilltobbox.png + ../../public/assets/nodes/floodfilltocolor.png + ../../public/assets/nodes/floodfilltogradient.png + ../../public/assets/nodes/floodfilltorandomcolor.png + ../../public/assets/nodes/floodfilltorandomintensity.png + ../../public/assets/nodes/gradientdynamic.png + ../../public/assets/nodes/gradientnoise.png + ../../public/assets/nodes/gradientnoisefractalsum.png + ../../public/assets/nodes/grayscale.png + ../../public/assets/nodes/histogramscan.png + ../../public/assets/nodes/histogramselect.png + ../../public/assets/nodes/histogramshift.png + ../../public/assets/nodes/hsl.png + ../../public/assets/nodes/hslextract.png + ../../public/assets/nodes/image.png + ../../public/assets/nodes/invertnormal.png + + ../../public/assets/nodes/polartocartesian.png + ../../public/assets/nodes/pow.png + ../../public/assets/nodes/quantize.png + ../../public/assets/nodes/rgbamerge.png + ../../public/assets/nodes/rgbashuffle.png + ../../public/assets/nodes/simplexnoise.png + ../../public/assets/nodes/skew.png + ../../public/assets/nodes/slopeblur.png + ../../public/assets/nodes/softflower.png + ../../public/assets/nodes/splat.png + ../../public/assets/nodes/star.png + ../../public/assets/nodes/stripes.png + ../../public/assets/nodes/tilesampler.png + + ../../public/assets/nodes/valuenoise.png + ../../public/assets/nodes/valuenoisefractalsum.png + + + ../../public/assets/nodes/curve.png + + + ../../public/assets/nodes/frame.png + ../../public/assets/nodes/comment.png + + + ../icons/save.svg + ../icons/grid.svg + ../icons/crosshair.svg + ../icons/copy.svg + ../icons/undo.svg + ../icons/redo.svg + ../icons/export.svg + ../icons/download.svg + ../icons/chevron-down.svg + ../icons/chevron-up.svg + ../icons/logo.png + + + ../../public/assets/env/docklands_01/docklands_01_1k.hdr + ../../public/assets/env/golden_bay/golden_bay_1k.hdr + ../../public/assets/env/little_paris_eiffel_tower/little_paris_eiffel_tower_1k.hdr + ../../public/assets/env/sepulchral_chapel_basement/sepulchral_chapel_basement_1k.hdr + ../../public/assets/env/st_peters_square_night/st_peters_square_night_1k.hdr + ../../public/assets/env/stadium_01/stadium_01_1k.hdr + ../../public/assets/env/studio_kontrast_03/studio_kontrast_03_1k.hdr + ../../public/assets/env/sunny_rose_garden_1k.hdr + ../../public/assets/env/university_workshop/university_workshop_1k.hdr + ../../public/assets/env/winter_river/winter_river_1k.hdr + + + ../../public/assets/examples/Copper.texture + ../../public/assets/examples/FoilGasket.texture + ../../public/assets/examples/GoldLinedMarbleTiles.texture + ../../public/assets/examples/Grass.texture + ../../public/assets/examples/GrassyRock.texture + ../../public/assets/examples/Grenade.texture + ../../public/assets/examples/Sand.texture + ../../public/assets/examples/Screws.texture + ../../public/assets/examples/WoodenPlanks.texture + ../../public/assets/examples/YellowTiles.texture + + \ No newline at end of file diff --git a/src/texturelab/catalogservice.cpp b/src/texturelab/catalogservice.cpp new file mode 100644 index 00000000..a124e8a1 --- /dev/null +++ b/src/texturelab/catalogservice.cpp @@ -0,0 +1,398 @@ +#include "catalogservice.h" + +#include "models.h" +#include "telemetry.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +qint64 nowMs() +{ + return QDateTime::currentMSecsSinceEpoch(); +} + +// TextureChannel is the app's enum; ChannelBit is the catalog's. They are +// deliberately separate types — see texturerecord.h — so this is the one place +// that knows both. Adding a channel means adding it here and to ChannelBit, +// never renumbering the existing bits. +int channelBitFor(TextureChannel channel) +{ + switch (channel) { + case TextureChannel::Albedo: + return catalog::ChannelAlbedo; + case TextureChannel::Normal: + return catalog::ChannelNormal; + case TextureChannel::Metalness: + return catalog::ChannelMetalness; + case TextureChannel::Roughness: + return catalog::ChannelRoughness; + case TextureChannel::Height: + return catalog::ChannelHeight; + case TextureChannel::Alpha: + return catalog::ChannelAlpha; + case TextureChannel::AO: + return catalog::ChannelAO; + case TextureChannel::None: + break; + } + return catalog::ChannelNone; +} + +// Walks every known path and records what's actually on disk. +// +// Opens its own database connections rather than borrowing the service's: a +// QSqlDatabase belongs to the thread that opened it, and sharing one across +// threads is the kind of bug that shows up as a corrupt read months later. +class ReconcileTask : public QRunnable { +public: + ReconcileTask(CatalogService* owner, const QString& indexPath, const QString& thumbsPath) + : service(owner), indexFile(indexPath), thumbsFile(thumbsPath) + { + setAutoDelete(true); + } + + void run() override + { + catalog::CatalogIndex index; + if (!index.open(indexFile)) { + report(0, 0); + return; + } + if (index.isReadOnly()) { + report(0, 0); + return; + } + + catalog::ThumbnailCache thumbs; + const bool haveThumbs = thumbs.open(thumbsFile); + + int missing = 0; + int updated = 0; + + for (const catalog::TextureRecord& rec : index.allRecords()) { + const QFileInfo info(rec.path); + + if (!info.exists()) { + if (!rec.isMissing()) { + index.markMissing(rec.id, nowMs()); + ++missing; + } + continue; + } + + const qint64 size = info.size(); + const qint64 mtime = info.lastModified().toMSecsSinceEpoch(); + + if (size == rec.fileSize && mtime == rec.fileMtime && !rec.isMissing()) + continue; + + // Size or mtime differ: the file was edited outside the app, so its + // cached thumbnail shows a material that no longer exists. Dropping + // it here is the whole reason this comparison exists — with no + // content hash, nothing else would ever notice. + if (haveThumbs && (size != rec.fileSize || mtime != rec.fileMtime)) + thumbs.removeTexture(rec.id); + + index.markPresent(rec.id, size, mtime); + ++updated; + } + + report(missing, updated); + } + +private: + void report(int missing, int updated) + { + // Back to the GUI thread; the worker's connections are closed by the + // time anyone reacts to this. + QMetaObject::invokeMethod( + service, [owner = service, missing, updated]() { + owner->onReconcileFinished(missing, updated); + }, + Qt::QueuedConnection); + } + + CatalogService* service; + QString indexFile; + QString thumbsFile; +}; + +} // namespace + +CatalogService& CatalogService::instance() +{ + // Heap-allocated and deliberately never deleted. + // + // A function-local static would be destroyed by __run_exit_handlers, which + // runs *after* Qt has torn down its own globals. Closing a QSqlDatabase at + // that point walks a driver registry whose lock has already been destroyed, + // and the process segfaults on exit — after a clean run, which makes it look + // like a crash on quit rather than a teardown-order bug. + // + // shutdown(), called from main() while Qt is still up, is the orderly path; + // leaking this object is what guarantees no destructor runs later. + static CatalogService* service = new CatalogService(); + return *service; +} + +void CatalogService::shutdown() +{ + if (!ready) + return; + + // An in-flight reconciliation holds its own connections and would otherwise + // still be writing while we close ours. + QThreadPool::globalInstance()->waitForDone(5000); + + thumbCache.close(); + catalogIndex.close(); + ready = false; +} + +QString CatalogService::dataDir() +{ + // Matches what Telemetry already does for the crash database, rather than + // the ~/.texturelab the spec sketched — one convention per app, and this + // one is correct on Windows and macOS too. + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); +} + +QString CatalogService::indexPath() +{ + return dataDir() + QStringLiteral("/index.db"); +} + +QString CatalogService::thumbsPath() +{ + return dataDir() + QStringLiteral("/thumbs.db"); +} + +bool CatalogService::init() +{ + if (ready) + return true; + + QDir().mkpath(dataDir()); + + if (!catalogIndex.open(indexPath())) { + qWarning("catalog: index unavailable (%s); launcher will be empty", + qPrintable(catalogIndex.lastError())); + Telemetry::breadcrumb("catalog", "index open failed"); + return false; + } + + // A cache that won't open is not a reason to fail: every read is allowed to + // miss, and cards fall back to placeholders. + if (!thumbCache.open(thumbsPath())) + qWarning("catalog: thumbnail cache unavailable; cards will show placeholders"); + + ready = true; + + if (!catalogIndex.isReadOnly()) + seedFromRecentFiles(); + + // No path: it sits under the user's home directory, and the consent prompt + // says we don't send folder paths. + Telemetry::breadcrumb("catalog", "index opened", + {{"read_only", catalogIndex.isReadOnly()}}); + return true; +} + +void CatalogService::seedFromRecentFiles() +{ + // index.db can't be rebuilt by rescanning (LAUNCHER_PRD.md §1.1), so the + // very first launch after this ships would otherwise show an empty window + // to someone with a year of work on disk. The old QSettings recents list is + // the one record of that history we already have. + QSettings settings(QSettings::UserScope, "texturelab", "texturelab"); + if (settings.value("catalogSeeded", false).toBool()) + return; + + const QStringList files = settings.value("recentFiles").toStringList(); + const qint64 now = nowMs(); + int seeded = 0; + + // Ordered most-recent-first with no timestamps, so synthesize descending + // ones a second apart. Preserving the order is the point; the absolute + // values are meaningless and get overwritten on first real open. + for (int i = 0; i < files.size(); ++i) { + const QFileInfo info(files[i]); + if (!info.exists()) + continue; + + catalog::TextureRecord rec; + rec.path = info.absoluteFilePath(); + rec.name = info.completeBaseName(); + rec.fileSize = info.size(); + rec.fileMtime = info.lastModified().toMSecsSinceEpoch(); + + // No metadata: reading it would mean parsing every file at startup, and + // it fills itself in the first time each one is opened. Until then the + // card shows a name and a date, which is what the recents menu showed. + if (catalogIndex.recordOpened(rec, now - qint64(i) * 1000)) + ++seeded; + } + + settings.setValue("catalogSeeded", true); + + if (seeded > 0) { + qInfo("catalog: seeded %d texture(s) from the recent-files list", seeded); + Telemetry::breadcrumb("catalog", "seeded " + std::to_string(seeded) + " from recents"); + emit catalogChanged(); + } +} + +catalog::TextureRecord CatalogService::recordFor(const TextureProjectPtr& project, + const QString& path) +{ + const QFileInfo info(path); + + catalog::TextureRecord rec; + rec.path = info.absoluteFilePath(); + rec.name = info.completeBaseName(); + rec.fileSize = info.size(); + rec.fileMtime = info.lastModified().toMSecsSinceEpoch(); + + if (!project) + return rec; + + // Read from the live project rather than re-parsing the JSON: it's already + // in memory at both write points, and it's the authority on anything the + // file format doesn't store. + rec.width = project->textureWidth; + rec.height = project->textureHeight; + rec.nodeCount = project->nodes.size(); + rec.libVersion = project->libraryVersion; + + int channels = catalog::ChannelNone; + for (auto it = project->textureChannels.begin(); it != project->textureChannels.end(); ++it) { + if (!it.value().isEmpty()) + channels |= channelBitFor(it.key()); + } + rec.channels = channels; + + return rec; +} + +qint64 CatalogService::recordOpened(const TextureProjectPtr& project, const QString& path) +{ + if (!ready || path.isEmpty()) + return -1; + + catalog::TextureRecord rec = recordFor(project, path); + if (!catalogIndex.recordOpened(rec, nowMs())) { + qWarning("catalog: could not record open of %s: %s", qPrintable(path), + qPrintable(catalogIndex.lastError())); + return -1; + } + + emit catalogChanged(); + return rec.id; +} + +qint64 CatalogService::recordSaved(const TextureProjectPtr& project, const QString& path) +{ + if (!ready || path.isEmpty()) + return -1; + + catalog::TextureRecord rec = recordFor(project, path); + if (!catalogIndex.recordSaved(rec, nowMs())) { + qWarning("catalog: could not record save of %s: %s", qPrintable(path), + qPrintable(catalogIndex.lastError())); + return -1; + } + + // The file just changed, so any cached thumbnail is of the old material. + // The caller captures a fresh one straight after this; dropping it here + // means a failed capture leaves a placeholder rather than a stale picture. + thumbCache.removeTexture(rec.id); + + emit catalogChanged(); + return rec.id; +} + +void CatalogService::captureThumbnail(qint64 textureId, const QImage& frame, + catalog::ThumbSource source) +{ + if (!ready || textureId < 0 || frame.isNull()) + return; + + const int side = qMin(frame.width(), frame.height()); + if (side < 32) + return; + + // Center-crop to a square: the viewport is whatever shape the user left the + // dock, and the cards are square. Cropping keeps the material at its own + // scale, where squashing to fit would distort it. + const QImage square = frame.copy((frame.width() - side) / 2, (frame.height() - side) / 2, + side, side); + + QVector entries; + for (int size : {512, 256}) { + const QImage scaled = + square.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + + QByteArray bytes; + QBuffer buffer(&bytes); + buffer.open(QIODevice::WriteOnly); + if (!scaled.save(&buffer, "JPG", 85)) + continue; + + catalog::ThumbnailCache::Entry entry; + entry.key.textureId = textureId; + entry.key.size = size; + entry.bytes = bytes; + entry.source = source; + entries << entry; + } + + if (entries.isEmpty()) + return; + + if (!thumbCache.putBatch(entries, nowMs())) { + qWarning("catalog: could not cache thumbnail: %s", qPrintable(thumbCache.lastError())); + return; + } + + emit catalogChanged(); +} + +void CatalogService::forget(qint64 textureId) +{ + if (!ready || textureId < 0) + return; + + thumbCache.removeTexture(textureId); + if (catalogIndex.remove(textureId)) + emit catalogChanged(); +} + +void CatalogService::reconcileAsync() +{ + if (!ready || reconcileRunning) + return; + + reconcileRunning = true; + QThreadPool::globalInstance()->start(new ReconcileTask(this, indexPath(), thumbsPath())); +} + +void CatalogService::onReconcileFinished(int missing, int updated) +{ + reconcileRunning = false; + + if (missing > 0 || updated > 0) { + qInfo("catalog: reconcile marked %d missing, refreshed %d", missing, updated); + emit catalogChanged(); + } + + emit reconcileFinished(missing, updated); +} diff --git a/src/texturelab/catalogservice.h b/src/texturelab/catalogservice.h new file mode 100644 index 00000000..b9526e91 --- /dev/null +++ b/src/texturelab/catalogservice.h @@ -0,0 +1,96 @@ +#pragma once + +#include "catalogindex.h" +#include "thumbnailcache.h" + +#include +#include +#include +#include + +class TextureProject; +typedef QSharedPointer TextureProjectPtr; + +// The boundary between the app and the launcher's data layer. +// +// src/catalog/ knows nothing about the node graph on purpose, so everything +// that has to understand a TextureProject — resolution, node count, which +// output channels are wired up, which library version — lives here. Keeping the +// translation in one place means the persisted schema can't drift just because +// the graph model changed. +// +// Owns the process's writable connections to both databases. The reconciliation +// pass opens its own on a worker thread; nothing else may touch these off the +// GUI thread. +class CatalogService : public QObject { + Q_OBJECT + +public: + static CatalogService& instance(); + + // Opens both databases, creating the data directory if needed, and seeds + // the index from the legacy recent-files list on first run. Safe to call + // more than once. Returns false if the index could not be opened — the app + // must still run in that case, just without a launcher. + bool init(); + bool isReady() const { return ready; } + + // Closes both databases while Qt is still alive. Must be called from main() + // after the event loop returns — see the comment on instance(). + void shutdown(); + + static QString dataDir(); + static QString indexPath(); + static QString thumbsPath(); + + catalog::CatalogIndex& index() { return catalogIndex; } + catalog::ThumbnailCache& thumbnails() { return thumbCache; } + + // --- write points (LAUNCHER_PRD.md §6.1) ------------------------------ + + // Both return the index row id, or -1 if the write failed. + qint64 recordOpened(const TextureProjectPtr& project, const QString& path); + qint64 recordSaved(const TextureProjectPtr& project, const QString& path); + + // Stores a thumbnail captured from the 3D viewport (LAUNCHER_PRD.md §4). + // `frame` is the raw grab; it gets center-cropped to a square and written at + // both cache sizes. A null or degenerate image is ignored rather than + // caching a blank card. + void captureThumbnail(qint64 textureId, const QImage& frame, catalog::ThumbSource source); + + // Forgets a texture and its thumbnails. Never touches the file on disk. + void forget(qint64 textureId); + + // --- reconciliation (LAUNCHER_PRD.md §6.2) ---------------------------- + + // stat()s every known path on a worker thread and updates missing/present + // state. Cheap — the row count is bounded by the user's own activity — but + // it runs off the GUI thread anyway, because one unmounted network path + // will block it regardless of how few rows there are. + void reconcileAsync(); + + // Builds the index row for a live project. Public for testing and for + // callers that want the metadata without writing it. + static catalog::TextureRecord recordFor(const TextureProjectPtr& project, const QString& path); + + // Internal: called on the GUI thread by the reconciliation task when it + // finishes. Not for general use. + void onReconcileFinished(int missing, int updated); + +signals: + // Emitted on the GUI thread after any change to the index, so a launcher + // model can refresh itself without polling. + void catalogChanged(); + + void reconcileFinished(int missing, int updated); + +private: + CatalogService() = default; + + void seedFromRecentFiles(); + + catalog::CatalogIndex catalogIndex; + catalog::ThumbnailCache thumbCache; + bool ready = false; + bool reconcileRunning = false; +}; diff --git a/src/texturelab/clipboard.cpp b/src/texturelab/clipboard.cpp new file mode 100644 index 00000000..1e4bda26 --- /dev/null +++ b/src/texturelab/clipboard.cpp @@ -0,0 +1,241 @@ +#include "clipboard.h" +#include "jsonutils.h" +#include "libraries/library.h" +#include "props.h" +#include +#include +#include +#include +#include +#include +#include + +const QString Clipboard::PREFIX = "texturelab-clipboard:"; + +void Clipboard::copyItems(TextureProjectPtr project, + const QList& nodeIds, + const QList& frameIds, + const QList& commentIds) +{ + QSet nodeIdSet(nodeIds.begin(), nodeIds.end()); + + QJsonObject root; + + // Nodes + QJsonArray nodeArray; + for (const auto& id : nodeIds) { + auto node = project->nodes.value(id); + if (!node) + continue; + + QJsonObject obj; + obj["id"] = node->id; + obj["typeName"] = node->typeName; + obj["exportName"] = node->exportName; + obj["randomSeed"] = (double)node->randomSeed; + obj["x"] = node->pos.x(); + obj["y"] = node->pos.y(); + + QJsonObject props; + for (auto key : node->props.keys()) + props[key] = node->props[key]->toJsonValue(); + obj["properties"] = props; + + nodeArray.append(obj); + } + root["nodes"] = nodeArray; + + // Connections — only those fully within the selection + QJsonArray conArray; + for (auto& con : project->connections) { + if (nodeIdSet.contains(con->leftNode->id) && + nodeIdSet.contains(con->rightNode->id)) { + QJsonObject obj; + obj["leftNodeId"] = con->leftNode->id; + obj["rightNodeId"] = con->rightNode->id; + obj["rightNodeInput"] = con->rightNodeInputName; + conArray.append(obj); + } + } + root["connections"] = conArray; + + // Comments + QJsonArray commentArray; + for (const auto& id : commentIds) { + auto comment = project->comments.value(id); + if (!comment) + continue; + QJsonObject obj; + obj["text"] = comment->text; + obj["x"] = comment->pos.x(); + obj["y"] = comment->pos.y(); + commentArray.append(obj); + } + root["comments"] = commentArray; + + // Frames + QJsonArray frameArray; + for (const auto& id : frameIds) { + auto frame = project->frames.value(id); + if (!frame) + continue; + QJsonObject obj; + obj["title"] = frame->text; + obj["color"] = frame->color.name(QColor::HexRgb); + obj["x"] = frame->pos.x(); + obj["y"] = frame->pos.y(); + obj["width"] = frame->size.x(); + obj["height"] = frame->size.y(); + frameArray.append(obj); + } + root["frames"] = frameArray; + + QJsonDocument doc(root); + QApplication::clipboard()->setText(PREFIX + doc.toJson(QJsonDocument::Compact)); +} + +bool Clipboard::hasData() +{ + return QApplication::clipboard()->text().startsWith(PREFIX); +} + +bool Clipboard::pasteItems(TextureProjectPtr project, + QPointF viewCenter, + QList& outNodes, + QList& outConnections, + QList& outComments, + QList& outFrames) +{ + if (!project || !project->library) + return false; + + QString text = QApplication::clipboard()->text(); + if (!text.startsWith(PREFIX)) + return false; + + QJsonParseError err; + auto doc = QJsonDocument::fromJson(text.mid(PREFIX.length()).toUtf8(), &err); + if (err.error || !doc.isObject()) + return false; + + auto root = doc.object(); + + // Compute bounding box of all items in the clipboard to find their center + double minX = std::numeric_limits::max(); + double minY = std::numeric_limits::max(); + double maxX = std::numeric_limits::lowest(); + double maxY = std::numeric_limits::lowest(); + + auto expandBBox = [&](double x, double y) { + minX = std::min(minX, x); minY = std::min(minY, y); + maxX = std::max(maxX, x); maxY = std::max(maxY, y); + }; + + for (auto item : root["nodes"].toArray()) { + auto o = item.toObject(); + expandBBox(jsonutils::getDouble(o["x"]), jsonutils::getDouble(o["y"])); + } + for (auto item : root["comments"].toArray()) { + auto o = item.toObject(); + expandBBox(jsonutils::getDouble(o["x"]), jsonutils::getDouble(o["y"])); + } + for (auto item : root["frames"].toArray()) { + auto o = item.toObject(); + expandBBox(jsonutils::getDouble(o["x"]), jsonutils::getDouble(o["y"])); + expandBBox(jsonutils::getDouble(o["x"]) + jsonutils::getDouble(o["width"]), + jsonutils::getDouble(o["y"]) + jsonutils::getDouble(o["height"])); + } + + // If nothing in the bbox (empty clipboard somehow), fall back to no shift + double offsetX = 0, offsetY = 0; + if (minX <= maxX && minY <= maxY) { + double bboxCenterX = (minX + maxX) / 2.0; + double bboxCenterY = (minY + maxY) / 2.0; + offsetX = viewCenter.x() - bboxCenterX; + offsetY = viewCenter.y() - bboxCenterY; + } + + // Build old→new node ID map + QMap nodeIdMap; + for (auto item : root["nodes"].toArray()) { + auto oldId = item.toObject()["id"].toString(); + nodeIdMap[oldId] = QUuid::createUuid().toString(QUuid::WithoutBraces); + } + + // Nodes + for (auto item : root["nodes"].toArray()) { + auto obj = item.toObject(); + auto node = project->library->createNode(obj["typeName"].toString()); + if (!node) + continue; + + node->id = nodeIdMap[obj["id"].toString()]; + node->exportName = obj["exportName"].toString(); + node->randomSeed = jsonutils::getLong(obj["randomSeed"]); + node->pos = QVector2D((float)(jsonutils::getDouble(obj["x"]) + offsetX), + (float)(jsonutils::getDouble(obj["y"]) + offsetY)); + + auto propObj = obj["properties"].toObject(); + for (auto key : propObj.keys()) { + auto prop = node->getProp(key); + if (prop) + prop->fromJsonValue(propObj[key]); + } + + outNodes.append(node); + } + + // Connections + for (auto item : root["connections"].toArray()) { + auto obj = item.toObject(); + auto newLeftId = nodeIdMap.value(obj["leftNodeId"].toString()); + auto newRightId = nodeIdMap.value(obj["rightNodeId"].toString()); + if (newLeftId.isEmpty() || newRightId.isEmpty()) + continue; + + TextureNodePtr leftNode, rightNode; + for (auto& n : outNodes) { + if (n->id == newLeftId) + leftNode = n; + if (n->id == newRightId) + rightNode = n; + } + if (!leftNode || !rightNode) + continue; + + auto con = ConnectionPtr(new Connection()); + con->id = QUuid::createUuid().toString(QUuid::WithoutBraces); + con->leftNode = leftNode; + con->rightNode = rightNode; + con->leftNodeOutputName = "output"; + con->rightNodeInputName = obj["rightNodeInput"].toString(); + outConnections.append(con); + } + + // Comments + for (auto item : root["comments"].toArray()) { + auto obj = item.toObject(); + auto comment = CommentPtr(new Comment()); + comment->id = QUuid::createUuid().toString(QUuid::WithoutBraces); + comment->text = obj["text"].toString(); + comment->pos = QVector2D((float)(jsonutils::getDouble(obj["x"]) + offsetX), + (float)(jsonutils::getDouble(obj["y"]) + offsetY)); + outComments.append(comment); + } + + // Frames + for (auto item : root["frames"].toArray()) { + auto obj = item.toObject(); + auto frame = FramePtr(new Frame()); + frame->id = QUuid::createUuid().toString(QUuid::WithoutBraces); + frame->text = obj["title"].toString(); + frame->color = QColor(obj["color"].toString()); + frame->pos = QVector2D((float)(jsonutils::getDouble(obj["x"]) + offsetX), + (float)(jsonutils::getDouble(obj["y"]) + offsetY)); + frame->size = QVector2D(jsonutils::getFloat(obj["width"], 300), + jsonutils::getFloat(obj["height"], 200)); + outFrames.append(frame); + } + + return !outNodes.isEmpty() || !outComments.isEmpty() || !outFrames.isEmpty(); +} diff --git a/src/texturelab/clipboard.h b/src/texturelab/clipboard.h new file mode 100644 index 00000000..b5040705 --- /dev/null +++ b/src/texturelab/clipboard.h @@ -0,0 +1,24 @@ +#pragma once + +#include "models.h" +#include + +class Clipboard { +public: + static void copyItems(TextureProjectPtr project, + const QList& nodeIds, + const QList& frameIds, + const QList& commentIds); + + static bool pasteItems(TextureProjectPtr project, + QPointF viewCenter, + QList& outNodes, + QList& outConnections, + QList& outComments, + QList& outFrames); + + static bool hasData(); + +private: + static const QString PREFIX; +}; diff --git a/src/texturelab/curve.cpp b/src/texturelab/curve.cpp new file mode 100644 index 00000000..7736f22a --- /dev/null +++ b/src/texturelab/curve.cpp @@ -0,0 +1,266 @@ +#include "curve.h" + +#include "jsonutils.h" +#include +#include + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static float clamp01(float v) { return qBound(0.0f, v, 1.0f); } + +static float snap(float v) +{ + return qRound(v * 1000.0f) / 1000.0f; +} + +float Curve::cubicBez(float p0, float p1, float p2, float p3, float t) +{ + float mt = 1.0f - t; + return mt*mt*mt*p0 + 3.0f*mt*mt*t*p1 + 3.0f*mt*t*t*p2 + t*t*t*p3; +} + +float Curve::cubicBezD(float p0, float p1, float p2, float p3, float t) +{ + float mt = 1.0f - t; + return 3.0f * (mt*mt*(p1-p0) + 2.0f*mt*t*(p2-p1) + t*t*(p3-p2)); +} + +// --------------------------------------------------------------------------- +// Curve +// --------------------------------------------------------------------------- + +Curve::Curve() +{ + CurvePoint p0; + p0.x = 0.0f; p0.y = 0.0f; + p0.lx = -0.15f; p0.ly = -0.15f; + p0.rx = 0.15f; p0.ry = 0.15f; + p0.smooth = true; + + CurvePoint p1; + p1.x = 1.0f; p1.y = 1.0f; + p1.lx = -0.15f; p1.ly = -0.15f; + p1.rx = 0.15f; p1.ry = 0.15f; + p1.smooth = true; + + points.append(p0); + points.append(p1); +} + +float Curve::derivAt(float x) const +{ + // Find which segment contains x and compute dy/dx numerically + const float eps = 0.001f; + float x1 = qBound(0.0f, x - eps, 1.0f); + float x2 = qBound(0.0f, x + eps, 1.0f); + + // Evaluate y at x1 and x2 by Newton-Raphson on each segment + auto evalY = [&](float tx) -> float { + if (points.size() < 2) return tx; + int seg = points.size() - 2; + for (int i = 0; i < points.size() - 1; i++) { + if (tx <= points[i + 1].x) { seg = i; break; } + } + const CurvePoint& a = points[seg]; + const CurvePoint& b = points[seg + 1]; + float P0x = a.x, P0y = a.y; + float P1x = a.x + a.rx, P1y = a.y + a.ry; + float P2x = b.x + b.lx, P2y = b.y + b.ly; + float P3x = b.x, P3y = b.y; + + float t = qBound(0.0f, (tx - P0x) / qMax(P3x - P0x, 1e-5f), 1.0f); + for (int i = 0; i < 8; i++) { + float bx = cubicBez(P0x, P1x, P2x, P3x, t); + float dbx = cubicBezD(P0x, P1x, P2x, P3x, t); + if (qAbs(dbx) > 1e-6f) t -= (bx - tx) / dbx; + t = qBound(0.0f, t, 1.0f); + } + return cubicBez(P0y, P1y, P2y, P3y, t); + }; + + float dy = evalY(x2) - evalY(x1); + float dx = x2 - x1; + return (dx > 1e-6f) ? dy / dx : 1.0f; +} + +void Curve::addPoint(float x, float y) +{ + if (points.size() >= CURVE_MAX_POINTS) return; + + x = snap(clamp01(x)); + y = snap(clamp01(y)); + + // Find insert position + int insertPos = points.size(); + for (int i = 0; i < points.size(); i++) { + if (points[i].x >= x) { insertPos = i; break; } + } + + CurvePoint pt; + pt.x = x; + pt.y = y; + pt.smooth = true; + + // Auto-tangent: set handles tangent to existing curve at this x + float slope = derivAt(x); + float handleLen = 0.1f; + pt.rx = handleLen; + pt.ry = slope * handleLen; + pt.lx = -handleLen; + pt.ly = -slope * handleLen; + + points.insert(insertPos, pt); + clampHandles(insertPos); +} + +void Curve::removePoint(int index) +{ + if (index <= 0 || index >= points.size() - 1) return; + points.removeAt(index); +} + +void Curve::clampHandles(int index) +{ + if (index < 0 || index >= points.size()) return; + CurvePoint& pt = points[index]; + + // rx must be >= 0 + pt.rx = qMax(pt.rx, 0.0f); + // lx must be <= 0 + pt.lx = qMin(pt.lx, 0.0f); + + // Right handle absolute x must not exceed next anchor x + if (index < points.size() - 1) { + float maxRx = points[index + 1].x - pt.x; + if (pt.rx > maxRx) { + // Scale down proportionally + float scale = (maxRx > 1e-6f) ? maxRx / pt.rx : 0.0f; + pt.rx *= scale; + pt.ry *= scale; + } + } + + // Left handle absolute x must not go below previous anchor x + if (index > 0) { + float minLx = points[index - 1].x - pt.x; // negative + if (pt.lx < minLx) { + float scale = (qAbs(minLx) > 1e-6f) ? minLx / pt.lx : 0.0f; + pt.lx *= scale; + pt.ly *= scale; + } + } +} + +void Curve::moveAnchor(int index, float x, float y) +{ + if (index < 0 || index >= points.size()) return; + CurvePoint& pt = points[index]; + + y = snap(clamp01(y)); + + if (index == 0) { + // x locked + pt.y = y; + } else if (index == points.size() - 1) { + // x locked + pt.y = y; + } else { + float minX = points[index - 1].x + 0.005f; + float maxX = points[index + 1].x - 0.005f; + pt.x = snap(qBound(minX, x, maxX)); + pt.y = y; + } + + clampHandles(index); + if (index > 0) clampHandles(index - 1); + if (index < points.size() - 1) clampHandles(index + 1); +} + +void Curve::moveHandle(int index, bool isLeft, float dx, float dy) +{ + if (index < 0 || index >= points.size()) return; + CurvePoint& pt = points[index]; + + if (isLeft) { + pt.lx = snap(pt.lx + dx); + pt.ly = snap(pt.ly + dy); + if (pt.smooth) { + // Mirror to right handle (same length, opposite direction) + float len = qSqrt(pt.lx*pt.lx + pt.ly*pt.ly); + if (len > 1e-6f) { + float rightLen = qSqrt(pt.rx*pt.rx + pt.ry*pt.ry); + pt.rx = (-pt.lx / len) * rightLen; + pt.ry = (-pt.ly / len) * rightLen; + } + } + } else { + pt.rx = snap(pt.rx + dx); + pt.ry = snap(pt.ry + dy); + if (pt.smooth) { + float len = qSqrt(pt.rx*pt.rx + pt.ry*pt.ry); + if (len > 1e-6f) { + float leftLen = qSqrt(pt.lx*pt.lx + pt.ly*pt.ly); + pt.lx = (-pt.rx / len) * leftLen; + pt.ly = (-pt.ry / len) * leftLen; + } + } + } + + clampHandles(index); +} + +QJsonObject Curve::toJson() const +{ + QJsonArray arr; + for (const auto& pt : points) { + QJsonObject obj; + obj["x"] = (double)pt.x; + obj["y"] = (double)pt.y; + obj["lx"] = (double)pt.lx; + obj["ly"] = (double)pt.ly; + obj["rx"] = (double)pt.rx; + obj["ry"] = (double)pt.ry; + obj["smooth"] = pt.smooth; + arr.append(obj); + } + QJsonObject root; + root["points"] = arr; + return root; +} + +Curve Curve::fromJson(const QJsonObject& obj) +{ + Curve curve; + if (!obj.contains("points") || !obj["points"].isArray()) return curve; + + QJsonArray arr = obj["points"].toArray(); + if (arr.size() < 2) return curve; + + curve.points.clear(); + for (const auto& val : arr) { + auto o = val.toObject(); + CurvePoint pt; + pt.x = clamp01(jsonutils::getFloat(o["x"])); + pt.y = clamp01(jsonutils::getFloat(o["y"])); + pt.lx = qMin(jsonutils::getFloat(o["lx"]), 0.0f); + pt.ly = jsonutils::getFloat(o["ly"]); + pt.rx = qMax(jsonutils::getFloat(o["rx"]), 0.0f); + pt.ry = jsonutils::getFloat(o["ry"]); + pt.smooth = jsonutils::getBool(o["smooth"], true); + curve.points.append(pt); + } + + // Sort by x, enforce minimum 2 points + std::sort(curve.points.begin(), curve.points.end(), + [](const CurvePoint& a, const CurvePoint& b) { return a.x < b.x; }); + + if (curve.points.size() < 2) return Curve(); // fallback to identity + + // Lock first/last x + curve.points.first().x = 0.0f; + curve.points.last().x = 1.0f; + + return curve; +} diff --git a/src/texturelab/curve.h b/src/texturelab/curve.h new file mode 100644 index 00000000..9933f108 --- /dev/null +++ b/src/texturelab/curve.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +constexpr int CURVE_MAX_POINTS = 32; + +struct CurvePoint { + float x = 0.0f, y = 0.0f; + float lx = -0.15f, ly = 0.0f; // left handle offset (lx <= 0) + float rx = 0.15f, ry = 0.0f; // right handle offset (rx >= 0) + bool smooth = true; // true: handles mirror through anchor +}; + +class Curve { +public: + QVector points; // always sorted by x; minimum 2 points + + // Default: linear identity — (0,0) to (1,1) with horizontal handles + Curve(); + + // Insert sorted by x. Auto-computes handles tangent to the existing + // curve at that x so the visible shape is preserved. + void addPoint(float x, float y); + + // Remove by index. No-op for index 0 or last. + void removePoint(int index); + + // Move anchor. Interior: x clamped between neighbours. + // First/last: x locked at 0/1, only y moves. + // If smooth, handles rotate to stay mirrored. + void moveAnchor(int index, float x, float y); + + // Move one handle by a delta. If smooth=true, opposite handle mirrors. + void moveHandle(int index, bool isLeft, float dx, float dy); + + QJsonObject toJson() const; + static Curve fromJson(const QJsonObject& obj); + +private: + // Evaluate the derivative dy/dx at a given x using finite differences, + // used for auto-tangent on addPoint. + float derivAt(float x) const; + + // Cubic bezier helper: evaluate B(t) for one component + static float cubicBez(float p0, float p1, float p2, float p3, float t); + static float cubicBezD(float p0, float p1, float p2, float p3, float t); + + // Clamp handle offsets to maintain x-monotonicity constraints + void clampHandles(int index); +}; + +Q_DECLARE_METATYPE(Curve) diff --git a/src/texturelab/exporter.cpp b/src/texturelab/exporter.cpp new file mode 100644 index 00000000..bbd74458 --- /dev/null +++ b/src/texturelab/exporter.cpp @@ -0,0 +1,229 @@ +#include "exporter.h" + +#include +#include +#include +#include +#include + +Exporter::Exporter() {} + +ExportResult Exporter::exportTexture(QOpenGLFramebufferObject* texture, + const QString& filePath, int precision, + int components) +{ + ExportResult result; + result.success = false; + + if (!texture) { + result.errorMessage = "Texture is null"; + return result; + } + + int width = texture->width(); + int height = texture->height(); + + if (width <= 0 || height <= 0) { + result.errorMessage = "Invalid texture dimensions for export"; + return result; + } + + // Bind the FBO and read pixels + texture->bind(); + QOpenGLFunctions* gl = QOpenGLContext::currentContext()->functions(); + + // Read as float data (since texture is GL_RGBA32F). + // (size_t) so width*height*4 can't overflow int for large exports. + std::vector floatData((size_t)width * height * 4); + gl->glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, floatData.data()); + texture->release(); + + // Convert to QImage + QImage img = + convertToImage(floatData, width, height, precision, components); + + // Flip image vertically (OpenGL reads bottom-to-top) + img = img.mirrored(false, true); + + // Save image + if (img.save(filePath)) { + result.success = true; + qDebug() << "Exported:" << filePath; + } + else { + result.errorMessage = "Failed to save image to " + filePath; + qDebug() << result.errorMessage; + } + + return result; +} + +QImage Exporter::convertToImage(const std::vector& floatData, int width, + int height, int precision, int components) +{ + QImage img; + + // Determine number of channels for output + int outputChannels = 4; + if (components == 1) + outputChannels = 3; // RGB + else if (components >= 2 && components <= 5) + outputChannels = 1; // Single channel + + if (precision == 1) { + // 16-bit precision + if (outputChannels == 1) { + // Grayscale 16-bit + img = QImage(width, height, QImage::Format_Grayscale16); + + for (int y = 0; y < height; y++) { + quint16* scanLine = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < width; x++) { + int idx = (y * width + x) * 4; + float value = 0.0f; + + // Extract the selected channel + if (components == 2) + value = floatData[idx + 0]; // Red + else if (components == 3) + value = floatData[idx + 1]; // Green + else if (components == 4) + value = floatData[idx + 2]; // Blue + else if (components == 5) + value = floatData[idx + 3]; // Alpha + + // Convert to 16-bit (0-65535) + scanLine[x] = + qBound(0, static_cast(value * 65535.0f), 65535); + } + } + } + else if (outputChannels == 3) { + // RGB 16-bit + img = QImage(width, height, QImage::Format_RGBX64); + + for (int y = 0; y < height; y++) { + QRgba64* scanLine = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < width; x++) { + int idx = (y * width + x) * 4; + + quint16 r = qBound( + 0, static_cast(floatData[idx + 0] * 65535.0f), + 65535); + quint16 g = qBound( + 0, static_cast(floatData[idx + 1] * 65535.0f), + 65535); + quint16 b = qBound( + 0, static_cast(floatData[idx + 2] * 65535.0f), + 65535); + + scanLine[x] = qRgba64(r, g, b, 65535); + } + } + } + else { + // RGBA 16-bit + img = QImage(width, height, QImage::Format_RGBA64); + + for (int y = 0; y < height; y++) { + QRgba64* scanLine = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < width; x++) { + int idx = (y * width + x) * 4; + + quint16 r = qBound( + 0, static_cast(floatData[idx + 0] * 65535.0f), + 65535); + quint16 g = qBound( + 0, static_cast(floatData[idx + 1] * 65535.0f), + 65535); + quint16 b = qBound( + 0, static_cast(floatData[idx + 2] * 65535.0f), + 65535); + quint16 a = qBound( + 0, static_cast(floatData[idx + 3] * 65535.0f), + 65535); + + scanLine[x] = qRgba64(r, g, b, a); + } + } + } + } + else { + // 8-bit precision + if (outputChannels == 1) { + // Grayscale 8-bit + img = QImage(width, height, QImage::Format_Grayscale8); + + for (int y = 0; y < height; y++) { + quint8* scanLine = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < width; x++) { + int idx = (y * width + x) * 4; + float value = 0.0f; + + // Extract the selected channel + if (components == 2) + value = floatData[idx + 0]; // Red + else if (components == 3) + value = floatData[idx + 1]; // Green + else if (components == 4) + value = floatData[idx + 2]; // Blue + else if (components == 5) + value = floatData[idx + 3]; // Alpha + + // Convert to 8-bit (0-255) + scanLine[x] = + qBound(0, static_cast(value * 255.0f), 255); + } + } + } + else if (outputChannels == 3) { + // RGB 8-bit + img = QImage(width, height, QImage::Format_RGB888); + + for (int y = 0; y < height; y++) { + uchar* scanLine = img.scanLine(y); + for (int x = 0; x < width; x++) { + int idx = (y * width + x) * 4; + + quint8 r = qBound( + 0, static_cast(floatData[idx + 0] * 255.0f), 255); + quint8 g = qBound( + 0, static_cast(floatData[idx + 1] * 255.0f), 255); + quint8 b = qBound( + 0, static_cast(floatData[idx + 2] * 255.0f), 255); + + scanLine[x * 3 + 0] = r; + scanLine[x * 3 + 1] = g; + scanLine[x * 3 + 2] = b; + } + } + } + else { + // RGBA 8-bit + img = QImage(width, height, QImage::Format_RGBA8888); + + for (int y = 0; y < height; y++) { + uchar* scanLine = img.scanLine(y); + for (int x = 0; x < width; x++) { + int idx = (y * width + x) * 4; + + quint8 r = qBound( + 0, static_cast(floatData[idx + 0] * 255.0f), 255); + quint8 g = qBound( + 0, static_cast(floatData[idx + 1] * 255.0f), 255); + quint8 b = qBound( + 0, static_cast(floatData[idx + 2] * 255.0f), 255); + quint8 a = qBound( + 0, static_cast(floatData[idx + 3] * 255.0f), 255); + + scanLine[x * 4 + 0] = r; + scanLine[x * 4 + 1] = g; + scanLine[x * 4 + 2] = b; + scanLine[x * 4 + 3] = a; + } + } + } + } + + return img; +} diff --git a/src/texturelab/exporter.h b/src/texturelab/exporter.h new file mode 100644 index 00000000..3f1551d3 --- /dev/null +++ b/src/texturelab/exporter.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +class QOpenGLFramebufferObject; + +struct ExportResult { + bool success; + QString errorMessage; +}; + +class Exporter { +public: + Exporter(); + + // Export a single texture to a file + ExportResult + exportTexture(QOpenGLFramebufferObject* texture, const QString& filePath, + int precision, // 0 = 8-bit, 1 = 16-bit + int components // 0=RGBA, 1=RGB, 2=R, 3=G, 4=B, 5=A + ); + +private: + QImage convertToImage(const std::vector& floatData, int width, + int height, int precision, int components); +}; diff --git a/src/texturelab/graphics/noderenderer.cpp b/src/texturelab/graphics/noderenderer.cpp new file mode 100644 index 00000000..87f72130 --- /dev/null +++ b/src/texturelab/graphics/noderenderer.cpp @@ -0,0 +1,495 @@ +#include "noderenderer.h" +#include "../props.h" + +#include +#include +#include +#include +#include + +// ============================================================================ +// RenderResourceCache +// ============================================================================ + +void RenderResourceCache::init(QOpenGLFunctions_3_2_Core* glFuncs, GLuint fboId) +{ + gl = glFuncs; + m_fboId = fboId; +} + +void RenderResourceCache::cleanup() +{ + if (!gl) + return; + + for (auto& tex : texturePool) { + if (tex.id != 0) + gl->glDeleteTextures(1, &tex.id); + } + texturePool.clear(); + + qDeleteAll(shaderCache); + shaderCache.clear(); +} + +void RenderResourceCache::applyDefaultTextureParams( + QOpenGLFunctions_3_2_Core* gl, GLuint textureId) +{ + gl->glBindTexture(GL_TEXTURE_2D, textureId); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl->glBindTexture(GL_TEXTURE_2D, 0); +} + +GLuint RenderResourceCache::acquireTexture(int width, int height) +{ + // Reuse an existing free texture of matching size + for (auto& tex : texturePool) { + if (!tex.inUse && tex.width == width && tex.height == height) { + tex.inUse = true; + // Reset here rather than trusting the previous user to have put + // things back: a renderer that returned early, or simply forgot, + // would otherwise hand the next one LINEAR or REPEAT sampling and + // produce a bug that only shows up in certain node orderings. + applyDefaultTextureParams(gl, tex.id); + return tex.id; + } + } + + // Create new texture + GLuint texId; + gl->glGenTextures(1, &texId); + gl->glBindTexture(GL_TEXTURE_2D, texId); + gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, + GL_FLOAT, nullptr); + gl->glBindTexture(GL_TEXTURE_2D, 0); + applyDefaultTextureParams(gl, texId); + + CachedTexture cached; + cached.id = texId; + cached.width = width; + cached.height = height; + cached.inUse = true; + texturePool.append(cached); + + return texId; +} + +void RenderResourceCache::releaseTexture(GLuint textureId) +{ + for (auto& tex : texturePool) { + if (tex.id == textureId) { + tex.inUse = false; + return; + } + } +} + +void RenderResourceCache::releaseAllTextures() +{ + for (auto& tex : texturePool) { + tex.inUse = false; + } +} + +GLuint RenderResourceCache::getOrCompileShader(const QString& key, + const QString& vertexSource, + const QString& fragmentSource) +{ + if (shaderCache.contains(key)) + return shaderCache[key]->programId(); + + auto program = new QOpenGLShaderProgram(); + + if (!program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexSource)) { + qDebug() << "NodeRenderer: Vertex shader error [" << key << "]"; + qDebug() << program->log(); + } + + if (!program->addShaderFromSourceCode(QOpenGLShader::Fragment, + fragmentSource)) { + qDebug() << "NodeRenderer: Fragment shader error [" << key << "]"; + qDebug() << program->log(); + } + + // Bind attribute locations matching the worker's VBO layout + program->bindAttributeLocation("a_pos", 0); // VertexUsage::Position + program->bindAttributeLocation("a_color", 1); // VertexUsage::Color + program->bindAttributeLocation("a_texCoord", 2); // VertexUsage::TexCoord0 + + if (!program->link()) { + qDebug() << "NodeRenderer: Shader link error [" << key << "]"; + qDebug() << program->log(); + } + + shaderCache[key] = program; + return program->programId(); +} + +GLuint RenderResourceCache::compileNodeShader( + const QString& key, const QString& processSource, + const QStringList& inputNames, + const QList>& propTypes) +{ + if (shaderCache.contains(key)) + return shaderCache[key]->programId(); + + QString fSource = fragmentPreamble() + randomLib() + gradientLib() + + curveLib() + + generateInputDeclarations(inputNames) + + generatePropDeclarations(propTypes) + "#line 0\n" + + processSource; + + return getOrCompileShader(key, standardVertexSource(), fSource); +} + +void RenderResourceCache::bindFboToTexture(GLuint textureId) +{ + gl->glBindFramebuffer(GL_FRAMEBUFFER, m_fboId); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, textureId, 0); +} + +// ============================================================================ +// Static shader source helpers +// ============================================================================ + +QString RenderResourceCache::standardVertexSource() +{ + return R""""( + #version 150 core + in vec3 a_pos; + in vec2 a_texCoord; + out vec2 v_texCoord; + void main() { + v_texCoord = a_texCoord; + gl_Position = vec4(a_pos, 1); + } + )""""; +} + +QString RenderResourceCache::fragmentPreamble() +{ + return R""""( + #version 150 core + in vec2 v_texCoord; + + #define GRADIENT_MAX_POINTS 32 + #define CURVE_MAX_POINTS 32 + + vec4 process(vec2 uv); + void initRandom(); + + uniform vec2 _textureSize; + + out vec4 fragColor; + + void main() { + initRandom(); + vec4 result = process(v_texCoord); + fragColor = clamp(result, 0.0, 1.0); + } + )""""; +} + +QString RenderResourceCache::randomLib() +{ + // Exact copy of TextureRenderer::createRandomLib() + return R""""( + uniform float _seed; + vec2 _randomStart; + + #define RANDOM_ITERATIONS 1 + + #define HASHSCALE1 443.8975 + #define HASHSCALE3 vec3(443.897, 441.423, 437.195) + #define HASHSCALE4 vec4(443.897, 441.423, 437.195, 444.129) + + float hash12(vec2 p) + { + vec3 p3 = fract(vec3(p.xyx) * HASHSCALE1); + p3 += dot(p3, p3.yzx + 19.19); + return fract((p3.x + p3.y) * p3.z); + } + + vec2 hash22(vec2 p) + { + vec3 p3 = fract(vec3(p.xyx) * HASHSCALE3); + p3 += dot(p3, p3.yzx+19.19); + return fract((p3.xx+p3.yz)*p3.zy); + } + + float _rand(vec2 uv) + { + float a = 0.0; + for (int t = 0; t < RANDOM_ITERATIONS; t++) + { + float v = float(t+1)*.152; + vec2 pos = (uv * v); + a += hash12(pos); + } + return a/float(RANDOM_ITERATIONS); + } + + vec2 _rand2(vec2 uv) + { + vec2 a = vec2(0.0); + for (int t = 0; t < RANDOM_ITERATIONS; t++) + { + float v = float(t+1)*.152; + vec2 pos = (uv * v); + a += hash22(pos); + } + return a/float(RANDOM_ITERATIONS); + } + + float randomFloat(int index) + { + return _rand(_randomStart + vec2(_seed) + vec2(index)); + } + + float randomVec2(int index) + { + return _rand(_randomStart + vec2(_seed) + vec2(index)); + } + + float randomFloat(int index, float start, float end) + { + float r = _rand(_randomStart + vec2(_seed) + vec2(index)); + return start + r*(end-start); + } + + int randomInt(int index, int start, int end) + { + float r = _rand(_randomStart + vec2(_seed) + vec2(index)); + return start + int(r*float(end-start)); + } + + bool randomBool(int index) + { + return _rand(_randomStart + vec2(_seed) + vec2(index)) > 0.5; + } + + void initRandom() + { + _randomStart = v_texCoord; + } + )""""; +} + +QString RenderResourceCache::gradientLib() +{ + // Exact copy of TextureRenderer::createGradientLib() + return R""""( + struct Gradient { + vec3 colors[GRADIENT_MAX_POINTS]; + float positions[GRADIENT_MAX_POINTS]; + int numPoints; + }; + + vec3 sampleGradient(vec3 colors[GRADIENT_MAX_POINTS], + float positions[GRADIENT_MAX_POINTS], + int numPoints, float t) + { + if (numPoints == 0) + return vec3(1,0,0); + + if (numPoints == 1) + return colors[0]; + + if (t <= positions[0]) + return colors[0]; + + int last = numPoints - 1; + if (t >= positions[last]) + return colors[last]; + + for(int i = 0; i < numPoints-1; i++) { + if (positions[i+1] > t) { + vec3 colorA = colors[i]; + vec3 colorB = colors[i+1]; + + float t1 = positions[i]; + float t2 = positions[i+1]; + + float lerpPos = (t - t1)/(t2 - t1); + return mix(colorA, colorB, lerpPos); + } + } + + return vec3(0,0,0); + } + + vec3 sampleGradient(Gradient gradient, float t) + { + return sampleGradient(gradient.colors, gradient.positions, + gradient.numPoints, t); + } + )""""; +} + +QString RenderResourceCache::curveLib() +{ + return R""""( + struct Curve { + int numPoints; + vec2 anchors[CURVE_MAX_POINTS]; + vec2 handleR[CURVE_MAX_POINTS]; + vec2 handleL[CURVE_MAX_POINTS]; + }; + + float _cubicBez(float p0, float p1, float p2, float p3, float t) { + float mt = 1.0 - t; + return mt*mt*mt*p0 + 3.0*mt*mt*t*p1 + 3.0*mt*t*t*p2 + t*t*t*p3; + } + + float _cubicBezD(float p0, float p1, float p2, float p3, float t) { + float mt = 1.0 - t; + return 3.0 * (mt*mt*(p1-p0) + 2.0*mt*t*(p2-p1) + t*t*(p3-p2)); + } + + float evalCurve(Curve c, float x) { + // Find segment: last anchor whose x <= input x + int seg = c.numPoints - 2; + for (int i = 0; i < CURVE_MAX_POINTS - 1; i++) { + if (i >= c.numPoints - 1) break; + if (x <= c.anchors[i + 1].x) { seg = i; break; } + } + + vec2 P0 = c.anchors[seg]; + vec2 P1 = c.handleR[seg]; + vec2 P2 = c.handleL[seg + 1]; + vec2 P3 = c.anchors[seg + 1]; + + // Newton-Raphson: solve Bx(t) = x (8 iterations, constant bound) + float t = clamp((x - P0.x) / max(P3.x - P0.x, 1e-5), 0.0, 1.0); + for (int i = 0; i < 8; i++) { + float bx = _cubicBez(P0.x, P1.x, P2.x, P3.x, t); + float dbx = _cubicBezD(P0.x, P1.x, P2.x, P3.x, t); + t -= (abs(dbx) > 1e-6) ? (bx - x) / dbx : 0.0; + t = clamp(t, 0.0, 1.0); + } + + return _cubicBez(P0.y, P1.y, P2.y, P3.y, t); + } + )""""; +} + +QString RenderResourceCache::generateInputDeclarations( + const QStringList& inputNames) +{ + QString code; + for (const auto& input : inputNames) { + code += "uniform sampler2D " + input + ";\n"; + code += "uniform bool " + input + "_connected;\n"; + } + return code; +} + +QString RenderResourceCache::generatePropDeclarations( + const QList>& propTypes) +{ + QString code; + for (const auto& prop : propTypes) { + const QString& name = prop.first; + auto type = static_cast(prop.second); + + switch (type) { + case PropType::Int: + code += "uniform int prop_" + name + ";\n"; + break; + case PropType::Float: + code += "uniform float prop_" + name + ";\n"; + break; + case PropType::Bool: + code += "uniform bool prop_" + name + ";\n"; + break; + case PropType::Enum: + code += "uniform int prop_" + name + ";\n"; + break; + case PropType::Color: + code += "uniform vec4 prop_" + name + ";\n"; + break; + case PropType::Gradient: + code += "uniform Gradient prop_" + name + ";\n"; + break; + case PropType::Image: + code += "uniform sampler2D prop_" + name + ";\n"; + break; + case PropType::Curve: + code += "uniform Curve prop_" + name + ";\n"; + break; + default: + break; + } + } + return code + "\n"; +} + +// ============================================================================ +// ScopedTextureParams +// ============================================================================ + +ScopedTextureParams::ScopedTextureParams(QOpenGLFunctions_3_2_Core* glFuncs, + GLuint textureId, GLint minFilter, + GLint magFilter, GLint wrap) + : gl(glFuncs), tex(textureId) +{ + gl->glBindTexture(GL_TEXTURE_2D, tex); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap); + gl->glBindTexture(GL_TEXTURE_2D, 0); +} + +ScopedTextureParams::~ScopedTextureParams() +{ + RenderResourceCache::applyDefaultTextureParams(gl, tex); +} + +// ============================================================================ +// NodeRenderContext +// ============================================================================ + +void NodeRenderContext::useShader(GLuint programId) +{ + gl->glUseProgram(programId); + gl->glViewport(0, 0, textureWidth, textureHeight); + gl->glUniform2f(gl->glGetUniformLocation(programId, "_textureSize"), + (float)textureWidth, (float)textureHeight); + gl->glUniform1f(gl->glGetUniformLocation(programId, "_seed"), randomSeed); +} + +void NodeRenderContext::bindTexture(GLuint programId, + const QString& uniformName, + GLuint textureId, int unit) +{ + gl->glActiveTexture(GL_TEXTURE0 + unit); + gl->glBindTexture(GL_TEXTURE_2D, textureId); + gl->glUniform1i( + gl->glGetUniformLocation(programId, + uniformName.toStdString().c_str()), + unit); +} + +void NodeRenderContext::drawQuad() +{ + vao->bind(); + vbo->bind(); + + // a_pos = attribute 0, a_texCoord = attribute 2 + gl->glEnableVertexAttribArray(0); + gl->glEnableVertexAttribArray(2); + gl->glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), + nullptr); + gl->glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), + reinterpret_cast(3 * sizeof(float))); + + gl->glDrawArrays(GL_TRIANGLES, 0, 6); + + vbo->release(); + vao->release(); +} diff --git a/src/texturelab/graphics/noderenderer.h b/src/texturelab/graphics/noderenderer.h new file mode 100644 index 00000000..c62ab258 --- /dev/null +++ b/src/texturelab/graphics/noderenderer.h @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class QOpenGLFunctions_3_2_Core; +class QOpenGLVertexArrayObject; +class QOpenGLBuffer; +class QOpenGLShaderProgram; + +// Input texture binding passed to custom renderers +struct NodeInputBinding { + QString name; + GLuint textureId; +}; + +// Base class for node-specific render data. +// Nodes subclass this to carry parameters to the render thread. +// Must not reference GPU resources — only plain values. +struct NodeRenderData { + virtual ~NodeRenderData() = default; +}; + +// Worker-owned GPU resource pool. +// Manages intermediate textures and compiled shaders. +// All methods must be called on the render thread with GL context active. +class RenderResourceCache { +public: + RenderResourceCache() = default; + + void init(QOpenGLFunctions_3_2_Core* glFuncs, GLuint fboId); + void cleanup(); + + // The parameters every texture in the pipeline is expected to carry. + // Node shaders sample at exact texel centres, so NEAREST is the correct + // default; CLAMP_TO_EDGE is the default because shaders that want to tile + // fract() their own coordinates. + static void applyDefaultTextureParams(QOpenGLFunctions_3_2_Core* gl, + GLuint textureId); + + // --- Intermediate textures --- + // Pooled textures are reset to the default parameters on acquire, so a + // renderer can never inherit filtering or wrapping left behind by whoever + // used the texture last. + GLuint acquireTexture(int width, int height); + void releaseTexture(GLuint textureId); + void releaseAllTextures(); + + // --- Shader cache (raw GLSL) --- + GLuint getOrCompileShader(const QString& key, + const QString& vertexSource, + const QString& fragmentSource); + + // --- Shader cache (node-style with standard wrapping) --- + // Wraps processSource with random lib, gradient lib, input/prop declarations. + // propTypes: list of (name, PropType::Value as int) pairs. + GLuint compileNodeShader(const QString& key, + const QString& processSource, + const QStringList& inputNames, + const QList>& propTypes); + + // --- FBO --- + GLuint fboId() const { return m_fboId; } + void bindFboToTexture(GLuint textureId); + + // Standard vertex shader source (shared across all node shaders) + static QString standardVertexSource(); + + // Shader source building blocks — public so other renderers can reuse them + static QString fragmentPreamble(); + static QString randomLib(); + static QString gradientLib(); + static QString curveLib(); + static QString generateInputDeclarations(const QStringList& inputNames); + static QString generatePropDeclarations( + const QList>& propTypes); + +private: + QOpenGLFunctions_3_2_Core* gl = nullptr; + GLuint m_fboId = 0; + + struct CachedTexture { + GLuint id = 0; + int width = 0; + int height = 0; + bool inUse = false; + }; + QList texturePool; + + QMap shaderCache; +}; + +// View into the worker's GL state, passed to renderers at render time. +// Provides helpers for common rendering operations. +struct NodeRenderContext { + QOpenGLFunctions_3_2_Core* gl; + RenderResourceCache* cache; + + GLuint outputTextureId; + int textureWidth; + int textureHeight; + float randomSeed; + + QList inputs; + + QOpenGLVertexArrayObject* vao; + QOpenGLBuffer* vbo; + + // Bind shader, set viewport, set _textureSize and _seed uniforms + void useShader(GLuint programId); + + // Bind a texture to a sampler uniform + void bindTexture(GLuint programId, const QString& uniformName, + GLuint textureId, int unit); + + // Draw a fullscreen quad using the currently bound shader + void drawQuad(); +}; + +// Temporarily re-parameterises a texture — LINEAR for bilinear taps, REPEAT +// for wrapping, a mipmap filter for cone taps — and puts the pipeline defaults +// back when it goes out of scope. +// +// Prefer this over hand-written glTexParameteri pairs. Input textures belong +// to the node upstream and outlive the renderer that borrowed them, so a +// missed restore silently changes how some unrelated node is sampled later; +// with the guard the restore cannot be skipped, including on an early return. +class ScopedTextureParams { +public: + ScopedTextureParams(QOpenGLFunctions_3_2_Core* glFuncs, GLuint textureId, + GLint minFilter, GLint magFilter, + GLint wrap = GL_CLAMP_TO_EDGE); + ~ScopedTextureParams(); + + ScopedTextureParams(const ScopedTextureParams&) = delete; + ScopedTextureParams& operator=(const ScopedTextureParams&) = delete; + +private: + QOpenGLFunctions_3_2_Core* gl; + GLuint tex; +}; + +// Abstract base for custom node renderers. +// Subclass this to implement multi-pass or custom rendering logic. +// The worker delegates to render() instead of the standard single-pass path. +class NodeTextureRenderer { +public: + virtual ~NodeTextureRenderer() = default; + + // Called on the render thread with full GL context. + // Must write final result to context.outputTextureId. + virtual void render(NodeRenderContext& context, + const NodeRenderData& data) = 0; +}; diff --git a/src/texturelab/graphics/renderdoc_app.h b/src/texturelab/graphics/renderdoc_app.h new file mode 100644 index 00000000..7ca7dfc6 --- /dev/null +++ b/src/texturelab/graphics/renderdoc_app.h @@ -0,0 +1,767 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2019-2025 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#pragma once + +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Documentation for the API is available at +// https://renderdoc.org/docs/in_application_api.html +// + +#if !defined(RENDERDOC_NO_STDINT) +#include +#endif + +#if defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) +#define RENDERDOC_CC __cdecl +#elif defined(__linux__) +#define RENDERDOC_CC +#elif defined(__APPLE__) +#define RENDERDOC_CC +#else +#error "Unknown platform" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Constants not used directly in below API + +// This is a GUID/magic value used for when applications pass a path where +// shader debug information can be found to match up with a stripped shader. the +// define can be used like so: const GUID RENDERDOC_ShaderDebugMagicValue = +// RENDERDOC_ShaderDebugMagicValue_value +#define RENDERDOC_ShaderDebugMagicValue_struct \ + {0xeab25520, 0x6670, 0x4865, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff} + +// as an alternative when you want a byte array (assuming x86 endianness): +#define RENDERDOC_ShaderDebugMagicValue_bytearray \ + {0x20, 0x55, 0xb2, 0xea, 0x70, 0x66, 0x65, 0x48, \ + 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff} + +// truncated version when only a uint64_t is available (e.g. Vulkan tags): +#define RENDERDOC_ShaderDebugMagicValue_truncated 0x48656670eab25520ULL + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc capture options +// + +typedef enum RENDERDOC_CaptureOption { + // Allow the application to enable vsync + // + // Default - enabled + // + // 1 - The application can enable or disable vsync at will + // 0 - vsync is force disabled + eRENDERDOC_Option_AllowVSync = 0, + + // Allow the application to enable fullscreen + // + // Default - enabled + // + // 1 - The application can enable or disable fullscreen at will + // 0 - fullscreen is force disabled + eRENDERDOC_Option_AllowFullscreen = 1, + + // Record API debugging events and messages + // + // Default - disabled + // + // 1 - Enable built-in API debugging features and records the results into + // the capture, which is matched up with events on replay + // 0 - no API debugging is forcibly enabled + eRENDERDOC_Option_APIValidation = 2, + eRENDERDOC_Option_DebugDeviceMode = 2, // deprecated name of this enum + + // Capture CPU callstacks for API events + // + // Default - disabled + // + // 1 - Enables capturing of callstacks + // 0 - no callstacks are captured + eRENDERDOC_Option_CaptureCallstacks = 3, + + // When capturing CPU callstacks, only capture them from actions. + // This option does nothing without the above option being enabled + // + // Default - disabled + // + // 1 - Only captures callstacks for actions. + // Ignored if CaptureCallstacks is disabled + // 0 - Callstacks, if enabled, are captured for every event. + eRENDERDOC_Option_CaptureCallstacksOnlyDraws = 4, + eRENDERDOC_Option_CaptureCallstacksOnlyActions = 4, + + // Specify a delay in seconds to wait for a debugger to attach, after + // creating or injecting into a process, before continuing to allow it to + // run. + // + // 0 indicates no delay, and the process will run immediately after + // injection + // + // Default - 0 seconds + // + eRENDERDOC_Option_DelayForDebugger = 5, + + // Verify buffer access. This includes checking the memory returned by a + // Map() call to detect any out-of-bounds modification, as well as + // initialising buffers with undefined contents to a marker value to catch + // use of uninitialised memory. + // + // NOTE: This option is only valid for OpenGL and D3D11. Explicit APIs such + // as D3D12 and Vulkan do not do the same kind of interception & checking + // and undefined contents are really undefined. + // + // Default - disabled + // + // 1 - Verify buffer access + // 0 - No verification is performed, and overwriting bounds may cause + // crashes or corruption in + // RenderDoc. + eRENDERDOC_Option_VerifyBufferAccess = 6, + + // The old name for eRENDERDOC_Option_VerifyBufferAccess was + // eRENDERDOC_Option_VerifyMapWrites. This option now controls the filling + // of uninitialised buffers with 0xdddddddd which was previously always + // enabled + eRENDERDOC_Option_VerifyMapWrites = eRENDERDOC_Option_VerifyBufferAccess, + + // Hooks any system API calls that create child processes, and injects + // RenderDoc into them recursively with the same options. + // + // Default - disabled + // + // 1 - Hooks into spawned child processes + // 0 - Child processes are not hooked by RenderDoc + eRENDERDOC_Option_HookIntoChildren = 7, + + // By default RenderDoc only includes resources in the final capture + // necessary for that frame, this allows you to override that behaviour. + // + // Default - disabled + // + // 1 - all live resources at the time of capture are included in the capture + // and available for inspection + // 0 - only the resources referenced by the captured frame are included + eRENDERDOC_Option_RefAllResources = 8, + + // **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting + // or getting it will be ignored, to allow compatibility with older + // versions. In v1.1 the option acts as if it's always enabled. + // + // By default RenderDoc skips saving initial states for resources where the + // previous contents don't appear to be used, assuming that writes before + // reads indicate previous contents aren't used. + // + // Default - disabled + // + // 1 - initial contents at the start of each captured frame are saved, even + // if + // they are later overwritten or cleared before being used. + // 0 - unless a read is detected, initial contents will not be saved and + // will + // appear as black or empty data. + eRENDERDOC_Option_SaveAllInitials = 9, + + // In APIs that allow for the recording of command lists to be replayed + // later, RenderDoc may choose to not capture command lists before a frame + // capture is triggered, to reduce overheads. This means any command lists + // recorded once and replayed many times will not be available and may cause + // a failure to capture. + // + // NOTE: This is only true for APIs where multithreading is difficult or + // discouraged. Newer APIs like Vulkan and D3D12 will ignore this option + // and always capture all command lists since the API is heavily oriented + // around it and the overheads have been reduced by API design. + // + // 1 - All command lists are captured from the start of the application + // 0 - Command lists are only captured if their recording begins during + // the period when a frame capture is in progress. + eRENDERDOC_Option_CaptureAllCmdLists = 10, + + // Mute API debugging output when the API validation mode option is enabled + // + // Default - enabled + // + // 1 - Mute any API debug messages from being displayed or passed through + // 0 - API debugging is displayed as normal + eRENDERDOC_Option_DebugOutputMute = 11, + + // Option to allow vendor extensions to be used even when they may be + // incompatible with RenderDoc and cause corrupted replays or crashes. + // + // Default - inactive + // + // No values are documented, this option should only be used when absolutely + // necessary as directed by a RenderDoc developer. + eRENDERDOC_Option_AllowUnsupportedVendorExtensions = 12, + +} RENDERDOC_CaptureOption; + +// Sets an option that controls how RenderDoc behaves on capture. +// +// Returns 1 if the option and value are valid +// Returns 0 if either is invalid and the option is unchanged +typedef int(RENDERDOC_CC* pRENDERDOC_SetCaptureOptionU32)( + RENDERDOC_CaptureOption opt, uint32_t val); +typedef int(RENDERDOC_CC* pRENDERDOC_SetCaptureOptionF32)( + RENDERDOC_CaptureOption opt, float val); + +// Gets the current value of an option as a uint32_t +// +// If the option is invalid, 0xffffffff is returned +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_GetCaptureOptionU32)( + RENDERDOC_CaptureOption opt); + +// Gets the current value of an option as a float +// +// If the option is invalid, -FLT_MAX is returned +typedef float(RENDERDOC_CC* pRENDERDOC_GetCaptureOptionF32)( + RENDERDOC_CaptureOption opt); + +typedef enum RENDERDOC_InputButton { + // '0' - '9' matches ASCII values + eRENDERDOC_Key_0 = 0x30, + eRENDERDOC_Key_1 = 0x31, + eRENDERDOC_Key_2 = 0x32, + eRENDERDOC_Key_3 = 0x33, + eRENDERDOC_Key_4 = 0x34, + eRENDERDOC_Key_5 = 0x35, + eRENDERDOC_Key_6 = 0x36, + eRENDERDOC_Key_7 = 0x37, + eRENDERDOC_Key_8 = 0x38, + eRENDERDOC_Key_9 = 0x39, + + // 'A' - 'Z' matches ASCII values + eRENDERDOC_Key_A = 0x41, + eRENDERDOC_Key_B = 0x42, + eRENDERDOC_Key_C = 0x43, + eRENDERDOC_Key_D = 0x44, + eRENDERDOC_Key_E = 0x45, + eRENDERDOC_Key_F = 0x46, + eRENDERDOC_Key_G = 0x47, + eRENDERDOC_Key_H = 0x48, + eRENDERDOC_Key_I = 0x49, + eRENDERDOC_Key_J = 0x4A, + eRENDERDOC_Key_K = 0x4B, + eRENDERDOC_Key_L = 0x4C, + eRENDERDOC_Key_M = 0x4D, + eRENDERDOC_Key_N = 0x4E, + eRENDERDOC_Key_O = 0x4F, + eRENDERDOC_Key_P = 0x50, + eRENDERDOC_Key_Q = 0x51, + eRENDERDOC_Key_R = 0x52, + eRENDERDOC_Key_S = 0x53, + eRENDERDOC_Key_T = 0x54, + eRENDERDOC_Key_U = 0x55, + eRENDERDOC_Key_V = 0x56, + eRENDERDOC_Key_W = 0x57, + eRENDERDOC_Key_X = 0x58, + eRENDERDOC_Key_Y = 0x59, + eRENDERDOC_Key_Z = 0x5A, + + // leave the rest of the ASCII range free + // in case we want to use it later + eRENDERDOC_Key_NonPrintable = 0x100, + + eRENDERDOC_Key_Divide, + eRENDERDOC_Key_Multiply, + eRENDERDOC_Key_Subtract, + eRENDERDOC_Key_Plus, + + eRENDERDOC_Key_F1, + eRENDERDOC_Key_F2, + eRENDERDOC_Key_F3, + eRENDERDOC_Key_F4, + eRENDERDOC_Key_F5, + eRENDERDOC_Key_F6, + eRENDERDOC_Key_F7, + eRENDERDOC_Key_F8, + eRENDERDOC_Key_F9, + eRENDERDOC_Key_F10, + eRENDERDOC_Key_F11, + eRENDERDOC_Key_F12, + + eRENDERDOC_Key_Home, + eRENDERDOC_Key_End, + eRENDERDOC_Key_Insert, + eRENDERDOC_Key_Delete, + eRENDERDOC_Key_PageUp, + eRENDERDOC_Key_PageDn, + + eRENDERDOC_Key_Backspace, + eRENDERDOC_Key_Tab, + eRENDERDOC_Key_PrtScrn, + eRENDERDOC_Key_Pause, + + eRENDERDOC_Key_Max, +} RENDERDOC_InputButton; + +// Sets which key or keys can be used to toggle focus between multiple windows +// +// If keys is NULL or num is 0, toggle keys will be disabled +typedef void(RENDERDOC_CC* pRENDERDOC_SetFocusToggleKeys)( + RENDERDOC_InputButton* keys, int num); + +// Sets which key or keys can be used to capture the next frame +// +// If keys is NULL or num is 0, captures keys will be disabled +typedef void(RENDERDOC_CC* pRENDERDOC_SetCaptureKeys)( + RENDERDOC_InputButton* keys, int num); + +typedef enum RENDERDOC_OverlayBits { + // This single bit controls whether the overlay is enabled or disabled + // globally + eRENDERDOC_Overlay_Enabled = 0x1, + + // Show the average framerate over several seconds as well as min/max + eRENDERDOC_Overlay_FrameRate = 0x2, + + // Show the current frame number + eRENDERDOC_Overlay_FrameNumber = 0x4, + + // Show a list of recent captures, and how many captures have been made + eRENDERDOC_Overlay_CaptureList = 0x8, + + // Default values for the overlay mask + eRENDERDOC_Overlay_Default = + (eRENDERDOC_Overlay_Enabled | eRENDERDOC_Overlay_FrameRate | + eRENDERDOC_Overlay_FrameNumber | eRENDERDOC_Overlay_CaptureList), + + // Enable all bits + eRENDERDOC_Overlay_All = ~0U, + + // Disable all bits + eRENDERDOC_Overlay_None = 0, +} RENDERDOC_OverlayBits; + +// returns the overlay bits that have been set +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_GetOverlayBits)(); +// sets the overlay bits with an and & or mask +typedef void(RENDERDOC_CC* pRENDERDOC_MaskOverlayBits)(uint32_t And, + uint32_t Or); + +// this function will attempt to remove RenderDoc's hooks in the application. +// +// Note: that this can only work correctly if done immediately after +// the module is loaded, before any API work happens. RenderDoc will remove its +// injected hooks and shut down. Behaviour is undefined if this is called +// after any API functions have been called, and there is still no guarantee of +// success. +typedef void(RENDERDOC_CC* pRENDERDOC_RemoveHooks)(); + +// DEPRECATED: compatibility for code compiled against pre-1.4.1 headers. +typedef pRENDERDOC_RemoveHooks pRENDERDOC_Shutdown; + +// This function will unload RenderDoc's crash handler. +// +// If you use your own crash handler and don't want RenderDoc's handler to +// intercede, you can call this function to unload it and any unhandled +// exceptions will pass to the next handler. +typedef void(RENDERDOC_CC* pRENDERDOC_UnloadCrashHandler)(); + +// Sets the capture file path template +// +// pathtemplate is a UTF-8 string that gives a template for how captures will be +// named and where they will be saved. +// +// Any extension is stripped off the path, and captures are saved in the +// directory specified, and named with the filename and the frame number +// appended. If the directory does not exist it will be created, including any +// parent directories. +// +// If pathtemplate is NULL, the template will remain unchanged +// +// Example: +// +// SetCaptureFilePathTemplate("my_captures/example"); +// +// Capture #1 -> my_captures/example_frame123.rdc +// Capture #2 -> my_captures/example_frame456.rdc +typedef void(RENDERDOC_CC* pRENDERDOC_SetCaptureFilePathTemplate)( + const char* pathtemplate); + +// returns the current capture path template, see SetCaptureFileTemplate above, +// as a UTF-8 string +typedef const char*(RENDERDOC_CC* pRENDERDOC_GetCaptureFilePathTemplate)(); + +// DEPRECATED: compatibility for code compiled against pre-1.1.2 headers. +typedef pRENDERDOC_SetCaptureFilePathTemplate pRENDERDOC_SetLogFilePathTemplate; +typedef pRENDERDOC_GetCaptureFilePathTemplate pRENDERDOC_GetLogFilePathTemplate; + +// returns the number of captures that have been made +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_GetNumCaptures)(); + +// This function returns the details of a capture, by index. New captures are +// added to the end of the list. +// +// filename will be filled with the absolute path to the capture file, as a +// UTF-8 string pathlength will be written with the length in bytes of the +// filename string timestamp will be written with the time of the capture, in +// seconds since the Unix epoch +// +// Any of the parameters can be NULL and they'll be skipped. +// +// The function will return 1 if the capture index is valid, or 0 if the index +// is invalid If the index is invalid, the values will be unchanged +// +// Note: when captures are deleted in the UI they will remain in this list, so +// the capture path may not exist anymore. +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_GetCapture)(uint32_t idx, + char* filename, + uint32_t* pathlength, + uint64_t* timestamp); + +// Sets the comments associated with a capture file. These comments are +// displayed in the UI program when opening. +// +// filePath should be a path to the capture file to add comments to. If set to +// NULL or "" the most recent capture file created made will be used instead. +// comments should be a NULL-terminated UTF-8 string to add as comments. +// +// Any existing comments will be overwritten. +typedef void(RENDERDOC_CC* pRENDERDOC_SetCaptureFileComments)( + const char* filePath, const char* comments); + +// returns 1 if the RenderDoc UI is connected to this application, 0 otherwise +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_IsTargetControlConnected)(); + +// DEPRECATED: compatibility for code compiled against pre-1.1.1 headers. +// This was renamed to IsTargetControlConnected in API 1.1.1, the old typedef is +// kept here for backwards compatibility with old code, it is castable either +// way since it's ABI compatible as the same function pointer type. +typedef pRENDERDOC_IsTargetControlConnected pRENDERDOC_IsRemoteAccessConnected; + +// This function will launch the Replay UI associated with the RenderDoc library +// injected into the running application. +// +// if connectTargetControl is 1, the Replay UI will be launched with a command +// line parameter to connect to this application cmdline is the rest of the +// command line, as a UTF-8 string. E.g. a captures to open if cmdline is NULL, +// the command line will be empty. +// +// returns the PID of the replay UI if successful, 0 if not successful. +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_LaunchReplayUI)( + uint32_t connectTargetControl, const char* cmdline); + +// RenderDoc can return a higher version than requested if it's backwards +// compatible, this function returns the actual version returned. If a parameter +// is NULL, it will be ignored and the others will be filled out. +typedef void(RENDERDOC_CC* pRENDERDOC_GetAPIVersion)(int* major, int* minor, + int* patch); + +// Requests that the replay UI show itself (if hidden or not the current top +// window). This can be used in conjunction with IsTargetControlConnected and +// LaunchReplayUI to intelligently handle showing the UI after making a capture. +// +// This will return 1 if the request was successfully passed on, though it's not +// guaranteed that the UI will be on top in all cases depending on OS rules. It +// will return 0 if there is no current target control connection to make such a +// request, or if there was another error +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_ShowReplayUI)(); + +////////////////////////////////////////////////////////////////////////// +// Capturing functions +// + +// A device pointer is a pointer to the API's root handle. +// +// This would be an ID3D11Device, HGLRC/GLXContext, ID3D12Device, etc +typedef void* RENDERDOC_DevicePointer; + +// A window handle is the OS's native window handle +// +// This would be an HWND, GLXDrawable, etc +typedef void* RENDERDOC_WindowHandle; + +// A helper macro for Vulkan, where the device handle cannot be used directly. +// +// Passing the VkInstance to this macro will return the RENDERDOC_DevicePointer +// to use. +// +// Specifically, the value needed is the dispatch table pointer, which sits as +// the first pointer-sized object in the memory pointed to by the VkInstance. +// Thus we cast to a void** and indirect once. +#define RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(inst) (*((void**)(inst))) + +// This sets the RenderDoc in-app overlay in the API/window pair as 'active' and +// it will respond to keypresses. Neither parameter can be NULL +typedef void(RENDERDOC_CC* pRENDERDOC_SetActiveWindow)( + RENDERDOC_DevicePointer device, RENDERDOC_WindowHandle wndHandle); + +// capture the next frame on whichever window and API is currently considered +// active +typedef void(RENDERDOC_CC* pRENDERDOC_TriggerCapture)(); + +// capture the next N frames on whichever window and API is currently considered +// active +typedef void(RENDERDOC_CC* pRENDERDOC_TriggerMultiFrameCapture)( + uint32_t numFrames); + +// When choosing either a device pointer or a window handle to capture, you can +// pass NULL. Passing NULL specifies a 'wildcard' match against anything. This +// allows you to specify any API rendering to a specific window, or a specific +// API instance rendering to any window, or in the simplest case of one window +// and one API, you can just pass NULL for both. +// +// In either case, if there are two or more possible matching (device,window) +// pairs it is undefined which one will be captured. +// +// Note: for headless rendering you can pass NULL for the window handle and +// either specify a device pointer or leave it NULL as above. + +// Immediately starts capturing API calls on the specified device pointer and +// window handle. +// +// If there is no matching thing to capture (e.g. no supported API has been +// initialised), this will do nothing. +// +// The results are undefined (including crashes) if two captures are started +// overlapping, even on separate devices and/oror windows. +typedef void(RENDERDOC_CC* pRENDERDOC_StartFrameCapture)( + RENDERDOC_DevicePointer device, RENDERDOC_WindowHandle wndHandle); + +// Returns whether or not a frame capture is currently ongoing anywhere. +// +// This will return 1 if a capture is ongoing, and 0 if there is no capture +// running +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_IsFrameCapturing)(); + +// Ends capturing immediately. +// +// This will return 1 if the capture succeeded, and 0 if there was an error +// capturing. +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_EndFrameCapture)( + RENDERDOC_DevicePointer device, RENDERDOC_WindowHandle wndHandle); + +// Ends capturing immediately and discard any data stored without saving to +// disk. +// +// This will return 1 if the capture was discarded, and 0 if there was an error +// or no capture was in progress +typedef uint32_t(RENDERDOC_CC* pRENDERDOC_DiscardFrameCapture)( + RENDERDOC_DevicePointer device, RENDERDOC_WindowHandle wndHandle); + +// Only valid to be called between a call to StartFrameCapture and +// EndFrameCapture. Gives a custom title to the capture produced which will be +// displayed in the UI. +// +// If multiple captures are ongoing, this title will be applied to the first +// capture to end after this call. The second capture to end will have no title, +// unless this function is called again. +// +// Calling this function has no effect if no capture is currently running +typedef void(RENDERDOC_CC* pRENDERDOC_SetCaptureTitle)(const char* title); + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc API versions +// + +// RenderDoc uses semantic versioning (http://semver.org/). +// +// MAJOR version is incremented when incompatible API changes happen. +// MINOR version is incremented when functionality is added in a +// backwards-compatible manner. PATCH version is incremented when +// backwards-compatible bug fixes happen. +// +// Note that this means the API returned can be higher than the one you might +// have requested. e.g. if you are running against a newer RenderDoc that +// supports 1.0.1, it will be returned instead of 1.0.0. You can check this with +// the GetAPIVersion entry point +typedef enum RENDERDOC_Version { + eRENDERDOC_API_Version_1_0_0 = 10000, // RENDERDOC_API_1_0_0 = 1 00 00 + eRENDERDOC_API_Version_1_0_1 = 10001, // RENDERDOC_API_1_0_1 = 1 00 01 + eRENDERDOC_API_Version_1_0_2 = 10002, // RENDERDOC_API_1_0_2 = 1 00 02 + eRENDERDOC_API_Version_1_1_0 = 10100, // RENDERDOC_API_1_1_0 = 1 01 00 + eRENDERDOC_API_Version_1_1_1 = 10101, // RENDERDOC_API_1_1_1 = 1 01 01 + eRENDERDOC_API_Version_1_1_2 = 10102, // RENDERDOC_API_1_1_2 = 1 01 02 + eRENDERDOC_API_Version_1_2_0 = 10200, // RENDERDOC_API_1_2_0 = 1 02 00 + eRENDERDOC_API_Version_1_3_0 = 10300, // RENDERDOC_API_1_3_0 = 1 03 00 + eRENDERDOC_API_Version_1_4_0 = 10400, // RENDERDOC_API_1_4_0 = 1 04 00 + eRENDERDOC_API_Version_1_4_1 = 10401, // RENDERDOC_API_1_4_1 = 1 04 01 + eRENDERDOC_API_Version_1_4_2 = 10402, // RENDERDOC_API_1_4_2 = 1 04 02 + eRENDERDOC_API_Version_1_5_0 = 10500, // RENDERDOC_API_1_5_0 = 1 05 00 + eRENDERDOC_API_Version_1_6_0 = 10600, // RENDERDOC_API_1_6_0 = 1 06 00 +} RENDERDOC_Version; + +// API version changelog: +// +// 1.0.0 - initial release +// 1.0.1 - Bugfix: IsFrameCapturing() was returning false for captures that were +// triggered +// by keypress or TriggerCapture, instead of Start/EndFrameCapture. +// 1.0.2 - Refactor: Renamed eRENDERDOC_Option_DebugDeviceMode to +// eRENDERDOC_Option_APIValidation 1.1.0 - Add feature: +// TriggerMultiFrameCapture(). Backwards compatible with 1.0.x since the new +// function pointer is added to the end of the struct, the original +// layout is identical +// 1.1.1 - Refactor: Renamed remote access to target control (to better +// disambiguate from remote +// replay/remote server concept in replay UI) +// 1.1.2 - Refactor: Renamed "log file" in function names to just capture, to +// clarify that these +// are captures and not debug logging files. This is the first API +// version in the v1.0 branch. +// 1.2.0 - Added feature: SetCaptureFileComments() to add comments to a capture +// file that will be +// displayed in the UI program on load. +// 1.3.0 - Added feature: New capture option +// eRENDERDOC_Option_AllowUnsupportedVendorExtensions +// which allows users to opt-in to allowing unsupported vendor +// extensions to function. Should be used at the user's own risk. +// Refactor: Renamed eRENDERDOC_Option_VerifyMapWrites to +// eRENDERDOC_Option_VerifyBufferAccess, which now also controls +// initialisation to 0xdddddddd of uninitialised buffer contents. +// 1.4.0 - Added feature: DiscardFrameCapture() to discard a frame capture in +// progress and stop +// capturing without saving anything to disk. +// 1.4.1 - Refactor: Renamed Shutdown to RemoveHooks to better clarify what is +// happening 1.4.2 - Refactor: Renamed 'draws' to 'actions' in callstack capture +// option. 1.5.0 - Added feature: ShowReplayUI() to request that the replay UI +// show itself if connected 1.6.0 - Added feature: SetCaptureTitle() which can +// be used to set a title for a +// capture made with StartFrameCapture() or EndFrameCapture() + +typedef struct RENDERDOC_API_1_6_0 { + pRENDERDOC_GetAPIVersion GetAPIVersion; + + pRENDERDOC_SetCaptureOptionU32 SetCaptureOptionU32; + pRENDERDOC_SetCaptureOptionF32 SetCaptureOptionF32; + + pRENDERDOC_GetCaptureOptionU32 GetCaptureOptionU32; + pRENDERDOC_GetCaptureOptionF32 GetCaptureOptionF32; + + pRENDERDOC_SetFocusToggleKeys SetFocusToggleKeys; + pRENDERDOC_SetCaptureKeys SetCaptureKeys; + + pRENDERDOC_GetOverlayBits GetOverlayBits; + pRENDERDOC_MaskOverlayBits MaskOverlayBits; + + // Shutdown was renamed to RemoveHooks in 1.4.1. + // These unions allow old code to continue compiling without changes + union { + pRENDERDOC_Shutdown Shutdown; + pRENDERDOC_RemoveHooks RemoveHooks; + }; + pRENDERDOC_UnloadCrashHandler UnloadCrashHandler; + + // Get/SetLogFilePathTemplate was renamed to Get/SetCaptureFilePathTemplate + // in 1.1.2. These unions allow old code to continue compiling without + // changes + union { + // deprecated name + pRENDERDOC_SetLogFilePathTemplate SetLogFilePathTemplate; + // current name + pRENDERDOC_SetCaptureFilePathTemplate SetCaptureFilePathTemplate; + }; + union { + // deprecated name + pRENDERDOC_GetLogFilePathTemplate GetLogFilePathTemplate; + // current name + pRENDERDOC_GetCaptureFilePathTemplate GetCaptureFilePathTemplate; + }; + + pRENDERDOC_GetNumCaptures GetNumCaptures; + pRENDERDOC_GetCapture GetCapture; + + pRENDERDOC_TriggerCapture TriggerCapture; + + // IsRemoteAccessConnected was renamed to IsTargetControlConnected in 1.1.1. + // This union allows old code to continue compiling without changes + union { + // deprecated name + pRENDERDOC_IsRemoteAccessConnected IsRemoteAccessConnected; + // current name + pRENDERDOC_IsTargetControlConnected IsTargetControlConnected; + }; + pRENDERDOC_LaunchReplayUI LaunchReplayUI; + + pRENDERDOC_SetActiveWindow SetActiveWindow; + + pRENDERDOC_StartFrameCapture StartFrameCapture; + pRENDERDOC_IsFrameCapturing IsFrameCapturing; + pRENDERDOC_EndFrameCapture EndFrameCapture; + + // new function in 1.1.0 + pRENDERDOC_TriggerMultiFrameCapture TriggerMultiFrameCapture; + + // new function in 1.2.0 + pRENDERDOC_SetCaptureFileComments SetCaptureFileComments; + + // new function in 1.4.0 + pRENDERDOC_DiscardFrameCapture DiscardFrameCapture; + + // new function in 1.5.0 + pRENDERDOC_ShowReplayUI ShowReplayUI; + + // new function in 1.6.0 + pRENDERDOC_SetCaptureTitle SetCaptureTitle; +} RENDERDOC_API_1_6_0; + +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_2_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_3_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_5_0; + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc API entry point +// +// This entry point can be obtained via GetProcAddress/dlsym if RenderDoc is +// available. +// +// The name is the same as the typedef - "RENDERDOC_GetAPI" +// +// This function is not thread safe, and should not be called on multiple +// threads at once. Ideally, call this once as early as possible in your +// application's startup, before doing any API work, since some configuration +// functionality etc has to be done also before initialising any APIs. +// +// Parameters: +// version is a single value from the RENDERDOC_Version above. +// +// outAPIPointers will be filled out with a pointer to the corresponding +// struct of function pointers. +// +// Returns: +// 1 - if the outAPIPointers has been filled with a pointer to the API struct +// requested 0 - if the requested version is not supported or the arguments +// are invalid. +// +typedef int(RENDERDOC_CC* pRENDERDOC_GetAPI)(RENDERDOC_Version version, + void** outAPIPointers); + +#ifdef __cplusplus +} // extern "C" +#endif \ No newline at end of file diff --git a/src/texturelab/graphics/renderworker.cpp b/src/texturelab/graphics/renderworker.cpp new file mode 100644 index 00000000..c66a92fa --- /dev/null +++ b/src/texturelab/graphics/renderworker.cpp @@ -0,0 +1,553 @@ +#include "renderworker.h" +#include "../models.h" +#include "../systeminfo.h" +#include "../telemetry.h" +#include "../curve.h" +#include "gradient.h" +#include "texturerenderer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// Qt6 only!! +#include +#include + +// https://renderdoc.org/docs/in_application_api.html +#include "renderdoc_app.h" +#ifdef __linux__ +#include +#endif + +const int TEXTURE_SIZE = 1024; + +RENDERDOC_API_1_1_2* rdoc_api = nullptr; + +namespace { + +// Attach GL/VRAM state to a Sentry event before a qFatal takes the process +// down. Continuing past a broken framebuffer on the render thread isn't safe, +// but these used to arrive as bare aborts with an unreadable driver stack. +void reportFatalGlState(const char* what, Telemetry::Fields extra) +{ + const SystemInfo::GpuMemory mem = SystemInfo::queryGpuMemory(); + extra.emplace_back("vram_known", mem.known); + extra.emplace_back("vram_total_mb", + mem.known ? mem.totalKb / 1024 : (int64_t)-1); + extra.emplace_back("vram_available_mb", + mem.known ? mem.availableKb / 1024 : (int64_t)-1); + extra.emplace_back("thread", std::string("render worker")); + Telemetry::captureException(what, extra); +} + +} // namespace + +RenderWorker::RenderWorker() + : QObject(), surface(nullptr), ctx(nullptr), gl(nullptr), vao(nullptr), + vbo(nullptr), vshader(nullptr), fshader(nullptr), fbo(nullptr), + running(false) +{ +} + +void RenderWorker::setRenderQueue(QQueue renderQueue) +{ + QMutexLocker locker(&mutex); + this->renderQueue = renderQueue; +} + +void RenderWorker::run() +{ + this->setup(); + + while (running) { + this->renderNextInQueue(); + } +} + +void RenderWorker::renderNextInQueue() +{ + mutex.lock(); + if (!renderQueue.isEmpty()) { + RenderCommand command = renderQueue.dequeue(); + mutex.unlock(); + this->processRenderCommand(command); + } + else { + mutex.unlock(); + } +} + +// Must be called from main/GUI thread before run() - Windows requires surface creation on GUI thread +void RenderWorker::initSurface() +{ + QSurfaceFormat format; + format.setDepthBufferSize(32); + format.setMajorVersion(3); + format.setMinorVersion(2); + format.setProfile(QSurfaceFormat::CoreProfile); + format.setSamples(1); + format.setSwapInterval(0); + format.setSwapBehavior(QSurfaceFormat::SingleBuffer); + format.setOption(QSurfaceFormat::DebugContext); // for debugging + + surface = new QOffscreenSurface(); + surface->setFormat(format); + surface->create(); // Safe here - called from main thread +} + +void RenderWorker::setup() +{ + running = true; + + Telemetry::breadcrumb("render.setup", "RenderWorker::setup() start"); + + // Surface must already be created via initSurface() from main thread + if (!surface || !surface->isValid()) { + qFatal("Surface not initialized! Call initSurface() from main thread before run()"); + } + + QSurfaceFormat format = surface->format(); + + // create context + ctx = new QOpenGLContext(); + // https://doc.qt.io/qt-6/qopenglcontext.html#globalShareContext + ctx->setShareContext(QOpenGLContext::globalShareContext()); + ctx->setFormat(format); + if (!ctx->create()) { + reportFatalGlState("render worker context creation failed", {}); + qFatal("unable to create surface!"); + } + + Telemetry::breadcrumb("render.setup", "OpenGL context created, making current"); + ctx->makeCurrent(surface); + + // https://doc-snapshots.qt.io/qt6-dev/gui-changes-qt6.html + gl = QOpenGLVersionFunctionsFactory::get(ctx); + if (!gl) { + reportFatalGlState("3.2 core functions unavailable on worker context", + {{"granted_major", + (int64_t)ctx->format().majorVersion()}, + {"granted_minor", + (int64_t)ctx->format().minorVersion()}}); + qFatal("Could not obtain required OpenGL context version"); + } + + // setup debugging + QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(); + logger->initialize(); + + QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, + [=](const QOpenGLDebugMessage& debugMessage) { + if (debugMessage.type() == + QOpenGLDebugMessage::ErrorType) + qFatal() << debugMessage; + // else + // qDebug() << debugMessage; + }); + + logger->startLogging(); + + gl->initializeOpenGLFunctions(); + + // simple setup + gl->glViewport(0, 0, TEXTURE_SIZE, TEXTURE_SIZE); + gl->glDisable(GL_BLEND); + gl->glDisable(GL_DEPTH_TEST); + gl->glDisable(GL_CULL_FACE); + + // setup vbo + vao = new QOpenGLVertexArrayObject; + if (vao->create()) + vao->bind(); + + // setup vertex array + QVector data; + // TRIANGLE 1 + data.append(-1); + data.append(-1); + data.append(0); + + data.append(0); + data.append(0); + + data.append(1); + data.append(-1); + data.append(0); + + data.append(1); + data.append(0); + + data.append(-1); + data.append(1); + data.append(0); + + data.append(0); + data.append(1); + + // TRIANGLE 2 + data.append(-1); + data.append(1); + data.append(0); + + data.append(0); + data.append(1); + + data.append(1); + data.append(-1); + data.append(0); + + data.append(1); + data.append(0); + + data.append(1); + data.append(1); + data.append(0); + + data.append(1); + data.append(1); + + vbo = new QOpenGLBuffer; + vbo->create(); + vbo->bind(); + vbo->allocate(data.constData(), data.count() * sizeof(float)); + vbo->release(); + + vao->release(); + + // create fbo + // https://doc.qt.io/qt-6/qopenglframebufferobject.html + // https://www.qt.io/blog/2015/09/21/using-modern-opengl-es-features-with-qopenglframebufferobject-in-qt-5-6 + fbo = new QOpenGLFramebufferObject(TEXTURE_SIZE, TEXTURE_SIZE); + if (!fbo->isValid()) { + reportFatalGlState("render worker scratch FBO could not be created", + {{"size", (int64_t)TEXTURE_SIZE}}); + qFatal("FBO could not be created"); + } + + // create FBO with no color attachment for depth-only rendering + fboId = 0; + gl->glGenFramebuffers(1, &fboId); + gl->glBindFramebuffer(GL_FRAMEBUFFER, fboId); + // gl->glDrawBuffer(GL_NONE); + // gl->glReadBuffer(GL_NONE); + gl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + + { + const QSurfaceFormat granted = ctx->format(); + Telemetry::breadcrumb( + "render.setup", "RenderWorker::setup() complete", + {{"granted_version", + std::to_string(granted.majorVersion()) + "." + + std::to_string(granted.minorVersion())}, + {"core_profile", + granted.profile() == QSurfaceFormat::CoreProfile}}); + } + + // Initialize resource cache for custom node renderers + resourceCache.init(gl, fboId); + +#ifdef __linux__ + // setup renderdoc + if (void* mod = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD)) { + pRENDERDOC_GetAPI RENDERDOC_GetAPI = + (pRENDERDOC_GetAPI)dlsym(mod, "RENDERDOC_GetAPI"); + RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_1_2, (void**)&rdoc_api); + } +#endif + + ctx->doneCurrent(); +} + +void RenderWorker::processRenderCommand(const RenderCommand& command) +{ + // Deliberately no breadcrumb here: with the Crashpad backend every crumb + // flushes the scope to disk, and this runs once per node per render pass. + // The batch-level crumbs in TextureRenderer cover what we actually need. + + if (rdoc_api) + rdoc_api->StartFrameCapture(NULL, NULL); + + ctx->makeCurrent(surface); + + // Custom renderer path — node defines its own multi-pass rendering. + // Require renderData too: the renderer immediately downcasts and reads + // fields off it, so a null (a node that overrides createRenderer() but not + // createRenderData()) would form a null reference and crash here. + if (command.renderer && command.renderData) { + NodeRenderContext renderCtx; + renderCtx.gl = gl; + renderCtx.cache = &resourceCache; + renderCtx.outputTextureId = command.textureId; + renderCtx.textureWidth = command.textureWidth; + renderCtx.textureHeight = command.textureHeight; + renderCtx.randomSeed = command.randomSeed; + renderCtx.vao = vao; + renderCtx.vbo = vbo; + + for (const auto& input : command.inputs) { + renderCtx.inputs.append( + NodeInputBinding{input.inputName, input.textureId}); + } + + command.renderer->render(renderCtx, *command.renderData); + resourceCache.releaseAllTextures(); + + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, 0, 0); + gl->glBindFramebuffer(GL_FRAMEBUFFER, ctx->defaultFramebufferObject()); + + ctx->doneCurrent(); + + if (rdoc_api) + rdoc_api->EndFrameCapture(NULL, NULL); + + emit nodeRendered(command.nodeId, command.textureId); + return; + } + + // CPU processing path + if (command.usesCpuProcessing && command.nodePtr) { + TextureNode* node = command.nodePtr.data(); + node->cpuProcess(gl, command); + + ctx->doneCurrent(); + + if (rdoc_api) + rdoc_api->EndFrameCapture(NULL, NULL); + + emit nodeRendered(command.nodeId, command.textureId); + return; + } + + // Standard single-pass GPU path + renderSinglePass(command); + + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, 0, 0); + gl->glBindFramebuffer(GL_FRAMEBUFFER, ctx->defaultFramebufferObject()); + + ctx->doneCurrent(); + + if (rdoc_api) + rdoc_api->EndFrameCapture(NULL, NULL); + + emit nodeRendered(command.nodeId, command.textureId); +} + +void RenderWorker::renderSinglePass(const RenderCommand& command) +{ + gl->glBindFramebuffer(GL_FRAMEBUFFER, fboId); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, command.textureId, 0); + + GLenum status = gl->glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) { + reportFatalGlState("render target framebuffer incomplete", + {{"status", (int64_t)status}, + {"width", (int64_t)command.textureWidth}, + {"height", (int64_t)command.textureHeight}}); + qFatal("FRAMEBUFFER IS NOT COMPLETE!"); + } + + gl->glViewport(0, 0, command.textureWidth, command.textureHeight); + + gl->glClearColor(0, 1, 0, 1); + gl->glClearDepth(0); + gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + vao->bind(); + + if (command.shaderLinked) { + gl->glUseProgram(command.shaderId); + + // Clear every declared input, not just the connected ones: uniforms + // live on the shader program, so an input that was connected the last + // time this node rendered would otherwise keep its stale texture and + // _connected == true after being disconnected. + int texIndex = 0; + for (const auto& inputName : command.inputNames) { + gl->glActiveTexture(GL_TEXTURE0 + texIndex); + gl->glBindTexture(GL_TEXTURE_2D, 0); + + gl->glUniform1i( + gl->glGetUniformLocation(command.shaderId, + inputName.toStdString().c_str()), + texIndex); + std::string connectedName = inputName.toStdString() + "_connected"; + gl->glUniform1i(gl->glGetUniformLocation(command.shaderId, + connectedName.c_str()), + 0); + + texIndex++; + } + + // pass inputs + for (auto nodeInput : command.inputs) { + auto name = nodeInput.inputName; + + // reuse the unit the clear loop above assigned to this input so + // the two stay in sync; unknown names get a fresh unit + int unit = command.inputNames.indexOf(name); + if (unit < 0) + unit = texIndex++; + + gl->glActiveTexture(GL_TEXTURE0 + unit); + gl->glBindTexture(GL_TEXTURE_2D, nodeInput.textureId); + + gl->glUniform1i(gl->glGetUniformLocation( + command.shaderId, name.toStdString().c_str()), + unit); + std::string connectedName = name.toStdString() + "_connected"; + gl->glUniform1i(gl->glGetUniformLocation(command.shaderId, + connectedName.c_str()), + 1); + } + + // pass seed + gl->glUniform1f(gl->glGetUniformLocation(command.shaderId, "_seed"), + (GLfloat)(command.randomSeed)); + + // texture size + gl->glUniform2f( + gl->glGetUniformLocation(command.shaderId, "_textureSize"), + (GLfloat)(command.textureWidth), (GLfloat)(command.textureHeight)); + + // pass props + for (auto prop : command.props) { + auto propCString = ("prop_" + prop.propName.toStdString()); + auto propName = propCString.c_str(); + + switch (prop.propType) { + case PropType::Int: { + GLint intVal = prop.value.toInt(); + gl->glUniform1i( + gl->glGetUniformLocation(command.shaderId, propName), + intVal); + } break; + case PropType::Float: { + GLfloat floatVal = prop.value.toFloat(); + gl->glUniform1f( + gl->glGetUniformLocation(command.shaderId, propName), + floatVal); + } break; + case PropType::Bool: { + GLint boolVal = prop.value.toBool() ? 1 : 0; + gl->glUniform1i( + gl->glGetUniformLocation(command.shaderId, propName), + boolVal); + } break; + case PropType::Enum: { + GLint enumVal = prop.value.toInt(); + gl->glUniform1i( + gl->glGetUniformLocation(command.shaderId, propName), + enumVal); + } break; + case PropType::Color: { + auto colorVal = prop.value.value(); + gl->glUniform4f( + gl->glGetUniformLocation(command.shaderId, propName), + colorVal.redF(), colorVal.greenF(), colorVal.blueF(), + colorVal.alphaF()); + } break; + case PropType::Gradient: { + auto gradientVal = prop.value.value(); + auto numPoints = gradientVal.points.size(); + + gl->glUniform1i( + gl->glGetUniformLocation( + command.shaderId, (propCString + ".numPoints").c_str()), + numPoints); + + for (int i = 0; i < numPoints; i++) { + const auto& point = gradientVal.points[i]; + const auto& color = point.color; + + std::string colorPath = + propCString + ".colors[" + std::to_string(i) + "]"; + gl->glUniform3f(gl->glGetUniformLocation(command.shaderId, + colorPath.c_str()), + color.redF(), color.greenF(), + color.blueF()); + + std::string posPath = + propCString + ".positions[" + std::to_string(i) + "]"; + gl->glUniform1f(gl->glGetUniformLocation(command.shaderId, + posPath.c_str()), + point.position); + } + } break; + case PropType::Image: { + if (prop.textureId != 0) { + gl->glActiveTexture(GL_TEXTURE0 + texIndex); + gl->glBindTexture(GL_TEXTURE_2D, prop.textureId); + gl->glUniform1i( + gl->glGetUniformLocation(command.shaderId, propName), + texIndex); + texIndex++; + } + else { + gl->glActiveTexture(GL_TEXTURE0 + texIndex); + gl->glBindTexture(GL_TEXTURE_2D, 0); + gl->glUniform1i( + gl->glGetUniformLocation(command.shaderId, propName), + texIndex); + texIndex++; + } + } break; + case PropType::Curve: { + auto curve = prop.value.value(); + int nPoints = qMin((int)curve.points.size(), CURVE_MAX_POINTS); + + gl->glUniform1i( + gl->glGetUniformLocation(command.shaderId, + (propCString + ".numPoints").c_str()), + nPoints); + + for (int i = 0; i < nPoints; i++) { + const auto& pt = curve.points[i]; + std::string idx = "[" + std::to_string(i) + "]"; + + gl->glUniform2f( + gl->glGetUniformLocation(command.shaderId, + (propCString + ".anchors" + idx).c_str()), + pt.x, pt.y); + + gl->glUniform2f( + gl->glGetUniformLocation(command.shaderId, + (propCString + ".handleR" + idx).c_str()), + pt.x + pt.rx, pt.y + pt.ry); + + gl->glUniform2f( + gl->glGetUniformLocation(command.shaderId, + (propCString + ".handleL" + idx).c_str()), + pt.x + pt.lx, pt.y + pt.ly); + } + } break; + } + } + + // render triangles + vbo->bind(); + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), nullptr); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), + reinterpret_cast(3 * sizeof(float))); + + gl->glDrawArrays(GL_TRIANGLES, 0, 6); + + vbo->release(); + } + + vao->release(); +} + +void RenderWorker::kill() { running = false; } diff --git a/src/texturelab/graphics/renderworker.h b/src/texturelab/graphics/renderworker.h new file mode 100644 index 00000000..a3f4b2f5 --- /dev/null +++ b/src/texturelab/graphics/renderworker.h @@ -0,0 +1,114 @@ +#pragma once + +#include "../props.h" +#include "noderenderer.h" +#include +#include +#include +#include +#include +#include +#include +#include + +class QOffscreenSurface; +class QOpenGLContext; +class QOpenGLFunctions_3_2_Core; +class QOpenGLVertexArrayObject; +class QOpenGLBuffer; +class QOpenGLShader; +class QOpenGLShaderProgram; +class QOpenGLFramebufferObject; + +class TextureNode; +typedef QSharedPointer TextureNodePtr; + +struct RenderNodeInput { + QString nodeId; + QString inputName; + GLuint textureId; +}; + +struct RenderProp { + QString propName; + PropType::Value propType; + QVariant value; + GLuint textureId; // For ImageProp: pre-uploaded texture ID +}; + +struct RenderCommand { + QString nodeId; + + GLuint textureId; + GLuint shaderId; + bool shaderLinked; + + GLuint fboId; + + int textureWidth; + int textureHeight; + float randomSeed; + + // CPU processing support + bool usesCpuProcessing = false; + // Shared (not raw) so the node stays alive for the lifetime of this + // command even if it's removed from the project while queued/in-flight. + TextureNodePtr nodePtr; + + // Every input the node declares, connected or not. Uniform state lives on + // the shader program, so an input left over from a previous render still + // has its sampler and _connected flag set: all declared inputs have + // to be cleared each render, not just the connected ones. + QStringList inputNames; + + // only the inputs that currently have a connection + QList inputs; + + // props + QList props; + + // Custom rendering support (null = standard single-pass) + std::shared_ptr renderer; + std::shared_ptr renderData; +}; + +class RenderWorker : public QObject { + Q_OBJECT + + QOffscreenSurface* surface; + QOpenGLContext* ctx; + QOpenGLFunctions_3_2_Core* gl; + QOpenGLVertexArrayObject* vao; + QOpenGLBuffer* vbo; + QOpenGLShader* vshader; + QOpenGLShader* fshader; + QOpenGLFramebufferObject* fbo; + + // use custom dbo that gets shared across render textures + GLuint fboId; + + RenderResourceCache resourceCache; + + QMutex mutex; + std::atomic running; + QQueue renderQueue; + + void renderSinglePass(const RenderCommand& command); + +public: + RenderWorker(); + + void setRenderQueue(QQueue renderQueue); + void run(); + void kill(); + void processRenderCommand(const RenderCommand& command); + void renderNextInQueue(); + + // Must be called from main thread before run() - Windows requires surface creation on GUI thread + void initSurface(); + void setup(); + +private: +signals: + void nodeRendered(QString nodeId, GLuint textureId); +}; \ No newline at end of file diff --git a/src/texturelab/graphics/texturerenderer.cpp b/src/texturelab/graphics/texturerenderer.cpp new file mode 100644 index 00000000..9d120c0a --- /dev/null +++ b/src/texturelab/graphics/texturerenderer.cpp @@ -0,0 +1,978 @@ +#include "texturerenderer.h" +#include "noderenderer.h" +#include "renderworker.h" +// #include "../models.h" + +// https://forum.qt.io/topic/84779/how-to-create-a-qoffscreensurface-correctly/4 +// #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// Qt6 only!! +#include +// #include +// #include + +#include "../props.h" +#include "../systeminfo.h" +#include "../telemetry.h" +#include "models.h" + +// #define RENDER_IN_MAIN_THREAD + +enum class VertexUsage : int { + Position = 0, + Color = 1, + TexCoord0 = 2, + TexCoord1 = 3, + TexCoord2 = 4, + TexCoord3 = 5, + Normal = 6, + Tangent = 7, + Count = 8 +}; + +const int TEXTURE_SIZE = 1024; + +// Test hook for the out-of-VRAM path. A dev box with a 6 GB card will never +// actually fail at 4K, so `TEXTURELAB_FBO_FAIL_ABOVE=2048` makes +// createNodeTexture() report failure above that size and lets the rollback, +// the dialog and the Sentry payload be exercised deterministically. +// Dev builds only — it must not be reachable in a release binary. +static bool shouldFailTextureAllocation(int resolution) +{ +#ifdef TEXTURELAB_DEV_BUILD + static const int threshold = []() { + bool ok = false; + const int value = + qEnvironmentVariableIntValue("TEXTURELAB_FBO_FAIL_ABOVE", &ok); + return ok ? value : 0; + }(); + return threshold > 0 && resolution > threshold; +#else + Q_UNUSED(resolution); + return false; +#endif +} + +// https://github.com/cromop/mOffscreenRendering/blob/master/OGLWidget.cpp +// https://github.com/florianblume/Qt3D-OffscreenRenderer/blob/master/offscreensurfaceframegraph.h +// https://stackoverflow.com/questions/60515589/offscreen-render-with-qoffscreensurface-using-docker +void TextureRenderer::testRendering() +{ + // create shader + QOpenGLShader* vshader = new QOpenGLShader(QOpenGLShader::Vertex); + QOpenGLShader* fshader = new QOpenGLShader(QOpenGLShader::Fragment); + auto program = new QOpenGLShaderProgram; + + QString vSource = R""""( + #version 150 core + + in vec3 a_pos; + in vec2 a_texCoord; + + out vec2 v_texCoord; + + void main() + { + v_texCoord = a_texCoord*vec2(1,1); + gl_Position = vec4(a_pos,1); + } + )""""; + + QString fSource = R""""( + #version 150 core + + in vec2 v_texCoord; + + out vec4 fragColor; + + void main() + { + vec4 col = vec4(v_texCoord.x, v_texCoord.y, 1.0, 1.0); + + fragColor = col; + } + )""""; + + if (!vshader->compileSourceCode(vSource)) { + qDebug() << "VERTEX SHADER ERROR"; + qDebug() << vshader->log(); + } + + if (!fshader->compileSourceCode(fSource)) { + qDebug() << "FRAGMENT SHADER ERROR"; + qDebug() << fshader->log(); + } + + program->removeAllShaders(); + + program->addShader(vshader); + program->addShader(fshader); + + program->bindAttributeLocation("a_pos", (int)VertexUsage::Position); + program->bindAttributeLocation("a_color", (int)VertexUsage::Color); + program->bindAttributeLocation("a_texCoord", (int)VertexUsage::TexCoord0); + + if (!program->link()) { + qDebug() << "SHADER LINK ERROR"; + qDebug() << program->log(); + } + + // render quad (bind vbo, shader, vbo, etc) + fbo->bind(); + gl->glViewport(0, 0, TEXTURE_SIZE, TEXTURE_SIZE); + + gl->glClearColor(0, 0, 0, 1); + gl->glClearDepth(0); + gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + vao->bind(); + + program->bind(); + + vbo->bind(); + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, GL_FALSE, + 5 * sizeof(float), nullptr); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), + reinterpret_cast(3 * sizeof(float))); + + gl->glDrawArrays(GL_TRIANGLES, 0, 6); + + vbo->release(); + + gl->glBindFramebuffer(GL_FRAMEBUFFER, ctx->defaultFramebufferObject()); + + // grab pixels to pixmap + auto img = fbo->toImage(); + img.save("./render.png"); + + // save to desktop +} + +void TextureRenderer::setup() +{ + // create surface + surface = new QOffscreenSurface(); + // QSurfaceFormat format = QSurfaceFormat::defaultFormat(); + // format.setMajorVersion(3); + // format.setMinorVersion(2); + + QSurfaceFormat format; + format.setDepthBufferSize(32); + format.setMajorVersion(3); + format.setMinorVersion(2); + format.setProfile(QSurfaceFormat::CoreProfile); + format.setSamples(1); + format.setSwapInterval(0); + format.setOption(QSurfaceFormat::DebugContext); // for debugging + + surface->setFormat(format); + surface->create(); + + // create context + ctx = new QOpenGLContext(); + // https://doc.qt.io/qt-6/qopenglcontext.html#globalShareContext + auto* globalContext = QOpenGLContext::globalShareContext(); + ctx->setShareContext(globalContext); + ctx->setFormat(format); + if (!ctx->create()) { + qFatal("unable to create surface!"); + } + + ctx->makeCurrent(surface); + + // https://doc-snapshots.qt.io/qt6-dev/gui-changes-qt6.html + gl = QOpenGLVersionFunctionsFactory::get(ctx); + if (!gl) { + qFatal("Could not obtain required OpenGL context version"); + } + + // setup debugging + QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(); + logger->initialize(); + + QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, + [=](const QOpenGLDebugMessage& debugMessage) { + qDebug() << debugMessage; + }); + + logger->startLogging(); + + gl->initializeOpenGLFunctions(); + + // simple setup + gl->glViewport(0, 0, TEXTURE_SIZE, TEXTURE_SIZE); + gl->glDisable(GL_BLEND); + gl->glDisable(GL_DEPTH_TEST); + gl->glDisable(GL_CULL_FACE); + + // setup vbo + vao = new QOpenGLVertexArrayObject; + if (vao->create()) + vao->bind(); + + // setup vertex array + QVector data; + // TRIANGLE 1 + data.append(-1); + data.append(-1); + data.append(0); + + data.append(0); + data.append(0); + + data.append(1); + data.append(-1); + data.append(0); + + data.append(1); + data.append(0); + + data.append(-1); + data.append(1); + data.append(0); + + data.append(0); + data.append(1); + + // TRIANGLE 2 + data.append(-1); + data.append(1); + data.append(0); + + data.append(0); + data.append(1); + + data.append(1); + data.append(-1); + data.append(0); + + data.append(1); + data.append(0); + + data.append(1); + data.append(1); + data.append(0); + + data.append(1); + data.append(1); + + vbo = new QOpenGLBuffer; + vbo->create(); + vbo->bind(); + vbo->allocate(data.constData(), data.count() * sizeof(float)); + vbo->release(); + + // create fbo + // https://doc.qt.io/qt-6/qopenglframebufferobject.html + // https://www.qt.io/blog/2015/09/21/using-modern-opengl-es-features-with-qopenglframebufferobject-in-qt-5-6 + QOpenGLFramebufferObjectFormat fboFormat; + fboFormat.setInternalTextureFormat(GL_RGBA32F); + fbo = new QOpenGLFramebufferObject(TEXTURE_SIZE, TEXTURE_SIZE, fboFormat); + if (!fbo->isValid()) { + qFatal("FBO could not be created"); + } + + // Our context is current here and this is the GUI thread, so this is the + // one place guaranteed to be able to ask the driver what hardware we're on. + SystemInfo::reportGpuContext(); + + this->initRenderWorker(); +} + +TextureRenderer::TextureRenderer() { this->setup(); } + +TextureRenderer::~TextureRenderer() +{ + // Stop and clean up render worker thread + if (renderWorker) { + renderWorker->kill(); + } + +#ifndef RENDER_IN_MAIN_THREAD + if (renderThread) { + renderThread->quit(); + renderThread->wait(); + delete renderThread; + renderThread = nullptr; + } +#endif + + if (renderWorker) { + delete renderWorker; + renderWorker = nullptr; + } + + // Clean up OpenGL resources (must have context current) + if (ctx && surface) { + ctx->makeCurrent(surface); + + if (fbo) { + delete fbo; + fbo = nullptr; + } + + if (vbo) { + vbo->destroy(); + delete vbo; + vbo = nullptr; + } + + if (vao) { + vao->destroy(); + delete vao; + vao = nullptr; + } + + ctx->doneCurrent(); + } + + // Clean up context and surface + if (ctx) { + delete ctx; + ctx = nullptr; + } + + if (surface) { + surface->destroy(); + delete surface; + surface = nullptr; + } +} + +void TextureRenderer::setProject(TextureProjectPtr project) +{ + this->project = project; +} + +void TextureRenderer::update() +{ + if (!project) + return; + + const int requested = project->textureWidth; + bool allocationFailed = false; + + // check for nodes that need updating and update + for (auto& node : project->nodes) { + // Defensive: a null entry should never reach the map now that lookups + // use .value() (Step 1), but guard the render loop regardless. + if (!node) + continue; + + if (!node->isGraphicsResourcesInitialized()) { + // create texture + if (!initializeNodeGraphicsResources(node)) { + allocationFailed = true; + break; + } + } + + // if the resolution has changed, resize texture + // Deferred while a render is in flight: the texture/FBO being + // replaced here may be captured as an input GLuint in the command + // currently queued/executing on the worker thread. Once that + // command completes, nodeRendered() re-invokes update(), which will + // pick this resize back up. + if (!renderInFlight && + (project->textureWidth != node->textureWidth || + project->textureHeight != node->textureHeight)) { + if (!this->createNodeTexture(node)) { + allocationFailed = true; + break; + } + + // clear pixmap and emit thumbnail changed? + } + } + + if (allocationFailed) { + // Out of VRAM part-way through the batch. Retreat to the last size we + // know fits rather than leaving half the graph unallocated (and, before + // this path existed, aborting the process outright). + rollBackResolution(requested); + return; + } + + if (lastGoodResolution != requested) { + lastGoodResolution = requested; + Telemetry::setTag("texture.resolution", std::to_string(requested)); + } + + if (!renderInFlight) + this->queueNextNodeToRender(); +} + +void TextureRenderer::rollBackResolution(int requested) +{ + // Nothing known-good to retreat to — the very first allocation failed, so + // there is no smaller size on record. Leave the graph unrendered; the + // captureException in createNodeTexture() has already reported why. + if (lastGoodResolution <= 0 || lastGoodResolution == requested) + return; + + const int fallback = lastGoodResolution; + project->textureWidth = fallback; + project->textureHeight = fallback; + + Telemetry::breadcrumb( + "render", "resolution rolled back after allocation failure", + {{"requested", (int64_t)requested}, + {"fallback", (int64_t)fallback}, + {"node_count", (int64_t)project->nodes.size()}}); + Telemetry::setTag("texture.resolution", std::to_string(fallback)); + + // Re-allocate everything at the size we know fits. A node whose texture was + // freed on the way up gets it back here; one that still fails is skipped by + // getNextUpdatableNode() rather than dereferenced. + for (auto& node : project->nodes) { + if (!node) + continue; + if (!node->texture || node->textureWidth != fallback || + node->textureHeight != fallback) + this->createNodeTexture(node); + node->isDirty = true; + } + + emit resolutionChangeFailed(requested, fallback); + + if (!renderInFlight) + this->queueNextNodeToRender(); +} + +void TextureRenderer::updateOld() +{ + if (!project) + return; + + // check for nodes that need updating and update + for (auto& node : project->nodes) { + if (!node->isGraphicsResourcesInitialized()) { + // create texture + initializeNodeGraphicsResources(node); + } + + // if the resolution has changed, resize texture + if (project->textureWidth != node->textureWidth || + project->textureHeight != node->textureHeight) { + // resize + // resizeNodeTexture(node); + node->textureWidth = project->textureWidth; + node->textureHeight = project->textureHeight; + QOpenGLFramebufferObjectFormat fboFormat; + fboFormat.setInternalTextureFormat(GL_RGBA32F); + node->texture = new QOpenGLFramebufferObject( + node->textureWidth, node->textureHeight, fboFormat); + + // clear pixmap and emit thumbnail changed? + } + + // if (node->needsUpdate()) { + + // // process + // } + } + + ctx->makeCurrent(surface); + // todo: use quota + while (true) { + auto nextNode = getNextUpdatableNode(); + if (!nextNode) + break; + + qDebug() << "Rendering node: " << nextNode->id; + renderNode(nextNode); + + nextNode->isDirty = false; + + // auto img = nextNode->texture->toImage(); + // emit thumbnailGenerated(nextNode->id, QPixmap::fromImage(img)); + + auto texId = nextNode->texture->texture(); + emit thumbnailGenerated(nextNode->id, texId, QPixmap()); + } + + ctx->doneCurrent(); +} + +bool TextureRenderer::initializeNodeGraphicsResources( + const TextureNodePtr& node) +{ + if (!this->createNodeTexture(node)) + return false; + + ctx->makeCurrent(surface); + // build and compile shaders + node->shader = buildShaderForNode(node); + ctx->doneCurrent(); + + return true; +} + +SystemInfo::GpuMemory TextureRenderer::queryGpuMemory() +{ + // Same save/restore dance as handleExport(): the caller may be inside a + // widget's paint or event handling with its own context bound. + QOpenGLContext* previous = QOpenGLContext::currentContext(); + QSurface* previousSurface = previous ? previous->surface() : nullptr; + + ctx->makeCurrent(surface); + const SystemInfo::GpuMemory mem = SystemInfo::queryGpuMemory(); + ctx->doneCurrent(); + + if (previous && previousSurface) + previous->makeCurrent(previousSurface); + + return mem; +} + +bool TextureRenderer::createNodeTexture(const TextureNodePtr& node) +{ + ctx->makeCurrent(surface); + + // Safe to free now: callers only reach here when !renderInFlight, so no + // queued/executing RenderCommand can be holding this texture's GLuint + // as an input. + if (node->texture) { + delete node->texture; + node->texture = nullptr; + } + + const int width = project->textureWidth; + const int height = project->textureHeight; + + // Start from a clean slate so the GL_OUT_OF_MEMORY check below can only be + // reporting on this allocation. + while (gl->glGetError() != GL_NO_ERROR) { + } + + // create fbo + QOpenGLFramebufferObjectFormat fboFormat; + fboFormat.setInternalTextureFormat(GL_RGBA32F); + node->texture = new QOpenGLFramebufferObject(width, height, fboFormat); + node->textureWidth = width; + node->textureHeight = height; + + // Running out of VRAM shows up either as an incomplete FBO or as a + // GL_OUT_OF_MEMORY left behind by the texture allocation, depending on the + // driver. 4096x4096 RGBA32F is 256 MiB per node, so a graph of any size at + // 4K will exhaust a 1-2 GB card — this used to qFatal() and take the whole + // app down with an unreadable driver-side stack. + const GLenum err = gl->glGetError(); + const bool failed = !node->texture->isValid() || err == GL_OUT_OF_MEMORY || + shouldFailTextureAllocation(width); + + if (failed) { + delete node->texture; + node->texture = nullptr; + + const SystemInfo::GpuMemory mem = SystemInfo::queryGpuMemory(); + + ctx->doneCurrent(); + + Telemetry::captureException( + "node texture allocation failed", + {{"width", (int64_t)width}, + {"height", (int64_t)height}, + {"bytes_per_node", estimatedNodeTextureBytes(width)}, + {"node_count", (int64_t)(project ? project->nodes.size() : 0)}, + {"estimated_total_bytes", + estimatedNodeTextureBytes(width) * + (int64_t)(project ? project->nodes.size() : 0)}, + {"gl_error", (int64_t)err}, + {"vram_known", mem.known}, + {"vram_total_mb", mem.known ? mem.totalKb / 1024 : (int64_t)-1}, + {"vram_available_mb", + mem.known ? mem.availableKb / 1024 : (int64_t)-1}}); + + qWarning("node texture allocation failed at %dx%d (glGetError 0x%04x)", + width, height, err); + return false; + } + + // make texture wrap + gl->glBindTexture(GL_TEXTURE_2D, node->textureId()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + gl->glBindTexture(GL_TEXTURE_2D, 0); + + // This texture ID is shared into the render worker's context on another + // thread; flush so its creation is visible there before it's used. + gl->glFlush(); + + ctx->doneCurrent(); + + return true; +} + +void TextureRenderer::renderNode(const TextureNodePtr& node) +{ + node->texture->bind(); + gl->glViewport(0, 0, node->textureWidth, node->textureHeight); + + gl->glClearColor(0, 0, 0, 1); + gl->glClearDepth(0); + gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + vao->bind(); + + auto shader = node->shader; + + if (shader->isLinked()) { + shader->bind(); + + // clear all inputs + int texIndex = 0; + for (auto input : node->inputs) { + gl->glActiveTexture(GL_TEXTURE0 + texIndex); + gl->glBindTexture(GL_TEXTURE_2D, 0); + + // gl->glUniform1i(node->shader->uniformLocation(input), 0); + shader->setUniformValue(input.toStdString().c_str(), 0); + shader->setUniformValue( + (input + "_connected").toStdString().c_str(), 0); + + texIndex++; + } + + // pass inputs + auto nodeInputs = getNodeInputs(node); + texIndex = 0; + for (auto nodeInput : nodeInputs) { + gl->glActiveTexture(GL_TEXTURE0 + texIndex); + // if (!nodeInput.node->texture->bind()) + // qFatal("could not bind texture"); + gl->glBindTexture(GL_TEXTURE_2D, + nodeInput.node->texture->texture()); + + auto name = nodeInput.name; + // gl->glUniform1i(node->shader->uniformLocation(input), 0); + shader->setUniformValue(name.toStdString().c_str(), texIndex); + shader->setUniformValue((name + "_connected").toStdString().c_str(), + 1); + + texIndex++; + } + + // pass seed + shader->setUniformValue( + "_seed", (GLfloat)(project->randomSeed + node->randomSeed)); + + // texture size + shader->setUniformValue( + "_textureSize", + QVector2D(project->textureWidth, project->textureHeight)); + + // pass props + for (auto prop : node->props) { + auto propCString = ("prop_" + prop->name.toStdString()); + auto propName = propCString.c_str(); + // qDebug() << "glsl prop: " << propName; + switch (prop->type) { + case PropType::Int: { + auto intVal = ((IntProp*)prop)->value; + shader->setUniformValue(propName, (GLint)intVal); + } break; + case PropType::Float: { + auto floatVal = ((FloatProp*)prop)->value; + shader->setUniformValue(propName, (GLfloat)floatVal); + } break; + case PropType::Bool: { + auto boolVal = ((BoolProp*)prop)->value; + shader->setUniformValue(propName, (GLint)boolVal == true); + } break; + case PropType::Enum: { + auto enumVal = ((EnumProp*)prop)->index; + shader->setUniformValue(propName, (GLint)enumVal); + } break; + case PropType::Color: { + auto colorVal = ((ColorProp*)prop)->value; + shader->setUniformValue( + propName, QVector4D(colorVal.redF(), colorVal.greenF(), + colorVal.blueF(), colorVal.alphaF())); + } break; + case PropType::Gradient: + // todo: pass gradient + break; + } + } + + // render triangles + vbo->bind(); + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), nullptr); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), + reinterpret_cast(3 * sizeof(float))); + + gl->glDrawArrays(GL_TRIANGLES, 0, 6); + + vbo->release(); + } + + vao->release(); + + // gl->glBindFramebuffer(GL_FRAMEBUFFER, + // ctx->defaultFramebufferObject()); + + // grab pixels to pixmap + // auto img = node->texture->toImage(); + // img.save(node->id + ".png"); + + node->texture->release(); +} + +void TextureRenderer::initRenderWorker() +{ + renderWorker = new RenderWorker(); + renderWorker->initSurface(); + + QObject::connect(renderWorker, &RenderWorker::nodeRendered, this, + &TextureRenderer::nodeRendered); + +#ifdef RENDER_IN_MAIN_THREAD + // ensure it creates its own context and resources on main thread + renderWorker->setup(); +#else + renderThread = new QThread(); + + renderWorker->moveToThread(renderThread); + + QObject::connect(renderThread, &QThread::started, renderWorker, + &RenderWorker::run); + + renderThread->start(); +#endif +} + +void TextureRenderer::nodeRendered(const QString& nodeId, GLuint texId) +{ + qDebug() << "TextureRenderer: Node rendered:" << nodeId; + renderInFlight = false; + emit thumbnailGenerated(nodeId, texId, QPixmap()); + // update() re-checks pending resizes deferred while a render was in + // flight, then queues the next node. + this->update(); + if (project) { + int total = project->nodes.size(); + int clean = 0; + // update() may have just queued the next node, which is already + // flagged clean. Counting it would report the graph finished while + // its last node is still rendering, and the launcher thumbnail taken + // on that signal would miss whatever channels that node feeds. + for (const auto& n : project->nodes) + if (!n->isDirty && !(renderInFlight && n->id == inFlightNodeId)) + clean++; + emit renderProgress(clean, total); + } +} + +void TextureRenderer::queueNextNodeToRender() +{ + auto nextNode = getNextUpdatableNode(); + if (!!nextNode) { + RenderCommand cmd; + cmd.textureWidth = nextNode->textureWidth; + cmd.textureHeight = nextNode->textureHeight; + cmd.fboId = nextNode->texture->handle(); + cmd.textureId = nextNode->textureId(); + cmd.nodeId = nextNode->id; + cmd.shaderId = nextNode->shader->programId(); + cmd.shaderLinked = nextNode->shader->isLinked(); + cmd.randomSeed = project->randomSeed + nextNode->randomSeed; + + // Custom renderer support + cmd.renderer = nextNode->createRenderer(); + if (cmd.renderer) { + cmd.renderData = nextNode->createRenderData(); + } + + // CPU processing support + cmd.usesCpuProcessing = nextNode->usesCpuProcessing; + // Keep the node alive for the lifetime of the command (it may still + // be queued or mid-render on the worker thread if the node is + // removed from the project in the meantime). + cmd.nodePtr = nextNode; + + cmd.inputNames = nextNode->inputs; + + // inputs + auto nodeInputs = getNodeInputs(nextNode); + for (auto input : nodeInputs) { + RenderNodeInput rin; + rin.nodeId = input.node->id; + rin.inputName = input.name; + rin.textureId = input.node->textureId(); + cmd.inputs.append(rin); + } + + // props + for (auto prop : nextNode->props) { + RenderProp rnp; + rnp.propName = prop->name; + rnp.propType = prop->type; + rnp.textureId = 0; + + // Upload ImageProp textures to GPU on main thread + if (prop->type == PropType::Image) { + auto imageProp = (ImageProp*)prop; + + if (!imageProp->value.isNull()) { + ctx->makeCurrent(surface); + imageProp->updateTexture(); + + rnp.textureId = imageProp->getTextureId(); + + // Shared into the render worker's context on another + // thread; flush so the upload is visible there. + gl->glFlush(); + + ctx->doneCurrent(); + } + } + else { + rnp.value = prop->getValue(); + } + + cmd.props.append(rnp); + } + + QQueue queue; + queue.enqueue(cmd); + + // pass to render worker to process + renderWorker->setRenderQueue(queue); + renderInFlight = true; + inFlightNodeId = nextNode->id; + + // mark node as clean before rendering to avoid double-queuing + nextNode->isDirty = false; + +#ifdef RENDER_IN_MAIN_THREAD + renderWorker->renderNextInQueue(); +#endif + } +} + +QVector TextureRenderer::getNodeInputs(const TextureNodePtr& node) +{ + QVector inputs; + for (auto con : project->connections) { + if (con->rightNode == node) { + inputs.append({con->leftNode, con->rightNodeInputName}); + } + } + + return inputs; +} + +TextureNodePtr TextureRenderer::getNextUpdatableNode() const +{ + // for each node, if node is dirty and all deps are + // non-dirty the this is a valid node + + for (auto node : project->nodes) { + if (!node) + continue; + + if (!node->isDirty) + continue; + + // A node whose texture/shader allocation failed has no FBO to render + // into; queueNextNodeToRender() would dereference it. Skip rather than + // crash — update() has already reported and rolled back. + if (!node->isGraphicsResourcesInitialized()) + continue; + + auto hasCleanDeps = true; + + // we have a dirty node, check if all deps are clean + auto deps = project->getNodeDependencies(node->id); + for (auto dep : deps) { + // A null dep means an input connection references a node that no + // longer exists; treat it as not-yet-renderable rather than + // dereferencing it. + if (!dep || dep->isDirty) { + hasCleanDeps = false; + break; + } + } + + if (hasCleanDeps) { + return node; + } + } + + return TextureNodePtr(nullptr); +} + +QOpenGLShaderProgram* +TextureRenderer::buildShaderForNode(const TextureNodePtr& node) +{ + ctx->makeCurrent(surface); + + QOpenGLShader* vshader = new QOpenGLShader(QOpenGLShader::Vertex); + QOpenGLShader* fshader = new QOpenGLShader(QOpenGLShader::Fragment); + auto program = new QOpenGLShaderProgram; + + // Build input/prop declaration lists for RenderResourceCache helpers + QStringList inputNames = node->inputs; + + QList> propTypes; + for (auto prop : node->props) + propTypes.append({prop->name, (int)prop->type}); + + QString fSource = RenderResourceCache::fragmentPreamble() + + RenderResourceCache::randomLib() + + RenderResourceCache::gradientLib() + + RenderResourceCache::curveLib() + + RenderResourceCache::generateInputDeclarations(inputNames) + + RenderResourceCache::generatePropDeclarations(propTypes) + + "#line 0\n" + + node->shaderSource; + + if (!vshader->compileSourceCode(RenderResourceCache::standardVertexSource())) { + qDebug() << "VERTEX SHADER ERROR"; + qDebug() << vshader->log(); + } + + if (!fshader->compileSourceCode(fSource)) { + qDebug() << "FRAGMENT SHADER ERROR"; + qDebug() << fshader->log(); + } + + program->removeAllShaders(); + program->addShader(vshader); + program->addShader(fshader); + + program->bindAttributeLocation("a_pos", (int)VertexUsage::Position); + program->bindAttributeLocation("a_color", (int)VertexUsage::Color); + program->bindAttributeLocation("a_texCoord", (int)VertexUsage::TexCoord0); + + if (!program->link()) { + qDebug() << "SHADER LINK ERROR"; + qDebug() << program->log(); + } + + // Shared into the render worker's context on another thread; flush so + // the link is visible there before glUseProgram() is called on it. + gl->glFlush(); + + ctx->doneCurrent(); + + return program; +} \ No newline at end of file diff --git a/src/texturelab/graphics/texturerenderer.h b/src/texturelab/graphics/texturerenderer.h new file mode 100644 index 00000000..9252d145 --- /dev/null +++ b/src/texturelab/graphics/texturerenderer.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include + +#include "../systeminfo.h" + +#include + +class QOffscreenSurface; +class QOpenGLContext; +class QOpenGLFunctions_3_2_Core; +class QOpenGLVertexArrayObject; +class QOpenGLBuffer; +class QOpenGLShader; +class QOpenGLShaderProgram; +class QOpenGLFramebufferObject; + +class RenderWorker; +class TextureProject; +typedef QSharedPointer TextureProjectPtr; +class TextureNode; +typedef QSharedPointer TextureNodePtr; + +struct NodeInput { + TextureNodePtr node; + QString name; +}; + +// https://stackoverflow.com/questions/31323749/easiest-way-for-offscreen-rendering-with-qopenglwidget +class TextureRenderer : public QObject { + Q_OBJECT + + QOpenGLFunctions_3_2_Core* gl; + QOpenGLVertexArrayObject* vao; + QOpenGLBuffer* vbo; + QOpenGLShader* vshader; + QOpenGLShader* fshader; + QOpenGLFramebufferObject* fbo; + + QThread* renderThread; + RenderWorker* renderWorker; + + // True from the moment a RenderCommand is handed to the worker until its + // nodeRendered() callback fires. GUI-thread only. While true, node + // textures/FBOs must not be resized/recreated: their GLuints may be + // captured as inputs in the in-flight command. + bool renderInFlight = false; + + // The node that in-flight command renders. It's marked clean when queued + // (to avoid double-queuing), so progress must not count it as done yet. + QString inFlightNodeId; + + // The last resolution every node was successfully allocated at. A failed + // resize (4K on a small-VRAM GPU is the case that prompted this) rolls the + // project back to it instead of aborting the process. + int lastGoodResolution = 0; + +public: + TextureRenderer(); + ~TextureRenderer(); + void setup(); + void setProject(TextureProjectPtr project); + void update(); + void updateOld(); + void testRendering(); + + // False when the node's texture could not be allocated. + bool initializeNodeGraphicsResources(const TextureNodePtr& node); + // False when the FBO could not be allocated — almost always the GPU being + // out of memory for a w*h*16-byte RGBA32F target per node. + bool createNodeTexture(const TextureNodePtr& node); + void renderNode(const TextureNodePtr& node); + + // Free/total VRAM as the driver reports it, queried on this renderer's own + // context. Callers on the GUI thread generally have no context current, and + // SystemInfo::queryGpuMemory() needs one — going through here rather than + // relying on whichever context a widget happened to leave bound. + SystemInfo::GpuMemory queryGpuMemory(); + + // Bytes of VRAM one node's texture needs at the given square resolution. + // RGBA32F: 4 channels x 4 bytes. + static int64_t estimatedNodeTextureBytes(int resolution) + { + return (int64_t)resolution * resolution * 16; + } + + TextureProjectPtr project; + QOffscreenSurface* surface; + QOpenGLContext* ctx; + +private: + // Retreat to lastGoodResolution after a failed allocation batch, re-allocate + // there, and emit resolutionChangeFailed(). + void rollBackResolution(int requested); + + void initRenderWorker(); + void nodeRendered(const QString& nodeId, GLuint texId); + void queueNextNodeToRender(); + + QVector getNodeInputs(const TextureNodePtr& node); + TextureNodePtr getNextUpdatableNode() const; + QOpenGLShaderProgram* buildShaderForNode(const TextureNodePtr& node); + +signals: + void thumbnailGenerated(const QString& nodeId, GLuint texId, + const QPixmap& pixmap); + void renderProgress(int clean, int total); + + // The project could not be allocated at `requested`; it has been rolled + // back to `fallback` and is rendering normally again. GraphWidget uses this + // to tell the user and put the resolution picker back. + void resolutionChangeFailed(int requested, int fallback); +}; + +// note: there's no specified fbo limit +// https://stackoverflow.com/a/41164761 +class NodeTexture { +public: + // texture id + int width; + int height; + + QPixmap thumbnail; +}; \ No newline at end of file diff --git a/src/texturelab/jsonutils.h b/src/texturelab/jsonutils.h new file mode 100644 index 00000000..c8782f10 --- /dev/null +++ b/src/texturelab/jsonutils.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +// Older project files aren't strict about json types: numbers were sometimes +// written as strings ("1.5", "3"), ints as floats (3.0) and bools as +// "true"/"false". These helpers coerce whatever is in the json into the type +// the caller wants and fall back to defaultValue when the value is missing or +// can't be read as one. +namespace jsonutils { + +inline double getDouble(const QJsonValue& val, double defaultValue = 0.0) +{ + if (val.isDouble()) + return val.toDouble(); + + if (val.isString()) { + bool ok = false; + auto num = val.toString().trimmed().toDouble(&ok); + return ok ? num : defaultValue; + } + + if (val.isBool()) + return val.toBool() ? 1.0 : 0.0; + + return defaultValue; +} + +inline float getFloat(const QJsonValue& val, float defaultValue = 0.0f) +{ + return (float)getDouble(val, (double)defaultValue); +} + +inline long getLong(const QJsonValue& val, long defaultValue = 0) +{ + return (long)std::llround(getDouble(val, (double)defaultValue)); +} + +inline int getInt(const QJsonValue& val, int defaultValue = 0) +{ + return (int)getLong(val, (long)defaultValue); +} + +inline bool getBool(const QJsonValue& val, bool defaultValue = false) +{ + if (val.isBool()) + return val.toBool(); + + if (val.isDouble()) + return val.toDouble() != 0.0; + + if (val.isString()) { + auto str = val.toString().trimmed().toLower(); + if (str == "true" || str == "yes") + return true; + if (str == "false" || str == "no" || str.isEmpty()) + return false; + return getDouble(val, defaultValue ? 1.0 : 0.0) != 0.0; + } + + return defaultValue; +} + +inline QString getString(const QJsonValue& val, + const QString& defaultValue = QString()) +{ + if (val.isString()) + return val.toString(); + + if (val.isDouble()) { + auto num = val.toDouble(); + return num == std::floor(num) ? QString::number((qlonglong)num) + : QString::number(num); + } + + if (val.isBool()) + return val.toBool() ? QStringLiteral("true") : QStringLiteral("false"); + + return defaultValue; +} + +} // namespace jsonutils diff --git a/src/texturelab/launcher/launcherformat.cpp b/src/texturelab/launcher/launcherformat.cpp new file mode 100644 index 00000000..1b6ca462 --- /dev/null +++ b/src/texturelab/launcher/launcherformat.cpp @@ -0,0 +1,172 @@ +#include "launcherformat.h" + +#include "texturelistmodel.h" +#include "thememanager.h" +#include "tokens.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace launcherfmt { + +QString relativeTime(qint64 whenMs) +{ + if (whenMs <= 0) + return QStringLiteral("—"); + + const QDateTime when = QDateTime::fromMSecsSinceEpoch(whenMs); + const qint64 secs = when.secsTo(QDateTime::currentDateTime()); + + if (secs < 60) + return QCoreApplication::translate("launcher", "just now"); + if (secs < 3600) + return QStringLiteral("%1m").arg(secs / 60); + if (secs < 86400) + return QStringLiteral("%1h").arg(secs / 3600); + if (secs < 172800) + return QCoreApplication::translate("launcher", "yesterday"); + if (secs < 604800) + return QStringLiteral("%1d").arg(secs / 86400); + if (secs < 2592000) + return QStringLiteral("%1w").arg(secs / 604800); + if (secs < 31536000) + return QStringLiteral("%1mo").arg(secs / 2592000); + return QStringLiteral("%1y").arg(secs / 31536000); +} + +QString resolutionLabel(int width, int height) +{ + if (width <= 0 || height <= 0) + return QString(); + + auto shorten = [](int value) -> QString { + if (value >= 1024 && value % 1024 == 0) + return QStringLiteral("%1K").arg(value / 1024); + return QString::number(value); + }; + + if (width == height) + return shorten(width); + return QStringLiteral("%1×%2").arg(shorten(width), shorten(height)); +} + +QString tooltipFor(const QModelIndex& index, const QString& currentVersionLabel) +{ + if (!index.isValid()) + return QString(); + + QStringList lines; + + // Always the path: with no folder tree in the window, this is the only + // place a texture's location is visible. + lines << index.data(TextureListModel::PathRole).toString(); + + if (index.data(TextureListModel::MissingRole).toBool()) { + lines << QCoreApplication::translate("launcher", + "Not found on disk — open it to locate the file."); + } + + if (index.data(TextureListModel::NeedsMigrationRole).toBool()) { + const QString from = index.data(TextureListModel::LibVersionRole).toString(); + lines << QCoreApplication::translate( + "launcher", "Created with library %1; opening offers an upgrade to %2.") + .arg(from, currentVersionLabel); + } + + const int nodes = index.data(TextureListModel::NodeCountRole).toInt(); + if (nodes > 0) + lines << QCoreApplication::translate("launcher", "%n node(s)", nullptr, nodes); + + return lines.join(QLatin1Char('\n')); +} + +void paintStar(QPainter* painter, const QRectF& box, const QColor& color) +{ + QPainterPath path; + const QPointF center = box.center(); + const double outer = box.width() / 2.0; + const double inner = outer * 0.45; + + for (int i = 0; i < 10; ++i) { + const double radius = (i % 2 == 0) ? outer : inner; + const double angle = -M_PI / 2.0 + i * M_PI / 5.0; + const QPointF point(center.x() + radius * std::cos(angle), + center.y() + radius * std::sin(angle)); + + // moveTo for the first point, not lineTo: a fresh QPainterPath starts at + // the origin, so lineTo here drags a stroke across the whole widget. + if (i == 0) + path.moveTo(point); + else + path.lineTo(point); + } + path.closeSubpath(); + + painter->setPen(Qt::NoPen); + painter->setBrush(color); + painter->drawPath(path); +} + +namespace { + +QFont versionBadgeFont() +{ + QFont font = ThemeManager::instance().theme().font("ui"); + + // Step down in whichever unit the theme actually set — it builds fonts with + // setPixelSize, which leaves pointSize() at -1. + if (font.pixelSize() > 0) + font.setPixelSize(qMax(9, font.pixelSize() - 2)); + else + font.setPointSize(qMax(7, font.pointSize() - 2)); + + return font; +} + +} // namespace + +QSize versionBadgeSize(const QString& version) +{ + if (version.isEmpty()) + return QSize(); + + const QFontMetrics metrics(versionBadgeFont()); + return QSize(metrics.horizontalAdvance(version) + 8, metrics.height() + 2); +} + +void paintVersionBadge(QPainter* painter, const QRect& rect, const QString& version) +{ + if (version.isEmpty() || rect.isEmpty()) + return; + + const Theme& theme = ThemeManager::instance().theme(); + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + // Translucent dark plate rather than a solid one: it sits on top of the + // thumbnail, and a fully opaque chip punches a hole in the material. + QColor plate = theme.color(Tokens::BgElevated); + plate.setAlpha(185); + + painter->setPen(Qt::NoPen); + painter->setBrush(plate); + + const int radius = qMax(2, theme.radius("sm")); + painter->drawRoundedRect(rect, radius, radius); + + painter->setFont(versionBadgeFont()); + painter->setPen(theme.color(Tokens::LauncherBadge)); + painter->drawText(rect, Qt::AlignCenter | Qt::TextSingleLine, version); + + painter->restore(); +} + +} // namespace launcherfmt diff --git a/src/texturelab/launcher/launcherformat.h b/src/texturelab/launcher/launcherformat.h new file mode 100644 index 00000000..4c7f50a9 --- /dev/null +++ b/src/texturelab/launcher/launcherformat.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +class QColor; +class QPainter; +class QRect; +class QRectF; +class QSize; + +// Presentation helpers shared by the card and row delegates. +// +// This is where the launcher's formatting lives, deliberately outside the +// model: TextureListModel exposes raw values so that two views can present the +// same row differently, and both views agreeing on "2h" and "4K" is a matter of +// sharing this file, not of pushing strings back into the model. +namespace launcherfmt { + +// "just now", "2h", "yesterday", "3d", "1mo" — short enough to sit under a 96px +// card without eliding. Returns an em dash for "never". +QString relativeTime(qint64 whenMs); + +// 4096 -> "4K", 1024 -> "1K", 512 -> "512", non-square -> "2K×1K". Empty when +// the resolution isn't known, which is the case for rows seeded from the old +// recent-files list and not yet opened. +QString resolutionLabel(int width, int height); + +// The full path, plus whatever explains an otherwise cryptic marker on the +// card: the missing state, the migration badge, the node count. +QString tooltipFor(const QModelIndex& index, const QString& currentVersionLabel); + +// A five-pointed star filling `box`. Shared so "starred" reads the same in the +// grid and the list — as a shape, not just a color. The migration badge is a +// dot in the same warn color, and two identical dots side by side in a row say +// nothing. +void paintStar(QPainter* painter, const QRectF& box, const QColor& color); + +// The library-version chip ("v1") shown on textures written by an older node +// library. A bare colored dot said only "something is unusual"; the version +// number says which one, which is the thing worth knowing before opening. +// Size first so the caller can place it, then paint into that rect. +QSize versionBadgeSize(const QString& version); +void paintVersionBadge(QPainter* painter, const QRect& rect, const QString& version); + +} // namespace launcherfmt diff --git a/src/texturelab/launcher/launcherwindow.cpp b/src/texturelab/launcher/launcherwindow.cpp new file mode 100644 index 00000000..9fb90649 --- /dev/null +++ b/src/texturelab/launcher/launcherwindow.cpp @@ -0,0 +1,1008 @@ +#include "launcherwindow.h" + +#include "catalogservice.h" +#include "libraries/libversion.h" +#include "texturecarddelegate.h" +#include "texturelistmodel.h" +#include "telemetry.h" +#include "texturerowdelegate.h" +#include "update/updatechecker.h" +#include "widgets/crashconsentdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Object names are the hook for resources/qss/app.qss.in — the launcher adds no +// inline stylesheets (see scripts/check-theme-hygiene.sh). +constexpr const char* kTopBarName = "launcherTopBar"; +constexpr const char* kActionBarName = "launcherActionBar"; +constexpr const char* kGridName = "launcherGrid"; +constexpr const char* kFilterTabName = "launcherFilterTab"; +constexpr const char* kEmptyPanelName = "launcherEmptyPanel"; +constexpr const char* kEmptyLabelName = "launcherEmptyLabel"; +constexpr const char* kUpdateButtonName = "launcherUpdateButton"; + +// Long enough for the window manager to have mapped and placed the launcher, +// which is what the prompt centres itself on. Nothing breaks if it is early — +// the dialog just opens off-centre — so this buys margin rather than being a +// timing the code depends on. +constexpr int kConsentPromptDelayMs = 250; + +bool isTextureFile(const QUrl& url) +{ + return url.isLocalFile() && url.toLocalFile().endsWith(QStringLiteral(".texture"), + Qt::CaseInsensitive); +} + +} // namespace + +// Exists only to reach setViewportMargins(), which QAbstractScrollArea keeps +// protected. The grid uses it to park the pixels left over from dividing the +// row into whole columns, half at each end, instead of letting them all collect +// past the last card. +class LauncherGridView : public QListView { +public: + using QListView::QListView; + + void setLeadingMargin(int margin) + { + if (margin == leading) + return; + leading = margin; + setViewportMargins(margin, 0, 0, 0); + } + +private: + int leading = 0; +}; + +LauncherWindow::LauncherWindow(QWidget* parent) : QWidget(parent) +{ + setWindowTitle(QStringLiteral("TextureLab")); + setMinimumSize(800, 600); + resize(1280, 820); + setAcceptDrops(true); + + model = new TextureListModel(this); + cardDelegate = new TextureCardDelegate(this); + rowDelegate = new TextureRowDelegate(this); + + const QString currentVersion = libVersionToString(currentLibVersion()); + model->setCurrentLibVersion(currentVersion); + cardDelegate->setCurrentVersionLabel(currentVersion); + rowDelegate->setCurrentVersionLabel(currentVersion); + + CatalogService& catalog = CatalogService::instance(); + if (catalog.isReady()) { + model->setIndex(&catalog.index()); + model->setThumbnailCache(&catalog.thumbnails()); + } + + // The index changes from save, open, and the reconciliation pass; the grid + // follows rather than polling. + connect(&catalog, &CatalogService::catalogChanged, this, &LauncherWindow::refresh); + + grid = new LauncherGridView(this); + grid->setObjectName(QLatin1String(kGridName)); + grid->setModel(model); + grid->setViewMode(QListView::IconMode); + grid->setResizeMode(QListView::Adjust); + grid->setMovement(QListView::Static); + grid->setUniformItemSizes(true); + grid->setSelectionMode(QAbstractItemView::ExtendedSelection); + grid->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + grid->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + grid->setMouseTracking(true); + grid->setSpacing(6); + grid->setContextMenuPolicy(Qt::CustomContextMenu); + grid->setFrameShape(QFrame::NoFrame); + + connect(grid, &QListView::doubleClicked, this, [this]() { openSelected(); }); + connect(grid, &QWidget::customContextMenuRequested, this, &LauncherWindow::showContextMenu); + + // Sits over the grid rather than replacing it, so switching filters can't + // leave the window structurally empty. + emptyPanel = new QWidget(grid); + emptyPanel->setObjectName(QLatin1String(kEmptyPanelName)); + emptyPanel->hide(); + + auto* emptyLayout = new QVBoxLayout(emptyPanel); + emptyLayout->setContentsMargins(24, 24, 24, 24); + emptyLayout->setSpacing(16); + emptyLayout->addStretch(1); + + emptyLabel = new QLabel(emptyPanel); + emptyLabel->setObjectName(QLatin1String(kEmptyLabelName)); + emptyLabel->setAlignment(Qt::AlignCenter); + emptyLabel->setWordWrap(true); + emptyLayout->addWidget(emptyLabel); + + // The way out of a first-run window. Same signal as the action bar's + // button, so the caller can't tell which one the user pressed. + emptyNewButton = new QPushButton(tr("New Texture"), emptyPanel); + emptyNewButton->setProperty("variant", "primary"); // the one thing to do here + emptyNewButton->setCursor(Qt::PointingHandCursor); + connect(emptyNewButton, &QPushButton::clicked, this, &LauncherWindow::newTextureRequested); + emptyLayout->addWidget(emptyNewButton, 0, Qt::AlignHCenter); + emptyLayout->addStretch(1); + + grid->viewport()->installEventFilter(this); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(buildTopBar()); + layout->addWidget(grid, 1); + layout->addWidget(buildActionBar()); + + connect(model, &QAbstractItemModel::modelReset, this, &LauncherWindow::updateEmptyState); + connect(model, &QAbstractItemModel::rowsInserted, this, &LauncherWindow::updateEmptyState); + + // Whether the grid scrolls decides whether it reserves room for a scrollbar, + // and that follows the row count, which moves without the window resizing. + connect(model, &QAbstractItemModel::modelReset, this, &LauncherWindow::relayoutGrid); + connect(model, &QAbstractItemModel::rowsInserted, this, &LauncherWindow::relayoutGrid); + connect(model, &QAbstractItemModel::rowsRemoved, this, &LauncherWindow::relayoutGrid); + + // Last, so it can drive widgets the two build* methods created. + restoreViewState(); + updateEmptyState(); + + updates = new UpdateChecker(this); + connect(updates, &UpdateChecker::updateAvailable, this, &LauncherWindow::showUpdateNotice); +} + +LauncherWindow::~LauncherWindow() = default; + +QWidget* LauncherWindow::buildTopBar() +{ + auto* bar = new QWidget(this); + bar->setObjectName(QLatin1String(kTopBarName)); + + auto* layout = new QHBoxLayout(bar); + layout->setContentsMargins(12, 8, 12, 8); + layout->setSpacing(8); + + // All · Recents · Starred — the saved views, as three text tabs rather than + // a sidebar. + auto* group = new QButtonGroup(bar); + group->setExclusive(true); + + auto makeTab = [&](const QString& text, catalog::Filter filter, bool checked) { + auto* tab = new QToolButton(bar); + tab->setObjectName(QLatin1String(kFilterTabName)); + tab->setText(text); + tab->setCheckable(true); + tab->setChecked(checked); + tab->setCursor(Qt::PointingHandCursor); + group->addButton(tab); + layout->addWidget(tab); + connect(tab, &QToolButton::clicked, this, [this, filter]() { + model->setFilter(filter); + updateEmptyState(); + saveViewState(); + }); + return tab; + }; + + allTab = makeTab(QStringLiteral("All"), catalog::Filter::All, true); + recentsTab = makeTab(QStringLiteral("Recents"), catalog::Filter::Recents, false); + starredTab = makeTab(QStringLiteral("Starred"), catalog::Filter::Starred, false); + + layout->addStretch(1); + + search = new QLineEdit(bar); + search->setPlaceholderText(QStringLiteral("Search…")); + search->setClearButtonEnabled(true); + search->setFixedWidth(240); + connect(search, &QLineEdit::textChanged, this, [this](const QString& text) { + model->setSearchTerm(text); + updateEmptyState(); + }); + layout->addWidget(search); + + // Sort dropdown, parked for now. sortBox stays null while this is commented + // out, and every other use of it is null-guarded, so the launcher just runs + // on the model's default order (Last Modified, newest first). Uncomment to + // bring it back — applySort() and the saved "sort" setting are still wired. + // + // sortBox = new QComboBox(bar); + // sortBox->addItem(QStringLiteral("Last Modified")); + // sortBox->addItem(QStringLiteral("Last Opened")); + // sortBox->addItem(QStringLiteral("Name")); + // sortBox->addItem(QStringLiteral("Size")); + // connect(sortBox, &QComboBox::currentIndexChanged, this, [this](int comboIndex) { + // applySort(comboIndex); + // saveViewState(); + // }); + // layout->addWidget(sortBox); + + // Update notice: absent entirely until a newer release exists, so the bar + // stays quiet in the normal case. + updateButton = new QToolButton(bar); + updateButton->setObjectName(QLatin1String(kUpdateButtonName)); + updateButton->setCursor(Qt::PointingHandCursor); + // The icon's stroke is white, which is what the accent-filled button wants; + // it is not recolored by the theme, so it must not be used on a light fill. + updateButton->setIcon(QIcon(QStringLiteral(":/icons/download.svg"))); + updateButton->setIconSize(QSize(14, 14)); + updateButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + updateButton->setVisible(false); + layout->addWidget(updateButton); + + auto* gear = new QToolButton(bar); + gear->setText(QStringLiteral("⚙")); + gear->setPopupMode(QToolButton::InstantPopup); + auto* menu = new QMenu(gear); + menu->addAction(QStringLiteral("Check for Updates…"), this, [this]() { + if (!UpdateChecker::isEnabled()) { + QMessageBox::information( + this, tr("Check for Updates"), + tr("Update checks are turned off.\n\nTurn them back on from this menu to let " + "TextureLab ask texturelab.io whether a newer build exists.")); + return; + } + // A manual check has to say something either way; the automatic one + // stays silent unless there's news. + connect( + updates, &UpdateChecker::checkFinished, this, + [this](bool found, const QString& error) { + if (found) + return; // the notice in the bar is the answer + // Both outcomes name the endpoint that was actually used. It is + // set at compile time and overridable by the environment, so + // "which server did it ask?" is otherwise unanswerable from + // inside the app — and that is exactly the question you have + // when a dev server sees no traffic. + const QString endpoint = + tr("Checked: %1 (%2 channel)") + .arg(UpdateChecker::apiBase(), UpdateChecker::channel()); + + if (error.isEmpty()) { + QMessageBox::information( + this, tr("Check for Updates"), + tr("TextureLab is up to date.\n\n%1").arg(endpoint)); + } + else { + QMessageBox::warning(this, tr("Check for Updates"), + tr("Could not reach the update server.\n\n%1\n\n%2") + .arg(error, endpoint)); + } + }, + Qt::SingleShotConnection); + + updates->check(/*force=*/true); + }); + + auto* toggleChecks = menu->addAction(QStringLiteral("Check for Updates Automatically")); + toggleChecks->setCheckable(true); + toggleChecks->setChecked(UpdateChecker::isEnabled()); + connect(toggleChecks, &QAction::toggled, this, [](bool on) { UpdateChecker::setEnabled(on); }); + + menu->addSeparator(); + + crashReportsAction = menu->addAction(tr("Send Anonymous Crash Reports")); + crashReportsAction->setCheckable(true); + crashReportsAction->setChecked(Telemetry::isAllowed()); + connect(crashReportsAction, &QAction::toggled, this, [](bool on) { + // Toggling is an answer too, so it stamps the version and stops the + // prompt coming back for a decision the user has just made by hand. + Telemetry::recordConsent(on); + Telemetry::setEnabled(on); + }); + + // The editor's Help menu writes the same setting, so the check can be stale + // by the time the menu is opened. + connect(menu, &QMenu::aboutToShow, this, [this]() { + QSignalBlocker block(crashReportsAction); + crashReportsAction->setChecked(Telemetry::isAllowed()); + }); + + menu->addSeparator(); + menu->addAction(QStringLiteral("Clear Missing Textures"), this, [this]() { + CatalogService& catalog = CatalogService::instance(); + if (!catalog.isReady()) + return; + const int removed = catalog.index().removeAllMissing(); + if (removed > 0) + refresh(); + }); + gear->setMenu(menu); + layout->addWidget(gear); + + return bar; +} + +QWidget* LauncherWindow::buildActionBar() +{ + auto* bar = new QWidget(this); + bar->setObjectName(QLatin1String(kActionBarName)); + + auto* layout = new QHBoxLayout(bar); + layout->setContentsMargins(12, 8, 12, 8); + layout->setSpacing(8); + + // ⊞ / ☰ — same model, different delegate. This is the payoff for keeping + // formatting out of the model. + auto* viewGroup = new QButtonGroup(bar); + viewGroup->setExclusive(true); + + auto makeViewToggle = [&](const QString& glyph, const QString& tip, bool checked) { + auto* button = new QToolButton(bar); + button->setObjectName(QLatin1String(kFilterTabName)); + button->setText(glyph); + button->setToolTip(tip); + button->setCheckable(true); + button->setChecked(checked); + button->setCursor(Qt::PointingHandCursor); + viewGroup->addButton(button); + layout->addWidget(button); + return button; + }; + + gridToggle = makeViewToggle(QStringLiteral("⊞"), tr("Grid"), true); + listToggle = makeViewToggle(QStringLiteral("☰"), tr("List"), false); + connect(gridToggle, &QToolButton::clicked, this, [this]() { setGridMode(true); }); + connect(listToggle, &QToolButton::clicked, this, [this]() { setGridMode(false); }); + + sizeSlider = new QSlider(Qt::Horizontal, bar); + sizeSlider->setRange(TextureCardDelegate::MinCardWidth, TextureCardDelegate::MaxCardWidth); + sizeSlider->setValue(targetCardWidth); + sizeSlider->setFixedWidth(120); + sizeSlider->setToolTip(tr("Card size — cards stretch to fill the row")); + connect(sizeSlider, &QSlider::valueChanged, this, [this](int value) { + // A target, not the drawn width: relayoutGrid() turns it into a column + // count and hands the delegate whatever divides the viewport evenly. + targetCardWidth = value; + relayoutGrid(); + saveViewState(); + }); + layout->addWidget(sizeSlider); + + layout->addStretch(1); + + auto* newButton = new QPushButton(QStringLiteral("New Texture"), bar); + connect(newButton, &QPushButton::clicked, this, &LauncherWindow::newTextureRequested); + layout->addWidget(newButton); + + openButton = new QPushButton(QStringLiteral("Open"), bar); + openButton->setDefault(true); + openButton->setAutoDefault(true); + connect(openButton, &QPushButton::clicked, this, [this]() { openSelected(); }); + layout->addWidget(openButton); + + return bar; +} + +void LauncherWindow::applySort(int comboIndex) +{ + switch (comboIndex) { + case 1: + model->setSort(catalog::SortKey::Opened, false); + break; + case 2: + model->setSort(catalog::SortKey::Name, true); + break; + case 3: + model->setSort(catalog::SortKey::Size, false); + break; + default: + model->setSort(catalog::SortKey::Modified, false); + break; + } +} + +void LauncherWindow::relayoutGrid() +{ + if (!gridMode || !grid || !cardDelegate || relayouting) + return; + + // Setting the viewport margins below resizes the viewport, and a viewport + // resize is what calls this — so the pass has to be allowed to finish + // before the one it provokes can start. + const QScopedValueRollback guard(relayouting, true); + + const int spacing = grid->spacing(); + + // grid->width(), not the viewport's: the viewport narrows when the + // scrollbar appears, and whether the scrollbar appears is one of the things + // this function decides. Measuring the frame keeps the input to the + // calculation independent of its own output. + const int total = grid->width() - grid->frameWidth() * 2; + if (total <= 0) + return; + + // Columns from the target width, then the pitch widened to consume the + // remainder, so the leftover lands in the thumbnails instead of collecting + // as a ragged right margin. + // + // Stated as an explicit grid size rather than left to icon mode's own + // packing: with a grid size set the view fits exactly usable / pitch cells, + // which is a rule this can invert. Its default wrapping folds in the item + // margins and a fencepost, and being one pixel over there costs a whole + // column silently. + auto fit = [&](int available) { + // A grid cell is the card plus one spacing, and the row is indented by + // half of one before the first cell — the card sits centered in its + // cell, so the leading half-gutter is not real estate the columns can + // have. + const int usable = available - spacing / 2; + + int columns = qMax(1, usable / (targetCardWidth + spacing)); + int width = usable / columns - spacing; + + // A maxed-out slider on a wide window would otherwise stretch past what + // the delegate is willing to draw, which puts the gap straight back. An + // extra column costs every card a few pixels and the row nothing. + while (width > TextureCardDelegate::MaxCardWidth) { + ++columns; + width = usable / columns - spacing; + } + + return qMakePair(columns, qMax(width, TextureCardDelegate::MinCardWidth)); + }; + + const int extent = grid->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, + grid->verticalScrollBar()); + + // Lay the row out against the full width first, then ask whether that many + // rows overflow. Narrowing only ever means fewer columns and so more rows, + // so an answer of "it scrolls" can't be undone by the second pass — no + // oscillation between the two states. + QPair fitted = fit(total); + const int rows = (model->rowCount() + fitted.first - 1) / fitted.first; + const int contentHeight = + spacing / 2 + rows * (cardDelegate->heightForWidth(fitted.second) + spacing); + + // One row-gap of headroom before committing to no scrollbar: guessing wrong + // in that direction would leave the last row unreachable, where guessing + // wrong the other way only costs the strip of margin this is here to + // reclaim. + const bool scrolls = contentHeight > grid->viewport()->height() - spacing; + const int layoutWidth = scrolls ? total - extent : total; + if (scrolls) + fitted = fit(layoutWidth); + + // Icon mode deducts the scrollbar's width from every row whenever the + // policy is ScrollBarAsNeeded, whether or not the bar is actually up — that + // phantom reservation was the dead strip down the right-hand side. Ask for + // it only when the bar really is coming; when everything fits there is + // nothing to scroll and so nothing to reserve. + grid->setVerticalScrollBarPolicy(scrolls ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff); + + + const int columns = fitted.first; + int width = fitted.second; + + // Dividing the row into whole columns leaves up to one pixel per column + // over, and it all collects past the last card — the asymmetry that reads + // as an odd right-hand margin. Split it instead, by indenting the leading + // edge. Measured against the frame width rather than the viewport's, so the + // margin this sets can't feed back into its own input. + int leftover = layoutWidth - spacing / 2 - columns * (width + spacing); + + // The row can't be centered on less slack than the half-gutter the wrap + // rule holds back at the far end. When the division came out nearly exact, + // giving up a pixel of card width buys that back at a pixel per column. + if (leftover < spacing / 2 && width > TextureCardDelegate::MinCardWidth) { + --width; + leftover += columns; + } + + // Never more than the slack itself: the columns were fitted to a row this + // wide, and indenting past what's spare would push the last one off it. + // Clamped before the split, because the pre-layout pass runs against a + // hundred-pixel window where the one column already overruns and the slack + // is negative. + const int slack = qMax(0, leftover); + grid->setLeadingMargin(qMin((slack + spacing / 2) / 2, slack)); + + // The card width alone can't gate this: a pass can run while the scrollbar + // is still up from a narrower state and lay out against that viewport, then + // the bar drops and the next pass computes the same width and would decline + // to re-fit the row it now has room for. + if (width == cardDelegate->cardWidth() && grid->viewport()->width() == laidOutWidth) + return; + + laidOutWidth = grid->viewport()->width(); + cardDelegate->setCardWidth(width); + + // setGridSize() relayouts on its own, but only when the value changes — the + // viewport-width case above arrives with the same grid size and still needs + // the row re-fitted. + grid->setGridSize(QSize(width + spacing, cardDelegate->heightForWidth(width) + spacing)); + grid->doItemsLayout(); +} + +void LauncherWindow::setGridMode(bool useGrid) +{ + gridMode = useGrid; + + if (useGrid) { + grid->setItemDelegate(cardDelegate); + grid->setViewMode(QListView::IconMode); + grid->setSpacing(6); + grid->setWordWrap(false); + } + else { + grid->setItemDelegate(rowDelegate); + grid->setViewMode(QListView::ListMode); + // Rows are separated by their own hairline, so view spacing would only + // break the continuous surface a table wants. + grid->setSpacing(0); + + // Rows size themselves; leaving the card pitch in place would stamp + // every one of them into a square cell. + grid->setGridSize(QSize()); + + // Rows run the full width of the window; the centering margin is a + // property of the card row, not of the view. + grid->setLeadingMargin(0); + } + + // The card width is a function of the viewport, and in list mode nothing + // has been maintaining it — recompute before the view asks for sizeHints. + laidOutWidth = -1; + relayoutGrid(); + + // Icon mode caches item positions; swapping the delegate changes every + // sizeHint, and only a reset makes the view ask again. + grid->reset(); + + if (gridToggle) + gridToggle->setChecked(useGrid); + if (listToggle) + listToggle->setChecked(!useGrid); + if (sizeSlider) + sizeSlider->setEnabled(useGrid); // the row height is fixed + + saveViewState(); +} + +void LauncherWindow::restoreViewState() +{ + QSettings settings; + settings.beginGroup(QStringLiteral("launcher")); + + if (sizeSlider) { + const int width = settings.value(QStringLiteral("cardWidth"), targetCardWidth).toInt(); + targetCardWidth = qBound(TextureCardDelegate::MinCardWidth, width, + TextureCardDelegate::MaxCardWidth); + QSignalBlocker block(sizeSlider); + sizeSlider->setValue(targetCardWidth); + } + + if (sortBox) { + const int sortIndex = settings.value(QStringLiteral("sort"), 0).toInt(); + if (sortIndex >= 0 && sortIndex < sortBox->count()) { + QSignalBlocker block(sortBox); + sortBox->setCurrentIndex(sortIndex); + applySort(sortIndex); + } + } + + // Filter last: All is the safe default if the stored value is nonsense. + const int filter = settings.value(QStringLiteral("filter"), 0).toInt(); + if (filter == 1 && recentsTab) { + recentsTab->setChecked(true); + model->setFilter(catalog::Filter::Recents); + } + else if (filter == 2 && starredTab) { + starredTab->setChecked(true); + model->setFilter(catalog::Filter::Starred); + } + + setGridMode(settings.value(QStringLiteral("gridMode"), true).toBool()); + + settings.endGroup(); +} + +void LauncherWindow::saveViewState() const +{ + QSettings settings; + settings.beginGroup(QStringLiteral("launcher")); + settings.setValue(QStringLiteral("gridMode"), gridMode); + // The target, not the stretched result — restoring the latter would let a + // window resized once permanently redefine what the slider means. + settings.setValue(QStringLiteral("cardWidth"), targetCardWidth); + if (sortBox) + settings.setValue(QStringLiteral("sort"), sortBox->currentIndex()); + + int filter = 0; + if (model->filter() == catalog::Filter::Recents) + filter = 1; + else if (model->filter() == catalog::Filter::Starred) + filter = 2; + settings.setValue(QStringLiteral("filter"), filter); + + settings.endGroup(); +} + +void LauncherWindow::refresh() +{ + model->refresh(); + updateEmptyState(); +} + +void LauncherWindow::updateEmptyState() +{ + if (model->totalCount() > 0) { + emptyPanel->hide(); + return; + } + + // Three different nothings, and conflating them is how a first-run window + // ends up looking broken instead of new. + QString message; + bool offerNew = false; + if (!model->searchTerm().isEmpty()) { + message = tr("No textures match “%1”").arg(model->searchTerm()); + } + else if (model->filter() == catalog::Filter::Starred) { + message = tr("No starred textures yet.\nStar one from its right-click menu."); + } + else if (model->filter() == catalog::Filter::Recents) { + message = tr("Nothing opened yet."); + } + else { + message = tr("No textures yet.\n\nTextures you create or open will appear here."); + offerNew = true; + } + + emptyLabel->setText(message); + // Only on the genuine first run — offering "New Texture" as the answer to a + // search that found nothing would be answering a different question. + emptyNewButton->setVisible(offerNew); + layoutEmptyPanel(); + emptyPanel->show(); + emptyPanel->raise(); +} + +void LauncherWindow::layoutEmptyPanel() +{ + if (emptyPanel) + emptyPanel->setGeometry(grid->viewport()->geometry()); +} + +bool LauncherWindow::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == grid->viewport() && event->type() == QEvent::Resize) { + // An overlay has to follow the viewport by hand; without this, resizing + // the window while empty leaves the button parked where the grid used + // to end. + layoutEmptyPanel(); + relayoutGrid(); + } + return QWidget::eventFilter(watched, event); +} + +void LauncherWindow::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + refresh(); + search->setFocus(); + + // Throttled internally, so reopening the launcher from the Home button all + // day costs one request every few hours. + if (updates) + updates->check(); + + maybeAskCrashConsent(); +} + +void LauncherWindow::maybeAskCrashConsent() +{ + if (consentPrompted || !Telemetry::consentNeeded()) + return; + + consentPrompted = true; + + // Deferred so the launcher is painted, and placed by the window manager, + // before the prompt measures it to centre itself. Asked over a blank + // window, the prompt reads like an installer step rather than something the + // app is asking for. + QTimer::singleShot(kConsentPromptDelayMs, this, [this]() { + if (Telemetry::consentNeeded()) + CrashConsentDialog::ask(this); + if (crashReportsAction) + crashReportsAction->setChecked(Telemetry::isAllowed()); + }); +} + +void LauncherWindow::showUpdateNotice(const QString& version, const QString& title, + const QString& downloadUrl) +{ + if (!updateButton) + return; + + // Fixed label rather than the version number: the button's job is to be + // recognisable at a glance, and the specific version is detail that belongs + // in the tooltip next to the release title. + updateButton->setText(tr("New Version Available")); + + QStringList tip; + tip << tr("Version %1").arg(version); + if (!title.isEmpty()) + tip << title; + tip << downloadUrl; + updateButton->setToolTip(tip.join(QLatin1Char('\n'))); + updateButton->setVisible(true); + + // Opening the browser is the whole action — the launcher never downloads or + // installs anything on the user's behalf. + disconnect(updateButton, &QToolButton::clicked, nullptr, nullptr); + connect(updateButton, &QToolButton::clicked, this, + [downloadUrl]() { QDesktopServices::openUrl(QUrl(downloadUrl)); }); +} + +void LauncherWindow::openSelected() +{ + const QModelIndexList selection = grid->selectionModel()->selectedIndexes(); + + // With nothing selected, Open falls back to the file dialog. That's also + // how a texture the launcher has never seen gets in, so it must never be + // disabled (LAUNCHER_PRD.md §3.1). + if (selection.isEmpty()) { + emit openDialogRequested(); + return; + } + + const catalog::TextureRecord rec = model->recordAt(selection.first()); + if (rec.path.isEmpty()) + return; + + if (!QFileInfo::exists(rec.path)) { + // Offer to fix it rather than just reporting the problem. Moves aren't + // detected automatically, so this is how a relocated texture keeps its + // stars and history. + const auto answer = QMessageBox::question( + this, tr("Texture Not Found"), + tr("This texture is no longer at:\n%1\n\nIf you moved it, you can point the " + "launcher at its new location.") + .arg(rec.path), + QMessageBox::Cancel | QMessageBox::Open, QMessageBox::Open); + + if (answer == QMessageBox::Open) + locate(rec); + return; + } + + emit openPathRequested(rec.path); +} + +void LauncherWindow::locate(const catalog::TextureRecord& rec) +{ + CatalogService& catalog = CatalogService::instance(); + if (!catalog.isReady()) + return; + + // Start where it used to live: a moved file is usually a sibling of its old + // home, or the user at least remembers the neighbourhood. + const QString startDir = QFileInfo(rec.path).absolutePath(); + + const QString chosen = QFileDialog::getOpenFileName( + this, tr("Locate “%1”").arg(rec.name), startDir, + tr("Texturelab File (*.texture)")); + + if (chosen.isEmpty()) + return; + + const QFileInfo info(chosen); + const qint64 survivor = catalog.index().relocate(rec.id, info.absoluteFilePath(), info.size(), + info.lastModified().toMSecsSinceEpoch()); + + if (survivor < 0) { + QMessageBox::warning(this, tr("Locate Texture"), + tr("Could not update the launcher entry:\n%1") + .arg(catalog.index().lastError())); + return; + } + + // The file at the new path may be different content entirely, so the old + // thumbnail can't be trusted. It regenerates on the next open or save. + catalog.thumbnails().removeTexture(survivor); + + refresh(); + + const QModelIndex found = model->indexForId(survivor); + if (found.isValid()) { + grid->setCurrentIndex(found); + grid->scrollTo(found); + } +} + +void LauncherWindow::showContextMenu(const QPoint& pos) +{ + const QModelIndex index = grid->indexAt(pos); + if (!index.isValid()) + return; + + if (!grid->selectionModel()->isSelected(index)) + grid->setCurrentIndex(index); + + const catalog::TextureRecord rec = model->recordAt(index); + + QMenu menu(this); + menu.addAction(tr("Open"), this, [this]() { openSelected(); }); + if (rec.isMissing()) + menu.addAction(tr("Locate…"), this, [this, rec]() { locate(rec); }); + menu.addSeparator(); + menu.addAction(rec.starred ? tr("Unstar") : tr("Star"), this, + [this]() { toggleStarOnSelection(); }); + menu.addAction(tr("Show in File Manager"), this, [rec]() { + // Opens the containing directory; selecting the file itself needs + // per-platform shell calls that aren't worth it here. + QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(rec.path).absolutePath())); + }); + menu.addSeparator(); + menu.addAction(tr("Remove from Launcher"), this, + [this]() { removeSelectionFromLauncher(); }); + + menu.exec(grid->viewport()->mapToGlobal(pos)); +} + +void LauncherWindow::toggleStarOnSelection() +{ + CatalogService& catalog = CatalogService::instance(); + if (!catalog.isReady()) + return; + + const QModelIndexList selection = grid->selectionModel()->selectedIndexes(); + if (selection.isEmpty()) + return; + + // One toggle for the whole selection, driven by the first item, so a + // multi-select doesn't half-star and half-unstar. + const bool starred = model->recordAt(selection.first()).starred; + for (const QModelIndex& index : selection) { + const catalog::TextureRecord rec = model->recordAt(index); + if (rec.isValid()) + catalog.index().setStarred(rec.id, !starred); + } + + refresh(); +} + +void LauncherWindow::removeSelectionFromLauncher() +{ + CatalogService& catalog = CatalogService::instance(); + if (!catalog.isReady()) + return; + + const QModelIndexList selection = grid->selectionModel()->selectedIndexes(); + if (selection.isEmpty()) + return; + + const auto answer = QMessageBox::question( + this, tr("Remove from Launcher"), + tr("Remove %n texture(s) from the launcher?\n\nThe files stay on disk — this only " + "forgets them here.", + nullptr, int(selection.size())), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (answer != QMessageBox::Yes) + return; + + for (const QModelIndex& index : selection) { + const catalog::TextureRecord rec = model->recordAt(index); + if (rec.isValid()) + catalog.forget(rec.id); + } + + refresh(); +} + +void LauncherWindow::setOpenPath(const QString& path) +{ + model->setOpenPath(path); +} + +void LauncherWindow::setHasDocument(bool value) +{ + hasDocument = value; +} + +void LauncherWindow::keyPressEvent(QKeyEvent* event) +{ + if (event->matches(QKeySequence::Find)) { + search->setFocus(); + search->selectAll(); + event->accept(); + return; + } + + if (event->key() == Qt::Key_Escape) { + // Clear the search first if there is one; only then close. And never + // close when there's no document behind us — that would leave the user + // staring at nothing. + if (!search->text().isEmpty()) { + search->clear(); + event->accept(); + return; + } + if (hasDocument) + emit closeRequested(); + event->accept(); + return; + } + + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { + openSelected(); + event->accept(); + return; + } + + QWidget::keyPressEvent(event); +} + +void LauncherWindow::closeEvent(QCloseEvent* event) +{ + if (!hasDocument) { + // The window manager's close button on first launch means "quit", not + // "show me the empty editor behind this". + event->accept(); + return; + } + + event->ignore(); + emit closeRequested(); +} + +void LauncherWindow::dragEnterEvent(QDragEnterEvent* event) +{ + if (!event->mimeData()->hasUrls()) { + event->ignore(); + return; + } + + for (const QUrl& url : event->mimeData()->urls()) { + if (isTextureFile(url)) { + event->acceptProposedAction(); + return; + } + } + event->ignore(); +} + +void LauncherWindow::dropEvent(QDropEvent* event) +{ + // Dropping a .texture adds it and opens it — the explicit recovery path if + // index.db is ever lost (LAUNCHER_PRD.md §1.1). + for (const QUrl& url : event->mimeData()->urls()) { + if (isTextureFile(url)) { + event->acceptProposedAction(); + emit openPathRequested(url.toLocalFile()); + return; + } + } + event->ignore(); +} diff --git a/src/texturelab/launcher/launcherwindow.h b/src/texturelab/launcher/launcherwindow.h new file mode 100644 index 00000000..448e281b --- /dev/null +++ b/src/texturelab/launcher/launcherwindow.h @@ -0,0 +1,141 @@ +#pragma once + +#include "texturerecord.h" + +#include + +class QAction; +class QComboBox; +class QLabel; +class QLineEdit; +class QPushButton; +class QSlider; +class QToolButton; + +// Defined in launcherwindow.cpp: a QListView that will let this window set the +// viewport margins, which QAbstractScrollArea otherwise keeps protected. +class LauncherGridView; + +class TextureCardDelegate; +class TextureListModel; +class TextureRowDelegate; +class UpdateChecker; + +// The launcher: a flat grid of every texture the app has touched. +// +// Knows nothing about MainWindow. It emits what the user asked for and lets the +// caller decide how to honor it — which is what keeps "Open" able to run through +// promptSaveIfDirty() without this window having to know that dirty documents +// are a concept. +class LauncherWindow : public QWidget { + Q_OBJECT + +public: + explicit LauncherWindow(QWidget* parent = nullptr); + ~LauncherWindow() override; + + // Marks the document currently loaded in the editor, for the card's "open" + // dot. Empty when nothing is loaded. + void setOpenPath(const QString& path); + + // True once a document exists behind the launcher, which is what makes Esc + // and the close button meaningful — on first launch there is nowhere to go. + void setHasDocument(bool hasDocument); + +signals: + void newTextureRequested(); + void openPathRequested(const QString& path); + void openDialogRequested(); + void closeRequested(); + +public slots: + void refresh(); + + // Reveals the "Update to X" affordance in the top bar. Nothing is + // downloaded; clicking it opens the release page in the browser. + void showUpdateNotice(const QString& version, const QString& title, + const QString& downloadUrl); + +protected: + void keyPressEvent(QKeyEvent* event) override; + void closeEvent(QCloseEvent* event) override; + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + void showEvent(QShowEvent* event) override; + bool eventFilter(QObject* watched, QEvent* event) override; + +private: + QWidget* buildTopBar(); + QWidget* buildActionBar(); + void applySort(int comboIndex); + void setGridMode(bool grid); + + // Re-fits the cards to the viewport: columns come from the slider's target + // width, then every cell stretches to consume the remainder. IconMode's own + // layout keeps a fixed pitch and dumps the leftover as a ragged right + // margin, which is the gap this exists to close. + void relayoutGrid(); + void restoreViewState(); + void saveViewState() const; + void updateEmptyState(); + + // The empty-state overlay isn't a layout child, so it has to be re-fitted + // to the viewport by hand whenever the grid changes size. + void layoutEmptyPanel(); + void openSelected(); + + // Asks the user where a missing texture went and re-points its index row, + // keeping stars, tags, and recency. + void locate(const catalog::TextureRecord& rec); + + // Puts the crash-report consent prompt up the first time the launcher is + // shown on a version that hasn't been asked about yet. + void maybeAskCrashConsent(); + + void showContextMenu(const QPoint& pos); + void toggleStarOnSelection(); + void removeSelectionFromLauncher(); + + UpdateChecker* updates = nullptr; + TextureListModel* model = nullptr; + TextureCardDelegate* cardDelegate = nullptr; + TextureRowDelegate* rowDelegate = nullptr; + + LauncherGridView* grid = nullptr; + QLineEdit* search = nullptr; + QComboBox* sortBox = nullptr; + QSlider* sizeSlider = nullptr; + QWidget* emptyPanel = nullptr; + QLabel* emptyLabel = nullptr; + QPushButton* emptyNewButton = nullptr; + QPushButton* openButton = nullptr; + QToolButton* updateButton = nullptr; + QAction* crashReportsAction = nullptr; + QToolButton* gridToggle = nullptr; + QToolButton* listToggle = nullptr; + QToolButton* allTab = nullptr; + QToolButton* recentsTab = nullptr; + QToolButton* starredTab = nullptr; + + bool hasDocument = false; + bool gridMode = true; + + // The prompt is deferred a tick so the launcher paints behind it, which + // leaves a window where a second showEvent could queue a second copy. + bool consentPrompted = false; + + // Guards against the re-entrant relayout that setting the viewport margins + // provokes: the margin resizes the viewport, and the viewport's resize is + // what calls this in the first place. + bool relayouting = false; + + // The viewport width the cards were last laid out against. The card width + // alone can't gate a relayout: the first pass can run while the scrollbar + // is still up from a narrower state, fit one column fewer than it computed, + // and then match on the next pass and decline to fix itself. + int laidOutWidth = -1; + + // What the slider asks for. The width the cards are actually drawn at is + // this one rounded to fill the row, and lives on the delegate. + int targetCardWidth = 160; +}; diff --git a/src/texturelab/launcher/texturecarddelegate.cpp b/src/texturelab/launcher/texturecarddelegate.cpp new file mode 100644 index 00000000..cd8247ab --- /dev/null +++ b/src/texturelab/launcher/texturecarddelegate.cpp @@ -0,0 +1,260 @@ +#include "texturecarddelegate.h" + +#include "launcherformat.h" +#include "texturelistmodel.h" +#include "texturerecord.h" +#include "thememanager.h" +#include "tokens.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +QColor tc(const char* token) +{ + return ThemeManager::instance().theme().color(token); +} + +QColor tc(const char* token, int alpha) +{ + QColor color = ThemeManager::instance().theme().color(token); + color.setAlpha(alpha); + return color; +} + +int themeRadius() +{ + const int r = ThemeManager::instance().theme().radius("sm"); + return r > 0 ? r : 3; +} + +int themeSpace(const char* key, int fallback) +{ + const int s = ThemeManager::instance().theme().space(key); + return s > 0 ? s : fallback; +} + +// The channel pips, in a fixed order so a card's silhouette is recognizable at +// a glance rather than shuffling with whatever the graph happens to define. +const int kPipOrder[] = { + catalog::ChannelAlbedo, catalog::ChannelNormal, catalog::ChannelRoughness, + catalog::ChannelHeight, catalog::ChannelMetalness, catalog::ChannelAO, +}; +constexpr int kPipCount = int(sizeof(kPipOrder) / sizeof(kPipOrder[0])); + +} // namespace + +TextureCardDelegate::TextureCardDelegate(QObject* parent) : QStyledItemDelegate(parent) {} + +void TextureCardDelegate::setCardWidth(int width) +{ + const int clamped = qBound(MinCardWidth, width, MaxCardWidth); + if (clamped == cardW) + return; + + cardW = clamped; + + // Tells the view its cached geometry is stale. An invalid index means "all + // of them", which is exactly the case when the card size changes. + emit sizeHintChanged(QModelIndex()); +} + +int TextureCardDelegate::textBlockHeight() const +{ + // Name, a gap, then "modified · resolution". The gap is load-bearing: with + // the two lines flush, a name ending in an underscore paints its glyph onto + // the metadata line below. + const QFontMetrics metrics(ThemeManager::instance().theme().font("ui")); + return metrics.lineSpacing() * 2 + themeSpace("sm", 4) * 3; +} + +QSize TextureCardDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const +{ + return QSize(cardW, cardW + textBlockHeight()); +} + +void TextureCardDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + const bool selected = option.state & QStyle::State_Selected; + const bool hovered = option.state & QStyle::State_MouseOver; + const bool missing = index.data(TextureListModel::MissingRole).toBool(); + const bool starred = index.data(TextureListModel::StarredRole).toBool(); + const bool isOpen = index.data(TextureListModel::IsOpenRole).toBool(); + const bool needsMigration = index.data(TextureListModel::NeedsMigrationRole).toBool(); + + const int pad = themeSpace("sm", 4); + const int radius = themeRadius(); + + QRect card = option.rect.adjusted(pad, pad, -pad, -pad); + const int thumbSide = card.width(); + QRect thumb(card.left(), card.top(), thumbSide, thumbSide); + + // Card body. Selection is a border, never a fill — the material renders are + // supposed to be the only saturated thing on screen. + painter->setPen(Qt::NoPen); + painter->setBrush(hovered ? tc(Tokens::LauncherCardHover) : tc(Tokens::LauncherCard)); + painter->drawRoundedRect(card, radius, radius); + + // Thumbnail well. Drawn even when there's an image, so the rounded corners + // and the "no preview yet" state share one shape. + painter->setBrush(tc(Tokens::LauncherThumbBg)); + painter->drawRoundedRect(thumb, radius, radius); + + const QPixmap preview = index.data(TextureListModel::ThumbnailRole).value(); + if (!preview.isNull()) { + painter->save(); + + // Clip to the well so the capture inherits its rounded corners instead + // of painting square over them. + QPainterPath clip; + clip.addRoundedRect(thumb, radius, radius); + painter->setClipPath(clip); + + // The stored image is square and so is the well, but the card can be + // resized to anything — scale to cover and center rather than letting a + // rounding difference letterbox it. + const QPixmap scaled = preview.scaled(thumb.size(), Qt::KeepAspectRatioByExpanding, + Qt::SmoothTransformation); + painter->drawPixmap(thumb.center() - QPoint(scaled.width() / 2, scaled.height() / 2), + scaled); + painter->restore(); + } + + if (missing) + painter->setOpacity(0.4); + + // Channel pips along the bottom of the thumbnail. + const int channels = index.data(TextureListModel::ChannelsRole).toInt(); + if (channels != catalog::ChannelNone) { + const int pipSize = qMax(3, thumbSide / 32); + const int gap = pipSize; + const int totalWidth = kPipCount * pipSize + (kPipCount - 1) * gap; + int x = thumb.left() + (thumb.width() - totalWidth) / 2; + const int y = thumb.bottom() - pipSize - pad; + + painter->setPen(Qt::NoPen); + for (int i = 0; i < kPipCount; ++i) { + const bool on = (channels & kPipOrder[i]) != 0; + painter->setBrush(on ? tc(Tokens::LauncherPipOn) : tc(Tokens::LauncherPipOff, 120)); + painter->drawEllipse(QRect(x, y, pipSize, pipSize)); + x += pipSize + gap; + } + } + + painter->setOpacity(1.0); + + // Text block. + const QFont uiFont = ThemeManager::instance().theme().font("ui"); + + // Bounded by the card, not by textBlockHeight(): the latter is the sizeHint + // budget and overruns the card body by one pad, which clips the descenders + // of the meta line against the border. + QRect textRect(card.left() + pad, thumb.bottom() + pad, card.width() - pad * 2, + card.bottom() - thumb.bottom() - pad * 2); + + QFont nameFont = uiFont; + QFontMetrics nameMetrics(nameFont); + nameFont.setStrikeOut(missing); + painter->setFont(nameFont); + painter->setPen(missing ? tc(Tokens::TextDisabled) : tc(Tokens::TextPrimary)); + + const QString name = index.data(TextureListModel::NameRole).toString(); + // lineSpacing(), not height(): height() ends flush with the descent, and a + // name containing an underscore then paints its glyph right on the meta + // line below, which reads as a stray dash next to the metadata. + QRect nameRect(textRect.left(), textRect.top(), textRect.width(), nameMetrics.lineSpacing()); + // TextSingleLine: without it drawText() may lay the string out as wrapped + // rich-ish text in a rect this short, which leaves a clipped fragment of the + // overflow visible as a stray mark next to the line. + painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, + nameMetrics.elidedText(name, Qt::ElideMiddle, nameRect.width())); + + // Secondary line: "2h · 4K", or the missing marker, which matters more than + // either of them. + QFont metaFont = uiFont; + // The theme builds fonts with setPixelSize (thememanager.cpp), so + // pointSize() returns -1 on them. Subtracting from that silently yields a + // 6pt font — small enough that the renderer leaves a stray hinting artifact + // past the end of the string. Step down in whichever unit is actually set. + if (uiFont.pixelSize() > 0) + metaFont.setPixelSize(qMax(9, uiFont.pixelSize() - 1)); + else + metaFont.setPointSize(qMax(7, uiFont.pointSize() - 1)); + painter->setFont(metaFont); + painter->setPen(tc(Tokens::TextSecondary)); + + QString meta; + if (missing) { + meta = QStringLiteral("missing"); + } + else { + meta = launcherfmt::relativeTime(index.data(TextureListModel::ModifiedRole).toLongLong()); + const QString resolution = launcherfmt::resolutionLabel( + index.data(TextureListModel::WidthRole).toInt(), + index.data(TextureListModel::HeightRole).toInt()); + if (!resolution.isEmpty()) + meta += QStringLiteral(" · ") + resolution; + } + + const QFontMetrics metaMetrics(metaFont); + QRect metaRect(textRect.left(), nameRect.bottom() + themeSpace("sm", 4) / 2, textRect.width(), + metaMetrics.height()); + painter->drawText(metaRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, + metaMetrics.elidedText(meta, Qt::ElideRight, metaRect.width())); + + + // Overlays on the thumbnail corners. + const int markSize = qMax(10, thumbSide / 10); + + if (starred) + launcherfmt::paintStar(painter, QRectF(thumb.right() - markSize - pad, thumb.top() + pad, markSize, + markSize), + tc(Tokens::LauncherStar)); + + if (isOpen) { + painter->setPen(Qt::NoPen); + painter->setBrush(tc(Tokens::LauncherOpenDot)); + painter->drawEllipse(QRect(thumb.left() + pad, thumb.top() + pad, markSize / 2, + markSize / 2)); + } + + if (needsMigration) { + // The version itself, not an anonymous marker: "v1" tells you which + // library wrote this and therefore what the upgrade prompt will offer. + const QString version = index.data(TextureListModel::LibVersionRole).toString(); + const QSize badge = launcherfmt::versionBadgeSize(version); + if (!badge.isEmpty()) { + launcherfmt::paintVersionBadge( + painter, + QRect(QPoint(thumb.left() + pad, thumb.bottom() - badge.height() - pad), badge), + version); + } + } + + // Borders last, so nothing paints over them. + painter->setBrush(Qt::NoBrush); + painter->setPen(QPen(selected ? tc(Tokens::Accent) : tc(Tokens::LauncherCardBorder), 1)); + painter->drawRoundedRect(card.adjusted(0, 0, -1, -1), radius, radius); + + painter->restore(); +} + +bool TextureCardDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, + const QStyleOptionViewItem& option, const QModelIndex& index) +{ + if (!event || !view || !index.isValid()) + return QStyledItemDelegate::helpEvent(event, view, option, index); + + QToolTip::showText(event->globalPos(), + launcherfmt::tooltipFor(index, currentVersionLabel), view); + return true; +} diff --git a/src/texturelab/launcher/texturecarddelegate.h b/src/texturelab/launcher/texturecarddelegate.h new file mode 100644 index 00000000..8e6f1792 --- /dev/null +++ b/src/texturelab/launcher/texturecarddelegate.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +// Paints one texture card: square thumbnail, name, and a "modified · resolution" +// line, with channel pips along the bottom of the thumbnail. +// +// All colors come from the theme at paint time, so --dev-theme hot-reload works +// without a rebuild. No QColor literals here — see scripts/check-theme-hygiene.sh. +class TextureCardDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + explicit TextureCardDelegate(QObject* parent = nullptr); + + // Card width in pixels; the thumbnail is square, so height follows. Set by + // LauncherWindow::relayoutGrid(), which stretches the slider's target width + // to whatever divides the viewport evenly — not by the slider directly. + void setCardWidth(int width); + int cardWidth() const { return cardW; } + + // The card height a given width implies. The grid needs it to state its + // cell size, which it has to do before handing the width over here. + int heightForWidth(int width) const { return width + textBlockHeight(); } + + static constexpr int MinCardWidth = 96; + static constexpr int MaxCardWidth = 256; + + // Shown in the migration tooltip as the version a file would be upgraded + // to. Injected so the delegate keeps no dependency on the node library. + void setCurrentVersionLabel(const QString& label) { currentVersionLabel = label; } + + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + + // Builds the tooltip from roles rather than letting the model return a + // display string — the model stays formatting-free so a second view can + // present the same data differently. + bool helpEvent(QHelpEvent* event, QAbstractItemView* view, + const QStyleOptionViewItem& option, const QModelIndex& index) override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + +private: + int textBlockHeight() const; + + int cardW = 160; + QString currentVersionLabel; +}; diff --git a/src/texturelab/launcher/texturelistmodel.cpp b/src/texturelab/launcher/texturelistmodel.cpp new file mode 100644 index 00000000..141a833d --- /dev/null +++ b/src/texturelab/launcher/texturelistmodel.cpp @@ -0,0 +1,255 @@ +#include "texturelistmodel.h" + +#include "catalogindex.h" +#include "thumbnailcache.h" + +#include + +TextureListModel::TextureListModel(QObject* parent) : QAbstractListModel(parent) +{ + query.filter = catalog::Filter::All; + query.sort = catalog::SortKey::Modified; + query.ascending = false; + query.limit = PageSize; + query.offset = 0; +} + +void TextureListModel::setIndex(catalog::CatalogIndex* index) +{ + catalogIndex = index; + reload(); +} + +void TextureListModel::setThumbnailCache(catalog::ThumbnailCache* cache) +{ + thumbnails = cache; + pixmaps.clear(); + + if (!rows.isEmpty()) + emit dataChanged(index(0), index(rows.size() - 1), {ThumbnailRole}); +} + +void TextureListModel::reload() +{ + beginResetModel(); + rows.clear(); + // Dropped wholesale rather than selectively: a reload follows a save or a + // reconcile, either of which may have replaced any card's image. + pixmaps.clear(); + total = 0; + + if (catalogIndex && catalogIndex->isOpen()) { + catalog::Query page = query; + page.limit = PageSize; + page.offset = 0; + rows = catalogIndex->list(page); + total = catalogIndex->count(query); + } + + endResetModel(); +} + +void TextureListModel::refresh() +{ + // Deliberately a full reset rather than a diff. The alternative is + // reconciling two ordered sets to emit precise row moves, which is a real + // source of off-by-one crashes, and the launcher only refreshes on discrete + // user actions — never mid-scroll. + reload(); +} + +int TextureListModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return rows.size(); +} + +bool TextureListModel::canFetchMore(const QModelIndex& parent) const +{ + if (parent.isValid()) + return false; + return rows.size() < total; +} + +void TextureListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid() || !catalogIndex || !catalogIndex->isOpen()) + return; + + catalog::Query page = query; + page.limit = PageSize; + page.offset = rows.size(); + + const QVector more = catalogIndex->list(page); + if (more.isEmpty()) { + // The table shrank under us (rows removed elsewhere). Trust what we + // actually have rather than looping on a stale total. + total = rows.size(); + return; + } + + beginInsertRows(QModelIndex(), rows.size(), rows.size() + more.size() - 1); + rows += more; + endInsertRows(); +} + +void TextureListModel::setFilter(catalog::Filter filter) +{ + if (query.filter == filter) + return; + query.filter = filter; + reload(); +} + +void TextureListModel::setSort(catalog::SortKey key, bool ascending) +{ + if (query.sort == key && query.ascending == ascending) + return; + query.sort = key; + query.ascending = ascending; + reload(); +} + +void TextureListModel::setSearchTerm(const QString& term) +{ + if (query.search == term) + return; + query.search = term; + reload(); +} + +void TextureListModel::setOpenPath(const QString& path) +{ + if (openPath == path) + return; + + openPath = path; + if (!rows.isEmpty()) + emit dataChanged(index(0), index(rows.size() - 1), {IsOpenRole}); +} + +void TextureListModel::setCurrentLibVersion(const QString& version) +{ + if (currentVersion == version) + return; + + currentVersion = version; + if (!rows.isEmpty()) + emit dataChanged(index(0), index(rows.size() - 1), {NeedsMigrationRole}); +} + +catalog::TextureRecord TextureListModel::recordAt(const QModelIndex& index) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= rows.size()) + return catalog::TextureRecord(); + return rows.at(index.row()); +} + +QModelIndex TextureListModel::indexForId(qint64 id) const +{ + for (int row = 0; row < rows.size(); ++row) { + if (rows.at(row).id == id) + return index(row); + } + return QModelIndex(); +} + +QVariant TextureListModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= rows.size()) + return QVariant(); + + const catalog::TextureRecord& rec = rows.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + case NameRole: + return rec.name; + case Qt::ToolTipRole: + // With no folder tree, the tooltip is where location lives. + return rec.path; + case IdRole: + return rec.id; + case PathRole: + return rec.path; + case ModifiedRole: + return rec.fileMtime; + case OpenedRole: + return rec.lastOpened; + case SavedRole: + return rec.lastSaved; + case FileSizeRole: + return rec.fileSize; + case WidthRole: + return rec.width; + case HeightRole: + return rec.height; + case NodeCountRole: + return rec.nodeCount; + case ChannelsRole: + return rec.channels; + case LibVersionRole: + return rec.libVersion; + case StarredRole: + return rec.starred; + case MissingRole: + return rec.isMissing(); + case NeedsMigrationRole: + // Seeded rows have no version recorded yet — don't badge those as + // outdated when we simply haven't looked inside the file. Same when the + // caller never told us what "current" is. + if (rec.libVersion.isEmpty() || currentVersion.isEmpty()) + return false; + return rec.libVersion.compare(currentVersion, Qt::CaseInsensitive) != 0; + case IsOpenRole: + return !openPath.isEmpty() && rec.path == openPath; + case ThumbnailRole: { + auto cached = pixmaps.constFind(rec.id); + if (cached != pixmaps.constEnd()) + return *cached; + + QPixmap pixmap; + if (thumbnails && thumbnails->isOpen()) { + catalog::ThumbKey key; + key.textureId = rec.id; + key.size = 256; + const QByteArray bytes = thumbnails->get(key); + if (!bytes.isEmpty()) + pixmap.loadFromData(bytes, "JPG"); + } + + // A null pixmap is cached too — it means "asked and there wasn't one", + // and without it every repaint re-queries the database for a miss. + pixmaps.insert(rec.id, pixmap); + return pixmap; + } + case RecordRole: + return QVariant::fromValue(rec); + default: + return QVariant(); + } +} + +QHash TextureListModel::roleNames() const +{ + QHash names = QAbstractListModel::roleNames(); + names[IdRole] = "textureId"; + names[NameRole] = "name"; + names[PathRole] = "path"; + names[ModifiedRole] = "modified"; + names[OpenedRole] = "opened"; + names[SavedRole] = "saved"; + names[FileSizeRole] = "fileSize"; + names[WidthRole] = "width"; + names[HeightRole] = "height"; + names[NodeCountRole] = "nodeCount"; + names[ChannelsRole] = "channels"; + names[LibVersionRole] = "libVersion"; + names[StarredRole] = "starred"; + names[MissingRole] = "missing"; + names[NeedsMigrationRole] = "needsMigration"; + names[IsOpenRole] = "isOpen"; + names[ThumbnailRole] = "thumbnail"; + return names; +} diff --git a/src/texturelab/launcher/texturelistmodel.h b/src/texturelab/launcher/texturelistmodel.h new file mode 100644 index 00000000..1d6b97c2 --- /dev/null +++ b/src/texturelab/launcher/texturelistmodel.h @@ -0,0 +1,116 @@ +#pragma once + +#include "texturerecord.h" + +#include +#include +#include +#include + +namespace catalog { +class CatalogIndex; +class ThumbnailCache; +} + +// The launcher's model over the catalog index. +// +// View-agnostic on purpose: it exposes typed roles and no formatting. The grid +// delegate and the eventual list delegate both read the same roles, and neither +// the "2h ago" string nor the "4K" string is built here — a model that formats +// is a model that can only feed one view. +// +// Rows are paged in with canFetchMore()/fetchMore() rather than loaded whole, so +// first paint costs one small query no matter how much history has accumulated. +class TextureListModel : public QAbstractListModel { + Q_OBJECT + +public: + enum Roles { + IdRole = Qt::UserRole + 1, + NameRole, + PathRole, + ModifiedRole, // qint64 ms — file mtime + OpenedRole, // qint64 ms, 0 if never + SavedRole, // qint64 ms, 0 if never + FileSizeRole, // qint64 bytes + WidthRole, + HeightRole, + NodeCountRole, + ChannelsRole, // catalog::ChannelBit mask + LibVersionRole, + StarredRole, + MissingRole, + NeedsMigrationRole, + IsOpenRole, // currently loaded in the editor + ThumbnailRole, // QPixmap; null when nothing has been captured yet + RecordRole, // the whole catalog::TextureRecord + }; + + // Rows fetched per batch. Comfortably more than fills a 1280×820 window, so + // the first screen never waits on a second query. + static constexpr int PageSize = 200; + + explicit TextureListModel(QObject* parent = nullptr); + + // The index is borrowed, not owned, and must outlive the model. + void setIndex(catalog::CatalogIndex* index); + + // Optional. Without it every card paints a placeholder, which is the + // correct degraded state when the cache can't be opened. + void setThumbnailCache(catalog::ThumbnailCache* cache); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + + bool canFetchMore(const QModelIndex& parent) const override; + void fetchMore(const QModelIndex& parent) override; + + // Total matching rows, including any not yet fetched. The empty-state and + // result-count copy needs this, not rowCount(). + int totalCount() const { return total; } + + catalog::Filter filter() const { return query.filter; } + void setFilter(catalog::Filter filter); + + void setSort(catalog::SortKey key, bool ascending); + catalog::SortKey sortKey() const { return query.sort; } + bool sortAscending() const { return query.ascending; } + + QString searchTerm() const { return query.search; } + void setSearchTerm(const QString& term); + + // Marks which texture is loaded in the editor, for the "currently open" + // indicator. Exactly one, since the app is single-document. + void setOpenPath(const QString& path); + + // The library version rows are compared against for the migration badge. + // Injected rather than read from libraries/libversion.h so this model + // depends on nothing but Qt and the catalog, and can be tested headless. + void setCurrentLibVersion(const QString& version); + + catalog::TextureRecord recordAt(const QModelIndex& index) const; + QModelIndex indexForId(qint64 id) const; + +public slots: + // Re-runs the query from scratch, keeping the first page's worth of rows. + // Called whenever the catalog changes underneath us. + void refresh(); + +private: + void reload(); + + catalog::CatalogIndex* catalogIndex = nullptr; + catalog::Query query; + QVector rows; + + // Decoded pixmaps, keyed by texture id. JPEG decoding during scroll would + // be visible, and the same card is repainted constantly — on hover, on + // selection, on every scroll pixel. + mutable QHash pixmaps; + + catalog::ThumbnailCache* thumbnails = nullptr; + QString openPath; + QString currentVersion; + int total = 0; +}; diff --git a/src/texturelab/launcher/texturerowdelegate.cpp b/src/texturelab/launcher/texturerowdelegate.cpp new file mode 100644 index 00000000..f4a75857 --- /dev/null +++ b/src/texturelab/launcher/texturerowdelegate.cpp @@ -0,0 +1,173 @@ +#include "texturerowdelegate.h" + +#include "launcherformat.h" +#include "texturelistmodel.h" +#include "texturerecord.h" +#include "thememanager.h" +#include "tokens.h" + +#include +#include +#include +#include +#include + +namespace { + +QColor tc(const char* token) +{ + return ThemeManager::instance().theme().color(token); +} + +int themeSpace(const char* key, int fallback) +{ + const int s = ThemeManager::instance().theme().space(key); + return s > 0 ? s : fallback; +} + +// Fixed right-hand columns, widest first so they don't jitter as content +// changes. The name takes whatever is left. +constexpr int kModifiedWidth = 96; +constexpr int kOpenedWidth = 96; +constexpr int kResolutionWidth = 72; +constexpr int kNodesWidth = 64; + +} // namespace + +TextureRowDelegate::TextureRowDelegate(QObject* parent) : QStyledItemDelegate(parent) {} + +QSize TextureRowDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const +{ + const int pad = themeSpace("sm", 4); + return QSize(0, ThumbSize + pad * 2); +} + +void TextureRowDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const +{ + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + const bool selected = option.state & QStyle::State_Selected; + const bool hovered = option.state & QStyle::State_MouseOver; + const bool missing = index.data(TextureListModel::MissingRole).toBool(); + const bool starred = index.data(TextureListModel::StarredRole).toBool(); + const bool isOpen = index.data(TextureListModel::IsOpenRole).toBool(); + const bool needsMigration = index.data(TextureListModel::NeedsMigrationRole).toBool(); + + const int pad = themeSpace("sm", 4); + const int radius = ThemeManager::instance().theme().radius("sm"); + + const QRect row = option.rect; + + if (hovered || selected) { + painter->setPen(Qt::NoPen); + painter->setBrush(selected ? tc(Tokens::LauncherCard) : tc(Tokens::LauncherCardHover)); + painter->drawRect(row); + } + + // Thumbnail. + QRect thumb(row.left() + pad * 2, row.top() + pad, ThumbSize, ThumbSize); + painter->setPen(Qt::NoPen); + painter->setBrush(tc(Tokens::LauncherThumbBg)); + painter->drawRoundedRect(thumb, radius, radius); + + const QPixmap preview = index.data(TextureListModel::ThumbnailRole).value(); + if (!preview.isNull()) { + painter->save(); + QPainterPath clip; + clip.addRoundedRect(thumb, radius, radius); + painter->setClipPath(clip); + const QPixmap scaled = preview.scaled(thumb.size(), Qt::KeepAspectRatioByExpanding, + Qt::SmoothTransformation); + painter->drawPixmap(thumb.center() - QPoint(scaled.width() / 2, scaled.height() / 2), + scaled); + painter->restore(); + } + + const QFont uiFont = ThemeManager::instance().theme().font("ui"); + const QFontMetrics metrics(uiFont); + painter->setFont(uiFont); + + // Right-hand columns are laid out from the right edge inwards, so the name + // column absorbs the window width rather than the numbers drifting. + int right = row.right() - pad * 2; + + auto column = [&](int width, const QString& text) { + const QRect cell(right - width, row.top(), width, row.height()); + painter->drawText(cell, Qt::AlignRight | Qt::AlignVCenter | Qt::TextSingleLine, + metrics.elidedText(text, Qt::ElideRight, width - pad)); + right -= width; + }; + + painter->setPen(tc(Tokens::TextSecondary)); + + const int nodes = index.data(TextureListModel::NodeCountRole).toInt(); + column(kNodesWidth, nodes > 0 ? QStringLiteral("%1").arg(nodes) : QString()); + column(kResolutionWidth, + launcherfmt::resolutionLabel(index.data(TextureListModel::WidthRole).toInt(), + index.data(TextureListModel::HeightRole).toInt())); + column(kOpenedWidth, + launcherfmt::relativeTime(index.data(TextureListModel::OpenedRole).toLongLong())); + column(kModifiedWidth, + missing ? QObject::tr("missing") + : launcherfmt::relativeTime( + index.data(TextureListModel::ModifiedRole).toLongLong())); + + // Markers sit between the thumbnail and the name so the name column starts + // at a predictable place whether or not a row carries any. + int left = thumb.right() + pad * 2; + const int markSize = metrics.height() / 2; + + if (isOpen) { + painter->setPen(Qt::NoPen); + painter->setBrush(tc(Tokens::LauncherOpenDot)); + painter->drawEllipse(QRect(left, row.center().y() - markSize / 2, markSize, markSize)); + left += markSize + pad; + } + if (needsMigration) { + const QString version = index.data(TextureListModel::LibVersionRole).toString(); + const QSize badge = launcherfmt::versionBadgeSize(version); + if (!badge.isEmpty()) { + launcherfmt::paintVersionBadge( + painter, + QRect(QPoint(left, row.center().y() - badge.height() / 2), badge), version); + left += badge.width() + pad; + } + } + if (starred) { + launcherfmt::paintStar( + painter, + QRectF(left, row.center().y() - markSize / 2.0, markSize, markSize), + tc(Tokens::LauncherStar)); + left += markSize + pad; + } + + QFont nameFont = uiFont; + nameFont.setStrikeOut(missing); + painter->setFont(nameFont); + painter->setPen(missing ? tc(Tokens::TextDisabled) : tc(Tokens::TextPrimary)); + + const QRect nameRect(left, row.top(), qMax(0, right - left - pad), row.height()); + painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, + QFontMetrics(nameFont).elidedText( + index.data(TextureListModel::NameRole).toString(), Qt::ElideMiddle, + nameRect.width())); + + // Hairline separator, drawn last and only between rows. + painter->setPen(tc(Tokens::LauncherCardBorder)); + painter->drawLine(row.left(), row.bottom(), row.right(), row.bottom()); + + painter->restore(); +} + +bool TextureRowDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, + const QStyleOptionViewItem& option, const QModelIndex& index) +{ + if (!event || !view || !index.isValid()) + return QStyledItemDelegate::helpEvent(event, view, option, index); + + QToolTip::showText(event->globalPos(), + launcherfmt::tooltipFor(index, currentVersionLabel), view); + return true; +} diff --git a/src/texturelab/launcher/texturerowdelegate.h b/src/texturelab/launcher/texturerowdelegate.h new file mode 100644 index 00000000..76e0e85b --- /dev/null +++ b/src/texturelab/launcher/texturerowdelegate.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +// Paints one texture as a table-like row: small thumbnail, name, then aligned +// columns for modified / opened / resolution / nodes. +// +// Shares the model with TextureCardDelegate and reads exactly the same roles — +// which is the whole reason the model exposes typed values and formats nothing. +// Past a few hundred textures a list beats a grid for finding a known name, and +// Resolve ships both for the same reason. +class TextureRowDelegate : public QStyledItemDelegate { + Q_OBJECT + +public: + explicit TextureRowDelegate(QObject* parent = nullptr); + + void setCurrentVersionLabel(const QString& label) { currentVersionLabel = label; } + + static constexpr int ThumbSize = 40; + + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const override; + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; + + bool helpEvent(QHelpEvent* event, QAbstractItemView* view, + const QStyleOptionViewItem& option, const QModelIndex& index) override; + +private: + QString currentVersionLabel; +}; diff --git a/src/texturelab/libraries/library.cpp b/src/texturelab/libraries/library.cpp new file mode 100644 index 00000000..fae41f48 --- /dev/null +++ b/src/texturelab/libraries/library.cpp @@ -0,0 +1,295 @@ +#include "library.h" +#include "../models.h" +#include "libv1.h" +#include "libv2.h" +#include "libv3.h" + +#include + +TextureNodePtr Library::createNode(QString name) +{ + if (this->items.contains(name)) { + auto& item = items[name]; + if (item.name == name) { + auto node = item.factoryFunction(); + node->typeName = name; + + // todo: put this in the appropriate place + node->init(); + + return node; + } + } + + return TextureNodePtr(nullptr); +} + +void Library::addNode(QString name, QString displayName, QString iconPath, + std::function factoryFunction) +{ + + LibraryEntry entry; + entry.name = name; + entry.displayName = displayName; + entry.icon = QIcon(iconPath); + entry.factoryFunction = factoryFunction; + + items[name] = entry; +} + +bool Library::hasNode(QString name) { return items.contains(name); } + +LibraryV1::LibraryV1() : Library() +{ + // add items +} + +Library* createLibraryV2() +{ + auto lib = new Library(); + lib->addNode("polygon", "Polygon", ":nodes/bevel.png"); + lib->addNode("circle", "Circle", ":nodes/circle.png"); + lib->addNode("color", "Color", ":nodes/color.png"); + lib->addNode("colorize", "Colorize", ":nodes/colorize.png"); + lib->addNode("blend", "Blend", ":nodes/blend.png"); + lib->addNode("output", "Output", ":nodes/output.png"); + lib->addNode("normalmap", "Normal Map", + ":nodes/normalmap.png"); + lib->addNode("brickgenerator", "Brick Generator", + ":nodes/brickgenerator.png"); + + lib->addNode("brightnesscontrast", + "Brightness Contrast", + ":nodes/brightnesscontrast.png"); + lib->addNode("cell", "Cell", ":nodes/cell.png"); + lib->addNode("checkerboard", "Checkerboard", + ":nodes/checkerboard.png"); + lib->addNode("copy", "Copy", ":nodes/copy.png"); + lib->addNode("directionalwarp", "Directional Warp", + ":nodes/directionalwarp.png"); + lib->addNode("fractalnoise", "Fractal Noise", + ":nodes/fractalnoise.png"); + + lib->addNode("gradient", "Gradient", ":nodes/gradient.png"); + lib->addNode("gradientmap", "Gradient Map", + ":nodes/gradientmap.png"); + lib->addNode("heightshift", "Height Shift", + ":nodes/heightshift.png"); + lib->addNode("hexagon", "Hexagon", ":nodes/hexagon.png"); + lib->addNode("invert", "Invert", ":nodes/invert.png"); + lib->addNode("linecell", "Line Cell", ":nodes/linecell.png"); + lib->addNode("maprange", "Map Range", ":nodes/maprange.png"); + lib->addNode("mask", "Mask", ":nodes/mask.png"); + lib->addNode("mirror", "Mirror", ":nodes/mirror.png"); + lib->addNode("perlin3d", "Perlin 3D", ":nodes/perlin3d.png"); + lib->addNode("solidcell", "Solid Cell", + ":nodes/solidcell.png"); + lib->addNode("splat", "Splat", ":nodes/splat.png"); + lib->addNode("threshold", "Threshold", + ":nodes/threshold.png"); + lib->addNode("tile", "Tile", ":nodes/tile.png"); + lib->addNode("transform2d", "Transform2D", + ":nodes/transform2d.png"); + lib->addNode("warp", "Warp", ":nodes/warp.png"); + lib->addNode("wave", "Wave", ":nodes/wave.png"); + + // V2 NODES START HERE + lib->addNode("advancesplatter", "Advance Splatter", + ":nodes/advancesplatter.png"); + lib->addNode("anisotropicblur", "Anisotropic Blur", + ":nodes/anisotropicblur.png"); + lib->addNode("bevel", "Bevel", ":nodes/bevel.png"); + lib->addNode("blurv2", "Blur", ":nodes/blurv2.png"); + lib->addNode("capsule", "Capsule", ":nodes/capsule.png"); + lib->addNode("cartesiantopolar", "Cartesian To Polar", + ":nodes/cartesiantopolar.png"); + lib->addNode("circularsplatter", "Circular Splatter", + ":nodes/circularsplatter.png"); + lib->addNode("clamp", "Clamp", ":nodes/clamp.png"); + lib->addNode("combinenormals", "Combine Normals", + ":nodes/combinenormals.png"); + lib->addNode("directionalblur", "Directional Blur", + ":nodes/directionalblur.png"); + lib->addNode("directionalwarp", "Directional Warp", + ":nodes/directionalwarp.png"); + lib->addNode("extractchannel", "Extract Channel", + ":nodes/extractchannel.png"); + lib->addNode("floodfill", "Flood Fill", + ":nodes/floodfill.png"); + lib->addNode("floodfillsampler", "FloodFill Sampler", + ":nodes/floodfillsampler.png"); + lib->addNode("floodfilltobbox", "FloodFill to BBox", + ":nodes/floodfilltobbox.png"); + lib->addNode("floodfilltocolor", "FloodFill to Color", + ":nodes/floodfilltocolor.png"); + lib->addNode("floodfilltogradient", + "FloodFill to Gradient", + ":nodes/floodfilltogradient.png"); + lib->addNode( + "floodfilltorandomcolor", "FloodFill to Random Color", + ":nodes/floodfilltorandomcolor.png"); + lib->addNode( + "floodfilltorandomintensity", "FloodFill to Random Intensity", + ":nodes/floodfilltorandomintensity.png"); + lib->addNode("gradientdynamic", "Gradient Dynamic", + ":nodes/gradientdynamic.png"); + lib->addNode("gradientnoise", "Gradient Noise", + ":nodes/gradientnoise.png"); + lib->addNode( + "gradientnoisefractalsum", "Gradient Noise Fractal Sum", + ":nodes/gradientnoisefractalsum.png"); + lib->addNode("grayscale", "Grayscale", + ":nodes/grayscale.png"); + lib->addNode("histogramscan", "Histogram Scan", + ":nodes/histogramscan.png"); + lib->addNode("histogramselect", "Histogram Select", + ":nodes/histogramselect.png"); + lib->addNode("histogramshift", "Histogram Shift", + ":nodes/histogramshift.png"); + lib->addNode("hsl", "HSL", ":nodes/hsl.png"); + lib->addNode("hslextract", "HSL Extract", + ":nodes/hslextract.png"); + lib->addNode("image", "Image", ":nodes/image.png"); + lib->addNode("invertnormal", "Invert Normal", + ":nodes/invertnormal.png"); + lib->addNode("normalmap", "Normal Map", + ":nodes/normalmap.png"); + lib->addNode("polartocartesian", "Polar to Cartesian", + ":nodes/polartocartesian.png"); + lib->addNode("pow", "Pow", ":nodes/pow.png"); + lib->addNode("quantize", "Quantize", ":nodes/quantize.png"); + lib->addNode("rgbamerge", "RGBA Merge", + ":nodes/rgbamerge.png"); + lib->addNode("rgbashuffle", "RGBA Shuffle", + ":nodes/rgbashuffle.png"); + lib->addNode("simplexnoise", "Simplex Noise", + ":nodes/simplexnoise.png"); + lib->addNode("skew", "Skew", ":nodes/skew.png"); + lib->addNode("slopeblur", "Slope Blur", + ":nodes/slopeblur.png"); + lib->addNode("softflower", "Soft Flower", + ":nodes/softflower.png"); + lib->addNode("splat", "Splat", ":nodes/splat.png"); + lib->addNode("star", "Star", ":nodes/star.png"); + lib->addNode("stripes", "Stripes", ":nodes/stripes.png"); + lib->addNode("tilesampler", "Tile Sampler", + ":nodes/tilesampler.png"); + lib->addNode("transform2d", "Transform2D", + ":nodes/transform2d.png"); + lib->addNode("valuenoise", "Value Noise", + ":nodes/valuenoise.png"); + lib->addNode("valuenoisefractalsum", + "Value Noise Fractal Sum", + ":nodes/valuenoisefractalsum.png"); + lib->addNode("warp", "Warp", ":nodes/warp.png"); + + return lib; +} + +Library* createLibraryV3() +{ + auto lib = createLibraryV2(); + + // Remove V1/V2 nodes that are superseded by V3 equivalents + lib->items.remove("floodfill"); + lib->items.remove("floodfillsampler"); + lib->items.remove("floodfilltobbox"); + lib->items.remove("floodfilltocolor"); + lib->items.remove("floodfilltogradient"); + lib->items.remove("floodfilltorandomcolor"); + lib->items.remove("floodfilltorandomintensity"); + lib->items.remove("bevel"); + lib->items.remove("perlin3d"); + lib->items.remove("blend"); + lib->items.remove("cell"); + lib->items.remove("linecell"); + lib->items.remove("solidcell"); + + // V3 NODES + lib->addNode("bevelv2", "Bevel V2", ":nodes/bevel.png"); + lib->addNode("ambientocclusion", "Ambient Occlusion", + ":nodes/bevel.png"); + lib->addNode("curvature", "Curvature", ":nodes/bevel.png"); + lib->addNode("maskedblur", "Masked Blur", + ":nodes/blurv2.png"); + lib->addNode("rays", "Rays", ":nodes/bevel.png"); + lib->addNode("swirl", "Swirl", ":nodes/bevel.png"); + lib->addNode("floodfillv2", "Flood Fill V2", + ":nodes/floodfill.png"); + lib->addNode("floodfillv2tocolor", "FF To Color V2", + ":nodes/floodfilltocolor.png"); + lib->addNode( + "floodfillv2torandomcolor", "FF To Random Color V2", + ":nodes/floodfilltorandomcolor.png"); + lib->addNode( + "floodfillv2torandomintensity", "FF To Random Intensity V2", + ":nodes/floodfilltorandomintensity.png"); + lib->addNode("floodfillv2tobbox", "FF To BBox V2", + ":nodes/floodfilltobbox.png"); + lib->addNode("floodfillv2togradient", + "FF To Gradient V2", + ":nodes/floodfilltogradient.png"); + lib->addNode("floodfillv2sampler", "FF Sampler V2", + ":nodes/floodfillsampler.png"); + + lib->addNode("curve", "Curve", ":nodes/curve.png"); + + // ----------------------------------------------------------------------- + // Phase 1 — Filters / Color + // ----------------------------------------------------------------------- + lib->addNode("blend", "Blend", ":nodes/blend.png"); + lib->addNode("edgedetect", "Edge Detect", + ":nodes/bevel.png"); + lib->addNode("highpass", "Highpass", ":nodes/blurv2.png"); + lib->addNode("emboss", "Emboss", ":nodes/normalmap.png"); + lib->addNode("vibrance", "Vibrance", ":nodes/hsl.png"); + lib->addNode("colortomask", "Color To Mask", + ":nodes/extractchannel.png"); + lib->addNode("setalpha", "Set Alpha", ":nodes/rgbamerge.png"); + lib->addNode("toongradient", "Toon Gradient", + ":nodes/gradientmap.png"); + lib->addNode("autolevels", "Auto Levels", + ":nodes/histogramscan.png"); + + // ----------------------------------------------------------------------- + // Phase 2 — Generators + // ----------------------------------------------------------------------- + lib->addNode("bricks2", "Bricks 2", + ":nodes/brickgenerator.png"); + lib->addNode( + "directionalscratches", "Directional Scratches", ":nodes/cell.png"); + lib->addNode("roughgrain", "Rough Grain", + ":nodes/cell.png"); + lib->addNode("voronoifractal", "Voronoi Fractal", + ":nodes/cell.png"); + lib->addNode("truchet", "Truchet", ":nodes/hexagon.png"); + lib->addNode("fbmdomainwarp", "FBM Domain Warp", + ":nodes/fractalnoise.png"); + lib->addNode("perlinnoise", "Perlin Noise", + ":nodes/fractalnoise.png"); + lib->addNode("perlinnoise3d", "Perlin Noise 3D", + ":nodes/fractalnoise.png"); + lib->addNode("cell", "Cell V3", ":nodes/cell.png"); + lib->addNode("linecell", "Line Cell V3", + ":nodes/linecell.png"); + lib->addNode("solidcell", "Solid Cell V3", + ":nodes/solidcell.png"); + + // ----------------------------------------------------------------------- + // Phase 3 — Multi-pass + // ----------------------------------------------------------------------- + lib->addNode("blurhq", "Blur HQ", ":nodes/blurv2.png"); + lib->addNode( + "distancetransform", "Distance Transform", ":nodes/bevel.png"); + lib->addNode("spread", "Spread", ":nodes/bevel.png"); + lib->addNode("fastao", "Fast AO", ":nodes/bevel.png"); + lib->addNode("heightblend", "Height Blend", + ":nodes/blend.png"); + lib->addNode("atlas2x2", "Atlas 2x2", ":nodes/tile.png"); + // lib->addNode("makeittile", "Make It Tile", + // ":nodes/tile.png"); + // lib->addNode("normalmapv3", "Normal Map V3", + // ":nodes/normalmap.png"); + + return lib; +} \ No newline at end of file diff --git a/src/texturelab/libraries/library.h b/src/texturelab/libraries/library.h new file mode 100644 index 00000000..aedefa65 --- /dev/null +++ b/src/texturelab/libraries/library.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include + +class TextureNode; +typedef QSharedPointer TextureNodePtr; + +class LibraryEntry +{ +public: + QString name; + QString displayName; + QIcon icon; + std::function factoryFunction; +}; + +class Library +{ +public: + QMap items; + virtual TextureNodePtr createNode(QString name); + + void addNode(QString name, QString displayName, QString iconPath, std::function factoryFunction); + bool hasNode(QString name); + + template + void addNode(QString name, + QString displayName, + QString iconPath) + { + // https://stackoverflow.com/a/22934960 + static_assert(std::is_base_of::value, "type parameter of this class must derive from TextureNode"); + + this->addNode(name, displayName, iconPath, [=]() + { return TextureNodePtr(new T); }); + } +}; + +Library *createLibraryV2(); +Library *createLibraryV3(); + +class LibraryV1 : public Library +{ +public: + LibraryV1(); +}; \ No newline at end of file diff --git a/src/texturelab/libraries/libraryversionmigrator.cpp b/src/texturelab/libraries/libraryversionmigrator.cpp new file mode 100644 index 00000000..e9929786 --- /dev/null +++ b/src/texturelab/libraries/libraryversionmigrator.cpp @@ -0,0 +1,158 @@ +#include "libraryversionmigrator.h" + +namespace { + +// v2 -> v3. The only non-empty step table that exists today. Every other +// v2 typeName resolves to the same class in v3 and needs no entry here — +// only the typeNames that were actually removed, renamed, or silently +// swapped for a different implementation are listed. +// +// No propertyKeyRenames are needed for any of these: the existing +// node-load loop (Project::loadTextureFromJson) already applies JSON +// properties by key and skips anything the target node doesn't have, +// which reproduces "copy matching props, drop the rest, default the new +// ones" without any extra code here. +const QVector& v2ToV3Table() +{ + static const QVector table = { + {"floodfill", "floodfillv2", {}}, + {"floodfillsampler", "floodfillv2sampler", {}}, + {"floodfilltobbox", "floodfillv2tobbox", {}}, + {"floodfilltocolor", "floodfillv2tocolor", {}}, + {"floodfilltogradient", "floodfillv2togradient", {}}, + {"floodfilltorandomcolor", "floodfillv2torandomcolor", {}}, + {"floodfilltorandomintensity", "floodfillv2torandomintensity", {}}, + // bevelv2 added "invert" and "scaleInvariant" toggles that don't + // exist on the old bevel node. Their library defaults (false, + // true) are tuned for new nodes; a migrated node needs the + // opposite of both to reproduce the old node's look: old bevel's + // output polarity was flipped relative to bevelv2's, and old + // bevel always worked in raw pixel distance (not normalized + // against texture resolution). + {"bevel", "bevelv2", {}, QJsonObject{ + {"invert", true}, + {"scaleInvariant", false}, + }}, + {"perlin3d", "perlinnoise3d", {}}, + {"blend", "blend", {}}, // same name, new class (BlendV3Node) + {"cell", "cell", {}}, // same name, new class (CellV3Node) + {"linecell", "linecell", {}}, // same name, new class (LineCellV3Node) + {"solidcell", "solidcell", {}}, // same name, new class (SolidCellV3Node) + }; + return table; +} + +} // namespace + +QVector migrationStepTable(LibVersion from) +{ + switch (from) { + case LibVersion::V1: + return {}; // v1 -> v2 is purely additive, nothing to migrate + case LibVersion::V2: + return v2ToV3Table(); + case LibVersion::V3: + return {}; // current version: no further step + } + return {}; +} + +LibraryVersionMigrator::LibraryVersionMigrator(QJsonObject projectJson) + : _json(std::move(projectJson)), _target(currentLibVersion()) +{ + auto versionStr = _json["libraryVersion"].toString(); + if (!versionStr.isEmpty()) + _source = libVersionFromString(versionStr); + else + _source = looksLegacy(_json) ? LibVersion::V2 : currentLibVersion(); +} + +bool LibraryVersionMigrator::needsMigration() const +{ + return _source != _target; +} + +QList LibraryVersionMigrator::versionsCrossed() const +{ + QList chain; + for (LibVersion v = _source; v != _target; v = nextLibVersion(v)) + chain.append(nextLibVersion(v)); + return chain; +} + +QJsonObject LibraryVersionMigrator::migrate() const +{ + QJsonObject result = _json; + QJsonArray nodes = result["nodes"].toArray(); + + for (LibVersion v = _source; v != _target; v = nextLibVersion(v)) + nodes = applyStep(nodes, migrationStepTable(v)); + + result["nodes"] = nodes; + result["libraryVersion"] = libVersionToString(_target); + return result; +} + +QJsonArray LibraryVersionMigrator::applyStep( + const QJsonArray& nodes, const QVector& table) +{ + if (table.isEmpty()) + return nodes; + + QJsonArray result; + for (const auto& item : nodes) { + auto nodeObj = item.toObject(); + auto typeName = nodeObj["typeName"].toString(); + + for (const auto& entry : table) { + if (entry.oldTypeName != typeName) + continue; + + nodeObj["typeName"] = entry.newTypeName; + + if (!entry.propertyKeyRenames.isEmpty() || + !entry.migratedPropertyDefaults.isEmpty()) { + auto props = nodeObj["properties"].toObject(); + + for (auto it = entry.propertyKeyRenames.begin(); + it != entry.propertyKeyRenames.end(); ++it) { + if (props.contains(it.key())) { + props[it.value()] = props[it.key()]; + props.remove(it.key()); + } + } + + for (auto it = entry.migratedPropertyDefaults.begin(); + it != entry.migratedPropertyDefaults.end(); ++it) { + props[it.key()] = it.value(); + } + + nodeObj["properties"] = props; + } + break; + } + + result.append(nodeObj); + } + return result; +} + +bool LibraryVersionMigrator::looksLegacy(const QJsonObject& json) +{ + auto nodes = json["nodes"].toArray(); + for (const auto& item : nodes) { + auto typeName = item.toObject()["typeName"].toString(); + for (const auto& entry : v2ToV3Table()) { + // Entries where oldTypeName == newTypeName (blend, cell, + // linecell, solidcell) only changed which class backs that + // name — the name itself is just as valid in a current-version + // file, so it can't be used as a legacy signal. Only count + // typeNames that were actually removed/renamed. + if (entry.oldTypeName == entry.newTypeName) + continue; + if (entry.oldTypeName == typeName) + return true; + } + } + return false; +} diff --git a/src/texturelab/libraries/libraryversionmigrator.h b/src/texturelab/libraries/libraryversionmigrator.h new file mode 100644 index 00000000..4df3507e --- /dev/null +++ b/src/texturelab/libraries/libraryversionmigrator.h @@ -0,0 +1,70 @@ +#pragma once + +#include "libversion.h" +#include +#include +#include +#include +#include +#include + +// One node's identity/shape change for a single version step (from -> next). +struct NodeTypeMigration { + QString oldTypeName; + QString newTypeName; + + // Rare: a property key that was renamed on an otherwise-equivalent + // node. Old key -> new key. Empty for every step table that exists + // today (see libraryversionmigrator.cpp). + QMap propertyKeyRenames; + + // Values for properties that only exist on the *new* node, set to + // whatever reproduces the old node's look instead of the new node's + // normal default. E.g. bevelv2 added an "invert" toggle (new-node + // default: false) that must be `true` for a migrated bevel node to + // match the old bevel's polarity. Only applied to nodes going through + // this migration step — a freshly-added bevelv2 node still gets its + // ordinary library default. Key -> value (any QJsonValue). + QJsonObject migratedPropertyDefaults; +}; + +// Returns the migration table to apply when stepping from `from` to +// nextLibVersion(from). An empty vector means that step has no typeName +// changes at all (e.g. v1 -> v2 today). +QVector migrationStepTable(LibVersion from); + +// Migrates a project's raw JSON from whatever library version it declares +// up to a target version (defaults to the current one), walking one +// version step at a time. Pure data transform: only QJsonObject / +// QJsonArray / QString are touched. No Library, TextureNode, Prop, or +// OpenGL/GPU resource is created anywhere in this class. +class LibraryVersionMigrator { +public: + explicit LibraryVersionMigrator(QJsonObject projectJson); + + LibVersion sourceVersion() const { return _source; } + LibVersion targetVersion() const { return _target; } + void setTargetVersion(LibVersion version) { _target = version; } + + bool needsMigration() const; + + // The chain of versions that migrate() will step into, e.g. [V2, V3] + // when migrating a V1 file up to V3. Empty if needsMigration() is + // false. Intended for building a human-readable "v1 -> v2 -> v3" + // message. + QList versionsCrossed() const; + + // Returns the migrated project JSON. Does not mutate the JSON passed + // to the constructor. Idempotent. Sets "libraryVersion" on the result + // to libVersionToString(targetVersion()). + QJsonObject migrate() const; + +private: + QJsonObject _json; + LibVersion _source; + LibVersion _target; + + static QJsonArray applyStep(const QJsonArray& nodes, + const QVector& table); + static bool looksLegacy(const QJsonObject& json); +}; diff --git a/src/texturelab/libraries/libv1.h b/src/texturelab/libraries/libv1.h new file mode 100644 index 00000000..477d84af --- /dev/null +++ b/src/texturelab/libraries/libv1.h @@ -0,0 +1,168 @@ +#pragma once + +#include "../models.h" + +class BlendNode : public TextureNode { +public: + virtual void init() override; +}; + +class BrickGeneratorNode : public TextureNode { +public: + virtual void init() override; +}; + +class BrightnessContrastNode : public TextureNode { +public: + virtual void init() override; +}; + +class CellNode : public TextureNode { +public: + virtual void init() override; +}; + +class CheckerboardNode : public TextureNode { +public: + virtual void init() override; +}; + +class CircleNode : public TextureNode { +public: + virtual void init() override; +}; + +class ColorNode : public TextureNode { +public: + virtual void init() override; +}; + +class ColorizeNode : public TextureNode { +public: + virtual void init() override; +}; + +class CopyNode : public TextureNode { +public: + virtual void init() override; +}; + +class DirectionalWarpNode : public TextureNode { +public: + virtual void init() override; +}; + +class FractalNoiseNode : public TextureNode { +public: + virtual void init() override; +}; + +class GradientNode : public TextureNode { +public: + virtual void init() override; +}; + +class GradientMapNode : public TextureNode { +public: + virtual void init() override; +}; + +class HeightShiftNode : public TextureNode { +public: + virtual void init() override; +}; + +class HexagonNode : public TextureNode { +public: + virtual void init() override; +}; + +class InvertNode : public TextureNode { +public: + virtual void init() override; +}; + +class LineCellNode : public TextureNode { +public: + virtual void init() override; +}; + +class MapRangeNode : public TextureNode { +public: + virtual void init() override; +}; + +class MaskNode : public TextureNode { +public: + virtual void init() override; +}; + +class MirrorNode : public TextureNode { +public: + virtual void init() override; +}; + +class NormalMapNode : public TextureNode { +public: + virtual void init() override; +}; + +class OutputNode : public TextureNode { +public: + virtual void init() override; +}; + +class PolygonNode : public TextureNode { +public: + virtual void init() override; +}; + +class Perlin3DNode : public TextureNode { +public: + virtual void init() override; +}; + +class ShapesNode : public TextureNode { +public: + virtual void init() override; +}; + +class SimplexNoiseNode : public TextureNode { +public: + virtual void init() override; +}; + +class SolidCellNode : public TextureNode { +public: + virtual void init() override; +}; + +class SplatNode : public TextureNode { +public: + virtual void init() override; +}; + +class ThresholdNode : public TextureNode { +public: + virtual void init() override; +}; + +class TileNode : public TextureNode { +public: + virtual void init() override; +}; + +class Transform2DNode : public TextureNode { +public: + virtual void init() override; +}; + +class WarpNode : public TextureNode { +public: + virtual void init() override; +}; + +class WaveNode : public TextureNode { +public: + virtual void init() override; +}; \ No newline at end of file diff --git a/src/texturelab/libraries/libv2.h b/src/texturelab/libraries/libv2.h new file mode 100644 index 00000000..f4fc682b --- /dev/null +++ b/src/texturelab/libraries/libv2.h @@ -0,0 +1,246 @@ +#pragma once + +#include "../models.h" + +class AdvanceSplatterNode : public TextureNode { +public: + virtual void init() override; +}; + +class AnisotropicBlurNode : public TextureNode { +public: + virtual void init() override; +}; + +class BevelNode : public TextureNode { +public: + virtual void init() override; + void cpuProcess(void* gl, const RenderCommand& command) override; +}; + +class BlurNodeV2 : public TextureNode { +public: + virtual void init() override; +}; + +class CapsuleNode : public TextureNode { +public: + virtual void init() override; +}; + +class CartesianToPolarNode : public TextureNode { +public: + virtual void init() override; +}; + +class CircularSplatterNode : public TextureNode { +public: + virtual void init() override; +}; + +class ClampNode : public TextureNode { +public: + virtual void init() override; +}; + +class CombineNormalsNode : public TextureNode { +public: + virtual void init() override; +}; + +class DirectionalBlurNode : public TextureNode { +public: + virtual void init() override; +}; + +class DirectionalWarpV2Node : public TextureNode { +public: + virtual void init() override; +}; + +class ExtractChannelNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillNode : public TextureNode { +public: + virtual void init() override; + void cpuProcess(void* gl, const RenderCommand& command) override; +}; + +class FloodFillSamplerNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillToBBoxNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillToColorNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillToGradientNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillToRandomColorNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillToRandomIntensityNode : public TextureNode { +public: + virtual void init() override; +}; + +class GradientDynamicNode : public TextureNode { +public: + virtual void init() override; +}; + +class GradientNoiseNode : public TextureNode { +public: + virtual void init() override; +}; + +class GradientNoiseFractalSumNode : public TextureNode { +public: + virtual void init() override; +}; + +class GrayscaleNode : public TextureNode { +public: + virtual void init() override; +}; + +class HistogramScanNode : public TextureNode { +public: + virtual void init() override; +}; + +class HistogramSelectNode : public TextureNode { +public: + virtual void init() override; +}; + +class HistogramShiftNode : public TextureNode { +public: + virtual void init() override; +}; +class HslNode : public TextureNode { +public: + virtual void init() override; +}; + +class HslExtractNode : public TextureNode { +public: + virtual void init() override; +}; + +class ImageNode : public TextureNode { + ImageProp* imageProp; + +public: + virtual void init() override; +}; + +class InvertNormalNode : public TextureNode { +public: + virtual void init() override; +}; + +class NormalMapV2Node : public TextureNode { +public: + virtual void init() override; +}; + +class PolarToCartesianNode : public TextureNode { +public: + virtual void init() override; +}; + +class PowNode : public TextureNode { +public: + virtual void init() override; +}; + +class QuantizeNode : public TextureNode { +public: + virtual void init() override; +}; + +class RgbaMergeNode : public TextureNode { +public: + virtual void init() override; +}; + +class RgbaShuffleNode : public TextureNode { +public: + virtual void init() override; +}; + +class SimplexNoiseV2Node : public TextureNode { +public: + virtual void init() override; +}; + +class SkewNode : public TextureNode { +public: + virtual void init() override; +}; + +class SlopeBlurNode : public TextureNode { +public: + virtual void init() override; +}; + +class SoftFlowerNode : public TextureNode { +public: + virtual void init() override; +}; + +class SplatV2Node : public TextureNode { +public: + virtual void init() override; +}; + +class StarNode : public TextureNode { +public: + virtual void init() override; +}; + +class StripesNode : public TextureNode { +public: + virtual void init() override; +}; + +class TileSamplerNode : public TextureNode { +public: + virtual void init() override; +}; + +class Transform2DV2Node : public TextureNode { +public: + virtual void init() override; +}; + +class ValueNoiseNode : public TextureNode { +public: + virtual void init() override; +}; + +class ValueNoiseFractalSumNode : public TextureNode { +public: + virtual void init() override; +}; + +class WarpNodeV2 : public TextureNode { +public: + virtual void init() override; +}; \ No newline at end of file diff --git a/src/texturelab/libraries/libv3.h b/src/texturelab/libraries/libv3.h new file mode 100644 index 00000000..238b585e --- /dev/null +++ b/src/texturelab/libraries/libv3.h @@ -0,0 +1,237 @@ +#pragma once + +#include "../models.h" +#include "v3/curvenode.h" +#include + +// --------------------------------------------------------------------------- +// Phase 1 — Single-pass filter / color nodes +// --------------------------------------------------------------------------- + +class EdgeDetectNode : public TextureNode { +public: + void init() override; +}; + +class HighpassNode : public TextureNode { +public: + void init() override; +}; + +class EmbossNode : public TextureNode { +public: + void init() override; +}; + +class VibranceNode : public TextureNode { +public: + void init() override; +}; + +class ColorToMaskNode : public TextureNode { +public: + void init() override; +}; + +class SetAlphaNode : public TextureNode { +public: + void init() override; +}; + +class ToonGradientNode : public TextureNode { +public: + void init() override; +}; + +class AutoLevelsNode : public TextureNode { +public: + void init() override; + std::shared_ptr createRenderer() override; + std::shared_ptr createRenderData() override; +}; + +// --------------------------------------------------------------------------- +// Phase 2 — Generator nodes +// --------------------------------------------------------------------------- + +class Bricks2Node : public TextureNode { +public: + void init() override; +}; + +class DirectionalScratchesNode : public TextureNode { +public: + void init() override; +}; + +class RoughGrainNode : public TextureNode { +public: + void init() override; +}; + +class VoronoiFractalNode : public TextureNode { +public: + void init() override; +}; + +class TruchetNode : public TextureNode { +public: + void init() override; +}; + +class FBMDomainWarpNode : public TextureNode { +public: + void init() override; +}; + +class PerlinNoiseNode : public TextureNode { +public: + void init() override; +}; + +class PerlinNoise3DNode : public TextureNode { +public: + void init() override; +}; + +// --------------------------------------------------------------------------- +// Phase 3 — Multi-pass nodes +// --------------------------------------------------------------------------- + +class BlurHQNode : public TextureNode { +public: + void init() override; + std::shared_ptr createRenderer() override; + std::shared_ptr createRenderData() override; +}; + +class DistanceTransformNode : public TextureNode { +public: + void init() override; + std::shared_ptr createRenderer() override; + std::shared_ptr createRenderData() override; +}; + +class ColorSpreadNode : public TextureNode { +public: + void init() override; + std::shared_ptr createRenderer() override; + std::shared_ptr createRenderData() override; +}; + +class HeightBlendNode : public TextureNode { +public: + void init() override; +}; + +class BlendV3Node : public TextureNode { +public: + void init() override; +}; + +class Atlas2x2Node : public TextureNode { +public: + void init() override; +}; + +class MakeItTileNode : public TextureNode { +public: + void init() override; +}; + +class NormalMapV3Node : public TextureNode { +public: + void init() override; +}; + +class BevelV2Node : public TextureNode { +public: + virtual void init() override; + std::shared_ptr createRenderer() override; + std::shared_ptr createRenderData() override; +}; + +class AmbientOcclusionNode : public TextureNode { +public: + virtual void init() override; +}; + +// High-quality multi-pass AO — see v3/fastao.cpp +class FastAONode : public TextureNode { +public: + void init() override; + std::shared_ptr createRenderer() override; + std::shared_ptr createRenderData() override; +}; + +class CurvatureNode : public TextureNode { +public: + virtual void init() override; +}; + +class MaskedBlurNode : public TextureNode { +public: + virtual void init() override; +}; + +class RaysNode : public TextureNode { +public: + virtual void init() override; +}; + +class SwirlNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillV2Node : public TextureNode { +public: + virtual void init() override; + std::shared_ptr createRenderer() override; + std::shared_ptr createRenderData() override; +}; + +class FloodFillV2ToColorNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillV2ToRandomColorNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillV2ToRandomIntensityNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillV2ToBBoxNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillV2ToGradientNode : public TextureNode { +public: + virtual void init() override; +}; + +class FloodFillV2SamplerNode : public TextureNode { +public: + virtual void init() override; +}; + +class CellV3Node : public TextureNode { +public: + void init() override; +}; + +class LineCellV3Node : public TextureNode { +public: + void init() override; +}; + +class SolidCellV3Node : public TextureNode { +public: + void init() override; +}; diff --git a/src/texturelab/libraries/libversion.cpp b/src/texturelab/libraries/libversion.cpp new file mode 100644 index 00000000..a67a2b0b --- /dev/null +++ b/src/texturelab/libraries/libversion.cpp @@ -0,0 +1,49 @@ +#include "libversion.h" +#include "library.h" + +LibVersion currentLibVersion() { return kCurrentLibVersion; } + +LibVersion libVersionFromString(const QString& str) +{ + auto s = str.trimmed().toLower(); + if (s == "v1") + return LibVersion::V1; + if (s == "v2") + return LibVersion::V2; + if (s == "v3") + return LibVersion::V3; + + return currentLibVersion(); +} + +QString libVersionToString(LibVersion version) +{ + switch (version) { + case LibVersion::V1: return "v1"; + case LibVersion::V2: return "v2"; + case LibVersion::V3: return "v3"; + } + return "v3"; +} + +bool hasNextLibVersion(LibVersion version) +{ + return version != currentLibVersion(); +} + +LibVersion nextLibVersion(LibVersion version) +{ + return static_cast(static_cast(version) + 1); +} + +Library* createLibraryForVersion(LibVersion version) +{ + switch (version) { + case LibVersion::V1: + case LibVersion::V2: + return createLibraryV2(); + case LibVersion::V3: + return createLibraryV3(); + } + return createLibraryV3(); +} diff --git a/src/texturelab/libraries/libversion.h b/src/texturelab/libraries/libversion.h new file mode 100644 index 00000000..7410a36e --- /dev/null +++ b/src/texturelab/libraries/libversion.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class Library; + +// Contiguous, ordered library versions. Adding a new version means: add the +// enum value here, move kCurrentLibVersion, extend createLibraryForVersion(), +// and add a migration step table in libraryversionmigrator.cpp. +enum class LibVersion { V1 = 0, V2 = 1, V3 = 2 }; + +constexpr LibVersion kCurrentLibVersion = LibVersion::V3; + +LibVersion currentLibVersion(); + +// Parses "v1"/"v2"/"v3" (case-insensitive). Unrecognized/empty strings fall +// back to currentLibVersion(). +LibVersion libVersionFromString(const QString& str); +QString libVersionToString(LibVersion version); + +bool hasNextLibVersion(LibVersion version); +LibVersion nextLibVersion(LibVersion version); + +// Returns the node library that should be used to load/edit a project at +// the given version. V1 and V2 currently share createLibraryV2() since the +// v1->v2 step never removed or renamed anything. +Library* createLibraryForVersion(LibVersion version); diff --git a/src/lib/library/v1/blend.ts b/src/texturelab/libraries/v1/blend.cpp similarity index 78% rename from src/lib/library/v1/blend.ts rename to src/texturelab/libraries/v1/blend.cpp index 97b3263f..4e429bfb 100644 --- a/src/lib/library/v1/blend.ts +++ b/src/texturelab/libraries/v1/blend.cpp @@ -1,29 +1,23 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -export class BlendNode extends GpuDesignerNode { - public init() { - this.title = "Blend"; +void BlendNode::init() +{ + this->title = "Blend"; - this.addInput("colorA"); // foreground - this.addInput("colorB"); // background - this.addInput("opacity"); + this->addInput("colorA"); + this->addInput("colorB"); + this->addInput("opacity"); - this.addEnumProperty("type", "Type", [ - "Multiply", - "Add", - "Subtract", - "Divide", - // "Add Sub", - "Max", - "Min", - "Switch", - "Overlay", - "Screen" - ]); - this.addFloatProperty("opacity", "Opacity", 1.0, 0.0, 1.0, 0.01); - - const source = ` + this->addEnumProp("type", "Type", + {"Multiply", "Add", "Subtract", "Divide", + // "Add Sub", + "Max", "Min", "Switch", "Overlay", "Screen"}); + this->addFloatProp("opacity", "Opacity", 1.0, 0.0, 1.0, 0.01); + // todo: add props + this->setShaderSource(R""""( float screen(float fg, float bg) { float res = (1.0 - fg) * (1.0 - bg); return 1.0 - res; @@ -77,8 +71,5 @@ export class BlendNode extends GpuDesignerNode { return col; } - `; - - this.buildShader(source); - } -} + )""""); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/brickgenerator.cpp b/src/texturelab/libraries/v1/brickgenerator.cpp new file mode 100644 index 00000000..e4f7ac88 --- /dev/null +++ b/src/texturelab/libraries/v1/brickgenerator.cpp @@ -0,0 +1,138 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void BrickGeneratorNode::init() +{ + this->title = "Brick Generator"; + + this->addIntProp("rows", "Rows", 6, 1, 20, 1); + this->addIntProp("columns", "Columns", 6, 1, 20, 1); + + this->addFloatProp("offset", "Offset", 0.5, 0, 1, 0.1); + + // brick size + auto sizeProps = this->createGroup("Size"); + sizeProps->collapsed = false; + sizeProps->add( + this->addFloatProp("brickWidth", "Brick Width", 0.9, 0, 1, 0.01)); + sizeProps->add( + this->addFloatProp("brickHeight", "Brick Height", 0.9, 0, 1, 0.01)); + + // height + auto heightProps = this->createGroup("Height"); + heightProps->collapsed = false; + heightProps->add( + this->addFloatProp("heightMin", "Height Min", 0.0, 0, 1, 0.05)); + heightProps->add( + this->addFloatProp("heightMax", "Height Max", 1.0, 0, 1, 0.05)); + heightProps->add( + this->addFloatProp("heightBalance", "Height Balance", 1.0, 0, 1, 0.05)); + heightProps->add( + this->addFloatProp("heightVariance", "Height Variance", 0, 0, 1, 0.05)); + + auto source = R""""( + //vec2 brickSize = vec2(prop_brickWidth, prop_brickHeight); + // vec2 tileSize = vec2(prop_rows, prop_columns); + + // float shiftX = 0.5; + // float shiftY = 0.0; + + // offset for alternating rows + // float offset = prop_offset; + + // HEIGHT FUNCTIONS + float + calculateHeight(vec2 brickId) + { + // height + float heightMin = prop_heightMin; + float heightMax = prop_heightMax; + float heightBalance = + prop_heightBalance; // threshold that decides whether to use + // height variance or not + float heightVariance = + prop_heightVariance; // multiplies the heightMax-heightMin range + + // check whether or not there should be a height range in the first + // place + float balRand = _rand(vec2(_seed) + brickId * vec2(0.01)); + + // if balRand is less than heightBalance it means it qualifies for a + // random height. This way if heightBalance is 0 then we only use + // the min luminance + if (balRand > heightBalance) { + return 1.0; + } + + // calculate height variance + // need to offset brickId to give new random result + float randVariance = + _rand(vec2(_seed) + (brickId + vec2(1)) * vec2(0.01)); + randVariance *= heightVariance; + + float range = (heightMax - heightMin); + float height = heightMax - range * randVariance; + + return height; + } + + // slope + // float slopeX;// slope x direction + // float slopeY;// slope y direction + // float slopeBalance;// threshold that determines whether or not to use + // slope float slopeVariation;// decreases the range of the slope + + vec2 is_brick(vec2 pos) + { + vec2 brickSize = vec2(prop_brickWidth, prop_brickHeight); + + vec2 edgeSize = (vec2(1.0) - brickSize) * vec2(0.5); + vec2 brick = vec2(0.0); + + if (pos.x > edgeSize.x && pos.x < (1.0 - edgeSize.x)) + brick.x = 1.0; + + if (pos.y > edgeSize.y && pos.y < (1.0 - edgeSize.y)) + brick.y = 1.0; + + return brick; + } + + vec4 process(vec2 uv) + { + vec2 tileSize = vec2(prop_columns, prop_rows); + float offset = prop_offset; + + // vec2 pos = uv * vec2(5); + vec2 pos = uv * tileSize; + + float xOffset = 0.0; + if (fract(pos.y * 0.5) > 0.5) { + xOffset = offset; + } + pos.x += xOffset; + + // a brick's id would be floor(pos) + // this gives us its origin + // this can act as a random seed for the entire brick + vec2 brickId = floor(pos); // - vec2(xOffset, 0); + + // wrap around x + if (brickId.x > tileSize.x - 1.0) + brickId.x = 0.0; + + // float lum = _rand(vec2(_seed) + brickId * vec2(0.01)); + // float lum = randomFloat(0); + float lum = calculateHeight(brickId); + pos = fract(pos); + + // vec2 isBrick = step(pos,vec2(0.95,0.9)); + vec2 isBrick = is_brick(pos); + + return vec4(vec3(isBrick.x * isBrick.y * lum), 1.0); + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/brightnesscontrast.cpp b/src/texturelab/libraries/v1/brightnesscontrast.cpp new file mode 100644 index 00000000..c33cf823 --- /dev/null +++ b/src/texturelab/libraries/v1/brightnesscontrast.cpp @@ -0,0 +1,30 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void BrightnessContrastNode::init() +{ + this->title = "Brightness Contrast"; + + this->addInput("image"); + + this->addFloatProp("contrast", "Contrast", 0.0, -1, 1, 0.1); + this->addFloatProp("brightness", "Brightness", 0.0, -1, 1, 0.1); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 col = texture(image, uv); + + col.rgb += prop_brightness; + if (prop_contrast > 0.0) + col.rgb = (col.rgb - 0.5) / (1.0 - prop_contrast) + 0.5; + else + col.rgb = (col.rgb - 0.5) * (1.0 + prop_contrast) + 0.5; + + return col; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/cellnode.ts b/src/texturelab/libraries/v1/cell.cpp similarity index 78% rename from src/lib/library/v1/cellnode.ts rename to src/texturelab/libraries/v1/cell.cpp index acb56677..30224b74 100644 --- a/src/lib/library/v1/cellnode.ts +++ b/src/texturelab/libraries/v1/cell.cpp @@ -1,16 +1,17 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -// https://thebookofshaders.com/12/ -export class CellNode extends GpuDesignerNode { - public init() { - this.title = "Cell"; +void CellNode::init() +{ + this->title = "Cell"; - this.addIntProperty("scale", "Scale", 5, 0, 256); - this.addBoolProperty("invert", "Invert", false); - this.addFloatProperty("entropy", "Order", 0, 0, 1, 0.01); - this.addFloatProperty("intensity", "Intensity", 1, 0, 2, 0.01); + this->addIntProp("scale", "Scale", 5, 0, 256); + this->addBoolProp("invert", "Invert", false); + this->addFloatProp("entropy", "Order", 0, 0, 1, 0.01); + this->addFloatProp("intensity", "Intensity", 1, 0, 2, 0.01); - const source = ` + auto source = R""""( vec2 random2( vec2 p ) { p += vec2(_seed); return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453); @@ -62,8 +63,7 @@ export class CellNode extends GpuDesignerNode { return vec4(vec3(m_dist) * prop_intensity, 1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/checkerboard.cpp b/src/texturelab/libraries/v1/checkerboard.cpp new file mode 100644 index 00000000..db590ba7 --- /dev/null +++ b/src/texturelab/libraries/v1/checkerboard.cpp @@ -0,0 +1,31 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +#include + +void CheckerboardNode::init() +{ + this->title = "Checkerboard"; + + this->addFloatProp("rows", "Rows", 2, 1, 20, 1); + this->addFloatProp("columns", "Columns", 2, 1, 20, 1); + + this->addColorProp("color", "Color", QColor()); + + auto source = R""""( + vec4 process(vec2 uv) + { + if ((mod(prop_columns*uv.x, 1.0) < 0.5) ^^ (mod(prop_rows*uv.y, 1.0) < 0.5)) + { + return vec4(prop_color.rgb, 1.0); + } + else + { + return vec4(1.0, 1.0, 1.0, 1.0); + } + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/color.cpp b/src/texturelab/libraries/v1/color.cpp new file mode 100644 index 00000000..e9cf3f7f --- /dev/null +++ b/src/texturelab/libraries/v1/color.cpp @@ -0,0 +1,35 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void ColorNode::init() +{ + this->title = "Color"; + + this->addColorProp("color", "Color", QColor(255, 255, 255, 255)); + + // todo: add props + this->setShaderSource(R""""( + vec4 process(vec2 uv) + { + return prop_color; + } + )""""); +} + +void ColorizeNode::init() +{ + this->title = "Colorize"; + + this->addInput("image"); + + this->addColorProp("color", "Color", QColor(255, 255, 255, 255)); + + // todo: add props + this->setShaderSource(R""""( + vec4 process(vec2 uv) + { + return texture(image,uv) * prop_color; + } + )""""); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/copy.cpp b/src/texturelab/libraries/v1/copy.cpp new file mode 100644 index 00000000..c36e6eb2 --- /dev/null +++ b/src/texturelab/libraries/v1/copy.cpp @@ -0,0 +1,21 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void CopyNode::init() +{ + this->title = "Copy"; + + this->addInput("image"); + this->addStringProp("name", "Name"); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 col = texture(image, uv); + return col; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/directionalwarp.cpp b/src/texturelab/libraries/v1/directionalwarp.cpp new file mode 100644 index 00000000..82f70b2a --- /dev/null +++ b/src/texturelab/libraries/v1/directionalwarp.cpp @@ -0,0 +1,32 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void DirectionalWarpNode::init() +{ + this->title = "Directional Warp"; + + this->addInput("inputImage"); + this->addInput("height"); + + this->addFloatProp("intensity", "Intensity", 0.1, -0.5, 0.5, 0.01); + this->addFloatProp("angle", "Angle", 0.85, 0.0, 3.142, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec2 step = vec2(1.0,1.0)/_textureSize; + + vec2 dir = normalize(vec2(cos(prop_angle), sin(prop_angle))); + + // center point + float dist = abs(texture(height, uv).r) - 0.5; + + vec4 color = texture(inputImage, uv + dir * prop_intensity * dist); + + return color; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/fractalnoise.ts b/src/texturelab/libraries/v1/fractalnoise.cpp similarity index 78% rename from src/lib/library/v1/fractalnoise.ts rename to src/texturelab/libraries/v1/fractalnoise.cpp index 1a8abdcb..50d659a2 100644 --- a/src/lib/library/v1/fractalnoise.ts +++ b/src/texturelab/libraries/v1/fractalnoise.cpp @@ -1,27 +1,22 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -// https://thebookofshaders.com/11/ -export class FractalNoiseNode extends GpuDesignerNode { - public init() { - this.title = "Fractal Noise"; +void FractalNoiseNode::init() +{ + this->title = "Fractal Noise"; - this.addIntProperty("scale", "Scale", 1, 1, 10, 1); - this.addIntProperty("scaleX", "X Scale", 1, 1, 15, 1); - this.addIntProperty("scaleY", "Y Scale", 1, 1, 15, 1); - this.addIntProperty("startBand", "Start Band", 4, 1, 10, 1); - this.addIntProperty("endBand", "End Band", 8, 1, 10, 1); - this.addFloatProperty("persistence", "Persistence", 0.6, 0.0, 1.0, 0.01); - this.addFloatProperty( - "persistenceStart", - "Persistence Start", - 1.0, - 0.0, - 1.0, - 0.01 - ); - this.addFloatProperty("brightness", "Brightness", 0.5, 0.0, 1.0, 0.01); + this->addIntProp("scale", "Scale", 1, 1, 10, 1); + this->addIntProp("scaleX", "X Scale", 1, 1, 15, 1); + this->addIntProp("scaleY", "Y Scale", 1, 1, 15, 1); + this->addIntProp("startBand", "Start Band", 4, 1, 10, 1); + this->addIntProp("endBand", "End Band", 8, 1, 10, 1); + this->addFloatProp("persistence", "Persistence", 0.6, 0.0, 1.0, 0.01); + this->addFloatProp("persistenceStart", "Persistence Start", 1.0, 0.0, 1.0, + 0.01); + this->addFloatProp("brightness", "Brightness", 0.5, 0.0, 1.0, 0.01); - const source = ` + auto source = R""""( // // GLSL textureless classic 2D noise "cnoise", // with an RSL-style periodic variant "pnoise". @@ -115,8 +110,7 @@ export class FractalNoiseNode extends GpuDesignerNode { return vec4(vec3(total + prop_brightness),1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/gradient.cpp b/src/texturelab/libraries/v1/gradient.cpp new file mode 100644 index 00000000..ec5cf1b0 --- /dev/null +++ b/src/texturelab/libraries/v1/gradient.cpp @@ -0,0 +1,96 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" +#include +#include + +void GradientNode::init() +{ + this->title = "Gradient"; + + QColor white(255, 255, 255); + + this->addColorProp("colorA", "Color A", QColor(0, 0, 0)); + this->addFloatProp("posA", "Position A", 0, 0, 1, 0.01); + this->addColorProp("colorB", "Color B", white); + this->addFloatProp("posB", "Position B", 1, 0, 1, 0.01); + + this->addEnumProp( + "mode", "Gradient Direction", + {"Left To Right", "Right To Left", "Top To Bottom", "Bottom To Top"}); + + auto source = R""""( + #define POINTS_MAX 32 + + // assumes points are sorted + vec3 calcGradient(float t, vec3 colors[POINTS_MAX], float positions[POINTS_MAX], int numPoints) + { + if (numPoints == 0) + return vec3(1,0,0); + + if (numPoints == 1) + return colors[0]; + + // here at least two points are available + if (t < positions[0]) + return colors[0]; + + int last = numPoints - 1; + if (t > positions[last]) + return colors[last]; + + // find two points in-between and lerp + + for(int i = 0; i < numPoints-1;i++) { + if (positions[i+1] > t) { + vec3 colorA = colors[i]; + vec3 colorB = colors[i+1]; + + float t1 = positions[i]; + float t2 = positions[i+1]; + + float lerpPos = (t - t1)/(t2 - t1); + return mix(colorA, colorB, lerpPos); + + } + + } + + return vec3(0,0,0); + } + + + vec4 process(vec2 uv) + { + float t = 0.0; + + // left to right + if (prop_mode == 0) + t = uv.x; + // right to left + else if (prop_mode == 1) + t = 1.0 - uv.x; + // top to bottom + else if (prop_mode == 2) + t = 1.0 - uv.y; + // bottom to top + else if (prop_mode == 3) + t = uv.y; + + + vec3 colors[POINTS_MAX]; + colors[0] = prop_colorA.rgb; + colors[1] = prop_colorB.rgb; + float positions[POINTS_MAX]; + positions[0] = prop_posA; + positions[1] = prop_posB; + + + vec3 col = calcGradient(t, colors, positions, 2); + + return vec4(col,1.0); + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/gradientmap.cpp b/src/texturelab/libraries/v1/gradientmap.cpp new file mode 100644 index 00000000..69b9261f --- /dev/null +++ b/src/texturelab/libraries/v1/gradientmap.cpp @@ -0,0 +1,30 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void GradientMapNode::init() +{ + this->title = "GradientMap"; + + this->addInput("inputImage"); + + this->addGradientProp("gradient", "Gradient", Gradient::defaultGradient()); + + auto source = R""""( + float grayscale(vec3 col) + { + return (col.r + col.g + col.b) / 3.0; + } + + vec4 process(vec2 uv) + { + // grayscale input color + float t = grayscale(texture(inputImage, uv).rgb); + vec3 col = sampleGradient(prop_gradient, t); + + return vec4(col, 1.0); + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/heightshift.cpp b/src/texturelab/libraries/v1/heightshift.cpp new file mode 100644 index 00000000..80880ff6 --- /dev/null +++ b/src/texturelab/libraries/v1/heightshift.cpp @@ -0,0 +1,23 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void HeightShiftNode::init() +{ + this->title = "Height Shift"; + + this->addInput("image"); + + this->addFloatProp("shift", "Shift", 0.0, -1.0, 1.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 a = texture(image, uv); + + return a + vec4(vec3(prop_shift), 0.0); + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/hexagon.ts b/src/texturelab/libraries/v1/hexagon.cpp similarity index 75% rename from src/lib/library/v1/hexagon.ts rename to src/texturelab/libraries/v1/hexagon.cpp index 08382b31..e09d5842 100644 --- a/src/lib/library/v1/hexagon.ts +++ b/src/texturelab/libraries/v1/hexagon.cpp @@ -1,15 +1,17 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -export class HexagonNode extends GpuDesignerNode { - public init() { - this.title = "Hexagon"; +void HexagonNode::init() +{ + this->title = "Hexagon"; - this.addFloatProperty("scaleX", "X Scale", 3, 1, 32, 1); - this.addFloatProperty("scaleY", "Y Scale", 2, 1, 32, 1); - this.addFloatProperty("margin", "Margin", 0.9, 0.0, 1.0, 0.01); - this.addFloatProperty("gradient", "Gradient", 0, 0, 1.0, 0.01); + this->addFloatProp("scaleX", "X Scale", 3, 1, 32, 1); + this->addFloatProp("scaleY", "Y Scale", 2, 1, 32, 1); + this->addFloatProp("margin", "Margin", 0.9, 0.0, 1.0, 0.01); + this->addFloatProp("gradient", "Gradient", 0, 0, 1.0, 0.01); - const source = ` + auto source = R""""( // https://www.shadertoy.com/view/Xljczw //todo: cleanup @@ -51,8 +53,7 @@ export class HexagonNode extends GpuDesignerNode { //todo: apply random color using h.zw as color id return vec4(vec3(finalDist),1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/invert.cpp b/src/texturelab/libraries/v1/invert.cpp new file mode 100644 index 00000000..37c0b548 --- /dev/null +++ b/src/texturelab/libraries/v1/invert.cpp @@ -0,0 +1,21 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void InvertNode::init() +{ + this->title = "Invert"; + + this->addInput("color"); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 col = vec4(1.0) - texture(color,uv); + col.a = 1.0; + return col; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/linecell.ts b/src/texturelab/libraries/v1/linecell.cpp similarity index 85% rename from src/lib/library/v1/linecell.ts rename to src/texturelab/libraries/v1/linecell.cpp index c60591f3..6cd00244 100644 --- a/src/lib/library/v1/linecell.ts +++ b/src/texturelab/libraries/v1/linecell.cpp @@ -1,17 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -// https://thebookofshaders.com/edit.php#12/tissue.frag -export class LineCellNode extends GpuDesignerNode { - public init() { - this.title = "Lined Cell"; +void LineCellNode::init() +{ + this->title = "Line Cell"; - this.addIntProperty("scale", "Scale", 5, 0, 256); - this.addBoolProperty("invert", "Invert", false); - this.addFloatProperty("entropy", "Order", 0, 0, 1, 0.01); - this.addFloatProperty("intensity", "Intensity", 1, 0, 2, 0.01); - this.addFloatProperty("thickness", "Line Thickness", 0.1, 0, 0.2, 0.01); + this->addIntProp("scale", "Scale", 5, 0, 256); + this->addBoolProp("invert", "Invert", false); + this->addFloatProp("entropy", "Order", 0, 0, 1, 0.01); + this->addFloatProp("intensity", "Intensity", 1, 0, 2, 0.01); + this->addFloatProp("thickness", "Line Thickness", 0.1, 0, 0.2, 0.01); - const source = ` + auto source = R""""( vec2 random2( vec2 p ) { p += vec2(_seed); return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453); @@ -104,8 +105,7 @@ export class LineCellNode extends GpuDesignerNode { return vec4(color * prop_intensity, 1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/maprange.cpp b/src/texturelab/libraries/v1/maprange.cpp new file mode 100644 index 00000000..759882a0 --- /dev/null +++ b/src/texturelab/libraries/v1/maprange.cpp @@ -0,0 +1,31 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void MapRangeNode::init() +{ + this->title = "Map Range"; + + this->addInput("color"); + + this->addFloatProp("in_min", "Input Minimum", 0, 0, 1.0, 0.01); + this->addFloatProp("in_max", "Input Maximum", 1, 0, 1.0, 0.01); + this->addFloatProp("out_min", "Output Minimum", 0, 0, 1.0, 0.01); + this->addFloatProp("out_max", "Output Maximum", 1, 0, 1.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 col = texture(color,uv); + + float inDiff = prop_in_max - prop_in_min; + col.rgb = (col.rgb - prop_in_min) / inDiff; + + float outDiff = prop_out_max - prop_out_min; + col.rgb = prop_out_min + col.rgb * vec3(outDiff); + return col; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/mask.ts b/src/texturelab/libraries/v1/mask.cpp similarity index 51% rename from src/lib/library/v1/mask.ts rename to src/texturelab/libraries/v1/mask.cpp index 6becb9a7..928bd079 100644 --- a/src/lib/library/v1/mask.ts +++ b/src/texturelab/libraries/v1/mask.cpp @@ -1,15 +1,17 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -export class MaskNode extends GpuDesignerNode { - public init() { - this.title = "Mask"; +void MaskNode::init() +{ + this->title = "Mask"; - this.addInput("textureA"); - this.addInput("textureB"); - this.addInput("mask"); + this->addInput("textureA"); + this->addInput("textureB"); + this->addInput("mask"); - const source = ` - float lum(vec4 col) + auto source = R""""( + float lum(vec4 col) { return (col.r + col.g + col.b) / 3.0; } @@ -24,8 +26,7 @@ export class MaskNode extends GpuDesignerNode { // lerp return a * t + b * (1.0 - t); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/mirror.ts b/src/texturelab/libraries/v1/mirror.cpp similarity index 62% rename from src/lib/library/v1/mirror.ts rename to src/texturelab/libraries/v1/mirror.cpp index 10aaab53..3bf62c99 100644 --- a/src/lib/library/v1/mirror.ts +++ b/src/texturelab/libraries/v1/mirror.cpp @@ -1,22 +1,21 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -export class MirrorNode extends GpuDesignerNode { - public init() { - this.title = "Mirror"; +void MirrorNode::init() +{ + this->title = "Mirror"; - this.addInput("image"); + this->addInput("image"); - this.addEnumProperty("mode", "Mirror Mode", [ - "Left To Right", - "Right To Left", - "Top To Bottom", - "Bottom To Top" - ]); - this.addFloatProperty("offset", "Offset", 0.5, 0, 1, 0.01); - this.addBoolProperty("clamp", "Clamp", true); + this->addEnumProp( + "mode", "Mirror Mode", + {"Left To Right", "Right To Left", "Top To Bottom", "Bottom To Top"}); + this->addFloatProp("offset", "Offset", 0.5, 0, 1, 0.01); + this->addBoolProp("clamp", "Clamp", true); - const source = ` - vec4 process(vec2 uv) + auto source = R""""( + vec4 process(vec2 uv) { // left to right if (prop_mode == 0) @@ -44,8 +43,7 @@ export class MirrorNode extends GpuDesignerNode { vec4 col = texture(image, uv); return col; } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/normalmap.cpp b/src/texturelab/libraries/v1/normalmap.cpp new file mode 100644 index 00000000..29265d33 --- /dev/null +++ b/src/texturelab/libraries/v1/normalmap.cpp @@ -0,0 +1,48 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void NormalMapNode::init() +{ + this->title = "Normal Map"; + + this->addInput("height"); + + this->addFloatProp("strength", "Strength", 1, -2.0, 2.0, 0.05); + this->addIntProp("range", "Range", 1, 1, 20, 1); + this->addBoolProp("res_ind", "Resolution Independent", false); + + this->setShaderSource(R""""( + vec4 process(vec2 uv) + { + vec2 step = (vec2(1.0, 1.0) / _textureSize) * float(prop_range); + if (prop_res_ind) + step = (vec2(1.0, 1.0) / 1024.0) * float(prop_range); + float strength = prop_strength * 0.1; + + // center point + float d0 = abs(texture(height, uv + vec2(0.0, 0.0)).r) * strength / 2.0; + + // sample horizontally + float d1 = abs(texture(height, uv + vec2(step.x, 0.0)).r) * strength / 2.0; + float d2 = abs(texture(height, uv + vec2(-step.x, 0.0)).r) * strength / 2.0; + + // sample vertically + float d3 = abs(texture(height, uv + vec2(0.0, step.y)).r) * strength / 2.0; + float d4 = abs(texture(height, uv + vec2(0.0, -step.y)).r) * strength / 2.0; + + // find diff horizontally and average + float dx = ((d2 - d0) + (d0 - d1)) * 0.5; + + // find diff vertically and average + float dy = ((d4 - d0) + (d0 - d3)) * 0.5; + + vec3 dvx = vec3(step.x, 0.0 , d1-d0); + vec3 dvy = vec3(0.0 , step.y, d3-d0); + vec3 normal = normalize(cross(dvx, dvy)); + vec3 final = normal.xyz * 0.5 + 0.5; + + return vec4(final, 1.0); + } + )""""); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/output.cpp b/src/texturelab/libraries/v1/output.cpp new file mode 100644 index 00000000..c52a0c2a --- /dev/null +++ b/src/texturelab/libraries/v1/output.cpp @@ -0,0 +1,50 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void OutputNode::init() +{ + this->title = "Output"; + + this->addInput("image"); + + this->addStringProp("name", "Output Name"); + this->addEnumProp("components", "Components", + { + "RGBA", + "RGB", + "R", + "G", + "B", + "A", + }); + + this->addEnumProp("precision", "Precision", + { + "8 Bits Per Component", + "16 Bits Per Component", + }); + this->addColorProp("color", "Default Color", QColor(0, 0, 0, 255)); + + this->setShaderSource(R""""( + vec4 process(vec2 uv) + { + vec4 col; + if (image_connected) { + col = vec4(0,1,0,1); + col = texture(image, uv); + // return texture(image, uv); + } else { + col = prop_color; + // return prop_color; + } + + if(prop_components == 2) col.rgb = vec3(col.r); + if(prop_components == 3) col.rgb = vec3(col.g); + if(prop_components == 4) col.rgb = vec3(col.b); + if(prop_components == 5) col.rgb = vec3(col.a); + + return col; + } + )""""); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/perlin3d.cpp b/src/texturelab/libraries/v1/perlin3d.cpp new file mode 100644 index 00000000..ad9555fb --- /dev/null +++ b/src/texturelab/libraries/v1/perlin3d.cpp @@ -0,0 +1,181 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void Perlin3DNode::init() +{ + this->title = "Perlin 3D"; + + this->setShaderSource(R""""( + vec3 mod289(vec3 x) + { + return x - floor(x * (1.0 / 289.0)) * 289.0; + } + + vec4 mod289(vec4 x) + { + return x - floor(x * (1.0 / 289.0)) * 289.0; + } + + vec4 permute(vec4 x) + { + return mod289(((x*34.0)+1.0)*x); + } + + vec4 taylorInvSqrt(vec4 r) + { + return 1.79284291400159 - 0.85373472095314 * r; + } + + vec3 fade(vec3 t) { + return t*t*t*(t*(t*6.0-15.0)+10.0); + } + + // Classic Perlin noise + float cnoise(vec3 P) + { + vec3 Pi0 = floor(P); // Integer part for indexing + vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 + Pi0 = mod289(Pi0); + Pi1 = mod289(Pi1); + vec3 Pf0 = fract(P); // Fractional part for interpolation + vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 + vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); + vec4 iy = vec4(Pi0.yy, Pi1.yy); + vec4 iz0 = Pi0.zzzz; + vec4 iz1 = Pi1.zzzz; + + vec4 ixy = permute(permute(ix) + iy); + vec4 ixy0 = permute(ixy + iz0); + vec4 ixy1 = permute(ixy + iz1); + + vec4 gx0 = ixy0 * (1.0 / 7.0); + vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; + gx0 = fract(gx0); + vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); + vec4 sz0 = step(gz0, vec4(0.0)); + gx0 -= sz0 * (step(0.0, gx0) - 0.5); + gy0 -= sz0 * (step(0.0, gy0) - 0.5); + + vec4 gx1 = ixy1 * (1.0 / 7.0); + vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; + gx1 = fract(gx1); + vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); + vec4 sz1 = step(gz1, vec4(0.0)); + gx1 -= sz1 * (step(0.0, gx1) - 0.5); + gy1 -= sz1 * (step(0.0, gy1) - 0.5); + + vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); + vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); + vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); + vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); + vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); + vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); + vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); + vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); + + vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); + g000 *= norm0.x; + g010 *= norm0.y; + g100 *= norm0.z; + g110 *= norm0.w; + vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); + g001 *= norm1.x; + g011 *= norm1.y; + g101 *= norm1.z; + g111 *= norm1.w; + + float n000 = dot(g000, Pf0); + float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); + float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); + float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); + float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); + float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); + float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); + float n111 = dot(g111, Pf1); + + vec3 fade_xyz = fade(Pf0); + vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); + vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); + float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); + return 2.2 * n_xyz; + } + + // Classic Perlin noise, periodic variant + float pnoise(vec3 P, vec3 rep) + { + vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period + vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period + Pi0 = mod289(Pi0); + Pi1 = mod289(Pi1); + vec3 Pf0 = fract(P); // Fractional part for interpolation + vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 + vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); + vec4 iy = vec4(Pi0.yy, Pi1.yy); + vec4 iz0 = Pi0.zzzz; + vec4 iz1 = Pi1.zzzz; + + vec4 ixy = permute(permute(ix) + iy); + vec4 ixy0 = permute(ixy + iz0); + vec4 ixy1 = permute(ixy + iz1); + + vec4 gx0 = ixy0 * (1.0 / 7.0); + vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; + gx0 = fract(gx0); + vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); + vec4 sz0 = step(gz0, vec4(0.0)); + gx0 -= sz0 * (step(0.0, gx0) - 0.5); + gy0 -= sz0 * (step(0.0, gy0) - 0.5); + + vec4 gx1 = ixy1 * (1.0 / 7.0); + vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; + gx1 = fract(gx1); + vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); + vec4 sz1 = step(gz1, vec4(0.0)); + gx1 -= sz1 * (step(0.0, gx1) - 0.5); + gy1 -= sz1 * (step(0.0, gy1) - 0.5); + + vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); + vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); + vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); + vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); + vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); + vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); + vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); + vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); + + vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); + g000 *= norm0.x; + g010 *= norm0.y; + g100 *= norm0.z; + g110 *= norm0.w; + vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); + g001 *= norm1.x; + g011 *= norm1.y; + g101 *= norm1.z; + g111 *= norm1.w; + + float n000 = dot(g000, Pf0); + float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); + float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); + float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); + float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); + float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); + float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); + float n111 = dot(g111, Pf1); + + vec3 fade_xyz = fade(Pf0); + vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); + vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); + float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); + return 2.2 * n_xyz; + } + + vec4 process(vec2 uv) + { + float c = pnoise(vec3(uv * 10.0,10.0), vec3(10.0,10.0,10.0)); + + return vec4(vec3(c), 1.0); + } + )""""); +} \ No newline at end of file diff --git a/src/lib/library/v1/shapes.ts b/src/texturelab/libraries/v1/shapes.cpp similarity index 61% rename from src/lib/library/v1/shapes.ts rename to src/texturelab/libraries/v1/shapes.cpp index a6a92f1c..a43b87b2 100644 --- a/src/lib/library/v1/shapes.ts +++ b/src/texturelab/libraries/v1/shapes.cpp @@ -1,16 +1,48 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -// https://thebookofshaders.com/07/ -export class PolygonNode extends GpuDesignerNode { - public init() { - this.title = "Polygon"; +void CircleNode::init() +{ + this->title = "Circle"; - this.addFloatProperty("radius", "Radius", 0.7, 0, 3, 0.01); - this.addFloatProperty("angle", "Angle", 0, 0.0, 360.0, 1); - this.addIntProperty("sides", "Sides", 5, 0, 20, 1); - this.addFloatProperty("gradient", "Gradient", 0, 0, 1.0, 0.01); + this->addFloatProp("radius", "Radius", 0.2, 0, 1, 0.01); + this->addEnumProp("color_gen", "Color Generation", + {"Flat", "Linear", "Exponential"}); - const source = ` + this->setShaderSource(R""""( + vec4 process(vec2 uv) + { + float dist = distance(uv, vec2(0.5)); + if( dist <= prop_radius) { + if (prop_color_gen==0) + return vec4(vec3(1.0), 1.0); + else if (prop_color_gen==1) + return vec4(vec3(1.0 - dist / prop_radius), 1.0); + else if (prop_color_gen==2) + { + float val = dist / prop_radius; + return vec4(vec3(1.0 - val * val), 1.0); + } + + } + + return vec4(vec3(0.0), 1.0); + } + )""""); +} + +void PolygonNode::init() +{ + this->title = "Polygon"; + + this->addFloatProp("radius", "Radius", 0.2, 0, 1, 0.01); + this->addFloatProp("angle", "Angle", 0, 0, 360, 0.1); + this->addIntProp("sides", "Sides", 5, 0, 20, 1); + this->addFloatProp("gradient", "Gradient", 0, 0, 1.0, 0.01); + + // todo: add props + this->setShaderSource(R""""( #define PI 3.14159265359 #define TWO_PI 6.28318530718 @@ -36,44 +68,5 @@ export class PolygonNode extends GpuDesignerNode { return vec4(color, 1.0); } - `; - - this.buildShader(source); - } -} - -export class CircleNode extends GpuDesignerNode { - public init() { - this.title = "Circle"; - - this.addFloatProperty("radius", "Radius", 0.4, 0, 1, 0.01); - this.addEnumProperty("color_gen", "Color Generation", [ - "Flat", - "Linear", - "Exponent" - ]); - - const source = ` - vec4 process(vec2 uv) - { - float dist = distance(uv, vec2(0.5)); - if( dist <= prop_radius) { - if (prop_color_gen==0) - return vec4(vec3(1.0), 1.0); - else if (prop_color_gen==1) - return vec4(vec3(1.0 - dist / prop_radius), 1.0); - else if (prop_color_gen==2) - { - float val = dist / prop_radius; - return vec4(vec3(1.0 - val * val), 1.0); - } - - } - - return vec4(vec3(0.0), 1.0); - } - `; - - this.buildShader(source); - } -} + )""""); +} \ No newline at end of file diff --git a/src/lib/library/v1/simplexnoise.ts b/src/texturelab/libraries/v1/simplexnoise.cpp similarity index 80% rename from src/lib/library/v1/simplexnoise.ts rename to src/texturelab/libraries/v1/simplexnoise.cpp index 6b779ae3..70bf665b 100644 --- a/src/lib/library/v1/simplexnoise.ts +++ b/src/texturelab/libraries/v1/simplexnoise.cpp @@ -1,14 +1,15 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -// https://thebookofshaders.com/11/ -export class SimplexNoiseNode extends GpuDesignerNode { - public init() { - this.title = "Simplex Noise"; +void SimplexNoiseNode::init() +{ + this->title = "Simplex Noise"; - this.addFloatProperty("scale", "Scale", 100, 1, 1000, 0.01); + this->addFloatProp("scale", "Scale", 100, 1, 1000, 0.01); - const source = ` - float random (in vec2 st) { + auto source = R""""( + float random (in vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); @@ -64,8 +65,7 @@ export class SimplexNoiseNode extends GpuDesignerNode { return vec4(color,1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/solidcell.ts b/src/texturelab/libraries/v1/solidcell.cpp similarity index 81% rename from src/lib/library/v1/solidcell.ts rename to src/texturelab/libraries/v1/solidcell.cpp index d7bb122f..64ec3024 100644 --- a/src/lib/library/v1/solidcell.ts +++ b/src/texturelab/libraries/v1/solidcell.cpp @@ -1,15 +1,17 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -export class SolidCellNode extends GpuDesignerNode { - public init() { - this.title = "Solid Cell"; +void SolidCellNode::init() +{ + this->title = "Solid Cell"; - this.addIntProperty("scale", "Scale", 5, 0, 256); - this.addBoolProperty("invert", "Invert", false); - this.addFloatProperty("entropy", "Order", 0, 0, 1, 0.01); - this.addFloatProperty("intensity", "Intensity", 1, 0, 2, 0.01); + this->addIntProp("scale", "Scale", 5, 0, 256); + this->addBoolProp("invert", "Invert", false); + this->addFloatProp("entropy", "Order", 0, 0, 1, 0.01); + this->addFloatProp("intensity", "Intensity", 1, 0, 2, 0.01); - const source = ` + auto source = R""""( vec2 random2( vec2 p ) { p += vec2(_seed); return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453); @@ -67,8 +69,7 @@ export class SolidCellNode extends GpuDesignerNode { return vec4(color * prop_intensity, 1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/splat.ts b/src/texturelab/libraries/v1/splat.cpp similarity index 92% rename from src/lib/library/v1/splat.ts rename to src/texturelab/libraries/v1/splat.cpp index 362082ef..43fbbaf0 100644 --- a/src/lib/library/v1/splat.ts +++ b/src/texturelab/libraries/v1/splat.cpp @@ -1,14 +1,16 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -export class SplatNode extends GpuDesignerNode { - public init() { - this.title = "Splat"; +void SplatNode::init() +{ + this->title = "Splat"; - this.addInput("image"); + this->addInput("image"); - this.addIntProperty("count", "Count", 50, 0, 1000, 1); + this->addIntProp("count", "Count", 50, 0, 1000, 1); - const source = ` + auto source = R""""( // https://github.com/glslify/glsl-inverse/blob/master/index.glsl // mat3 inverse(mat3 m) { // float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; @@ -104,8 +106,7 @@ export class SplatNode extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/threshold.cpp b/src/texturelab/libraries/v1/threshold.cpp new file mode 100644 index 00000000..e9e6a326 --- /dev/null +++ b/src/texturelab/libraries/v1/threshold.cpp @@ -0,0 +1,29 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void ThresholdNode::init() +{ + this->title = "Threshold"; + + this->addInput("image"); + + this->addFloatProp("threshold", "Threshold", 0.0, 0.0, 1.0, 0.01); + this->addBoolProp("invert", "Invert", true); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 a = texture(image, uv); + + if (prop_invert) + a.rgb = step(1.0 - prop_threshold, a.rgb); + else + a.rgb = step(prop_threshold, a.rgb); + + return a; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/tile.ts b/src/texturelab/libraries/v1/tile.cpp similarity index 72% rename from src/lib/library/v1/tile.ts rename to src/texturelab/libraries/v1/tile.cpp index e5392289..ee890a2d 100644 --- a/src/lib/library/v1/tile.ts +++ b/src/texturelab/libraries/v1/tile.cpp @@ -1,21 +1,23 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -export class TileNode extends GpuDesignerNode { - public init() { - this.title = "Tile"; +void TileNode::init() +{ + this->title = "Tile"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("offset", "Offset", 0.5, 0, 1, 0.1); + this->addFloatProp("offset", "Offset", 0.5, 0, 1, 0.1); - // brick size - this.addFloatProperty("brickWidth", "Tile Width", 1.0, 0, 1, 0.01); - this.addFloatProperty("brickHeight", "Tile Height", 1.0, 0, 1, 0.01); + // brick size + this->addFloatProp("brickWidth", "Tile Width", 1.0, 0, 1, 0.01); + this->addFloatProp("brickHeight", "Tile Height", 1.0, 0, 1, 0.01); - this.addFloatProperty("rows", "Rows", 6, 1, 20, 1); - this.addFloatProperty("columns", "Columns", 6, 1, 20, 1); + this->addIntProp("rows", "Rows", 6, 1, 30, 1); + this->addIntProp("columns", "Columns", 6, 1, 30, 1); - const source = ` + auto source = R""""( // offset for alternating rows vec2 is_brick(vec2 pos) @@ -64,8 +66,7 @@ export class TileNode extends GpuDesignerNode { return vec4(col.rgb, isBrick.x * isBrick.y * col.a); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v1/transform2d.ts b/src/texturelab/libraries/v1/transform2d.cpp similarity index 77% rename from src/lib/library/v1/transform2d.ts rename to src/texturelab/libraries/v1/transform2d.cpp index ae162168..366b0cbf 100644 --- a/src/lib/library/v1/transform2d.ts +++ b/src/texturelab/libraries/v1/transform2d.cpp @@ -1,22 +1,24 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" -export class Transform2DNode extends GpuDesignerNode { - public init() { - this.title = "Transform2D"; +void Transform2DNode::init() +{ + this->title = "Transform2D"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("translateX", "Translate X", 0, -1.0, 1.0, 0.01); - this.addFloatProperty("translateY", "Translate Y", 0, -1.0, 1.0, 0.01); + this->addFloatProp("translateX", "Translate X", 0, -1.0, 1.0, 0.01); + this->addFloatProp("translateY", "Translate Y", 0, -1.0, 1.0, 0.01); - this.addFloatProperty("scaleX", "Scale X", 1, -2.0, 2.0, 0.01); - this.addFloatProperty("scaleY", "Scale Y", 1, -2.0, 2.0, 0.01); + this->addFloatProp("scaleX", "Scale X", 1, -2.0, 2.0, 0.01); + this->addFloatProp("scaleY", "Scale Y", 1, -2.0, 2.0, 0.01); - this.addFloatProperty("rot", "Rotation", 0, 0.0, 360.0, 0.01); + this->addFloatProp("rot", "Rotation", 0, 0.0, 360.0, 0.01); - this.addBoolProperty("clamp", "Clamp", true); + this->addBoolProp("clamp", "Clamp", true); - const source = ` + auto source = R""""( // https://github.com/glslify/glsl-inverse/blob/master/index.glsl // mat3 inverse(mat3 m) { // float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; @@ -84,8 +86,7 @@ export class Transform2DNode extends GpuDesignerNode { return texture(image, clamp(uv,vec2(0.0), vec2(1.0))); return texture(image, uv); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/warp.cpp b/src/texturelab/libraries/v1/warp.cpp new file mode 100644 index 00000000..0e3ed426 --- /dev/null +++ b/src/texturelab/libraries/v1/warp.cpp @@ -0,0 +1,28 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void WarpNode::init() +{ + this->title = "Warp"; + + this->addInput("inputImage"); + this->addInput("height"); + + this->addFloatProp("intensity", "Intensity", 0.1, -1.0, 1.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec2 step = vec2(1.0,1.0)/_textureSize; + vec4 warpCol = texture(height, uv); + float warp = (warpCol.r + warpCol.g + warpCol.b) / 3.0; + + vec4 color = texture(inputImage, uv + (vec2(warp) - 0.5) * vec2(1.0, -1.0) * prop_intensity); + + return color; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v1/wave.cpp b/src/texturelab/libraries/v1/wave.cpp new file mode 100644 index 00000000..b3005bca --- /dev/null +++ b/src/texturelab/libraries/v1/wave.cpp @@ -0,0 +1,29 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv1.h" + +void WaveNode::init() +{ + this->title = "Wave"; + + this->addIntProp("xfrequency", "X Frequency", 1, 0, 20, 1); + this->addIntProp("yfrequency", "Y Frequency", 0, 0, 20, 1); + this->addFloatProp("phase", "Phase Offset", 0.0, 0.0, 3.142, 0.01); + this->addFloatProp("amp", "Amplitude", 0.5, 0.0, 1.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + float fx = uv.x * 3.142 * 2.0 * float(prop_xfrequency); + float fy = uv.y * 3.142 * 2.0 * float(prop_yfrequency); + float wave = sin(fx + fy + prop_phase) * prop_amp; + + // bring wave to range 0...1 + wave = wave * 0.5 + 0.5; + + return vec4(vec3(wave), 1.0); + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/advancesplatter.ts b/src/texturelab/libraries/v2/advancesplatter.cpp similarity index 89% rename from src/lib/library/v2/advancesplatter.ts rename to src/texturelab/libraries/v2/advancesplatter.cpp index dd336047..036fc4c6 100644 --- a/src/lib/library/v2/advancesplatter.ts +++ b/src/texturelab/libraries/v2/advancesplatter.cpp @@ -1,25 +1,27 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class AdvanceSplatterV2 extends GpuDesignerNode { - public init() { - this.title = "Advance Splatter"; +void AdvanceSplatterNode::init() +{ + this->title = "Advance Splatter"; - this.addInput("image"); - this.addInput("mask"); - this.addInput("size"); - this.addInput("intensity"); + this->addInput("image"); + this->addInput("mask"); + this->addInput("size"); + this->addInput("intensity"); - this.addIntProperty("count", "Count", 50, 0, 1000, 1); - this.addFloatProperty("rot", "Rotation", 0, 0, 360, 0.1); + this->addIntProp("count", "Count", 50, 0, 1000, 1); + this->addFloatProp("rot", "Rotation", 0, 0, 360, 0.1); - this.addFloatProperty("intensityRand", "Random Intensity", 0, 0, 1.0, 0.01); + this->addFloatProp("intensityRand", "Random Intensity", 0, 0, 1.0, 0.01); - this.addFloatProperty("scale", "Scale", 1, 0, 4, 0.1); - this.addFloatProperty("scaleRand", "Scale random", 0, 0, 1, 0.1); + this->addFloatProp("scale", "Scale", 1, 0, 4, 0.1); + this->addFloatProp("scaleRand", "Scale random", 0, 0, 1, 0.1); - this.addEnumProperty("blendType", "Blend Type", ["Max", "Add"]); + this->addEnumProp("blendType", "Blend Type", {"Max", "Add"}); - const source = ` + auto source = R""""( // https://github.com/glslify/glsl-inverse/blob/master/index.glsl // mat3 inverse(mat3 m) { // float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; @@ -184,8 +186,7 @@ export class AdvanceSplatterV2 extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/anisotropicblur.ts b/src/texturelab/libraries/v2/anisotropicblur.cpp similarity index 76% rename from src/lib/library/v2/anisotropicblur.ts rename to src/texturelab/libraries/v2/anisotropicblur.cpp index f4d17dc2..796df601 100644 --- a/src/lib/library/v2/anisotropicblur.ts +++ b/src/texturelab/libraries/v2/anisotropicblur.cpp @@ -1,20 +1,22 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // uses single pass gaussian // https://www.shadertoy.com/view/4tSyzy // https://stackoverflow.com/questions/2157920/why-define-pi-4atan1-d0 -export class AnisotropicBlur extends GpuDesignerNode { - public init() { - this.title = "Anisotropic Blur"; +void AnisotropicBlurNode::init() +{ + this->title = "Anisotropic Blur"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("intensity", "Intensity", 2, 0, 10, 0.1); - this.addFloatProperty("anisotropy", "Anisotropy", 0.7, 0, 1.0, 0.1); - this.addIntProperty("samples", "Samples", 50, 0, 100, 1); - this.addFloatProperty("angle", "Angle", 0, 0, 360, 1); + this->addFloatProp("intensity", "Intensity", 2, 0, 10, 0.1); + this->addFloatProp("anisotropy", "Anisotropy", 0.7, 0, 1.0, 0.1); + this->addIntProp("samples", "Samples", 50, 0, 100, 1); + this->addFloatProp("angle", "Angle", 0, 0, 360, 1); - const source = ` + auto source = R""""( #define pow2(x) (x * x) const float pi = atan(1.0) * 4.0; @@ -66,8 +68,7 @@ export class AnisotropicBlur extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/texturelab/libraries/v2/bevel.cpp b/src/texturelab/libraries/v2/bevel.cpp new file mode 100644 index 00000000..ea84d5db --- /dev/null +++ b/src/texturelab/libraries/v2/bevel.cpp @@ -0,0 +1,212 @@ +#include "../../graphics/renderworker.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" +#include +#include +#include +#include +#include + +// Constants for Euclidean Distance Transform +static const double INF = 1e20; +// Use normalized float range [0.0, 1.0] for GL_RGBA32F textures +static const float VALUE_MAX = 1.0f; + +// Forward declarations for EDT functions +static void edt(std::vector& data, int width, int height, + std::vector& f, std::vector& v, + std::vector& z); + +static void edt1d(std::vector& grid, int offset, int stride, int length, + std::vector& f, std::vector& v, + std::vector& z); + +void BevelNode::init() +{ + this->title = "Bevel"; + this->addInput("image"); + this->addFloatProp("distance", "Distance", 50.0, 0.0, 100.0, 0.01); + + // This node uses CPU processing instead of GPU shader + this->usesCpuProcessing = true; + + // Set a passthrough shader initially (not used, but required for + // initialization) + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 col = texture(image, uv); + return col; + } + )""""; + this->setShaderSource(source); +} + +void BevelNode::cpuProcess(void* glPtr, const RenderCommand& command) +{ + // Cast to QOpenGLFunctions_3_2_Core + auto gl = static_cast(glPtr); + + // Get the first input texture if available + GLuint inputTextureId = 0; + if (!command.inputs.isEmpty()) { + inputTextureId = command.inputs[0].textureId; + } + + if (inputTextureId == 0) + return; + + int width = command.textureWidth; + int height = command.textureHeight; + + // Guard against zero/negative dimensions before any allocation or indexing. + if (width <= 0 || height <= 0) + return; + + // Allocate buffers ((size_t) casts so the *4 can't overflow int). + int gridSize = width * height; + std::vector readPixels((size_t)gridSize * 4); + std::vector resultPixels((size_t)gridSize * 4); + + // Read pixels from input texture + GLuint fbo; + gl->glGenFramebuffers(1, &fbo); + gl->glBindFramebuffer(GL_FRAMEBUFFER, fbo); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, inputTextureId, 0); + + if (gl->glCheckFramebufferStatus(GL_FRAMEBUFFER) == + GL_FRAMEBUFFER_COMPLETE) { + gl->glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, + readPixels.data()); + } + + gl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + gl->glDeleteFramebuffers(1, &fbo); + + // Allocate working arrays + int maxSize = std::max(width, height); + std::vector f(maxSize * 3); + std::vector z(maxSize * 3 + 1); + std::vector v(maxSize * 3); + + std::vector gridOuter(gridSize); + std::vector gridInner(gridSize); + std::vector grid(gridSize); + + // Convert pixels to distance fields + for (int i = 0; i < gridSize; i++) { + float a = readPixels[i * 4 + 0]; // Use red channel + + gridOuter[i] = (a == 1.0f) ? 0.0 + : (a == 0.0f) ? INF + : std::pow(std::max(0.0f, 0.5f - a), 2); + gridInner[i] = (a == 1.0f) ? INF + : (a == 0.0f) ? 0.0 + : std::pow(std::max(0.0f, a - 0.5f), 2); + } + + // Apply Euclidean Distance Transform + edt(gridOuter, width, height, f, v, z); + edt(gridInner, width, height, f, v, z); + + // Get distance property from RenderCommand props + float radius = 50.0f; + for (const auto& prop : command.props) { + if (prop.propName == "distance" && prop.propType == PropType::Float) { + radius = prop.value.toFloat(); + break; + } + } + float offset = 0.25f; + + // Calculate bevel + float minVal = 1.0f; + float maxVal = 0.0f; + + for (int i = 0; i < gridSize; i++) { + double d = std::sqrt(gridOuter[i]) - std::sqrt(gridInner[i]); + float col = VALUE_MAX - VALUE_MAX * (d / radius + offset); + col = std::max(0.0f, std::min(VALUE_MAX, col)); + + minVal = std::min(minVal, col); + maxVal = std::max(maxVal, col); + grid[i] = col; + } + + // Normalize and invert + float range = maxVal - minVal; + float scale = (range > 0.0f) ? (1.0f / range) : 1.0f; + + for (int i = 0; i < gridSize; i++) { + float col = 1.0f - (grid[i] - minVal) * scale; // de-invert + + resultPixels[i * 4 + 0] = col; + resultPixels[i * 4 + 1] = col; + resultPixels[i * 4 + 2] = col; + resultPixels[i * 4 + 3] = 1.0f; + } + + // Upload result to texture + gl->glBindTexture(GL_TEXTURE_2D, command.textureId); + gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, + GL_FLOAT, resultPixels.data()); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + gl->glBindTexture(GL_TEXTURE_2D, 0); +} + +// 2D Euclidean squared distance transform by Felzenszwalb & Huttenlocher +// https://cs.brown.edu/~pff/papers/dt-final.pdf +static void edt(std::vector& data, int width, int height, + std::vector& f, std::vector& v, + std::vector& z) +{ + for (int x = 0; x < width; x++) + edt1d(data, x, width, height, f, v, z); + for (int y = 0; y < height; y++) + edt1d(data, y * width, 1, width, f, v, z); +} + +// 1D squared distance transform +static void edt1d(std::vector& grid, int offset, int stride, int length, + std::vector& f, std::vector& v, + std::vector& z) +{ + v[0] = 0; + z[0] = -INF; + z[1] = INF; + + // Load line in array three times for wrapping + for (int q = 0; q < length; q++) + f[q] = grid[offset + q * stride]; + for (int q = 0; q < length; q++) + f[q + length] = grid[offset + q * stride]; + for (int q = 0; q < length; q++) + f[q + length + length] = grid[offset + q * stride]; + + int k = 0; + for (int q = 1; q < length * 3; q++) { + double s; + do { + int r = v[k]; + s = (f[q] - f[r] + q * q - r * r) / (q - r) / 2.0; + } while (s <= z[k] && --k > -1); + + k++; + v[k] = q; + z[k] = s; + z[k + 1] = INF; + } + + // Copy over middle section + for (int q = length, k = 0; q < length + length; q++) { + while (z[k + 1] < q) + k++; + int r = v[k]; + grid[offset + (q - length) * stride] = f[r] + (q - r) * (q - r); + } +} \ No newline at end of file diff --git a/src/lib/library/v2/blur.ts b/src/texturelab/libraries/v2/blur.cpp similarity index 79% rename from src/lib/library/v2/blur.ts rename to src/texturelab/libraries/v2/blur.cpp index 0576250e..7c2e7d78 100644 --- a/src/lib/library/v2/blur.ts +++ b/src/texturelab/libraries/v2/blur.cpp @@ -1,18 +1,20 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // uses single pass gaussian // https://www.shadertoy.com/view/4tSyzy // https://stackoverflow.com/questions/2157920/why-define-pi-4atan1-d0 -export class BlurV2 extends GpuDesignerNode { - public init() { - this.title = "Blur"; +void BlurNodeV2::init() +{ + this->title = "Blur"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("intensity", "Intensity", 1, 0, 10, 0.1); - this.addIntProperty("samples", "Samples", 50, 0, 100, 1); + this->addFloatProp("intensity", "Intensity", 1, 0, 10, 0.1); + this->addIntProp("samples", "Samples", 50, 0, 100, 1); - const source = ` + auto source = R""""( #define pow2(x) (x * x) const float pi = atan(1.0) * 4.0; @@ -53,8 +55,7 @@ export class BlurV2 extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/capsule.ts b/src/texturelab/libraries/v2/capsule.cpp similarity index 66% rename from src/lib/library/v2/capsule.ts rename to src/texturelab/libraries/v2/capsule.cpp index 059f62c5..d1945837 100644 --- a/src/lib/library/v2/capsule.ts +++ b/src/texturelab/libraries/v2/capsule.cpp @@ -1,17 +1,19 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://thebookofshaders.com/07/ -export class CapsuleNode extends GpuDesignerNode { - public init() { - this.title = "Capsule"; +void CapsuleNode::init() +{ + this->title = "Capsule"; - this.addFloatProperty("radius", "Radius", 0.2, 0, 1.0, 0.01); - this.addFloatProperty("topRadius", "Top Radius", 0.0, 0.0, 0.5, 0.01); - this.addFloatProperty("bottomRadius", "Bottom Radius", 0.0, 0, 0.5, 0.01); - this.addFloatProperty("length", "Length", 0.6, 0, 2.0, 0.01); - this.addFloatProperty("gradient", "Gradient", 0, 0, 1.0, 0.01); + this->addFloatProp("radius", "Radius", 0.2, 0, 1.0, 0.01); + this->addFloatProp("topRadius", "Top Radius", 0.0, 0.0, 0.5, 0.01); + this->addFloatProp("bottomRadius", "Bottom Radius", 0.0, 0, 0.5, 0.01); + this->addFloatProp("length", "Length", 0.6, 0, 2.0, 0.01); + this->addFloatProp("gradient", "Gradient", 0, 0, 1.0, 0.01); - const source = ` + auto source = R""""( #define PI 3.14159265359 #define TWO_PI 6.28318530718 @@ -47,8 +49,7 @@ export class CapsuleNode extends GpuDesignerNode { return vec4(color, 1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/texturelab/libraries/v2/cartesiantopolar.cpp b/src/texturelab/libraries/v2/cartesiantopolar.cpp new file mode 100644 index 00000000..6c9fabbc --- /dev/null +++ b/src/texturelab/libraries/v2/cartesiantopolar.cpp @@ -0,0 +1,36 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +// https://www.mathsisfun.com/polar-cartesian-coordinates.html +void CartesianToPolarNode::init() +{ + this->title = "Cartesian To Polar"; + + this->addInput("image"); + + this->addFloatProp("yscale", "Y Scale", 1.0, 0, 4.0, 0.01); + this->addIntProp("xtile", "X Tile", 2, 0, 5, 1); + this->addFloatProp("angle_offset", "Angle Offset", 0.0, 0, 360, 1.0); + // this->addBoolProp("clamp", "Clamp", false); + + auto source = R""""( + const float PI = 3.142; + vec4 process(vec2 uv) + { + vec2 dir = uv - vec2(0.5, 0.5); + float y = length(dir) * 2.0 * prop_yscale; + + float x = atan(dir.y, dir.x); + x = (x + PI) / (PI * 2.0); // bring to range 0..1 + x += (prop_angle_offset / 360.0); + x = x * float(prop_xtile); + + vec4 col = texture(image, vec2(x, y)); + + return col; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/lib/library/v2/circularsplatter.ts b/src/texturelab/libraries/v2/circularsplatter.cpp similarity index 74% rename from src/lib/library/v2/circularsplatter.ts rename to src/texturelab/libraries/v2/circularsplatter.cpp index bd86ab3f..e3179e13 100644 --- a/src/lib/library/v2/circularsplatter.ts +++ b/src/texturelab/libraries/v2/circularsplatter.cpp @@ -1,142 +1,75 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class CircularSplatter extends GpuDesignerNode { - public init() { - this.title = "Circular Splatter"; - - this.addInput("image"); - this.addInput("mask"); - this.addInput("size"); - this.addInput("intensity"); - - this.addIntProperty("count", "Count", 10, 0, 50, 1); - this.addIntProperty("rings", "Rings", 1, 0, 5, 1); - - this.addEnumProperty("blendType", "Blend Type", ["Max", "Add"]); - - this.addFloatProperty("radius", "Radius", 0.3, 0, 1.0, 0.01); - this.addFloatProperty("spacing", "Spacing", 1.0, 0, 2.0, 0.01); - this.addFloatProperty( - "spiralInfluence", - "Spiral Influence", - 0.0, - 0.0, - 1.0, - 0.01 - ); - this.addBoolProperty("reverseSpiral", "Reverse Spiral Direction", false); - - // ROTATION - const rotProp = this.createGroup("Rotation"); - rotProp.collapsed = false; - rotProp.add(this.addFloatProperty("rot", "Rotation", 0, 0, 360, 0.1)); - rotProp.add( - this.addFloatProperty("rotRand", "Random Rotation", 0.0, 0.0, 1.0, 0.01) - ); - rotProp.add( - this.addFloatProperty("ringRot", "Ring Rotation", 0, 0, 360, 0.1) - ); - rotProp.add( - this.addFloatProperty( - "ringRotRand", - "Ring Rotation Random", - 0.0, - 0.0, - 1.0, - 0.01 - ) - ); - rotProp.add( - this.addFloatProperty( - "ringRotOffset", - "Ring Rotation Offset", - 0.0, - 0.0, - 1.0, - 0.01 - ) - ); - rotProp.add( - this.addBoolProperty("pivotCenter", "Pivot Orientation From Center", true) - ); - - // INTENSITY - const intensityProp = this.createGroup("Intensity"); - intensityProp.collapsed = false; - intensityProp.add( - this.addFloatProperty( - "intensityRand", - "Random Intensity", - 0, - 0, - 1.0, - 0.01 - ) - ); - intensityProp.add( - this.addFloatProperty( - "intensityByRing", - "Intensity By Ring", - 0, - 0, - 1.0, - 0.01 - ) - ); - intensityProp.add( - this.addBoolProperty( - "invertIntensityByRing", - "Invert Intensity By Ring", - true - ) - ); - intensityProp.add( - this.addFloatProperty( - "intensityByAngle", - "Intensity By Angle", - 0, - 0, - 1.0, - 0.01 - ) - ); - intensityProp.add( - this.addBoolProperty( - "invertIntensityByAngle", - "Invert Intensity By Angle", - true - ) - ); - - // SCALE - const scaleProp = this.createGroup("Scale"); - scaleProp.collapsed = false; - scaleProp.add( - this.addFloatProperty("inputSize", "Input Size", 0.1, 0, 1, 0.01) - ); - scaleProp.add(this.addFloatProperty("scale", "Scale", 1, 0, 1, 0.01)); - scaleProp.add( - this.addFloatProperty("scaleRand", "Scale random", 0, 0, 1, 0.01) - ); - - scaleProp.add( - this.addFloatProperty("scaleByRing", "Scale By Ring", 0, 0, 1.0, 0.01) - ); - scaleProp.add( - this.addBoolProperty("invertScaleByRing", "Scale Intensity By Ring", true) - ); - scaleProp.add( - this.addFloatProperty("scaleByAngle", "Scale By Angle", 0, 0, 1.0, 0.01) - ); - scaleProp.add( - this.addBoolProperty( - "invertScaleByAngle", - "Scale Intensity By Angle", - true - ) - ); - - const source = ` +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void CircularSplatterNode::init() +{ + this->title = "Circular Splatter"; + + this->addInput("image"); + this->addInput("mask"); + this->addInput("size"); + this->addInput("intensity"); + + this->addIntProp("count", "Count", 10, 0, 50, 1); + this->addIntProp("rings", "Rings", 1, 0, 5, 1); + + this->addEnumProp("blendType", "Blend Type", {"Max", "Add"}); + + this->addFloatProp("radius", "Radius", 0.3, 0, 1.0, 0.01); + this->addFloatProp("spacing", "Spacing", 1.0, 0, 2.0, 0.01); + this->addFloatProp("spiralInfluence", "Spiral Influence", 0.0, 0.0, 1.0, + 0.01); + this->addBoolProp("reverseSpiral", "Reverse Spiral Direction", false); + + // ROTATION + auto rotProp = this->createGroup("Rotation"); + rotProp->collapsed = false; + rotProp->add(this->addFloatProp("rot", "Rotation", 0, 0, 360, 0.1)); + rotProp->add( + this->addFloatProp("rotRand", "Random Rotation", 0.0, 0.0, 1.0, 0.01)); + rotProp->add( + this->addFloatProp("ringRot", "Ring Rotation", 0, 0, 360, 0.1)); + rotProp->add(this->addFloatProp("ringRotRand", "Ring Rotation Random", 0.0, + 0.0, 1.0, 0.01)); + rotProp->add(this->addFloatProp("ringRotOffset", "Ring Rotation Offset", + 0.0, 0.0, 1.0, 0.01)); + rotProp->add(this->addBoolProp("pivotCenter", + "Pivot Orientation From Center", true)); + + // INTENSITY + auto intensityProp = this->createGroup("Intensity"); + intensityProp->collapsed = false; + intensityProp->add(this->addFloatProp("intensityRand", "Random Intensity", + 0, 0, 1.0, 0.01)); + intensityProp->add(this->addFloatProp( + "intensityByRing", "Intensity By Ring", 0, 0, 1.0, 0.01)); + intensityProp->add(this->addBoolProp("invertIntensityByRing", + "Invert Intensity By Ring", true)); + intensityProp->add(this->addFloatProp( + "intensityByAngle", "Intensity By Angle", 0, 0, 1.0, 0.01)); + intensityProp->add(this->addBoolProp("invertIntensityByAngle", + "Invert Intensity By Angle", true)); + + // SCALE + auto scaleProp = this->createGroup("Scale"); + scaleProp->collapsed = false; + scaleProp->add( + this->addFloatProp("inputSize", "Input Size", 0.1, 0, 1, 0.01)); + scaleProp->add(this->addFloatProp("scale", "Scale", 1, 0, 1, 0.01)); + scaleProp->add( + this->addFloatProp("scaleRand", "Scale random", 0, 0, 1, 0.01)); + + scaleProp->add( + this->addFloatProp("scaleByRing", "Scale By Ring", 0, 0, 1.0, 0.01)); + scaleProp->add(this->addBoolProp("invertScaleByRing", + "Scale Intensity By Ring", true)); + scaleProp->add( + this->addFloatProp("scaleByAngle", "Scale By Angle", 0, 0, 1.0, 0.01)); + scaleProp->add(this->addBoolProp("invertScaleByAngle", + "Scale Intensity By Angle", true)); + + auto source = R""""( mat3 transMat(vec2 t) { return mat3(vec3(1.0,0.0,0.0), vec3(0.0,1.0,0.0), vec3(t, 1.0)); @@ -361,8 +294,7 @@ export class CircularSplatter extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/texturelab/libraries/v2/clamp.cpp b/src/texturelab/libraries/v2/clamp.cpp new file mode 100644 index 00000000..f3e02776 --- /dev/null +++ b/src/texturelab/libraries/v2/clamp.cpp @@ -0,0 +1,36 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void ClampNode::init() +{ + this->title = "Clamp"; + + this->addInput("inputImage"); + + this->addFloatProp("min", "Minimum", 0, 0, 1.0, 0.01); + this->addFloatProp("max", "Maximum", 1, 0, 1.0, 0.01); + + this->addBoolProp("clamp_r", "Clamp R Chanel", true); + this->addBoolProp("clamp_g", "Clamp G Chanel", true); + this->addBoolProp("clamp_b", "Clamp B Chanel", true); + this->addBoolProp("clamp_a", "Clamp A Chanel", false); + + auto source = R""""( + + vec4 process(vec2 uv) + { + // grayscale input color + vec4 col = texture(inputImage,uv); + + if (prop_clamp_r) col.r = clamp(col.r, prop_min, prop_max); + if (prop_clamp_g) col.g = clamp(col.g, prop_min, prop_max); + if (prop_clamp_b) col.b = clamp(col.b, prop_min, prop_max); + if (prop_clamp_a) col.a = clamp(col.a, prop_min, prop_max); + + return col; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/lib/library/v2/combinenormals.ts b/src/texturelab/libraries/v2/combinenormals.cpp similarity index 70% rename from src/lib/library/v2/combinenormals.ts rename to src/texturelab/libraries/v2/combinenormals.cpp index 44099550..ebada81a 100644 --- a/src/lib/library/v2/combinenormals.ts +++ b/src/texturelab/libraries/v2/combinenormals.cpp @@ -1,26 +1,23 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://blog.selfshadow.com/publications/blending-in-detail/ // https://blog.selfshadow.com/sandbox/normals.html // https://www.shadertoy.com/view/4t2SzR -export class CombineNormals extends GpuDesignerNode { - public init() { - this.title = "Combine Normals"; +void CombineNormalsNode::init() +{ + this->title = "Combine Normals"; - this.addInput("detail"); - this.addInput("base"); + this->addInput("detail"); + this->addInput("base"); - let prop = this.addEnumProperty("technique", "Technique", [ - "Linear Blending", - "Overlay Blending", - "Whiteout", - "Detail Oriented" - ]); - prop.setValue(3); + auto prop = this->addEnumProp( + "technique", "Technique", + {"Linear Blending", "Overlay Blending", "Whiteout", "Detail Oriented"}); + prop->setValue(3); - const source = ` + auto source = R""""( float overlay(float x, float y) { @@ -70,8 +67,7 @@ export class CombineNormals extends GpuDesignerNode { return vec4(result, 1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/directionalblur.ts b/src/texturelab/libraries/v2/directionalblur.cpp similarity index 75% rename from src/lib/library/v2/directionalblur.ts rename to src/texturelab/libraries/v2/directionalblur.cpp index 04901f87..231e0847 100644 --- a/src/lib/library/v2/directionalblur.ts +++ b/src/texturelab/libraries/v2/directionalblur.cpp @@ -1,20 +1,22 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // uses single pass gaussian // https://www.shadertoy.com/view/4tSyzy // https://stackoverflow.com/questions/2157920/why-define-pi-4atan1-d0 -export class DirectionalBlur extends GpuDesignerNode { - public init() { - this.title = "Directional Blur"; +void DirectionalBlurNode::init() +{ + this->title = "Directional Blur"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("intensity", "Intensity", 1, 0, 10, 0.1); - this.addIntProperty("samples", "Samples", 50, 0, 100, 1); - this.addFloatProperty("angle", "Angle", 0, 0, 360, 1); - this.addFloatProperty("balance", "Balance", 0, -1.0, 1.0, 0.1); + this->addFloatProp("intensity", "Intensity", 1, 0, 10, 0.1); + this->addIntProp("samples", "Samples", 50, 0, 100, 1); + this->addFloatProp("angle", "Angle", 0, 0, 360, 1); + this->addFloatProp("balance", "Balance", 0, -1.0, 1.0, 0.1); - const source = ` + auto source = R""""( #define pow2(x) (x * x) const float pi = atan(1.0) * 4.0; @@ -61,8 +63,7 @@ export class DirectionalBlur extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/texturelab/libraries/v2/directionalwarpv2.cpp b/src/texturelab/libraries/v2/directionalwarpv2.cpp new file mode 100644 index 00000000..9d48518a --- /dev/null +++ b/src/texturelab/libraries/v2/directionalwarpv2.cpp @@ -0,0 +1,33 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void DirectionalWarpV2Node::init() +{ + this->title = "Directional Warp"; + + this->addInput("inputImage"); + this->addInput("height"); + + this->addFloatProp("intensity", "Intensity", 0.1, -0.5, 0.5, 0.01); + this->addFloatProp("angle", "Angle", 0, 0, 360, 1); + + // calculates normal, then warps uv by it + auto source = R""""( + vec4 process(vec2 uv) + { + vec2 step = vec2(1.0,1.0)/_textureSize; + + vec2 dir = normalize(vec2(cos(radians(prop_angle)), sin(radians(prop_angle)))); + + // center point + float dist = abs(texture(height, uv).r) - 0.5; + + vec4 color = texture(inputImage, uv + dir * prop_intensity * dist); + + return color; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/lib/library/v2/extractchannel.ts b/src/texturelab/libraries/v2/extractchannel.cpp similarity index 54% rename from src/lib/library/v2/extractchannel.ts rename to src/texturelab/libraries/v2/extractchannel.cpp index e0e69c32..2f7eee54 100644 --- a/src/lib/library/v2/extractchannel.ts +++ b/src/texturelab/libraries/v2/extractchannel.cpp @@ -1,23 +1,19 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class ExtractChannel extends GpuDesignerNode { - public init() { - this.title = "Extract Channel"; +void ExtractChannelNode::init() +{ + this->title = "Extract Channel"; - this.addInput("image"); + this->addInput("image"); - // Red - let prop = this.addEnumProperty("channel", "Channel", [ - "Red", - "Green", - "Blue", - "Alpha", - "Average (RGB)" - ]); + // Red + auto prop = + this->addEnumProp("channel", "Channel", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); - const source = ` + auto source = R""""( float getChannel(vec4 inputData, int mode) { @@ -43,8 +39,7 @@ export class ExtractChannel extends GpuDesignerNode { return vec4(result); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/texturelab/libraries/v2/floodfill.cpp b/src/texturelab/libraries/v2/floodfill.cpp new file mode 100644 index 00000000..78b5bb7d --- /dev/null +++ b/src/texturelab/libraries/v2/floodfill.cpp @@ -0,0 +1,212 @@ +#include "../../graphics/renderworker.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" +#include +#include +#include +#include +#include +#include + +// Use normalized float range [0.0, 1.0] for GL_RGBA32F textures +static const float VALUE_MAX = 1.0f; +static const float ONE_OVER_VALUE_MAX = 1.0f / VALUE_MAX; + +struct Vector2i { + int x, y; + Vector2i(int x, int y) : x(x), y(y) {} +}; + +struct FloodFillPixel { + int localX, localY; + int globalX, globalY; + FloodFillPixel(int lx, int ly, int gx, int gy) + : localX(lx), localY(ly), globalX(gx), globalY(gy) + { + } +}; + +struct Box { + int left = 0, top = 0, right = 0, bottom = 0; + std::vector pixels; + + void expand(int x, int y) + { + left = std::min(left, x); + top = std::min(top, y); + right = std::max(right, x); + bottom = std::max(bottom, y); + } + + void negativeInfinity() + { + left = INT_MAX; + top = INT_MAX; + right = INT_MIN; + bottom = INT_MIN; + } + + int width() const { return right - left; } + int height() const { return bottom - top; } +}; + +static int wrapAround(int value, int upperBound) +{ + return ((value % upperBound) + upperBound) % upperBound; +} + +static Vector2i mapPixelToLocal(const Vector2i& globalPixel, int width, + int height) +{ + return Vector2i(wrapAround(globalPixel.x, width), + wrapAround(globalPixel.y, height)); +} + +static float getIntensity(int x, int y, const std::vector& pixels, + int width, int height) +{ + int idx = 4 * (width * y + x); + float col = pixels[idx + 0] + pixels[idx + 1] + pixels[idx + 2]; + return col * (1.0f / 3.0f) * ONE_OVER_VALUE_MAX; +} + +static void setColorAtPixel(std::vector& data, int width, int x, int y, + float r, float g, float b, float a) +{ + int idx = 4 * (width * y + x); + data[idx + 0] = r; + data[idx + 1] = g; + data[idx + 2] = b; + data[idx + 3] = a; +} + +void FloodFillNode::init() +{ + this->title = "Flood Fill"; + this->addInput("image"); + this->usesCpuProcessing = true; + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 col = texture(image, uv); + return col; + } + )""""; + this->setShaderSource(source); +} + +void FloodFillNode::cpuProcess(void* glPtr, const RenderCommand& command) +{ + auto gl = static_cast(glPtr); + + GLuint inputTextureId = 0; + if (!command.inputs.isEmpty()) { + inputTextureId = command.inputs[0].textureId; + } + + if (inputTextureId == 0) + return; + + int width = command.textureWidth; + int height = command.textureHeight; + // Guard against zero/negative dimensions: wrapAround() below does value % + // width/height (division by zero), and a negative product would wrap to a + // huge size_t at allocation. + if (width <= 0 || height <= 0) + return; + // size_t so width*height*4 can't overflow int for large textures. + size_t gridSize = (size_t)width * height; + + // Read pixels from input texture + std::vector readPixels(gridSize * 4); + GLuint fbo; + gl->glGenFramebuffers(1, &fbo); + gl->glBindFramebuffer(GL_FRAMEBUFFER, fbo); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, inputTextureId, 0); + + if (gl->glCheckFramebufferStatus(GL_FRAMEBUFFER) == + GL_FRAMEBUFFER_COMPLETE) { + gl->glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, + readPixels.data()); + } + + gl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + gl->glDeleteFramebuffers(1, &fbo); + + // Process flood fill + std::vector visited(gridSize, false); + std::vector rects; + float threshold = 0.1f; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (visited[y * width + x]) + continue; + + // Capture island + Box rect; + rect.negativeInfinity(); + + std::queue queue; + queue.push(Vector2i(x, y)); + + while (!queue.empty()) { + Vector2i globalPixel = queue.front(); + queue.pop(); + + Vector2i pixel = mapPixelToLocal(globalPixel, width, height); + + if (visited[pixel.y * width + pixel.x]) + continue; + visited[pixel.y * width + pixel.x] = true; + + float intensity = + getIntensity(pixel.x, pixel.y, readPixels, width, height); + if (intensity < threshold) + continue; + + rect.expand(globalPixel.x, globalPixel.y); + rect.pixels.emplace_back(pixel.x, pixel.y, globalPixel.x, + globalPixel.y); + + queue.push(Vector2i(globalPixel.x + 1, globalPixel.y)); + queue.push(Vector2i(globalPixel.x - 1, globalPixel.y)); + queue.push(Vector2i(globalPixel.x, globalPixel.y + 1)); + queue.push(Vector2i(globalPixel.x, globalPixel.y - 1)); + } + + if (rect.width() > 0 && rect.height() > 0) { + rects.push_back(rect); + } + } + } + + // Render output + std::vector results(gridSize * 4, 0.0f); + + for (const auto& rect : rects) { + float sx = rect.width() * (1.0f / width); + float sy = rect.height() * (1.0f / height); + + for (const auto& pixel : rect.pixels) { + float u = (pixel.globalX - rect.left) / (float)rect.width(); + float v = (pixel.globalY - rect.top) / (float)rect.height(); + + setColorAtPixel(results, width, pixel.localX, pixel.localY, u, v, + sx, sy); + } + } + + // Upload result to texture + gl->glBindTexture(GL_TEXTURE_2D, command.textureId); + gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, + GL_FLOAT, results.data()); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + gl->glBindTexture(GL_TEXTURE_2D, 0); +} \ No newline at end of file diff --git a/src/lib/library/v2/floodfillsampler.ts b/src/texturelab/libraries/v2/floodfillsampler.cpp similarity index 82% rename from src/lib/library/v2/floodfillsampler.ts rename to src/texturelab/libraries/v2/floodfillsampler.cpp index 499c7873..279c9ac3 100644 --- a/src/lib/library/v2/floodfillsampler.ts +++ b/src/texturelab/libraries/v2/floodfillsampler.cpp @@ -1,28 +1,31 @@ -import { Color } from "@/lib/designer/color"; -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class FloodFillSampler extends GpuDesignerNode { - public init() { - this.title = "Flood Fill Sampler"; +#include - this.addInput("floodfill"); - this.addInput("image"); - this.addInput("mask"); - this.addInput("size"); - this.addInput("intensity"); +void FloodFillSamplerNode::init() +{ + this->title = "Flood Fill Sampler"; - this.addFloatProperty("rot", "Rotation", 0, 0, 360, 0.1); - this.addFloatProperty("rotRand", "Random Rotation", 0, 0, 1.0, 0.01); - this.addFloatProperty("posRand", "Random Position", 0, 0, 1.0, 0.01); - this.addFloatProperty("intensityRand", "Random Intensity", 0, 0, 1.0, 0.01); - this.addFloatProperty("scale", "Scale", 1, 0, 4, 0.1); - this.addFloatProperty("scaleRand", "Scale random", 0, 0, 1, 0.1); + this->addInput("floodfill"); + this->addInput("image"); + this->addInput("mask"); + this->addInput("size"); + this->addInput("intensity"); - this.addIntProperty("precision", "Precision", 2, 1, 3, 1); + this->addFloatProp("rot", "Rotation", 0, 0, 360, 0.1); + this->addFloatProp("rotRand", "Random Rotation", 0, 0, 1.0, 0.01); + this->addFloatProp("posRand", "Random Position", 0, 0, 1.0, 0.01); + this->addFloatProp("intensityRand", "Random Intensity", 0, 0, 1.0, 0.01); + this->addFloatProp("scale", "Scale", 1, 0, 4, 0.1); + this->addFloatProp("scaleRand", "Scale random", 0, 0, 1, 0.1); - this.addColorProperty("bg", "Background Color", new Color()); + this->addIntProp("precision", "Precision", 2, 1, 3, 1); - const source = ` + this->addColorProp("bg", "Background Color", QColor()); + + auto source = R""""( mat3 transMat(vec2 t) { @@ -167,8 +170,7 @@ export class FloodFillSampler extends GpuDesignerNode { return vec4(color * intens, 1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/floodfilltobbox.ts b/src/texturelab/libraries/v2/floodfilltobbox.cpp similarity index 66% rename from src/lib/library/v2/floodfilltobbox.ts rename to src/texturelab/libraries/v2/floodfilltobbox.cpp index ba9bb90c..18be1c8e 100644 --- a/src/lib/library/v2/floodfilltobbox.ts +++ b/src/texturelab/libraries/v2/floodfilltobbox.cpp @@ -1,20 +1,17 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class FloodFillToBBox extends GpuDesignerNode { - public init() { - this.title = "Flood Fill To BBox"; +void FloodFillToBBoxNode::init() +{ + this->title = "Flood Fill To BBox"; - this.addInput("floodfill"); + this->addInput("floodfill"); - this.addEnumProperty("function", "Function", [ - "max(x,y)", - "min(x,y)", - "x", - "y", - "length(x,y)" - ]); + this->addEnumProp("function", "Function", + {"max(x,y)", "min(x,y)", "x", "y", "length(x,y)"}); - const source = ` + auto source = R""""( vec4 process(vec2 uv) { vec4 pixelData = texture(floodfill, uv); @@ -36,8 +33,7 @@ export class FloodFillToBBox extends GpuDesignerNode { return vec4(vec3(intensity), 1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/floodfilltocolor.ts b/src/texturelab/libraries/v2/floodfilltocolor.cpp similarity index 80% rename from src/lib/library/v2/floodfilltocolor.ts rename to src/texturelab/libraries/v2/floodfilltocolor.cpp index fc979799..005a42fc 100644 --- a/src/lib/library/v2/floodfilltocolor.ts +++ b/src/texturelab/libraries/v2/floodfilltocolor.cpp @@ -1,20 +1,20 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class FloodFillToColor extends GpuDesignerNode { - public init() { - this.title = "Flood Fill To Color"; +void FloodFillToColorNode::init() +{ + this->title = "Flood Fill To Color"; - this.addInput("floodfill"); - this.addInput("color"); + this->addInput("floodfill"); + this->addInput("color"); - this.addIntProperty("precision", "Precision", 2, 1, 3, 1); + this->addIntProp("precision", "Precision", 2, 1, 3, 1); - let prop = this.addEnumProperty("source", "Sample Origin", [ - "Top-Left", - "Center" - ]); + auto prop = + this->addEnumProp("source", "Sample Origin", {"Top-Left", "Center"}); - const source = ` + auto source = R""""( vec2 calcFloodFillOrigin(vec2 uv, vec4 pixelData) { // pixelPos box width pixel uv to box @@ -67,8 +67,7 @@ export class FloodFillToColor extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/floodfilltogradient.ts b/src/texturelab/libraries/v2/floodfilltogradient.cpp similarity index 84% rename from src/lib/library/v2/floodfilltogradient.ts rename to src/texturelab/libraries/v2/floodfilltogradient.cpp index 8be3f6af..280d5e70 100644 --- a/src/lib/library/v2/floodfilltogradient.ts +++ b/src/texturelab/libraries/v2/floodfilltogradient.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class FloodFillToGradient extends GpuDesignerNode { - public init() { - this.title = "Flood Fill To Gradient"; +void FloodFillToGradientNode::init() +{ + this->title = "Flood Fill To Gradient"; - this.addInput("floodfill"); + this->addInput("floodfill"); - this.addFloatProperty("angle", "Angle", 0, 0, 360, 1); - this.addFloatProperty("variation", "Angle Variation", 0, 0, 1.0, 0.05); - this.addIntProperty("precision", "Precision", 2, 1, 3, 1); + this->addFloatProp("angle", "Angle", 0, 0, 360, 1); + this->addFloatProp("variation", "Angle Variation", 0, 0, 1.0, 0.05); + this->addIntProp("precision", "Precision", 2, 1, 3, 1); - const source = ` + auto source = R""""( vec2 calcFloodFillOrigin(vec2 uv, vec4 pixelData) { // pixelPos box width pixel uv to box @@ -80,8 +82,7 @@ export class FloodFillToGradient extends GpuDesignerNode { return vec4(vec3(grad), 1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/floodfilltorandomcolor.ts b/src/texturelab/libraries/v2/floodfilltorandomcolor.cpp similarity index 85% rename from src/lib/library/v2/floodfilltorandomcolor.ts rename to src/texturelab/libraries/v2/floodfilltorandomcolor.cpp index 9f3094c1..a92468d7 100644 --- a/src/lib/library/v2/floodfilltorandomcolor.ts +++ b/src/texturelab/libraries/v2/floodfilltorandomcolor.cpp @@ -1,13 +1,15 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class FloodFillToRandomColor extends GpuDesignerNode { - public init() { - this.title = "Flood Fill To Random Color"; +void FloodFillToRandomColorNode::init() +{ + this->title = "Flood Fill To Random Color"; - this.addInput("floodfill"); - this.addIntProperty("precision", "Precision", 2, 1, 3, 1); + this->addInput("floodfill"); + this->addIntProp("precision", "Precision", 2, 1, 3, 1); - const source = ` + auto source = R""""( vec2 calcFloodFillOrigin(vec2 uv, vec4 pixelData) { // pixelPos box width pixel uv to box @@ -59,8 +61,7 @@ export class FloodFillToRandomColor extends GpuDesignerNode { return color; //return vec4(center.x, center.y, 0.0, 1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/floodfilltorandomintensity.ts b/src/texturelab/libraries/v2/floodfilltorandomintensity.cpp similarity index 83% rename from src/lib/library/v2/floodfilltorandomintensity.ts rename to src/texturelab/libraries/v2/floodfilltorandomintensity.cpp index 1c36c0fe..b9a76b3b 100644 --- a/src/lib/library/v2/floodfilltorandomintensity.ts +++ b/src/texturelab/libraries/v2/floodfilltorandomintensity.cpp @@ -1,13 +1,15 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class FloodFillToRandomIntensity extends GpuDesignerNode { - public init() { - this.title = "Flood Fill To Random Intensity"; +void FloodFillToRandomIntensityNode::init() +{ + this->title = "Flood Fill To Random Intensity"; - this.addInput("floodfill"); - this.addIntProperty("precision", "Precision", 2, 1, 3, 1); + this->addInput("floodfill"); + this->addIntProp("precision", "Precision", 2, 1, 3, 1); - const source = ` + auto source = R""""( vec2 calcFloodFillOrigin(vec2 uv, vec4 pixelData) { // pixelPos box width pixel uv to box @@ -55,8 +57,7 @@ export class FloodFillToRandomIntensity extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/texturelab/libraries/v2/gradientdynamic.cpp b/src/texturelab/libraries/v2/gradientdynamic.cpp new file mode 100644 index 00000000..cd377d04 --- /dev/null +++ b/src/texturelab/libraries/v2/gradientdynamic.cpp @@ -0,0 +1,35 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void GradientDynamicNode::init() +{ + this->title = "Gradient Dynamic"; + + this->addInput("image"); + this->addInput("gradient"); + + this->addFloatProp("position", "Position", 0, 0, 1, 0.01); + + this->addEnumProp("orientation", "Gradient Direction", + {"Vertical", "Horizontal"}); + + auto source = R""""( + vec4 process(vec2 uv) + { + float t = 0.0; + + float intensity = texture(image, uv).r; + + vec4 col = vec4(0); + if (prop_orientation == 0) + col = texture(gradient, vec2(prop_position, intensity)); + else + col = texture(gradient, vec2(intensity, prop_position)); + + return col; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/gradientnoise.ts b/src/texturelab/libraries/v2/gradientnoise.cpp similarity index 80% rename from src/lib/library/v2/gradientnoise.ts rename to src/texturelab/libraries/v2/gradientnoise.cpp index 614e2d3d..0a41c62b 100644 --- a/src/lib/library/v2/gradientnoise.ts +++ b/src/texturelab/libraries/v2/gradientnoise.cpp @@ -1,15 +1,17 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://www.shadertoy.com/view/XdXGW8 -export class GradientNoise extends GpuDesignerNode { - public init() { - this.title = "Gradient Noise"; +void GradientNoiseNode::init() +{ + this->title = "Gradient Noise"; - this.addIntProperty("scale", "Scale", 20, 1, 1000, 1); - this.addIntProperty("scaleX", "Scale X", 1, 1, 5, 1); - this.addIntProperty("scaleY", "Scale Y", 1, 1, 5, 1); + this->addIntProp("scale", "Scale", 20, 1, 1000, 1); + this->addIntProp("scaleX", "Scale X", 1, 1, 5, 1); + this->addIntProp("scaleY", "Scale Y", 1, 1, 5, 1); - const source = ` + auto source = R""""( vec2 hash( vec2 x ) // replace this by something better { const vec2 k = vec2( 0.3183099, 0.3678794 ); @@ -54,8 +56,7 @@ export class GradientNoise extends GpuDesignerNode { return vec4(color,1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/gradientnoisefractalsum.ts b/src/texturelab/libraries/v2/gradientnoisefractalsum.cpp similarity index 78% rename from src/lib/library/v2/gradientnoisefractalsum.ts rename to src/texturelab/libraries/v2/gradientnoisefractalsum.cpp index 56094791..a4cff0b1 100644 --- a/src/lib/library/v2/gradientnoisefractalsum.ts +++ b/src/texturelab/libraries/v2/gradientnoisefractalsum.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://www.shadertoy.com/view/XdXGW8 -export class GradientNoiseFractalSum extends GpuDesignerNode { - public init() { - this.title = "Gradient Noise Fractal Sum"; +void GradientNoiseFractalSumNode::init() +{ + this->title = "Gradient Noise Fractal Sum"; - this.addIntProperty("scale", "Scale", 8, 1, 100, 1); - this.addIntProperty("layers", "Layers", 5, 1, 20, 1); - this.addFloatProperty("gain", "Gain", 0.5, 0.1, 2, 0.1); - this.addFloatProperty("lacunarity", "Lacunarity", 2.0, 0.1, 2, 0.1); + this->addIntProp("scale", "Scale", 8, 1, 100, 1); + this->addIntProp("layers", "Layers", 5, 1, 20, 1); + this->addFloatProp("gain", "Gain", 0.5, 0.1, 2, 0.1); + this->addFloatProp("lacunarity", "Lacunarity", 2.0, 0.1, 2, 0.1); - const source = ` + auto source = R""""( vec2 hash( vec2 x ) // replace this by something better { const vec2 k = vec2( 0.3183099, 0.3678794 ); @@ -66,8 +68,7 @@ export class GradientNoiseFractalSum extends GpuDesignerNode { return vec4(color,1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/texturelab/libraries/v2/grayscale.cpp b/src/texturelab/libraries/v2/grayscale.cpp new file mode 100644 index 00000000..401fccaa --- /dev/null +++ b/src/texturelab/libraries/v2/grayscale.cpp @@ -0,0 +1,24 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void GrayscaleNode::init() +{ + this->title = "Grayscale"; + + this->addInput("image"); + + auto source = R""""( + vec4 process(vec2 uv) + { + // grayscale input color + vec4 col = texture(image, uv); + + col.rgb = vec3((col.r + col.g + col.b) * 0.3333333); + + return col; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/lib/library/v2/histogramscan.ts b/src/texturelab/libraries/v2/histogramscan.cpp similarity index 69% rename from src/lib/library/v2/histogramscan.ts rename to src/texturelab/libraries/v2/histogramscan.cpp index 97d5fb5e..096badb5 100644 --- a/src/lib/library/v2/histogramscan.ts +++ b/src/texturelab/libraries/v2/histogramscan.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class HistogramScan extends GpuDesignerNode { - public init() { - this.title = "Histogram Scan"; +void HistogramScanNode::init() +{ + this->title = "Histogram Scan"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("position", "Position", 0.5, 0.0, 1.0, 0.01); - this.addFloatProperty("contrast", "Contrast", 0.01, 0.01, 1.0, 0.01); - this.addBoolProperty("invert", "Invert", false); + this->addFloatProp("position", "Position", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("contrast", "Contrast", 0.01, 0.01, 1.0, 0.01); + this->addBoolProp("invert", "Invert", false); - const source = ` + auto source = R""""( vec4 process(vec2 uv) { vec4 a = texture(image, uv); @@ -27,10 +29,9 @@ export class HistogramScan extends GpuDesignerNode { else return vec4(vec3(1.0) - result, a.a); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } // SHADERTOY diff --git a/src/lib/library/v2/histogramselect.ts b/src/texturelab/libraries/v2/histogramselect.cpp similarity index 67% rename from src/lib/library/v2/histogramselect.ts rename to src/texturelab/libraries/v2/histogramselect.cpp index d444069f..034a92da 100644 --- a/src/lib/library/v2/histogramselect.ts +++ b/src/texturelab/libraries/v2/histogramselect.cpp @@ -1,17 +1,19 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class HistogramSelect extends GpuDesignerNode { - public init() { - this.title = "Histogram Select"; +void HistogramSelectNode::init() +{ + this->title = "Histogram Select"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("position", "Position", 0.5, 0.0, 1.0, 0.01); - this.addFloatProperty("range", "Range", 0.1, 0.0, 1.0, 0.01); - this.addFloatProperty("contrast", "Contrast", 0.01, 0.01, 1.0, 0.01); - this.addBoolProperty("invert", "Invert", false); + this->addFloatProp("position", "Position", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("range", "Range", 0.1, 0.0, 1.0, 0.01); + this->addFloatProp("contrast", "Contrast", 0.01, 0.01, 1.0, 0.01); + this->addBoolProp("invert", "Invert", false); - const source = ` + auto source = R""""( vec4 process(vec2 uv) { vec4 a = texture(image, uv); @@ -29,10 +31,9 @@ export class HistogramSelect extends GpuDesignerNode { else return vec4(vec3(1.0) - result, a.a); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } // SHADERTOY diff --git a/src/texturelab/libraries/v2/histogramshift.cpp b/src/texturelab/libraries/v2/histogramshift.cpp new file mode 100644 index 00000000..dc280f4a --- /dev/null +++ b/src/texturelab/libraries/v2/histogramshift.cpp @@ -0,0 +1,26 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void HistogramShiftNode::init() +{ + this->title = "Histogram Shift"; + + this->addInput("image"); + + this->addFloatProp("position", "Position", 0.0, 0.0, 1.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 a = texture(image, uv); + + // shows artifacts for pixels at 1 when position is at 0 + a.rgb = mod(a.rgb + vec3(prop_position), vec3(1.0)); + + return a; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/hsl.ts b/src/texturelab/libraries/v2/hsl.cpp similarity index 71% rename from src/lib/library/v2/hsl.ts rename to src/texturelab/libraries/v2/hsl.cpp index 5ae514aa..3d8de04c 100644 --- a/src/lib/library/v2/hsl.ts +++ b/src/texturelab/libraries/v2/hsl.cpp @@ -1,28 +1,21 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://www.shadertoy.com/view/XljGzV // https://www.rapidtables.com/convert/color/rgb-to-hsl.html // https://gist.github.com/yiwenl/745bfea7f04c456e0101 -export class Hsl extends GpuDesignerNode { - public init() { - this.title = "HSL"; +void HslNode::init() +{ + this->title = "HSL"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("hue", "Hue Offset", 0.0, -0.5, 0.5, 0.01); - this.addFloatProperty("sat", "Saturation Offset", 0.0, -0.5, 0.5, 0.01); - this.addFloatProperty( - "lightness", - "Lightness Offset", - 0.0, - -0.5, - 0.5, - 0.01 - ); + this->addFloatProp("hue", "Hue Offset", 0.0, -0.5, 0.5, 0.01); + this->addFloatProp("sat", "Saturation Offset", 0.0, -0.5, 0.5, 0.01); + this->addFloatProp("lightness", "Lightness Offset", 0.0, -0.5, 0.5, 0.01); - const source = ` + auto source = R""""( vec3 hsl2rgb( in vec3 c ) { @@ -79,8 +72,7 @@ export class Hsl extends GpuDesignerNode { return vec4(rgb, 1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/hslextract.ts b/src/texturelab/libraries/v2/hslextract.cpp similarity index 74% rename from src/lib/library/v2/hslextract.ts rename to src/texturelab/libraries/v2/hslextract.cpp index ed2b8fa0..36752dc1 100644 --- a/src/lib/library/v2/hslextract.ts +++ b/src/texturelab/libraries/v2/hslextract.cpp @@ -1,23 +1,20 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://www.shadertoy.com/view/XljGzV // https://www.rapidtables.com/convert/color/rgb-to-hsl.html // https://gist.github.com/yiwenl/745bfea7f04c456e0101 -export class HslExtract extends GpuDesignerNode { - public init() { - this.title = "HSL Extract"; +void HslExtractNode::init() +{ + this->title = "HSL Extract"; - this.addInput("image"); + this->addInput("image"); - let prop = this.addEnumProperty("source", "Source", [ - "Hue", - "Saturation", - "Lightness" - ]); + auto prop = this->addEnumProp("source", "Source", + {"Hue", "Saturation", "Lightness"}); - const source = ` + auto source = R""""( vec3 rgb2hsl( in vec3 c ){ float h = 0.0; @@ -66,8 +63,7 @@ export class HslExtract extends GpuDesignerNode { return vec4(vec3(result), 1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/texturelab/libraries/v2/imagenode.cpp b/src/texturelab/libraries/v2/imagenode.cpp new file mode 100644 index 00000000..4a919ae6 --- /dev/null +++ b/src/texturelab/libraries/v2/imagenode.cpp @@ -0,0 +1,17 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void ImageNode::init() +{ + this->title = "Image"; + + this->imageProp = this->addImageProp("image", "Image"); + + this->setShaderSource(R""""( + vec4 process(vec2 uv) + { + return texture(prop_image,uv); + } + )""""); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v2/invertnormal.cpp b/src/texturelab/libraries/v2/invertnormal.cpp new file mode 100644 index 00000000..2f80bdd0 --- /dev/null +++ b/src/texturelab/libraries/v2/invertnormal.cpp @@ -0,0 +1,33 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void InvertNormalNode::init() +{ + this->title = "Invert Normal"; + + this->addInput("inputImage"); + + this->addBoolProp("invertRed", "Invert Red", false); + this->addBoolProp("invertGreen", "Invert Green", true); + this->addBoolProp("invertBlue", "Invert Blue", false); + this->addBoolProp("invertAlpha", "Invert Alpha", false); + + auto source = R""""( + + vec4 process(vec2 uv) + { + // grayscale input color + vec4 result = texture(inputImage,uv); + + if(prop_invertRed) result.r = 1.0 - result.r; + if(prop_invertGreen) result.g = 1.0 - result.g; + if(prop_invertBlue) result.b = 1.0 - result.b; + if(prop_invertAlpha) result.a = 1.0 - result.a; + + return result; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v2/normalmapv2.cpp b/src/texturelab/libraries/v2/normalmapv2.cpp new file mode 100644 index 00000000..1fd70b10 --- /dev/null +++ b/src/texturelab/libraries/v2/normalmapv2.cpp @@ -0,0 +1,53 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +// http://www.smart-page.net/smartnormal/js/SmartNormalMapFilter.js +void NormalMapV2Node::init() +{ + this->title = "Normal Map"; + // this.exportName = "result"; + this->addInput("height"); + + // this->addFloatProp("strength", "Strength", 0.02, -0.02, 0.02, + // 0.00001); + this->addFloatProp("strength", "Strength", 1, -2.0, 2.0, 0.05); + this->addIntProp("range", "Range", 1, 1, 20, 1); + this->addBoolProp("res_ind", "Resolution Independent", false); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec2 step = (vec2(1.0, 1.0) / _textureSize) * float(prop_range); + if (prop_res_ind) + step = (vec2(1.0, 1.0) / 1024.0) * float(prop_range); + float strength = prop_strength * 0.1; + + // center point + float d0 = abs(texture(height, uv + vec2(0.0, 0.0)).r) * strength / 2.0; + + // sample horizontally + float d1 = abs(texture(height, uv + vec2(step.x, 0.0)).r) * strength / 2.0; + float d2 = abs(texture(height, uv + vec2(-step.x, 0.0)).r) * strength / 2.0; + + // sample vertically + float d3 = abs(texture(height, uv + vec2(0.0, step.y)).r) * strength / 2.0; + float d4 = abs(texture(height, uv + vec2(0.0, -step.y)).r) * strength / 2.0; + + // find diff horizontally and average + float dx = ((d2 - d0) + (d0 - d1)) * 0.5; + + // find diff vertically and average + float dy = ((d4 - d0) + (d0 - d3)) * 0.5; + + vec3 dvx = vec3(step.x, 0.0 , d1-d0); + vec3 dvy = vec3(0.0 , step.y, d3-d0); + vec3 normal = normalize(cross(dvx, dvy)); + vec3 final = normal.xyz * 0.5 + 0.5; + + return vec4(final, 1.0); + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/polartocartesian.ts b/src/texturelab/libraries/v2/polartocartesian.cpp similarity index 53% rename from src/lib/library/v2/polartocartesian.ts rename to src/texturelab/libraries/v2/polartocartesian.cpp index 1bc678ad..f45ffe44 100644 --- a/src/lib/library/v2/polartocartesian.ts +++ b/src/texturelab/libraries/v2/polartocartesian.cpp @@ -1,24 +1,17 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; -import { Color } from "@/lib/designer/color"; -import { Gradient } from "@/lib/designer/gradient"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://www.mathsisfun.com/polar-cartesian-coordinates.html -export class PolarToCartesian extends GpuDesignerNode { - public init() { - this.title = "Polar To Cartesian"; - - this.addInput("image"); - - this.addFloatProperty( - "angle_offset", - "Sample Angle Offset", - 0.0, - 0, - 360, - 1 - ); - - const source = ` +void PolarToCartesianNode::init() +{ + this->title = "Polar To Cartesian"; + + this->addInput("image"); + + this->addFloatProp("angle_offset", "Sample Angle Offset", 0.0, 0, 360, 1); + + auto source = R""""( const float PI = 3.1415926538; vec4 process(vec2 uv) { @@ -35,8 +28,7 @@ export class PolarToCartesian extends GpuDesignerNode { return col; } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v2/pow.cpp b/src/texturelab/libraries/v2/pow.cpp new file mode 100644 index 00000000..4a79092e --- /dev/null +++ b/src/texturelab/libraries/v2/pow.cpp @@ -0,0 +1,24 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void PowNode::init() +{ + this->title = "Pow"; + + this->addInput("image"); + + this->addFloatProp("exponent", "Exponent", 1.0, 0.0, 10.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 a = texture(image, uv); + a.rgb = pow(a.rgb, vec3(prop_exponent)); + + return a; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v2/quantize.cpp b/src/texturelab/libraries/v2/quantize.cpp new file mode 100644 index 00000000..5dcf476d --- /dev/null +++ b/src/texturelab/libraries/v2/quantize.cpp @@ -0,0 +1,24 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void QuantizeNode::init() +{ + this->title = "Quantize"; + + this->addInput("image"); + + this->addIntProp("steps", "Steps", 12, 2, 20, 1); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 a = texture(image, uv); + a.rgb = floor(a.rgb * vec3(float(prop_steps))) / vec3(float(prop_steps)); + + return a; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v2/rgbamerge.cpp b/src/texturelab/libraries/v2/rgbamerge.cpp new file mode 100644 index 00000000..4306531d --- /dev/null +++ b/src/texturelab/libraries/v2/rgbamerge.cpp @@ -0,0 +1,76 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void RgbaMergeNode::init() +{ + this->title = "RGBA Merge"; + + this->addInput("redSource"); + this->addInput("greenSource"); + this->addInput("blueSource"); + this->addInput("alphaSource"); + + // Red + auto prop = + this->addEnumProp("redSource", "Red Source", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(0); + + // Green + prop = + this->addEnumProp("greenSource", "Green Source", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(1); + + // Blue + prop = + this->addEnumProp("blueSource", "Blue Source", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(2); + + // Alpha + prop = + this->addEnumProp("alphaSource", "Alpha Source", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(3); + + auto source = R""""( + + float getChannel(vec4 inputData, int mode) + { + if (mode == 0) return inputData.r; + if (mode == 1) return inputData.g; + if (mode == 2) return inputData.b; + if (mode == 3) return inputData.a; + if (mode == 4) { + return (inputData.r + inputData.g + inputData.b) * 0.3333333; + } + + return 0.0; + } + + vec4 process(vec2 uv) + { + // grayscale input color + vec4 r = texture(redSource,uv); + vec4 g = texture(greenSource,uv); + vec4 b = texture(blueSource,uv); + vec4 a = texture(alphaSource,uv); + + vec4 result = vec4(0, 0, 0, 1); + if (redSource_connected) + result.r = getChannel(r, prop_redSource); + if (greenSource_connected) + result.g = getChannel(g, prop_greenSource); + if (blueSource_connected) + result.b = getChannel(b, prop_blueSource); + if (alphaSource_connected) + result.a = getChannel(a, prop_alphaSource); + + return result; + } + )""""; + + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v2/rgbashuffle.cpp b/src/texturelab/libraries/v2/rgbashuffle.cpp new file mode 100644 index 00000000..e7bc65a9 --- /dev/null +++ b/src/texturelab/libraries/v2/rgbashuffle.cpp @@ -0,0 +1,66 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void RgbaShuffleNode::init() +{ + this->title = "RGBA Shuffle"; + + this->addInput("inputImage"); + + // Red + auto prop = + this->addEnumProp("redSource", "Red Source", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(0); + + // Green + prop = + this->addEnumProp("greenSource", "Green Source", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(1); + + // Blue + prop = + this->addEnumProp("blueSource", "Blue Source", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(2); + + // Alpha + prop = + this->addEnumProp("alphaSource", "Alpha Source", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(3); + + auto source = R""""( + + float getChannel(vec4 inputData, int mode) + { + if (mode == 0) return inputData.r; + if (mode == 1) return inputData.g; + if (mode == 2) return inputData.b; + if (mode == 3) return inputData.a; + if (mode == 4) { + return (inputData.r + inputData.g + inputData.b) * 0.3333333; + } + + return 0.0; + } + + vec4 process(vec2 uv) + { + // grayscale input color + vec4 col = texture(inputImage,uv); + + vec4 result = vec4(0); + result.r = getChannel(col, prop_redSource); + result.g = getChannel(col, prop_greenSource); + result.b = getChannel(col, prop_blueSource); + result.a = getChannel(col, prop_alphaSource); + + return result; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/lib/library/v2/simplexnoise.ts b/src/texturelab/libraries/v2/simplexnoisev2.cpp similarity index 81% rename from src/lib/library/v2/simplexnoise.ts rename to src/texturelab/libraries/v2/simplexnoisev2.cpp index 33b2a00f..efffdcd7 100644 --- a/src/lib/library/v2/simplexnoise.ts +++ b/src/texturelab/libraries/v2/simplexnoisev2.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://www.shadertoy.com/view/Msf3WH // previous implementation was actually value noise -export class SimplexNoiseV2 extends GpuDesignerNode { - public init() { - this.title = "Simplex Noise"; +void SimplexNoiseV2Node::init() +{ + this->title = "Simplex Noise"; - this.addIntProperty("scale", "Scale", 100, 1, 1000, 1); - this.addIntProperty("scaleX", "Scale X", 1, 1, 5, 1); - this.addIntProperty("scaleY", "Scale Y", 1, 1, 5, 1); + this->addIntProp("scale", "Scale", 100, 1, 1000, 1); + this->addIntProp("scaleX", "Scale X", 1, 1, 5, 1); + this->addIntProp("scaleY", "Scale Y", 1, 1, 5, 1); - const source = ` + auto source = R""""( vec2 hash( vec2 x ) // replace this by something better { const vec2 k = vec2( 0.3183099, 0.3678794 ); @@ -58,8 +60,7 @@ export class SimplexNoiseV2 extends GpuDesignerNode { return vec4(color,1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/skew.ts b/src/texturelab/libraries/v2/skew.cpp similarity index 76% rename from src/lib/library/v2/skew.ts rename to src/texturelab/libraries/v2/skew.cpp index 354ba3b9..00b2c9bc 100644 --- a/src/lib/library/v2/skew.ts +++ b/src/texturelab/libraries/v2/skew.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class Skew extends GpuDesignerNode { - public init() { - this.title = "Skew"; +void SkewNode::init() +{ + this->title = "Skew"; - this.addInput("image"); + this->addInput("image"); - this.addEnumProperty("axis", "Axis", ["Horizontal", "Vertical"]); - this.addFloatProperty("skew", "skew", 0.5, -1.0, 1.0, 0.01); - this.addFloatProperty("position", "Position", 0.5, 0.0, 1.0, 0.01); + this->addEnumProp("axis", "Axis", {"Horizontal", "Vertical"}); + this->addFloatProp("skew", "skew", 0.5, -1.0, 1.0, 0.01); + this->addFloatProp("position", "Position", 0.5, 0.0, 1.0, 0.01); - const source = ` + auto source = R""""( vec4 process(vec2 uv) { float xSkew = 0.0; @@ -37,10 +39,9 @@ export class Skew extends GpuDesignerNode { vec4 col = texture(image, uv); return col; } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } // SHADERTOY diff --git a/src/lib/library/v2/slopeblur.ts b/src/texturelab/libraries/v2/slopeblur.cpp similarity index 77% rename from src/lib/library/v2/slopeblur.ts rename to src/texturelab/libraries/v2/slopeblur.cpp index 95c8071e..2eef9bf6 100644 --- a/src/lib/library/v2/slopeblur.ts +++ b/src/texturelab/libraries/v2/slopeblur.cpp @@ -1,17 +1,19 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // multiple pass warp -export class SlopeBlur extends GpuDesignerNode { - public init() { - this.title = "Slope Blur"; +void SlopeBlurNode::init() +{ + this->title = "Slope Blur"; - this.addInput("image"); - this.addInput("slope"); + this->addInput("image"); + this->addInput("slope"); - this.addFloatProperty("intensity", "Intensity", 1, 0, 5, 0.1); - this.addIntProperty("quality", "Quality", 5, 0, 10, 1); + this->addFloatProp("intensity", "Intensity", 1, 0, 5, 0.1); + this->addIntProp("quality", "Quality", 5, 0, 10, 1); - const source = ` + auto source = R""""( vec2 calcSlope(vec2 uv) { vec3 sl = vec3(0.0,0.0,0.0); @@ -49,8 +51,7 @@ export class SlopeBlur extends GpuDesignerNode { return vec4(vec3(final),1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/softflower.ts b/src/texturelab/libraries/v2/softflower.cpp similarity index 77% rename from src/lib/library/v2/softflower.ts rename to src/texturelab/libraries/v2/softflower.cpp index fdd9f2f5..f61d70de 100644 --- a/src/lib/library/v2/softflower.ts +++ b/src/texturelab/libraries/v2/softflower.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://thebookofshaders.com/07/ -export class SoftFlower extends GpuDesignerNode { - public init() { - this.title = "Soft Flower"; +void SoftFlowerNode::init() +{ + this->title = "Soft Flower"; - this.addFloatProperty("radius", "Radius", 0.2, 0, 1.0, 0.01); - this.addIntProperty("sides", "Sides", 6, 0, 10, 1); - this.addFloatProperty("depth", "Depth", 1.0, 0, 1.0, 0.01); - this.addFloatProperty("gradient", "Gradient", 0.1, 0, 1.0, 0.01); + this->addFloatProp("radius", "Radius", 0.2, 0, 1.0, 0.01); + this->addIntProp("sides", "Sides", 6, 0, 10, 1); + this->addFloatProp("depth", "Depth", 1.0, 0, 1.0, 0.01); + this->addFloatProp("gradient", "Gradient", 0.1, 0, 1.0, 0.01); - const source = ` + auto source = R""""( #define PI 3.14159265359 #define TWO_PI 6.28318530718 @@ -56,8 +58,7 @@ export class SoftFlower extends GpuDesignerNode { return vec4(color, 1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/splat.ts b/src/texturelab/libraries/v2/splatv2.cpp similarity index 92% rename from src/lib/library/v2/splat.ts rename to src/texturelab/libraries/v2/splatv2.cpp index 4bee9b3c..f3ab9a17 100644 --- a/src/lib/library/v2/splat.ts +++ b/src/texturelab/libraries/v2/splatv2.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class SplatNodeV2 extends GpuDesignerNode { - public init() { - this.title = "Splat"; +void SplatV2Node::init() +{ + this->title = "Splat"; - this.addInput("image"); + this->addInput("image"); - this.addIntProperty("count", "Count", 50, 0, 1000, 1); + this->addIntProp("count", "Count", 50, 0, 1000, 1); - this.addEnumProperty("blendType", "Blend Type", ["Max", "Add"]); + this->addEnumProp("blendType", "Blend Type", {"Max", "Add"}); - const source = ` + auto source = R""""( // https://github.com/glslify/glsl-inverse/blob/master/index.glsl // mat3 inverse(mat3 m) { // float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; @@ -127,8 +129,7 @@ export class SplatNodeV2 extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/star.ts b/src/texturelab/libraries/v2/star.cpp similarity index 77% rename from src/lib/library/v2/star.ts rename to src/texturelab/libraries/v2/star.cpp index 71983ae6..5bc6bd06 100644 --- a/src/lib/library/v2/star.ts +++ b/src/texturelab/libraries/v2/star.cpp @@ -1,17 +1,19 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://thebookofshaders.com/07/ -export class Star extends GpuDesignerNode { - public init() { - this.title = "Star"; +void StarNode::init() +{ + this->title = "Star"; - this.addFloatProperty("radius", "Radius", 0.2, 0, 0.5, 0.01); - this.addIntProperty("sides", "Sides", 5, 3, 20, 1); - this.addFloatProperty("depth", "Depth", 0.5, 0, 1.0, 0.01); - this.addBoolProperty("antialias", "Anti-Alias", true); - // this.addFloatProperty("gradient", "Gradient", 0.1, 0, 1.0, 0.01); + this->addFloatProp("radius", "Radius", 0.2, 0, 0.5, 0.01); + this->addIntProp("sides", "Sides", 5, 3, 20, 1); + this->addFloatProp("depth", "Depth", 0.5, 0, 1.0, 0.01); + this->addBoolProp("antialias", "Anti-Alias", true); + // this->addFloatProp("gradient", "Gradient", 0.1, 0, 1.0, 0.01); - const source = ` + auto source = R""""( #define PI 3.14159265359 #define TWO_PI 6.28318530718 @@ -64,8 +66,7 @@ export class Star extends GpuDesignerNode { return vec4(color, 1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v2/stripes.cpp b/src/texturelab/libraries/v2/stripes.cpp new file mode 100644 index 00000000..244a0946 --- /dev/null +++ b/src/texturelab/libraries/v2/stripes.cpp @@ -0,0 +1,30 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void StripesNode::init() +{ + this->title = "Stripes"; + + this->addIntProp("stripes", "Stripes", 10, 0, 30, 1); + this->addIntProp("shift", "Shift", 0, 0, 30, 1); + this->addFloatProp("width", "Width", 0.5, 0.0, 1.0, 0.01); + + // calculates normal, then warps uv by it + auto source = R""""( + vec4 process(vec2 uv) + { + float fx = uv.x * 3.142 * 2.0 * float(prop_stripes); + float fy = uv.y * 3.142 * 2.0 * float(prop_shift); + float wave = sin(fx + fy); + + // bring wave to range 0...1 + wave = wave * 0.5 + 0.5; + wave = step(prop_width, wave); + + return vec4(vec3(wave), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/lib/library/v2/tilesampler.ts b/src/texturelab/libraries/v2/tilesampler.cpp similarity index 81% rename from src/lib/library/v2/tilesampler.ts rename to src/texturelab/libraries/v2/tilesampler.cpp index 59a7cc92..88aa7408 100644 --- a/src/lib/library/v2/tilesampler.ts +++ b/src/texturelab/libraries/v2/tilesampler.cpp @@ -1,69 +1,49 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; - -export class TileSampler extends GpuDesignerNode { - public init() { - this.title = "Tile Sampler"; - - this.addInput("image"); - this.addInput("mask"); - this.addInput("size"); - this.addInput("intensity"); - this.addInput("vector"); - - this.addEnumProperty("blendType", "Blend Type", ["Max", "Add"]); - - this.addIntProperty("rows", "Row Count", 8, 0, 15, 1); - this.addIntProperty("columns", "Column Count", 8, 0, 15, 1); - - const posProps = this.createGroup("Position"); - posProps.add(this.addFloatProperty("offset", "Offset", 0.5, 0, 1, 0.1)); - posProps.add( - this.addEnumProperty("offset_axis", "Offset Axis", ["X Axis", "Y Axis"]) - ); - posProps.add( - this.addIntProperty("offset_interval", "Offset Interval", 1, 1, 5, 1) - ); - - const rotProps = this.createGroup("Rotation"); - rotProps.add(this.addFloatProperty("rot", "Rotation", 0, 0, 360, 0.1)); - rotProps.add( - this.addFloatProperty("rotRand", "Random Rotation", 0, 0, 1.0, 0.01) - ); - rotProps.add( - this.addFloatProperty("posRand", "Random Position", 0, 0, 1.0, 0.01) - ); - rotProps.add( - this.addFloatProperty( - "intensityRand", - "Random Intensity", - 0, - 0, - 1.0, - 0.01 - ) - ); - - rotProps.add( - this.addFloatProperty( - "vector_influence", - "Vector Rotation Influence", - 1, - 0, - 1, - 0.1 - ) - ); - - const scaleProps = this.createGroup("Scale"); - scaleProps.add( - this.addEnumProperty("sizeMode", "Size Mode", ["Normal", "Keep Aspect"]) - ); - scaleProps.add(this.addFloatProperty("scale", "Scale", 1, 0, 4, 0.1)); - scaleProps.add( - this.addFloatProperty("scaleRand", "Scale random", 0, 0, 1, 0.1) - ); - - const source = ` +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" + +void TileSamplerNode::init() +{ + this->title = "Tile Sampler"; + + this->addInput("image"); + this->addInput("mask"); + this->addInput("size"); + this->addInput("intensity"); + this->addInput("vector"); + + this->addEnumProp("blendType", "Blend Type", {"Max", "Add"}); + + this->addIntProp("rows", "Row Count", 8, 0, 30, 1); + this->addIntProp("columns", "Column Count", 8, 0, 30, 1); + + auto posProps = this->createGroup("Position"); + posProps->add(this->addFloatProp("offset", "Offset", 0.0, 0, 1, 0.1)); + posProps->add( + this->addEnumProp("offset_axis", "Offset Axis", {"X Axis", "Y Axis"})); + posProps->add( + this->addIntProp("offset_interval", "Offset Interval", 2, 1, 5, 1)); + + auto rotProps = this->createGroup("Rotation"); + rotProps->add(this->addFloatProp("rot", "Rotation", 0, 0, 360, 0.1)); + rotProps->add( + this->addFloatProp("rotRand", "Random Rotation", 0, 0, 1.0, 0.01)); + rotProps->add( + this->addFloatProp("posRand", "Random Position", 0, 0, 1.0, 0.01)); + rotProps->add(this->addFloatProp("intensityRand", "Random Intensity", 0, 0, + 1.0, 0.01)); + + rotProps->add(this->addFloatProp( + "vector_influence", "Vector Rotation Influence", 1, 0, 1, 0.1)); + + auto scaleProps = this->createGroup("Scale"); + scaleProps->add( + this->addEnumProp("sizeMode", "Size Mode", {"Normal", "Keep Aspect"})); + scaleProps->add(this->addFloatProp("scale", "Scale", 1, 0, 4, 0.1)); + scaleProps->add( + this->addFloatProp("scaleRand", "Scale random", 0, 0, 1, 0.1)); + + auto source = R""""( mat3 transMat(vec2 t) { @@ -248,8 +228,7 @@ export class TileSampler extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/transform2dv2.ts b/src/texturelab/libraries/v2/transform2dv2.cpp similarity index 76% rename from src/lib/library/v2/transform2dv2.ts rename to src/texturelab/libraries/v2/transform2dv2.cpp index 2b654930..8872fa0f 100644 --- a/src/lib/library/v2/transform2dv2.ts +++ b/src/texturelab/libraries/v2/transform2dv2.cpp @@ -1,23 +1,25 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class Transform2DNodeV2 extends GpuDesignerNode { - public init() { - this.title = "Transform2D"; +void Transform2DV2Node::init() +{ + this->title = "Transform2D"; - this.addInput("image"); + this->addInput("image"); - this.addFloatProperty("translateX", "Translate X", 0, -1.0, 1.0, 0.01); - this.addFloatProperty("translateY", "Translate Y", 0, -1.0, 1.0, 0.01); + this->addFloatProp("translateX", "Translate X", 0, -1.0, 1.0, 0.01); + this->addFloatProp("translateY", "Translate Y", 0, -1.0, 1.0, 0.01); - this.addFloatProperty("scale", "Scale", 1, 0.0, 2.0, 0.01); - this.addFloatProperty("scaleX", "Scale X", 1, -2.0, 2.0, 0.01); - this.addFloatProperty("scaleY", "Scale Y", 1, -2.0, 2.0, 0.01); + this->addFloatProp("scale", "Scale", 1, 0.0, 2.0, 0.01); + this->addFloatProp("scaleX", "Scale X", 1, -2.0, 2.0, 0.01); + this->addFloatProp("scaleY", "Scale Y", 1, -2.0, 2.0, 0.01); - this.addFloatProperty("rot", "Rotation", 0, 0.0, 360.0, 0.01); + this->addFloatProp("rot", "Rotation", 0, 0.0, 360.0, 0.01); - this.addBoolProperty("clamp", "Clamp", true); + this->addBoolProp("clamp", "Clamp", true); - const source = ` + auto source = R""""( // https://github.com/glslify/glsl-inverse/blob/master/index.glsl // mat3 inverse(mat3 m) { // float a00 = m[0][0], a01 = m[0][1], a02 = m[0][2]; @@ -85,8 +87,7 @@ export class Transform2DNodeV2 extends GpuDesignerNode { return texture(image, clamp(uv,vec2(0.0), vec2(1.0))); return texture(image, uv); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/valuenoise.ts b/src/texturelab/libraries/v2/valuenoise.cpp similarity index 74% rename from src/lib/library/v2/valuenoise.ts rename to src/texturelab/libraries/v2/valuenoise.cpp index 61bc421a..b5a439dd 100644 --- a/src/lib/library/v2/valuenoise.ts +++ b/src/texturelab/libraries/v2/valuenoise.cpp @@ -1,15 +1,17 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://www.shadertoy.com/view/lsf3WH -export class ValueNoise extends GpuDesignerNode { - public init() { - this.title = "Value Noise"; +void ValueNoiseNode::init() +{ + this->title = "Value Noise"; - this.addIntProperty("scale", "Scale", 100, 1, 1000, 1); - this.addIntProperty("scaleX", "Scale X", 1, 1, 5, 1); - this.addIntProperty("scaleY", "Scale Y", 1, 1, 5, 1); + this->addIntProp("scale", "Scale", 100, 1, 1000, 1); + this->addIntProp("scaleX", "Scale X", 1, 1, 5, 1); + this->addIntProp("scaleY", "Scale Y", 1, 1, 5, 1); - const source = ` + auto source = R""""( float wrapAround(float value, float upperBound) { return mod((value + upperBound - 1.0), upperBound); } @@ -42,8 +44,7 @@ export class ValueNoise extends GpuDesignerNode { return vec4(color,1.0); } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/lib/library/v2/valuenoisefractalsum.ts b/src/texturelab/libraries/v2/valuenoisefractalsum.cpp similarity index 75% rename from src/lib/library/v2/valuenoisefractalsum.ts rename to src/texturelab/libraries/v2/valuenoisefractalsum.cpp index a50b9ea1..660f4356 100644 --- a/src/lib/library/v2/valuenoisefractalsum.ts +++ b/src/texturelab/libraries/v2/valuenoisefractalsum.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" // https://www.shadertoy.com/view/XdXGW8 -export class ValueNoiseFractalSum extends GpuDesignerNode { - public init() { - this.title = "Value Noise Fractal Sum"; +void ValueNoiseFractalSumNode::init() +{ + this->title = "Value Noise Fractal Sum"; - this.addIntProperty("scale", "Scale", 8, 1, 100, 1); - this.addIntProperty("layers", "Layers", 5, 1, 20, 1); - this.addFloatProperty("gain", "Gain", 0.5, 0.1, 2, 0.1); - this.addFloatProperty("lacunarity", "Lacunarity", 2.0, 0.1, 2, 0.1); + this->addIntProp("scale", "Scale", 8, 1, 100, 1); + this->addIntProp("layers", "Layers", 5, 1, 20, 1); + this->addFloatProp("gain", "Gain", 0.5, 0.1, 2, 0.1); + this->addFloatProp("lacunarity", "Lacunarity", 2.0, 0.1, 2, 0.1); - const source = ` + auto source = R""""( float wrapAround(float value, float upperBound) { return mod((value + upperBound - 1.0), upperBound); } @@ -59,8 +61,7 @@ export class ValueNoiseFractalSum extends GpuDesignerNode { return vec4(color,1.0); } - `; + )""""; - this.buildShader(source); - } + this->setShaderSource(source); } diff --git a/src/lib/library/v2/warpv2.ts b/src/texturelab/libraries/v2/warpv2.cpp similarity index 67% rename from src/lib/library/v2/warpv2.ts rename to src/texturelab/libraries/v2/warpv2.cpp index 78913c81..1ab23700 100644 --- a/src/lib/library/v2/warpv2.ts +++ b/src/texturelab/libraries/v2/warpv2.cpp @@ -1,16 +1,18 @@ -import { GpuDesignerNode } from "../../designer/gpudesignernode"; +#include "../../models.h" +#include "../../props.h" +#include "../libv2.h" -export class WarpNodeV2 extends GpuDesignerNode { - public init() { - this.title = "Warp"; +void WarpNodeV2::init() +{ + this->title = "Warp"; - this.addInput("inputImage"); - this.addInput("height"); + this->addInput("inputImage"); + this->addInput("height"); - this.addFloatProperty("intensity", "Intensity", 0.1, -1.0, 1.0, 0.01); + this->addFloatProp("intensity", "Intensity", 0.1, -1.0, 1.0, 0.01); - // calculates normal, then warps uv by it - const source = ` + // calculates normal, then warps uv by it + auto source = R""""( vec2 calcSlope(vec2 uv) { vec3 sl = vec3(0.0,0.0,0.0); @@ -35,8 +37,7 @@ export class WarpNodeV2 extends GpuDesignerNode { return color; } - `; + )""""; - this.buildShader(source); - } -} + this->setShaderSource(source); +} \ No newline at end of file diff --git a/src/texturelab/libraries/v3/ambientocclusion.cpp b/src/texturelab/libraries/v3/ambientocclusion.cpp new file mode 100644 index 00000000..d6e81471 --- /dev/null +++ b/src/texturelab/libraries/v3/ambientocclusion.cpp @@ -0,0 +1,88 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Screen-space-style ambient occlusion over a heightfield. +// +// Vogel, "A better way to construct the sunflower head," Math. Biosciences 1979 +// (golden-angle disc — the sample distribution used below) +// https://blog.demofox.org/2017/05/29/when-random-numbers-are-too-random-low-discrepancy-sequences/ +// +// Occlusion is estimated as the mean *elevation angle* of the neighbourhood, +// not the mean height difference: sin(atan(dh/dist)) is bounded in [0,1] per +// sample and correctly scale-aware, so a bump close by occludes far more than +// the same bump at the edge of the radius. +// +// For the high-quality variant (mip-pyramid cone taps, progressive +// accumulation, two-scale detail, bilateral denoise) see fastao.cpp. +void AmbientOcclusionNode::init() +{ + this->title = "Ambient Occlusion"; + this->addInput("height"); + + this->addFloatProp("radius", "Radius", 0.05, 0.001, 1.0, 0.005); + this->addIntProp("samples", "Samples", 100, 4, 128, 4); + this->addFloatProp("intensity", "Intensity", 1.0, 0.1, 5.0, 0.1); + this->addFloatProp("bias", "Bias", 0.02, 0.0, 0.5, 0.005); + this->addFloatProp("height_scale", "Height Scale", 1.0, 0.01, 4.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + float centerH = texture(height, uv).r * prop_height_scale; + float sampleCount = float(prop_samples); + float radius = prop_radius; + + // Radius is expressed in UV, so on a non-square texture the + // sampling disc would be an ellipse in texel space. Scale x to + // keep it circular; a no-op when the texture is square. + vec2 aspect = vec2(_textureSize.y / _textureSize.x, 1.0); + + // A single random per pixel rotates the whole spiral. This + // decorrelates neighbouring pixels without reintroducing the + // clumping that per-sample white noise causes. + float rot = randomFloat(0) * 6.28318530718; + + float occlusion = 0.0; + float weightSum = 0.0; + + for (int i = 0; i < prop_samples; i++) + { + // Vogel disc: golden-angle spiral. Deterministic and evenly + // stratified, so variance falls off far faster than with the + // white-noise angle/distance pair it replaces. + float t = (float(i) + 0.5) / sampleCount; + float angle = float(i) * 2.39996322973 + rot; + // sqrt(t) makes the taps uniform over the disc *area* rather + // than over the radius, which otherwise over-samples the centre. + float dist = sqrt(t) * radius; + + vec2 dir = vec2(cos(angle), sin(angle)) * aspect; + + // fract() wraps the tap: node textures are CLAMP_TO_EDGE, so + // without this the AO breaks the seam on a tiling texture. + float sampleH = + texture(height, fract(uv + dir * dist)).r * prop_height_scale; + + float dh = sampleH - centerH; + + // sin(atan(dh / dist)) — the elevation angle of this neighbour. + float sinElev = dh * inversesqrt(dh * dh + dist * dist); + + // Smooth window over the disc edge, so a feature crossing the + // radius boundary fades in instead of popping. + float w = 1.0 - t; + + occlusion += clamp(sinElev - prop_bias, 0.0, 1.0) * w; + weightSum += w; + } + + occlusion = occlusion / max(weightSum, 0.0001); + occlusion = 1.0 - clamp(occlusion * prop_intensity, 0.0, 1.0); + + return vec4(vec3(occlusion), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/atlas2x2.cpp b/src/texturelab/libraries/v3/atlas2x2.cpp new file mode 100644 index 00000000..6db26a40 --- /dev/null +++ b/src/texturelab/libraries/v3/atlas2x2.cpp @@ -0,0 +1,38 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Packs four textures into a 2x2 atlas. Each input is scaled down to fill +// its own quadrant. Unconnected quadrants show the background color. +void Atlas2x2Node::init() +{ + this->title = "Atlas 2x2"; + + this->addInput("top_left"); + this->addInput("top_right"); + this->addInput("bottom_left"); + this->addInput("bottom_right"); + + this->addColorProp("background", "Background Color", QColor(0, 0, 0)); + + auto source = R""""( + vec4 process(vec2 uv) + { + // uv.y == 1.0 is the top of the image + vec2 cell = step(vec2(0.5), uv); + vec2 localUV = fract(uv * 2.0); + + if (cell.y > 0.5) { + if (cell.x < 0.5) + return top_left_connected ? texture(top_left, localUV) : prop_background; + return top_right_connected ? texture(top_right, localUV) : prop_background; + } + + if (cell.x < 0.5) + return bottom_left_connected ? texture(bottom_left, localUV) : prop_background; + return bottom_right_connected ? texture(bottom_right, localUV) : prop_background; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/autolevels.cpp b/src/texturelab/libraries/v3/autolevels.cpp new file mode 100644 index 00000000..2d93f71c --- /dev/null +++ b/src/texturelab/libraries/v3/autolevels.cpp @@ -0,0 +1,267 @@ +#include "../../graphics/noderenderer.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +#include +#include + +// GPU min/max reduction via progressive 2×2 downsampling. +// Pipeline: +// 1. Init pass — convert full-res source to luma (per_channel=false) +// or keep RGB (per_channel=true) +// 2. Min chain — halve resolution each step, computing component-wise min +// 3. Max chain — same for max +// Both chains converge to a 1×1 texture storing the global min/max. +// 4. Apply pass — remap source using the 1×1 min/max values + gamma. +// +// This gives a true per-frame automatic levels without any CPU readback. +// Follow with a Curve or Map Range node for further tonal shaping. + +// ============================================================================ +// AutoLevelsRenderData +// ============================================================================ +struct AutoLevelsRenderData : public NodeRenderData { + float gamma = 1.0f; + bool perChannel = false; +}; + +// ============================================================================ +// AutoLevelsRenderer +// ============================================================================ +class AutoLevelsRenderer : public NodeTextureRenderer { +public: + void render(NodeRenderContext& ctx, const NodeRenderData& baseData) override + { + auto& data = static_cast(baseData); + auto gl = ctx.gl; + auto cache = ctx.cache; + int w = ctx.textureWidth; + int h = ctx.textureHeight; + + if (ctx.inputs.isEmpty() || ctx.inputs[0].textureId == 0) { + cache->bindFboToTexture(ctx.outputTextureId); + gl->glViewport(0, 0, w, h); + gl->glClearColor(0, 0, 0, 1); + gl->glClear(GL_COLOR_BUFFER_BIT); + return; + } + + GLuint srcTex = ctx.inputs[0].textureId; + GLuint initSh = cache->getOrCompileShader("al_init", stdVert(), initFrag()); + GLuint minSh = cache->getOrCompileShader("al_min", stdVert(), reduceFrag(true)); + GLuint maxSh = cache->getOrCompileShader("al_max", stdVert(), reduceFrag(false)); + GLuint applySh = cache->getOrCompileShader("al_apply", stdVert(), applyFrag()); + + // --- 1. Init pass (full resolution) --- + // per_channel=false → store luma in RGB so all 3 channels reduce identically + // per_channel=true → keep RGB as-is + GLuint initTex = cache->acquireTexture(w, h); + cache->bindFboToTexture(initTex); + ctx.useShader(initSh); + ctx.bindTexture(initSh, "u_src", srcTex, 0); + gl->glUniform1i( + gl->glGetUniformLocation(initSh, "u_per_channel"), + data.perChannel ? 1 : 0); + ctx.drawQuad(); + + // --- 2 & 3. Min/Max reduction chains --- + GLuint minTex = reduce(ctx, cache, gl, initTex, w, h, minSh); + GLuint maxTex = reduce(ctx, cache, gl, initTex, w, h, maxSh); + + cache->releaseTexture(initTex); + + // --- 4. Apply pass (full resolution) --- + cache->bindFboToTexture(ctx.outputTextureId); + ctx.useShader(applySh); + ctx.bindTexture(applySh, "u_src", srcTex, 0); + ctx.bindTexture(applySh, "u_min", minTex, 1); + ctx.bindTexture(applySh, "u_max", maxTex, 2); + gl->glUniform1f( + gl->glGetUniformLocation(applySh, "u_gamma"), + data.gamma); + gl->glUniform1i( + gl->glGetUniformLocation(applySh, "u_per_channel"), + data.perChannel ? 1 : 0); + ctx.drawQuad(); + + cache->releaseTexture(minTex); + cache->releaseTexture(maxTex); + } + +private: + // Progressively halve the texture, applying the given reduce shader + // (min or max) at each step. Returns a 1×1 texture the caller must release. + static GLuint reduce(NodeRenderContext& ctx, RenderResourceCache* cache, + QOpenGLFunctions_3_2_Core* gl, + GLuint srcTex, int w, int h, GLuint shader) + { + GLuint current = srcTex; + int cw = w, ch = h; + bool ownsCurrent = false; + + while (cw > 1 || ch > 1) { + int nextW = std::max(cw / 2, 1); + int nextH = std::max(ch / 2, 1); + GLuint next = cache->acquireTexture(nextW, nextH); + + // Bind FBO manually — we don't use ctx.useShader here because + // it would force the viewport to the node's final output size. + cache->bindFboToTexture(next); + gl->glViewport(0, 0, nextW, nextH); + gl->glUseProgram(shader); + + // Tell the shader how big the SOURCE texture is so it can + // compute the correct half-texel offset for 2×2 sampling. + gl->glUniform2f( + gl->glGetUniformLocation(shader, "u_src_size"), + float(cw), float(ch)); + + ctx.bindTexture(shader, "u_src", current, 0); + ctx.drawQuad(); + + if (ownsCurrent) cache->releaseTexture(current); + current = next; + ownsCurrent = true; + cw = nextW; + ch = nextH; + } + + return current; // 1×1 result, caller must release + } + + static QString stdVert() { return RenderResourceCache::standardVertexSource(); } + + // 1. Init: convert source to values suitable for reduction + static QString initFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_src; + uniform int u_per_channel; + + void main() + { + vec4 c = texture(u_src, v_texCoord); + if (u_per_channel == 1) { + // Keep RGB — reduce each channel independently + fragColor = vec4(c.rgb, 1.0); + } else { + // Replicate luma to RGB so all channels carry the same value; + // the min/max of any channel then equals the global luma min/max. + float luma = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722)); + fragColor = vec4(luma, luma, luma, 1.0); + } + } + )""""; + } + + // 2/3. Reduction: component-wise min or max over a 2×2 block of the source + static QString reduceFrag(bool isMin) + { + QString op = isMin ? "min" : "max"; + return QString(R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_src; + uniform vec2 u_src_size; // size of the INPUT texture for this pass + + void main() + { + // Half-texel offset in source-texture space centres the 4 samples + // on the 2×2 block that maps to this output pixel. + vec2 ht = 0.5 / u_src_size; + vec2 uv = v_texCoord; + + vec4 s00 = texture(u_src, uv + vec2(-ht.x, -ht.y)); + vec4 s10 = texture(u_src, uv + vec2( ht.x, -ht.y)); + vec4 s01 = texture(u_src, uv + vec2(-ht.x, ht.y)); + vec4 s11 = texture(u_src, uv + vec2( ht.x, ht.y)); + + fragColor = %1(%1(s00, s10), %1(s01, s11)); + } + )"""").arg(op); + } + + // 4. Apply: remap using the 1×1 min/max textures + gamma + static QString applyFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_src; + uniform sampler2D u_min; + uniform sampler2D u_max; + uniform float u_gamma; + uniform int u_per_channel; + + float remap(float v, float lo, float hi) { + float range = max(hi - lo, 0.001); + float t = clamp((v - lo) / range, 0.0, 1.0); + // Gamma > 1 brightens midtones; gamma < 1 darkens. + return (u_gamma > 0.999 && u_gamma < 1.001) + ? t + : pow(t, 1.0 / max(u_gamma, 0.001)); + } + + void main() + { + vec4 src = texture(u_src, v_texCoord); + // 1×1 textures — sample at the centre; exact UV doesn't matter. + vec4 lo = texture(u_min, vec2(0.5)); + vec4 hi = texture(u_max, vec2(0.5)); + + vec3 result; + if (u_per_channel == 1) { + // Independent per-channel remap (can shift colour balance) + result.r = remap(src.r, lo.r, hi.r); + result.g = remap(src.g, lo.g, hi.g); + result.b = remap(src.b, lo.b, hi.b); + } else { + // Luminance-preserving remap: scale RGB uniformly so hue is kept + float luma = dot(src.rgb, vec3(0.2126, 0.7152, 0.0722)); + float newLuma = remap(luma, lo.r, hi.r); + float scale = (luma > 0.0001) ? newLuma / luma : 0.0; + result = clamp(src.rgb * scale, 0.0, 1.0); + } + + fragColor = vec4(result, src.a); + } + )""""; + } +}; + +// ============================================================================ +// AutoLevelsNode +// ============================================================================ +void AutoLevelsNode::init() +{ + this->title = "Auto Levels"; + + this->addInput("image"); + + this->addFloatProp("gamma", "Midpoint Gamma", 1.0, 0.1, 4.0, 0.05); + this->addBoolProp ("per_channel", "Per Channel", false); +} + +std::shared_ptr AutoLevelsNode::createRenderer() +{ + return std::make_shared(); +} + +std::shared_ptr AutoLevelsNode::createRenderData() +{ + auto data = std::make_shared(); + if (auto* p = dynamic_cast(getProp("gamma"))) + data->gamma = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("per_channel"))) + data->perChannel = p->value; + return data; +} diff --git a/src/texturelab/libraries/v3/bevelv2.cpp b/src/texturelab/libraries/v3/bevelv2.cpp new file mode 100644 index 00000000..d7dd5a6b --- /dev/null +++ b/src/texturelab/libraries/v3/bevelv2.cpp @@ -0,0 +1,302 @@ +#include "../../graphics/noderenderer.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +#include +#include +#include + +// ============================================================================ +// BevelV2RenderData — parameters passed to the render thread +// ============================================================================ + +struct BevelV2RenderData : public NodeRenderData { + float distance = 50.0f; + float threshold = 0.5f; + int shape = 0; // 0=Linear, 1=Round, 2=Smooth + bool invert = false; + bool scaleInvariant = true; +}; + +// ============================================================================ +// BevelV2Renderer — JFA-based GPU bevel implementation +// ============================================================================ + +class BevelV2Renderer : public NodeTextureRenderer { +public: + void render(NodeRenderContext& ctx, const NodeRenderData& baseData) override + { + auto& data = static_cast(baseData); + auto gl = ctx.gl; + auto cache = ctx.cache; + + int w = ctx.textureWidth; + int h = ctx.textureHeight; + + // No input connected — output black + if (ctx.inputs.isEmpty() || ctx.inputs[0].textureId == 0) { + cache->bindFboToTexture(ctx.outputTextureId); + gl->glViewport(0, 0, w, h); + gl->glClearColor(0, 0, 0, 1); + gl->glClear(GL_COLOR_BUFFER_BIT); + return; + } + + // Compile shaders (cached after first call) + GLuint seedShader = + cache->getOrCompileShader("jfa_seed", standardVert(), seedFrag()); + GLuint jfaShader = + cache->getOrCompileShader("jfa_step", standardVert(), jfaFrag()); + GLuint bevelShader = + cache->getOrCompileShader("jfa_bevel", standardVert(), bevelFrag()); + + // Acquire two intermediate textures for ping-pong + GLuint texA = cache->acquireTexture(w, h); + GLuint texB = cache->acquireTexture(w, h); + + // --- Seed initialization --- + cache->bindFboToTexture(texA); + ctx.useShader(seedShader); + ctx.bindTexture(seedShader, "image", ctx.inputs[0].textureId, 0); + gl->glUniform1f(gl->glGetUniformLocation(seedShader, "u_threshold"), + data.threshold); + gl->glUniform1i(gl->glGetUniformLocation(seedShader, "u_invert"), + data.invert ? 1 : 0); + ctx.drawQuad(); + + // --- JFA iteration (ping-pong) --- + int maxDim = std::max(w, h); + int stepSize = maxDim / 2; + + while (stepSize >= 1) { + cache->bindFboToTexture(texB); + ctx.useShader(jfaShader); + ctx.bindTexture(jfaShader, "u_input", texA, 0); + gl->glUniform1i(gl->glGetUniformLocation(jfaShader, "u_stepSize"), + stepSize); + ctx.drawQuad(); + + std::swap(texA, texB); + stepSize /= 2; + } + + // --- Final pass: Distance field → bevel height --- + cache->bindFboToTexture(ctx.outputTextureId); + ctx.useShader(bevelShader); + ctx.bindTexture(bevelShader, "u_jfa", texA, 0); + gl->glUniform1f(gl->glGetUniformLocation(bevelShader, "u_distance"), + data.distance); + gl->glUniform1i(gl->glGetUniformLocation(bevelShader, "u_shape"), + data.shape); + gl->glUniform1i(gl->glGetUniformLocation(bevelShader, "u_invert"), + data.invert ? 1 : 0); + gl->glUniform1i( + gl->glGetUniformLocation(bevelShader, "u_scaleInvariant"), + data.scaleInvariant ? 1 : 0); + ctx.drawQuad(); + } + +private: + static QString standardVert() + { + return RenderResourceCache::standardVertexSource(); + } + + static QString seedFrag() + { + return R""""( + #version 150 core + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D image; + uniform vec2 _textureSize; + uniform float u_threshold; + uniform int u_invert; + + void main() { + vec2 uv = v_texCoord; + float v = texture(image, uv).r; + + // Black pixels (below threshold) are seeds by default — + // JFA spreads distance from them into white regions, so + // white shapes end up raised and black background stays + // flat. `invert` swaps which side is treated as the seed + // (matching the old bevel node, whose two-sided signed + // distance field meant flipping its single "invert" had + // the effect of swapping which side the flat plateau fell + // on, on top of flipping the output polarity below). + bool isSeed = (u_invert == 1) ? (v >= u_threshold) + : (v < u_threshold); + + if (isSeed) + fragColor = vec4(uv, v, 1.0); // Seed: store own UV + else + fragColor = vec4(-1.0, -1.0, v, 0.0); // No seed + } + )""""; + } + + static QString jfaFrag() + { + return R""""( + #version 150 core + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_input; + uniform vec2 _textureSize; + uniform int u_stepSize; + + void main() { + vec2 uv = v_texCoord; + vec2 texel = vec2(1.0) / _textureSize; + vec4 best = texture(u_input, uv); + float bestDist = (best.a < 0.5) ? 9999.0 : length(uv - best.xy); + + // Check 3x3 neighborhood at current step size + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + if (x == 0 && y == 0) continue; + + vec2 offset = vec2(float(x), float(y)) + * float(u_stepSize) * texel; + vec4 neighbor = texture(u_input, uv + offset); + + if (neighbor.a < 0.5) continue; // No seed + + float d = length(uv - neighbor.xy); + if (d < bestDist) { + bestDist = d; + best = neighbor; + } + } + } + + fragColor = best; + } + )""""; + } + + static QString bevelFrag() + { + return R""""( + #version 150 core + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_jfa; + uniform vec2 _textureSize; + uniform float u_distance; + uniform int u_shape; + uniform int u_invert; + uniform int u_scaleInvariant; + + #define SHAPE_LINEAR 0 + #define SHAPE_ROUND 1 + #define SHAPE_SMOOTH 2 + + void main() { + vec2 uv = v_texCoord; + vec4 data = texture(u_jfa, uv); + + if (data.a < 0.5) { + // No nearest seed found + fragColor = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + + // Convert UV-space distance to pixel distance + float dist = length(uv - data.xy) + * max(_textureSize.x, _textureSize.y); + + // Scale-invariant: normalize against texture resolution so + // the bevel width in UV space stays consistent across + // resolutions (512 is the reference size the prop range + // was tuned against). Disabled, `distance` is read as a + // raw pixel count instead — matching the old (v1/v2) Bevel + // node, which always operated in absolute pixel terms. + float pixelDistance = (u_scaleInvariant == 1) + ? u_distance * (max(_textureSize.x, _textureSize.y) / 512.0) + : u_distance; + float t = clamp(dist / pixelDistance, 0.0, 1.0); + + float bevel; + if (u_shape == SHAPE_ROUND) { + // Circular cross-section: quarter-circle falloff + bevel = sqrt(1.0 - (1.0 - t) * (1.0 - t)); + } else if (u_shape == SHAPE_SMOOTH) { + // S-curve: smoothstep for gentle transitions + bevel = smoothstep(0.0, 1.0, t); + } else { + // Linear ramp (default) + bevel = t; + } + + // if (u_invert == 1) + // bevel = 1.0 - bevel; + + fragColor = vec4(vec3(bevel), 1.0); + } + )""""; + } +}; + +// ============================================================================ +// BevelV2Node +// ============================================================================ + +void BevelV2Node::init() +{ + this->title = "Bevel V2"; + this->addInput("image"); + this->addFloatProp("distance", "Distance", 50.0, 0.0, 200.0, 0.5); + this->addFloatProp("threshold", "Threshold", 0.5, 0.0, 1.0, 0.01); + this->addEnumProp("shape", "Shape", {"Linear", "Round", "Smooth"}); + this->addBoolProp("invert", "Invert", false); + this->addBoolProp("scaleInvariant", "Scale Invariant", true); + + // Passthrough shader for initialization (not used during rendering — + // custom renderer handles all passes) + auto source = R""""( + vec4 process(vec2 uv) + { + return texture(image, uv); + } + )""""; + this->setShaderSource(source); +} + +std::shared_ptr BevelV2Node::createRenderer() +{ + return std::make_shared(); +} + +std::shared_ptr BevelV2Node::createRenderData() +{ + auto data = std::make_shared(); + + auto distProp = static_cast(this->getProp("distance")); + if (distProp) + data->distance = distProp->value; + + auto threshProp = static_cast(this->getProp("threshold")); + if (threshProp) + data->threshold = threshProp->value; + + auto shapeProp = static_cast(this->getProp("shape")); + if (shapeProp) + data->shape = shapeProp->index; + + auto invertProp = static_cast(this->getProp("invert")); + if (invertProp) + data->invert = invertProp->value; + + auto scaleInvariantProp = + static_cast(this->getProp("scaleInvariant")); + if (scaleInvariantProp) + data->scaleInvariant = scaleInvariantProp->value; + + return data; +} diff --git a/src/texturelab/libraries/v3/blendv3.cpp b/src/texturelab/libraries/v3/blendv3.cpp new file mode 100644 index 00000000..e9e5cc68 --- /dev/null +++ b/src/texturelab/libraries/v3/blendv3.cpp @@ -0,0 +1,140 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// reference: +// https://ssp.impulsetrain.com/porterduff.html +// https://github.com/dpt/Porter-Duff/blob/master/Porter-Duff.md + +void BlendV3Node::init() +{ + this->title = "Blend"; + + this->addInput("colorA"); // top / foreground + this->addInput("colorB"); // bottom / background + this->addInput("opacity"); // optional mask + + // Indices 0-8 match the v1 blend node for consistency. + // New modes are appended from index 9 onward. + this->addEnumProp("type", "Type", + { + "Multiply", // 0 + "Add", // 1 + "Subtract", // 2 + "Divide", // 3 + "Max", // 4 + "Min", // 5 + "Switch", // 6 (shows colorA — same as v1) + "Overlay", // 7 (fixed: correct 2× factor) + "Screen", // 8 + "Soft Light", // 9 + "Hard Light", // 10 + "Color Dodge", // 11 + "Color Burn", // 12 + "Linear Dodge", // 13 + "Linear Burn", // 14 + "Difference", // 15 + "Exclusion", // 16 + "Normal", // 17 + }); + this->addFloatProp("opacity", "Opacity", 1.0, 0.0, 1.0, 0.01); + + this->setShaderSource(R""""( + vec3 blend_screen(vec3 a, vec3 b) { + return 1.0 - (1.0 - a) * (1.0 - b); + } + + // Standard overlay: base is bottom layer, blend is top layer. + // if base < 0.5: 2*base*blend, else 1 - 2*(1-base)*(1-blend) + // Uses explicit per-channel conditionals to avoid evaluating both + // branches simultaneously, which can produce NaN/Inf with HDR inputs. + vec3 blend_overlay(vec3 base, vec3 blend) { + vec3 dark = 2.0 * base * blend; + vec3 light = 1.0 - 2.0 * (1.0 - base) * (1.0 - blend); + return vec3( + base.r < 0.5 ? dark.r : light.r, + base.g < 0.5 ? dark.g : light.g, + base.b < 0.5 ? dark.b : light.b + ); + } + + // Pegtop / W3C two-case approximation for soft light. + // base = bottom, blend = top (light source). + vec3 blend_soft_light(vec3 base, vec3 blend) { + vec3 dark = 2.0 * base * blend + base * base * (1.0 - 2.0 * blend); + vec3 light = 2.0 * base * (1.0 - blend) + sqrt(clamp(base, 0.0, 1.0)) * (2.0 * blend - 1.0); + return vec3( + blend.r < 0.5 ? dark.r : light.r, + blend.g < 0.5 ? dark.g : light.g, + blend.b < 0.5 ? dark.b : light.b + ); + } + + // Hard light = overlay with layers swapped. + vec3 blend_hard_light(vec3 base, vec3 blend) { + return blend_overlay(blend, base); + } + + vec3 blend_color_dodge(vec3 base, vec3 blend) { + return clamp(base / max(1.0 - blend, vec3(1e-4)), 0.0, 1.0); + } + + vec3 blend_color_burn(vec3 base, vec3 blend) { + return clamp(1.0 - (1.0 - base) / max(blend, vec3(1e-4)), 0.0, 1.0); + } + + vec4 process(vec2 uv) + { + float finalOpacity = prop_opacity; + if (opacity_connected) + finalOpacity *= texture(opacity, uv).r; + + vec4 colA = texture(colorA, uv); // top / foreground + vec4 colB = texture(colorB, uv); // bottom / background + vec3 result = colB.rgb; + + if (prop_type == 0) // Multiply + result = colA.rgb * colB.rgb; + else if (prop_type == 1) // Add + result = colA.rgb + colB.rgb; + else if (prop_type == 2) // Subtract + result = colB.rgb - colA.rgb; + else if (prop_type == 3) // Divide + result = colB.rgb / max(colA.rgb, vec3(1e-4)); + else if (prop_type == 4) // Max + result = max(colA.rgb, colB.rgb); + else if (prop_type == 5) // Min + result = min(colA.rgb, colB.rgb); + else if (prop_type == 6) // Switch (show colorA) + result = colA.rgb; + else if (prop_type == 7) // Overlay (fixed) + result = blend_overlay(colB.rgb, colA.rgb); + else if (prop_type == 8) // Screen + result = blend_screen(colA.rgb, colB.rgb); + else if (prop_type == 9) // Soft Light + result = blend_soft_light(colB.rgb, colA.rgb); + else if (prop_type == 10) // Hard Light + result = blend_hard_light(colB.rgb, colA.rgb); + else if (prop_type == 11) // Color Dodge + result = blend_color_dodge(colB.rgb, colA.rgb); + else if (prop_type == 12) // Color Burn + result = blend_color_burn(colB.rgb, colA.rgb); + else if (prop_type == 13) // Linear Dodge (clamped Add) + result = clamp(colA.rgb + colB.rgb, 0.0, 1.0); + else if (prop_type == 14) // Linear Burn + result = clamp(colA.rgb + colB.rgb - 1.0, 0.0, 1.0); + else if (prop_type == 15) // Difference + result = abs(colA.rgb - colB.rgb); + else if (prop_type == 16) // Exclusion + result = colA.rgb + colB.rgb - 2.0 * colA.rgb * colB.rgb; + else // Normal (17) + result = colA.rgb; + + // Factor in the foreground's own alpha so transparent regions + // of the overlay let the background show through. + float blendFactor = finalOpacity * colA.a; + float outAlpha = blendFactor + colB.a * (1.0 - blendFactor); + return vec4(mix(colB.rgb, result, blendFactor), outAlpha); + } + )""""); +} diff --git a/src/texturelab/libraries/v3/blurhq.cpp b/src/texturelab/libraries/v3/blurhq.cpp new file mode 100644 index 00000000..42b9c668 --- /dev/null +++ b/src/texturelab/libraries/v3/blurhq.cpp @@ -0,0 +1,181 @@ +#include "../../graphics/noderenderer.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +#include +#include +#include + +// https://learnopengl.com/Advanced-Lighting/Bloom (separated Gaussian in GLSL) +// Wells, "Efficient Synthesis of Gaussian Filters by Cascaded Uniform Filters," +// IEEE PAMI 1986 +// True separated Gaussian blur: two 1D passes (H then V) give O(2N) samples +// vs O(N²) for a 2D kernel — making large-radius blurs practical on the GPU. +// Use in preference to the V2 Blur node whenever quality matters. + +// ============================================================================ +// BlurHQRenderData +// ============================================================================ +struct BlurHQRenderData : public NodeRenderData { + float radius = 8.0f; +}; + +// ============================================================================ +// BlurHQRenderer +// ============================================================================ +class BlurHQRenderer : public NodeTextureRenderer { +public: + void render(NodeRenderContext& ctx, const NodeRenderData& baseData) override + { + auto& data = static_cast(baseData); + auto gl = ctx.gl; + auto cache = ctx.cache; + int w = ctx.textureWidth; + int h = ctx.textureHeight; + + // No input — output black + if (ctx.inputs.isEmpty() || ctx.inputs[0].textureId == 0) { + cache->bindFboToTexture(ctx.outputTextureId); + gl->glViewport(0, 0, w, h); + gl->glClearColor(0, 0, 0, 1); + gl->glClear(GL_COLOR_BUFFER_BIT); + return; + } + + GLuint hShader = cache->getOrCompileShader( + "blurhq_horizontal", + RenderResourceCache::standardVertexSource(), + horizontalFrag()); + GLuint vShader = cache->getOrCompileShader( + "blurhq_vertical", + RenderResourceCache::standardVertexSource(), + verticalFrag()); + + GLuint intermediate = cache->acquireTexture(w, h); + GLuint inputTex = ctx.inputs[0].textureId; + + { + // GL_LINEAR is required for the bilinear tap trick in the shaders — + // sampling at fractional offsets must interpolate rather than snap. + // The guards put GL_NEAREST back on the way out: the input texture + // belongs to the node upstream and outlives this render. + ScopedTextureParams intermediateParams(gl, intermediate, GL_LINEAR, + GL_LINEAR); + ScopedTextureParams inputParams(gl, inputTex, GL_LINEAR, GL_LINEAR); + + // --- Pass 1: horizontal blur --- + cache->bindFboToTexture(intermediate); + ctx.useShader(hShader); + ctx.bindTexture(hShader, "u_image", inputTex, 0); + gl->glUniform1f( + gl->glGetUniformLocation(hShader, "u_radius"), data.radius); + gl->glUniform2f( + gl->glGetUniformLocation(hShader, "_textureSize"), + float(w), float(h)); + ctx.drawQuad(); + + // --- Pass 2: vertical blur --- + cache->bindFboToTexture(ctx.outputTextureId); + ctx.useShader(vShader); + ctx.bindTexture(vShader, "u_image", intermediate, 0); + gl->glUniform1f( + gl->glGetUniformLocation(vShader, "u_radius"), data.radius); + gl->glUniform2f( + gl->glGetUniformLocation(vShader, "_textureSize"), + float(w), float(h)); + ctx.drawQuad(); + } + + cache->releaseTexture(intermediate); + } + +private: + // Shared Gaussian sampling code — axis is injected per-pass. + // + // Uses the bilinear tap trick: instead of sampling at each integer pixel + // offset i, adjacent taps i and i+1 are collapsed into a single fetch at + // the Gaussian-weighted midpoint between them. Hardware bilinear filtering + // then delivers the exact weighted average in one sample, halving fetch + // count and eliminating the discrete-step banding that arises from + // integer-only sampling on smooth gradients. + static QString gaussianBody(const QString& axis) + { + return QString(R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_image; + uniform vec2 _textureSize; + uniform float u_radius; + + void main() + { + vec2 uv = v_texCoord; + vec2 step = 1.0 / _textureSize; + + // Scale radius by resolution so the blur covers the same + // visual proportion regardless of texture size (512 = reference). + float pixelRadius = u_radius * (_textureSize.x / 512.0); + float sigma = max(pixelRadius / 3.0, 0.001); + float twoSigSq = 2.0 * sigma * sigma; + + // Center tap: G(0) = 1, no offset needed. + vec4 result = texture(u_image, uv); + float totalW = 1.0; + + int iRadius = int(ceil(pixelRadius)); + + // Bilinear tap trick: pair taps i and i+1, sample once at the + // weighted midpoint. GL_LINEAR on u_image makes the hardware + // perform the blend. Use float(i)*float(i) to avoid any + // potential int overflow at large radii. + for (int i = 1; i <= iRadius; i += 2) { + float fi = float(i); + float w0 = exp(-(fi * fi) / twoSigSq); + float w1 = (i + 1 <= iRadius) + ? exp(-((fi + 1.0) * (fi + 1.0)) / twoSigSq) + : 0.0; + float w = w0 + w1; + float offset = fi + w1 / w; + + vec2 o = %1 * offset * step; + result += texture(u_image, fract(uv + o)) * w; + result += texture(u_image, fract(uv - o)) * w; + totalW += 2.0 * w; + } + + fragColor = result / max(totalW, 0.0001); + } + )"""").arg(axis); + } + + static QString horizontalFrag() { return gaussianBody("vec2(1.0, 0.0)"); } + static QString verticalFrag() { return gaussianBody("vec2(0.0, 1.0)"); } +}; + +// ============================================================================ +// BlurHQNode +// ============================================================================ +void BlurHQNode::init() +{ + this->title = "Blur HQ"; + + this->addInput("image"); + + this->addFloatProp("radius", "Radius", 8.0, 0.5, 128.0, 0.5); +} + +std::shared_ptr BlurHQNode::createRenderer() +{ + return std::make_shared(); +} + +std::shared_ptr BlurHQNode::createRenderData() +{ + auto data = std::make_shared(); + auto* prop = dynamic_cast(getProp("radius")); + if (prop) data->radius = static_cast(prop->value); + return data; +} diff --git a/src/texturelab/libraries/v3/bricks2.cpp b/src/texturelab/libraries/v3/bricks2.cpp new file mode 100644 index 00000000..a90084ec --- /dev/null +++ b/src/texturelab/libraries/v3/bricks2.cpp @@ -0,0 +1,264 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void Bricks2Node::init() +{ + this->title = "Bricks 2"; + + this->addInput("mortarMap"); + this->addInput("bevelMap"); + + this->addEnumProp("pattern", "Pattern", + {"Running Bond", "Stack Bond", "Herringbone", + "Basket Weave"}); + + this->addIntProp("rows", "Rows", 6, 1, 20, 1); + this->addIntProp("columns", "Columns", 6, 1, 20, 1); + this->addFloatProp("brickAspect", "Brick Aspect Ratio", 2.0, 0.5, 4.0, 0.1); + this->addFloatProp("offset", "Offset", 0.5, 0, 1, 0.1); + + // mortar + auto mortarProps = this->createGroup("Mortar"); + mortarProps->collapsed = false; + mortarProps->add( + this->addFloatProp("mortarWidth", "Mortar Width", 0.08, 0, 0.5, 0.01)); + mortarProps->add(this->addFloatProp("mortarMapStrength", + "Mortar Map Strength", 0.5, 0, 1, + 0.01)); + + // shape + auto shapeProps = this->createGroup("Shape"); + shapeProps->collapsed = false; + shapeProps->add(this->addFloatProp("shapeVariance", "Shape Variance", 0.0, + 0, 1, 0.01)); + shapeProps->add( + this->addFloatProp("roundness", "Roundness", 0.0, 0, 1, 0.01)); + + // bevel + auto bevelProps = this->createGroup("Bevel"); + bevelProps->collapsed = false; + bevelProps->add( + this->addFloatProp("bevelAmount", "Bevel Amount", 0.0, 0, 1, 0.01)); + bevelProps->add(this->addFloatProp("bevelMapStrength", + "Bevel Map Strength", 1.0, 0, 1, 0.01)); + + // height + auto heightProps = this->createGroup("Height"); + heightProps->collapsed = false; + heightProps->add( + this->addFloatProp("heightMin", "Height Min", 0.0, 0, 1, 0.05)); + heightProps->add( + this->addFloatProp("heightMax", "Height Max", 1.0, 0, 1, 0.05)); + heightProps->add( + this->addFloatProp("heightBalance", "Height Balance", 1.0, 0, 1, 0.05)); + heightProps->add( + this->addFloatProp("heightVariance", "Height Variance", 0, 0, 1, 0.05)); + + auto source = R""""( + // ===================== height variation ===================== + float calculateHeight(vec2 brickId) + { + float heightMin = prop_heightMin; + float heightMax = prop_heightMax; + float heightBalance = prop_heightBalance; + float heightVariance = prop_heightVariance; + + float balRand = _rand(vec2(_seed) + brickId * vec2(0.01)); + if (balRand > heightBalance) { + return 1.0; + } + + float randVariance = + _rand(vec2(_seed) + (brickId + vec2(1)) * vec2(0.01)); + randVariance *= heightVariance; + + float range = (heightMax - heightMin); + float height = heightMax - range * randVariance; + + return height; + } + + // ===================== brick cell solver ===================== + // localUV: position within the brick's bounding cell (0..1) + // brickId: stable per-brick id used for hashing + // brickDim: relative (width,height) proportions of the brick, + // used to give roundness/bevel the correct aspect ratio + struct CellInfo { + vec2 localUV; + vec2 brickId; + vec2 brickDim; + }; + + // Running Bond (staggered rows) and Stack Bond (no stagger) + CellInfo solveOrthoBond(vec2 uv, bool stagger) + { + vec2 tileSize = vec2(prop_columns, prop_rows); + vec2 pos = uv * tileSize; + + if (stagger) { + float xOffset = 0.0; + if (fract(pos.y * 0.5) > 0.5) { + xOffset = prop_offset; + } + pos.x += xOffset; + } + + vec2 brickId = floor(pos); + + // wrap around x so the hash matches the brick this one + // continues into on the opposite edge + if (brickId.x > tileSize.x - 1.0) + brickId.x = 0.0; + + CellInfo c; + c.localUV = fract(pos); + c.brickId = brickId; + c.brickDim = vec2(prop_brickAspect, 1.0); + return c; + } + + // Herringbone and Basket Weave share an LxL "weave" grid where L is + // the (rounded) brick aspect ratio. Each weave cell is either filled + // with L stacked horizontal bricks or L side-by-side vertical + // bricks, alternating in a checkerboard. Herringbone additionally + // staggers each row/column by its own index, producing the + // characteristic zig-zag. + CellInfo solveWeave(vec2 uv, bool herringbone) + { + vec2 tileSize = vec2(prop_columns, prop_rows); + vec2 pos = uv * tileSize; + + float L = max(1.0, floor(prop_brickAspect + 0.5)); + + vec2 weaveId = floor(pos / L); + vec2 q = pos - weaveId * L; + + bool vertical = mod(weaveId.x + weaveId.y, 2.0) > 0.5; + float stagger = herringbone ? 1.0 : 0.0; + + CellInfo c; + + if (!vertical) { + // L horizontal (L x 1) bricks stacked vertically + float row = floor(q.y); + float shiftedX = mod(pos.x + row * stagger, L); + + c.localUV = vec2(shiftedX / L, fract(pos.y)); + c.brickId = + vec2(floor((pos.x + row * stagger) / L), floor(pos.y)); + c.brickDim = vec2(L, 1.0); + } + else { + // L vertical (1 x L) bricks side by side + float col = floor(q.x); + float shiftedY = mod(pos.y + col * stagger, L); + + c.localUV = vec2(fract(pos.x), shiftedY / L); + c.brickId = + vec2(floor(pos.x), floor((pos.y + col * stagger) / L)); + c.brickDim = vec2(1.0, L); + } + + return c; + } + + // ===================== shape ===================== + // per-edge jitter amounts in [-1,1]: x=left, y=right, z=bottom, w=top + vec4 brickEdgeJitter(vec2 brickId) + { + vec4 j; + j.x = _rand(vec2(_seed) + brickId * vec2(0.0123) + vec2(11.0, 3.0)); + j.y = _rand(vec2(_seed) + brickId * vec2(0.0123) + vec2(23.0, 7.0)); + j.z = + _rand(vec2(_seed) + brickId * vec2(0.0123) + vec2(37.0, 17.0)); + j.w = + _rand(vec2(_seed) + brickId * vec2(0.0123) + vec2(51.0, 29.0)); + return j * 2.0 - 1.0; + } + + // rounded box SDF (centered at origin, half-extents b, corner radius r) + float sdRoundBox(vec2 p, vec2 b, float r) + { + vec2 q = abs(p) - b + r; + return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - r; + } + + vec4 process(vec2 uv) + { + CellInfo c; + if (prop_pattern == 0) + c = solveOrthoBond(uv, true); // Running Bond + else if (prop_pattern == 1) + c = solveOrthoBond(uv, false); // Stack Bond + else if (prop_pattern == 2) + c = solveWeave(uv, true); // Herringbone + else + c = solveWeave(uv, false); // Basket Weave + + // mortar width: uniform + optional per-pixel map + float mortarW = prop_mortarWidth; + if (mortarMap_connected) { + float m = texture(mortarMap, uv).r; + mortarW += (m - 0.5) * prop_mortarMapStrength; + } + mortarW = clamp(mortarW, 0.0, 0.45); + + // non-uniform brick shapes: jitter each edge independently, + // capped so edges never cross into the mortar of the + // neighboring brick + vec4 jitter = brickEdgeJitter(c.brickId); + float jitterAmount = prop_shapeVariance * mortarW * 0.9; + + float left = mortarW + jitter.x * jitterAmount; + float right = mortarW + jitter.y * jitterAmount; + float bottom = mortarW + jitter.z * jitterAmount; + float top = mortarW + jitter.w * jitterAmount; + + vec2 boxMin = vec2(left, bottom); + vec2 boxMax = vec2(1.0 - right, 1.0 - top); + vec2 center = (boxMin + boxMax) * 0.5; + vec2 halfExtent = (boxMax - boxMin) * 0.5; + + // scale into the brick's own proportions so roundness/bevel + // are relative to the brick shape, not the grid cell + vec2 p = (c.localUV - center) * c.brickDim; + vec2 b = halfExtent * c.brickDim; + + float shortSide = 2.0 * min(b.x, b.y); + + float radius = clamp(prop_roundness, 0.0, 1.0) * 0.5 * shortSide; + radius = min(radius, min(b.x, b.y)); + + float sdf = sdRoundBox(p, b, radius); + + float mask = sdf <= 0.0 ? 1.0 : 0.0; + + // bevel: darken a band along the inside of each brick edge + float bevelAmt = prop_bevelAmount; + if (bevelMap_connected) { + float bm = texture(bevelMap, uv).r; + bevelAmt *= + mix(1.0, bm, clamp(prop_bevelMapStrength, 0.0, 1.0)); + } + bevelAmt = clamp(bevelAmt, 0.0, 1.0); + + float bevelWidth = bevelAmt * 0.5 * shortSide; + float bevelMix = 1.0; + if (bevelWidth > 0.0001) { + bevelMix = clamp(-sdf / bevelWidth, 0.0, 1.0); + } + + const float bevelFloor = 0.5; + float bevelMultiplier = mix(bevelFloor, 1.0, bevelMix); + + float height = calculateHeight(c.brickId); + + float finalHeight = mask * height * bevelMultiplier; + + return vec4(vec3(finalHeight), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/cellv3.cpp b/src/texturelab/libraries/v3/cellv3.cpp new file mode 100644 index 00000000..61efd978 --- /dev/null +++ b/src/texturelab/libraries/v3/cellv3.cpp @@ -0,0 +1,47 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Voronoi F1 cell node with independent X/Y scale. +// Seamless: grid sizes are rounded to integers so mod()-wrapped cell hashes +// produce identical values at opposite UV boundaries. +void CellV3Node::init() +{ + this->title = "Cell"; + + this->addFloatProp("scale", "Scale", 30.0, 1.0, 256.0, 1.0); + this->addFloatProp("scaleX", "Scale X", 1.0, 0.1, 10.0, 0.1); + this->addFloatProp("scaleY", "Scale Y", 1.0, 0.1, 10.0, 0.1); + this->addBoolProp ("invert", "Invert", false); + this->addFloatProp("entropy", "Order", 0.0, 0.0, 1.0, 0.01); + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 2.0, 0.01); + + this->setShaderSource(R""""( + vec4 process(vec2 uv) + { + float gsX = max(1.0, floor(prop_scale * prop_scaleX + 0.5)); + float gsY = max(1.0, floor(prop_scale * prop_scaleY + 0.5)); + + vec2 scaledUV = vec2(uv.x * gsX, uv.y * gsY); + vec2 i_st = floor(scaledUV); + vec2 f_st = fract(scaledUV); + + vec2 seedOff = vec2(_seed * 0.173, _seed * 0.251); + float m_dist = 1.0; + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 neighbor = vec2(float(x), float(y)); + vec2 wrapped = mod(i_st + neighbor, vec2(gsX, gsY)); + vec2 point = hash22(wrapped + seedOff); + point = mix(point, vec2(0.5), prop_entropy); + vec2 diff = neighbor + point - f_st; + m_dist = min(m_dist, length(diff)); + } + } + + if (prop_invert) m_dist = 1.0 - m_dist; + return vec4(vec3(m_dist) * prop_intensity, 1.0); + } + )""""); +} diff --git a/src/texturelab/libraries/v3/colortomask.cpp b/src/texturelab/libraries/v3/colortomask.cpp new file mode 100644 index 00000000..cc468658 --- /dev/null +++ b/src/texturelab/libraries/v3/colortomask.cpp @@ -0,0 +1,70 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// https://en.wikipedia.org/wiki/HSL_and_HSV +// Converts a target colour range in an input image to a greyscale mask. +// Uses HSL-space distance for perceptually accurate colour matching — far +// more reliable than RGB Euclidean distance. +// Primary use: extract zones from a flat-colour ID map to drive per-material +// Height Blend, roughness or metalness branches. +void ColorToMaskNode::init() +{ + this->title = "Color To Mask"; + + this->addInput("image"); + + this->addColorProp("target_color", "Target Color", QColor(255, 0, 0)); + this->addFloatProp("hue_range", "Hue Range", 1.0, 0.1, 4.0, 0.1); + this->addFloatProp("sat_range", "Sat Range", 0.5, 0.0, 4.0, 0.1); + this->addFloatProp("lum_range", "Lum Range", 0.3, 0.0, 4.0, 0.1); + this->addFloatProp("softness", "Softness", 0.2, 0.01, 1.0, 0.01); + this->addBoolProp ("invert", "Invert", false); + + auto source = R""""( + vec3 rgb2hsl(vec3 c) { + float maxC = max(c.r, max(c.g, c.b)); + float minC = min(c.r, min(c.g, c.b)); + float delta = maxC - minC; + + float h = 0.0; + if (delta > 0.001) { + if (maxC == c.r) h = mod((c.g - c.b) / delta, 6.0); + else if (maxC == c.g) h = (c.b - c.r) / delta + 2.0; + else h = (c.r - c.g) / delta + 4.0; + h /= 6.0; + if (h < 0.0) h += 1.0; + } + float l = (maxC + minC) * 0.5; + float s = (delta < 0.001) ? 0.0 + : delta / (1.0 - abs(2.0 * l - 1.0)); + return vec3(h, s, l); + } + + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.0, 0.0, 0.0, 1.0); + + vec3 col = texture(image, uv).rgb; + vec3 hsl = rgb2hsl(col); + vec3 targetHSL = rgb2hsl(prop_target_color.rgb); + + // Hue distance is circular + float hueDist = abs(hsl.x - targetHSL.x); + hueDist = min(hueDist, 1.0 - hueDist); + + float dist = hueDist * prop_hue_range + + abs(hsl.y - targetHSL.y) * prop_sat_range + + abs(hsl.z - targetHSL.z) * prop_lum_range; + + float mask = 1.0 - smoothstep(0.0, prop_softness, dist); + + if (prop_invert) mask = 1.0 - mask; + + return vec4(vec3(clamp(mask, 0.0, 1.0)), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/curvature.cpp b/src/texturelab/libraries/v3/curvature.cpp new file mode 100644 index 00000000..5a872671 --- /dev/null +++ b/src/texturelab/libraries/v3/curvature.cpp @@ -0,0 +1,97 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void CurvatureNode::init() +{ + this->title = "Curvature"; + this->addInput("height"); + + auto typeProp = this->addEnumProp("type", "Type", + {"Mix", "Sharp", "Medium", "Smooth"}); + typeProp->index = 3; + + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 2.0, 0.1); + this->addFloatProp("angle", "Angle", 0.5, -1.0, 1.0, 0.1); + this->addIntProp("samples", "Samples", 12, 1, 32, 1); + + auto advancedProps = this->createGroup("Advanced"); + advancedProps->add( + this->addFloatProp("sh_c", "Sharp Curvature", 0.5, 0.0, 1.0, 0.1)); + advancedProps->add( + this->addFloatProp("me_c", "Medium Curvature", 1.5, 0.0, 3.0, 0.1)); + advancedProps->add( + this->addFloatProp("sm_c", "Smooth Curvature", 3.0, 0.0, 4.0, 0.1)); + + auto source = R""""( + #define LAPLACIAN_CENTER_WEIGHT 2.0 + #define RADIUS_SCALE 0.01 + #define SHARP_WEIGHT 8.0 + #define MEDIUM_WEIGHT 3.0 + #define SMOOTH_WEIGHT 1.5 + + #define TYPE_MIX 0 + #define TYPE_SHARP 1 + #define TYPE_MEDIUM 2 + #define TYPE_SMOOTH 3 + + float HeightMap(vec2 p) + { + return texture(height, p).x; + } + + float _curveSample(vec2 p, vec2 o) + { + float a = HeightMap(p + o); + float b = HeightMap(p - o); + return -a - b; + } + + float CurvatureMap(vec2 p, float r) + { + float q = float(prop_samples); + float s = r / q; + float H = HeightMap(p) * LAPLACIAN_CENTER_WEIGHT; + float v = 0.0; + + for (float ox = -q; ox < q; ox++) + for (float oy = -q; oy < q; oy++) + { + vec2 o = vec2(ox, oy); + float c = _curveSample(p, o * s); + v += (H + c) * ((r - length(o * s)) / r); + } + + return v / (q * q); + } + + vec4 process(vec2 uv) + { + if (!height_connected) + return vec4(0.0, 0.0, 0.0, 1.0); + + float i = prop_intensity; + float c = 0.0; + + if (prop_type == TYPE_MIX) { + c += CurvatureMap(uv, i * prop_sh_c * RADIUS_SCALE) * SHARP_WEIGHT; + c += CurvatureMap(uv, i * prop_me_c * RADIUS_SCALE) * MEDIUM_WEIGHT; + c += CurvatureMap(uv, i * prop_sm_c * RADIUS_SCALE) * SMOOTH_WEIGHT; + } else if (prop_type == TYPE_SHARP) { + c += CurvatureMap(uv, i * prop_sh_c * RADIUS_SCALE) * SHARP_WEIGHT; + } else if (prop_type == TYPE_MEDIUM) { + c += CurvatureMap(uv, i * prop_me_c * RADIUS_SCALE) * MEDIUM_WEIGHT; + } else if (prop_type == TYPE_SMOOTH) { + c += CurvatureMap(uv, i * prop_sm_c * RADIUS_SCALE) * SMOOTH_WEIGHT; + } + + vec4 color; + color.rgb = vec3(prop_angle + c); + color.a = 1.0; + + return color; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/curvenode.cpp b/src/texturelab/libraries/v3/curvenode.cpp new file mode 100644 index 00000000..c12e4824 --- /dev/null +++ b/src/texturelab/libraries/v3/curvenode.cpp @@ -0,0 +1,20 @@ +#include "curvenode.h" + +void CurveNode::init() +{ + this->title = "Curve"; + this->addInput("image"); + this->addCurveProp("curve", "Curve"); + + auto source = R""""( + vec4 process(vec2 uv) { + vec4 col = texture(image, uv); + float gray = (col.r + col.g + col.b) * 0.3333333; + float mapped = evalCurve(prop_curve, gray); + return vec4(vec3(mapped), col.a); + // return vec4(1.0,0.0,0.0,1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/curvenode.h b/src/texturelab/libraries/v3/curvenode.h new file mode 100644 index 00000000..639f1763 --- /dev/null +++ b/src/texturelab/libraries/v3/curvenode.h @@ -0,0 +1,8 @@ +#pragma once + +#include "../../models.h" + +class CurveNode : public TextureNode { +public: + void init() override; +}; diff --git a/src/texturelab/libraries/v3/directionalscratches.cpp b/src/texturelab/libraries/v3/directionalscratches.cpp new file mode 100644 index 00000000..bad2e8a0 --- /dev/null +++ b/src/texturelab/libraries/v3/directionalscratches.cpp @@ -0,0 +1,124 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Procedural directional scratch / brushed-metal generator. +// Divides the rotated UV space into N horizontal bands; each band has a +// chance to contain ONE scratch with random start position, length, vertical +// offset, thickness and brightness. +// +// Seamless tiling: angle is restricted to a stepped enum of geometrically +// tileable orientations (0°, 45°, 90°, 135°). Each option uses an +// integer-coefficient rotation matrix so that a UV-tile shift translates to +// an integer band-index shift; combined with mod()-wrapped band indices and +// fract()-wrapped scratch coordinates, the pattern tiles perfectly. +void DirectionalScratchesNode::init() +{ + this->title = "Directional Scratches"; + + auto angleProp = this->addEnumProp("angle", "Angle", + {"0° (Horizontal)", "45°", "90° (Vertical)", "135°"}); + angleProp->index = 0; + + this->addIntProp ("count", "Count", 80, 10, 500, 5); + this->addFloatProp("density", "Density", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("length", "Length", 0.6, 0.05, 1.0, 0.01); + this->addFloatProp("length_var", "Length Variance", 0.5, 0.0, 1.0, 0.05); + this->addFloatProp("width", "Width", 0.3, 0.05, 1.0, 0.01); + this->addFloatProp("waviness", "Waviness", 0.0, 0.0, 1.0, 0.01); + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 2.0, 0.05); + this->addIntProp ("layers", "Layers", 1, 1, 4, 1); + + auto source = R""""( + #define PI 3.14159265358979 + + #define ANGLE_0 0 + #define ANGLE_45 1 + #define ANGLE_90 2 + #define ANGLE_135 3 + + // Hash with project-wide seed offset + float scrHash(vec2 p) { + return hash12(p + vec2(_seed * 0.131, _seed * 0.379)); + } + + // Integer-coefficient rotation: preserves the property that a UV + // shift of (1,0) or (0,1) produces an integer rotated-coordinate + // shift, which is what makes the pattern tile perfectly. + // Diagonal angles (45°/135°) come out scaled by √2 — this just means + // diagonal patterns appear √2 denser per UV tile than cardinal ones. + vec2 rotateUV(vec2 uv) { + if (prop_angle == ANGLE_0) return uv; + if (prop_angle == ANGLE_45) return vec2(uv.x + uv.y, uv.y - uv.x); + if (prop_angle == ANGLE_90) return vec2(uv.y, -uv.x); + /* ANGLE_135 */ return vec2(uv.y - uv.x, -uv.x - uv.y); + } + + // One layer of discrete scratch line segments. + // Each band gets at most one scratch with random properties. + float scratchLayer(vec2 uv, float layerIdx) { + vec2 rot = rotateUV(uv); + + float bandCount = float(prop_count); + + // mod() wraps band index to [0, bandCount-1] so band hashes match + // across UV-tile boundaries. Integer-rotation guarantees that + // floor(rot.y * bandCount) shifts by exactly bandCount across one + // UV tile, so the mod is exact. + float band = mod(floor(rot.y * bandCount), bandCount); + float bandFract = fract(rot.y * bandCount); + + // Per-band deterministic random properties + vec2 seed = vec2(band, layerIdx * 113.7); + float rExist = scrHash(seed + vec2(0.11, 0.23)); + float rStartX = scrHash(seed + vec2(0.37, 0.59)); + float rLength = scrHash(seed + vec2(0.71, 0.97)); + float rOffset = scrHash(seed + vec2(1.13, 1.31)); + float rThickness = scrHash(seed + vec2(1.51, 1.79)); + float rIntensity = scrHash(seed + vec2(1.97, 2.13)); + + // Sparsity: only a fraction of bands actually contain a scratch + if (rExist > prop_density) return 0.0; + + // Scratch length, with optional variance pulling shorter + float lenFactor = mix(1.0, rLength, prop_length_var); + float scratchLen = clamp(prop_length * lenFactor, 0.02, 1.0); + + // X-position along the scratch — fract handles tiling automatically + float xLocal = fract(rot.x - rStartX); + if (xLocal > scratchLen) return 0.0; + + // Soft fade at both ends, proportional to length + float fade = min(0.04, scratchLen * 0.25); + float xMask = smoothstep(0.0, fade, xLocal) + * (1.0 - smoothstep(scratchLen - fade, scratchLen, xLocal)); + + // Optional gentle waviness along the scratch + float wave = sin(xLocal * 28.0 + band * 13.7) + * prop_waviness * 0.15; + + // Vertical position within the band + float lineCenter = 0.5 + (rOffset - 0.5) * 0.3 + wave; + + // Line profile across band; width is fraction of band height + float dist = abs(bandFract - lineCenter); + float halfW = prop_width * 0.25 * mix(0.6, 1.0, rThickness); + float line = 1.0 - smoothstep(halfW * 0.5, halfW, dist); + + // Per-scratch brightness variation + return line * xMask * mix(0.4, 1.0, rIntensity); + } + + vec4 process(vec2 uv) + { + float result = 0.0; + for (int i = 0; i < prop_layers; i++) { + // Each layer generates an independent scratch set via the hash + result = max(result, scratchLayer(uv, float(i) + 1.0)); + } + return vec4(vec3(clamp(result * prop_intensity, 0.0, 1.0)), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/distancetransform.cpp b/src/texturelab/libraries/v3/distancetransform.cpp new file mode 100644 index 00000000..0a6f753f --- /dev/null +++ b/src/texturelab/libraries/v3/distancetransform.cpp @@ -0,0 +1,234 @@ +#include "../../graphics/noderenderer.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +#include +#include +#include + +// Rong & Tan, "Jump Flooding in GPU with Applications to Voronoi Diagram and +// Distance Transform," I3D 2006 +// https://bgolus.medium.com/the-quest-for-very-wide-outlines-ba82ed442cd9 +// (Ben Golus — GPU distance transforms, JFA accuracy analysis) +// +// Jump Flooding Algorithm (JFA) Euclidean distance transform. +// Each foreground pixel in the thresholded mask seeds the JFA; subsequent +// passes propagate nearest-seed UVs across the texture in O(log N) passes. +// The final pass converts nearest-seed UV to a normalised distance value. +// +// Reuses the same JFA infrastructure as BevelV2; this node exposes the raw +// distance field rather than a bevel profile, making it a universal primitive +// for soft edge halos, wear gradients and stencil masks. + +// ============================================================================ +// DistanceTransformRenderData +// ============================================================================ +struct DistanceTransformRenderData : public NodeRenderData { + float threshold = 0.5f; + float spread = 0.5f; + bool invert = false; +}; + +// ============================================================================ +// DistanceTransformRenderer +// ============================================================================ +class DistanceTransformRenderer : public NodeTextureRenderer { +public: + void render(NodeRenderContext& ctx, const NodeRenderData& baseData) override + { + auto& data = static_cast(baseData); + auto gl = ctx.gl; + auto cache = ctx.cache; + int w = ctx.textureWidth; + int h = ctx.textureHeight; + + if (ctx.inputs.isEmpty() || ctx.inputs[0].textureId == 0) { + cache->bindFboToTexture(ctx.outputTextureId); + gl->glViewport(0, 0, w, h); + gl->glClearColor(0, 0, 0, 1); + gl->glClear(GL_COLOR_BUFFER_BIT); + return; + } + + GLuint seedShader = cache->getOrCompileShader( + "jfadt_seed", RenderResourceCache::standardVertexSource(), seedFrag()); + GLuint jfaShader = cache->getOrCompileShader( + "jfadt_step", RenderResourceCache::standardVertexSource(), jfaFrag()); + GLuint distShader = cache->getOrCompileShader( + "jfadt_dist", RenderResourceCache::standardVertexSource(), distFrag()); + + GLuint texA = cache->acquireTexture(w, h); + GLuint texB = cache->acquireTexture(w, h); + + // --- Seed pass: foreground pixels write their own UV; background writes (-1,-1) --- + cache->bindFboToTexture(texA); + ctx.useShader(seedShader); + ctx.bindTexture(seedShader, "u_mask", ctx.inputs[0].textureId, 0); + gl->glUniform1f( + gl->glGetUniformLocation(seedShader, "u_threshold"), + data.threshold); + ctx.drawQuad(); + + // --- JFA passes --- + int maxDim = std::max(w, h); + int stepSize = maxDim / 2; + while (stepSize >= 1) { + cache->bindFboToTexture(texB); + ctx.useShader(jfaShader); + ctx.bindTexture(jfaShader, "u_input", texA, 0); + gl->glUniform1i( + gl->glGetUniformLocation(jfaShader, "u_step"), stepSize); + ctx.drawQuad(); + std::swap(texA, texB); + stepSize /= 2; + } + + // --- Distance pass: convert nearest-seed UV to normalised distance --- + cache->bindFboToTexture(ctx.outputTextureId); + ctx.useShader(distShader); + ctx.bindTexture(distShader, "u_jfa", texA, 0); + gl->glUniform1f( + gl->glGetUniformLocation(distShader, "u_spread"), + data.spread); + gl->glUniform1i( + gl->glGetUniformLocation(distShader, "u_invert"), + data.invert ? 1 : 0); + ctx.drawQuad(); + + cache->releaseTexture(texA); + cache->releaseTexture(texB); + } + +private: + // Seed pass: store UV in RG if foreground, sentinel (-1,-1) if background + static QString seedFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_mask; + uniform float u_threshold; + + void main() + { + float v = texture(u_mask, v_texCoord).r; + if (v >= u_threshold) + fragColor = vec4(v_texCoord, 0.0, 1.0); // seed: own UV + else + fragColor = vec4(-1.0, -1.0, 0.0, 1.0); // no seed + } + )""""; + } + + // JFA step: for each pixel, sample 8 neighbours at ±stepSize and keep + // whichever carries the nearest valid seed UV + static QString jfaFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_input; + uniform int u_step; + + void main() + { + vec2 texSize = vec2(textureSize(u_input, 0)); + vec2 step = vec2(float(u_step)) / texSize; + + vec2 bestUV = vec2(-1.0); + float bestDist = 1e9; + + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + vec2 sampleUV = v_texCoord + vec2(float(x), float(y)) * step; + vec4 s = texture(u_input, sampleUV); + vec2 seedUV = s.rg; + if (seedUV.x < 0.0) continue; // no seed stored here + float d = length(v_texCoord - seedUV); + if (d < bestDist) { + bestDist = d; + bestUV = seedUV; + } + } + } + + fragColor = vec4(bestUV, 0.0, 1.0); + } + )""""; + } + + // Distance pass: convert nearest-seed UV to a normalised, spread-scaled + // grayscale value + static QString distFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_jfa; + uniform float u_spread; + uniform int u_invert; + + void main() + { + vec2 seedUV = texture(u_jfa, v_texCoord).rg; + + float dist; + if (seedUV.x < 0.0) { + // No seed found — maximum distance + dist = 1.0; + } else { + // Raw distance in [0,~1.41] UV space + dist = length(v_texCoord - seedUV); + // Scale by spread: spread=0.5 → moderate falloff + float invSpread = 1.0 / max(u_spread, 0.001); + dist = clamp(dist * invSpread * 2.0, 0.0, 1.0); + } + + if (u_invert == 0) + dist = 1.0 - dist; // bright near seed, dark far away (default) + + fragColor = vec4(vec3(dist), 1.0); + } + )""""; + } +}; + +// ============================================================================ +// DistanceTransformNode +// ============================================================================ +void DistanceTransformNode::init() +{ + this->title = "Distance Transform"; + + this->addInput("mask"); + + this->addFloatProp("threshold", "Threshold", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("spread", "Spread", 0.5, 0.0, 1.0, 0.01); + this->addBoolProp ("invert", "Invert", false); +} + +std::shared_ptr DistanceTransformNode::createRenderer() +{ + return std::make_shared(); +} + +std::shared_ptr DistanceTransformNode::createRenderData() +{ + auto data = std::make_shared(); + + if (auto* p = dynamic_cast(getProp("threshold"))) + data->threshold = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("spread"))) + data->spread = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("invert"))) + data->invert = p->value; + + return data; +} diff --git a/src/texturelab/libraries/v3/edgedetect.cpp b/src/texturelab/libraries/v3/edgedetect.cpp new file mode 100644 index 00000000..0987ed68 --- /dev/null +++ b/src/texturelab/libraries/v3/edgedetect.cpp @@ -0,0 +1,82 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// https://en.wikipedia.org/wiki/Sobel_operator +// https://docs.opencv.org/4.x/d2/d2c/tutorial_sobel_derivatives.html +void EdgeDetectNode::init() +{ + this->title = "Edge Detect"; + + this->addInput("image"); + + auto kernelProp = this->addEnumProp("kernel", "Kernel", + {"Sobel", "Prewitt", "Laplacian"}); + kernelProp->index = 0; + + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 4.0, 0.1); + this->addFloatProp("threshold", "Threshold", 0.0, 0.0, 1.0, 0.01); + this->addFloatProp("width", "Width", 1.0, 0.5, 8.0, 0.5); + this->addBoolProp ("invert", "Invert", false); + + auto source = R""""( + #define KERNEL_SOBEL 0 + #define KERNEL_PREWITT 1 + #define KERNEL_LAPLACIAN 2 + + float luminance(vec3 c) { + return dot(c, vec3(0.2126, 0.7152, 0.0722)); + } + + float sampleLum(vec2 uv) { + return luminance(texture(image, uv).rgb); + } + + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.0, 0.0, 0.0, 1.0); + + vec2 step = (prop_width / _textureSize.xy); + + float tl = sampleLum(uv + vec2(-step.x, step.y)); + float t = sampleLum(uv + vec2( 0.0, step.y)); + float tr = sampleLum(uv + vec2( step.x, step.y)); + float l = sampleLum(uv + vec2(-step.x, 0.0)); + float r = sampleLum(uv + vec2( step.x, 0.0)); + float bl = sampleLum(uv + vec2(-step.x, -step.y)); + float b = sampleLum(uv + vec2( 0.0, -step.y)); + float br = sampleLum(uv + vec2( step.x, -step.y)); + + float gx = 0.0; + float gy = 0.0; + float edge = 0.0; + + if (prop_kernel == KERNEL_SOBEL) { + gx = -tl - 2.0*l - bl + tr + 2.0*r + br; + gy = -tl - 2.0*t - tr + bl + 2.0*b + br; + edge = length(vec2(gx, gy)); + } else if (prop_kernel == KERNEL_PREWITT) { + gx = -tl - l - bl + tr + r + br; + gy = -tl - t - tr + bl + b + br; + edge = length(vec2(gx, gy)); + } else { + // Laplacian: [-1,-1,-1; -1,8,-1; -1,-1,-1] × center + float center = sampleLum(uv); + edge = abs(8.0 * center - (tl + t + tr + l + r + bl + b + br)); + } + + edge *= prop_intensity; + edge = max(0.0, edge - prop_threshold); + + if (prop_invert) + edge = 1.0 - clamp(edge, 0.0, 1.0); + else + edge = clamp(edge, 0.0, 1.0); + + return vec4(vec3(edge), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/emboss.cpp b/src/texturelab/libraries/v3/emboss.cpp new file mode 100644 index 00000000..3386ca9c --- /dev/null +++ b/src/texturelab/libraries/v3/emboss.cpp @@ -0,0 +1,66 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// https://en.wikipedia.org/wiki/Emboss_(photography) +// Fake 2D directional light across a height-map surface. +// Distinct from Normal Map: outputs a shaded image, not a vector map. +// Use as a quick height-preview or for stylised cel-shading effects. +void EmbossNode::init() +{ + this->title = "Emboss"; + + this->addInput("image"); + + this->addFloatProp("angle", "Light Angle", 45.0, 0.0, 360.0, 1.0); + this->addFloatProp("elevation", "Light Elevation", 0.5, 0.0, 1.0, 0.05); + this->addFloatProp("intensity", "Intensity", 3.0, 0.1, 16.0, 0.1); + this->addBoolProp ("invert", "Invert Height", false); + + auto source = R""""( + #define PI 3.14159265358979 + + float sampleH(vec2 uv) { + float h = texture(image, uv).r; + return prop_invert ? 1.0 - h : h; + } + + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.5, 0.5, 0.5, 1.0); + + float angleRad = prop_angle * PI / 180.0; + vec2 texel = 1.0 / _textureSize.xy; + + // Central-difference gradient + float hL = sampleH(uv - vec2(texel.x, 0.0)); + float hR = sampleH(uv + vec2(texel.x, 0.0)); + float hD = sampleH(uv - vec2(0.0, texel.y)); + float hU = sampleH(uv + vec2(0.0, texel.y)); + + // Build surface normal from finite differences + // intensity scales how steeply height maps to angle + vec3 normal = normalize(vec3( + (hL - hR) * prop_intensity, + (hD - hU) * prop_intensity, + 1.0 + )); + + // Light direction from angle and elevation + float elev = prop_elevation * PI * 0.5; // [0..PI/2] + vec3 lightDir = normalize(vec3( + cos(angleRad) * cos(elev), + sin(angleRad) * cos(elev), + sin(elev) + )); + + float diffuse = dot(normal, lightDir); + float result = diffuse * 0.5 + 0.5; // remap [-1,1] → [0,1] + + return vec4(vec3(clamp(result, 0.0, 1.0)), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/fastao.cpp b/src/texturelab/libraries/v3/fastao.cpp new file mode 100644 index 00000000..1b66495f --- /dev/null +++ b/src/texturelab/libraries/v3/fastao.cpp @@ -0,0 +1,435 @@ +#include "../../graphics/noderenderer.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +#include +#include +#include + +// Vogel, "A better way to construct the sunflower head," Math. Biosciences 1979 +// (golden-angle disc — the sample distribution used here) +// Jimenez et al., "Next Generation Post Processing in Call of Duty: Advanced +// Warfare," SIGGRAPH 2014 (interleaved gradient noise + matched 4x4 filter) +// Crassin et al., "Interactive Indirect Illumination Using Voxel Cone Tracing," +// PG 2011 (mip level chosen from cone footprint) +// +// Fast AO — the high-quality counterpart to the single-pass Ambient +// Occlusion node. Four ideas the single-pass version cannot express: +// +// 1. Cone taps. A height pyramid is built once, and each tap reads the mip +// level matching its footprint. A far tap then represents the average +// of the region it stands for instead of one arbitrary texel, which is +// where most of the remaining noise and all of the large-radius aliasing +// came from. +// 2. Progressive accumulation. This is an offline texture tool, not a +// 16 ms frame budget, so quality is bought with passes rather than with +// a longer inner loop: N additive passes, each laying down a differently +// rotated spiral, converge to a noise-free result while Draft stays +// interactive. +// 3. Two scales. A broad occlusion term and a tight cavity term are +// accumulated into separate channels and combined at resolve, because a +// single radius always compromises between contact darkening and broad +// ambient shading. +// 4. Bilateral resolve. The per-pixel spiral rotation comes from +// interleaved gradient noise, whose 4x4 tile the resolve pass's 4x4 box +// cancels almost exactly — worth roughly 16x the sample count for the +// cost of one pass. The box is height-guided so it never blurs +// occlusion across a step in the surface. + +// ============================================================================ +// FastAORenderData +// ============================================================================ +struct FastAORenderData : public NodeRenderData { + float radius = 0.08f; + int samples = 48; + int quality = 1; // enum index -> pass count + float intensity = 1.0f; + float bias = 0.02f; + float heightScale = 1.0f; + float detail = 0.35f; + float detailScale = 0.15f; + float denoise = 0.5f; + float contrast = 1.0f; + bool cone = true; +}; + +// ============================================================================ +// FastAORenderer +// ============================================================================ +class FastAORenderer : public NodeTextureRenderer { +public: + void render(NodeRenderContext& ctx, const NodeRenderData& baseData) override + { + auto& data = static_cast(baseData); + auto gl = ctx.gl; + auto cache = ctx.cache; + int w = ctx.textureWidth; + int h = ctx.textureHeight; + + // No input — unoccluded is white, not black. + if (ctx.inputs.isEmpty() || ctx.inputs[0].textureId == 0) { + cache->bindFboToTexture(ctx.outputTextureId); + gl->glViewport(0, 0, w, h); + gl->glClearColor(1, 1, 1, 1); + gl->glClear(GL_COLOR_BUFFER_BIT); + return; + } + + GLuint prepShader = cache->getOrCompileShader( + "fastao_prep", RenderResourceCache::standardVertexSource(), + prepFrag()); + GLuint aoShader = cache->getOrCompileShader( + "fastao_ao", RenderResourceCache::standardVertexSource(), + aoFrag()); + GLuint resolveShader = cache->getOrCompileShader( + "fastao_resolve", RenderResourceCache::standardVertexSource(), + resolveFrag()); + + GLuint heightTex = cache->acquireTexture(w, h); + GLuint accumTex = cache->acquireTexture(w, h); + + // --- Pass 1: linearise the height into its own texture ------------- + // Applying height_scale once here keeps it out of the inner loop, and + // gives us a texture we own and may re-parameterise freely. + cache->bindFboToTexture(heightTex); + ctx.useShader(prepShader); + ctx.bindTexture(prepShader, "u_input", ctx.inputs[0].textureId, 0); + gl->glUniform1f(gl->glGetUniformLocation(prepShader, "u_heightScale"), + data.heightScale); + ctx.drawQuad(); + + // --- Pass 2: accumulate N rotated AO passes ------------------------ + const int passes = passCountFor(data.quality); + + // Point the FBO at the accumulation target *before* generating the + // pyramid: generating mips for a texture still attached to the bound + // framebuffer is a feedback loop the spec leaves undefined. + cache->bindFboToTexture(accumTex); + gl->glViewport(0, 0, w, h); + gl->glClearColor(0, 0, 0, 0); + gl->glClear(GL_COLOR_BUFFER_BIT); + + { + // Trilinear + REPEAT for the duration of the AO passes: the cone + // taps read down the pyramid, and taps have to wrap because the AO + // of a tiling texture has to tile too. The guard restores the + // pool's defaults on the way out. The mip storage stays allocated + // on the texture, but with GL_NEAREST it is never sampled again. + ScopedTextureParams heightParams(gl, heightTex, + GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, + GL_REPEAT); + // The guard leaves nothing bound, so bind explicitly here — + // glGenerateMipmap acts on whatever is bound to the target. + gl->glBindTexture(GL_TEXTURE_2D, heightTex); + gl->glGenerateMipmap(GL_TEXTURE_2D); + gl->glBindTexture(GL_TEXTURE_2D, 0); + + // Additive blending is what makes accumulation a single texture + // instead of a ping-pong pair; the resolve divides by the pass + // count. + gl->glEnable(GL_BLEND); + gl->glBlendFunc(GL_ONE, GL_ONE); + + for (int p = 0; p < passes; p++) { + ctx.useShader(aoShader); + ctx.bindTexture(aoShader, "u_height", heightTex, 0); + gl->glUniform1f(gl->glGetUniformLocation(aoShader, "u_radius"), + data.radius); + gl->glUniform1i(gl->glGetUniformLocation(aoShader, "u_samples"), + data.samples); + gl->glUniform1f(gl->glGetUniformLocation(aoShader, "u_bias"), + data.bias); + gl->glUniform1f( + gl->glGetUniformLocation(aoShader, "u_detailScale"), + data.detailScale); + gl->glUniform1i(gl->glGetUniformLocation(aoShader, "u_detail"), + data.detail > 0.0f ? 1 : 0); + gl->glUniform1i(gl->glGetUniformLocation(aoShader, "u_cone"), + data.cone ? 1 : 0); + gl->glUniform1i(gl->glGetUniformLocation(aoShader, "u_pass"), p); + ctx.drawQuad(); + } + + gl->glDisable(GL_BLEND); + + // --- Pass 3: bilateral resolve --------------------------------- + cache->bindFboToTexture(ctx.outputTextureId); + ctx.useShader(resolveShader); + ctx.bindTexture(resolveShader, "u_accum", accumTex, 0); + ctx.bindTexture(resolveShader, "u_height", heightTex, 1); + gl->glUniform1f( + gl->glGetUniformLocation(resolveShader, "u_invPasses"), + 1.0f / float(passes)); + gl->glUniform1f( + gl->glGetUniformLocation(resolveShader, "u_intensity"), + data.intensity); + gl->glUniform1f(gl->glGetUniformLocation(resolveShader, "u_detail"), + data.detail); + gl->glUniform1f(gl->glGetUniformLocation(resolveShader, "u_denoise"), + data.denoise); + gl->glUniform1f( + gl->glGetUniformLocation(resolveShader, "u_contrast"), + data.contrast); + ctx.drawQuad(); + } + + cache->releaseTexture(heightTex); + cache->releaseTexture(accumTex); + } + +private: + static int passCountFor(int qualityIndex) + { + switch (qualityIndex) { + case 0: + return 1; // Draft + case 1: + return 2; // Medium + case 2: + return 4; // High + case 3: + return 8; // Ultra + default: + return 2; + } + } + + // Copy the height channel out, pre-scaled, so the AO pass reads a texture + // we control the filtering and wrap mode of. + static QString prepFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_input; + uniform float u_heightScale; + + void main() + { + float h = texture(u_input, v_texCoord).r * u_heightScale; + fragColor = vec4(h, h, h, 1.0); + } + )""""; + } + + // One accumulation pass: a rotated Vogel disc, sampled as cones, writing + // the broad term to R and the cavity term to G. + static QString aoFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_height; + uniform vec2 _textureSize; + uniform float u_radius; + uniform int u_samples; + uniform float u_bias; + uniform float u_detailScale; + uniform int u_detail; + uniform int u_cone; + uniform int u_pass; + + #define TAU 6.28318530718 + #define GOLDEN_ANGLE 2.39996322973 + + // Interleaved gradient noise: over any 4x4 pixel tile this yields + // 16 evenly spread values, which is exactly what the resolve + // pass's 4x4 box is built to average back out. + float ign(vec2 p) + { + return fract(52.9829189 * + fract(dot(p, vec2(0.06711056, 0.00583715)))); + } + + void main() + { + float sampleCount = float(u_samples); + + // Radius is in UV; keep the disc circular in texel space. + vec2 aspect = vec2(_textureSize.y / _textureSize.x, 1.0); + + // Golden-ratio offset per pass, so N passes lay down N + // maximally separated rotations of the same spiral rather + // than N arbitrary ones. + float rot = ign(gl_FragCoord.xy + + vec2(float(u_pass) * 5.588238)) * TAU; + + float centerH = textureLod(u_height, v_texCoord, 0.0).r; + + // N taps over a disc of radius R cannot resolve detail finer + // than their spacing, so prefilter each tap to roughly its + // own footprint instead of point-sampling one texel out of + // the region it is meant to stand for. + float coneScale = (u_cone == 1) ? 2.0 / sqrt(sampleCount) : 0.0; + float texelScale = max(_textureSize.x, _textureSize.y); + + float occBroad = 0.0; + float occFine = 0.0; + float weightSum = 0.0; + + for (int i = 0; i < u_samples; i++) + { + float t = (float(i) + 0.5) / sampleCount; + float angle = float(i) * GOLDEN_ANGLE + rot; + vec2 dir = vec2(cos(angle), sin(angle)) * aspect; + + // Smooth window over the disc edge, so a feature crossing + // the radius boundary fades in instead of popping. + float w = 1.0 - t; + + // --- broad term --- + // sqrt(t) distributes the taps uniformly over the disc + // area rather than over the radius. + float dist = sqrt(t) * u_radius; + float lod = log2(max(1.0, dist * texelScale * coneScale)); + float dh = textureLod(u_height, v_texCoord + dir * dist, + lod).r - centerH; + // sin(atan(dh / dist)) — the elevation angle of this + // neighbour. Bounded in [0,1] and scale-aware, unlike a + // raw height difference. + occBroad += clamp(dh * inversesqrt(dh * dh + dist * dist) + - u_bias, 0.0, 1.0) * w; + + // --- cavity term: the same spiral, tightened --- + if (u_detail == 1) { + float distF = dist * u_detailScale; + float lodF = log2(max(1.0, distF * texelScale * coneScale)); + float dhF = textureLod(u_height, + v_texCoord + dir * distF, + lodF).r - centerH; + occFine += clamp(dhF * inversesqrt(dhF * dhF + distF * distF) + - u_bias, 0.0, 1.0) * w; + } + + weightSum += w; + } + + float inv = 1.0 / max(weightSum, 0.0001); + fragColor = vec4(occBroad * inv, occFine * inv, 0.0, 0.0); + } + )""""; + } + + // Average the accumulation, denoise it, combine the two scales, invert. + static QString resolveFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_accum; + uniform sampler2D u_height; + uniform vec2 _textureSize; + uniform float u_invPasses; + uniform float u_intensity; + uniform float u_detail; + uniform float u_denoise; + uniform float u_contrast; + + void main() + { + vec2 texel = 1.0 / _textureSize; + float centerH = textureLod(u_height, v_texCoord, 0.0).r; + + vec2 raw = texture(u_accum, v_texCoord).rg * u_invPasses; + + // 4x4 box aligned to the interleaved-gradient-noise tile, so + // the 16 complementary rotations laid down by the AO pass + // cancel. Height-guided: occlusion is never blurred across a + // step in the surface, which is what a plain blur would eat. + vec2 sum = vec2(0.0); + float wsum = 0.0; + for (int y = -1; y <= 2; y++) { + for (int x = -1; x <= 2; x++) { + // fract() wraps the tap — pooled textures are + // CLAMP_TO_EDGE and the result has to tile. + vec2 uv = fract(v_texCoord + + vec2(float(x), float(y)) * texel); + float hs = textureLod(u_height, uv, 0.0).r; + float w = exp(-abs(hs - centerH) * 32.0); + sum += texture(u_accum, uv).rg * u_invPasses * w; + wsum += w; + } + } + + vec2 ao = mix(raw, sum / max(wsum, 0.0001), u_denoise); + + float broad = 1.0 - clamp(ao.r * u_intensity, 0.0, 1.0); + float fine = 1.0 - clamp(ao.g * u_intensity, 0.0, 1.0); + + // The cavity term reads as a separate layer of shading — it + // multiplies the broad occlusion rather than adding to it. + float result = broad * mix(1.0, fine, u_detail); + result = pow(max(result, 0.0), u_contrast); + + fragColor = vec4(vec3(result), 1.0); + } + )""""; + } +}; + +// ============================================================================ +// FastAONode +// ============================================================================ +void FastAONode::init() +{ + this->title = "Fast AO"; + + this->addInput("height"); + + this->addFloatProp("radius", "Radius", 0.01, 0.001, 1.0, 0.005); + this->addIntProp("samples", "Samples", 48, 8, 256, 8); + auto qualityEnum = + this->addEnumProp("quality", "Quality", + QList{"Draft", "Medium", "High", "Ultra"}); + qualityEnum->index = 1; // Medium + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 5.0, 0.1); + this->addFloatProp("bias", "Bias", 0.02, 0.0, 0.5, 0.005); + this->addFloatProp("height_scale", "Height Scale", 1.0, 0.01, 8.0, 0.01); + this->addFloatProp("detail", "Detail", 0.35, 0.0, 1.0, 0.01); + this->addFloatProp("detail_scale", "Detail Scale", 0.15, 0.02, 0.5, 0.01); + this->addFloatProp("denoise", "Denoise", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("contrast", "Contrast", 0.5, 0.25, 4.0, 0.05); + this->addBoolProp("cone", "Cone Filtering", true); +} + +std::shared_ptr FastAONode::createRenderer() +{ + return std::make_shared(); +} + +std::shared_ptr FastAONode::createRenderData() +{ + auto data = std::make_shared(); + + if (auto* p = dynamic_cast(getProp("radius"))) + data->radius = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("samples"))) + data->samples = p->value; + if (auto* p = dynamic_cast(getProp("quality"))) + data->quality = p->index; + if (auto* p = dynamic_cast(getProp("intensity"))) + data->intensity = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("bias"))) + data->bias = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("height_scale"))) + data->heightScale = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("detail"))) + data->detail = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("detail_scale"))) + data->detailScale = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("denoise"))) + data->denoise = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("contrast"))) + data->contrast = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("cone"))) + data->cone = p->value; + + return data; +} diff --git a/src/texturelab/libraries/v3/fbmdomainwarp.cpp b/src/texturelab/libraries/v3/fbmdomainwarp.cpp new file mode 100644 index 00000000..fb1926b4 --- /dev/null +++ b/src/texturelab/libraries/v3/fbmdomainwarp.cpp @@ -0,0 +1,96 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// https://iquilezles.org/articles/warp/ +// Quilez, "Domain Warping" — the definitive reference with interactive demos. +// +// fBm where the sampling coordinates are themselves offset by another layer of +// fBm, producing swirling organic turbulence that is impossible to achieve +// with standard layered noise. Warp 1 creates mild swirling; enabling +// Double Warp adds a second recursive step for maximum complexity. +// +// Seamless tiling: the base scale is an integer grid size (cells per UV +// tile), each octave doubles it, and value-noise hashes use mod()-wrapped +// cell indices. Because the warp itself is also seamless (its inputs share +// the same wrapping), warped sampling positions differ by integer amounts +// across the texture boundary — preserving the periodic property end-to-end. +void FBMDomainWarpNode::init() +{ + this->title = "FBM Domain Warp"; + + this->addIntProp ("scale", "Scale", 3, 1, 12, 1); + this->addIntProp ("octaves", "Octaves", 6, 1, 8, 1); + this->addFloatProp("warp", "Primary Warp", 1.0, 0.0, 4.0, 0.1); + this->addFloatProp("warp2", "Secondary Warp", 0.5, 0.0, 4.0, 0.1); + this->addBoolProp ("two_pass", "Double Warp", true); + + auto source = R""""( + // Tileable value noise: cell hashes use mod()-wrapped indices so the + // noise repeats exactly every gridSize cells along each axis. + float vnoise(vec2 sampleUV, float gridSize) { + vec2 i = floor(sampleUV); + vec2 f = fract(sampleUV); + vec2 u = f * f * (3.0 - 2.0 * f); + + vec2 sOff = vec2(_seed * 0.173); + + float a = hash12(mod(i + vec2(0.0, 0.0), vec2(gridSize)) + sOff); + float b = hash12(mod(i + vec2(1.0, 0.0), vec2(gridSize)) + sOff); + float c = hash12(mod(i + vec2(0.0, 1.0), vec2(gridSize)) + sOff); + float d = hash12(mod(i + vec2(1.0, 1.0), vec2(gridSize)) + sOff); + + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); + } + + // Multi-octave fBm. Each octave doubles the grid size (lacunarity 2); + // since baseScale is integer, every octave's grid size remains integer + // → mod() wrapping is exact and the entire fBm is seamlessly tileable. + float fbm(vec2 uv) { + float value = 0.0; + float amp = 0.5; + float gridSize = float(prop_scale); + for (int i = 0; i < prop_octaves; i++) { + value += amp * vnoise(uv * gridSize, gridSize); + gridSize *= 2.0; + amp *= 0.5; + } + return value; + } + + vec4 process(vec2 uv) + { + // Warp pass 1: offset UV by two independent fBm samples. + // The constant offsets (5.2, 1.3) etc. just shift which part of + // the noise we read; they don't affect tileability because mod() + // wrapping handles any sample position. + vec2 q = vec2( + fbm(uv + vec2(0.000, 0.000)), + fbm(uv + vec2(5.200, 1.300)) + ); + + float f; + if (prop_two_pass) { + // Warp pass 2: use q to offset again (recursive swirling). + // Since q is itself seamless, q at uv=0 equals q at uv=1, + // so the warped sampling positions still differ by exactly 1 + // across the tile boundary — fbm's mod() wrap maps them to + // the same noise value. + vec2 r = vec2( + fbm(uv + prop_warp * q + vec2(1.700, 9.200)), + fbm(uv + prop_warp * q + vec2(8.300, 2.800)) + ); + f = fbm(uv + prop_warp2 * r); + } else { + f = fbm(uv + prop_warp * q); + } + + // Remap to [0,1] and add a slight contrast lift + f = clamp(f * 1.4 - 0.1, 0.0, 1.0); + + return vec4(vec3(f), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/floodfillv2.cpp b/src/texturelab/libraries/v3/floodfillv2.cpp new file mode 100644 index 00000000..926d27af --- /dev/null +++ b/src/texturelab/libraries/v3/floodfillv2.cpp @@ -0,0 +1,215 @@ +#include "../../graphics/noderenderer.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +#include +#include +#include +#include +#include +#include + +// ============================================================================ +// FloodFillV2RenderData +// ============================================================================ + +struct FloodFillV2RenderData : public NodeRenderData { + float threshold = 0.1f; +}; + +// ============================================================================ +// FloodFillV2Renderer — CPU BFS flood fill via NodeTextureRenderer +// +// Output encoding (RGBA32F per pixel): +// R = island origin x (UV, wrapped to [0,1], quantized to texel center) +// G = island origin y (same) +// B = bbox width / texture width +// A = bbox height / texture height +// Background = (0, 0, 0, 0) +// +// The origin is stored directly — downstream nodes use it as a per-island +// identity without reconstruction arithmetic, eliminating precision issues. +// UV-within-bbox is derived by downstream nodes as (uv - origin) / bbox_size. +// ============================================================================ + +class FloodFillV2Renderer : public NodeTextureRenderer { +public: + void render(NodeRenderContext& ctx, + const NodeRenderData& baseData) override + { + auto& data = static_cast(baseData); + auto gl = ctx.gl; + auto cache = ctx.cache; + + int w = ctx.textureWidth; + int h = ctx.textureHeight; + + // Guard against zero/negative dimensions: wrapAround() below divides by + // w/h, and a negative product would wrap to a huge size_t allocation. + if (w <= 0 || h <= 0) + return; + + // No input — output black + if (ctx.inputs.isEmpty() || ctx.inputs[0].textureId == 0) { + cache->bindFboToTexture(ctx.outputTextureId); + gl->glViewport(0, 0, w, h); + gl->glClearColor(0, 0, 0, 1); + gl->glClear(GL_COLOR_BUFFER_BIT); + return; + } + + // size_t so w*h*4 can't overflow int for large textures. + size_t gridSize = (size_t)w * h; + + // Read input pixels via FBO + std::vector readPixels(gridSize * 4); + cache->bindFboToTexture(ctx.inputs[0].textureId); + gl->glReadPixels(0, 0, w, h, GL_RGBA, GL_FLOAT, readPixels.data()); + + // Helper: wrap pixel coordinate into [0, bound) + auto wrapAround = [](int value, int bound) -> int { + return ((value % bound) + bound) % bound; + }; + + // Helper: get pixel intensity (average of RGB) + auto getIntensity = [&](int x, int y) -> float { + int idx = 4 * (w * y + x); + return (readPixels[idx] + readPixels[idx + 1] + readPixels[idx + 2]) + / 3.0f; + }; + + // BFS flood fill with wrap-around + struct Island { + int left = INT_MAX, top = INT_MAX; + int right = INT_MIN, bottom = INT_MIN; + struct Pixel { + int localX, localY, globalX, globalY; + }; + std::vector pixels; + + void expand(int x, int y) + { + left = std::min(left, x); + top = std::min(top, y); + right = std::max(right, x); + bottom = std::max(bottom, y); + } + int width() const { return right - left; } + int height() const { return bottom - top; } + }; + + std::vector visited(gridSize, false); + std::vector islands; + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + if (visited[y * w + x]) + continue; + + Island island; + std::queue> queue; + queue.push({x, y}); + + while (!queue.empty()) { + auto [gx, gy] = queue.front(); + queue.pop(); + + int lx = wrapAround(gx, w); + int ly = wrapAround(gy, h); + + if (visited[ly * w + lx]) + continue; + visited[ly * w + lx] = true; + + if (getIntensity(lx, ly) < data.threshold) + continue; + + island.expand(gx, gy); + island.pixels.push_back({lx, ly, gx, gy}); + + queue.push({gx + 1, gy}); + queue.push({gx - 1, gy}); + queue.push({gx, gy + 1}); + queue.push({gx, gy - 1}); + } + + if (island.width() > 0 && island.height() > 0) + islands.push_back(std::move(island)); + } + } + + // Build output texture + // Encoding: (origin_x, origin_y, bbox_w, bbox_h) + // origin is the top-left of the bbox, wrapped to [0,1] UV space, + // quantized to texel centers for consistency across all pixels + std::vector results(gridSize * 4, 0.0f); + + float invW = 1.0f / w; + float invH = 1.0f / h; + + for (const auto& island : islands) { + float bboxW = island.width() * invW; + float bboxH = island.height() * invH; + + // Origin in UV space, wrapped to [0,1] and quantized to texel center + int originLocalX = wrapAround(island.left, w); + int originLocalY = wrapAround(island.top, h); + float originU = (originLocalX + 0.5f) * invW; + float originV = (originLocalY + 0.5f) * invH; + + for (const auto& px : island.pixels) { + int idx = 4 * (w * px.localY + px.localX); + results[idx + 0] = originU; + results[idx + 1] = originV; + results[idx + 2] = bboxW; + results[idx + 3] = bboxH; + } + } + + // Upload result to output texture + gl->glBindTexture(GL_TEXTURE_2D, ctx.outputTextureId); + gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, w, h, 0, GL_RGBA, + GL_FLOAT, results.data()); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + gl->glBindTexture(GL_TEXTURE_2D, 0); + } +}; + +// ============================================================================ +// FloodFillV2Node +// ============================================================================ + +void FloodFillV2Node::init() +{ + this->title = "Flood Fill V2"; + this->addInput("image"); + this->addFloatProp("threshold", "Threshold", 0.1, 0.0, 1.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + return texture(image, uv); + } + )""""; + this->setShaderSource(source); +} + +std::shared_ptr FloodFillV2Node::createRenderer() +{ + return std::make_shared(); +} + +std::shared_ptr FloodFillV2Node::createRenderData() +{ + auto data = std::make_shared(); + + auto threshProp = static_cast(this->getProp("threshold")); + if (threshProp) + data->threshold = threshProp->value; + + return data; +} diff --git a/src/texturelab/libraries/v3/floodfillv2sampler.cpp b/src/texturelab/libraries/v3/floodfillv2sampler.cpp new file mode 100644 index 00000000..c4bf6edd --- /dev/null +++ b/src/texturelab/libraries/v3/floodfillv2sampler.cpp @@ -0,0 +1,124 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +#include + +void FloodFillV2SamplerNode::init() +{ + this->title = "FF Sampler V2"; + + this->addInput("floodfill"); + this->addInput("image"); + this->addInput("mask"); + this->addInput("size"); + this->addInput("intensity"); + + this->addFloatProp("rot", "Rotation", 0, 0, 360, 0.1); + this->addFloatProp("rotRand", "Random Rotation", 0, 0, 1.0, 0.01); + this->addFloatProp("posRand", "Random Position", 0, 0, 1.0, 0.01); + this->addFloatProp("intensityRand", "Random Intensity", 0, 0, 1.0, 0.01); + this->addFloatProp("scale", "Scale", 1, 0, 4, 0.1); + this->addFloatProp("scaleRand", "Scale random", 0, 0, 1, 0.1); + + this->addColorProp("bg", "Background Color", QColor()); + + auto source = R""""( + mat3 transMat(vec2 t) + { + return mat3(vec3(1.0,0.0,0.0), vec3(0.0,1.0,0.0), vec3(t, 1.0)); + } + + mat3 scaleMat(vec2 s) + { + return mat3(vec3(s.x,0.0,0.0), vec3(0.0,s.y,0.0), vec3(0.0, 0.0, 1.0)); + } + + mat3 rotMat(float rot) + { + float r = radians(rot); + return mat3(vec3(cos(r), -sin(r),0.0), vec3(sin(r), cos(r),0.0), vec3(0.0, 0.0, 1.0)); + } + + vec2 transformUV(vec2 uv, vec2 translate, float rot, vec2 scale) + { + mat3 trans = transMat(vec2(0.5, 0.5)) * + transMat(vec2(translate.x, translate.y)) * + rotMat(rot) * + scaleMat(vec2(scale.x, scale.y)) * + transMat(vec2(-0.5, -0.5)); + + vec3 res = inverse(trans) * vec3(uv, 1.0); + uv = res.xy; + + return clamp(uv, vec2(0.0), vec2(1.0)); + } + + float randomFloatRange(vec2 seed, int offset, float fmin, float fmax) + { + float r = _rand(vec2(_seed) + seed + vec2(float(offset)) * 0.01); + return fmin + (fmax - fmin) * r; + } + + vec4 sampleImage(sampler2D img, vec2 uv) + { + if (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0) + return texture(img, uv); + return vec4(prop_bg.rgb, 1.0); + } + + vec4 process(vec2 uv) + { + vec4 data = texture(floodfill, uv); + + if (data.ba == vec2(0.0, 0.0)) + return vec4(prop_bg.rgb, 1.0); + + // Origin and bbox from encoding + vec2 origin = data.rg; + vec2 bboxSize = data.ba; + vec2 center = fract(origin + bboxSize * vec2(0.5)); + + // Compute UV within bbox for this pixel (wrap-aware) + vec2 uvInBbox = mod(uv - origin + 1.0, 1.0) / bboxSize; + + // Per-island random values seeded from origin + vec2 randOffset = vec2(0.0); + { + float rx = randomFloatRange(origin, 4, -1.0, 1.0); + float ry = randomFloatRange(origin, 5, -1.0, 1.0); + randOffset = normalize(vec2(rx, ry)) * prop_posRand; + } + + float rot = randomFloatRange(origin, 3, -180.0, 180.0) * prop_rotRand + prop_rot; + + // Mask + if (mask_connected) { + float m = texture(mask, center).r; + if (m < 0.001) + return vec4(prop_bg.rgb, 1.0); + } + + // Scale + float s = prop_scale; + if (size_connected) + s *= texture(size, center).r; + float randScale = randomFloatRange(origin, 3, 0.0, 1.0); + s = mix(s, randScale, prop_scaleRand); + + // Intensity + float intens = 1.0; + if (intensity_connected) + intens *= texture(intensity, center).r; + float randIntensity = randomFloatRange(origin, 8, 0.0, 1.0); + intens = mix(intens, randIntensity, prop_intensityRand); + + vec2 finalUv = transformUV(uvInBbox, randOffset, rot, vec2(s)); + vec3 color = texture(image, finalUv).rgb; + + return vec4(color * intens, 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/floodfillv2tobbox.cpp b/src/texturelab/libraries/v3/floodfillv2tobbox.cpp new file mode 100644 index 00000000..728f231e --- /dev/null +++ b/src/texturelab/libraries/v3/floodfillv2tobbox.cpp @@ -0,0 +1,41 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void FloodFillV2ToBBoxNode::init() +{ + this->title = "FF To BBox V2"; + + this->addInput("floodfill"); + + this->addEnumProp("function", "Function", + {"max(x,y)", "min(x,y)", "x", "y", "length(x,y)"}); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 data = texture(floodfill, uv); + + if (data.ba == vec2(0.0, 0.0)) + return vec4(0.0, 0.0, 0.0, 1.0); + + // BA = bbox size (width, height) relative to texture + float intensity = 0.0; + + if (prop_function == 0) + intensity = max(data.b, data.a); + else if (prop_function == 1) + intensity = min(data.b, data.a); + else if (prop_function == 2) + intensity = data.b; + else if (prop_function == 3) + intensity = data.a; + else if (prop_function == 4) + intensity = sqrt(data.b * data.b + data.a * data.a); + + return vec4(vec3(intensity), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/floodfillv2tocolor.cpp b/src/texturelab/libraries/v3/floodfillv2tocolor.cpp new file mode 100644 index 00000000..e1fa274c --- /dev/null +++ b/src/texturelab/libraries/v3/floodfillv2tocolor.cpp @@ -0,0 +1,29 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void FloodFillV2ToColorNode::init() +{ + this->title = "FF To Color V2"; + + this->addInput("floodfill"); + this->addInput("color"); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 data = texture(floodfill, uv); + + // Background check: bbox size is zero + if (data.ba == vec2(0.0, 0.0)) + return vec4(0.0, 0.0, 0.0, 1.0); + + // Origin is stored directly in RG — no reconstruction needed + vec2 origin = data.rg; + + return texture(color, origin); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/floodfillv2togradient.cpp b/src/texturelab/libraries/v3/floodfillv2togradient.cpp new file mode 100644 index 00000000..71072c4b --- /dev/null +++ b/src/texturelab/libraries/v3/floodfillv2togradient.cpp @@ -0,0 +1,56 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void FloodFillV2ToGradientNode::init() +{ + this->title = "FF To Gradient V2"; + + this->addInput("floodfill"); + + this->addFloatProp("angle", "Angle", 0, 0, 360, 1); + this->addFloatProp("variation", "Angle Variation", 0, 0, 1.0, 0.05); + + auto source = R""""( + mat2 buildRot(float rot) + { + float r = radians(rot); + return mat2(cos(r), -sin(r), sin(r), cos(r)); + } + + float distAlongDir(vec2 x, vec2 dir) + { + return dot(x, dir) / dot(dir, dir); + } + + vec4 process(vec2 uv) + { + vec4 data = texture(floodfill, uv); + + if (data.ba == vec2(0.0, 0.0)) + return vec4(0.0, 0.0, 0.0, 1.0); + + vec2 origin = data.rg; + vec2 bboxSize = data.ba; + + float radius = length(bboxSize) * 0.5; + + // Per-island random rotation from origin seed + float rotRand = _rand(vec2(_seed) + origin * vec2(0.01)); + float addedRot = rotRand * 360.0 * prop_variation; + + vec2 dir = buildRot(prop_angle + addedRot) * vec2(-radius, 0); + + // Wrap-aware offset from center + vec2 offsetFromOrigin = mod(uv - origin + 1.0, 1.0); + vec2 centerToUv = offsetFromOrigin - bboxSize * vec2(0.5); + + float grad = distAlongDir(centerToUv, dir); + grad = grad * 0.5 + 0.5; + + return vec4(vec3(grad), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/floodfillv2torandomcolor.cpp b/src/texturelab/libraries/v3/floodfillv2torandomcolor.cpp new file mode 100644 index 00000000..67667d71 --- /dev/null +++ b/src/texturelab/libraries/v3/floodfillv2torandomcolor.cpp @@ -0,0 +1,32 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void FloodFillV2ToRandomColorNode::init() +{ + this->title = "FF To Random Color V2"; + + this->addInput("floodfill"); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 data = texture(floodfill, uv); + + if (data.ba == vec2(0.0, 0.0)) + return vec4(0.0, 0.0, 0.0, 1.0); + + // Origin stored directly — use as hash seed + vec2 origin = data.rg; + + vec4 color = vec4(0.0, 0.0, 0.0, 1.0); + color.r = _rand(vec2(_seed) + origin + vec2(1) * vec2(0.01)); + color.g = _rand(vec2(_seed) + origin + vec2(2) * vec2(0.01)); + color.b = _rand(vec2(_seed) + origin + vec2(3) * vec2(0.01)); + + return color; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/floodfillv2torandomintensity.cpp b/src/texturelab/libraries/v3/floodfillv2torandomintensity.cpp new file mode 100644 index 00000000..2b936071 --- /dev/null +++ b/src/texturelab/libraries/v3/floodfillv2torandomintensity.cpp @@ -0,0 +1,29 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void FloodFillV2ToRandomIntensityNode::init() +{ + this->title = "FF To Random Intensity V2"; + + this->addInput("floodfill"); + + auto source = R""""( + vec4 process(vec2 uv) + { + vec4 data = texture(floodfill, uv); + + if (data.ba == vec2(0.0, 0.0)) + return vec4(0.0, 0.0, 0.0, 1.0); + + vec2 origin = data.rg; + + vec4 color = vec4(0.0, 0.0, 0.0, 1.0); + color.rgb = vec3(_rand(vec2(_seed) + origin + vec2(1) * vec2(0.01))); + + return color; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/heightblend.cpp b/src/texturelab/libraries/v3/heightblend.cpp new file mode 100644 index 00000000..147cfff0 --- /dev/null +++ b/src/texturelab/libraries/v3/heightblend.cpp @@ -0,0 +1,65 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// https://blog.selfshadow.com/publications/blending-in-detail/ +// Barré-Brisebois & Bouchard, "Approximating Translucency for a Fast, Cheap +// and Convincing Subsurface-Scattering Look," GDC 2011 +// Height-aware material blend: the top layer wears away at high-height +// features of the bottom layer, revealing it underneath — physically +// plausible layering without the muddy result of linear alpha blending. +// Classic use: paint coat over scratched metal, where scratches cut through. +// Feed Curvature output as the mask for curvature-driven edge wear. +void HeightBlendNode::init() +{ + this->title = "Height Blend"; + + this->addInput("color_a"); // bottom material (e.g. bare metal) + this->addInput("color_b"); // top material (e.g. paint) + this->addInput("height_a"); // height map for bottom material + this->addInput("height_b"); // height map for top material + this->addInput("mask"); // wear mask: white=top present, black=bottom exposed + + this->addFloatProp("blend", "Blend Factor", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("edge_width", "Edge Width", 0.1, 0.0, 0.5, 0.01); + this->addFloatProp("contrast", "Contrast", 1.0, 0.1, 4.0, 0.1); + + auto source = R""""( + vec4 process(vec2 uv) + { + // Gather inputs; fall back gracefully if not connected + vec4 colA = color_a_connected ? texture(color_a, uv) : vec4(0.2, 0.2, 0.2, 1.0); + vec4 colB = color_b_connected ? texture(color_b, uv) : vec4(0.8, 0.8, 0.8, 1.0); + + float hA = height_a_connected ? texture(height_a, uv).r : 0.5; + float hB = height_b_connected ? texture(height_b, uv).r : 0.5; + float wearMask = mask_connected ? texture(mask, uv).r : prop_blend; + + // wearMask=1 → full top coat; wearMask=0 → fully worn to base + float coverage = clamp(wearMask * prop_blend * 2.0, 0.0, 1.0); + + // Height-based cutoff: find where the two height fields "meet" + float hAscaled = hA * (1.0 - coverage); + float hBscaled = hB * coverage; + float cutoff = max(hAscaled, hBscaled); + + float ew = max(prop_edge_width, 0.001); + + // Per-pixel blend factor from height intersection + float bFactor = smoothstep(cutoff - ew, cutoff + ew, hBscaled); + + // Apply contrast to the blend factor + if (prop_contrast != 1.0) { + bFactor = clamp( + (bFactor - 0.5) * prop_contrast + 0.5, + 0.0, 1.0 + ); + } + + vec4 result = mix(colA, colB, bFactor); + return result; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/highpass.cpp b/src/texturelab/libraries/v3/highpass.cpp new file mode 100644 index 00000000..221a5349 --- /dev/null +++ b/src/texturelab/libraries/v3/highpass.cpp @@ -0,0 +1,50 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// https://en.wikipedia.org/wiki/Unsharp_masking +// Highpass = input - blur(input) + 0.5 +// Output is neutral-gray where there is no detail; overlay-blend it onto a +// base material to layer micro-variation without altering base tone. +void HighpassNode::init() +{ + this->title = "Highpass"; + + this->addInput("image"); + + this->addFloatProp("radius", "Radius", 8.0, 0.5, 64.0, 0.5); + this->addIntProp ("quality", "Quality", 4, 1, 8, 1); + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 4.0, 0.1); + + auto source = R""""( + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.5, 0.5, 0.5, 1.0); + + vec4 original = texture(image, uv); + + // Box blur approximation of Gaussian + vec4 blurred = vec4(0.0); + float total = 0.0; + int q = prop_quality; + + for (int x = -q; x <= q; x++) { + for (int y = -q; y <= q; y++) { + vec2 offset = vec2(float(x), float(y)) + * prop_radius / _textureSize.xy; + blurred += texture(image, uv + offset); + total += 1.0; + } + } + blurred /= total; + + // Highpass centered on 0.5 so it is neutral for Overlay blending + vec4 detail = (original - blurred) * prop_intensity + 0.5; + + return clamp(detail, 0.0, 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/linecellv3.cpp b/src/texturelab/libraries/v3/linecellv3.cpp new file mode 100644 index 00000000..236bfe03 --- /dev/null +++ b/src/texturelab/libraries/v3/linecellv3.cpp @@ -0,0 +1,86 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Voronoi edge (line-cell) node with independent X/Y scale. +// Renders the borders between Voronoi cells as bright lines. +// Seamless: integer-rounded grid sizes with mod()-wrapped cell hashes. +// Reference: https://iquilezles.org/articles/voronoilines/ +void LineCellV3Node::init() +{ + this->title = "Line Cell"; + + this->addFloatProp("scale", "Scale", 30.0, 1.0, 256.0, 1.0); + this->addFloatProp("scaleX", "Scale X", 1.0, 0.1, 10.0, 0.1); + this->addFloatProp("scaleY", "Scale Y", 1.0, 0.1, 10.0, 0.1); + this->addBoolProp ("invert", "Invert", false); + this->addFloatProp("entropy", "Order", 0.0, 0.0, 1.0, 0.01); + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 2.0, 0.01); + this->addFloatProp("thickness", "Line Thickness", 0.05, 0.0, 0.3, 0.005); + + this->setShaderSource(R""""( + // Returns (edgeDist, nearestDiff.xy). + // edgeDist = perpendicular distance to the nearest Voronoi edge. + vec3 voronoiEdge(vec2 scaledUV, float gsX, float gsY) + { + vec2 i_st = floor(scaledUV); + vec2 f_st = fract(scaledUV); + vec2 seedOff = vec2(_seed * 0.173, _seed * 0.251); + + // Pass 1: find nearest cell. + float md = 1e9; + vec2 mg = vec2(0.0); + vec2 mr = vec2(0.0); + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 neighbor = vec2(float(x), float(y)); + vec2 wrapped = mod(i_st + neighbor, vec2(gsX, gsY)); + vec2 point = hash22(wrapped + seedOff); + point = mix(point, vec2(0.5), prop_entropy); + vec2 diff = neighbor + point - f_st; + float dist = length(diff); + if (dist < md) { + md = dist; + mr = diff; + mg = neighbor; + } + } + } + + // Pass 2: distance to bisector of nearest and second-nearest cells. + float edgeDist = 1e9; + for (int j = -2; j <= 2; j++) { + for (int i = -2; i <= 2; i++) { + vec2 neighbor = mg + vec2(float(i), float(j)); + vec2 wrapped = mod(i_st + neighbor, vec2(gsX, gsY)); + vec2 point = hash22(wrapped + seedOff); + point = mix(point, vec2(0.5), prop_entropy); + vec2 diff = neighbor + point - f_st; + if (dot(mr - diff, mr - diff) > 0.00001) + edgeDist = min(edgeDist, + dot(0.5 * (mr + diff), normalize(diff - mr))); + } + } + + return vec3(edgeDist, mr); + } + + vec4 process(vec2 uv) + { + float gsX = max(1.0, floor(prop_scale * prop_scaleX + 0.5)); + float gsY = max(1.0, floor(prop_scale * prop_scaleY + 0.5)); + + vec2 scaledUV = vec2(uv.x * gsX, uv.y * gsY); + vec3 c = voronoiEdge(scaledUV, gsX, gsY); + + // c.x = 0 at edge centre, grows away from it. + // Smooth bright line that fades to black past `thickness`. + float edge = 1.0 - smoothstep(0.0, prop_thickness, c.x); + vec3 color = vec3(edge); + + if (prop_invert) color = 1.0 - color; + return vec4(color * prop_intensity, 1.0); + } + )""""); +} diff --git a/src/texturelab/libraries/v3/makeittile.cpp b/src/texturelab/libraries/v3/makeittile.cpp new file mode 100644 index 00000000..5f397c83 --- /dev/null +++ b/src/texturelab/libraries/v3/makeittile.cpp @@ -0,0 +1,64 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// https://iquilezles.org/articles/texturerepetition/ (complementary approach) +// Adobe Substance Designer "Make It Tile" reference behaviour. +// +// Offset-cross-blend tiling: +// 1. Sample the input at uv +// 2. Sample the input again at fract(uv + 0.5) — a half-period shift +// 3. Blend toward the shifted copy near the tile edges; keep the original +// near the centre. +// +// At any tile boundary (uv.x = 0 or 1, uv.y = 0 or 1) the weight is 0 so the +// output reduces to the shifted sample, which evaluates to texture(0.5, *) on +// both sides of the seam — exactly matching across the boundary. +// Tileability is therefore mathematically guaranteed, not approximate. +// +// `Horizontal` and `Vertical` independently toggle which axis is fixed; +// disable either if the input is already tileable in that direction so the +// node leaves it untouched. +void MakeItTileNode::init() +{ + this->title = "Make It Tile"; + + this->addInput("image"); + + this->addFloatProp("blend_width", "Blend Width", 0.2, 0.02, 0.5, 0.01); + this->addBoolProp ("horizontal", "Horizontal", true); + this->addBoolProp ("vertical", "Vertical", true); + + auto source = R""""( + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.0, 0.0, 0.0, 1.0); + + // Sample the original + vec4 orig = texture(image, uv); + + // Build the half-period shift on enabled axes only. + // Disabled axes use 0 shift, which means orig == shifted along + // that axis — the blend collapses to a no-op for that direction. + vec2 shift = vec2(prop_horizontal ? 0.5 : 0.0, + prop_vertical ? 0.5 : 0.0); + vec2 sUV = fract(uv + shift); + vec4 shifted = texture(image, sUV); + + // Distance to the nearest edge on each enabled axis. + // For disabled axes use 1.0 so the axis never reduces minD. + float dx = prop_horizontal ? min(uv.x, 1.0 - uv.x) : 1.0; + float dy = prop_vertical ? min(uv.y, 1.0 - uv.y) : 1.0; + float minD = min(dx, dy); + + // Blend weight: 0 at the seam → use shifted; 1 in centre → use original + float bw = max(prop_blend_width, 0.001); + float w = smoothstep(0.0, bw, minD); + + return mix(shifted, orig, w); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/maskedblur.cpp b/src/texturelab/libraries/v3/maskedblur.cpp new file mode 100644 index 00000000..d1f20c7d --- /dev/null +++ b/src/texturelab/libraries/v3/maskedblur.cpp @@ -0,0 +1,62 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void MaskedBlurNode::init() +{ + this->title = "Masked Blur"; + this->addInput("image"); + this->addInput("mask"); + + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 10.0, 0.1); + this->addIntProp("samples", "Samples", 50, 1, 100, 1); + this->addFloatProp("opacity", "Opacity", 0.5, 0.0, 1.0, 0.01); + + auto source = R""""( + #define PI 3.14159265359 + #define POW2(x) ((x) * (x)) + + float gaussian(vec2 i, float sigma) + { + return 1.0 / (2.0 * PI * POW2(sigma)) + * exp(-(POW2(i.x) + POW2(i.y)) / (2.0 * POW2(sigma))); + } + + vec3 blur(sampler2D sp, vec2 uv, vec2 scale) + { + vec3 col = vec3(0.0); + float accum = 0.0; + float sigma = float(prop_samples) * 0.25; + int half_samples = prop_samples / 2; + + for (int x = -half_samples; x < half_samples; x++) + for (int y = -half_samples; y < half_samples; y++) + { + vec2 offset = vec2(float(x), float(y)); + float weight = gaussian(offset, sigma); + col += texture(sp, uv + scale * offset).rgb * weight; + accum += weight; + } + + return col / accum; + } + + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.0, 0.0, 0.0, 1.0); + + float maskVal = mask_connected ? 1.0 - texture(mask, uv).r : 1.0; + float blurAmount = 1.0 - maskVal * prop_opacity; + + vec2 ps = vec2(1.0) / _textureSize; + vec4 color; + color.rgb = blur(image, uv, ps * prop_intensity * blurAmount); + color.a = 1.0; + + return color; + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/normalmapv3.cpp b/src/texturelab/libraries/v3/normalmapv3.cpp new file mode 100644 index 00000000..898c0fe5 --- /dev/null +++ b/src/texturelab/libraries/v3/normalmapv3.cpp @@ -0,0 +1,87 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void NormalMapV3Node::init() +{ + this->title = "Normal Map"; + this->addInput("height"); + + this->addEnumProp("filter", "Filter", {"Simple", "Sobel", "Scharr"}); + this->addEnumProp("channel", "Channel", {"Red", "Green", "Blue", "Alpha", "Luminance"}); + this->addFloatProp("strength", "Strength", 1.0, -4.0, 4.0, 0.05); + this->addIntProp("range", "Range", 1, 1, 20, 1); + this->addBoolProp("res_ind", "Resolution Independent", false); + this->addIntProp("ref_res", "Reference Resolution", 1024, 64, 8192, 64); + this->addEnumProp("convention", "Convention", {"OpenGL (Y+)", "DirectX (Y-)"}); + + auto source = R""""( + // Extract a single channel from the height input. + // Channel: 0=R 1=G 2=B 3=A 4=Luminance(BT.709) + float sampleChannel(vec2 uv) + { + vec4 s = texture(height, uv); + if (prop_channel == 1) return s.g; + if (prop_channel == 2) return s.b; + if (prop_channel == 3) return s.a; + if (prop_channel == 4) return dot(s.rgb, vec3(0.2126, 0.7152, 0.0722)); + return s.r; + } + + vec4 process(vec2 uv) + { + vec2 step = (vec2(1.0) / _textureSize) * float(prop_range); + if (prop_res_ind) + step = (vec2(1.0) / float(prop_ref_res)) * float(prop_range); + + // Scale matches V2's effective strength (prop_strength * 0.1 / 2.0). + // The cross-product construction below naturally incorporates step.x + // into the z-component, keeping XY and Z in the same magnitude range. + float scale = prop_strength * 0.05; + float gx, gy; + + if (prop_filter == 0) { + // Simple: forward-difference (3-tap), max response = 1.0 + float c = sampleChannel(uv); + float r = sampleChannel(uv + vec2( step.x, 0.0)); + float u = sampleChannel(uv + vec2( 0.0, step.y)); + gx = r - c; + gy = u - c; + } else { + // 3x3 neighbourhood for Sobel / Scharr + float tl = sampleChannel(uv + vec2(-step.x, step.y)); + float tc = sampleChannel(uv + vec2( 0.0, step.y)); + float tr = sampleChannel(uv + vec2( step.x, step.y)); + float ml = sampleChannel(uv + vec2(-step.x, 0.0)); + float mr = sampleChannel(uv + vec2( step.x, 0.0)); + float bl = sampleChannel(uv + vec2(-step.x, -step.y)); + float bc = sampleChannel(uv + vec2( 0.0, -step.y)); + float br = sampleChannel(uv + vec2( step.x, -step.y)); + + if (prop_filter == 1) { + // Sobel — max response = 4.0, normalise so strength is + // perceptually equivalent to Simple at the same value + gx = (-tl + tr - 2.0*ml + 2.0*mr - bl + br) / 4.0; + gy = ( tl + 2.0*tc + tr - bl - 2.0*bc - br) / 4.0; + } else { + // Scharr — max response = 16.0 + gx = (-3.0*tl + 3.0*tr - 10.0*ml + 10.0*mr - 3.0*bl + 3.0*br) / 16.0; + gy = ( 3.0*tl + 10.0*tc + 3.0*tr - 3.0*bl - 10.0*bc - 3.0*br) / 16.0; + } + } + + // Cross product of surface tangent vectors — step.x in the z slot keeps + // the XY/Z ratio consistent regardless of texture resolution or range. + vec3 dvx = vec3(step.x, 0.0, gx * scale); + vec3 dvy = vec3(0.0, step.y, gy * scale); + vec3 normal = normalize(cross(dvx, dvy)); + + // Convention: OpenGL = Y+ (green-up), DirectX = Y- (green-down) + if (prop_convention == 1) normal.y = -normal.y; + + return vec4(normal * 0.5 + 0.5, 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/perlinnoise.cpp b/src/texturelab/libraries/v3/perlinnoise.cpp new file mode 100644 index 00000000..d66f2312 --- /dev/null +++ b/src/texturelab/libraries/v3/perlinnoise.cpp @@ -0,0 +1,99 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Classic 2D gradient (Perlin) noise, upgraded for v3: +// - Quintic smooth step: C2-continuous, no gradient discontinuities at cell +// edges +// - Normalized pseudo-random gradients via hash22 → unit circle +// - Seamless tiling: integer grid + mod()-wrapped cell indices +// - fBm with per-octave lacunarity / gain controls +// - Three output modes: Standard, Ridged, Billowy +// +// References: +// Perlin, "An Image Synthesizer," SIGGRAPH 1985 +// https://iquilezles.org/articles/gradientnoise/ — gradient noise primer +void PerlinNoiseNode::init() +{ + this->title = "Perlin Noise"; + + auto modeProp = this->addEnumProp("output_mode", "Output", + {"Standard", "Ridged", "Billowy"}); + modeProp->index = 0; + + this->addIntProp("scale", "Scale", 4, 1, 12, 1); + this->addIntProp("octaves", "Octaves", 6, 1, 8, 1); + this->addFloatProp("lacunarity", "Lacunarity", 2.0, 1.5, 4.0, 0.1); + this->addFloatProp("gain", "Gain", 0.5, 0.2, 0.8, 0.05); + + auto source = R""""( + #define OUT_STANDARD 0 + #define OUT_RIDGED 1 + #define OUT_BILLOWY 2 + + // 2D gradient noise. + // gridSize must be a positive integer so that mod() wrapping produces + // seamless tiling: cell (gridSize) hashes identically to cell 0. + float gnoise(vec2 uv, float gridSize) { + vec2 p = uv * gridSize; + vec2 i = floor(p); + vec2 f = fract(p); + + // Quintic interpolant: 6t^5 - 15t^4 + 10t^3 (zero first & second derivative at 0 and 1) + vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); + + vec2 sOff = vec2(_seed * 0.173, _seed * 0.251); + vec2 gs = vec2(gridSize); + + // Unit-circle gradients from hash22 (maps [0,1]^2 → [-1,1]^2, then normalize) + vec2 g00 = normalize(-1.0 + 2.0 * hash22(mod(i, gs) + sOff)); + vec2 g10 = normalize(-1.0 + 2.0 * hash22(mod(i + vec2(1,0), gs) + sOff)); + vec2 g01 = normalize(-1.0 + 2.0 * hash22(mod(i + vec2(0,1), gs) + sOff)); + vec2 g11 = normalize(-1.0 + 2.0 * hash22(mod(i + vec2(1,1), gs) + sOff)); + + // Dot gradient with distance-to-corner vector + float n00 = dot(g00, f); + float n10 = dot(g10, f - vec2(1,0)); + float n01 = dot(g01, f - vec2(0,1)); + float n11 = dot(g11, f - vec2(1,1)); + + // Scale ~1/sqrt(0.5) ≈ 1.41 to stretch typical ±0.7 output to ±1 + return 1.41 * mix(mix(n00, n10, u.x), mix(n01, n11, u.x), u.y); + } + + vec4 process(vec2 uv) { + float value = 0.0; + float amp = 0.5; + float freq = float(prop_scale); + float maxAmp = 0.0; + + for (int i = 0; i < prop_octaves; i++) { + // Round to integer so every octave grid tiles exactly + float gs = max(1.0, floor(freq + 0.5)); + float n = gnoise(uv, gs); // in approximately [-1, 1] + + if (prop_output_mode == OUT_RIDGED) + n = 1.0 - abs(n); // sharp ridges at zero-crossings + else if (prop_output_mode == OUT_BILLOWY) + n = abs(n); // rounded bumps everywhere + + value += amp * n; + maxAmp += amp; + freq *= prop_lacunarity; + amp *= prop_gain; + } + + float f = value / maxAmp; + + float result; + if (prop_output_mode == OUT_STANDARD) + result = clamp(0.5 + 0.5 * f, 0.0, 1.0); + else + result = clamp(f, 0.0, 1.0); + + return vec4(vec3(result), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/perlinnoise3d.cpp b/src/texturelab/libraries/v3/perlinnoise3d.cpp new file mode 100644 index 00000000..ef674cce --- /dev/null +++ b/src/texturelab/libraries/v3/perlinnoise3d.cpp @@ -0,0 +1,151 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// 3D gradient (Perlin) noise for texture work. +// +// The core insight: sampling a 2D cross-section through a 3D noise volume +// gives patterns that a purely 2D noise cannot — specifically wood grain and +// marble veins, the two applications Perlin described in his original 1985 +// paper. The Z Slice prop lets you navigate through the volume; rotating +// the cut angle gives completely different grain/vein patterns from the same +// seed, just as cutting a log at different angles produces different grain. +// +// Tiling: XY cell indices are mod()-wrapped at the integer grid boundary so +// the noise repeats seamlessly. Z is intentionally left free — tiling in Z +// would create visible repetition along the depth axis. +// +// Modes: +// Standard — raw fBm, remapped to [0,1] +// Ridged — 1 - |n| per octave → sharp mountain-ridge crests +// Billowy — |n| per octave → rounded pillow-cloud bumps +// Wood — noise-distorted concentric rings (log cross-section) +// Marble — noise-distorted sine stripes (classic Perlin marble veining) +// +// References: +// Perlin, "An Image Synthesizer," SIGGRAPH 1985 +// https://iquilezles.org/articles/gradientnoise/ +void PerlinNoise3DNode::init() +{ + this->title = "Perlin Noise 3D"; + + auto modeProp = this->addEnumProp("output_mode", "Output", + {"Standard", "Ridged", "Billowy", + "Wood", "Marble"}); + modeProp->index = 0; + + this->addIntProp ("scale", "Scale", 4, 1, 12, 1); + this->addFloatProp("z_offset", "Z Slice", 0.0, 0.0, 10.0, 0.1); + this->addIntProp ("octaves", "Octaves", 6, 1, 8, 1); + this->addFloatProp("lacunarity", "Lacunarity", 2.0, 1.5, 4.0, 0.1); + this->addFloatProp("gain", "Gain", 0.5, 0.2, 0.8, 0.05); + this->addFloatProp("distortion", "Distortion", 2.0, 0.0, 8.0, 0.1); + + auto source = R""""( + #define OUT_STANDARD 0 + #define OUT_RIDGED 1 + #define OUT_BILLOWY 2 + #define OUT_WOOD 3 + #define OUT_MARBLE 4 + + // Unit-sphere gradient for a 3D cell. + // XY indices are mod()-wrapped for seamless XY tiling; Z is free. + vec3 grad3(vec3 cell, float gridSize) { + vec2 wrapped = mod(cell.xy, vec2(gridSize)); + vec3 p = vec3(wrapped, cell.z) + + vec3(_seed * 0.173, _seed * 0.251, _seed * 0.317); + p = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p += dot(p, p.yzx + 33.33); + return normalize(fract((p.xxy + p.yxx) * p.zyx) * 2.0 - 1.0); + } + + // 3D gradient noise with quintic interpolation. + // Scale factor 1.155 ≈ 1/sqrt(0.75): normalises the theoretical + // max of sqrt(3/4) for unit gradients in 3D to approximately ±1. + float gnoise3(vec2 uv, float z, float gridSize) { + vec3 p = vec3(uv * gridSize, z); + vec3 i = floor(p); + vec3 f = fract(p); + vec3 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); + + float n000 = dot(grad3(i + vec3(0,0,0), gridSize), f - vec3(0,0,0)); + float n100 = dot(grad3(i + vec3(1,0,0), gridSize), f - vec3(1,0,0)); + float n010 = dot(grad3(i + vec3(0,1,0), gridSize), f - vec3(0,1,0)); + float n110 = dot(grad3(i + vec3(1,1,0), gridSize), f - vec3(1,1,0)); + float n001 = dot(grad3(i + vec3(0,0,1), gridSize), f - vec3(0,0,1)); + float n101 = dot(grad3(i + vec3(1,0,1), gridSize), f - vec3(1,0,1)); + float n011 = dot(grad3(i + vec3(0,1,1), gridSize), f - vec3(0,1,1)); + float n111 = dot(grad3(i + vec3(1,1,1), gridSize), f - vec3(1,1,1)); + + return 1.155 * mix( + mix(mix(n000, n100, u.x), mix(n010, n110, u.x), u.y), + mix(mix(n001, n101, u.x), mix(n011, n111, u.x), u.y), + u.z); + } + + // fBm over gnoise3. Ridged/Billowy transformations are applied + // per-octave so the feedback shapes fine detail correctly. + // Wood/Marble use the raw accumulated noise as turbulence. + float fbm3(vec2 uv, float z) { + float value = 0.0; + float amp = 0.5; + float freq = float(prop_scale); + float zScale = 1.0; + float maxAmp = 0.0; + + for (int i = 0; i < prop_octaves; i++) { + float gs = max(1.0, floor(freq + 0.5)); + float n = gnoise3(uv, z * zScale, gs); + + if (prop_output_mode == OUT_RIDGED) + n = 1.0 - abs(n); + else if (prop_output_mode == OUT_BILLOWY) + n = abs(n); + + value += amp * n; + maxAmp += amp; + freq *= prop_lacunarity; + zScale *= prop_lacunarity; + amp *= prop_gain; + } + + return value / maxAmp; + } + + vec4 process(vec2 uv) { + float f = fbm3(uv, prop_z_offset); + + float result; + + if (prop_output_mode == OUT_STANDARD) { + result = clamp(0.5 + 0.5 * f, 0.0, 1.0); + + } else if (prop_output_mode == OUT_RIDGED || + prop_output_mode == OUT_BILLOWY) { + result = clamp(f, 0.0, 1.0); + + } else if (prop_output_mode == OUT_WOOD) { + // Concentric rings centered on UV (0.5, 0.5), distorted by + // the noise turbulence. Scale drives ring frequency; + // Distortion controls how wavy the rings become. + float rings = length(uv - 0.5) * float(prop_scale) * 2.0 + + f * prop_distortion; + result = 0.5 + 0.5 * cos(rings * 6.28318530); + + } else { // MARBLE + // Horizontal bands distorted by noise turbulence — the + // canonical Perlin marble. Scale controls vein frequency; + // Distortion controls how much the noise bends the veins. + float vein = uv.x * float(prop_scale) + + f * prop_distortion; + float m = sin(vein * 3.14159265); + // Gamma curve sharpens vein edges for a more realistic look + result = clamp(pow(abs(m), 0.6) * sign(m) * 0.5 + 0.5, 0.0, 1.0); + } + + return vec4(vec3(result), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/rays.cpp b/src/texturelab/libraries/v3/rays.cpp new file mode 100644 index 00000000..11a90370 --- /dev/null +++ b/src/texturelab/libraries/v3/rays.cpp @@ -0,0 +1,29 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void RaysNode::init() +{ + this->title = "Rays"; + + this->addIntProp("sides", "Sides", 3, 1, 32, 1); + this->addFloatProp("angle", "Angle", 0.0, 0.0, 360.0, 1.0); + this->addFloatProp("translateX", "Translate X", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("translateY", "Translate Y", 0.5, 0.0, 1.0, 0.01); + + auto source = R""""( + #define PI 3.14159265359 + + vec4 process(vec2 uv) + { + uv -= vec2(prop_translateX, prop_translateY); + + float a = atan(uv.x, uv.y) + radians(prop_angle); + float shape = sin(a * float(prop_sides)); + + return vec4(vec3(shape), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/roughgrain.cpp b/src/texturelab/libraries/v3/roughgrain.cpp new file mode 100644 index 00000000..64f7a0a6 --- /dev/null +++ b/src/texturelab/libraries/v3/roughgrain.cpp @@ -0,0 +1,92 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Rough Grain — dense field of fine granular dots/specks. +// Produced by layered anisotropic noise with random per-layer orientation and +// per-column phase shifts; the high-frequency phase aliasing breaks the +// would-be streaks into isotropic grains. +// Uses the engine-wide _seed uniform so a single Random Seed change in the +// project regenerates this grain consistently with other procedural nodes. +// Use as: micro-roughness variation on painted surfaces, sandblasted metal +// base, fine textile noise, or the granular substrate of any material. +void RoughGrainNode::init() +{ + this->title = "Rough Grain"; + + this->addInput("mask"); + + this->addIntProp ("density", "Density", 400, 10, 2000, 10); + this->addFloatProp("grain_size", "Grain Size", 0.04, 0.001, 0.3, 0.001); + this->addFloatProp("variation", "Variation", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 2.0, 0.05); + this->addIntProp ("layers", "Layers", 2, 1, 4, 1); + + auto source = R""""( + #define PI 3.14159265358979 + + // 2-D hash with engine-wide seed offset + float grainHash(vec2 p) { + p += vec2(_seed * 0.127, _seed * 0.311); + return hash12(p); + } + + // One layer of anisotropic noise at a given orientation and frequency. + // When the perpendicular axis is stretched far more than the primary + // axis, the per-column random phase aliases the would-be streaks into + // tightly packed dots — that grain effect is the whole point. + float grainLayer(vec2 uv, float freq, float angleRad) { + float cosA = cos(angleRad); + float sinA = sin(angleRad); + + vec2 rot = vec2(uv.x * cosA + uv.y * sinA, + -uv.x * sinA + uv.y * cosA); + + // Heavy anisotropic scaling (drives the dot aliasing) + rot.x *= freq * 0.05; + rot.y *= freq * 10.0; + + // Per-column phase perturbation + float perturb = (grainHash(vec2(rot.x, 0.0)) * 2.0 - 1.0) + * prop_variation * 0.5; + rot.y += perturb * freq; + + float phase = grainHash(vec2(floor(rot.y * float(prop_density)), 0.731)); + + float pos = fract(rot.y * float(prop_density) + phase); + float halfW = prop_grain_size * 0.5; + float grain = smoothstep(0.5 - halfW, 0.5, pos) + * (1.0 - smoothstep(0.5, 0.5 + halfW, pos)); + + return grain; + } + + vec4 process(vec2 uv) + { + float result = 0.0; + float weight = 1.0; + float totalW = 0.0; + + for (int i = 0; i < prop_layers; i++) { + // Random orientation per layer — multi-directional dot field + float layerAngle = grainHash(vec2(float(i) * 3.7, 1.1)) * PI * 2.0; + float freq = 1.0 + float(i) * 0.5; + + result += grainLayer(uv, freq, layerAngle) * weight; + totalW += weight; + weight *= 0.55; + } + + result = (totalW > 0.0) ? result / totalW : 0.0; + result = clamp(result * prop_intensity, 0.0, 1.0); + + if (mask_connected) { + result *= texture(mask, uv).r; + } + + return vec4(vec3(result), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/setalpha.cpp b/src/texturelab/libraries/v3/setalpha.cpp new file mode 100644 index 00000000..7c6a3d4c --- /dev/null +++ b/src/texturelab/libraries/v3/setalpha.cpp @@ -0,0 +1,51 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Quick utility to attach an alpha channel to a texture: RGB comes from +// `rgba`, alpha comes from a chosen channel of `alpha`. Saves building a +// 4-input RGBA Merge graph just to swap one channel. +void SetAlphaNode::init() +{ + this->title = "Set Alpha"; + + this->addInput("rgba"); + this->addInput("alpha"); + + auto prop = this->addEnumProp( + "alphaChannel", "Alpha Channel", + {"Red", "Green", "Blue", "Alpha", "Average (RGB)"}); + prop->setValue(0); + + this->addBoolProp("invert", "Invert Alpha", false); + + auto source = R""""( + float getChannel(vec4 inputData, int mode) + { + if (mode == 0) return inputData.r; + if (mode == 1) return inputData.g; + if (mode == 2) return inputData.b; + if (mode == 3) return inputData.a; + if (mode == 4) { + return (inputData.r + inputData.g + inputData.b) * 0.3333333; + } + + return 0.0; + } + + vec4 process(vec2 uv) + { + vec3 rgb = rgba_connected ? texture(rgba, uv).rgb : vec3(0.0); + + float a = 1.0; + if (alpha_connected) { + a = getChannel(texture(alpha, uv), prop_alphaChannel); + if (prop_invert) a = 1.0 - a; + } + + return vec4(rgb, a); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/solidcellv3.cpp b/src/texturelab/libraries/v3/solidcellv3.cpp new file mode 100644 index 00000000..6fdf811b --- /dev/null +++ b/src/texturelab/libraries/v3/solidcellv3.cpp @@ -0,0 +1,56 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Voronoi solid-fill cell node with independent X/Y scale. +// Each cell gets a unique random grayscale value derived from its wrapped index, +// so opposite UV boundaries share identical cell hashes — seamless tiling. +void SolidCellV3Node::init() +{ + this->title = "Solid Cell"; + + this->addFloatProp("scale", "Scale", 30.0, 1.0, 256.0, 1.0); + this->addFloatProp("scaleX", "Scale X", 1.0, 0.1, 10.0, 0.1); + this->addFloatProp("scaleY", "Scale Y", 1.0, 0.1, 10.0, 0.1); + this->addBoolProp ("invert", "Invert", false); + this->addFloatProp("entropy", "Order", 0.0, 0.0, 1.0, 0.01); + this->addFloatProp("intensity", "Intensity", 1.0, 0.0, 2.0, 0.01); + + this->setShaderSource(R""""( + vec4 process(vec2 uv) + { + float gsX = max(1.0, floor(prop_scale * prop_scaleX + 0.5)); + float gsY = max(1.0, floor(prop_scale * prop_scaleY + 0.5)); + + vec2 scaledUV = vec2(uv.x * gsX, uv.y * gsY); + vec2 i_st = floor(scaledUV); + vec2 f_st = fract(scaledUV); + + vec2 seedOff = vec2(_seed * 0.173, _seed * 0.251); + float m_dist = 1e9; + vec2 closestWrapped = vec2(0.0); + + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 neighbor = vec2(float(x), float(y)); + vec2 wrapped = mod(i_st + neighbor, vec2(gsX, gsY)); + vec2 point = hash22(wrapped + seedOff); + point = mix(point, vec2(0.5), prop_entropy); + vec2 diff = neighbor + point - f_st; + float dist = length(diff); + if (dist < m_dist) { + m_dist = dist; + closestWrapped = wrapped; + } + } + } + + // Use a second hash to generate per-cell color so it is independent + // of the cell-point position hash used above. + float cellColor = hash22(closestWrapped + vec2(_seed * 0.391, _seed * 0.617)).x; + vec3 color = vec3(cellColor); + if (prop_invert) color = 1.0 - color; + return vec4(color * prop_intensity, 1.0); + } + )""""); +} diff --git a/src/texturelab/libraries/v3/spread.cpp b/src/texturelab/libraries/v3/spread.cpp new file mode 100644 index 00000000..f947d1cc --- /dev/null +++ b/src/texturelab/libraries/v3/spread.cpp @@ -0,0 +1,220 @@ +#include "../../graphics/noderenderer.h" +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +#include +#include + +// Color Spread — JFA nearest-seed lookup that samples a color map instead of +// computing a distance value. Each masked-out (black) pixel receives the exact +// color of the closest foreground pixel in the mask, producing solid filled +// regions with no gradient blending. +// +// Use case: flood-fill a pattern with random intensities, then spread those +// colors outward to cover the lines/gaps in the pattern. + +// ============================================================================ +// ColorSpreadRenderData +// ============================================================================ +struct ColorSpreadRenderData : public NodeRenderData { + float threshold = 0.5f; + float distance = 1.0f; +}; + +// ============================================================================ +// ColorSpreadRenderer +// ============================================================================ +class ColorSpreadRenderer : public NodeTextureRenderer { +public: + void render(NodeRenderContext& ctx, const NodeRenderData& baseData) override + { + auto& data = static_cast(baseData); + auto gl = ctx.gl; + auto cache = ctx.cache; + int w = ctx.textureWidth; + int h = ctx.textureHeight; + + // Require at least the mask input; color map is optional (falls back to + // mask) + if (ctx.inputs.isEmpty() || ctx.inputs[0].textureId == 0) { + cache->bindFboToTexture(ctx.outputTextureId); + gl->glViewport(0, 0, w, h); + gl->glClearColor(0, 0, 0, 1); + gl->glClear(GL_COLOR_BUFFER_BIT); + return; + } + + GLuint maskTex = ctx.inputs[0].textureId; + GLuint colorTex = + (ctx.inputs.size() > 1 && ctx.inputs[1].textureId != 0) + ? ctx.inputs[1].textureId + : maskTex; + + GLuint seedShader = cache->getOrCompileShader( + "cs_seed", RenderResourceCache::standardVertexSource(), seedFrag()); + GLuint jfaShader = cache->getOrCompileShader( + "cs_jfa", RenderResourceCache::standardVertexSource(), jfaFrag()); + GLuint sampleShader = cache->getOrCompileShader( + "cs_sample", RenderResourceCache::standardVertexSource(), + sampleFrag()); + + GLuint texA = cache->acquireTexture(w, h); + GLuint texB = cache->acquireTexture(w, h); + + // Seed pass: foreground pixels (mask >= threshold) write their UV; + // background pixels write the sentinel (-1, -1). + cache->bindFboToTexture(texA); + ctx.useShader(seedShader); + ctx.bindTexture(seedShader, "u_mask", maskTex, 0); + gl->glUniform1f(gl->glGetUniformLocation(seedShader, "u_threshold"), + data.threshold); + ctx.drawQuad(); + + // JFA passes: propagate nearest-seed UV across the texture + int maxDim = std::max(w, h); + int stepSize = maxDim / 2; + while (stepSize >= 1) { + cache->bindFboToTexture(texB); + ctx.useShader(jfaShader); + ctx.bindTexture(jfaShader, "u_input", texA, 0); + gl->glUniform1i(gl->glGetUniformLocation(jfaShader, "u_step"), + stepSize); + ctx.drawQuad(); + std::swap(texA, texB); + stepSize /= 2; + } + + // Sample pass: look up the color map at the nearest-seed UV + cache->bindFboToTexture(ctx.outputTextureId); + ctx.useShader(sampleShader); + ctx.bindTexture(sampleShader, "u_jfa", texA, 0); + ctx.bindTexture(sampleShader, "u_color", colorTex, 1); + gl->glUniform1f(gl->glGetUniformLocation(sampleShader, "u_distance"), + data.distance); + ctx.drawQuad(); + + cache->releaseTexture(texA); + cache->releaseTexture(texB); + } + +private: + static QString seedFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_mask; + uniform float u_threshold; + + void main() + { + float v = texture(u_mask, v_texCoord).r; + if (v >= u_threshold) + fragColor = vec4(v_texCoord, 0.0, 1.0); + else + fragColor = vec4(-1.0, -1.0, 0.0, 1.0); + } + )""""; + } + + static QString jfaFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_input; + uniform int u_step; + + void main() + { + vec2 texSize = vec2(textureSize(u_input, 0)); + vec2 step = vec2(float(u_step)) / texSize; + + vec2 bestUV = vec2(-1.0); + float bestDist = 1e9; + + for (int x = -1; x <= 1; x++) { + for (int y = -1; y <= 1; y++) { + vec2 sampleUV = v_texCoord + vec2(float(x), float(y)) * step; + vec4 s = texture(u_input, sampleUV); + vec2 seedUV = s.rg; + if (seedUV.x < 0.0) continue; + float d = length(v_texCoord - seedUV); + if (d < bestDist) { + bestDist = d; + bestUV = seedUV; + } + } + } + + fragColor = vec4(bestUV, 0.0, 1.0); + } + )""""; + } + + // Sample the color map at the nearest-seed UV for a solid, gradient-free + // fill + static QString sampleFrag() + { + return R""""( + #version 150 + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D u_jfa; + uniform sampler2D u_color; + uniform float u_distance; + + void main() + { + vec2 seedUV = texture(u_jfa, v_texCoord).rg; + + if (seedUV.x < 0.0) { + fragColor = vec4(0.0, 0.0, 0.0, 1.0); + } else { + float d = length(v_texCoord - seedUV); + if (d > u_distance * 0.5) + fragColor = vec4(0.0, 0.0, 0.0, 1.0); + else + fragColor = texture(u_color, seedUV); + } + } + )""""; + } +}; + +// ============================================================================ +// ColorSpreadNode +// ============================================================================ +void ColorSpreadNode::init() +{ + this->title = "Spread"; + + this->addInput("color"); + this->addInput("mask"); + + this->addFloatProp("threshold", "Threshold", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("distance", "Distance", 1.0, 0.0, 1.0, 0.01); +} + +std::shared_ptr ColorSpreadNode::createRenderer() +{ + return std::make_shared(); +} + +std::shared_ptr ColorSpreadNode::createRenderData() +{ + auto data = std::make_shared(); + + if (auto* p = dynamic_cast(getProp("threshold"))) + data->threshold = static_cast(p->value); + if (auto* p = dynamic_cast(getProp("distance"))) + data->distance = static_cast(p->value); + + return data; +} diff --git a/src/texturelab/libraries/v3/swirl.cpp b/src/texturelab/libraries/v3/swirl.cpp new file mode 100644 index 00000000..e66c4486 --- /dev/null +++ b/src/texturelab/libraries/v3/swirl.cpp @@ -0,0 +1,36 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +void SwirlNode::init() +{ + this->title = "Swirl"; + this->addInput("image"); + + this->addFloatProp("radius", "Radius", 0.7, 0.0, 1.0, 0.01); + this->addFloatProp("angle", "Angle", 90.0, 0.0, 360.0, 0.1); + this->addFloatProp("centerX", "Center X", 0.5, 0.0, 1.0, 0.01); + this->addFloatProp("centerY", "Center Y", 0.5, 0.0, 1.0, 0.01); + + auto source = R""""( + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.0, 0.0, 0.0, 1.0); + + vec2 center = vec2(prop_centerX, prop_centerY); + vec2 delta = uv - center; + float len = length(delta); + float effectAngle = radians(prop_angle); + + float swirlAmount = effectAngle * smoothstep(prop_radius, 0.0, len); + float a = atan(delta.y, delta.x) + swirlAmount; + + vec2 swirlUV = center + vec2(cos(a), sin(a)) * len; + + return texture(image, swirlUV); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/toongradient.cpp b/src/texturelab/libraries/v3/toongradient.cpp new file mode 100644 index 00000000..dde1d002 --- /dev/null +++ b/src/texturelab/libraries/v3/toongradient.cpp @@ -0,0 +1,48 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Toon / posterised gradient — remaps greyscale input to a stepped, +// band-mapped gradient. Each band maps to a colour stop in the gradient +// prop, giving crisp toon-shading zones while preserving artist colour +// control. Critical for R&C-style roughness (3-band) and albedo (4-6 band) +// maps that read as stylised even up close. +void ToonGradientNode::init() +{ + this->title = "Toon Gradient"; + + this->addInput("image"); + + this->addIntProp ("bands", "Bands", 4, 2, 16, 1); + this->addFloatProp("softness", "Edge Softness", 0.05, 0.0, 0.5, 0.01); + this->addGradientProp("gradient", "Gradient", Gradient::defaultGradient()); + + auto source = R""""( + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.0, 0.0, 0.0, 1.0); + + float input = texture(image, uv).r; + float bands = float(prop_bands); + + // Quantise to N bands + float bandIdx = floor(input * bands); + float bandFract = fract(input * bands); + + // Soft transition between bands using smoothstep on the intra-band fraction + float soft = smoothstep(0.0, prop_softness * bands, bandFract) + * (1.0 - smoothstep((1.0 - prop_softness) * bands, + bands, bandFract + bandIdx * 1.0)); + + // Map to [0,1] for gradient sampling + float t = (bandIdx + clamp(soft, 0.0, 1.0)) / bands; + t = clamp(t, 0.0, 1.0); + + vec3 col = sampleGradient(prop_gradient, t); + return vec4(col, 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/truchet.cpp b/src/texturelab/libraries/v3/truchet.cpp new file mode 100644 index 00000000..fc344826 --- /dev/null +++ b/src/texturelab/libraries/v3/truchet.cpp @@ -0,0 +1,98 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Smith & Bouvier, "Truchet Tilings Revisited," The Mathematical Intelligencer 1987 +// https://iquilezles.org/articles/truchet/ +// Truchet tile patterns — per-cell randomly oriented arcs or diagonal lines +// that form continuous curved traces across the grid. Used in sci-fi surface +// tech patterns, circuit board traces and alien architecture detail. +// +// Scale is an integer (cells per UV tile), which combined with the Truchet +// edge-midpoint property guarantees seamless UV tiling: every cell edge +// crosses curves at the same midpoint, so adjacent tiles always connect +// smoothly regardless of per-cell orientation. +void TruchetNode::init() +{ + this->title = "Truchet"; + + this->addIntProp ("scale", "Scale", 8, 2, 32, 1); + this->addFloatProp("line_width", "Line Width", 0.08, 0.01, 0.4, 0.01); + + auto variantProp = this->addEnumProp("variant", "Variant", + {"Arc", "Diagonal", "Diagonal Mirrored"}); + variantProp->index = 0; + + auto source = R""""( + #define VARIANT_ARC 0 + #define VARIANT_DIAG 1 + #define VARIANT_DIAGMIR 2 + + // Signed distance from a line segment for anti-aliased lines + float lineSDF(vec2 p, vec2 a, vec2 b) { + vec2 ab = b - a; + vec2 ap = p - a; + float t = clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0); + return length(ap - ab * t); + } + + vec4 process(vec2 uv) + { + vec2 scaled = uv * float(prop_scale); + vec2 cell = floor(scaled); + vec2 local = fract(scaled) - 0.5; // centred at (0,0) in [-0.5, 0.5] + + // Random orientation per cell (0 or 1), driven by engine seed + float r = hash12(cell + vec2(_seed * 0.179, _seed * 0.413)); + float orient = step(0.5, r); // 0.0 or 1.0 + + float d = 1.0; + float lw = prop_line_width * 0.5; + float radius = 0.5; + + if (prop_variant == VARIANT_ARC) { + // Two quarter-circle arcs connecting midpoints of adjacent edges + float d1, d2; + if (orient < 0.5) { + d1 = abs(length(local - vec2(-0.5, -0.5)) - radius); + d2 = abs(length(local - vec2( 0.5, 0.5)) - radius); + } else { + d1 = abs(length(local - vec2( 0.5, -0.5)) - radius); + d2 = abs(length(local - vec2(-0.5, 0.5)) - radius); + } + d = min(d1, d2); + } else if (prop_variant == VARIANT_DIAG) { + // Midpoint-to-midpoint diagonals (Smith-Truchet variant). + // Two short segments connect adjacent edge midpoints; every + // cell touches all 4 edge midpoints regardless of orientation, + // so adjacent cells always connect smoothly. + float d1, d2; + if (orient < 0.5) { + // Upper-left and lower-right segments + d1 = lineSDF(local, vec2(-0.5, 0.0), vec2(0.0, 0.5)); + d2 = lineSDF(local, vec2( 0.5, 0.0), vec2(0.0, -0.5)); + } else { + // Lower-left and upper-right segments (mirror) + d1 = lineSDF(local, vec2(-0.5, 0.0), vec2(0.0, -0.5)); + d2 = lineSDF(local, vec2( 0.5, 0.0), vec2(0.0, 0.5)); + } + d = min(d1, d2); + } else { + // Two corner-to-corner diagonals forming an X in every cell. + // The X always touches all 4 corners of the cell, so adjacent + // cells' diagonals always meet at the shared corner. + float d1 = lineSDF(local, vec2(-0.5, -0.5), vec2( 0.5, 0.5)); + float d2 = lineSDF(local, vec2( 0.5, -0.5), vec2(-0.5, 0.5)); + d = min(d1, d2); + } + + // Anti-aliased line with smoothstep + float aa = fwidth(d); + float line = 1.0 - smoothstep(lw - aa, lw + aa, d); + + return vec4(vec3(line), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/vibrance.cpp b/src/texturelab/libraries/v3/vibrance.cpp new file mode 100644 index 00000000..f8737a04 --- /dev/null +++ b/src/texturelab/libraries/v3/vibrance.cpp @@ -0,0 +1,50 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// https://en.wikipedia.org/wiki/Colorfulness +// Vibrance selectively boosts the saturation of low-saturation pixels while +// protecting already-saturated hues from over-saturation clipping. +// Prefer Vibrance over raw Saturation for artist-safe color enhancement. +void VibranceNode::init() +{ + this->title = "Vibrance"; + + this->addInput("image"); + + this->addFloatProp("vibrance", "Vibrance", 0.3, -1.0, 1.0, 0.05); + this->addFloatProp("saturation", "Saturation", 0.0, -1.0, 1.0, 0.05); + + auto source = R""""( + vec4 process(vec2 uv) + { + if (!image_connected) + return vec4(0.0, 0.0, 0.0, 1.0); + + vec4 col = texture(image, uv); + float lum = dot(col.rgb, vec3(0.2126, 0.7152, 0.0722)); + vec3 gray = vec3(lum); + + // --- Uniform saturation (applied first) --- + vec3 saturated = mix(gray, col.rgb, 1.0 + prop_saturation); + + // --- Vibrance: pixel's current saturation range [0..1] --- + float maxC = max(saturated.r, max(saturated.g, saturated.b)); + float minC = min(saturated.r, min(saturated.g, saturated.b)); + float sat = maxC - minC; // 0 = fully gray, 1 = fully saturated + + // Protection factor: low-sat pixels get most boost, saturated pixels get none + float protection = 1.0 - sat; + + // Apply vibrance modulated by protection factor + float boostLum = dot(saturated, vec3(0.2126, 0.7152, 0.0722)); + vec3 boostGray = vec3(boostLum); + vec3 result = mix(boostGray, saturated, + 1.0 + prop_vibrance * protection); + + return vec4(clamp(result, 0.0, 1.0), col.a); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/libraries/v3/voronoifractal.cpp b/src/texturelab/libraries/v3/voronoifractal.cpp new file mode 100644 index 00000000..f4ae6b9e --- /dev/null +++ b/src/texturelab/libraries/v3/voronoifractal.cpp @@ -0,0 +1,113 @@ +#include "../../models.h" +#include "../../props.h" +#include "../libv3.h" + +// Worley, "A Cellular Texture Basis Function," SIGGRAPH 1996 +// https://iquilezles.org/articles/voronoilines/ +// Multi-octave Worley noise (fractal Voronoi). Each octave doubles the +// frequency and halves the amplitude (Lacunarity / Gain). +// Output modes: F1 = distance to nearest cell (soft blobs), +// F2 = second nearest, F2-F1 = cell edges only (crack lines), +// F1+F2 = softer combined cells. +// +// Seamless tiling: each octave's frequency is snapped to the nearest integer +// grid size, and cell hashes use mod()-wrapped indices. This makes the +// pattern repeat exactly across UV tile boundaries. Frequencies are +// effectively rounded — at small Scale values (1-3) you may notice slight +// snapping, but the pattern tiles perfectly at every setting. +void VoronoiFractalNode::init() +{ + this->title = "Voronoi Fractal"; + + this->addFloatProp("scale", "Scale", 4.0, 0.5, 16.0, 0.5); + this->addIntProp ("octaves", "Octaves", 4, 1, 8, 1); + this->addFloatProp("lacunarity", "Lacunarity", 2.0, 1.5, 4.0, 0.1); + this->addFloatProp("gain", "Gain", 0.5, 0.2, 0.8, 0.05); + + auto distProp = this->addEnumProp("distance", "Distance Func", + {"Euclidean", "Manhattan", "Chebyshev"}); + distProp->index = 0; + + auto outProp = this->addEnumProp("output_mode", "Output", + {"F1", "F2", "F2-F1", "F1+F2"}); + outProp->index = 0; + + auto source = R""""( + #define DIST_EUCLIDEAN 0 + #define DIST_MANHATTAN 1 + #define DIST_CHEBYSHEV 2 + #define OUT_F1 0 + #define OUT_F2 1 + #define OUT_F2F1 2 + #define OUT_F1F2 3 + + // Cell point at index `cellIdx` on a periodic grid of size `gridSize`. + // The hash uses mod()-wrapped indices so cells at the tile boundary + // (e.g. cell 0 and cell gridSize) share the same random offset — + // the prerequisite for seamless tiling. + vec2 cellPoint(vec2 cellIdx, float gridSize) { + vec2 wrapped = mod(cellIdx, vec2(gridSize)); + return cellIdx + hash22(wrapped + vec2(_seed * 0.193, _seed * 0.457)); + } + + float cellDist(vec2 a, vec2 b) { + vec2 d = abs(a - b); + if (prop_distance == DIST_MANHATTAN) return d.x + d.y; + if (prop_distance == DIST_CHEBYSHEV) return max(d.x, d.y); + return length(a - b); // Euclidean + } + + // Returns (F1, F2) for UV sampled on an integer-sized grid. + vec2 voronoi(vec2 uv, float gridSize) { + vec2 sampleUV = uv * gridSize; + vec2 cell = floor(sampleUV); + float f1 = 1e9, f2 = 1e9; + + for (int x = -2; x <= 2; x++) { + for (int y = -2; y <= 2; y++) { + vec2 nb = vec2(float(x), float(y)); + vec2 pt = cellPoint(cell + nb, gridSize); + float d = cellDist(sampleUV, pt); + if (d < f1) { f2 = f1; f1 = d; } + else if (d < f2) { f2 = d; } + } + } + // Normalise: in Euclidean space max F1 for uniform random points ≈ 0.67 + return vec2(f1, f2) / 0.67; + } + + vec4 process(vec2 uv) + { + float value = 0.0; + float amplitude = 1.0; + float freq = prop_scale; + float totalAmp = 0.0; + + for (int i = 0; i < prop_octaves; i++) { + // Snap frequency to integer grid size for seamless tiling + float gridSize = max(1.0, floor(freq + 0.5)); + vec2 f = voronoi(uv, gridSize); + + float octaveVal = 0.0; + if (prop_output_mode == OUT_F1) + octaveVal = f.x; + else if (prop_output_mode == OUT_F2) + octaveVal = f.y; + else if (prop_output_mode == OUT_F2F1) + octaveVal = clamp(f.y - f.x, 0.0, 1.0); + else + octaveVal = clamp((f.x + f.y) * 0.5, 0.0, 1.0); + + value += octaveVal * amplitude; + totalAmp += amplitude; + freq *= prop_lacunarity; + amplitude *= prop_gain; + } + + float result = clamp(value / totalAmp, 0.0, 1.0); + return vec4(vec3(result), 1.0); + } + )""""; + + this->setShaderSource(source); +} diff --git a/src/texturelab/main.cpp b/src/texturelab/main.cpp new file mode 100644 index 00000000..da9c2986 --- /dev/null +++ b/src/texturelab/main.cpp @@ -0,0 +1,215 @@ +#include "catalogservice.h" +#include "mainwindow.h" +#include "systeminfo.h" +#include "telemetry.h" +#include "thememanager.h" +#include "version.h" + +#include +#include +#include +#include + +#include +#include + +// Hints that a dedicated GPU should be used whenever possible +// https://stackoverflow.com/a/39047129/991834 +#ifdef Q_OS_WIN +extern "C" { +__declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +} +#endif + +// Forward qWarning/qCritical as Sentry breadcrumbs; qFatal as a captured event +// so we get context before Crashpad's abort handler fires. +static void qtMessageHandler(QtMsgType type, const QMessageLogContext& /*ctx*/, + const QString& msg) +{ + switch (type) { + case QtDebugMsg: + break; + case QtInfoMsg: + Telemetry::breadcrumb("qt.info", msg.toStdString()); + break; + case QtWarningMsg: + Telemetry::breadcrumb("qt.warning", msg.toStdString()); + break; + case QtCriticalMsg: + Telemetry::breadcrumb("qt.critical", msg.toStdString()); + break; + case QtFatalMsg: + Telemetry::captureException("qFatal: " + msg.toStdString()); + // Allow default abort() so Crashpad captures the minidump + abort(); + } +} + +#if defined(_MSC_VER) +#define TL_NOINLINE __declspec(noinline) +#else +#define TL_NOINLINE __attribute__((noinline)) +#endif + +// Deliberately dereference a null pointer so Crashpad captures a minidump. +// Kept in a named, non-inlined function so the symbolicated Sentry stack trace +// shows a recognizable frame. Triggered only via the --sentry-crash-test flag; +// remove this hook once symbolication is confirmed in production. +TL_NOINLINE static void sentryCrashTest() +{ + volatile int* p = nullptr; + *p = 0xC0FFEE; +} + +// Force a consistent dark theme on every platform, independent of the host +// system theme. Loads the design-token theme (Fusion + dark palette + app +// stylesheet) from resources so it works even in a minimal Linux AppImage that +// has no desktop theme plugin — which is why CI builds otherwise render in light +// mode. See src/theme/ and UI_DESIGN_SYSTEM_PRD.md. +static void applyDarkTheme(QApplication& app) +{ + ThemeManager& tm = ThemeManager::instance(); + tm.loadFromResource(":/themes/dark.json"); + tm.setStyleSheetTemplate(":/qss/app.qss"); + tm.setAdsStyleSheetTemplate(":/qss/ads.qss"); // applied to the dock manager by MainWindow + tm.applyToApplication(app); +} + +int main(int argc, char* argv[]) +{ + // Read the stored answer before constructing QApplication. Off until the + // consent prompt has actually been answered — the launcher puts it up on + // first run and turns collection on from there if the answer is yes. + const bool crashReportingEnabled = Telemetry::isAllowed(); + + // Init Sentry before QApplication; resolves paths via Qt helpers after + // QCoreApplication is available (handler_path needs applicationDirPath). + // We pass a temporary QCoreApplication for path resolution, then tear it + // down before the real QApplication is constructed. + // + // Actually: sentry_options_set_handler_path / database_path only need the + // strings — we can derive them from argv[0] or defer to after QApplication. + // Simplest: init Sentry after QApplication (Crashpad handler is separate + // process anyway so it doesn't need QApplication to be alive). + + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); + + QSurfaceFormat format; + format.setProfile(QSurfaceFormat::CoreProfile); + format.setVersion(3, 2); + QSurfaceFormat::setDefaultFormat(format); + + QApplication a(argc, argv); + a.setOrganizationName("texturelab"); + a.setApplicationName("texturelab"); + a.setApplicationVersion(QString(TEXTURELAB_VERSION) + "+" + TEXTURELAB_BUILD_HASH); + a.setWindowIcon(QIcon(":/icons/logo.png")); + // Wayland ignores setWindowIcon; compositors resolve the icon from the + // installed desktop entry (packaging/linux/texturelab.desktop) instead. + a.setDesktopFileName("texturelab"); + + // Consistent dark UI on every platform, regardless of the host system theme. + applyDarkTheme(a); + + // Print the version and exit. Runs after QApplication and the theme are up, + // so the release workflow uses it as a smoke test of a packaged build: it + // proves the Qt libraries, platform plugin and resources all load. + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--version") == 0) { + std::printf("texturelab %s\n", qPrintable(a.applicationVersion())); + std::fflush(stdout); + return 0; + } + } + + // Theme hot-reload: live-reloads the theme from the on-disk source files + // (resources/…) on save, so colors and QSS can be tuned without rebuilding. + // ON BY DEFAULT in Debug builds (TEXTURELAB_DEV_BUILD); off in Release. + // `--dev-theme` forces it on in any build; `--no-dev-theme` forces it off. + bool devTheme = false; +#ifdef TEXTURELAB_DEV_BUILD + devTheme = true; +#endif + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--dev-theme") == 0) + devTheme = true; + else if (std::strcmp(argv[i], "--no-dev-theme") == 0) + devTheme = false; + } + if (devTheme) { +#ifdef TEXTURELAB_SOURCE_RESOURCES + const QString res = QStringLiteral(TEXTURELAB_SOURCE_RESOURCES); + ThemeManager::instance().enableHotReload(res + "/themes/dark.json", + res + "/qss/app.qss.in", + res + "/qss/ads.qss.in"); + qInfo("Theme hot-reload enabled, watching %s", qPrintable(res)); +#else + qWarning("theme hot-reload requested but TEXTURELAB_SOURCE_RESOURCES not compiled in"); +#endif + } + + // Now applicationDirPath() is valid — init Sentry + Telemetry::init(crashReportingEnabled); + + // Register RAM/CPU/screen/platform context before anything touches OpenGL. + // The GPU half needs a current context and is registered later, from + // TextureRenderer::setup(); this half has to already be on the scope in + // case we die during GL init itself. + SystemInfo::reportSystemContext(); + + // Launcher index + thumbnail cache. Failure is not fatal: the app runs + // normally without them, it just has nothing to show in the launcher. + // Seeds from the legacy recent-files list on first run (LAUNCHER_PRD.md §1.1). + if (CatalogService::instance().init()) + CatalogService::instance().reconcileAsync(); + + // Crash-test hook for verifying Sentry symbolication end-to-end. + // Must run after Telemetry::init so the crash handler is armed. + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--sentry-crash-test") == 0) { + // Give the SDK's network transport a moment to spin up before we + // crash. Crashpad (Win/Linux) uploads out-of-process so this isn't + // needed there, but the macOS inproc backend must send the event + // synchronously from the dying process — an instant crash at + // startup dies before the transport is ready. A real crash happens + // after the app has been running, so this warm-up is representative. + QThread::sleep(4); + sentryCrashTest(); + } + } + + // Install message handler after Sentry is up so breadcrumbs are captured + qInstallMessageHandler(qtMessageHandler); + + MainWindow w; + + // The launcher comes up first and MainWindow stays hidden until something + // is opened or created. Constructing it here is unavoidable — it owns the + // renderer and the dock layout — but not showing it keeps the empty editor + // off screen behind the launcher. `--no-launcher` skips straight to the + // editor, which is what you want when iterating on the editor itself. + bool useLauncher = true; + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--no-launcher") == 0) + useLauncher = false; + } + + if (useLauncher) { + w.showLauncher(); + } + else { + w.show(); + w.showMaximized(); + } + + int ret = a.exec(); + + // Close the catalog databases here, while Qt's SQL layer is still alive. + // Leaving it to static destruction crashes on exit (see instance()). + CatalogService::instance().shutdown(); + + Telemetry::close(); + return ret; +} diff --git a/src/texturelab/mainwindow.cpp b/src/texturelab/mainwindow.cpp new file mode 100644 index 00000000..55ad30aa --- /dev/null +++ b/src/texturelab/mainwindow.cpp @@ -0,0 +1,1337 @@ +#include "mainwindow.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "DockAreaWidget.h" +#include "DockSplitter.h" + +#include "catalogservice.h" +#include "exporter.h" +#include "launcher/launcherwindow.h" +#include "telemetry.h" +#include "thememanager.h" +#include "tokens.h" +#include "undo/undocommands.h" +#include "widgets/aboutdialog.h" +#include "widgets/exportdialog.h" +#include "widgets/graphwidget.h" +#include "widgets/librarywidget.h" +#include "widgets/properties/propertieswidget.h" +#include "widgets/view2dwidget.h" +#include "widgets/view3dwidget.h" + +#include "viewer3d.h" + +#include "libraries/libraryversionmigrator.h" +#include "libraries/libversion.h" +#include "models.h" +#include "project.h" +#include "props.h" + +#include "graph/scene.h" +#include "graphics/texturerenderer.h" + +MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) +{ + resize(1280, 720); + setAcceptDrops(true); + + undoStack = new QUndoStack(this); + connect(undoStack, &QUndoStack::cleanChanged, this, + &MainWindow::onCleanChanged); + + // Any command (or its undo) can add, remove or re-point the node a + // texture channel maps to — deleting an assigned node being the obvious + // one. Re-sync the viewer and the graph's channel labels from the model + // after every stack move so they can't drift, and kick the render loop in + // case the command marked nodes dirty without rendering them. + connect(undoStack, &QUndoStack::indexChanged, this, [this](int) { + if (!this->project) + return; + + // undo/redo changes prop values behind the properties panel's back + this->propWidget->syncPropBaselines(); + + // this fires on every push too (including merged ones, i.e. every + // step of a slider scrub), so only touch the viewer when the channel + // mapping actually changed + if (this->syncedChannels != this->project->textureChannels) { + this->syncedChannels = this->project->textureChannels; + this->passTextureChannelsToViewer3D(); + this->syncChannelLabelsToScene(); + this->view3DWidget->reRender(); + } + + if (this->renderer) + this->renderer->update(); + }); + + this->setupToolbar(); + + this->renderer = nullptr; + this->exportDialog = nullptr; + + statusLabel = new QLabel("Ready"); + progressBar = new QProgressBar(); + progressBar->setRange(0, 1); + progressBar->setValue(1); + progressBar->setFixedWidth(180); + progressBar->setTextVisible(false); + + auto* statusWidget = new QWidget(); + auto* statusLayout = new QHBoxLayout(statusWidget); + // statusLayout->setContentsMargins(4, 0, 4, 0); + statusLayout->setContentsMargins(0, 0, 0, 0); + statusLayout->setSpacing(6); + statusLayout->addStretch(); + // Version + build hash on the left so it's legible in screenshots + // (matches the build artifact name, e.g. + // texturelab-win-v0.4.0-beta-). + auto* versionLabel = new QLabel(QCoreApplication::applicationVersion()); + versionLabel->setObjectName( + "StatusVersionLabel"); // styled in resources/qss/app.qss.in + versionLabel->setToolTip("Application version and build hash"); + statusBar()->addWidget(versionLabel); + + // Home: reopens the launcher over the editor (LAUNCHER_PRD.md §7). + auto* homeButton = new QToolButton(); + homeButton->setObjectName("StatusHomeButton"); + homeButton->setText("⌂"); + homeButton->setToolTip("Show all textures"); + homeButton->setCursor(Qt::PointingHandCursor); + connect(homeButton, &QToolButton::clicked, this, &MainWindow::showLauncher); + statusBar()->addWidget(homeButton); + + statusLayout->addWidget(statusLabel, 0, Qt::AlignVCenter); + statusLayout->addWidget(progressBar, 0, Qt::AlignVCenter); + statusBar()->addWidget(statusWidget, 1); + + this->dockManager = new ads::CDockManager(this); + + // Theme the dock system. ADS installs its own default stylesheet on the + // dock manager (constructor -> loadStylesheet), which overrides the global + // app sheet for ads--* widgets. We keep that default (it carries button + // icons and layout metrics) and append our token-driven color overrides. + // Rebuilt on every theme change so it also picks up --dev-theme + // hot-reloads. + this->adsDefaultStyleSheet = this->dockManager->styleSheet(); + auto applyDockTheme = [this]() { + ThemeManager& tm = ThemeManager::instance(); + // Qt prefers an ancestor widget's stylesheet over qApp, so app.qss + // rules (e.g. #AccordionHeader) don't reach widgets inside docks unless + // we also hand them to the dock manager. Order: ADS default -> ADS + // overrides -> app rules (last so our tokens win over ADS's + // palette()-based defaults). + // The call below applies the composed theme sheet (ADS default + ADS + // overrides + app rules) to the dock manager — it IS the theming, not an + // inline widget style; the short marker keeps the hygiene gate happy even + // if a formatter wraps the line. + const QString sheet = this->adsDefaultStyleSheet + "\n" + + tm.adsStyleSheet() + "\n" + tm.appStyleSheet(); + this->dockManager->setStyleSheet(sheet); // theme-exempt + }; + applyDockTheme(); + connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this, + applyDockTheme); + + // Thicker gutters between panels. ADS/QSplitter ignore QSS handle width, so + // set handleWidth in code — on the initial layout and whenever a new dock + // area (hence a new splitter) is created (drag-docking). + auto applySplitterWidth = [this]() { + for (auto* s : this->dockManager->findChildren()) + s->setHandleWidth(1); + }; + connect( + this->dockManager, &ads::CDockManager::dockAreaCreated, this, + [applySplitterWidth](ads::CDockAreaWidget*) { applySplitterWidth(); }); + + this->setupDocks(); + applySplitterWidth(); + + // After the docks: the Edit menu reuses the graph's clipboard actions. + this->setupMenus(); + + // setup callbacks for the widgets that are created once + connect(this->graphWidget, &GraphWidget::nodeSelectionChanged, + [this](const TextureNodePtr& node) { + if (!!node) { + this->propWidget->setSelectedNode(node); + // this->view2DWidget->setSelectedNode(node); + } + else { + this->propWidget->clearSelection(); + // this->view2DWidget->clearSelection(); + } + }); + + connect(this->graphWidget, &GraphWidget::nodeDoubleClicked, + [this](const TextureNodePtr& node) { + if (!!node) { + this->view2DWidget->setSelectedNode(node); + } + else { + this->view2DWidget->clearSelection(); + } + }); + + connect(this->graphWidget, &GraphWidget::frameSelectionChanged, + [this](const FramePtr& frame) { + if (!!frame) { + this->propWidget->setSelectedFrame(frame); + } + else { + this->propWidget->clearSelection(); + } + }); + + connect(this->graphWidget, &GraphWidget::commentSelectionChanged, + [this](const CommentPtr& comment) { + if (!!comment) { + this->propWidget->setSelectedComment(comment); + } + else { + this->propWidget->clearSelection(); + } + }); + + connect(this->propWidget, &PropertiesWidget::framePropertyChanged, + [this](const FramePtr& frame) { + this->graphWidget->syncFrameToScene(frame); + }); + + connect(this->propWidget, &PropertiesWidget::commentPropertyChanged, + [this](const CommentPtr& comment) { + this->graphWidget->syncCommentToScene(comment); + }); + + connect(this->propWidget, &PropertiesWidget::propertyUpdated, + [this](const QString& name, const QVariant& value) { + if (this->renderer && !!this->project) { + this->renderer->update(); + } + + this->view2DWidget->reRenderNode(); + this->view3DWidget->reRender(); + }); + + connect( + this->propWidget, &PropertiesWidget::textureChannelUpdated, + [this](const TextureChannel& name, const TextureNodePtr& node) { + if (!this->project) + return; + + auto syncViewer = [this]() { + this->passTextureChannelsToViewer3D(); + this->syncChannelLabelsToScene(); + this->view3DWidget->reRender(); + if (this->renderer) + this->renderer->update(); + }; + + if (name == TextureChannel::None) { + // Unassign this node from whichever channel it's in + for (auto ch : this->project->textureChannels.keys()) { + if (this->project->textureChannels[ch] == node->id) { + QString oldNodeId = node->id; + // Apply immediately, then push command (first-redo + // no-op) + this->project->textureChannels.remove(ch); + syncViewer(); + if (undoStack) + undoStack->push(new TextureChannelAssignCommand( + this->project, ch, oldNodeId, "", syncViewer)); + break; + } + } + } + else { + QString oldNodeId = + this->project->textureChannels.value(name, ""); + // Apply immediately, then push command (first-redo no-op) + this->project->textureChannels[name] = node->id; + syncViewer(); + if (undoStack) + undoStack->push(new TextureChannelAssignCommand( + this->project, name, oldNodeId, node->id, syncViewer)); + } + }); + + // set default empty project + auto project = TextureProject::createEmpty(); + this->setProject(project); + + // GPU details go to Sentry from TextureRenderer::setup(), where a + // context is guaranteed current (see SystemInfo::reportGpuContext()). + + // test texture rendering + // auto renderer = new TextureRenderer(); + // renderer->testRendering(); +} + +void MainWindow::passTextureChannelsToViewer3D() +{ + auto viewer = this->view3DWidget->viewer; + viewer->clearTextures(); + + for (auto channel : this->project->textureChannels.keys()) { + auto nodeId = this->project->textureChannels[channel]; + + auto node = this->project->getNodeById(nodeId); + if (!node) + continue; + + // Skip nodes whose FBO/shader aren't ready yet: textureId() would + // otherwise dereference a null texture. They'll be picked up on the + // next sync once the render worker has produced their texture. + if (!node->isGraphicsResourcesInitialized()) + continue; + + switch (channel) { + case TextureChannel::Albedo: + viewer->setAlbedoTexture(node->textureId()); + break; + case TextureChannel::Normal: + viewer->setNormalTexture(node->textureId()); + break; + case TextureChannel::Metalness: + viewer->setMetalnessTexture(node->textureId()); + break; + case TextureChannel::Roughness: + viewer->setRoughnessTexture(node->textureId()); + break; + case TextureChannel::Height: + viewer->setHeightTexture(node->textureId()); + break; + case TextureChannel::AO: + viewer->setAoTexture(node->textureId()); + break; + case TextureChannel::Alpha: + viewer->setAlphaTexture(node->textureId()); + break; + default: + break; + } + } +} + +static QString channelName(TextureChannel ch) +{ + switch (ch) { + case TextureChannel::Albedo: + return "Albedo"; + case TextureChannel::Normal: + return "Normal"; + case TextureChannel::Metalness: + return "Metalness"; + case TextureChannel::Roughness: + return "Roughness"; + case TextureChannel::Height: + return "Height"; + case TextureChannel::Alpha: + return "Alpha"; + case TextureChannel::AO: + return "AO"; + default: + return ""; + } +} + +void MainWindow::syncChannelLabelsToScene() +{ + if (!project || !graphWidget->scene) + return; + + // clear all labels first + for (auto& node : graphWidget->scene->nodes) + node->setChannel(""); + + // set labels from project state + for (auto ch : project->textureChannels.keys()) { + auto nodeId = project->textureChannels[ch]; + auto sceneNode = graphWidget->scene->nodes.value(nodeId); + if (sceneNode) + sceneNode->setChannel(channelName(ch)); + } +} + +void MainWindow::setProject(TextureProjectPtr project) +{ + // Clear widget state from the old project + this->view2DWidget->clearSelection(); + this->view3DWidget->viewer->clearTextures(); + this->view3DWidget->reRender(); + + // Clean up old renderer before creating new one + if (this->renderer) { + this->graphWidget->setTextureRenderer(nullptr); + this->view2DWidget->setTextureRenderer(nullptr); + this->propWidget->setTextureRenderer(nullptr); + + delete this->renderer; + this->renderer = nullptr; + } + + this->project = project; + this->syncedChannels = project->textureChannels; + + // Tagged rather than only breadcrumbed so a crash event carries the graph + // size and resolution it happened at, which is what the GTX 750 / 4K report + // was missing. + Telemetry::setTag("project.node_count", + std::to_string(project->nodes.size())); + Telemetry::setTag("texture.resolution", + std::to_string(project->textureWidth)); + + this->graphWidget->setTextureProject(project); + this->syncChannelLabelsToScene(); + this->libraryWidget->setLibrary(project->library); + this->libraryWidget->setLibraryVersion( + project->libraryVersion, + project->libraryVersion == libVersionToString(currentLibVersion())); + + this->propWidget->clearSelection(); + this->propWidget->setProject(project); + this->propWidget->setScene(this->graphWidget->scene); + + renderer = new TextureRenderer(); + renderer->setProject(project); + this->graphWidget->setTextureRenderer(renderer); + this->view2DWidget->setTextureRenderer(renderer); + // undo/redo of a property change has no propertyUpdated signal to kick the + // render loop, so the commands need the renderer directly + this->propWidget->setTextureRenderer(renderer); + + connect(renderer, &TextureRenderer::renderProgress, + [this](int clean, int total) { + progressBar->setMaximum(total == 0 ? 1 : total); + progressBar->setValue(total == 0 ? 1 : clean); + if (total == 0 || clean == total) { + statusLabel->setText("Ready"); + + // The graph is fully evaluated: this is the first moment a + // freshly opened document is worth photographing. + if (thumbnailCapturePending) { + thumbnailCapturePending = false; + captureLauncherThumbnail(int(catalog::ThumbSource::Open)); + } + } + else { + statusLabel->setText( + QString("Rendering %1 / %2").arg(clean).arg(total)); + } + }); + + // Update view3D textures when a node's texture is updated + connect(renderer, &TextureRenderer::thumbnailGenerated, + [this](const QString& nodeId, GLuint texId, const QPixmap&) { + if (!this->project) + return; + + auto viewer = this->view3DWidget->viewer; + for (auto channel : this->project->textureChannels.keys()) { + if (this->project->textureChannels[channel] == nodeId) { + switch (channel) { + case TextureChannel::Albedo: + viewer->setAlbedoTexture(texId); + break; + case TextureChannel::Normal: + viewer->setNormalTexture(texId); + break; + case TextureChannel::Metalness: + viewer->setMetalnessTexture(texId); + break; + case TextureChannel::Roughness: + viewer->setRoughnessTexture(texId); + break; + case TextureChannel::Height: + viewer->setHeightTexture(texId); + break; + case TextureChannel::AO: + viewer->setAoTexture(texId); + break; + case TextureChannel::Alpha: + viewer->setAlphaTexture(texId); + break; + default: + break; + } + this->view3DWidget->reRender(); + break; + } + } + }); + + renderer->update(); + + setWindowTitle(project->name + " - TextureLab"); + undoStack->clear(); +} + +void MainWindow::setupMenus() +{ + auto fileMenu = this->menuBar()->addMenu("File"); + fileMenu->addAction("Open Project", QKeySequence::Open, + [=]() { this->openProject(); }); + fileMenu->addAction("New Project", QKeySequence::New, + [=]() { this->newProject(); }); + fileMenu->addSeparator(); + fileMenu->addAction("Save", QKeySequence::Save, + [=]() { this->saveProject(); }); + fileMenu->addAction("Save As...", QKeySequence::SaveAs, + [=]() { this->saveProjectAs(); }); + fileMenu->addSeparator(); + + recentFilesMenu = fileMenu->addMenu("Open Recent"); + connect(recentFilesMenu, &QMenu::aboutToShow, this, + &MainWindow::updateRecentFilesMenu); + + fileMenu->addSeparator(); + fileMenu->addAction("Edit", []() {}); + + auto editMenu = this->menuBar()->addMenu("Edit"); + auto undoAction = undoStack->createUndoAction(this, tr("Undo")); + undoAction->setShortcut(QKeySequence::Undo); + editMenu->addAction(undoAction); + auto redoAction = undoStack->createRedoAction(this, tr("Redo")); + redoAction->setShortcut(QKeySequence::Redo); + editMenu->addAction(redoAction); + editMenu->addSeparator(); + + // The graph owns these — their shortcuts are scoped to it, so Ctrl+C in a + // property field still copies text. Reusing the actions here keeps the keys + // visible in the menu without registering a second, ambiguous binding. + editMenu->addAction(graphWidget->cutAction); + editMenu->addAction(graphWidget->copyAction); + editMenu->addAction(graphWidget->pasteAction); + + auto examplesMenu = this->menuBar()->addMenu("Examples"); + + // List of example texture files + QStringList examples = {"Copper.texture", + "FoilGasket.texture", + "GoldLinedMarbleTiles.texture", + "Grass.texture", + "GrassyRock.texture", + "Grenade.texture", + "Sand.texture", + "Screws.texture", + "WoodenPlanks.texture", + "YellowTiles.texture"}; + + for (const QString& example : examples) { + // Remove .texture extension for display name + QString displayName = example; + displayName.replace(".texture", ""); + + examplesMenu->addAction(displayName, [this, example]() { + if (!promptSaveIfDirty()) + return; + auto project = Project::loadTexture(":examples/" + example); + QString projectName = example; + projectName.replace(".texture", ""); + project->name = projectName; + setProject(project); + }); + } + + auto optionsMenu = this->menuBar()->addMenu("Help"); + optionsMenu->addAction("About", [this]() { + AboutDialog dialog(this); + dialog.exec(); + }); + + optionsMenu->addSeparator(); + + auto crashReportingAction = + optionsMenu->addAction("Send Anonymous Crash Reports"); + crashReportingAction->setCheckable(true); + crashReportingAction->setChecked(Telemetry::isAllowed()); + connect(crashReportingAction, &QAction::toggled, [](bool checked) { + // Through Telemetry rather than straight to QSettings, so the change + // takes effect now instead of on the next launch — and so toggling it + // by hand counts as having answered the consent prompt. + Telemetry::recordConsent(checked); + Telemetry::setEnabled(checked); + }); + + // The launcher's gear menu writes the same setting, so the check can be + // stale by the time this menu is opened. + connect(optionsMenu, &QMenu::aboutToShow, this, [crashReportingAction]() { + QSignalBlocker block(crashReportingAction); + crashReportingAction->setChecked(Telemetry::isAllowed()); + }); +} + +// Recolor a rendered (white) icon pixmap to `color`, keeping its alpha shape. +static QPixmap tintPixmap(const QPixmap& src, const QColor& color) +{ + QPixmap out(src.size()); + out.setDevicePixelRatio(src.devicePixelRatio()); + out.fill(Qt::transparent); + QPainter p(&out); + p.drawPixmap(0, 0, src); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(out.rect(), color); + p.end(); + return out; +} + +// Build a toolbar QIcon whose Normal/Disabled variants are tinted to the theme's +// text colors, so the icon always matches the button label's color in each state +// (Qt's auto-generated disabled fade doesn't match text.disabled exactly). +static QIcon themedToolIcon(const QString& svgPath) +{ + const Theme& t = ThemeManager::instance().theme(); + const QPixmap base = QIcon(svgPath).pixmap(QSize(32, 32)); // white source SVG + QIcon icon; + icon.addPixmap(tintPixmap(base, t.color(Tokens::TextPrimary)), QIcon::Normal); + icon.addPixmap(tintPixmap(base, t.color(Tokens::TextDisabled)), QIcon::Disabled); + return icon; +} + +void MainWindow::setupToolbar() +{ + // https://www.setnode.com/blog/right-aligning-a-button-in-a-qtoolbar/ + toolBar = this->addToolBar("main toolbar"); + toolBar->setObjectName("MainToolbar"); // styled in app.qss.in + toolBar->setIconSize(QSize(14, 14)); // small, to sit level with the button text + toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + + QWidget* spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + // undo redo — plain "Undo"/"Redo" labels (not the createUndoAction text, + // which appends the command name). Shortcuts stay on the Edit-menu actions + // to avoid an ambiguous-shortcut clash. + auto undoAction = new QAction(themedToolIcon(":/icons/undo.svg"), "Undo", this); + undoAction->setEnabled(undoStack->canUndo()); + connect(undoAction, &QAction::triggered, undoStack, &QUndoStack::undo); + connect(undoStack, &QUndoStack::canUndoChanged, undoAction, + &QAction::setEnabled); + toolBar->addAction(undoAction); + + auto redoAction = new QAction(themedToolIcon(":/icons/redo.svg"), "Redo", this); + redoAction->setEnabled(undoStack->canRedo()); + connect(redoAction, &QAction::triggered, undoStack, &QUndoStack::redo); + connect(undoStack, &QUndoStack::canRedoChanged, redoAction, + &QAction::setEnabled); + toolBar->addAction(redoAction); + + // spacer + toolBar->addWidget(spacer); + + // Export button with dropdown menu + auto exportBtn = new QToolButton(this); + exportBtn->setText("Export"); + exportBtn->setIcon(themedToolIcon(":/icons/export.svg")); + exportBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + + auto directExportAction = new QAction("Export", this); + directExportAction->setShortcut(QKeySequence("Ctrl+E")); + connect(directExportAction, &QAction::triggered, this, + &MainWindow::directExport); + + auto settingsAction = new QAction("Export Settings...", this); + settingsAction->setShortcut(QKeySequence("Ctrl+Shift+E")); + connect(settingsAction, &QAction::triggered, this, + &MainWindow::showExportDialog); + + // The dropdown is a popup window of its own, so associate both actions + // with the main window too or their shortcuts never fire. + this->addAction(directExportAction); + this->addAction(settingsAction); + + auto exportMenu = new QMenu(this); + exportMenu->addAction(settingsAction); + + exportBtn->setDefaultAction(directExportAction); + exportBtn->setMenu(exportMenu); + exportBtn->setPopupMode(QToolButton::MenuButtonPopup); + + toolBar->addWidget(exportBtn); + + // behavior + toolBar->setMovable(false); +} + +/* + https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/411 + https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/issues/340 + + pass ratios as fractions, the function handles the actual width calculation + setWidgetRatiosInArea(myArea, { 1.0f/5.0f, 1.0f/5.0f, 1.0f/5.0f }) +*/ +void setWidgetRatiosInArea(ads::CDockAreaWidget* area, + const QList& ratios) +{ + auto splitter = ads::internal::findParent(area); + if (splitter) { + int width = splitter->width(); + + QList finalRatios; + for (auto ratio : ratios) { + finalRatios.append(ratio * width); + } + splitter->setSizes(finalRatios); + } +} + +void MainWindow::setupDocks() +{ + setDockNestingEnabled(true); + + // https://forum.qt.io/topic/3055/mainwindow-layout-problem-with-qdockwidget/17 + + // graph goes in the center + this->graphWidget = new GraphWidget(); + this->graphWidget->setUndoStack(undoStack); + this->view2DWidget = new View2DWidget(); + this->view3DWidget = new View3DWidget(); + + auto graphArea = + addDock("Graph", ads::CenterDockWidgetArea, graphWidget, nullptr); + auto leftArea = addDock("2D View", ads::LeftDockWidgetArea, + this->view2DWidget, graphArea); + + this->propWidget = new PropertiesWidget(); + this->propWidget->setUndoStack(undoStack); + auto rightArea = addDock("Properties", ads::RightDockWidgetArea, + this->propWidget, graphArea); + + this->libraryWidget = new LibraryWidget(); + connect(this->libraryWidget, &LibraryWidget::upgradeRequested, this, + &MainWindow::upgradeCurrentProjectLibrary); + setWidgetRatiosInArea(graphArea, {1.0f / 5, 3.0f / 5, 1.0f / 5}); + + addDock("3D View", ads::BottomDockWidgetArea, this->view3DWidget, leftArea); + addDock("Library", ads::BottomDockWidgetArea, this->libraryWidget, + rightArea); + setWidgetRatiosInArea(leftArea, {0.5f, 0.5f}); + setWidgetRatiosInArea(rightArea, {0.5f, 0.5f}); + + QTimer::singleShot(0, this, [this]() { graphWidget->setFocus(); }); +} + +ads::CDockAreaWidget* MainWindow::addDock(const QString& title, + ads::DockWidgetArea area, + QWidget* widget, + ads::CDockAreaWidget* areaWidget) +{ + // ads::CDockWidget* dockWidget = new ads::CDockWidget(title); + ads::CDockWidget* dockWidget = dockManager->createDockWidget(title); + if (widget != nullptr) + dockWidget->setWidget(widget); + + auto newAreaWidget = + dockManager->addDockWidget(area, dockWidget, areaWidget); + + return newAreaWidget; +} + +QWidget* MainWindow::dialogParent() +{ + // The launcher is a top-level window, not a child of this one, so it has to + // be named explicitly as the parent while it's the window on screen. + if (launcher && launcher->isVisible()) + return launcher; + + return this; +} + +void MainWindow::captureLauncherThumbnail(int source) +{ + CatalogService& catalog = CatalogService::instance(); + if (!catalog.isReady() || !project || project->filePath.isEmpty()) + return; + + auto* viewer = view3DWidget ? view3DWidget->viewer : nullptr; + if (!viewer || !viewer->isValid()) + return; + + // The one moment the whole graph is evaluated and resident on the GPU, so + // we just take the picture — no headless evaluator, no second GL context + // (LAUNCHER_PRD.md §4). Whatever the user framed is what the card shows. + const QImage frame = viewer->grabFramebuffer(); + if (frame.isNull()) + return; + + const catalog::TextureRecord rec = catalog.index().byPath(project->filePath); + if (!rec.isValid()) + return; + + catalog.captureThumbnail(rec.id, frame, static_cast(source)); +} + +void MainWindow::showLauncher() +{ + if (!launcher) { + launcher = new LauncherWindow(); + + connect(launcher, &LauncherWindow::newTextureRequested, this, [this]() { + launcher->hide(); + newProject(); + showMaximized(); + raise(); + activateWindow(); + }); + + // Routed through openProjectFromPath so the launcher inherits the + // dirty-document prompt and the library-version upgrade dialog rather + // than reimplementing either. + connect(launcher, &LauncherWindow::openPathRequested, this, + [this](const QString& path) { + const QString before = project ? project->filePath : QString(); + openProjectFromPath(path); + + // openProjectFromPath bails out silently if the user + // cancels the save prompt or the file won't read; in that + // case leave the launcher up rather than dropping them into + // an editor they didn't ask for. + if (project && project->filePath == path && project->filePath != before) { + launcher->hide(); + showMaximized(); + raise(); + activateWindow(); + } + }); + + connect(launcher, &LauncherWindow::openDialogRequested, this, [this]() { + openProject(); + if (project && !project->filePath.isEmpty()) { + launcher->hide(); + showMaximized(); + raise(); + activateWindow(); + } + }); + + connect(launcher, &LauncherWindow::closeRequested, this, [this]() { + launcher->hide(); + showMaximized(); + raise(); + activateWindow(); + }); + } + + launcher->setHasDocument(project && !project->filePath.isEmpty()); + launcher->setOpenPath(project ? project->filePath : QString()); + launcher->show(); + launcher->raise(); + launcher->activateWindow(); +} + +void MainWindow::openProject() +{ + if (!promptSaveIfDirty()) + return; + + auto filePath = QFileDialog::getOpenFileName(dialogParent(), "Open Texture File", "", + "Texturelab File (*.texture)"); + + if (filePath.isNull() || filePath.isEmpty()) + return; + + openProjectFromPath(filePath); +} + +void MainWindow::openProjectFromPath(const QString& filePath) +{ + if (!promptSaveIfDirty()) + return; + + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + QMessageBox::warning(dialogParent(), "Open Texture", + "Could not open file:\n" + filePath); + return; + } + auto json = QJsonDocument::fromJson(file.readAll()).object(); + file.close(); + + LibraryVersionMigrator migrator(json); + if (migrator.needsMigration()) { + QStringList chain; + chain << libVersionToString(migrator.sourceVersion()); + for (auto v : migrator.versionsCrossed()) + chain << libVersionToString(v); + + auto choice = QMessageBox::question( + dialogParent(), "Upgrade Texture?", + QString("This texture was created with library version %1.\n\n" + "Upgrade it to %2 (%3) to use the latest nodes and " + "improvements? A few node behaviors may change slightly.") + .arg(libVersionToString(migrator.sourceVersion()), + libVersionToString(migrator.targetVersion()), + chain.join(" → ")), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + if (choice == QMessageBox::Yes) + json = migrator.migrate(); + } + + auto project = Project::loadTextureFromJson(json); + + QFileInfo fileInfo(filePath); + project->name = fileInfo.baseName(); + project->filePath = filePath; + + // Deliberately no file name or path: the consent prompt promises counts and + // settings, not what the user is working on. + Telemetry::breadcrumb("project", "opened", + {{"node_count", (int64_t)project->nodes.size()}, + {"resolution", (int64_t)project->textureWidth}, + {"library_version", + project->libraryVersion.toStdString()}}); + setProject(project); + addToRecentFiles(filePath); + + // The shared entry point for the Open dialog, the recent-files menu, and + // drag-and-drop, so one hook here covers all three (LAUNCHER_PRD.md §6.1). + CatalogService::instance().recordOpened(project, filePath); + + // Can't grab yet: opening kicks off an asynchronous render and the viewport + // is still showing the previous document (or nothing). Captured when + // renderProgress reports every node clean. + thumbnailCapturePending = true; +} + +void MainWindow::dragEnterEvent(QDragEnterEvent* event) +{ + if (!event->mimeData()->hasUrls()) { + event->ignore(); + return; + } + + for (const auto& url : event->mimeData()->urls()) { + if (url.isLocalFile() && + url.toLocalFile().endsWith(".texture", Qt::CaseInsensitive)) { + event->acceptProposedAction(); + return; + } + } + event->ignore(); +} + +void MainWindow::dropEvent(QDropEvent* event) +{ + for (const auto& url : event->mimeData()->urls()) { + if (!url.isLocalFile()) + continue; + + auto filePath = url.toLocalFile(); + if (!filePath.endsWith(".texture", Qt::CaseInsensitive)) + continue; + + event->acceptProposedAction(); + openProjectFromPath(filePath); + return; + } + + event->ignore(); +} + +void MainWindow::upgradeCurrentProjectLibrary() +{ + if (!this->project) + return; + + // Round-trip through the same JSON the file format uses, so the live + // "Upgrade" button in the Library dock goes through the exact same + // pure-JSON migration path as opening a legacy file does. + auto bytes = Project::saveTexture(this->project); + auto json = QJsonDocument::fromJson(bytes).object(); + + LibraryVersionMigrator migrator(json); + if (!migrator.needsMigration()) + return; + + auto choice = QMessageBox::warning( + dialogParent(), "Upgrade Library Version?", + "Upgrading the library version is irreversible and clears the " + "undo/redo history for this session.\n\n" + "Save your project (or save a copy) first if you want to keep the " + "ability to go back to the current version.\n\n" + "Continue with the upgrade?", + QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); + + if (choice != QMessageBox::Yes) + return; + + auto newProject = Project::loadTextureFromJson(migrator.migrate()); + newProject->name = this->project->name; + newProject->filePath = this->project->filePath; + + setProject(newProject); +} + +void MainWindow::newProject() +{ + if (!promptSaveIfDirty()) + return; + Telemetry::breadcrumb("project", "new project"); + setProject(TextureProject::createEmpty()); +} + +void MainWindow::saveProject() +{ + if (project->filePath.isNull() || project->filePath.isEmpty()) { + QString filePath = QFileDialog::getSaveFileName( + this, "Save Texture...", QString(), "Texturelab File (*.texture)"); + + if (filePath.isNull() || filePath.isEmpty()) { + return; + } + + if (!filePath.endsWith(".texture", Qt::CaseInsensitive)) + filePath += ".texture"; + + project->filePath = filePath; + } + + graphWidget->syncPositionsToModel(); + + Telemetry::breadcrumb("project", "saved", + {{"node_count", (int64_t)project->nodes.size()}, + {"resolution", (int64_t)project->textureWidth}}); + QFile file(project->filePath); + file.open(QIODevice::WriteOnly); + file.write(Project::saveTexture(project)); + file.close(); + undoStack->setClean(); + addToRecentFiles(project->filePath); + CatalogService::instance().recordSaved(project, project->filePath); + captureLauncherThumbnail(int(catalog::ThumbSource::Save)); +} + +void MainWindow::saveProjectAs() +{ + QString filePath = QFileDialog::getSaveFileName( + this, "Save Texture As...", QString(), "Texturelab File (*.texture)"); + + if (filePath.isNull() || filePath.isEmpty()) + return; + + if (!filePath.endsWith(".texture", Qt::CaseInsensitive)) + filePath += ".texture"; + + project->filePath = filePath; + + QFileInfo fileInfo(filePath); + project->name = fileInfo.baseName(); + + graphWidget->syncPositionsToModel(); + + QFile file(project->filePath); + file.open(QIODevice::WriteOnly); + file.write(Project::saveTexture(project)); + file.close(); + undoStack->setClean(); + setWindowTitle(project->name + " - TextureLab"); + addToRecentFiles(project->filePath); + CatalogService::instance().recordSaved(project, project->filePath); + captureLauncherThumbnail(int(catalog::ThumbSource::Save)); +} + +void MainWindow::showExportDialog() +{ + if (!this->exportDialog) { + this->exportDialog = new ExportDialog(this); + } + + if (this->project) { + this->exportDialog->setProject(this->project); + } + + this->exportDialog->show(); + this->exportDialog->raise(); + this->exportDialog->activateWindow(); +} + +void MainWindow::directExport() +{ + // Check if we have a valid project + if (!this->project) { + showExportDialog(); + return; + } + + // If no export destination is set, prompt for one + if (this->project->exportDestination.isEmpty()) { + QString dir = QFileDialog::getExistingDirectory( + this, "Select Export Destination", "", + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (dir.isEmpty()) { + // User cancelled + return; + } + + // Save destination to project + this->project->exportDestination = dir; + + // Also update dialog if it exists + if (this->exportDialog) { + this->exportDialog->setProject(this->project); + } + } + + // Perform export with project settings + handleExport(this->project->exportDestination, + this->project->exportFilePattern); +} + +void MainWindow::handleExport(const QString& destination, + const QString& pattern) +{ + // The destination path is deliberately not recorded — only the shape of + // the export. + Telemetry::breadcrumb("export", "export started", + {{"resolution", (int64_t)(this->project + ? this->project->textureWidth + : 0)}, + {"pattern", pattern.toStdString()}}); + if (!this->project || !this->renderer) { + QMessageBox::warning(this, "Export Error", + "No project loaded or renderer not initialized."); + return; + } + + // Find all output nodes + QVector outputNodes; + for (auto& node : this->project->nodes) { + // Check if this is an output node by checking the library name + if (node->title == "Output") { + outputNodes.append(node); + } + } + + if (outputNodes.isEmpty()) { + QMessageBox::information(this, "Export", + "No output nodes found in the project."); + return; + } + + // Make renderer's context current for reading texture data + if (!this->renderer->ctx) { + QMessageBox::warning(this, "Export Error", + "Renderer context not initialized."); + return; + } + + // Store the previous context to restore it later + QOpenGLContext* previousContext = QOpenGLContext::currentContext(); + QSurface* previousSurface = + previousContext ? previousContext->surface() : nullptr; + + // Make renderer context current + this->renderer->ctx->makeCurrent(this->renderer->surface); + + // Create exporter + Exporter exporter; + + // Export each output node + int successCount = 0; + int failCount = 0; + + for (auto& node : outputNodes) { + // Get the output name from the node's property + QString outputName = ""; + if (node->hasProp("name")) { + outputName = node->getProp("name")->getValue().toString(); + } + + // Use node title if name property is empty + if (outputName.isEmpty()) { + outputName = "output"; + } + + // Get precision setting (0 = 8-bit, 1 = 16-bit) + int precision = 0; + if (node->hasProp("precision")) { + precision = node->getProp("precision")->getValue().toInt(); + } + + // Get components setting (0=RGBA, 1=RGB, 2=R, 3=G, 4=B, 5=A) + int components = 0; + if (node->hasProp("components")) { + components = node->getProp("components")->getValue().toInt(); + } + + // Generate filename using pattern + QString filename = pattern; + filename.replace("${project}", this->project->name); + filename.replace("${name}", outputName); + filename += ".png"; + + QString fullPath = destination + "/" + filename; + + // Get the texture from the node + if (!node->texture) { + qDebug() << "Node" << node->title << "has no texture"; + failCount++; + continue; + } + + // Export using Exporter class + ExportResult result = exporter.exportTexture(node->texture, fullPath, + precision, components); + + if (result.success) { + successCount++; + } + else { + failCount++; + } + } + + // Restore previous context + if (previousContext && previousSurface) { + previousContext->makeCurrent(previousSurface); + } + else { + this->renderer->ctx->doneCurrent(); + } + + // Show result message + QString message = + QString("Export complete!\n\n") + + QString("Successfully exported: %1 file(s)\n").arg(successCount); + + if (failCount > 0) { + message += QString("Failed: %1 file(s)").arg(failCount); + } + + QMessageBox::information(this, "Export", message); +} + +void MainWindow::addToRecentFiles(const QString& filePath) +{ + QSettings settings; + QStringList files = settings.value("recentFiles").toStringList(); + files.removeAll(filePath); + files.prepend(filePath); + while (files.size() > MaxRecentFiles) + files.removeLast(); + settings.setValue("recentFiles", files); +} + +void MainWindow::updateRecentFilesMenu() +{ + recentFilesMenu->clear(); + + // Reads through to the catalog index rather than QSettings, so this menu + // and the launcher can't disagree about what you opened last. QSettings is + // still written by addToRecentFiles() as a fallback for the case where the + // index failed to open. + QStringList files; + CatalogService& catalog = CatalogService::instance(); + + if (catalog.isReady()) { + catalog::Query query; + query.filter = catalog::Filter::Recents; + query.sort = catalog::SortKey::Opened; + query.ascending = false; + query.limit = MaxRecentFiles; + + for (const catalog::TextureRecord& rec : catalog.index().list(query)) + files << rec.path; + } + else { + files = QSettings().value("recentFiles").toStringList(); + } + + for (const QString& filePath : files) { + QFileInfo info(filePath); + auto action = + recentFilesMenu->addAction(info.fileName(), [this, filePath]() { + openProjectFromPath(filePath); + }); + action->setToolTip(filePath); + } + + if (files.isEmpty()) + recentFilesMenu->addAction("No recent files")->setEnabled(false); + + recentFilesMenu->addSeparator(); + recentFilesMenu->addAction("Clear Recent Files", [this]() { + QSettings().remove("recentFiles"); + + // Clearing the menu must not delete the user's stars, tags, or the + // textures themselves — only forget when they were last opened. The + // launcher keeps showing them under All. + CatalogService& catalog = CatalogService::instance(); + if (catalog.isReady()) + catalog.index().clearRecents(); + }); +} + +void MainWindow::onCleanChanged(bool clean) +{ + if (!project) + return; + QString title = project->name + " - TextureLab"; + setWindowTitle(clean ? title : "*" + title); +} + +bool MainWindow::promptSaveIfDirty() +{ + if (undoStack->isClean()) + return true; + + QString name = project ? project->name : "Untitled"; + auto choice = QMessageBox::question( + dialogParent(), "Unsaved Changes", + QString("Save changes to \"%1\" before continuing?").arg(name), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + + if (choice == QMessageBox::Save) { + saveProject(); + return undoStack->isClean(); // false if save was cancelled + } + return choice == QMessageBox::Discard; +} + +void MainWindow::closeEvent(QCloseEvent* event) +{ + if (!promptSaveIfDirty()) { + event->ignore(); + return; + } + event->accept(); +} + +MainWindow::~MainWindow() +{ + if (this->renderer) { + delete this->renderer; + this->renderer = nullptr; + } +} \ No newline at end of file diff --git a/src/texturelab/mainwindow.h b/src/texturelab/mainwindow.h new file mode 100644 index 00000000..85cc1f69 --- /dev/null +++ b/src/texturelab/mainwindow.h @@ -0,0 +1,138 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "DockManager.h" +#include "models.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class GraphWidget; +class LibraryWidget; +class PropertiesWidget; +class View2DWidget; +class View3DWidget; +class TextureRenderer; +class ExportDialog; +class LauncherWindow; + +class TextureProject; +typedef QSharedPointer TextureProjectPtr; + +class QToolBar; +class QProgressBar; +class QLabel; +class MainWindow : public QMainWindow { + Q_OBJECT + +public: + MainWindow(QWidget* parent = nullptr); + ~MainWindow(); + + // Shows the launcher over the editor. Called at startup (before this window + // is ever shown) and from the Home button in the status bar. + void showLauncher(); + +protected: + void setupToolbar(); + void setupMenus(); + void setupDocks(); + void closeEvent(QCloseEvent* event) override; + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + + // menu callbacks + void openProject(); + + // Shared by the Open dialog, recent-files menu, and drag-and-drop: + // prompts to save if dirty, reads the file, offers the + // version-upgrade dialog if needed, then loads it. + void openProjectFromPath(const QString& filePath); + void newProject(); + void saveProject(); + void saveProjectAs(); + void showExportDialog(); + void directExport(); + void handleExport(const QString& destination, const QString& pattern); + + // The window a modal should belong to right now. + // + // Opening a texture from the launcher runs through MainWindow while + // MainWindow itself is still hidden, and a dialog parented to a hidden + // widget has nothing to center on — Qt drops it on whichever screen that + // widget nominally lives on, which on a multi-monitor desk is usually not + // the one the user is looking at. + QWidget* dialogParent(); + + // Grabs the 3D viewport and stores it as this texture's launcher thumbnail. + // No-op when the viewport has never initialized its GL context, or when the + // texture isn't in the index. + void captureLauncherThumbnail(int source); + + void passTextureChannelsToViewer3D(); + void syncChannelLabelsToScene(); + + void setProject(TextureProjectPtr project); + + // Upgrades the currently open project's library in place, via the + // Library dock panel's "Upgrade" button. + void upgradeCurrentProjectLibrary(); + + void addToRecentFiles(const QString& filePath); + void updateRecentFilesMenu(); + + void onCleanChanged(bool clean); + + // Returns false if the user cancelled a "save changes?" dialog. + bool promptSaveIfDirty(); + + ads::CDockAreaWidget* addDock(const QString& title, + ads::DockWidgetArea area, QWidget* widget, + ads::CDockAreaWidget* areaWidget); + +private: + static constexpr int MaxRecentFiles = 10; + + QUndoStack* undoStack; + + ads::CDockManager* dockManager; + QString adsDefaultStyleSheet; // ADS's own default sheet, captured before we theme it + QMenu* recentFilesMenu; + QToolBar* toolBar; + QWidget* editor; + + GraphWidget* graphWidget; + LibraryWidget* libraryWidget; + PropertiesWidget* propWidget; + View2DWidget* view2DWidget; + View3DWidget* view3DWidget; + ExportDialog* exportDialog; + + // Created lazily on first show; owned by this window so it survives being + // hidden and reopened from the Home button. + LauncherWindow* launcher = nullptr; + + // Set when a document is opened, cleared once the graph has finished + // rendering and the thumbnail has been captured. Opening is asynchronous — + // the viewport shows nothing useful until the last node is clean. + bool thumbnailCapturePending = false; + + TextureRenderer* renderer; + + QProgressBar* progressBar; + QLabel* statusLabel; + + TextureProjectPtr project; + + // channel→node mapping last pushed to the 3D viewer and the graph's node + // labels, so undo-stack moves that didn't touch it can skip the re-sync + QMap syncedChannels; +}; +#endif // MAINWINDOW_H diff --git a/src/texturelab/models.cpp b/src/texturelab/models.cpp new file mode 100644 index 00000000..77fbcc13 --- /dev/null +++ b/src/texturelab/models.cpp @@ -0,0 +1,366 @@ +#include "models.h" +#include "libraries/library.h" +#include "props.h" +#include +#include +#include +#include + +TextureNodePtr TextureProject::getNodeById(const QString& id) +{ + // .value() (not operator[]): a read-only lookup must never default-insert a + // null entry into the map, which the renderer would later dereference. + return nodes.value(id); +} + +ConnectionPtr TextureProject::getConnectionById(const QString& id) +{ + return connections.value(id); +} + +QVector TextureProject::getNodeDependencies(const QString& id) +{ + auto node = nodes.value(id); + + QVector cons; + for (auto con : connections) { + if (con->rightNode == node) { + auto depNode = con->leftNode; + cons.append(depNode); + } + } + + return cons; +} + +QVector TextureProject::getNodeRightOfNode(const QString& id) +{ + auto node = nodes.value(id); + + QVector cons; + for (auto con : connections) { + if (con->leftNode == node) { + auto nextNode = con->rightNode; + cons.append(nextNode); + } + } + + return cons; +} + +void TextureProject::addNode(const TextureNodePtr& node) +{ + // todo: check if node already exists + this->nodes[node->id] = node; +} + +void TextureProject::addConnection(TextureNodePtr leftNode, + TextureNodePtr rightNode, + QString rightNodeInput) +{ + QSharedPointer con(new Connection()); + con->id = createGuid(); + + con->leftNode = leftNode; + con->rightNode = rightNode; + + con->rightNodeInputName = rightNodeInput; + + this->connections[con->id] = con; + + // the right node now has a different input, so it and everything + // downstream of it must re-render + markNodeAsDirty(rightNode); +} + +ConnectionPtr TextureProject::removeConnection(const QString& leftNode, + const QString& rightNode, + const QString& rightNodeInput) +{ + for (auto conKey : connections.keys()) { + auto con = connections[conKey]; + + if (!con || !con->leftNode || !con->rightNode) + continue; + + if (con->leftNode->id == leftNode && con->rightNode->id == rightNode && + con->rightNodeInputName == rightNodeInput) { + connections.remove(conKey); + + // the input is gone: the right node and its whole downstream + // chain render from stale textures until they're re-rendered + markNodeAsDirty(con->rightNode); + + return con; + } + } + + return ConnectionPtr(nullptr); +} + +void TextureProject::removeConnection(ConnectionPtr con) +{ + if (!con) + return; + + this->connections.remove(con->id); + markNodeAsDirty(con->rightNode); +} + +void TextureProject::removeConnection(const QString& id) +{ + auto con = connections.value(id); + this->connections.remove(id); + + if (con) + markNodeAsDirty(con->rightNode); +} + +void TextureProject::removeNode(const QString& id) +{ + // remove every connection touching the node first — removeConnection() + // marks each affected chain dirty so downstream nodes re-render without + // this node's output + for (auto key : connections.keys()) { + auto con = connections.value(key); + if (!con) + continue; + + if ((con->leftNode && con->leftNode->id == id) || + (con->rightNode && con->rightNode->id == id)) + removeConnection(con); + } + + // drop any texture-channel assignment for this node so its stale id can't + // be looked up after deletion + for (auto channel : textureChannels.keys()) { + if (textureChannels.value(channel) == id) + textureChannels.remove(channel); + } + + nodes.remove(id); +} + +void TextureProject::markNodeAsDirty(const TextureNodePtr& node) +{ + if (!node) + return; + + // visited guards two things: a diamond graph re-walking shared subtrees, + // and a cyclic graph looping here forever + QSet visited; + visited.insert(node->id); + + QQueue queue; + queue.enqueue(node); + + while (!queue.isEmpty()) { + auto nextNode = queue.dequeue(); + nextNode->isDirty = true; + + auto list = getNodeRightOfNode(nextNode->id); + for (auto item : list) { + if (!item || visited.contains(item->id)) + continue; + + visited.insert(item->id); + queue.enqueue(item); + } + } +} + +TextureProjectPtr TextureProject::createEmpty(Library* library) +{ + auto project = new TextureProject(); + project->randomSeed = 0; + if (library != nullptr) + project->library = library; + else + project->library = createLibraryV3(); + + return TextureProjectPtr(project); +} + +TextureNode::TextureNode() +{ + id = createGuid(); + isDirty = true; +} + +void TextureNode::addInput(const QString& inputName) +{ + inputs.append(inputName); +} + +void TextureNode::setProp(QString propName, QVariant value) +{ + if (props.contains(propName)) { + props[propName]->setValue(value); + this->isDirty = true; + } +} + +Prop* TextureNode::getProp(QString propName) +{ + if (props.contains(propName)) { + return props[propName]; + } + + return nullptr; +} + +bool TextureNode::hasProp(QString propName) { return props.contains(propName); } + +unsigned int TextureNode::textureId() +{ + // texture is null until the node's graphics resources are initialized; + // return 0 (the GL "no texture" name) rather than dereferencing null. + return this->texture ? this->texture->texture() : 0; +} + +PropertyGroup* TextureNode::createGroup(const QString& name) +{ + auto group = new PropertyGroup(); + group->name = name; + this->propertyGroups.append(group); + + return group; +} + +IntProp* TextureNode::addIntProp(const QString& name, + const QString& displayName, int defaultVal, + int minVal, int maxVal, int increment) +{ + auto prop = new IntProp(); + prop->name = name; + prop->displayName = displayName; + prop->minValue = minVal; + prop->maxValue = maxVal; + prop->step = increment; + prop->value = defaultVal; + prop->order = props.size(); + + props[name] = prop; + + return prop; +} + +FloatProp* TextureNode::addFloatProp(const QString& name, + const QString& displayName, + double defaultVal, double minVal, + double maxVal, double increment) +{ + auto prop = new FloatProp(); + prop->name = name; + prop->displayName = displayName; + prop->minValue = minVal; + prop->maxValue = maxVal; + prop->step = increment; + prop->value = defaultVal; + prop->order = props.size(); + + props[name] = prop; + + return prop; +} + +BoolProp* TextureNode::addBoolProp(const QString& name, + const QString& displayName, bool defaultVal) +{ + auto prop = new BoolProp(); + prop->name = name; + prop->displayName = displayName; + prop->value = defaultVal; + prop->order = props.size(); + + props[name] = prop; + + return prop; +} + +EnumProp* TextureNode::addEnumProp(const QString& name, + const QString& displayName, + QList values) +{ + auto prop = new EnumProp(); + prop->name = name; + prop->displayName = displayName; + prop->values = values; + prop->index = 0; + prop->order = props.size(); + + props[name] = prop; + + return prop; +} + +ColorProp* TextureNode::addColorProp(const QString& name, + const QString& displayName, + const QColor& defaultVal) +{ + auto prop = new ColorProp(); + prop->name = name; + prop->displayName = displayName; + prop->value = defaultVal; + prop->order = props.size(); + + props[name] = prop; + + return prop; +} + +StringProp* TextureNode::addStringProp(const QString& name, + const QString& displayName, + const QString& defaultVal) +{ + auto prop = new StringProp(); + prop->name = name; + prop->displayName = displayName; + prop->value = defaultVal; + prop->order = props.size(); + + props[name] = prop; + + return prop; +} + +GradientProp* TextureNode::addGradientProp(const QString& name, + const QString& displayName, + const Gradient& defaultVal) +{ + auto prop = new GradientProp(); + prop->name = name; + prop->displayName = displayName; + prop->setValue(QVariant::fromValue(Gradient(defaultVal))); + prop->order = props.size(); + + props[name] = prop; + + return prop; +} + +ImageProp* TextureNode::addImageProp(const QString& name, + const QString& displayName) +{ + auto prop = new ImageProp(); + prop->name = name; + prop->displayName = displayName; + prop->order = props.size(); + + props[name] = prop; + + return prop; +} + +CurveProp* TextureNode::addCurveProp(const QString& name, + const QString& displayName) +{ + auto prop = new CurveProp(); + prop->name = name; + prop->displayName = displayName; + prop->order = props.size(); + + props[name] = prop; + + return prop; +} \ No newline at end of file diff --git a/src/texturelab/models.h b/src/texturelab/models.h new file mode 100644 index 00000000..a20e74b9 --- /dev/null +++ b/src/texturelab/models.h @@ -0,0 +1,238 @@ +#ifndef MODELS_H +#define MODELS_H + +#include "gradient.h" +#include +#include +#include +#include +#include +#include +#include +#include + +class QOpenGLFramebufferObject; +class QOpenGLShaderProgram; + +struct RenderCommand; + +class TextureProject; +class TextureNode; +class Comment; +class Frame; +class Connection; +typedef QSharedPointer TextureProjectPtr; +typedef QSharedPointer TextureNodePtr; +typedef QSharedPointer CommentPtr; +typedef QSharedPointer FramePtr; +typedef QSharedPointer ConnectionPtr; + +class Prop; +class PropertyGroup; +class Library; +class NodeTextureRenderer; +struct NodeRenderData; + +class IntProp; +class FloatProp; +class BoolProp; +class EnumProp; +class ColorProp; +class StringProp; +class GradientProp; +class ImageProp; +class CurveProp; + +enum class PackageFileType { Texture, Image }; + +enum class TextureChannel : int { + None = 0, + Albedo = 1, + Normal = 2, + Metalness = 3, + Roughness = 4, + Height = 5, + Alpha = 6, + AO = 7 +}; + +class ProjectFile { +public: + QByteArray contents(); +}; + +class TextureProject : public QEnableSharedFromThis { +public: + QString name = "untitled"; + int randomSeed = 0; + int textureWidth = 1024; + int textureHeight = 1024; + + // texturechannel:nodeid + QMap textureChannels; + + Library* library = nullptr; + QString libraryVersion = "v3"; + + QMap nodes; + QMap connections; + QMap comments; + QMap frames; + + TextureNodePtr getNodeById(const QString& id); + ConnectionPtr getConnectionById(const QString& id); + QVector getNodeDependencies(const QString& id); + QVector getNodeRightOfNode(const QString& id); + + QString exportFilePattern = "${project}_${name}"; + QString exportDestination = ""; + QString filePath = nullptr; + + void addNode(const TextureNodePtr& node); + + // Removes the node along with every connection touching it and any + // texture-channel assignment pointing at it. Downstream nodes are marked + // dirty so they re-render without this node's output. + void removeNode(const QString& id); + + // todo: make two port variant + // Adding or removing a connection marks the right node and everything + // downstream of it dirty — callers don't need to do it themselves. + void addConnection(TextureNodePtr leftNode, TextureNodePtr rightNode, + QString rightNodeInput); + + ConnectionPtr removeConnection(const QString& leftNode, + const QString& rightNode, + const QString& rightNodeInput); + void removeConnection(ConnectionPtr con); + void removeConnection(const QString& id); + + // Marks the node and every node downstream of it as needing a re-render. + void markNodeAsDirty(const TextureNodePtr& node); + + static TextureProjectPtr createEmpty(Library* library = nullptr); +}; + +class TextureNode : public QEnableSharedFromThis { +public: + QString id; + QString typeName; + QString title; + + QVector2D pos; + + QList inputs; + + long randomSeed = 0; + QString exportName; + + QMap props; + QList propertyGroups; + + // texture needs updating + bool isDirty = true; + + // flag to indicate this node processes on CPU instead of GPU shader + bool usesCpuProcessing = false; + + int textureWidth = 0; + int textureHeight = 0; + QOpenGLFramebufferObject* texture = nullptr; + QOpenGLShaderProgram* shader = nullptr; + QString shaderSource; + + TextureNode(); + + virtual void init() {}; + + // Virtual method for CPU processing + virtual void cpuProcess(void* gl, const struct RenderCommand& command) {}; + + void addInput(const QString& inputName); + + void setProp(QString propName, QVariant value); + + Prop* getProp(QString propName); + + bool hasProp(QString propName); + + void setShaderSource(const QString& source) { shaderSource = source; } + + // Override to provide a custom renderer for multi-pass or non-standard + // rendering. Called on the main thread during queueNextNodeToRender(). + // Return nullptr for standard single-pass rendering. + virtual std::shared_ptr createRenderer() + { + return nullptr; + } + + // Override to provide render-time data for the custom renderer. + // Called on the main thread. Must not reference GPU resources. + virtual std::shared_ptr createRenderData() + { + return nullptr; + } + + bool isGraphicsResourcesInitialized() + { + return texture != nullptr && shader != nullptr; + } + + unsigned int textureId(); + + PropertyGroup* createGroup(const QString& name); + + // add prop functions + IntProp* addIntProp(const QString& name, const QString& displayName, + int defaultVal = 1, int minVal = 1, int maxVal = 100, + int increment = 1); + FloatProp* addFloatProp(const QString& name, const QString& displayName, + double defaultVal = 1, double minVal = 1, + double maxVal = 100, double increment = 1); + BoolProp* addBoolProp(const QString& name, const QString& displayName, + bool defaultVal = false); + EnumProp* addEnumProp(const QString& name, const QString& displayName, + QList defaultVal); + ColorProp* addColorProp(const QString& name, const QString& displayName, + const QColor& defaultVal); + StringProp* addStringProp(const QString& name, const QString& displayName, + const QString& defaultVal = ""); + + GradientProp* addGradientProp(const QString& name, + const QString& displayName, + const Gradient& defaultVal); + + ImageProp* addImageProp(const QString& name, const QString& displayName); + + CurveProp* addCurveProp(const QString& name, const QString& displayName); +}; + +class Comment : public QEnableSharedFromThis { +public: + QString id; + QString text; + QVector2D pos; +}; + +class Frame : public QEnableSharedFromThis { +public: + QString id; + QString text; + QColor color = QColor(25, 0, 51); + + QVector2D pos; + QVector2D size; +}; + +class Connection : public QEnableSharedFromThis { +public: + QString id; + + TextureNodePtr leftNode; + TextureNodePtr rightNode; + + QString leftNodeOutputName; + QString rightNodeInputName; +}; + +#endif \ No newline at end of file diff --git a/src/texturelab/project.cpp b/src/texturelab/project.cpp new file mode 100644 index 00000000..a7d16fa2 --- /dev/null +++ b/src/texturelab/project.cpp @@ -0,0 +1,289 @@ +#include "project.h" +#include "libraries/library.h" +#include "libraries/libraryversionmigrator.h" +#include "jsonutils.h" +#include "libraries/libversion.h" +#include "props.h" +#include +#include +#include +#include +#include + +TextureProjectPtr Project::loadTexture(QString path) +{ + QFile file(path); + file.open(QIODevice::ReadOnly); + QJsonParseError error; + auto doc = QJsonDocument::fromJson(file.readAll(), &error); + file.close(); + + if (error.error) { + // report error + return TextureProjectPtr(nullptr); + } + + return Project::loadTextureFromJson(doc.object()); +} + +TextureProjectPtr Project::loadTextureFromJson(QJsonObject json) +{ + TextureProjectPtr texture(new TextureProject()); + + // Pick the library matching this JSON's own version, so legacy + // typeNames (e.g. "floodfill", "bevel", "perlin3d") that no longer + // exist in the current library still resolve instead of crashing. + // Callers that want the file upgraded should run it through + // LibraryVersionMigrator first and pass in the migrated JSON. + LibVersion version = LibraryVersionMigrator(json).sourceVersion(); + Library* lib = createLibraryForVersion(version); + texture->libraryVersion = libVersionToString(version); + + // scene objects + auto sceneObj = json["scene"].toObject(); + auto sceneNodesObj = sceneObj["nodes"].toObject(); + + // load nodes + auto nodeArray = json["nodes"].toArray(); + for (auto item : nodeArray) { + auto nodeDef = item.toObject(); + auto nodeName = nodeDef["typeName"].toString(); + auto node = lib->createNode(nodeName); + // createNode returns null for an unknown/legacy typeName the resolved + // library can't build; skip it rather than dereferencing null. + if (!node) + continue; + node->exportName = nodeDef["exportName"].toString(""); + node->id = nodeDef["id"].toString(); + node->randomSeed = jsonutils::getLong(nodeDef["randomSeed"]); + + // get position from scene + // we're converging the scene and designer props into one + auto sceneObj = sceneNodesObj[node->id].toObject(); + auto x = jsonutils::getFloat(sceneObj["x"]); + auto y = jsonutils::getFloat(sceneObj["y"]); + node->pos = QVector2D(x, y); + + // add props + auto propObj = nodeDef["properties"].toObject(); + for (auto key : propObj.keys()) { + auto prop = node->getProp(key); + if (prop == nullptr) + continue; + + auto jsonProp = propObj[key]; + prop->fromJsonValue(jsonProp); + qDebug() << "Loaded prop" << key << "for node" << prop->getValue(); + // if (jsonProp.isObject()) + // prop->fromJsonValue(jsonProp.toObject()); + // else + // prop->setValue(jsonProp.toVariant()); + } + + texture->nodes[node->id] = node; + } + + // load connections + auto conArray = json["connections"].toArray(); + for (auto item : conArray) { + auto conObj = item.toObject(); + + QString leftNodeId = conObj["leftNodeId"].toString(); + TextureNodePtr leftNode = texture->getNodeById(leftNodeId); + + QString rightNodeId = conObj["rightNodeId"].toString(); + TextureNodePtr rightNode = texture->getNodeById(rightNodeId); + + // Drop connections whose endpoints didn't load (unknown/legacy node + // that was skipped, or a hand-edited/migrated file). Storing a + // connection with a null endpoint would crash save/remove later. + if (!leftNode || !rightNode) + continue; + + QString rightNodeInputId = conObj["rightNodeInput"].toString(); + texture->addConnection(leftNode, rightNode, rightNodeInputId); + } + + // load comments + auto commentArray = sceneObj["comments"].toArray(); + for (auto item : commentArray) { + auto obj = item.toObject(); + CommentPtr comment(new Comment()); + auto commentId = obj["id"].toString(); + comment->id = commentId.isEmpty() + ? QUuid::createUuid().toString(QUuid::WithoutBraces) + : commentId; + comment->text = obj["text"].toString(); + comment->pos = QVector2D(jsonutils::getFloat(obj["x"]), + jsonutils::getFloat(obj["y"])); + texture->comments[comment->id] = comment; + } + + // load frames + auto frameArray = sceneObj["frames"].toArray(); + for (auto item : frameArray) { + auto obj = item.toObject(); + FramePtr frame(new Frame()); + auto frameId = obj["id"].toString(); + frame->id = frameId.isEmpty() + ? QUuid::createUuid().toString(QUuid::WithoutBraces) + : frameId; + frame->text = obj["title"].toString(); + frame->pos = QVector2D(jsonutils::getFloat(obj["x"]), + jsonutils::getFloat(obj["y"])); + frame->size = QVector2D(jsonutils::getFloat(obj["width"], 300), + jsonutils::getFloat(obj["height"], 200)); + auto colorStr = obj["color"].toString(); + if (!colorStr.isEmpty()) + frame->color = QColor(colorStr); + texture->frames[frame->id] = frame; + } + + if (json["export"].isObject()) { + auto exportObj = json["export"].toObject(); + texture->exportFilePattern = + exportObj["filePattern"].toString("${project}_${name}"); + texture->exportDestination = exportObj["destination"].toString(""); + } + + if (json["editor"].isObject()) { + auto editorObject = json["editor"].toObject(); + auto channels = editorObject["textureChannels"].toObject(); + for (auto key : channels.keys()) { + TextureChannel channel = TextureChannel::None; + if (key == "albedo") + channel = TextureChannel::Albedo; + else if (key == "normal") + channel = TextureChannel::Normal; + else if (key == "metalness") + channel = TextureChannel::Metalness; + else if (key == "roughness") + channel = TextureChannel::Roughness; + else if (key == "height") + channel = TextureChannel::Height; + else if (key == "alpha") + channel = TextureChannel::Alpha; + else if (key == "ao") + channel = TextureChannel::AO; + + if (channel != TextureChannel::None) { + auto nodeId = channels[key].toString(); + texture->textureChannels[channel] = nodeId; + } + } + } + + texture->library = lib; + + return texture; +} + +QByteArray Project::saveTexture(TextureProjectPtr texture) +{ + QJsonObject json; + + // nodes + QJsonArray nodeArray; + for (auto& node : texture->nodes) { + QJsonObject nodeDef; + nodeDef["typeName"] = node->typeName; + nodeDef["id"] = node->id; + nodeDef["exportName"] = node->exportName; + nodeDef["randomSeed"] = (double)node->randomSeed; + + QJsonObject propObj; + for (auto key : node->props.keys()) { + propObj[key] = node->props[key]->toJsonValue(); + } + nodeDef["properties"] = propObj; + + nodeArray.append(nodeDef); + } + json["nodes"] = nodeArray; + + // scene (node positions, comments, frames) + QJsonObject sceneObj; + + QJsonObject sceneNodesObj; + for (auto& node : texture->nodes) { + QJsonObject posObj; + posObj["x"] = node->pos.x(); + posObj["y"] = node->pos.y(); + sceneNodesObj[node->id] = posObj; + } + sceneObj["nodes"] = sceneNodesObj; + + QJsonArray commentArray; + for (auto& comment : texture->comments) { + QJsonObject obj; + obj["id"] = comment->id; + obj["text"] = comment->text; + obj["x"] = comment->pos.x(); + obj["y"] = comment->pos.y(); + commentArray.append(obj); + } + sceneObj["comments"] = commentArray; + + QJsonArray frameArray; + for (auto& frame : texture->frames) { + QJsonObject obj; + obj["id"] = frame->id; + obj["title"] = frame->text; + obj["color"] = frame->color.name(QColor::HexRgb); + obj["x"] = frame->pos.x(); + obj["y"] = frame->pos.y(); + obj["width"] = frame->size.x(); + obj["height"] = frame->size.y(); + frameArray.append(obj); + } + sceneObj["frames"] = frameArray; + + json["scene"] = sceneObj; + + // connections + QJsonArray conArray; + for (auto& con : texture->connections) { + // Never serialize a connection with a missing endpoint (would crash on + // the deref below and produce an unloadable file). + if (!con || !con->leftNode || !con->rightNode) + continue; + QJsonObject conObj; + conObj["leftNodeId"] = con->leftNode->id; + conObj["rightNodeId"] = con->rightNode->id; + conObj["rightNodeInput"] = con->rightNodeInputName; + conArray.append(conObj); + } + json["connections"] = conArray; + + // library version this project's nodes/properties were saved against + json["libraryVersion"] = texture->libraryVersion; + + // export settings + QJsonObject exportObj; + exportObj["filePattern"] = texture->exportFilePattern; + exportObj["destination"] = texture->exportDestination; + json["export"] = exportObj; + + // editor texture channels + QJsonObject channelsObj; + for (auto it = texture->textureChannels.begin(); + it != texture->textureChannels.end(); ++it) { + QString key; + switch (it.key()) { + case TextureChannel::Albedo: key = "albedo"; break; + case TextureChannel::Normal: key = "normal"; break; + case TextureChannel::Metalness: key = "metalness"; break; + case TextureChannel::Roughness: key = "roughness"; break; + case TextureChannel::Height: key = "height"; break; + case TextureChannel::Alpha: key = "alpha"; break; + case TextureChannel::AO: key = "ao"; break; + default: continue; + } + channelsObj[key] = it.value(); + } + QJsonObject editorObj; + editorObj["textureChannels"] = channelsObj; + json["editor"] = editorObj; + + return QJsonDocument(json).toJson(); +} \ No newline at end of file diff --git a/src/texturelab/project.h b/src/texturelab/project.h new file mode 100644 index 00000000..9c21ca1d --- /dev/null +++ b/src/texturelab/project.h @@ -0,0 +1,17 @@ +#pragma once + +#include "models.h" +#include + +class Project +{ +public: + static TextureProjectPtr loadTexture(QString path); + + // Builds a project from already-parsed (and possibly migrated) JSON. + // Used directly by MainWindow when it has run the JSON through + // LibraryVersionMigrator before constructing any node/library objects. + static TextureProjectPtr loadTextureFromJson(QJsonObject json); + + static QByteArray saveTexture(TextureProjectPtr texture); +}; \ No newline at end of file diff --git a/src/texturelab/props.cpp b/src/texturelab/props.cpp new file mode 100644 index 00000000..8d8fd467 --- /dev/null +++ b/src/texturelab/props.cpp @@ -0,0 +1,118 @@ +#include "props.h" + +#include + +QString createGuid() +{ + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +QString PropType::toString(Value propType) +{ + switch (propType) { + case Float: + return "float"; + case Int: + return "int"; + case Bool: + return "bool"; + case Color: + return "color"; + case Enum: + return "enum"; + case String: + return "string"; + case Gradient: + return "gradient"; + case Image: + return "image"; + } + + return "unknown"; +} + +PropType::Value PropType::fromString(QString propType) +{ + if (propType == "float") + return Float; + if (propType == "int") + return Int; + if (propType == "bool") + return Bool; + if (propType == "color") + return Color; + if (propType == "enum") + return Enum; + if (propType == "string") + return String; + if (propType == "gradient") + return Gradient; + if (propType == "image") + return Image; + + return Unknown; +} + +Prop::Prop() { id = createGuid(); } + +QJsonObject Prop::toJson() +{ + QJsonObject obj; + // obj["id"] = id; + // obj["name"] = name; + // obj["displayName"] = displayName; + return obj; +} +void Prop::fromJson(const QJsonObject& obj) +{ + // id = obj["id"].toString(); + // name = obj["name"].toString(); + // displayName = obj["displayName"].toString(); +} + +QJsonValue Prop::toJsonValue() +{ + // Default implementation returns null + return QJsonValue(); +} + +void Prop::fromJsonValue(const QJsonValue& obj) +{ + // Default implementation does nothing +} + +void ImageProp::updateTexture() +{ + // If value is null, clear the texture + if (value.isNull()) { + if (texture != nullptr) { + delete texture; + texture = nullptr; + } + _textureDirty = false; + return; + } + + // Only upload texture if dirty or not yet created + if (!_textureDirty && texture != nullptr) + return; + + // Delete old texture if it exists + if (texture != nullptr) { + delete texture; + texture = nullptr; + } + + // Create and upload new texture + texture = new QOpenGLTexture(value.mirrored(false, true)); + texture->setMinificationFilter(QOpenGLTexture::Nearest); + texture->setMagnificationFilter(QOpenGLTexture::Nearest); + texture->setWrapMode(QOpenGLTexture::Repeat); + + _textureDirty = false; +} + +GLuint ImageProp::getTextureId() const +{ + return texture ? texture->textureId() : 0; +} \ No newline at end of file diff --git a/src/texturelab/props.h b/src/texturelab/props.h new file mode 100644 index 00000000..50be9911 --- /dev/null +++ b/src/texturelab/props.h @@ -0,0 +1,641 @@ +#pragma once + +#include "../colorpicker/gradient.h" +#include "curve.h" +#include "jsonutils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class Prop; +class PropertyGroup; +class QOpenGLTexture; + +// http://techiesolves.blogspot.com/2018/01/base64-qstring-to-qimage-to-qstring-in.html +QString createGuid(); +class PropType { +public: + enum Value { + Unknown = -1, + Float = 0, + Int, + Bool, + Color, + Enum, + String, + Gradient, + Image, + Curve + }; + + static QString toString(Value propType); + + static Value fromString(QString propType); +}; + +class Prop { +public: + QString id; + QString name; + QString displayName; + PropType::Value type; + int order = 0; // for tracking order in UI + + PropertyGroup* group = nullptr; + + Prop(); + + virtual QVariant getValue() = 0; + virtual void setValue(QVariant val) = 0; + virtual Prop* clone() const = 0; + + virtual QJsonObject toJson(); + virtual void fromJson(const QJsonObject& obj); + + // for old style direct value parsing + virtual QJsonValue toJsonValue(); + virtual void fromJsonValue(const QJsonValue& obj); + + virtual ~Prop() {} +}; + +class PropertyGroup { +public: + QString name; + QList props; + bool collapsed = true; + + void add(Prop* prop) + { + this->props.append(prop); + prop->group = this; + } +}; + +class FloatProp : public Prop { +public: + double value; + double minValue; + double maxValue; + double step; + + FloatProp() : Prop() + { + value = 0; + minValue = 0; + maxValue = 1; + step = 0.1; + type = PropType::Float; + } + + Prop* clone() const override + { + auto* copy = new FloatProp(*this); + copy->group = nullptr; + return copy; + } + + QVariant getValue() override { return QVariant::fromValue(value); } + + void setValue(QVariant val) override { value = val.toDouble(); } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + obj["value"] = value; + obj["minValue"] = minValue; + obj["maxValue"] = maxValue; + obj["step"] = step; + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + value = jsonutils::getDouble(obj["value"], value); + minValue = jsonutils::getDouble(obj["minValue"], minValue); + maxValue = jsonutils::getDouble(obj["maxValue"], maxValue); + step = jsonutils::getDouble(obj["step"], step); + } + + QJsonValue toJsonValue() override { return value; } + + void fromJsonValue(const QJsonValue& val) override + { + value = jsonutils::getDouble(val, value); + } +}; + +class IntProp : public Prop { +public: + long value; + long minValue; + long maxValue; + long step; + + IntProp() : Prop() + { + value = 0; + minValue = 0; + maxValue = 100; + step = 1; + type = PropType::Int; + } + + Prop* clone() const override + { + auto* copy = new IntProp(*this); + copy->group = nullptr; + return copy; + } + + QVariant getValue() override { return QVariant::fromValue(value); } + + void setValue(QVariant val) override { value = (long)val.toDouble(); } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + obj["value"] = (qlonglong)value; + obj["minValue"] = (qlonglong)minValue; + obj["maxValue"] = (qlonglong)maxValue; + obj["step"] = (qlonglong)step; + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + value = jsonutils::getLong(obj["value"], value); + minValue = jsonutils::getLong(obj["minValue"], minValue); + maxValue = jsonutils::getLong(obj["maxValue"], maxValue); + step = jsonutils::getLong(obj["step"], step); + } + + QJsonValue toJsonValue() override { return (qlonglong)value; } + + void fromJsonValue(const QJsonValue& val) override + { + value = jsonutils::getLong(val, value); + } +}; + +class BoolProp : public Prop { +public: + bool value; + + BoolProp() : Prop() + { + type = PropType::Bool; + value = false; + } + + Prop* clone() const override + { + auto* copy = new BoolProp(*this); + copy->group = nullptr; + return copy; + } + + QVariant getValue() override { return value; } + + void setValue(QVariant val) override { value = val.toBool(); } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + obj["value"] = value ? "true" : "false"; + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + value = jsonutils::getBool(obj["value"], value); + } + + QJsonValue toJsonValue() override { return value; } + + void fromJsonValue(const QJsonValue& val) override + { + value = jsonutils::getBool(val, value); + } +}; + +class EnumProp : public Prop { +public: + QList values; + int index; + + EnumProp() : Prop() + { + type = PropType::Enum; + index = 0; + } + + Prop* clone() const override + { + auto* copy = new EnumProp(*this); + copy->group = nullptr; + return copy; + } + + QVariant getValue() override { return index; } + + void setValue(QVariant val) override { index = val.value(); } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + obj["index"] = index; + + // values + QJsonArray valueList; + for (auto enumValue : values) { + valueList.append(enumValue); + } + obj["values"] = valueList; + + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + index = jsonutils::getInt(obj["index"], index); + + auto list = obj["values"].toArray(); + values.clear(); + for (auto item : list) { + values.append(item.toString()); + } + } + + QJsonValue toJsonValue() override { return index; } + + void fromJsonValue(const QJsonValue& val) override + { + index = jsonutils::getInt(val, index); + } +}; + +struct ColorProp : public Prop { + QColor value; + + ColorProp() : Prop() { type = PropType::Color; } + + Prop* clone() const override + { + auto* copy = new ColorProp(*this); + copy->group = nullptr; + return copy; + } + + QVariant getValue() override { return value; } + + void setValue(QVariant val) override { value = val.value(); } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + QJsonObject colObj; + colObj["r"] = value.redF(); + colObj["g"] = value.greenF(); + colObj["b"] = value.blueF(); + colObj["a"] = value.alphaF(); + obj["value"] = colObj; + + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + auto colorObj = obj["value"].toObject(); + value.setRedF(jsonutils::getFloat(colorObj["r"])); + value.setGreenF(jsonutils::getFloat(colorObj["g"])); + value.setBlueF(jsonutils::getFloat(colorObj["b"])); + value.setAlphaF(jsonutils::getFloat(colorObj["a"], 1.0f)); + } + + QJsonValue toJsonValue() override + { + QJsonObject colObj; + colObj["r"] = value.redF(); + colObj["g"] = value.greenF(); + colObj["b"] = value.blueF(); + colObj["a"] = value.alphaF(); + return colObj; + } + + void fromJsonValue(const QJsonValue& val) override + { + auto colorObj = val.toObject(); + value.setRedF(jsonutils::getFloat(colorObj["r"])); + value.setGreenF(jsonutils::getFloat(colorObj["g"])); + value.setBlueF(jsonutils::getFloat(colorObj["b"])); + value.setAlphaF(jsonutils::getFloat(colorObj["a"], 1.0f)); + } +}; + +class StringProp : public Prop { +public: + QString value; + + StringProp() : Prop() { type = PropType::String; } + + Prop* clone() const override + { + auto* copy = new StringProp(*this); + copy->group = nullptr; + return copy; + } + + QVariant getValue() override { return value; } + + void setValue(QVariant val) override { value = val.value(); } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + obj["value"] = value; + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + value = obj["value"].toString(); + } + + QJsonValue toJsonValue() override { return value; } + + void fromJsonValue(const QJsonValue& val) override + { + value = val.toString(); + } +}; + +class GradientProp : public Prop { +public: + Gradient value; + + GradientProp() : Prop() + { + type = PropType::Gradient; + value = Gradient::defaultGradient(); + } + + Prop* clone() const override + { + auto* copy = new GradientProp(*this); + copy->group = nullptr; + return copy; + } + + QVariant getValue() override { return QVariant::fromValue(value); } + + void setValue(QVariant val) override { value = val.value(); } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + + // Serialize gradient points + QJsonArray pointsArray; + for (const auto& point : value.points) { + QJsonObject pointObj; + pointObj["t"] = point.position; + QJsonObject colorObj; + colorObj["r"] = point.color.redF(); + colorObj["g"] = point.color.greenF(); + colorObj["b"] = point.color.blueF(); + colorObj["a"] = point.color.alphaF(); + pointObj["color"] = colorObj; + pointsArray.append(pointObj); + } + obj["points"] = pointsArray; + + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + + auto pointsArray = obj["points"].toArray(); + value.points.clear(); + + for (const auto& pointValue : pointsArray) { + auto pointObj = pointValue.toObject(); + float position = jsonutils::getFloat(pointObj["t"]); + auto colorObj = pointObj["color"].toObject(); + QColor color; + color.setRedF(jsonutils::getFloat(colorObj["r"])); + color.setGreenF(jsonutils::getFloat(colorObj["g"])); + color.setBlueF(jsonutils::getFloat(colorObj["b"])); + color.setAlphaF(jsonutils::getFloat(colorObj["a"], 1.0f)); + + value.addPoint(GradientPoint(position, color)); + } + } + + QJsonValue toJsonValue() override + { + QJsonObject obj; + QJsonArray pointsArray; + for (const auto& point : value.points) { + QJsonObject pointObj; + pointObj["t"] = point.position; + QJsonObject colorObj; + colorObj["r"] = point.color.redF(); + colorObj["g"] = point.color.greenF(); + colorObj["b"] = point.color.blueF(); + colorObj["a"] = point.color.alphaF(); + pointObj["color"] = colorObj; + pointsArray.append(pointObj); + } + obj["points"] = pointsArray; + + return obj; + } + + void fromJsonValue(const QJsonValue& val) override + { + auto pointsArray = val.toObject()["points"].toArray(); + value.points.clear(); + + for (const auto& pointValue : pointsArray) { + auto pointObj = pointValue.toObject(); + float position = jsonutils::getFloat(pointObj["t"]); + auto colorObj = pointObj["color"].toObject(); + QColor color; + color.setRedF(jsonutils::getFloat(colorObj["r"])); + color.setGreenF(jsonutils::getFloat(colorObj["g"])); + color.setBlueF(jsonutils::getFloat(colorObj["b"])); + color.setAlphaF(jsonutils::getFloat(colorObj["a"], 1.0f)); + + value.addPoint(GradientPoint(position, color)); + } + } +}; + +class CurveProp : public Prop { +public: + Curve value; // default: linear identity + + CurveProp() : Prop() { type = PropType::Curve; } + + Prop* clone() const override + { + auto* copy = new CurveProp(*this); + copy->group = nullptr; + return copy; + } + + QVariant getValue() override { return QVariant::fromValue(value); } + + void setValue(QVariant val) override { value = val.value(); } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + obj["value"] = value.toJson(); + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + if (obj.contains("value") && obj["value"].isObject()) + value = Curve::fromJson(obj["value"].toObject()); + else + value = Curve(); // fallback to linear identity + } + + QJsonValue toJsonValue() override { return value.toJson(); } + + void fromJsonValue(const QJsonValue& val) override + { + if (val.isObject()) + value = Curve::fromJson(val.toObject()); + else + value = Curve(); + } +}; + +class ImageProp : public Prop { + +public: + QImage value; + + // when this is true, the next render should update the texture + // with the image data + // std::atomic _textureDirty = true; + bool _textureDirty = true; + // GLuint textureId = 0; + QOpenGLTexture* texture = nullptr; + + ImageProp() : Prop() { type = PropType::Image; } + + Prop* clone() const override + { + auto* copy = new ImageProp(*this); + copy->group = nullptr; + return copy; + } + + bool isTextureDirty() const { return _textureDirty; } + void setTextureClean() { _textureDirty = false; } + + // Updates or creates the OpenGL texture from the image data + // Should be called from the main OpenGL context thread + void updateTexture(); + + GLuint getTextureId() const; + + QVariant getValue() override { return value; } + + void setValue(QVariant val) override + { + value = val.value(); + _textureDirty = true; // Mark texture as dirty when value changes + } + + QJsonObject toJson() override + { + auto obj = Prop::toJson(); + if (value.isNull()) { + obj["value"] = ""; + return obj; + } + + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + value.save(&buffer, "PNG"); + QString encoded = buffer.data().toBase64(); + + obj["value"] = "data:image/png;base64," + encoded; + return obj; + } + + void fromJson(const QJsonObject& obj) override + { + Prop::fromJson(obj); + + auto stringData = obj["value"].toString(); + if (stringData.isNull() || stringData.isEmpty()) + return; + + auto parts = stringData.split(";base64,"); + if (parts.length() < 2) + return; + + auto bytes = QByteArray::fromBase64(parts[1].toUtf8()); + + QImage image; + image.loadFromData(bytes); + this->value = image; + _textureDirty = true; + } + + QJsonValue toJsonValue() override + { + if (value.isNull()) { + return ""; + } + + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + value.save(&buffer, "PNG"); + QString encoded = buffer.data().toBase64(); + + return "data:image/png;base64," + encoded; + } + + void fromJsonValue(const QJsonValue& val) override + { + auto stringData = val.toString(); + if (stringData.isNull() || stringData.isEmpty()) + return; + + auto parts = stringData.split(";base64,"); + if (parts.length() < 2) + return; + + auto bytes = QByteArray::fromBase64(parts[1].toUtf8()); + + QImage image; + image.loadFromData(bytes); + this->value = image; + _textureDirty = true; + } +}; \ No newline at end of file diff --git a/src/texturelab/systeminfo.cpp b/src/texturelab/systeminfo.cpp new file mode 100644 index 00000000..45c235bc --- /dev/null +++ b/src/texturelab/systeminfo.cpp @@ -0,0 +1,244 @@ +#include "systeminfo.h" + +#include "telemetry.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(Q_OS_LINUX) +#include +#elif defined(Q_OS_WIN) +#include +#elif defined(Q_OS_MACOS) +#include +#include +#endif + +namespace { + +// Not in QOpenGLFunctions' enum set; both extensions are queried through the +// plain glGetIntegerv the base functions object already gives us. +constexpr GLenum kGpuMemInfoDedicatedVidmemNvx = 0x9047; +constexpr GLenum kGpuMemInfoCurrentAvailableVidmemNvx = 0x9049; +constexpr GLenum kTextureFreeMemoryAti = 0x87FC; + +// Windows' GL/gl.h stops at 1.1, so these aren't guaranteed to exist even +// though every context we ever create is 3.2 core. +#ifndef GL_MAX_RENDERBUFFER_SIZE +#define GL_MAX_RENDERBUFFER_SIZE 0x84E8 +#endif +#ifndef GL_MAX_SAMPLES +#define GL_MAX_SAMPLES 0x8D57 +#endif +#ifndef GL_SHADING_LANGUAGE_VERSION +#define GL_SHADING_LANGUAGE_VERSION 0x8B8C +#endif + +std::string glStringOr(QOpenGLFunctions* f, GLenum name, const char* fallback) +{ + const GLubyte* s = f->glGetString(name); + return s ? reinterpret_cast(s) : fallback; +} + +int64_t physicalMemoryKb() +{ +#if defined(Q_OS_LINUX) + struct sysinfo info; + if (sysinfo(&info) == 0) + return (int64_t)info.totalram * info.mem_unit / 1024; +#elif defined(Q_OS_WIN) + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + if (GlobalMemoryStatusEx(&status)) + return (int64_t)(status.ullTotalPhys / 1024); +#elif defined(Q_OS_MACOS) + int64_t bytes = 0; + size_t len = sizeof(bytes); + if (sysctlbyname("hw.memsize", &bytes, &len, nullptr, 0) == 0) + return bytes / 1024; +#endif + return 0; +} + +int64_t freeMemoryKb() +{ +#if defined(Q_OS_LINUX) + struct sysinfo info; + if (sysinfo(&info) == 0) + return (int64_t)(info.freeram + info.bufferram) * info.mem_unit / 1024; +#elif defined(Q_OS_WIN) + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + if (GlobalMemoryStatusEx(&status)) + return (int64_t)(status.ullAvailPhys / 1024); +#endif + return 0; +} + +// How this build was delivered, which decides whether a driver-side crash is +// even our code's fault — an AppImage carries its own Qt and libstdc++ into a +// host GL stack it was never built against. +std::string packaging() +{ + const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + if (env.contains(QStringLiteral("APPIMAGE"))) + return "appimage"; +#if defined(Q_OS_MACOS) + if (QCoreApplication::applicationDirPath().contains(QStringLiteral(".app/Contents/"))) + return "macos-bundle"; +#endif + return "native"; +} + +} // namespace + +SystemInfo::GpuMemory SystemInfo::queryGpuMemory() +{ + GpuMemory mem; + + QOpenGLContext* ctx = QOpenGLContext::currentContext(); + if (!ctx) + return mem; + + QOpenGLFunctions* f = ctx->functions(); + + if (ctx->hasExtension(QByteArrayLiteral("GL_NVX_gpu_memory_info"))) { + GLint total = 0; + GLint available = 0; + f->glGetIntegerv(kGpuMemInfoDedicatedVidmemNvx, &total); + f->glGetIntegerv(kGpuMemInfoCurrentAvailableVidmemNvx, &available); + mem.known = true; + mem.totalKb = total; + mem.availableKb = available; + } + else if (ctx->hasExtension(QByteArrayLiteral("GL_ATI_meminfo"))) { + // Returns 4 ints; the first is the free pool in KB. Total isn't + // exposed, so it stays 0 and callers fall back to the free figure. + GLint values[4] = {0, 0, 0, 0}; + f->glGetIntegerv(kTextureFreeMemoryAti, values); + mem.known = true; + mem.availableKb = values[0]; + } + + // Whatever the query left behind is not our caller's problem to notice. + while (f->glGetError() != GL_NO_ERROR) { + } + + return mem; +} + +void SystemInfo::reportSystemContext() +{ + const int64_t totalRamKb = physicalMemoryKb(); + + Telemetry::Fields device = { + {"arch", QSysInfo::currentCpuArchitecture().toStdString()}, + {"cpu_count", (int64_t)QThread::idealThreadCount()}, + {"memory_size", totalRamKb * 1024}, + {"free_memory", freeMemoryKb() * 1024}, + {"model", QSysInfo::prettyProductName().toStdString()}, + {"kernel_version", QSysInfo::kernelVersion().toStdString()}, + }; + + if (QScreen* screen = QGuiApplication::primaryScreen()) { + device.emplace_back("screen_width_pixels", + (int64_t)screen->geometry().width()); + device.emplace_back("screen_height_pixels", + (int64_t)screen->geometry().height()); + device.emplace_back("screen_density", screen->devicePixelRatio()); + } + device.emplace_back("screen_count", + (int64_t)QGuiApplication::screens().size()); + + Telemetry::setContext("device", device); + + const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + const std::string pkg = packaging(); + + Telemetry::setContext( + "runtime", + { + {"name", std::string("Qt")}, + {"version", std::string(qVersion())}, + {"build_version", std::string(QT_VERSION_STR)}, + {"platform_plugin", QGuiApplication::platformName().toStdString()}, + {"session_type", + env.value(QStringLiteral("XDG_SESSION_TYPE"), QStringLiteral("unknown")) + .toStdString()}, + {"packaging", pkg}, + }); + + Telemetry::setTag("qt.platform", QGuiApplication::platformName().toStdString()); + Telemetry::setTag("packaging", pkg); +} + +void SystemInfo::reportGpuContext() +{ + QOpenGLContext* ctx = QOpenGLContext::currentContext(); + if (!ctx) { + Telemetry::breadcrumb("gpu", "reportGpuContext called with no current context"); + return; + } + + QOpenGLFunctions* f = ctx->functions(); + + const std::string vendor = glStringOr(f, GL_VENDOR, "unknown"); + const std::string renderer = glStringOr(f, GL_RENDERER, "unknown"); + const std::string version = glStringOr(f, GL_VERSION, "unknown"); + + GLint maxTextureSize = 0; + GLint maxRenderbufferSize = 0; + GLint maxSamples = 0; + f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize); + f->glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxRenderbufferSize); + f->glGetIntegerv(GL_MAX_SAMPLES, &maxSamples); + + const QSurfaceFormat fmt = ctx->format(); + const GpuMemory mem = queryGpuMemory(); + + Telemetry::Fields gpu = { + {"name", renderer}, + {"vendor_name", vendor}, + {"version", version}, + {"api_type", std::string("OpenGL")}, + {"shading_language_version", + glStringOr(f, GL_SHADING_LANGUAGE_VERSION, "unknown")}, + {"granted_version", + std::to_string(fmt.majorVersion()) + "." + std::to_string(fmt.minorVersion())}, + {"granted_profile", + std::string(fmt.profile() == QSurfaceFormat::CoreProfile ? "core" + : fmt.profile() == QSurfaceFormat::CompatibilityProfile + ? "compatibility" + : "none")}, + {"max_texture_size", (int64_t)maxTextureSize}, + {"max_renderbuffer_size", (int64_t)maxRenderbufferSize}, + {"max_samples", (int64_t)maxSamples}, + {"memory_reporting", mem.known}, + }; + + if (mem.known) { + // Sentry renders gpu.memory_size as MB. + gpu.emplace_back("memory_size", mem.totalKb / 1024); + gpu.emplace_back("free_memory_mb", mem.availableKb / 1024); + } + + Telemetry::setContext("gpu", gpu); + + Telemetry::setTag("gpu.vendor", vendor); + Telemetry::setTag("gpu.renderer", renderer); + Telemetry::setTag("gl.version", version); + + Telemetry::breadcrumb("gpu", renderer + " / " + version, + {{"vendor", vendor}, + {"max_texture_size", (int64_t)maxTextureSize}, + {"vram_total_mb", mem.known ? mem.totalKb / 1024 : (int64_t)-1}}); +} diff --git a/src/texturelab/systeminfo.h b/src/texturelab/systeminfo.h new file mode 100644 index 00000000..576614f6 --- /dev/null +++ b/src/texturelab/systeminfo.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +// Hardware and environment reporting for crash triage. +// +// Exists because a Sentry report that says only "it aborted somewhere in the GL +// driver" is unactionable: the GTX 750 / 4K report that prompted this had no +// GPU, no VRAM figure, and no clue that a resolution change had just happened. +// Everything here funnels into Telemetry contexts/tags and no-ops when crash +// reporting is off. +namespace SystemInfo { + +// What the driver will tell us about video memory. `known` is false when +// neither GL_NVX_gpu_memory_info nor GL_ATI_meminfo is advertised — which is +// common enough that callers must handle it rather than assume a number. +struct GpuMemory { + bool known = false; + int64_t totalKb = 0; + int64_t availableKb = 0; +}; + +// Requires a current OpenGL context. Cheap enough to call per user action. +GpuMemory queryGpuMemory(); + +// Register the non-GL half: RAM, CPU, screens, Qt/platform/packaging. +// Call once, before any GL work — the crash we're chasing happened *during* GL +// init, so this has to already be on the scope by then. +void reportSystemContext(); + +// Register the GPU half. Requires a current OpenGL context; call once from +// TextureRenderer::setup() where that's guaranteed. +void reportGpuContext(); + +} // namespace SystemInfo diff --git a/src/texturelab/telemetry.cpp b/src/texturelab/telemetry.cpp new file mode 100644 index 00000000..d276cc94 --- /dev/null +++ b/src/texturelab/telemetry.cpp @@ -0,0 +1,194 @@ +#include "telemetry.h" +#include "version.h" + +#include + +#include +#include +#include +#include +#include + +#include +#include + +static bool g_enabled = false; + +namespace { + +// Explicit scope: init() runs before the organization and application names are +// set on QApplication, so the default constructor would read the wrong file. +QSettings consentSettings() +{ + return QSettings(QSettings::UserScope, QStringLiteral("texturelab"), + QStringLiteral("texturelab")); +} + +constexpr const char* kAllowedKey = "crashReporting"; +constexpr const char* kAskedVersionKey = "crashReportingConsentVersion"; + +// Fields -> sentry object. Kept in one place so breadcrumbs, contexts and +// events all serialize identically. +sentry_value_t toSentryObject(const Telemetry::Fields& fields) +{ + sentry_value_t obj = sentry_value_new_object(); + for (const auto& [key, value] : fields) { + sentry_value_t v = std::visit( + [](const auto& held) -> sentry_value_t { + using T = std::decay_t; + if constexpr (std::is_same_v) + return sentry_value_new_string(held.c_str()); + else if constexpr (std::is_same_v) + // sentry_value_new_int32 would silently truncate byte + // counts, so anything that doesn't fit goes as a double. + return held >= INT32_MIN && held <= INT32_MAX + ? sentry_value_new_int32((int32_t)held) + : sentry_value_new_double((double)held); + else if constexpr (std::is_same_v) + return sentry_value_new_double(held); + else + return sentry_value_new_bool(held); + }, + value); + sentry_value_set_by_key(obj, key.c_str(), v); + } + return obj; +} + +} // namespace + +void Telemetry::init(bool enabled) +{ + // TEXTURELAB_SENTRY_DSN is injected at compile time from CMake. + // Fall back to the hardcoded DSN for local dev builds. + const char* dsn = TEXTURELAB_SENTRY_DSN; + if (!dsn || dsn[0] == '\0') + dsn = "https://93029fba3adb2d1782affd712d013a2b@o216182.ingest.us.sentry.io/4511628329680896"; + if (!enabled) { + g_enabled = false; + return; + } + + sentry_options_t* options = sentry_options_new(); + + sentry_options_set_dsn(options, dsn); + sentry_options_set_release(options, "texturelab@" TEXTURELAB_VERSION "+" TEXTURELAB_BUILD_HASH); + sentry_options_set_debug(options, 0); + + // Writable per-user directory for Crashpad's crash database + QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + + "/sentry"; + QDir().mkpath(dataDir); + sentry_options_set_database_path(options, dataDir.toStdString().c_str()); + + // crashpad_handler lives next to the texturelab executable + QString handlerPath = QCoreApplication::applicationDirPath() +#ifdef Q_OS_WIN + + "/crashpad_handler.exe"; +#else + + "/crashpad_handler"; +#endif + sentry_options_set_handler_path(options, handlerPath.toStdString().c_str()); + + sentry_init(options); + g_enabled = true; +} + +void Telemetry::close() +{ + if (g_enabled) + sentry_close(); +} + +void Telemetry::setEnabled(bool enabled) +{ + if (enabled == g_enabled) + return; + + if (enabled) { + init(true); + } + else { + sentry_close(); + g_enabled = false; + } +} + +bool Telemetry::isEnabled() +{ + return g_enabled; +} + +bool Telemetry::isAllowed() +{ + // Defaults to off: an install that has never answered the prompt has not + // agreed to anything, and a crash before the first answer is the one case + // where staying quiet costs the least. + return consentSettings().value(QLatin1String(kAllowedKey), false).toBool(); +} + +bool Telemetry::consentNeeded() +{ + const QString asked = + consentSettings().value(QLatin1String(kAskedVersionKey)).toString(); + return asked != QLatin1String(TEXTURELAB_VERSION); +} + +void Telemetry::recordConsent(bool allowed) +{ + QSettings settings = consentSettings(); + settings.setValue(QLatin1String(kAllowedKey), allowed); + settings.setValue(QLatin1String(kAskedVersionKey), QLatin1String(TEXTURELAB_VERSION)); +} + +void Telemetry::breadcrumb(const char* category, const std::string& message) +{ + Telemetry::breadcrumb(category, message, {}); +} + +void Telemetry::breadcrumb(const char* category, const std::string& message, + const Fields& data) +{ + if (!g_enabled) + return; + + sentry_value_t crumb = sentry_value_new_breadcrumb("default", message.c_str()); + sentry_value_set_by_key(crumb, "category", sentry_value_new_string(category)); + if (!data.empty()) + sentry_value_set_by_key(crumb, "data", toSentryObject(data)); + sentry_add_breadcrumb(crumb); +} + +void Telemetry::setTag(const char* key, const std::string& value) +{ + if (!g_enabled) + return; + + sentry_set_tag(key, value.c_str()); +} + +void Telemetry::setContext(const char* name, const Fields& fields) +{ + if (!g_enabled) + return; + + sentry_set_context(name, toSentryObject(fields)); +} + +void Telemetry::captureException(const std::string& message) +{ + Telemetry::captureException(message, {}); +} + +void Telemetry::captureException(const std::string& message, const Fields& data) +{ + if (!g_enabled) + return; + + sentry_value_t event = sentry_value_new_event(); + sentry_value_t exc = sentry_value_new_exception("Exception", message.c_str()); + sentry_event_add_exception(event, exc); + if (!data.empty()) + sentry_value_set_by_key(event, "extra", toSentryObject(data)); + sentry_capture_event(event); +} diff --git a/src/texturelab/telemetry.h b/src/texturelab/telemetry.h new file mode 100644 index 00000000..704e9ed5 --- /dev/null +++ b/src/texturelab/telemetry.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Telemetry { + +// Structured payload attached to a breadcrumb, context or event. Deliberately +// Qt-free and sentry-free so this header stays cheap to include: the mapping +// onto sentry_value_t lives in telemetry.cpp. +using FieldValue = std::variant; +using Fields = std::vector>; + +// Call before QApplication. No-op if DSN is empty or user opted out. +void init(bool enabled); + +// Call after a.exec() returns to flush queued events. +void close(); + +// Turns collection on or off after startup, which is what the consent prompt +// needs: answering it has to take effect now, not on the next launch. +void setEnabled(bool enabled); + +// Whether crash reporting is currently running. +bool isEnabled(); + +// The user's stored answer. Off until they have actually been asked — the +// absence of an answer is not consent. +bool isAllowed(); + +// True when the consent prompt is owed. Asked once per released version, so a +// build that changes what gets collected gets a fresh answer rather than +// inheriting one given about an older one. The build hash is deliberately not +// part of the comparison, or every dev build would re-ask. +bool consentNeeded(); + +// Stores the answer, and stamps the version it was given about. +void recordConsent(bool allowed); + +// Add a breadcrumb (category + message) to the current session context. +// +// With the Crashpad backend every breadcrumb flushes the scope to disk so it +// survives a hard crash — which is the point, but it also means these belong at +// user-action granularity. Do not add per-node or per-frame breadcrumbs. +void breadcrumb(const char* category, const std::string& message); +void breadcrumb(const char* category, const std::string& message, + const Fields& data); + +// Set a searchable tag on every subsequent event. Cheap enough to keep current +// as session state changes (resolution, node count), which is what makes a +// crash event self-describing. +void setTag(const char* key, const std::string& value); + +// Set a structured context block (Sentry's "gpu", "device", "app", ...). +void setContext(const char* name, const Fields& fields); + +// Capture a handled exception (e.g. from a catch block) as a Sentry error event. +void captureException(const std::string& message); +void captureException(const std::string& message, const Fields& data); + +} // namespace Telemetry diff --git a/src/texturelab/texturelab.rc b/src/texturelab/texturelab.rc new file mode 100644 index 00000000..d19f9761 --- /dev/null +++ b/src/texturelab/texturelab.rc @@ -0,0 +1,3 @@ +// Windows resource script: embeds the application icon in texturelab.exe +// (Explorer, taskbar, Alt-Tab). The first ICON resource is the one Windows uses. +IDI_ICON1 ICON "../icons/logo.ico" diff --git a/src/texturelab/undo/addcommentcommand.cpp b/src/texturelab/undo/addcommentcommand.cpp new file mode 100644 index 00000000..40fd6794 --- /dev/null +++ b/src/texturelab/undo/addcommentcommand.cpp @@ -0,0 +1,36 @@ +#include "addcommentcommand.h" + +#include "graph/comment.h" +#include "graph/scene.h" + +AddCommentCommand::AddCommentCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QString& commentId, + QVector2D pos) + : QUndoCommand("Add Comment") + , _project(project) + , _scene(scene) + , _commentId(commentId) + , _pos(pos) +{} + +void AddCommentCommand::redo() +{ + auto modelComment = CommentPtr(new Comment()); + modelComment->id = _commentId; + modelComment->pos = _pos; + _project->comments[_commentId] = modelComment; + + auto gcomment = nodegraph::Comment::create(); + gcomment->setId(_commentId); + gcomment->setPos(_pos.x(), _pos.y()); + _scene->addComment(gcomment); +} + +void AddCommentCommand::undo() +{ + auto gcomment = _scene->getCommentById(_commentId); + if (gcomment) + _scene->removeComment(gcomment); + _project->comments.remove(_commentId); +} diff --git a/src/texturelab/undo/addcommentcommand.h b/src/texturelab/undo/addcommentcommand.h new file mode 100644 index 00000000..e5496460 --- /dev/null +++ b/src/texturelab/undo/addcommentcommand.h @@ -0,0 +1,27 @@ +#pragma once + +#include "../models.h" +#include +#include + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +class AddCommentCommand : public QUndoCommand { +public: + AddCommentCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QString& commentId, + QVector2D pos); + + void redo() override; + void undo() override; + +private: + TextureProjectPtr _project; + nodegraph::ScenePtr _scene; + QString _commentId; + QVector2D _pos; +}; diff --git a/src/texturelab/undo/addconnectioncommand.cpp b/src/texturelab/undo/addconnectioncommand.cpp new file mode 100644 index 00000000..a9c30c90 --- /dev/null +++ b/src/texturelab/undo/addconnectioncommand.cpp @@ -0,0 +1,59 @@ +#include "addconnectioncommand.h" + +#include "../graphics/texturerenderer.h" +#include "graph/scene.h" + +AddConnectionCommand::AddConnectionCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& leftNodeId, + const QString& leftOutput, + const QString& rightNodeId, + const QString& rightInput) + : QUndoCommand("Connect Nodes") + , _project(project) + , _scene(scene) + , _renderer(renderer) + , _leftNodeId(leftNodeId) + , _leftOutput(leftOutput) + , _rightNodeId(rightNodeId) + , _rightInput(rightInput) +{} + +void AddConnectionCommand::redo() +{ + if (_firstRedo) { + // Scene already has the connection — just add to project model + auto left = _project->getNodeById(_leftNodeId); + auto right = _project->getNodeById(_rightNodeId); + if (left && right) + _project->addConnection(left, right, _rightInput); + _firstRedo = false; + } else { + auto leftG = _scene->getNodeById(_leftNodeId); + auto rightG = _scene->getNodeById(_rightNodeId); + if (leftG && rightG) + _scene->connectNodes(leftG, _leftOutput, rightG, _rightInput); + + auto left = _project->getNodeById(_leftNodeId); + auto right = _project->getNodeById(_rightNodeId); + if (left && right) + _project->addConnection(left, right, _rightInput); + } + if (_renderer) + _renderer->update(); +} + +void AddConnectionCommand::undo() +{ + auto rightG = _scene->getNodeById(_rightNodeId); + if (rightG) { + auto port = rightG->getInPortByName(_rightInput); + if (port && !port->connections.isEmpty()) + _scene->removeConnection(port->connections.first()); + } + // removeConnection() marks the right node and its downstream chain dirty + _project->removeConnection(_leftNodeId, _rightNodeId, _rightInput); + if (_renderer) + _renderer->update(); +} diff --git a/src/texturelab/undo/addconnectioncommand.h b/src/texturelab/undo/addconnectioncommand.h new file mode 100644 index 00000000..1535f40a --- /dev/null +++ b/src/texturelab/undo/addconnectioncommand.h @@ -0,0 +1,34 @@ +#pragma once + +#include "../models.h" +#include + +class TextureRenderer; + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +// Records a connection drawn in the scene. First redo is a no-op for the scene +// (already created); subsequent redos recreate it. +class AddConnectionCommand : public QUndoCommand { +public: + AddConnectionCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& leftNodeId, + const QString& leftOutput, + const QString& rightNodeId, + const QString& rightInput); + + void redo() override; + void undo() override; + +private: + TextureProjectPtr _project; + nodegraph::ScenePtr _scene; + TextureRenderer* _renderer; + QString _leftNodeId, _leftOutput, _rightNodeId, _rightInput; + bool _firstRedo = true; +}; diff --git a/src/texturelab/undo/addframecommand.cpp b/src/texturelab/undo/addframecommand.cpp new file mode 100644 index 00000000..9be7972b --- /dev/null +++ b/src/texturelab/undo/addframecommand.cpp @@ -0,0 +1,36 @@ +#include "addframecommand.h" + +#include "graph/frame.h" +#include "graph/scene.h" + +AddFrameCommand::AddFrameCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QString& frameId, + QVector2D pos) + : QUndoCommand("Add Frame") + , _project(project) + , _scene(scene) + , _frameId(frameId) + , _pos(pos) +{} + +void AddFrameCommand::redo() +{ + auto modelFrame = FramePtr(new Frame()); + modelFrame->id = _frameId; + modelFrame->pos = _pos; + _project->frames[_frameId] = modelFrame; + + auto gframe = nodegraph::Frame::create(); + gframe->setId(_frameId); + gframe->setPos(_pos.x(), _pos.y()); + _scene->addFrame(gframe); +} + +void AddFrameCommand::undo() +{ + auto gframe = _scene->getFrameById(_frameId); + if (gframe) + _scene->removeFrame(gframe); + _project->frames.remove(_frameId); +} diff --git a/src/texturelab/undo/addframecommand.h b/src/texturelab/undo/addframecommand.h new file mode 100644 index 00000000..46b0b17f --- /dev/null +++ b/src/texturelab/undo/addframecommand.h @@ -0,0 +1,27 @@ +#pragma once + +#include "../models.h" +#include +#include + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +class AddFrameCommand : public QUndoCommand { +public: + AddFrameCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QString& frameId, + QVector2D pos); + + void redo() override; + void undo() override; + +private: + TextureProjectPtr _project; + nodegraph::ScenePtr _scene; + QString _frameId; + QVector2D _pos; +}; diff --git a/src/texturelab/undo/addnodecommand.cpp b/src/texturelab/undo/addnodecommand.cpp new file mode 100644 index 00000000..3e952515 --- /dev/null +++ b/src/texturelab/undo/addnodecommand.cpp @@ -0,0 +1,70 @@ +#include "addnodecommand.h" + +#include "../graphics/texturerenderer.h" +#include "../libraries/library.h" +#include "../telemetry.h" +#include "graph/scene.h" + +#include + +static QString newId() +{ + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +static void addNodeToScene(nodegraph::ScenePtr scene, const TextureNodePtr& node) +{ + auto gnode = nodegraph::Node::create(); + gnode->setId(node->id); + gnode->setName(node->title); + for (auto& input : node->inputs) + gnode->addInPort(input); + gnode->addOutPort("output"); + gnode->setCenter(node->pos.x(), node->pos.y()); + scene->addNode(gnode); +} + +AddNodeCommand::AddNodeCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& typeName, + QVector2D pos) + : QUndoCommand(QString("Add %1 Node").arg(typeName)) + , _project(project) + , _scene(scene) + , _renderer(renderer) + , _typeName(typeName) + , _pos(pos) + , _nodeId(newId()) +{} + +void AddNodeCommand::redo() +{ + auto node = _project->library->createNode(_typeName); + node->id = _nodeId; + node->pos = _pos; + _project->addNode(node); + addNodeToScene(_scene, node); + Telemetry::breadcrumb("graph.node", "node added", + {{"node_count", (int64_t)_project->nodes.size()}}); + Telemetry::setTag("project.node_count", + std::to_string(_project->nodes.size())); + if (_renderer) + _renderer->update(); +} + +void AddNodeCommand::undo() +{ + auto sceneNode = _scene->getNodeById(_nodeId); + if (sceneNode) + _scene->removeNode(sceneNode); + + // also drops the node's connections, marking every downstream chain dirty + _project->removeNode(_nodeId); + Telemetry::breadcrumb("graph.node", "node add undone", + {{"node_count", (int64_t)_project->nodes.size()}}); + Telemetry::setTag("project.node_count", + std::to_string(_project->nodes.size())); + if (_renderer) + _renderer->update(); +} diff --git a/src/texturelab/undo/addnodecommand.h b/src/texturelab/undo/addnodecommand.h new file mode 100644 index 00000000..2240b683 --- /dev/null +++ b/src/texturelab/undo/addnodecommand.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../models.h" +#include +#include + +class TextureRenderer; + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +class AddNodeCommand : public QUndoCommand { +public: + AddNodeCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& typeName, + QVector2D pos); + + void redo() override; + void undo() override; + +private: + TextureProjectPtr _project; + nodegraph::ScenePtr _scene; + TextureRenderer* _renderer; + QString _typeName; + QVector2D _pos; + QString _nodeId; // generated in constructor, reused on re-redo +}; diff --git a/src/texturelab/undo/deleteitemscommand.cpp b/src/texturelab/undo/deleteitemscommand.cpp new file mode 100644 index 00000000..8468b86d --- /dev/null +++ b/src/texturelab/undo/deleteitemscommand.cpp @@ -0,0 +1,233 @@ +#include "deleteitemscommand.h" + +#include "../graphics/texturerenderer.h" +#include "../libraries/library.h" +#include "../props.h" +#include "../telemetry.h" +#include "graph/comment.h" +#include "graph/frame.h" +#include "graph/scene.h" + +static void addNodeToScene(nodegraph::ScenePtr scene, const TextureNodePtr& node) +{ + auto gnode = nodegraph::Node::create(); + gnode->setId(node->id); + gnode->setName(node->title); + for (auto& input : node->inputs) + gnode->addInPort(input); + gnode->addOutPort("output"); + gnode->setCenter(node->pos.x(), node->pos.y()); + scene->addNode(gnode); +} + +DeleteItemsCommand::DeleteItemsCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QList& nodeIds, + const QList& frameIds, + const QList& commentIds) + : QUndoCommand() + , _project(project) + , _scene(scene) + , _renderer(renderer) +{ + int total = nodeIds.size() + frameIds.size() + commentIds.size(); + setText(QString("Delete %1 Item%2").arg(total).arg(total == 1 ? "" : "s")); + + QSet deletedNodeIds(nodeIds.begin(), nodeIds.end()); + + // Capture any texture-channel assignments pointing at a deleted node so we + // can remove them in redo() and restore them in undo(). Left behind, a + // stale id here is later looked up by passTextureChannelsToViewer3D(). + for (auto it = _project->textureChannels.constBegin(); + it != _project->textureChannels.constEnd(); ++it) { + if (deletedNodeIds.contains(it.value())) + _channelAssignments.insert(it.key(), it.value()); + } + + for (const auto& id : nodeIds) { + auto node = _project->getNodeById(id); + if (!node) + continue; + SerializedNode sn; + sn.typeName = node->typeName; + sn.id = node->id; + sn.exportName = node->exportName; + sn.randomSeed = node->randomSeed; + sn.pos = node->pos; + auto gnode = _scene->getNodeById(id); + if (gnode) + sn.pos = QVector2D(gnode->getCenter()); + for (auto key : node->props.keys()) + sn.props[key] = node->props[key]->toJsonValue(); + _nodes.append(sn); + } + + for (const auto& con : _project->connections) { + if (deletedNodeIds.contains(con->leftNode->id) || + deletedNodeIds.contains(con->rightNode->id)) { + SerializedConnection sc; + sc.id = con->id; + sc.leftNodeId = con->leftNode->id; + sc.leftOutput = con->leftNodeOutputName.isEmpty() ? "output" : con->leftNodeOutputName; + sc.rightNodeId = con->rightNode->id; + sc.rightInput = con->rightNodeInputName; + _connections.append(sc); + } + } + + for (const auto& id : frameIds) { + auto frame = _project->frames.value(id); + if (!frame) + continue; + SerializedFrame sf; + sf.id = frame->id; + sf.title = frame->text; + sf.color = frame->color; + sf.pos = frame->pos; + sf.size = frame->size; + auto gframe = _scene->getFrameById(id); + if (gframe) { + sf.pos = QVector2D(gframe->pos()); + auto sz = gframe->frameRect().size(); + sf.size = QVector2D(sz.width(), sz.height()); + } + _frames.append(sf); + } + + for (const auto& id : commentIds) { + auto comment = _project->comments.value(id); + if (!comment) + continue; + SerializedComment sc; + sc.id = comment->id; + sc.text = comment->text; + sc.pos = comment->pos; + auto gcomment = _scene->getCommentById(id); + if (gcomment) + sc.pos = QVector2D(gcomment->pos()); + _comments.append(sc); + } +} + +void DeleteItemsCommand::redo() +{ + // removeConnection() marks each right node and its downstream chain dirty + for (const auto& sc : _connections) + _project->removeConnection(sc.leftNodeId, sc.rightNodeId, sc.rightInput); + + for (const auto& sn : _nodes) { + auto gnode = _scene->getNodeById(sn.id); + if (gnode) + _scene->removeNode(gnode); + _project->nodes.remove(sn.id); + } + + for (const auto& sf : _frames) { + auto gframe = _scene->getFrameById(sf.id); + if (gframe) + _scene->removeFrame(gframe); + _project->frames.remove(sf.id); + } + + for (const auto& sc : _comments) { + auto gcomment = _scene->getCommentById(sc.id); + if (gcomment) + _scene->removeComment(gcomment); + _project->comments.remove(sc.id); + } + + // Drop channel assignments that referenced the now-deleted nodes. + for (auto it = _channelAssignments.constBegin(); + it != _channelAssignments.constEnd(); ++it) { + _project->textureChannels.remove(it.key()); + } + + Telemetry::breadcrumb("graph.node", "items deleted", + {{"deleted_nodes", (int64_t)_nodes.size()}, + {"node_count", (int64_t)_project->nodes.size()}}); + Telemetry::setTag("project.node_count", + std::to_string(_project->nodes.size())); + + if (_renderer) + _renderer->update(); +} + +void DeleteItemsCommand::undo() +{ + for (const auto& sn : _nodes) { + auto node = _project->library->createNode(sn.typeName); + node->id = sn.id; + node->exportName = sn.exportName; + node->randomSeed = sn.randomSeed; + node->pos = sn.pos; + node->isDirty = true; + for (auto key : sn.props.keys()) { + auto prop = node->getProp(key); + if (prop) + prop->fromJsonValue(sn.props[key]); + } + _project->addNode(node); + addNodeToScene(_scene, node); + } + + for (const auto& sc : _connections) { + auto leftNode = _project->getNodeById(sc.leftNodeId); + auto rightNode = _project->getNodeById(sc.rightNodeId); + if (!leftNode || !rightNode) + continue; + _project->addConnection(leftNode, rightNode, sc.rightInput); + auto leftG = _scene->getNodeById(sc.leftNodeId); + auto rightG = _scene->getNodeById(sc.rightNodeId); + if (leftG && rightG) + _scene->connectNodes(leftG, sc.leftOutput, rightG, sc.rightInput); + } + + for (const auto& sf : _frames) { + auto modelFrame = FramePtr(new Frame()); + modelFrame->id = sf.id; + modelFrame->text = sf.title; + modelFrame->color = sf.color; + modelFrame->pos = sf.pos; + modelFrame->size = sf.size; + _project->frames[sf.id] = modelFrame; + + auto gframe = nodegraph::Frame::create(); + gframe->setId(sf.id); + gframe->setTitle(sf.title); + gframe->setColor(sf.color); + gframe->setPos(sf.pos.x(), sf.pos.y()); + if (sf.size.x() > 0 && sf.size.y() > 0) + gframe->setSize(sf.size.x(), sf.size.y()); + _scene->addFrame(gframe); + } + + for (const auto& sc : _comments) { + auto modelComment = CommentPtr(new Comment()); + modelComment->id = sc.id; + modelComment->text = sc.text; + modelComment->pos = sc.pos; + _project->comments[sc.id] = modelComment; + + auto gcomment = nodegraph::Comment::create(); + gcomment->setId(sc.id); + gcomment->setText(sc.text); + gcomment->setPos(sc.pos.x(), sc.pos.y()); + _scene->addComment(gcomment); + } + + // Restore channel assignments now that the nodes they reference exist again. + for (auto it = _channelAssignments.constBegin(); + it != _channelAssignments.constEnd(); ++it) { + _project->textureChannels.insert(it.key(), it.value()); + } + + Telemetry::breadcrumb("graph.node", "delete undone", + {{"restored_nodes", (int64_t)_nodes.size()}, + {"node_count", (int64_t)_project->nodes.size()}}); + Telemetry::setTag("project.node_count", + std::to_string(_project->nodes.size())); + + if (_renderer) + _renderer->update(); +} diff --git a/src/texturelab/undo/deleteitemscommand.h b/src/texturelab/undo/deleteitemscommand.h new file mode 100644 index 00000000..5f8eae77 --- /dev/null +++ b/src/texturelab/undo/deleteitemscommand.h @@ -0,0 +1,63 @@ +#pragma once + +#include "../models.h" +#include +#include +#include +#include + +class TextureRenderer; + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +class DeleteItemsCommand : public QUndoCommand { +public: + struct SerializedNode { + QString typeName, id, exportName; + long randomSeed; + QVector2D pos; + QJsonObject props; + }; + + struct SerializedConnection { + QString id, leftNodeId, leftOutput, rightNodeId, rightInput; + }; + + struct SerializedFrame { + QString id, title; + QColor color; + QVector2D pos, size; + }; + + struct SerializedComment { + QString id, text; + QVector2D pos; + }; + + DeleteItemsCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QList& nodeIds, + const QList& frameIds, + const QList& commentIds); + + void redo() override; + void undo() override; + +private: + TextureProjectPtr _project; + nodegraph::ScenePtr _scene; + TextureRenderer* _renderer; + + QList _nodes; + QList _connections; + QList _frames; + QList _comments; + + // Texture-channel (Albedo/Normal/…) assignments that pointed at a deleted + // node; captured so redo() can drop them and undo() can restore them. + QMap _channelAssignments; +}; diff --git a/src/texturelab/undo/editcommentcommand.cpp b/src/texturelab/undo/editcommentcommand.cpp new file mode 100644 index 00000000..b1c426d3 --- /dev/null +++ b/src/texturelab/undo/editcommentcommand.cpp @@ -0,0 +1,44 @@ +#include "editcommentcommand.h" + +#include "graph/comment.h" +#include "graph/scene.h" + +EditCommentCommand::EditCommentCommand(CommentPtr comment, + nodegraph::ScenePtr scene, + const QString& oldText, + const QString& newText) + : QUndoCommand("Edit Comment") + , _comment(comment) + , _scene(scene) + , _commentId(comment->id) + , _oldText(oldText) + , _newText(newText) +{} + +bool EditCommentCommand::mergeWith(const QUndoCommand* other) +{ + auto* cmd = static_cast(other); + if (cmd->_commentId != _commentId) + return false; + _newText = cmd->_newText; + return true; +} + +void EditCommentCommand::apply(const QString& text) +{ + _comment->text = text; + auto gcomment = _scene->getCommentById(_commentId); + if (gcomment) + gcomment->setText(text); +} + +void EditCommentCommand::redo() +{ + if (_firstRedo) { _firstRedo = false; return; } + apply(_newText); +} + +void EditCommentCommand::undo() +{ + apply(_oldText); +} diff --git a/src/texturelab/undo/editcommentcommand.h b/src/texturelab/undo/editcommentcommand.h new file mode 100644 index 00000000..1f499aaf --- /dev/null +++ b/src/texturelab/undo/editcommentcommand.h @@ -0,0 +1,33 @@ +#pragma once + +#include "../models.h" +#include "undocommandids.h" +#include + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +class EditCommentCommand : public QUndoCommand { +public: + EditCommentCommand(CommentPtr comment, + nodegraph::ScenePtr scene, + const QString& oldText, + const QString& newText); + + int id() const override { return UndoCommandId::EditComment; } + bool mergeWith(const QUndoCommand* other) override; + + void redo() override; + void undo() override; + +private: + void apply(const QString& text); + + CommentPtr _comment; + nodegraph::ScenePtr _scene; + QString _commentId; + QString _oldText, _newText; + bool _firstRedo = true; +}; diff --git a/src/texturelab/undo/editframecommand.cpp b/src/texturelab/undo/editframecommand.cpp new file mode 100644 index 00000000..fc9a65fe --- /dev/null +++ b/src/texturelab/undo/editframecommand.cpp @@ -0,0 +1,48 @@ +#include "editframecommand.h" + +#include "graph/frame.h" +#include "graph/scene.h" + +EditFrameCommand::EditFrameCommand(FramePtr frame, + nodegraph::ScenePtr scene, + const QString& oldTitle, const QColor& oldColor, + const QString& newTitle, const QColor& newColor) + : QUndoCommand("Edit Frame") + , _frame(frame) + , _scene(scene) + , _frameId(frame->id) + , _oldTitle(oldTitle), _newTitle(newTitle) + , _oldColor(oldColor), _newColor(newColor) +{} + +bool EditFrameCommand::mergeWith(const QUndoCommand* other) +{ + auto* cmd = static_cast(other); + if (cmd->_frameId != _frameId) + return false; + _newTitle = cmd->_newTitle; + _newColor = cmd->_newColor; + return true; +} + +void EditFrameCommand::apply(const QString& title, const QColor& color) +{ + _frame->text = title; + _frame->color = color; + auto gframe = _scene->getFrameById(_frameId); + if (gframe) { + gframe->setTitle(title); + gframe->setColor(color); + } +} + +void EditFrameCommand::redo() +{ + if (_firstRedo) { _firstRedo = false; return; } + apply(_newTitle, _newColor); +} + +void EditFrameCommand::undo() +{ + apply(_oldTitle, _oldColor); +} diff --git a/src/texturelab/undo/editframecommand.h b/src/texturelab/undo/editframecommand.h new file mode 100644 index 00000000..49d7e3d4 --- /dev/null +++ b/src/texturelab/undo/editframecommand.h @@ -0,0 +1,35 @@ +#pragma once + +#include "../models.h" +#include "undocommandids.h" +#include +#include + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +class EditFrameCommand : public QUndoCommand { +public: + EditFrameCommand(FramePtr frame, + nodegraph::ScenePtr scene, + const QString& oldTitle, const QColor& oldColor, + const QString& newTitle, const QColor& newColor); + + int id() const override { return UndoCommandId::EditFrame; } + bool mergeWith(const QUndoCommand* other) override; + + void redo() override; + void undo() override; + +private: + void apply(const QString& title, const QColor& color); + + FramePtr _frame; + nodegraph::ScenePtr _scene; + QString _frameId; + QString _oldTitle, _newTitle; + QColor _oldColor, _newColor; + bool _firstRedo = true; +}; diff --git a/src/texturelab/undo/moveitemscommand.cpp b/src/texturelab/undo/moveitemscommand.cpp new file mode 100644 index 00000000..5fd2ff2f --- /dev/null +++ b/src/texturelab/undo/moveitemscommand.cpp @@ -0,0 +1,45 @@ +#include "moveitemscommand.h" + +#include "graph/scene.h" + +MoveItemsCommand::MoveItemsCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QMap& oldPositions, + const QMap& newPositions) + : QUndoCommand("Move Nodes") + , _project(project) + , _scene(scene) + , _oldPositions(oldPositions) + , _newPositions(newPositions) +{} + +bool MoveItemsCommand::mergeWith(const QUndoCommand* other) +{ + auto* cmd = static_cast(other); + if (cmd->_newPositions.keys() != _newPositions.keys()) + return false; + _newPositions = cmd->_newPositions; + return true; +} + +void MoveItemsCommand::applyPositions(const QMap& positions) +{ + for (auto it = positions.begin(); it != positions.end(); ++it) { + auto gnode = _scene->getNodeById(it.key()); + if (gnode) + gnode->setCenter(it.value().x(), it.value().y()); + auto node = _project->getNodeById(it.key()); + if (node) + node->pos = QVector2D(it.value()); + } +} + +void MoveItemsCommand::redo() +{ + applyPositions(_newPositions); +} + +void MoveItemsCommand::undo() +{ + applyPositions(_oldPositions); +} diff --git a/src/texturelab/undo/moveitemscommand.h b/src/texturelab/undo/moveitemscommand.h new file mode 100644 index 00000000..34943efa --- /dev/null +++ b/src/texturelab/undo/moveitemscommand.h @@ -0,0 +1,34 @@ +#pragma once + +#include "../models.h" +#include "undocommandids.h" +#include +#include +#include + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +class MoveItemsCommand : public QUndoCommand { +public: + MoveItemsCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QMap& oldPositions, + const QMap& newPositions); + + int id() const override { return UndoCommandId::MoveItems; } + bool mergeWith(const QUndoCommand* other) override; + + void redo() override; + void undo() override; + +private: + void applyPositions(const QMap& positions); + + TextureProjectPtr _project; + nodegraph::ScenePtr _scene; + QMap _oldPositions; + QMap _newPositions; +}; diff --git a/src/texturelab/undo/pastecommand.cpp b/src/texturelab/undo/pastecommand.cpp new file mode 100644 index 00000000..21e5628d --- /dev/null +++ b/src/texturelab/undo/pastecommand.cpp @@ -0,0 +1,134 @@ +#include "pastecommand.h" + +#include "../clipboard.h" +#include "../graphics/texturerenderer.h" +#include "../telemetry.h" +#include "graph/comment.h" +#include "graph/frame.h" +#include "graph/scene.h" + +PasteCommand::PasteCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + QPointF viewCenter) + : QUndoCommand() + , _project(project) + , _scene(scene) + , _renderer(renderer) +{ + if (!Clipboard::pasteItems(project, viewCenter, + _nodes, _connections, _comments, _frames)) + return; + + int total = _nodes.size() + _frames.size() + _comments.size(); + setText(QString("Paste %1 Item%2").arg(total).arg(total == 1 ? "" : "s")); +} + +bool PasteCommand::isEmpty() const +{ + return _nodes.isEmpty() && _frames.isEmpty() && _comments.isEmpty(); +} + +void PasteCommand::addNodeToScene(const TextureNodePtr& node) +{ + auto gnode = nodegraph::Node::create(); + gnode->setId(node->id); + gnode->setName(node->title); + for (auto& input : node->inputs) + gnode->addInPort(input); + gnode->addOutPort("output"); + gnode->setCenter(node->pos.x(), node->pos.y()); + _scene->addNode(gnode); +} + +void PasteCommand::redo() +{ + _scene->clearSelection(); + + for (auto& node : _nodes) { + _project->nodes[node->id] = node; + addNodeToScene(node); + auto gnode = _scene->getNodeById(node->id); + if (gnode) + gnode->setSelected(true); + } + + for (auto& con : _connections) { + // inserted directly (rather than via addConnection) to keep the + // pasted connection's id stable across undo/redo + _project->connections[con->id] = con; + _project->markNodeAsDirty(con->rightNode); + auto leftG = _scene->getNodeById(con->leftNode->id); + auto rightG = _scene->getNodeById(con->rightNode->id); + if (leftG && rightG) + _scene->connectNodes(leftG, "output", rightG, con->rightNodeInputName); + } + + for (auto& comment : _comments) { + _project->comments[comment->id] = comment; + auto gc = nodegraph::Comment::create(); + gc->setId(comment->id); + gc->setText(comment->text); + gc->setPos(comment->pos.x(), comment->pos.y()); + _scene->addComment(gc); + gc->setSelected(true); + } + + for (auto& frame : _frames) { + _project->frames[frame->id] = frame; + auto gf = nodegraph::Frame::create(); + gf->setId(frame->id); + gf->setTitle(frame->text); + gf->setColor(frame->color); + gf->setPos(frame->pos.x(), frame->pos.y()); + if (frame->size.x() > 0 && frame->size.y() > 0) + gf->setSize(frame->size.x(), frame->size.y()); + _scene->addFrame(gf); + gf->setSelected(true); + } + + Telemetry::breadcrumb("graph.node", "nodes pasted", + {{"pasted_nodes", (int64_t)_nodes.size()}, + {"node_count", (int64_t)_project->nodes.size()}}); + Telemetry::setTag("project.node_count", + std::to_string(_project->nodes.size())); + + if (_renderer) + _renderer->update(); +} + +void PasteCommand::undo() +{ + // removeConnection() marks the downstream chain dirty + for (auto& con : _connections) + _project->removeConnection(con->id); + + for (auto& node : _nodes) { + auto gnode = _scene->getNodeById(node->id); + if (gnode) + _scene->removeNode(gnode); + _project->removeNode(node->id); + } + + for (auto& comment : _comments) { + auto gc = _scene->getCommentById(comment->id); + if (gc) + _scene->removeComment(gc); + _project->comments.remove(comment->id); + } + + for (auto& frame : _frames) { + auto gf = _scene->getFrameById(frame->id); + if (gf) + _scene->removeFrame(gf); + _project->frames.remove(frame->id); + } + + Telemetry::breadcrumb("graph.node", "paste undone", + {{"node_count", (int64_t)_project->nodes.size()}}); + Telemetry::setTag("project.node_count", + std::to_string(_project->nodes.size())); + + if (_renderer) + _renderer->update(); +} diff --git a/src/texturelab/undo/pastecommand.h b/src/texturelab/undo/pastecommand.h new file mode 100644 index 00000000..a466e523 --- /dev/null +++ b/src/texturelab/undo/pastecommand.h @@ -0,0 +1,37 @@ +#pragma once + +#include "../models.h" +#include +#include + +class TextureRenderer; + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +class PasteCommand : public QUndoCommand { +public: + PasteCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + QPointF viewCenter); + + void redo() override; + void undo() override; + + bool isEmpty() const; + +private: + void addNodeToScene(const TextureNodePtr& node); + + TextureProjectPtr _project; + nodegraph::ScenePtr _scene; + TextureRenderer* _renderer; + + QList _nodes; + QList _connections; + QList _comments; + QList _frames; +}; diff --git a/src/texturelab/undo/propertychangecommand.cpp b/src/texturelab/undo/propertychangecommand.cpp new file mode 100644 index 00000000..cb7be4e3 --- /dev/null +++ b/src/texturelab/undo/propertychangecommand.cpp @@ -0,0 +1,49 @@ +#include "propertychangecommand.h" + +#include "../graphics/texturerenderer.h" + +PropertyChangeCommand::PropertyChangeCommand(TextureNodePtr node, + TextureProjectPtr project, + TextureRenderer* renderer, + const QString& propName, + QVariant oldValue, + QVariant newValue) + : QUndoCommand(QString("Change %1").arg(propName)) + , _node(node) + , _project(project) + , _renderer(renderer) + , _propName(propName) + , _oldValue(oldValue) + , _newValue(newValue) +{} + +bool PropertyChangeCommand::mergeWith(const QUndoCommand* other) +{ + auto* cmd = static_cast(other); + if (cmd->_node != _node || cmd->_propName != _propName) + return false; + _newValue = cmd->_newValue; + return true; +} + +void PropertyChangeCommand::applyValue(const QVariant& value) +{ + _node->setProp(_propName, value); + _project->markNodeAsDirty(_node); + if (_renderer) + _renderer->update(); +} + +void PropertyChangeCommand::redo() +{ + if (_firstRedo) { + _firstRedo = false; + return; // already applied by the signal handler + } + applyValue(_newValue); +} + +void PropertyChangeCommand::undo() +{ + applyValue(_oldValue); +} diff --git a/src/texturelab/undo/propertychangecommand.h b/src/texturelab/undo/propertychangecommand.h new file mode 100644 index 00000000..efc9f6fb --- /dev/null +++ b/src/texturelab/undo/propertychangecommand.h @@ -0,0 +1,38 @@ +#pragma once + +#include "../models.h" +#include "undocommandids.h" +#include +#include + +class TextureRenderer; + +// A single node property value change. Consecutive changes to the same prop +// merge into one undo step. First redo is skipped (already applied by the +// signal handler). +class PropertyChangeCommand : public QUndoCommand { +public: + PropertyChangeCommand(TextureNodePtr node, + TextureProjectPtr project, + TextureRenderer* renderer, + const QString& propName, + QVariant oldValue, + QVariant newValue); + + int id() const override { return UndoCommandId::PropertyChange; } + bool mergeWith(const QUndoCommand* other) override; + + void redo() override; + void undo() override; + +private: + void applyValue(const QVariant& value); + + TextureNodePtr _node; + TextureProjectPtr _project; + TextureRenderer* _renderer; + QString _propName; + QVariant _oldValue; + QVariant _newValue; + bool _firstRedo = true; +}; diff --git a/src/texturelab/undo/randomseedchangecommand.cpp b/src/texturelab/undo/randomseedchangecommand.cpp new file mode 100644 index 00000000..a9cb8601 --- /dev/null +++ b/src/texturelab/undo/randomseedchangecommand.cpp @@ -0,0 +1,36 @@ +#include "randomseedchangecommand.h" + +#include "../graphics/texturerenderer.h" + +RandomSeedChangeCommand::RandomSeedChangeCommand(TextureNodePtr node, + TextureProjectPtr project, + TextureRenderer* renderer, + long oldSeed, + long newSeed) + : QUndoCommand("Change Random Seed") + , _node(node) + , _project(project) + , _renderer(renderer) + , _oldSeed(oldSeed) + , _newSeed(newSeed) +{} + +void RandomSeedChangeCommand::redo() +{ + if (_firstRedo) { + _firstRedo = false; + return; + } + _node->randomSeed = _newSeed; + _project->markNodeAsDirty(_node); + if (_renderer) + _renderer->update(); +} + +void RandomSeedChangeCommand::undo() +{ + _node->randomSeed = _oldSeed; + _project->markNodeAsDirty(_node); + if (_renderer) + _renderer->update(); +} diff --git a/src/texturelab/undo/randomseedchangecommand.h b/src/texturelab/undo/randomseedchangecommand.h new file mode 100644 index 00000000..3eafd46e --- /dev/null +++ b/src/texturelab/undo/randomseedchangecommand.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../models.h" +#include + +class TextureRenderer; + +class RandomSeedChangeCommand : public QUndoCommand { +public: + RandomSeedChangeCommand(TextureNodePtr node, + TextureProjectPtr project, + TextureRenderer* renderer, + long oldSeed, + long newSeed); + + void redo() override; + void undo() override; + +private: + TextureNodePtr _node; + TextureProjectPtr _project; + TextureRenderer* _renderer; + long _oldSeed, _newSeed; + bool _firstRedo = true; +}; diff --git a/src/texturelab/undo/removeconnectioncommand.cpp b/src/texturelab/undo/removeconnectioncommand.cpp new file mode 100644 index 00000000..521fa5d4 --- /dev/null +++ b/src/texturelab/undo/removeconnectioncommand.cpp @@ -0,0 +1,56 @@ +#include "removeconnectioncommand.h" + +#include "../graphics/texturerenderer.h" +#include "graph/scene.h" + +RemoveConnectionCommand::RemoveConnectionCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& leftNodeId, + const QString& leftOutput, + const QString& rightNodeId, + const QString& rightInput) + : QUndoCommand("Remove Connection") + , _project(project) + , _scene(scene) + , _renderer(renderer) + , _leftNodeId(leftNodeId) + , _leftOutput(leftOutput) + , _rightNodeId(rightNodeId) + , _rightInput(rightInput) +{} + +void RemoveConnectionCommand::redo() +{ + if (_firstRedo) { + // Scene already removed it — just remove from project model + // (removeConnection() marks the downstream chain dirty) + _project->removeConnection(_leftNodeId, _rightNodeId, _rightInput); + _firstRedo = false; + } else { + auto rightG = _scene->getNodeById(_rightNodeId); + if (rightG) { + auto port = rightG->getInPortByName(_rightInput); + if (port && !port->connections.isEmpty()) + _scene->removeConnection(port->connections.first()); + } + _project->removeConnection(_leftNodeId, _rightNodeId, _rightInput); + } + if (_renderer) + _renderer->update(); +} + +void RemoveConnectionCommand::undo() +{ + auto leftG = _scene->getNodeById(_leftNodeId); + auto rightG = _scene->getNodeById(_rightNodeId); + if (leftG && rightG) + _scene->connectNodes(leftG, _leftOutput, rightG, _rightInput); + + auto left = _project->getNodeById(_leftNodeId); + auto right = _project->getNodeById(_rightNodeId); + if (left && right) + _project->addConnection(left, right, _rightInput); + if (_renderer) + _renderer->update(); +} diff --git a/src/texturelab/undo/removeconnectioncommand.h b/src/texturelab/undo/removeconnectioncommand.h new file mode 100644 index 00000000..9fc1eb7a --- /dev/null +++ b/src/texturelab/undo/removeconnectioncommand.h @@ -0,0 +1,34 @@ +#pragma once + +#include "../models.h" +#include + +class TextureRenderer; + +namespace nodegraph { +class Scene; +typedef QSharedPointer ScenePtr; +} // namespace nodegraph + +// Records a connection removed in the scene. First redo only removes from the +// project model (scene already done). +class RemoveConnectionCommand : public QUndoCommand { +public: + RemoveConnectionCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& leftNodeId, + const QString& leftOutput, + const QString& rightNodeId, + const QString& rightInput); + + void redo() override; + void undo() override; + +private: + TextureProjectPtr _project; + nodegraph::ScenePtr _scene; + TextureRenderer* _renderer; + QString _leftNodeId, _leftOutput, _rightNodeId, _rightInput; + bool _firstRedo = true; +}; diff --git a/src/texturelab/undo/texturechannelassigncommand.cpp b/src/texturelab/undo/texturechannelassigncommand.cpp new file mode 100644 index 00000000..8fec1a4d --- /dev/null +++ b/src/texturelab/undo/texturechannelassigncommand.cpp @@ -0,0 +1,36 @@ +#include "texturechannelassigncommand.h" + +TextureChannelAssignCommand::TextureChannelAssignCommand( + TextureProjectPtr project, + TextureChannel channel, + const QString& oldNodeId, + const QString& newNodeId, + std::function syncViewer) + : QUndoCommand("Assign Texture Channel") + , _project(project) + , _channel(channel) + , _oldNodeId(oldNodeId) + , _newNodeId(newNodeId) + , _syncViewer(syncViewer) +{} + +void TextureChannelAssignCommand::apply(const QString& nodeId) +{ + if (nodeId.isEmpty()) + _project->textureChannels.remove(_channel); + else + _project->textureChannels[_channel] = nodeId; + if (_syncViewer) + _syncViewer(); +} + +void TextureChannelAssignCommand::redo() +{ + if (_firstRedo) { _firstRedo = false; return; } + apply(_newNodeId); +} + +void TextureChannelAssignCommand::undo() +{ + apply(_oldNodeId); +} diff --git a/src/texturelab/undo/texturechannelassigncommand.h b/src/texturelab/undo/texturechannelassigncommand.h new file mode 100644 index 00000000..c91776a0 --- /dev/null +++ b/src/texturelab/undo/texturechannelassigncommand.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../models.h" +#include +#include + +class TextureChannelAssignCommand : public QUndoCommand { +public: + TextureChannelAssignCommand(TextureProjectPtr project, + TextureChannel channel, + const QString& oldNodeId, + const QString& newNodeId, + std::function syncViewer); + + void redo() override; + void undo() override; + +private: + void apply(const QString& nodeId); + + TextureProjectPtr _project; + TextureChannel _channel; + QString _oldNodeId, _newNodeId; + std::function _syncViewer; + bool _firstRedo = true; +}; diff --git a/src/texturelab/undo/undocommandids.h b/src/texturelab/undo/undocommandids.h new file mode 100644 index 00000000..0d80bce0 --- /dev/null +++ b/src/texturelab/undo/undocommandids.h @@ -0,0 +1,8 @@ +#pragma once + +namespace UndoCommandId { +static constexpr int PropertyChange = 1; +static constexpr int MoveItems = 2; +static constexpr int EditFrame = 3; +static constexpr int EditComment = 4; +} // namespace UndoCommandId diff --git a/src/texturelab/undo/undocommands.cpp b/src/texturelab/undo/undocommands.cpp new file mode 100644 index 00000000..07d9a5fb --- /dev/null +++ b/src/texturelab/undo/undocommands.cpp @@ -0,0 +1,871 @@ +#include "undocommands.h" + +#include "../clipboard.h" +#include "../graphics/texturerenderer.h" +#include "../libraries/library.h" +#include "../props.h" +#include "graph/comment.h" +#include "graph/frame.h" +#include "graph/scene.h" + +#include + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static QString newId() +{ + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} + +// Add a TextureNode to the nodegraph scene (mirrors GraphWidget::addNode) +static void addNodeToScene(nodegraph::ScenePtr scene, const TextureNodePtr& node) +{ + auto gnode = nodegraph::Node::create(); + gnode->setId(node->id); + gnode->setName(node->title); + for (auto& input : node->inputs) + gnode->addInPort(input); + gnode->addOutPort("output"); + gnode->setCenter(node->pos.x(), node->pos.y()); + scene->addNode(gnode); +} + +// --------------------------------------------------------------------------- +// AddNodeCommand +// --------------------------------------------------------------------------- + +AddNodeCommand::AddNodeCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& typeName, + QVector2D pos) + : QUndoCommand(QString("Add %1 Node").arg(typeName)) + , _project(project) + , _scene(scene) + , _renderer(renderer) + , _typeName(typeName) + , _pos(pos) + , _nodeId(newId()) +{} + +void AddNodeCommand::redo() +{ + auto node = _project->library->createNode(_typeName); + node->id = _nodeId; + node->pos = _pos; + _project->addNode(node); + addNodeToScene(_scene, node); + if (_renderer) + _renderer->update(); +} + +void AddNodeCommand::undo() +{ + auto sceneNode = _scene->getNodeById(_nodeId); + if (sceneNode) + _scene->removeNode(sceneNode); + + // Remove all connections involving this node from the project + for (auto key : _project->connections.keys()) { + auto con = _project->connections.value(key); + if (con->leftNode->id == _nodeId || con->rightNode->id == _nodeId) { + if (con->leftNode->id == _nodeId) + con->rightNode->isDirty = true; + _project->connections.remove(key); + } + } + + _project->nodes.remove(_nodeId); + if (_renderer) + _renderer->update(); +} + +// --------------------------------------------------------------------------- +// DeleteItemsCommand +// --------------------------------------------------------------------------- + +DeleteItemsCommand::DeleteItemsCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QList& nodeIds, + const QList& frameIds, + const QList& commentIds) + : QUndoCommand() + , _project(project) + , _scene(scene) + , _renderer(renderer) +{ + // Determine display text + int total = nodeIds.size() + frameIds.size() + commentIds.size(); + setText(QString("Delete %1 Item%2").arg(total).arg(total == 1 ? "" : "s")); + + QSet deletedNodeIds(nodeIds.begin(), nodeIds.end()); + + // Serialize nodes + for (const auto& id : nodeIds) { + auto node = _project->getNodeById(id); + if (!node) + continue; + SerializedNode sn; + sn.typeName = node->typeName; + sn.id = node->id; + sn.exportName = node->exportName; + sn.randomSeed = node->randomSeed; + sn.pos = node->pos; + // Sync visual position before serializing + auto gnode = _scene->getNodeById(id); + if (gnode) + sn.pos = QVector2D(gnode->getCenter()); + for (auto key : node->props.keys()) + sn.props[key] = node->props[key]->toJsonValue(); + _nodes.append(sn); + } + + // Serialize all connections touching any deleted node + for (const auto& con : _project->connections) { + if (deletedNodeIds.contains(con->leftNode->id) || + deletedNodeIds.contains(con->rightNode->id)) { + SerializedConnection sc; + sc.id = con->id; + sc.leftNodeId = con->leftNode->id; + sc.leftOutput = con->leftNodeOutputName.isEmpty() ? "output" : con->leftNodeOutputName; + sc.rightNodeId = con->rightNode->id; + sc.rightInput = con->rightNodeInputName; + _connections.append(sc); + } + } + + // Serialize frames + for (const auto& id : frameIds) { + auto frame = _project->frames.value(id); + if (!frame) + continue; + SerializedFrame sf; + sf.id = frame->id; + sf.title = frame->text; + sf.color = frame->color; + sf.pos = frame->pos; + sf.size = frame->size; + // Sync visual position/size + auto gframe = _scene->getFrameById(id); + if (gframe) { + sf.pos = QVector2D(gframe->pos()); + auto sz = gframe->frameRect().size(); + sf.size = QVector2D(sz.width(), sz.height()); + } + _frames.append(sf); + } + + // Serialize comments + for (const auto& id : commentIds) { + auto comment = _project->comments.value(id); + if (!comment) + continue; + SerializedComment sc; + sc.id = comment->id; + sc.text = comment->text; + sc.pos = comment->pos; + auto gcomment = _scene->getCommentById(id); + if (gcomment) + sc.pos = QVector2D(gcomment->pos()); + _comments.append(sc); + } +} + +void DeleteItemsCommand::redo() +{ + // Remove project connections first (mark downstream nodes dirty) + for (const auto& sc : _connections) { + auto con = _project->removeConnection(sc.leftNodeId, sc.rightNodeId, sc.rightInput); + if (con && con->rightNode) + con->rightNode->isDirty = true; + } + + // Remove nodes (scene::removeNode also removes scene-level connections) + for (const auto& sn : _nodes) { + auto gnode = _scene->getNodeById(sn.id); + if (gnode) + _scene->removeNode(gnode); + _project->nodes.remove(sn.id); + } + + for (const auto& sf : _frames) { + auto gframe = _scene->getFrameById(sf.id); + if (gframe) + _scene->removeFrame(gframe); + _project->frames.remove(sf.id); + } + + for (const auto& sc : _comments) { + auto gcomment = _scene->getCommentById(sc.id); + if (gcomment) + _scene->removeComment(gcomment); + _project->comments.remove(sc.id); + } + + if (_renderer) + _renderer->update(); +} + +void DeleteItemsCommand::undo() +{ + // Recreate nodes in project + scene + for (const auto& sn : _nodes) { + auto node = _project->library->createNode(sn.typeName); + node->id = sn.id; + node->exportName = sn.exportName; + node->randomSeed = sn.randomSeed; + node->pos = sn.pos; + node->isDirty = true; + + for (auto key : sn.props.keys()) { + auto prop = node->getProp(key); + if (prop) + prop->fromJsonValue(sn.props[key]); + } + + _project->addNode(node); + addNodeToScene(_scene, node); + } + + // Recreate connections in project + scene + for (const auto& sc : _connections) { + auto leftNode = _project->getNodeById(sc.leftNodeId); + auto rightNode = _project->getNodeById(sc.rightNodeId); + if (!leftNode || !rightNode) + continue; + + _project->addConnection(leftNode, rightNode, sc.rightInput); + rightNode->isDirty = true; + + auto leftGNode = _scene->getNodeById(sc.leftNodeId); + auto rightGNode = _scene->getNodeById(sc.rightNodeId); + if (leftGNode && rightGNode) + _scene->connectNodes(leftGNode, sc.leftOutput, rightGNode, sc.rightInput); + } + + // Recreate frames + for (const auto& sf : _frames) { + auto modelFrame = FramePtr(new Frame()); + modelFrame->id = sf.id; + modelFrame->text = sf.title; + modelFrame->color = sf.color; + modelFrame->pos = sf.pos; + modelFrame->size = sf.size; + _project->frames[sf.id] = modelFrame; + + auto gframe = nodegraph::Frame::create(); + gframe->setId(sf.id); + gframe->setTitle(sf.title); + gframe->setColor(sf.color); + gframe->setPos(sf.pos.x(), sf.pos.y()); + if (sf.size.x() > 0 && sf.size.y() > 0) + gframe->setSize(sf.size.x(), sf.size.y()); + _scene->addFrame(gframe); + } + + // Recreate comments + for (const auto& sc : _comments) { + auto modelComment = CommentPtr(new Comment()); + modelComment->id = sc.id; + modelComment->text = sc.text; + modelComment->pos = sc.pos; + _project->comments[sc.id] = modelComment; + + auto gcomment = nodegraph::Comment::create(); + gcomment->setId(sc.id); + gcomment->setText(sc.text); + gcomment->setPos(sc.pos.x(), sc.pos.y()); + _scene->addComment(gcomment); + } + + if (_renderer) + _renderer->update(); +} + +// --------------------------------------------------------------------------- +// AddConnectionCommand +// --------------------------------------------------------------------------- + +AddConnectionCommand::AddConnectionCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& leftNodeId, + const QString& leftOutput, + const QString& rightNodeId, + const QString& rightInput) + : QUndoCommand(QString("Connect Nodes")) + , _project(project) + , _scene(scene) + , _renderer(renderer) + , _leftNodeId(leftNodeId) + , _leftOutput(leftOutput) + , _rightNodeId(rightNodeId) + , _rightInput(rightInput) +{} + +void AddConnectionCommand::redo() +{ + if (_firstRedo) { + // Scene already has the connection — just add to project model + auto left = _project->getNodeById(_leftNodeId); + auto right = _project->getNodeById(_rightNodeId); + if (left && right) { + _project->addConnection(left, right, _rightInput); + right->isDirty = true; + } + _firstRedo = false; + } else { + // Recreate in scene + project + auto leftG = _scene->getNodeById(_leftNodeId); + auto rightG = _scene->getNodeById(_rightNodeId); + if (leftG && rightG) + _scene->connectNodes(leftG, _leftOutput, rightG, _rightInput); + + auto left = _project->getNodeById(_leftNodeId); + auto right = _project->getNodeById(_rightNodeId); + if (left && right) { + _project->addConnection(left, right, _rightInput); + right->isDirty = true; + } + } + if (_renderer) + _renderer->update(); +} + +void AddConnectionCommand::undo() +{ + // Remove from scene + auto rightG = _scene->getNodeById(_rightNodeId); + if (rightG) { + auto port = rightG->getInPortByName(_rightInput); + if (port && !port->connections.isEmpty()) + _scene->removeConnection(port->connections.first()); + } + // Remove from project + auto right = _project->getNodeById(_rightNodeId); + if (right) + right->isDirty = true; + _project->removeConnection(_leftNodeId, _rightNodeId, _rightInput); + if (_renderer) + _renderer->update(); +} + +// --------------------------------------------------------------------------- +// RemoveConnectionCommand +// --------------------------------------------------------------------------- + +RemoveConnectionCommand::RemoveConnectionCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + const QString& leftNodeId, + const QString& leftOutput, + const QString& rightNodeId, + const QString& rightInput) + : QUndoCommand("Remove Connection") + , _project(project) + , _scene(scene) + , _renderer(renderer) + , _leftNodeId(leftNodeId) + , _leftOutput(leftOutput) + , _rightNodeId(rightNodeId) + , _rightInput(rightInput) +{} + +void RemoveConnectionCommand::redo() +{ + if (_firstRedo) { + // Scene already removed it — just remove from project model + auto con = _project->removeConnection(_leftNodeId, _rightNodeId, _rightInput); + if (con && con->rightNode) + con->rightNode->isDirty = true; + _firstRedo = false; + } else { + // Remove from scene + auto rightG = _scene->getNodeById(_rightNodeId); + if (rightG) { + auto port = rightG->getInPortByName(_rightInput); + if (port && !port->connections.isEmpty()) + _scene->removeConnection(port->connections.first()); + } + // Remove from project + auto con = _project->removeConnection(_leftNodeId, _rightNodeId, _rightInput); + if (con && con->rightNode) + con->rightNode->isDirty = true; + } + if (_renderer) + _renderer->update(); +} + +void RemoveConnectionCommand::undo() +{ + // Restore in scene + auto leftG = _scene->getNodeById(_leftNodeId); + auto rightG = _scene->getNodeById(_rightNodeId); + if (leftG && rightG) + _scene->connectNodes(leftG, _leftOutput, rightG, _rightInput); + + // Restore in project + auto left = _project->getNodeById(_leftNodeId); + auto right = _project->getNodeById(_rightNodeId); + if (left && right) { + _project->addConnection(left, right, _rightInput); + right->isDirty = true; + } + if (_renderer) + _renderer->update(); +} + +// --------------------------------------------------------------------------- +// MoveItemsCommand +// --------------------------------------------------------------------------- + +MoveItemsCommand::MoveItemsCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QMap& oldPositions, + const QMap& newPositions) + : QUndoCommand("Move Nodes") + , _project(project) + , _scene(scene) + , _oldPositions(oldPositions) + , _newPositions(newPositions) +{} + +bool MoveItemsCommand::mergeWith(const QUndoCommand* other) +{ + auto* cmd = static_cast(other); + if (cmd->_newPositions.keys() != _newPositions.keys()) + return false; + _newPositions = cmd->_newPositions; + return true; +} + +void MoveItemsCommand::applyPositions(const QMap& positions) +{ + for (auto it = positions.begin(); it != positions.end(); ++it) { + auto gnode = _scene->getNodeById(it.key()); + if (gnode) + gnode->setCenter(it.value().x(), it.value().y()); + auto node = _project->getNodeById(it.key()); + if (node) + node->pos = QVector2D(it.value()); + } +} + +void MoveItemsCommand::redo() +{ + applyPositions(_newPositions); +} + +void MoveItemsCommand::undo() +{ + applyPositions(_oldPositions); +} + +// --------------------------------------------------------------------------- +// PropertyChangeCommand +// --------------------------------------------------------------------------- + +PropertyChangeCommand::PropertyChangeCommand(TextureNodePtr node, + TextureProjectPtr project, + TextureRenderer* renderer, + const QString& propName, + QVariant oldValue, + QVariant newValue) + : QUndoCommand(QString("Change %1").arg(propName)) + , _node(node) + , _project(project) + , _renderer(renderer) + , _propName(propName) + , _oldValue(oldValue) + , _newValue(newValue) +{} + +bool PropertyChangeCommand::mergeWith(const QUndoCommand* other) +{ + auto* cmd = static_cast(other); + if (cmd->_node != _node || cmd->_propName != _propName) + return false; + _newValue = cmd->_newValue; + return true; +} + +void PropertyChangeCommand::applyValue(const QVariant& value) +{ + _node->setProp(_propName, value); + _project->markNodeAsDirty(_node); + if (_renderer) + _renderer->update(); +} + +void PropertyChangeCommand::redo() +{ + if (_firstRedo) { + _firstRedo = false; + return; // already applied by the signal handler + } + applyValue(_newValue); +} + +void PropertyChangeCommand::undo() +{ + applyValue(_oldValue); +} + +// --------------------------------------------------------------------------- +// RandomSeedChangeCommand +// --------------------------------------------------------------------------- + +RandomSeedChangeCommand::RandomSeedChangeCommand(TextureNodePtr node, + TextureProjectPtr project, + TextureRenderer* renderer, + long oldSeed, + long newSeed) + : QUndoCommand("Change Random Seed") + , _node(node) + , _project(project) + , _renderer(renderer) + , _oldSeed(oldSeed) + , _newSeed(newSeed) +{} + +void RandomSeedChangeCommand::redo() +{ + if (_firstRedo) { + _firstRedo = false; + return; + } + _node->randomSeed = _newSeed; + _project->markNodeAsDirty(_node); + if (_renderer) + _renderer->update(); +} + +void RandomSeedChangeCommand::undo() +{ + _node->randomSeed = _oldSeed; + _project->markNodeAsDirty(_node); + if (_renderer) + _renderer->update(); +} + +// --------------------------------------------------------------------------- +// AddFrameCommand +// --------------------------------------------------------------------------- + +AddFrameCommand::AddFrameCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QString& frameId, + QVector2D pos) + : QUndoCommand("Add Frame") + , _project(project) + , _scene(scene) + , _frameId(frameId) + , _pos(pos) +{} + +void AddFrameCommand::redo() +{ + auto modelFrame = FramePtr(new Frame()); + modelFrame->id = _frameId; + modelFrame->pos = _pos; + _project->frames[_frameId] = modelFrame; + + auto gframe = nodegraph::Frame::create(); + gframe->setId(_frameId); + gframe->setPos(_pos.x(), _pos.y()); + _scene->addFrame(gframe); +} + +void AddFrameCommand::undo() +{ + auto gframe = _scene->getFrameById(_frameId); + if (gframe) + _scene->removeFrame(gframe); + _project->frames.remove(_frameId); +} + +// --------------------------------------------------------------------------- +// AddCommentCommand +// --------------------------------------------------------------------------- + +AddCommentCommand::AddCommentCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + const QString& commentId, + QVector2D pos) + : QUndoCommand("Add Comment") + , _project(project) + , _scene(scene) + , _commentId(commentId) + , _pos(pos) +{} + +void AddCommentCommand::redo() +{ + auto modelComment = CommentPtr(new Comment()); + modelComment->id = _commentId; + modelComment->pos = _pos; + _project->comments[_commentId] = modelComment; + + auto gcomment = nodegraph::Comment::create(); + gcomment->setId(_commentId); + gcomment->setPos(_pos.x(), _pos.y()); + _scene->addComment(gcomment); +} + +void AddCommentCommand::undo() +{ + auto gcomment = _scene->getCommentById(_commentId); + if (gcomment) + _scene->removeComment(gcomment); + _project->comments.remove(_commentId); +} + +// --------------------------------------------------------------------------- +// EditFrameCommand +// --------------------------------------------------------------------------- + +EditFrameCommand::EditFrameCommand(FramePtr frame, + nodegraph::ScenePtr scene, + const QString& oldTitle, const QColor& oldColor, + const QString& newTitle, const QColor& newColor) + : QUndoCommand("Edit Frame") + , _frame(frame) + , _scene(scene) + , _frameId(frame->id) + , _oldTitle(oldTitle), _newTitle(newTitle) + , _oldColor(oldColor), _newColor(newColor) +{} + +bool EditFrameCommand::mergeWith(const QUndoCommand* other) +{ + auto* cmd = static_cast(other); + if (cmd->_frameId != _frameId) + return false; + _newTitle = cmd->_newTitle; + _newColor = cmd->_newColor; + return true; +} + +void EditFrameCommand::apply(const QString& title, const QColor& color) +{ + _frame->text = title; + _frame->color = color; + auto gframe = _scene->getFrameById(_frameId); + if (gframe) { + gframe->setTitle(title); + gframe->setColor(color); + } +} + +void EditFrameCommand::redo() +{ + if (_firstRedo) { _firstRedo = false; return; } + apply(_newTitle, _newColor); +} + +void EditFrameCommand::undo() +{ + apply(_oldTitle, _oldColor); +} + +// --------------------------------------------------------------------------- +// EditCommentCommand +// --------------------------------------------------------------------------- + +EditCommentCommand::EditCommentCommand(CommentPtr comment, + nodegraph::ScenePtr scene, + const QString& oldText, + const QString& newText) + : QUndoCommand("Edit Comment") + , _comment(comment) + , _scene(scene) + , _commentId(comment->id) + , _oldText(oldText) + , _newText(newText) +{} + +bool EditCommentCommand::mergeWith(const QUndoCommand* other) +{ + auto* cmd = static_cast(other); + if (cmd->_commentId != _commentId) + return false; + _newText = cmd->_newText; + return true; +} + +void EditCommentCommand::apply(const QString& text) +{ + _comment->text = text; + auto gcomment = _scene->getCommentById(_commentId); + if (gcomment) + gcomment->setText(text); +} + +void EditCommentCommand::redo() +{ + if (_firstRedo) { _firstRedo = false; return; } + apply(_newText); +} + +void EditCommentCommand::undo() +{ + apply(_oldText); +} + +// --------------------------------------------------------------------------- +// PasteCommand +// --------------------------------------------------------------------------- + +PasteCommand::PasteCommand(TextureProjectPtr project, + nodegraph::ScenePtr scene, + TextureRenderer* renderer, + QPointF viewCenter) + : QUndoCommand() + , _project(project) + , _scene(scene) + , _renderer(renderer) +{ + if (!Clipboard::pasteItems(project, viewCenter, + _nodes, _connections, _comments, _frames)) + return; // nothing to paste + + int total = _nodes.size() + _frames.size() + _comments.size(); + setText(QString("Paste %1 Item%2").arg(total).arg(total == 1 ? "" : "s")); +} + +bool PasteCommand::isEmpty() const +{ + return _nodes.isEmpty() && _frames.isEmpty() && _comments.isEmpty(); +} + +void PasteCommand::addNodeToScene(const TextureNodePtr& node) +{ + auto gnode = nodegraph::Node::create(); + gnode->setId(node->id); + gnode->setName(node->title); + for (auto& input : node->inputs) + gnode->addInPort(input); + gnode->addOutPort("output"); + gnode->setCenter(node->pos.x(), node->pos.y()); + _scene->addNode(gnode); +} + +void PasteCommand::redo() +{ + _scene->clearSelection(); + + for (auto& node : _nodes) { + _project->nodes[node->id] = node; + addNodeToScene(node); + auto gnode = _scene->getNodeById(node->id); + if (gnode) + gnode->setSelected(true); + } + + for (auto& con : _connections) { + _project->connections[con->id] = con; + con->rightNode->isDirty = true; + auto leftG = _scene->getNodeById(con->leftNode->id); + auto rightG = _scene->getNodeById(con->rightNode->id); + if (leftG && rightG) + _scene->connectNodes(leftG, "output", rightG, con->rightNodeInputName); + } + + for (auto& comment : _comments) { + _project->comments[comment->id] = comment; + auto gc = nodegraph::Comment::create(); + gc->setId(comment->id); + gc->setText(comment->text); + gc->setPos(comment->pos.x(), comment->pos.y()); + _scene->addComment(gc); + gc->setSelected(true); + } + + for (auto& frame : _frames) { + _project->frames[frame->id] = frame; + auto gf = nodegraph::Frame::create(); + gf->setId(frame->id); + gf->setTitle(frame->text); + gf->setColor(frame->color); + gf->setPos(frame->pos.x(), frame->pos.y()); + if (frame->size.x() > 0 && frame->size.y() > 0) + gf->setSize(frame->size.x(), frame->size.y()); + _scene->addFrame(gf); + gf->setSelected(true); + } + + if (_renderer) + _renderer->update(); +} + +void PasteCommand::undo() +{ + for (auto& con : _connections) + _project->connections.remove(con->id); + + for (auto& node : _nodes) { + auto gnode = _scene->getNodeById(node->id); + if (gnode) + _scene->removeNode(gnode); + _project->nodes.remove(node->id); + } + + for (auto& comment : _comments) { + auto gc = _scene->getCommentById(comment->id); + if (gc) + _scene->removeComment(gc); + _project->comments.remove(comment->id); + } + + for (auto& frame : _frames) { + auto gf = _scene->getFrameById(frame->id); + if (gf) + _scene->removeFrame(gf); + _project->frames.remove(frame->id); + } + + if (_renderer) + _renderer->update(); +} + +// --------------------------------------------------------------------------- +// TextureChannelAssignCommand +// --------------------------------------------------------------------------- + +TextureChannelAssignCommand::TextureChannelAssignCommand( + TextureProjectPtr project, + TextureChannel channel, + const QString& oldNodeId, + const QString& newNodeId, + std::function syncViewer) + : QUndoCommand("Assign Texture Channel") + , _project(project) + , _channel(channel) + , _oldNodeId(oldNodeId) + , _newNodeId(newNodeId) + , _syncViewer(syncViewer) +{} + +void TextureChannelAssignCommand::apply(const QString& nodeId) +{ + if (nodeId.isEmpty()) + _project->textureChannels.remove(_channel); + else + _project->textureChannels[_channel] = nodeId; + if (_syncViewer) + _syncViewer(); +} + +void TextureChannelAssignCommand::redo() +{ + if (_firstRedo) { _firstRedo = false; return; } + apply(_newNodeId); +} + +void TextureChannelAssignCommand::undo() +{ + apply(_oldNodeId); +} diff --git a/src/texturelab/undo/undocommands.h b/src/texturelab/undo/undocommands.h new file mode 100644 index 00000000..e2a950bc --- /dev/null +++ b/src/texturelab/undo/undocommands.h @@ -0,0 +1,16 @@ +#pragma once + +#include "addcommentcommand.h" +#include "addconnectioncommand.h" +#include "addframecommand.h" +#include "addnodecommand.h" +#include "deleteitemscommand.h" +#include "editcommentcommand.h" +#include "editframecommand.h" +#include "moveitemscommand.h" +#include "pastecommand.h" +#include "propertychangecommand.h" +#include "randomseedchangecommand.h" +#include "removeconnectioncommand.h" +#include "texturechannelassigncommand.h" +#include "undocommandids.h" diff --git a/src/texturelab/update/updatechecker.cpp b/src/texturelab/update/updatechecker.cpp new file mode 100644 index 00000000..cb2e4ab6 --- /dev/null +++ b/src/texturelab/update/updatechecker.cpp @@ -0,0 +1,222 @@ +#include "updatechecker.h" + +#include "telemetry.h" +#include "versioncompare.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// The update endpoint. Hardcoded on purpose — no build flag, no environment +// override — so what the app asks is whatever this one line says. +// +// !!! Currently pointed at the local dev server. Set this back to +// !!! "https://texturelab.io" before cutting a release: a shipped build pointed +// !!! at localhost never reaches anything and silently reports no updates. +// constexpr const char* kApiBase = "http://localhost:3333"; +constexpr const char* kApiBase = "https://texturelab.io"; + +constexpr const char* kEnabledKey = "updateCheck"; + +// The last release the server told us about. Remembered so the launcher can say +// so on every open, not just during the minutes after a check completes. +constexpr const char* kKnownVersionKey = "updateKnownVersion"; +constexpr const char* kKnownTitleKey = "updateKnownTitle"; +constexpr const char* kKnownUrlKey = "updateKnownUrl"; + +QSettings appSettings() +{ + return QSettings(QSettings::UserScope, "texturelab", "texturelab"); +} + +} // namespace + +UpdateChecker::UpdateChecker(QObject* parent) : QObject(parent) {} + +UpdateChecker::~UpdateChecker() = default; + +QString UpdateChecker::apiBase() +{ + // Trailing slashes are trimmed rather than assumed absent: the request path + // is appended directly, and "…3333/" + "/api/…" is a double-slashed path + // that some routers answer with a 404. + QString base = QString::fromLatin1(kApiBase).trimmed(); + while (base.endsWith(QLatin1Char('/'))) + base.chop(1); + + return base; +} + +bool UpdateChecker::isEnabled() +{ + return appSettings().value(QLatin1String(kEnabledKey), true).toBool(); +} + +void UpdateChecker::setEnabled(bool enabled) +{ + QSettings settings = appSettings(); + settings.setValue(QLatin1String(kEnabledKey), enabled); +} + +QString UpdateChecker::channel() +{ + const QString stored = + appSettings().value(QStringLiteral("updateChannel")).toString(); + if (stored == QLatin1String("stable") || stored == QLatin1String("beta")) + return stored; + + // A pre-release tag on our own version means this is a beta build, and its + // user is better served by beta releases than by being told nothing exists. + const QString self = + appversion::normalize(QCoreApplication::applicationVersion()); + return self.contains(QLatin1Char('-')) ? QStringLiteral("beta") + : QStringLiteral("stable"); +} + +QString UpdateChecker::knownUpdateVersion() +{ + const QString version = + appSettings().value(QLatin1String(kKnownVersionKey)).toString(); + if (version.isEmpty()) + return QString(); + + // A remembered update the running build has caught up with is stale — the + // user updated, and nothing should still be nagging them. + if (!appversion::isNewer(version, QCoreApplication::applicationVersion())) + return QString(); + + return version; +} + +void UpdateChecker::rememberUpdate(const QString& version, const QString& title, + const QString& url) +{ + QSettings settings = appSettings(); + settings.setValue(QLatin1String(kKnownVersionKey), version); + settings.setValue(QLatin1String(kKnownTitleKey), title); + settings.setValue(QLatin1String(kKnownUrlKey), url); +} + +void UpdateChecker::forgetUpdate() +{ + QSettings settings = appSettings(); + settings.remove(QLatin1String(kKnownVersionKey)); + settings.remove(QLatin1String(kKnownTitleKey)); + settings.remove(QLatin1String(kKnownUrlKey)); +} + +void UpdateChecker::check(bool force) +{ + if (!isEnabled() || inFlight) + return; + + QSettings settings = appSettings(); + + // Say what we already know before deciding whether to ask again. + const QString known = knownUpdateVersion(); + if (!known.isEmpty()) { + emit updateAvailable( + known, settings.value(QLatin1String(kKnownTitleKey)).toString(), + settings.value(QLatin1String(kKnownUrlKey)).toString()); + } + + // Once per run unless explicitly forced. + if (!force && checkedThisRun) + return; + + QUrl url(apiBase() + QStringLiteral("/api/releases/latest")); + QUrlQuery query; + query.addQueryItem(QStringLiteral("channel"), channel()); + url.setQuery(query); + + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::UserAgentHeader, + QStringLiteral("TextureLab/%1") + .arg(QCoreApplication::applicationVersion())); + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + request.setTransferTimeout(8000); + + if (!network) + network = new QNetworkAccessManager(this); + + inFlight = true; + checkedThisRun = true; + + QNetworkReply* reply = network->get(request); + connect(reply, &QNetworkReply::finished, this, + [this, reply]() { handleReply(reply); }); +} + +void UpdateChecker::handleReply(QNetworkReply* reply) +{ + inFlight = false; + reply->deleteLater(); + + if (reply->error() != QNetworkReply::NoError) { + // Being offline is the normal case, not an incident: report it to the + // caller and leave no trace beyond a breadcrumb. + const QString error = reply->errorString(); + Telemetry::breadcrumb("update", "check failed: " + error.toStdString()); + emit checkFinished(false, error); + return; + } + + const QByteArray body = reply->readAll(); + + QJsonParseError parseError; + const QJsonDocument doc = QJsonDocument::fromJson(body, &parseError); + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { + emit checkFinished( + false, QStringLiteral("Malformed response from the update server")); + return; + } + + // 404 with {"error": ...} is a legitimate answer: the channel has no + // published release yet. + const QJsonObject root = doc.object(); + if (!root.value(QStringLiteral("data")).isObject()) { + const QString error = root.value(QStringLiteral("error")).toString(); + emit checkFinished(false, error); + return; + } + + const QJsonObject data = root.value(QStringLiteral("data")).toObject(); + const QString version = data.value(QStringLiteral("version")).toString(); + if (version.isEmpty()) { + emit checkFinished(false, + QStringLiteral("Update server returned no version")); + return; + } + + const QString current = QCoreApplication::applicationVersion(); + if (!appversion::isNewer(version, current)) { + // Up to date now — drop anything remembered from before, so a notice + // can't outlive the update it referred to. + forgetUpdate(); + emit checkFinished(false, QString()); + return; + } + + // Always the site's download page rather than a per-platform binary URL: + // it lists every build, so it stays correct when the server has no artifact + // for this platform yet, and the user lands somewhere that explains itself. + const QString downloadUrl = apiBase() + QStringLiteral("/download"); + + Telemetry::breadcrumb("update", "found " + version.toStdString()); + + rememberUpdate(version, data.value(QStringLiteral("title")).toString(), + downloadUrl); + + emit updateAvailable( + version, data.value(QStringLiteral("title")).toString(), downloadUrl); + emit checkFinished(true, QString()); +} diff --git a/src/texturelab/update/updatechecker.h b/src/texturelab/update/updatechecker.h new file mode 100644 index 00000000..80367727 --- /dev/null +++ b/src/texturelab/update/updatechecker.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +class QNetworkAccessManager; + +// Asks texturelab.io whether a newer build exists. +// +// Read-only and fire-and-forget: one GET to a public, unauthenticated endpoint, +// no payload, no identifiers beyond what any HTTP request carries. It never +// downloads or installs anything — finding an update just surfaces a link the +// user can choose to click. +// +// Opt-out lives in QSettings under "updateCheck", matching how crash reporting +// is handled. Off means no request is made at all, not a discarded response. +class UpdateChecker : public QObject { + Q_OBJECT + +public: + explicit UpdateChecker(QObject* parent = nullptr); + ~UpdateChecker() override; + + // Endpoint base, without a trailing slash. Hardcoded in updatechecker.cpp; + // point it at a dev server by editing that constant and rebuilding. + static QString apiBase(); + + static bool isEnabled(); + static void setEnabled(bool enabled); + + // "stable" or "beta". Defaults to beta when this build's own version + // carries a pre-release tag — someone running a beta wants beta news. + static QString channel(); + + // Surfaces any already-known update immediately, then asks the server — + // once per application run. Reopening the launcher from the Home button + // reuses the answer from the first check rather than asking again. + // + // `force` overrides that, for the menu-driven "check now". + // + // The two halves are independent on purpose: how often the server is asked + // is not how often the user is told. Without the first half, an update + // found at startup would stop being displayed the moment the launcher was + // closed and reopened. + void check(bool force = false); + + // The newest release seen by a previous check, remembered across restarts. + // Empty when there is none, or when this build has caught up with it. + static QString knownUpdateVersion(); + +signals: + // Emitted only when the published version is strictly newer than this one. + // `downloadUrl` is the site's /download page, which lists every build. + void updateAvailable(const QString& version, const QString& title, + const QString& downloadUrl); + + // Emitted on every completed check, including "you're up to date" and + // failures, so a manual check can report something either way. + void checkFinished(bool foundUpdate, const QString& error); + +private: + void handleReply(class QNetworkReply* reply); + static void rememberUpdate(const QString& version, const QString& title, const QString& url); + static void forgetUpdate(); + + QNetworkAccessManager* network = nullptr; + bool inFlight = false; + + // One automatic check per run. The endpoint sends Cache-Control: + // max-age=300 and a launch is not a frequent event, so there is nothing to + // gain from asking twice in a session. + bool checkedThisRun = false; +}; diff --git a/src/texturelab/update/versioncompare.cpp b/src/texturelab/update/versioncompare.cpp new file mode 100644 index 00000000..e7ec9887 --- /dev/null +++ b/src/texturelab/update/versioncompare.cpp @@ -0,0 +1,133 @@ +#include "versioncompare.h" + +#include + +namespace appversion { + +namespace { + +struct Parsed { + int major = 0; + int minor = 0; + int patch = 0; + QString preRelease; +}; + +int toInt(const QString& text) +{ + bool ok = false; + const int value = text.toInt(&ok); + return ok && value >= 0 ? value : 0; +} + +Parsed parse(const QString& version) +{ + Parsed out; + + QString core = normalize(version); + + const int dash = core.indexOf(QLatin1Char('-')); + if (dash >= 0) { + out.preRelease = core.mid(dash + 1); + core = core.left(dash); + } + + const QStringList parts = core.split(QLatin1Char('.')); + if (parts.size() > 0) + out.major = toInt(parts[0]); + if (parts.size() > 1) + out.minor = toInt(parts[1]); + if (parts.size() > 2) + out.patch = toInt(parts[2]); + + return out; +} + +// Semver identifier comparison: numeric identifiers compare numerically and +// always sort below alphanumeric ones; a shorter run of equal identifiers sorts +// lower. So beta < beta.2 < rc. +int comparePreRelease(const QString& a, const QString& b) +{ + if (a == b) + return 0; + + // Neither having a pre-release is handled by the caller; here, exactly one + // being empty means that one is the full release and therefore greater. + if (a.isEmpty()) + return 1; + if (b.isEmpty()) + return -1; + + const QStringList left = a.split(QLatin1Char('.')); + const QStringList right = b.split(QLatin1Char('.')); + + for (int i = 0; i < qMax(left.size(), right.size()); ++i) { + if (i >= left.size()) + return -1; + if (i >= right.size()) + return 1; + + const QString& l = left[i]; + const QString& r = right[i]; + + bool lNumeric = false; + bool rNumeric = false; + const int lValue = l.toInt(&lNumeric); + const int rValue = r.toInt(&rNumeric); + + if (lNumeric && rNumeric) { + if (lValue != rValue) + return lValue < rValue ? -1 : 1; + continue; + } + if (lNumeric != rNumeric) + return lNumeric ? -1 : 1; + + const int textual = QString::compare(l, r); + if (textual != 0) + return textual < 0 ? -1 : 1; + } + + return 0; +} + +} // namespace + +QString normalize(const QString& version) +{ + QString out = version.trimmed(); + + if (out.startsWith(QLatin1Char('v')) || out.startsWith(QLatin1Char('V'))) + out = out.mid(1); + + const int plus = out.indexOf(QLatin1Char('+')); + if (plus >= 0) + out = out.left(plus); + + return out; +} + +int compare(const QString& a, const QString& b) +{ + const Parsed left = parse(a); + const Parsed right = parse(b); + + if (left.major != right.major) + return left.major < right.major ? -1 : 1; + if (left.minor != right.minor) + return left.minor < right.minor ? -1 : 1; + if (left.patch != right.patch) + return left.patch < right.patch ? -1 : 1; + + if (left.preRelease.isEmpty() && right.preRelease.isEmpty()) + return 0; + + return comparePreRelease(left.preRelease, right.preRelease); +} + +bool isNewer(const QString& candidate, const QString& current) +{ + return compare(candidate, current) > 0; +} + +} // namespace appversion diff --git a/src/texturelab/update/versioncompare.h b/src/texturelab/update/versioncompare.h new file mode 100644 index 00000000..74fdae27 --- /dev/null +++ b/src/texturelab/update/versioncompare.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +// Semver comparison for "is the release on the site newer than this build?". +// +// Kept apart from UpdateChecker, and free of any dependency beyond QtCore, so +// the ordering rules can be tested without a network stack. Getting this wrong +// is quiet and bad in both directions: nagging users who are already current, +// or never telling anyone an update exists. +namespace appversion { + +// -1 if a < b, 0 if equal, 1 if a > b. +// +// Follows semver precedence, including the rule people get wrong: a release +// with a pre-release tag sorts *below* the same numbers without one, so +// 0.4.0-beta < 0.4.0. Build metadata after '+' is ignored entirely, which is +// what lets the app compare its own "0.4.0-beta+a1b2c3d" against the plain +// "0.4.0-beta" the API publishes. +// +// Missing numeric parts are zero, so "1.2" == "1.2.0". Non-numeric junk in a +// numeric field compares as 0 rather than throwing — a malformed version from +// the server should mean "no update", never a crash. +int compare(const QString& a, const QString& b); + +// True when `candidate` is strictly newer than `current`. +bool isNewer(const QString& candidate, const QString& current); + +// Strips build metadata and any leading 'v' — "v1.2.3+abc" becomes "1.2.3". +QString normalize(const QString& version); + +} // namespace appversion diff --git a/src/texturelab/utils.h b/src/texturelab/utils.h new file mode 100644 index 00000000..8bbc3aa6 --- /dev/null +++ b/src/texturelab/utils.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +enum class Roles : int { + ItemType = Qt::UserRole + 1, + IdentityData = Qt::UserRole + 2, + + LibraryItemName = Qt::UserRole + 10 +}; + +const QString LIBRARY_ITEM_MIME_FORMAT = "texturelab/library-item"; \ No newline at end of file diff --git a/src/texturelab/version.h.in b/src/texturelab/version.h.in new file mode 100644 index 00000000..242faa38 --- /dev/null +++ b/src/texturelab/version.h.in @@ -0,0 +1,2 @@ +#pragma once +#define TEXTURELAB_BUILD_HASH "@TEXTURELAB_BUILD_HASH@" diff --git a/src/texturelab/widgets/aboutdialog.cpp b/src/texturelab/widgets/aboutdialog.cpp new file mode 100644 index 00000000..989de8e2 --- /dev/null +++ b/src/texturelab/widgets/aboutdialog.cpp @@ -0,0 +1,108 @@ +#include "aboutdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent) +{ + setWindowTitle("About TextureLab"); + setFixedSize(440, 320); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setupUI(); +} + +void AboutDialog::setupUI() +{ + // Base widget/dialog/button styling comes from the global theme (app.qss.in); + // only the About-specific label typography is set here (see #About* rules). + + auto outerLayout = new QVBoxLayout(this); + outerLayout->setContentsMargins(0, 0, 0, 0); + outerLayout->setSpacing(0); + + // Header band + auto header = new QWidget(); + header->setFixedHeight(110); + + auto headerLayout = new QHBoxLayout(header); + headerLayout->setContentsMargins(28, 0, 28, 0); + headerLayout->setSpacing(20); + + auto logoLabel = new QLabel(); + QPixmap logo(":/icons/logo.png"); + if (!logo.isNull()) { + logoLabel->setPixmap( + logo.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + logoLabel->setFixedSize(64, 64); + headerLayout->addWidget(logoLabel); + + auto titleBlock = new QVBoxLayout(); + titleBlock->setSpacing(4); + + auto nameLabel = new QLabel("TextureLab"); + nameLabel->setObjectName("AboutTitle"); + titleBlock->addWidget(nameLabel); + + auto tagLabel = new QLabel("Procedural Texture Authoring"); + tagLabel->setObjectName("AboutTag"); + titleBlock->addWidget(tagLabel); + + headerLayout->addLayout(titleBlock); + headerLayout->addStretch(); + + outerLayout->addWidget(header); + + // Body + auto body = new QWidget(); + auto bodyLayout = new QVBoxLayout(body); + bodyLayout->setContentsMargins(28, 22, 28, 20); + bodyLayout->setSpacing(10); + + QString version = QCoreApplication::applicationVersion(); + auto versionLabel = new QLabel(QString("Version %1").arg(version)); + versionLabel->setObjectName("AboutVersion"); + bodyLayout->addWidget(versionLabel); + + auto separator = new QFrame(); + separator->setObjectName("AboutSeparator"); + separator->setFrameShape(QFrame::HLine); + bodyLayout->addWidget(separator); + + auto descLabel = new QLabel( + "A node-based texture creation tool for game artists and developers."); + descLabel->setWordWrap(true); + descLabel->setObjectName("AboutDesc"); + bodyLayout->addWidget(descLabel); + + auto linkLabel = new QLabel( + "" + "github.com/njbrown/texturelab"); + linkLabel->setOpenExternalLinks(true); + linkLabel->setObjectName("AboutLink"); + bodyLayout->addWidget(linkLabel); + + bodyLayout->addStretch(); + + auto footerLayout = new QHBoxLayout(); + auto copyrightLabel = new QLabel("© Nicolas Brown"); + copyrightLabel->setObjectName("AboutCopyright"); + footerLayout->addWidget(copyrightLabel); + footerLayout->addStretch(); + + auto closeBtn = new QPushButton("Close"); + closeBtn->setObjectName("closeBtn"); + closeBtn->setDefault(true); + connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept); + footerLayout->addWidget(closeBtn); + + bodyLayout->addLayout(footerLayout); + outerLayout->addWidget(body); +} diff --git a/src/texturelab/widgets/aboutdialog.h b/src/texturelab/widgets/aboutdialog.h new file mode 100644 index 00000000..161598c1 --- /dev/null +++ b/src/texturelab/widgets/aboutdialog.h @@ -0,0 +1,16 @@ +#ifndef ABOUTDIALOG_H +#define ABOUTDIALOG_H + +#include + +class AboutDialog : public QDialog { + Q_OBJECT + +public: + explicit AboutDialog(QWidget* parent = nullptr); + +private: + void setupUI(); +}; + +#endif // ABOUTDIALOG_H diff --git a/src/texturelab/widgets/crashconsentdialog.cpp b/src/texturelab/widgets/crashconsentdialog.cpp new file mode 100644 index 00000000..165be0f4 --- /dev/null +++ b/src/texturelab/widgets/crashconsentdialog.cpp @@ -0,0 +1,146 @@ +#include "crashconsentdialog.h" + +#include "telemetry.h" + +#include +#include +#include +#include +#include + +namespace { + +// Wide enough that the two fact lines wrap at most once, which is what keeps +// the block reading as a list rather than a paragraph. +constexpr int kDialogWidth = 460; +constexpr int kMargin = 28; +constexpr int kLeadColumn = 72; + +} // namespace + +CrashConsentDialog::CrashConsentDialog(QWidget* parent) : QDialog(parent) +{ + setWindowTitle(tr("Crash Reports")); + setObjectName(QStringLiteral("ConsentDialog")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setFixedWidth(kDialogWidth); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(kMargin, kMargin - 4, kMargin, kMargin - 6); + layout->setSpacing(14); + + auto* title = new QLabel(tr("Help fix crashes"), this); + title->setObjectName(QStringLiteral("ConsentTitle")); + layout->addWidget(title); + + auto* body = new QLabel( + tr("If TextureLab crashes, it can send a report so the bug can be found and fixed. " + "Nothing is sent unless it crashes."), + this); + body->setObjectName(QStringLiteral("ConsentBody")); + body->setWordWrap(true); + layout->addWidget(body); + + auto* rule = new QFrame(this); + rule->setObjectName(QStringLiteral("ConsentSeparator")); + rule->setFrameShape(QFrame::HLine); + layout->addWidget(rule); + + // The two halves of the answer to "what are you actually sending?", which + // is the question the prompt exists to answer. + auto* facts = new QVBoxLayout(); + facts->setSpacing(8); + facts->addWidget(buildFact( + tr("Sent"), + tr("Where in the code the crash happened, TextureLab's version, your " + "operating system, your graphics card and driver, and what you were " + "doing just before \u2014 for example \u201cchanged resolution to 4096\u201d."))); + facts->addWidget(buildFact( + tr("Not sent"), + tr("Your textures. No project files, file names or folder paths are " + "uploaded, and there is no account or sign-in involved."))); + layout->addLayout(facts); + + auto* provider = new QLabel( + tr("Reports are handled by Sentry, a crash-reporting service. You can change this " + "any time from the ⚙ menu."), + this); + provider->setObjectName(QStringLiteral("ConsentNote")); + provider->setWordWrap(true); + layout->addWidget(provider); + + layout->addSpacing(2); + + auto* buttons = new QHBoxLayout(); + buttons->setSpacing(8); + buttons->addStretch(1); + + auto* decline = new QPushButton(tr("Not now"), this); + decline->setCursor(Qt::PointingHandCursor); + connect(decline, &QPushButton::clicked, this, &QDialog::reject); + buttons->addWidget(decline); + + auto* accept = new QPushButton(tr("Send crash reports"), this); + accept->setProperty("variant", "primary"); + accept->setCursor(Qt::PointingHandCursor); + accept->setDefault(true); + connect(accept, &QPushButton::clicked, this, &QDialog::accept); + buttons->addWidget(accept); + + layout->addLayout(buttons); + + // Word-wrapped labels only know their height once they know their width, so + // the height has to be pinned after the fixed width is in effect. + layout->activate(); + setFixedHeight(sizeHint().height()); +} + +void CrashConsentDialog::showEvent(QShowEvent* event) +{ + QDialog::showEvent(event); + + // Centred by hand rather than left to QDialog. Its own placement runs off + // the parent's geometry at construction, and on a multi-monitor X11 desktop + // the launcher has not been placed by the window manager that early — the + // prompt then lands centred on where the launcher was going to be, which + // can be a different screen from where it ended up. + if (const QWidget* owner = parentWidget() ? parentWidget()->window() : nullptr) { + const QRect area = owner->frameGeometry(); + move(area.center() - QPoint(width() / 2, height() / 2)); + } +} + +QWidget* CrashConsentDialog::buildFact(const QString& lead, const QString& detail) +{ + auto* row = new QWidget(this); + + auto* layout = new QHBoxLayout(row); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(12); + + auto* leadLabel = new QLabel(lead, row); + leadLabel->setObjectName(QStringLiteral("ConsentFactLead")); + leadLabel->setFixedWidth(kLeadColumn); + // Top-aligned so a lead word stays level with the first line of a detail + // that wraps, rather than drifting to the middle of the block. + leadLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); + layout->addWidget(leadLabel); + + auto* detailLabel = new QLabel(detail, row); + detailLabel->setObjectName(QStringLiteral("ConsentFact")); + detailLabel->setWordWrap(true); + detailLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop); + layout->addWidget(detailLabel, 1); + + return row; +} + +bool CrashConsentDialog::ask(QWidget* parent) +{ + CrashConsentDialog dialog(parent); + const bool allowed = dialog.exec() == QDialog::Accepted; + + Telemetry::recordConsent(allowed); + Telemetry::setEnabled(allowed); + return allowed; +} diff --git a/src/texturelab/widgets/crashconsentdialog.h b/src/texturelab/widgets/crashconsentdialog.h new file mode 100644 index 00000000..4e720c4c --- /dev/null +++ b/src/texturelab/widgets/crashconsentdialog.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Asks permission to send crash reports, once per released version. +// +// Says what leaves the machine and what doesn't, because "help improve the app" +// on its own asks the user to agree to something they can't see. All styling +// comes from the theme (see #Consent* in app.qss.in) — no inline colors here. +class CrashConsentDialog : public QDialog { + Q_OBJECT + +public: + explicit CrashConsentDialog(QWidget* parent = nullptr); + + // Puts the prompt up and records the answer, turning collection on or off + // to match. Returns what the user chose. + // + // Dismissing the dialog counts as declining but still counts as asked: a + // prompt that reappears every launch until it gets the answer it wants is + // not a question. + static bool ask(QWidget* parent); + +protected: + void showEvent(QShowEvent* event) override; + +private: + QWidget* buildFact(const QString& lead, const QString& detail); +}; diff --git a/src/texturelab/widgets/exportdialog.cpp b/src/texturelab/widgets/exportdialog.cpp new file mode 100644 index 00000000..a1c279e6 --- /dev/null +++ b/src/texturelab/widgets/exportdialog.cpp @@ -0,0 +1,160 @@ +#include "exportdialog.h" +#include "../models.h" +#include "../project.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ExportDialog::ExportDialog(QWidget* parent) : QDialog(parent) +{ + setWindowTitle("Export Settings"); + setModal(true); + setMinimumWidth(500); + + setupUI(); +} + +ExportDialog::~ExportDialog() {} + +void ExportDialog::setProject(TextureProjectPtr project) +{ + this->project = project; + if (project) { + if (patternEdit) { + patternEdit->setText(project->exportFilePattern); + } + exportDestination = project->exportDestination; + updateDestinationDisplay(); + } +} + +QString ExportDialog::getExportPattern() const +{ + return project ? project->exportFilePattern : "${project}_${name}"; +} + +void ExportDialog::setupUI() +{ + auto mainLayout = new QVBoxLayout(this); + mainLayout->setSpacing(15); + + // Destination section + auto destLabel = new QLabel("Destination:"); + mainLayout->addWidget(destLabel); + + auto destLayout = new QHBoxLayout(); + destinationLabel = new QLabel("No destination selected"); + destinationLabel->setObjectName("ExportDestination"); // styled in app.qss.in + destinationLabel->setWordWrap(true); + destLayout->addWidget(destinationLabel, 1); + + chooseDestinationBtn = new QPushButton("Choose Folder"); + connect(chooseDestinationBtn, &QPushButton::clicked, this, + &ExportDialog::onChooseDestination); + destLayout->addWidget(chooseDestinationBtn); + + mainLayout->addLayout(destLayout); + + // Pattern section + auto patternLabel = new QLabel("Pattern:"); + mainLayout->addWidget(patternLabel); + + auto patternLayout = new QHBoxLayout(); + patternEdit = new QLineEdit("${project}_${name}"); + patternLayout->addWidget(patternEdit, 1); + + resetPatternBtn = new QPushButton("Reset"); + connect(resetPatternBtn, &QPushButton::clicked, this, + &ExportDialog::onResetPattern); + patternLayout->addWidget(resetPatternBtn); + + mainLayout->addLayout(patternLayout); + + // Help text + auto helpLabel = new QLabel( + "${project} - Project Name
${name} - Output Node " + "Name
"); + helpLabel->setObjectName("ExportHelp"); // styled in app.qss.in + mainLayout->addWidget(helpLabel); + + // Spacer + mainLayout->addStretch(); + + // Buttons + auto buttonLayout = new QHBoxLayout(); + buttonLayout->addStretch(); + + cancelBtn = new QPushButton("Cancel"); + connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); + buttonLayout->addWidget(cancelBtn); + + okBtn = new QPushButton("OK"); + okBtn->setDefault(true); + connect(okBtn, &QPushButton::clicked, this, &ExportDialog::onOk); + buttonLayout->addWidget(okBtn); + + mainLayout->addLayout(buttonLayout); +} + +void ExportDialog::updateDestinationDisplay() +{ + const bool empty = exportDestination.isEmpty(); + destinationLabel->setText(empty ? "No destination selected" : exportDestination); + // "empty" drives the muted color via app.qss.in (#ExportDestination[empty="true"]) + destinationLabel->setProperty("empty", empty); + destinationLabel->style()->unpolish(destinationLabel); + destinationLabel->style()->polish(destinationLabel); + chooseDestinationBtn->setText(empty ? "Choose Folder" : "..."); +} + +void ExportDialog::onChooseDestination() +{ + QString dir = QFileDialog::getExistingDirectory( + this, "Select Export Destination", exportDestination, + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (!dir.isEmpty()) { + exportDestination = dir; + if (project) { + project->exportDestination = dir; + } + updateDestinationDisplay(); + } +} + +void ExportDialog::onResetPattern() +{ + QString defaultPattern = "${project}_${name}"; + patternEdit->setText(defaultPattern); + if (project) { + project->exportFilePattern = defaultPattern; + } +} + +void ExportDialog::onOk() +{ + // Get current pattern from the input + QString pattern = patternEdit->text().trimmed(); + + // Validate pattern + if (pattern.isEmpty()) { + QMessageBox::warning(this, "Invalid Pattern", + "Please specify an export pattern."); + return; + } + + // Save pattern to project + if (project) { + project->exportFilePattern = pattern; + } + + // Close dialog + accept(); +} diff --git a/src/texturelab/widgets/exportdialog.h b/src/texturelab/widgets/exportdialog.h new file mode 100644 index 00000000..e45c554e --- /dev/null +++ b/src/texturelab/widgets/exportdialog.h @@ -0,0 +1,49 @@ +#ifndef EXPORTDIALOG_H +#define EXPORTDIALOG_H + +#include +#include +#include + +class QLabel; +class QLineEdit; +class QPushButton; +class QVBoxLayout; + +class TextureProject; +typedef QSharedPointer TextureProjectPtr; + +class ExportDialog : public QDialog { + Q_OBJECT + +public: + explicit ExportDialog(QWidget* parent = nullptr); + ~ExportDialog(); + + void setProject(TextureProjectPtr project); + + QString getExportDestination() const { return exportDestination; } + QString getExportPattern() const; + +private slots: + void onChooseDestination(); + void onResetPattern(); + void onOk(); + +private: + void setupUI(); + void updateDestinationDisplay(); + + TextureProjectPtr project; + QString exportDestination; + + // UI components + QLabel* destinationLabel; + QPushButton* chooseDestinationBtn; + QLineEdit* patternEdit; + QPushButton* resetPatternBtn; + QPushButton* okBtn; + QPushButton* cancelBtn; +}; + +#endif // EXPORTDIALOG_H diff --git a/src/texturelab/widgets/graphwidget.cpp b/src/texturelab/widgets/graphwidget.cpp new file mode 100644 index 00000000..f4a8b4cd --- /dev/null +++ b/src/texturelab/widgets/graphwidget.cpp @@ -0,0 +1,824 @@ +#include "graphwidget.h" +#include "../clipboard.h" +#include "../undo/undocommands.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class NoWheelComboBox : public QComboBox { +public: + using QComboBox::QComboBox; + void wheelEvent(QWheelEvent* event) override { event->ignore(); } +}; + +#include "../systeminfo.h" +#include "../telemetry.h" +#include "./graphics/texturerenderer.h" +#include "./models.h" +#include "./utils.h" +#include "graph/comment.h" +#include "graph/frame.h" +#include "graph/scene.h" +#include "libraries/library.h" +#include "librarywidget.h" +#include "nodegraph.h" +#include "nodesearchpopup.h" + +void GraphWidget::syncFrameToScene(const FramePtr& frame) +{ + if (!frame || !scene) + return; + auto ngFrame = scene->getFrameById(frame->id); + if (ngFrame) { + ngFrame->setTitle(frame->text); + ngFrame->setColor(frame->color); + } +} + +void GraphWidget::syncCommentToScene(const CommentPtr& comment) +{ + if (!comment || !scene) + return; + auto ngComment = scene->getCommentById(comment->id); + if (ngComment) + ngComment->setText(comment->text); +} + +GraphWidget::GraphWidget() : QMainWindow(nullptr) +{ + graph = new nodegraph::NodeGraph(this); + this->setCentralWidget(graph); + + this->setAcceptDrops(true); + + setupToolbar(); + + // Create search popup + searchPopup = new NodeSearchPopup(this); + connect(searchPopup, &NodeSearchPopup::itemSelected, this, + &GraphWidget::addItemFromSearch); + + // Enable mouse tracking to capture cursor position + setMouseTracking(true); + graph->setMouseTracking(true); + + connect(graph, &nodegraph::NodeGraph::connectionAdded, + [=](nodegraph::ConnectionPtr con) { + auto leftNodeId = con->startPort->node->id(); + auto leftOutput = con->startPort->name; + auto rightNodeId = con->endPort->node->id(); + auto rightInput = con->endPort->name; + if (undoStack) + undoStack->push(new AddConnectionCommand( + project, scene, renderer, leftNodeId, leftOutput, + rightNodeId, rightInput)); + else { + // addConnection() marks the right node and everything + // downstream of it dirty + project->addConnection(project->getNodeById(leftNodeId), + project->getNodeById(rightNodeId), + rightInput); + renderer->update(); + } + }); + + connect(graph, &nodegraph::NodeGraph::connectionRemoved, + [=](nodegraph::ConnectionPtr con) { + auto leftNodeId = con->startPort->node->id(); + auto leftOutput = con->startPort->name; + auto rightNodeId = con->endPort->node->id(); + auto rightInput = con->endPort->name; + if (undoStack) + undoStack->push(new RemoveConnectionCommand( + project, scene, renderer, leftNodeId, leftOutput, + rightNodeId, rightInput)); + else { + // removeConnection() marks the right node and everything + // downstream of it dirty + project->removeConnection(leftNodeId, rightNodeId, + rightInput); + renderer->update(); + } + }); + + connect(graph, &nodegraph::NodeGraph::nodeSelectionChanged, + [=](nodegraph::NodePtr node) { + if (!!node) { + qDebug() << "NODE SELECTED"; + + if (!!project) { + auto texNode = project->getNodeById(node->id()); + emit nodeSelectionChanged(texNode); + } + } + else { + qDebug() << "NODE DESELECTED"; + emit nodeSelectionChanged(TextureNodePtr(nullptr)); + } + }); + + connect(graph, &nodegraph::NodeGraph::nodeDoubleClicked, + [=](nodegraph::NodePtr node) { + if (!!node) { + // qDebug() << "NODE DOUBLE CLICKED"; + + if (!!project) { + auto texNode = project->getNodeById(node->id()); + emit nodeDoubleClicked(texNode); + } + } + else { + // qDebug() << "NODE DESELECTED"; + emit nodeDoubleClicked(TextureNodePtr(nullptr)); + } + }); + + connect( + graph, &nodegraph::NodeGraph::deleteRequested, + [=](QList nodes, QList frames, + QList comments) { + QList nodeIds, frameIds, commentIds; + for (auto& n : nodes) + nodeIds.append(n->id()); + for (auto& f : frames) + frameIds.append(f->id()); + for (auto& c : comments) + commentIds.append(c->id()); + if (undoStack) + undoStack->push(new DeleteItemsCommand( + project, scene, renderer, nodeIds, frameIds, commentIds)); + else { + // Fallback: direct deletion (no undo) + for (auto& n : nodes) { + scene->removeNode(n); + + // also drops the node's connections and channel + // assignment, marking downstream nodes dirty + project->removeNode(n->id()); + } + for (auto& f : frames) { + scene->removeFrame(f); + project->frames.remove(f->id()); + } + for (auto& c : comments) { + scene->removeComment(c); + project->comments.remove(c->id()); + } + renderer->update(); + } + emit nodeSelectionChanged(TextureNodePtr(nullptr)); + emit frameSelectionChanged(FramePtr(nullptr)); + emit commentSelectionChanged(CommentPtr(nullptr)); + }); + + connect(graph, &nodegraph::NodeGraph::itemsMoveFinished, + [=](QMap oldPos, QMap newPos) { + if (undoStack) + undoStack->push( + new MoveItemsCommand(project, scene, oldPos, newPos)); + }); + + connect(graph, &nodegraph::NodeGraph::frameSelectionChanged, + [=](nodegraph::FramePtr ngFrame) { + if (!ngFrame || !project) { + emit frameSelectionChanged(FramePtr(nullptr)); + return; + } + auto modelFrame = project->frames.value(ngFrame->id()); + emit frameSelectionChanged(modelFrame); + }); + + connect(graph, &nodegraph::NodeGraph::commentSelectionChanged, + [=](nodegraph::CommentPtr ngComment) { + if (!ngComment || !project) { + emit commentSelectionChanged(CommentPtr(nullptr)); + return; + } + auto modelComment = project->comments.value(ngComment->id()); + emit commentSelectionChanged(modelComment); + }); + + // library = nullptr; + + // Actions rather than plain shortcuts, so the main window's Edit menu can + // reuse them and display the keys. The widget context keeps them off text + // fields in the other docks. + auto makeClipboardAction = [this](const QString& text, + QKeySequence::StandardKey key, + void (GraphWidget::*slot)()) { + auto action = new QAction(text, this); + action->setShortcut(key); + action->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(action, &QAction::triggered, this, slot); + this->addAction(action); + return action; + }; + + cutAction = makeClipboardAction("Cut", QKeySequence::Cut, + &GraphWidget::executeCut); + copyAction = makeClipboardAction("Copy", QKeySequence::Copy, + &GraphWidget::executeCopy); + pasteAction = makeClipboardAction("Paste", QKeySequence::Paste, + &GraphWidget::executePaste); +} + +void GraphWidget::setupToolbar() +{ + auto toolbar = this->addToolBar("Graph"); + toolbar->setMovable(false); + + toolbar->addWidget(new QLabel("Resolution: ")); + + resolutionPicker = new NoWheelComboBox(); + for (int res : {32, 64, 128, 256, 512, 1024, 2048, 4096}) + resolutionPicker->addItem(QString("%1 x %1").arg(res), res); + resolutionPicker->setCurrentIndex(5); // default: 1024 + resolutionPicker->setEnabled(false); + toolbar->addWidget(resolutionPicker); + + connect(resolutionPicker, &QComboBox::currentIndexChanged, this, + [=](int /*index*/) { + if (!project) + return; + const int previous = project->textureWidth; + const int res = resolutionPicker->currentData().toInt(); + if (res == previous) + return; + + if (!confirmResolutionChange(previous, res)) { + QSignalBlocker block(resolutionPicker); + const int back = resolutionPicker->findData(previous); + if (back >= 0) + resolutionPicker->setCurrentIndex(back); + return; + } + + project->textureWidth = res; + project->textureHeight = res; + for (auto& node : project->nodes) + node->isDirty = true; + if (renderer) + renderer->update(); + }); + + toolbar->addSeparator(); + + toolbar->addWidget(new QLabel("Seed: ")); + + seedInput = new QSpinBox(); + seedInput->setMinimum(0); + seedInput->setMaximum(99999); + seedInput->setValue(0); + seedInput->setEnabled(false); + toolbar->addWidget(seedInput); + + connect(seedInput, &QSpinBox::valueChanged, this, [=]() { + if (!project) + return; + Telemetry::breadcrumb("ui.seed", "random seed changed", + {{"seed", (int64_t)seedInput->value()}, + {"node_count", (int64_t)project->nodes.size()}}); + project->randomSeed = seedInput->value(); + for (auto& node : project->nodes) + node->isDirty = true; + if (renderer) + renderer->update(); + }); +} + +void GraphWidget::setTextureProject(TextureProjectPtr project) +{ + // generate nodes from texture project + + // auto scene = graph->scene(); + auto scene = nodegraph::Scene::create(); + // auto scene = new nodegraph::Scene(); + this->scene = scene; + this->project = project; + + // Update toolbar controls from project settings + { + QSignalBlocker b1(resolutionPicker); + QSignalBlocker b2(seedInput); + int resIndex = resolutionPicker->findData(project->textureWidth); + resolutionPicker->setCurrentIndex(resIndex >= 0 ? resIndex : 5); + resolutionPicker->setEnabled(true); + seedInput->setValue(project->randomSeed); + seedInput->setEnabled(true); + } + + // Set library for search popup + if (project && project->library) { + searchPopup->setLibrary(project->library); + } + + // add nodes + for (auto node : project->nodes) { + this->addNode(node); + } + + // add connections + for (auto con : project->connections) { + auto leftNode = scene->getNodeById(con->leftNode->id); + auto rightNode = scene->getNodeById(con->rightNode->id); + + scene->connectNodes(leftNode, "output", rightNode, + con->rightNodeInputName); + } + + // add comments + for (auto comment : project->comments) { + auto gcomment = nodegraph::Comment::create(); + gcomment->setId(comment->id); + gcomment->setText(comment->text); + gcomment->setPos(comment->pos.x(), comment->pos.y()); + scene->addComment(gcomment); + } + + // add frames + for (auto frame : project->frames) { + auto gframe = nodegraph::Frame::create(); + gframe->setId(frame->id); + gframe->setTitle(frame->text); + gframe->setColor(frame->color); + gframe->setPos(frame->pos.x(), frame->pos.y()); + if (frame->size.x() > 0 && frame->size.y() > 0) + gframe->setSize(frame->size.x(), frame->size.y()); + scene->addFrame(gframe); + } + + // graph->setNodeGraphScene(nodegraph::ScenePtr(scene)); + graph->setNodeGraphScene(scene); +} + +void GraphWidget::addNode(const TextureNodePtr& node) +{ + auto gnode = nodegraph::Node::create(); + gnode->setName(node->title); + for (auto input : node->inputs) { + gnode->addInPort(input); + } + + gnode->setId(node->id); + gnode->addOutPort("output"); + gnode->setCenter(node->pos.x(), node->pos.y()); + + scene->addNode(gnode); +} + +void GraphWidget::syncPositionsToModel() +{ + if (!project || !scene) + return; + + for (auto& node : project->nodes) { + auto gnode = scene->getNodeById(node->id); + if (gnode) { + auto center = gnode->getCenter(); + node->pos = QVector2D(center.x(), center.y()); + } + } + + for (auto& comment : project->comments) { + auto gcomment = scene->getCommentById(comment->id); + if (gcomment) { + auto p = gcomment->pos(); + comment->pos = QVector2D(p.x(), p.y()); + } + } + + for (auto& frame : project->frames) { + auto gframe = scene->getFrameById(frame->id); + if (gframe) { + auto p = gframe->pos(); + frame->pos = QVector2D(p.x(), p.y()); + auto rect = gframe->frameRect(); + frame->size = QVector2D(rect.width(), rect.height()); + } + } +} + +void GraphWidget::dragEnterEvent(QDragEnterEvent* evt) +{ + // Only claim drags we actually handle (nodes/frames/comments dragged + // from the Library panel). Anything else — e.g. a .texture file + // dragged in from the OS — must be left ignored so Qt forwards it up + // to MainWindow's dragEnterEvent instead of it being swallowed here. + if (evt->mimeData()->hasFormat(LIBRARY_ITEM_MIME_FORMAT)) + evt->acceptProposedAction(); + else + evt->ignore(); +} + +void GraphWidget::dragMoveEvent(QDragMoveEvent* evt) +{ + if (evt->mimeData()->hasFormat(LIBRARY_ITEM_MIME_FORMAT)) + evt->acceptProposedAction(); + else + evt->ignore(); +} + +void GraphWidget::dropEvent(QDropEvent* evt) +{ + auto mimeData = evt->mimeData(); + if (mimeData->hasFormat(LIBRARY_ITEM_MIME_FORMAT)) { + auto data = (const LibraryItemMimeData*)mimeData; + auto scenePos = this->graph->mapToScene(evt->position().toPoint()); + + if (data->itemType == PopupItemType::Frame) { + QString frameId = + QUuid::createUuid().toString(QUuid::WithoutBraces); + if (undoStack) + undoStack->push(new AddFrameCommand(project, scene, frameId, + QVector2D(scenePos))); + else { + auto frame = nodegraph::Frame::create(); + frame->setPos(scenePos); + scene->addFrame(frame); + if (project) { + auto modelFrame = FramePtr(new Frame()); + modelFrame->id = frame->id(); + modelFrame->pos = QVector2D(scenePos); + project->frames[modelFrame->id] = modelFrame; + } + } + } + else if (data->itemType == PopupItemType::Comment) { + QString commentId = + QUuid::createUuid().toString(QUuid::WithoutBraces); + if (undoStack) + undoStack->push(new AddCommentCommand(project, scene, commentId, + QVector2D(scenePos))); + else { + auto comment = nodegraph::Comment::create(); + comment->setPos(scenePos); + scene->addComment(comment); + if (project) { + auto modelComment = CommentPtr(new Comment()); + modelComment->id = comment->id(); + modelComment->pos = QVector2D(scenePos); + project->comments[modelComment->id] = modelComment; + } + } + } + else { + if (undoStack) + undoStack->push(new AddNodeCommand(project, scene, renderer, + data->libraryItemName, + QVector2D(scenePos))); + else { + auto node = project->library->createNode(data->libraryItemName); + node->pos = QVector2D(scenePos); + project->addNode(node); + addNode(node); + renderer->update(); + } + } + + evt->accept(); + } + else { + evt->ignore(); + } +} + +bool GraphWidget::confirmResolutionChange(int from, int to) +{ + const int nodeCount = project ? project->nodes.size() : 0; + const int64_t estimated = + TextureRenderer::estimatedNodeTextureBytes(to) * nodeCount; + const SystemInfo::GpuMemory mem = + renderer ? renderer->queryGpuMemory() : SystemInfo::GpuMemory{}; + + Telemetry::breadcrumb( + "ui.resolution", + std::to_string(from) + " -> " + std::to_string(to), + {{"from", (int64_t)from}, + {"to", (int64_t)to}, + {"node_count", (int64_t)nodeCount}, + {"estimated_mb", estimated / (1024 * 1024)}, + {"vram_available_mb", + mem.known ? mem.availableKb / 1024 : (int64_t)-1}}); + + // Only worth asking when the driver actually reports free VRAM and we're + // clearly over it. Where it doesn't (plenty of Mesa configurations), the + // rollback in TextureRenderer covers us instead of nagging on a guess. + const int64_t availableBytes = mem.availableKb * 1024; + if (!mem.known || estimated <= availableBytes * 7 / 10) + return true; + + const auto mb = [](int64_t bytes) { + return QString::number(bytes / (1024 * 1024)); + }; + + const auto choice = QMessageBox::warning( + this, tr("Not enough video memory?"), + tr("Rendering %1 nodes at %2 x %2 needs about %3 MB of video memory, " + "but your GPU reports only %4 MB free.\n\n" + "TextureLab will fall back to %5 x %5 if it runs out. Continue?") + .arg(nodeCount) + .arg(to) + .arg(mb(estimated)) + .arg(mb(availableBytes)) + .arg(from), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (choice != QMessageBox::Yes) { + Telemetry::breadcrumb("ui.resolution", "resolution change declined by user", + {{"to", (int64_t)to}}); + return false; + } + + return true; +} + +void GraphWidget::onResolutionChangeFailed(int requested, int fallback) +{ + { + QSignalBlocker block(resolutionPicker); + const int index = resolutionPicker->findData(fallback); + if (index >= 0) + resolutionPicker->setCurrentIndex(index); + } + + QMessageBox::warning( + this, tr("Resolution change failed"), + tr("Your GPU ran out of memory allocating textures at %1 x %1.\n\n" + "The project has been returned to %2 x %2.") + .arg(requested) + .arg(fallback)); +} + +void GraphWidget::setTextureRenderer(TextureRenderer* renderer) +{ + this->renderer = renderer; + + // MainWindow clears the renderer with a null before building the next one + // (setProject); connecting to it emits "invalid nullptr parameter" for + // every signal below. + if (!renderer) + return; + + // Queued: rollBackResolution() emits this from inside TextureRenderer's + // update loop, and onResolutionChangeFailed puts up a modal dialog. Letting + // that spin an event loop mid-update would re-enter update() through the + // renderer's queued nodeRendered callbacks. + connect(renderer, &TextureRenderer::resolutionChangeFailed, this, + &GraphWidget::onResolutionChangeFailed, Qt::QueuedConnection); + + connect(renderer, &TextureRenderer::thumbnailGenerated, + [=](const QString& nodeId, GLint texId, const QPixmap& pixmap) { + // scene->setNodeThumbnail(nodeId, pixmap); + auto node = scene->getNodeById(nodeId); + if (node) { + node->setTextureId(texId); + node->setThumbnail(pixmap); + } + }); +} + +void GraphWidget::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Space) { + // Show the search popup at the current mouse cursor position + QPoint globalPos = QCursor::pos(); + searchPopup->show(globalPos); + event->accept(); + } + else { + QMainWindow::keyPressEvent(event); + } +} + +void GraphWidget::executeCopy() +{ + if (!project || !scene) + return; + + syncPositionsToModel(); + + QList nodeIds, frameIds, commentIds; + for (auto item : scene->selectedItems()) { + if (item->type() == (int)nodegraph::SceneItemType::Node) { + auto node = qgraphicsitem_cast(item); + if (node) + nodeIds.append(node->id()); + } + else if (item->type() == (int)nodegraph::SceneItemType::Frame) { + auto frame = qgraphicsitem_cast(item); + if (frame) + frameIds.append(frame->id()); + } + else if (item->type() == (int)nodegraph::SceneItemType::Comment) { + auto comment = qgraphicsitem_cast(item); + if (comment) + commentIds.append(comment->id()); + } + } + + if (nodeIds.isEmpty() && frameIds.isEmpty() && commentIds.isEmpty()) + return; + + Clipboard::copyItems(project, nodeIds, frameIds, commentIds); +} + +void GraphWidget::executeCut() +{ + if (!project || !scene) + return; + + executeCopy(); + + QList nodeIds, frameIds, commentIds; + for (auto item : scene->selectedItems()) { + if (item->type() == (int)nodegraph::SceneItemType::Node) { + auto node = qgraphicsitem_cast(item); + if (node) + nodeIds.append(node->id()); + } + else if (item->type() == (int)nodegraph::SceneItemType::Frame) { + auto frame = qgraphicsitem_cast(item); + if (frame) + frameIds.append(frame->id()); + } + else if (item->type() == (int)nodegraph::SceneItemType::Comment) { + auto comment = qgraphicsitem_cast(item); + if (comment) + commentIds.append(comment->id()); + } + } + + if (nodeIds.isEmpty() && frameIds.isEmpty() && commentIds.isEmpty()) + return; + + if (undoStack) + undoStack->push(new DeleteItemsCommand(project, scene, renderer, + nodeIds, frameIds, commentIds)); + else { + for (const auto& id : nodeIds) { + auto ngNode = scene->getNodeById(id); + if (ngNode) + scene->removeNode(ngNode); + // also drops the node's connections and channel assignment, + // marking downstream nodes dirty + project->removeNode(id); + } + for (const auto& id : frameIds) { + auto f = scene->getFrameById(id); + if (f) + scene->removeFrame(f); + project->frames.remove(id); + } + for (const auto& id : commentIds) { + auto c = scene->getCommentById(id); + if (c) + scene->removeComment(c); + project->comments.remove(id); + } + if (renderer) + renderer->update(); + } + + emit nodeSelectionChanged(TextureNodePtr(nullptr)); + emit frameSelectionChanged(FramePtr(nullptr)); + emit commentSelectionChanged(CommentPtr(nullptr)); +} + +void GraphWidget::executePaste() +{ + if (!project || !scene) + return; + + QPointF viewCenter = graph->mapToScene(graph->viewport()->rect().center()); + + if (undoStack) { + auto* cmd = new PasteCommand(project, scene, renderer, viewCenter); + if (cmd->isEmpty()) { + delete cmd; + return; + } + undoStack->push(cmd); + } + else { + QList newNodes; + QList newConnections; + QList newComments; + QList newFrames; + + if (!Clipboard::pasteItems(project, viewCenter, newNodes, + newConnections, newComments, newFrames)) + return; + + scene->clearSelection(); + for (auto& node : newNodes) { + project->nodes[node->id] = node; + addNode(node); + auto ngNode = scene->getNodeById(node->id); + if (ngNode) + ngNode->setSelected(true); + } + for (auto& con : newConnections) { + project->connections[con->id] = con; + project->markNodeAsDirty(con->rightNode); + auto l = scene->getNodeById(con->leftNode->id); + auto r = scene->getNodeById(con->rightNode->id); + if (l && r) + scene->connectNodes(l, "output", r, con->rightNodeInputName); + } + for (auto& comment : newComments) { + project->comments[comment->id] = comment; + auto gc = nodegraph::Comment::create(); + gc->setId(comment->id); + gc->setText(comment->text); + gc->setPos(comment->pos.x(), comment->pos.y()); + scene->addComment(gc); + gc->setSelected(true); + } + for (auto& frame : newFrames) { + project->frames[frame->id] = frame; + auto gf = nodegraph::Frame::create(); + gf->setId(frame->id); + gf->setTitle(frame->text); + gf->setColor(frame->color); + gf->setPos(frame->pos.x(), frame->pos.y()); + if (frame->size.x() > 0 && frame->size.y() > 0) + gf->setSize(frame->size.x(), frame->size.y()); + scene->addFrame(gf); + gf->setSelected(true); + } + if (renderer) + renderer->update(); + } +} + +void GraphWidget::setUndoStack(QUndoStack* stack) { undoStack = stack; } + +void GraphWidget::addItemFromSearch(const QString& name, PopupItemType type, + const QPoint& position) +{ + QPoint localPos = graph->mapFromGlobal(position); + auto scenePos = graph->mapToScene(localPos); + + if (type == PopupItemType::Frame) { + QString frameId = QUuid::createUuid().toString(QUuid::WithoutBraces); + if (undoStack) + undoStack->push(new AddFrameCommand(project, scene, frameId, + QVector2D(scenePos))); + else { + auto frame = nodegraph::Frame::create(); + frame->setPos(scenePos); + scene->addFrame(frame); + if (project) { + auto modelFrame = FramePtr(new Frame()); + modelFrame->id = frame->id(); + modelFrame->pos = QVector2D(scenePos); + project->frames[modelFrame->id] = modelFrame; + } + } + } + else if (type == PopupItemType::Comment) { + QString commentId = QUuid::createUuid().toString(QUuid::WithoutBraces); + if (undoStack) + undoStack->push(new AddCommentCommand(project, scene, commentId, + QVector2D(scenePos))); + else { + auto comment = nodegraph::Comment::create(); + comment->setPos(scenePos); + scene->addComment(comment); + if (project) { + auto modelComment = CommentPtr(new Comment()); + modelComment->id = comment->id(); + modelComment->pos = QVector2D(scenePos); + project->comments[modelComment->id] = modelComment; + } + } + } + else { + if (!project || !project->library) + return; + + if (undoStack) + undoStack->push(new AddNodeCommand(project, scene, renderer, name, + QVector2D(scenePos))); + else { + auto node = project->library->createNode(name); + node->pos = QVector2D(scenePos); + project->addNode(node); + addNode(node); + if (renderer) + renderer->update(); + } + } +} \ No newline at end of file diff --git a/src/texturelab/widgets/graphwidget.h b/src/texturelab/widgets/graphwidget.h new file mode 100644 index 00000000..10fa3313 --- /dev/null +++ b/src/texturelab/widgets/graphwidget.h @@ -0,0 +1,100 @@ +#pragma once + +#include "nodesearchpopup.h" +#include +#include +#include +#include +#include + +class QAction; +class QDragEnterEvent; +class TextureRenderer; + +namespace nodegraph { +class NodeGraph; +class Scene; +typedef QSharedPointer ScenePtr; + +} // namespace nodegraph +class Library; + +class TextureProject; +class TextureNode; +class Comment; +class Frame; +typedef QSharedPointer TextureProjectPtr; +typedef QSharedPointer TextureNodePtr; +typedef QSharedPointer CommentPtr; +typedef QSharedPointer FramePtr; + +class GraphWidget : public QMainWindow { + Q_OBJECT + +public: + GraphWidget(); + + void setTextureProject(TextureProjectPtr project); + void setUndoStack(QUndoStack* stack); + + void dragEnterEvent(QDragEnterEvent* evt); + void dragMoveEvent(QDragMoveEvent* event); + void dropEvent(QDropEvent* event); + void keyPressEvent(QKeyEvent* event) override; + + void setTextureRenderer(TextureRenderer* renderer); + void syncPositionsToModel(); + + void syncFrameToScene(const FramePtr& frame); + void syncCommentToScene(const CommentPtr& comment); + + nodegraph::NodeGraph* graph; + // Library* library; + nodegraph::ScenePtr scene; + TextureProjectPtr project; + + TextureRenderer* renderer; + QUndoStack* undoStack = nullptr; + + // Clipboard actions. These own the Cut/Copy/Paste shortcuts, scoped to + // this widget so line edits elsewhere in the window keep their own, and + // are reused by the main window's Edit menu so it shows the same keys + // without registering a second, ambiguous binding. + QAction* cutAction; + QAction* copyAction; + QAction* pasteAction; + +protected: + void addNode(const TextureNodePtr& node); + void addItemFromSearch(const QString& name, PopupItemType type, + const QPoint& position); + +private: + void setupToolbar(); + + // Breadcrumbs the change, and — when the driver tells us how much VRAM is + // free — asks first if the new resolution plausibly won't fit. Returns + // false if the user backed out. + bool confirmResolutionChange(int from, int to); + + // Puts the picker back and explains, after TextureRenderer gave up on a + // resolution and rolled the project back. + void onResolutionChangeFailed(int requested, int fallback); + + NodeSearchPopup* searchPopup; + QPoint lastMousePos; + + QComboBox* resolutionPicker; + QSpinBox* seedInput; + +public slots: + void executeCopy(); + void executeCut(); + void executePaste(); + +signals: + void nodeSelectionChanged(const TextureNodePtr& node); + void nodeDoubleClicked(const TextureNodePtr& node); + void frameSelectionChanged(const FramePtr& frame); + void commentSelectionChanged(const CommentPtr& comment); +}; \ No newline at end of file diff --git a/src/texturelab/widgets/librarywidget.cpp b/src/texturelab/widgets/librarywidget.cpp new file mode 100644 index 00000000..9cb13690 --- /dev/null +++ b/src/texturelab/widgets/librarywidget.cpp @@ -0,0 +1,221 @@ +#include "librarywidget.h" +#include "../utils.h" +#include "./libraries/library.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Grid geometry for the library thumbnails. The cell is tall enough for the +// icon plus two lines of text, so titles like "Gradient Noise Fractal Sum" wrap +// instead of being elided to one line. +static constexpr int kIconSize = 64; +static constexpr int kTextLines = 2; +static constexpr int kTextLineHeight = 14; +static constexpr int kItemWidth = 90; +static constexpr int kItemPadding = 6; +static constexpr int kItemHeight = + kIconSize + kTextLines * kTextLineHeight + kItemPadding; + +// https://doc.qt.io/qt-6/qmimedata.html +// subclassing QMimeData is cleaner + +bool LibraryItemMimeData::hasFormat(const QString& format) const +{ + if (format == LIBRARY_ITEM_MIME_FORMAT) + return true; + + return false; +} + +LibraryWidget::LibraryWidget() : QWidget() +{ + this->setObjectName("LibraryPanel"); // QSS scoping (app.qss.in) + this->setMinimumWidth(100); + this->setLayout(new QVBoxLayout()); + + // library version indicator + upgrade button + auto versionRow = new QWidget(this); + auto versionLayout = new QHBoxLayout(versionRow); + versionLayout->setContentsMargins(0, 0, 0, 0); + + versionLabel = new QLabel(versionRow); + versionLabel->setObjectName("LibraryVersionLabel"); // styled in app.qss.in + versionLayout->addWidget(versionLabel); + + versionLayout->addStretch(); + + upgradeButton = new QPushButton("Upgrade", versionRow); + upgradeButton->setProperty("variant", + "primary"); // draw attention to the action + upgradeButton->setVisible(false); + connect(upgradeButton, &QPushButton::clicked, this, + &LibraryWidget::upgradeRequested); + versionLayout->addWidget(upgradeButton); + + this->layout()->addWidget(versionRow); + + // search box + searchBar = new QLineEdit(this); + searchBar->setObjectName("LibrarySearch"); + searchBar->setPlaceholderText("search"); + searchBar->setAlignment(Qt::AlignLeft); + connect(searchBar, &QLineEdit::textChanged, this, + &LibraryWidget::filterList); + + this->layout()->addWidget(searchBar); + + // list widget + listWidget = new LibraryListWidget(); + listWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + this->layout()->addWidget(listWidget); + + this->setLibrary(nullptr); +} + +void LibraryWidget::setLibraryVersion(const QString& version, bool isCurrent) +{ + versionLabel->setText(isCurrent + ? QString("Library: %1").arg(version) + : QString("Library: %1 (outdated)").arg(version)); + + // Drive the color from a dynamic property so the "outdated" tint lives in + // app.qss.in (uses the theme's warn token) rather than a hardcoded hex. + versionLabel->setProperty("outdated", !isCurrent); + versionLabel->style()->unpolish(versionLabel); + versionLabel->style()->polish(versionLabel); + + upgradeButton->setVisible(!isCurrent); +} + +void LibraryWidget::addSpecialItem(const QString& name, const QString& iconPath, + PopupItemType type) +{ + QListWidgetItem* item = new QListWidgetItem; + item->setData(Qt::DisplayRole, name); + item->setData((int)Roles::ItemType, "LibraryItem"); + item->setData((int)Roles::LibraryItemName, name); + item->setData(Qt::UserRole, (int)type); + item->setSizeHint(QSize(kItemWidth, kItemHeight)); + item->setTextAlignment(Qt::AlignCenter); + item->setFlags(item->flags() | Qt::ItemIsEditable); + item->setIcon(QIcon(iconPath)); + this->listWidget->addItem(item); +} + +void LibraryWidget::setLibrary(Library* lib) +{ + this->listWidget->clear(); + + addSpecialItem("Frame", ":nodes/frame.png", PopupItemType::Frame); + addSpecialItem("Comment", ":nodes/comment.png", PopupItemType::Comment); + + if (!lib) + return; + + for (auto& libraryItem : lib->items) { + QListWidgetItem* item = new QListWidgetItem; + // Show the official title ("Gradient Noise"), not the internal type + // name ("gradientnoise"); the type name travels in LibraryItemName. + item->setData(Qt::DisplayRole, libraryItem.displayName); + item->setData((int)Roles::ItemType, "LibraryItem"); + item->setData((int)Roles::LibraryItemName, libraryItem.name); + item->setData(Qt::UserRole, (int)PopupItemType::Node); + item->setSizeHint(QSize(kItemWidth, kItemHeight)); + item->setTextAlignment(Qt::AlignCenter); + item->setFlags(item->flags() | Qt::ItemIsEditable); + item->setIcon(libraryItem.icon); + this->listWidget->addItem(item); + } +} + +void LibraryWidget::filterList(const QString& text) +{ + QString searchText = text.toLower(); + + for (int i = 0; i < listWidget->count(); i++) { + QListWidgetItem* item = listWidget->item(i); + // Match the visible title as well as the type name, so searching + // "gradientnoise" still finds "Gradient Noise". + QString title = item->data(Qt::DisplayRole).toString().toLower(); + QString typeName = + item->data((int)Roles::LibraryItemName).toString().toLower(); + + bool matches = searchText.isEmpty() || title.contains(searchText) || + typeName.contains(searchText); + item->setHidden(!matches); + } +} + +LibraryListWidget::LibraryListWidget() : QListWidget() +{ + setAlternatingRowColors(false); + setSpacing(0); + setViewMode(QListWidget::IconMode); + setIconSize(QSize(kIconSize, kIconSize)); + setMouseTracking(true); + setEditTriggers(QAbstractItemView::NoEditTriggers); + + setDragDropMode(QAbstractItemView::DragDrop); + setDragEnabled(true); + + setResizeMode(QListWidget::Adjust); + setDefaultDropAction(Qt::CopyAction); + setSelectionMode(QAbstractItemView::SingleSelection); + + setWordWrap(true); + setGridSize(QSize(kItemWidth, kItemHeight)); + + setContentsMargins(0, 0, 0, 0); + + // setDragEnabled(true); + // viewport()->setAcceptDrops(true); + // setAcceptDrops(true); + setDropIndicatorShown(true); + + setObjectName("LibraryList"); // item styling in app.qss.in +} + +void LibraryListWidget::resizeEvent(QResizeEvent* event) +{ + QListWidget::resizeEvent(event); + updateGridSize(); +} + +void LibraryListWidget::updateGridSize() +{ + int availableWidth = viewport()->width() - verticalScrollBar()->width(); + int itemsPerRow = qMax(1, availableWidth / kItemWidth); + int adjustedItemWidth = availableWidth / itemsPerRow; + + setGridSize(QSize(adjustedItemWidth, kItemHeight)); +} + +QMimeData* +LibraryListWidget::mimeData(const QList& items) const +{ + // QMimeData* data = new QMimeData(); + // // set text for item + // data->setText(items[0]->data(Qt::DisplayRole).toString()); + // data->setData("ITEM_TYPE", "LIBRARY_ITEM"); + + auto itemName = items[0]->data((int)Roles::LibraryItemName).toString(); + auto itemType = (PopupItemType)items[0]->data(Qt::UserRole).toInt(); + qDebug() << "Mime Data Dragging: " << itemName; + + auto mimeData = new LibraryItemMimeData(); + mimeData->libraryItemName = itemName; + mimeData->itemType = itemType; + + return mimeData; +} \ No newline at end of file diff --git a/src/texturelab/widgets/librarywidget.h b/src/texturelab/widgets/librarywidget.h new file mode 100644 index 00000000..2571ad23 --- /dev/null +++ b/src/texturelab/widgets/librarywidget.h @@ -0,0 +1,71 @@ +#pragma once + +#include "nodesearchpopup.h" +#include +#include +#include + +class Library; +class LibraryListWidget; +class QLineEdit; +class QLabel; +class QPushButton; + +// https://stackoverflow.com/questions/37331270/how-to-create-grid-style-qlistwidget +class LibraryWidget : public QWidget { + Q_OBJECT +public: + LibraryWidget(); + void setLibrary(Library* lib); + + // Shows which library version the open project is on. When + // `isCurrent` is false, an "Upgrade" button is shown that emits + // upgradeRequested(). + void setLibraryVersion(const QString& version, bool isCurrent); + + LibraryListWidget* listWidget; + QLineEdit* searchBar; + +signals: + void upgradeRequested(); + +private slots: + void filterList(const QString& text); + +private: + void addSpecialItem(const QString& name, const QString& iconPath, + PopupItemType type); + + QLabel* versionLabel; + QPushButton* upgradeButton; +}; + +class LibraryItemMimeData : public QMimeData { + Q_OBJECT + +public: + QString libraryItemName; + PopupItemType itemType = PopupItemType::Node; + + virtual bool hasFormat(const QString& format) const override; +}; + +class LibraryListWidget : public QListWidget { +public: + LibraryListWidget(); + +protected: + // https://stackoverflow.com/questions/4295838/how-to-implement-drag-in-a-qlistwidget-that-contains-files + // QStringList mimeTypes() const + // { + // QStringList qstrList; + // qstrList.append("texturelab/library-item"); + // return qstrList; + // } + + QMimeData* mimeData(const QList& items) const; + void resizeEvent(QResizeEvent* event) override; + +private: + void updateGridSize(); +}; \ No newline at end of file diff --git a/src/texturelab/widgets/nodesearchpopup.cpp b/src/texturelab/widgets/nodesearchpopup.cpp new file mode 100644 index 00000000..183fc7c7 --- /dev/null +++ b/src/texturelab/widgets/nodesearchpopup.cpp @@ -0,0 +1,264 @@ +#include "nodesearchpopup.h" +#include "../utils.h" +#include "./libraries/library.h" + +#include +#include +#include +#include +#include +#include + +NodeSearchPopup::NodeSearchPopup(QWidget* parent) : QFrame(parent) +{ + library = nullptr; + + // Setup frame styling for a floating popup (see #NodeSearchPopup in app.qss.in) + setObjectName("NodeSearchPopup"); + setWindowFlags(Qt::Popup | Qt::FramelessWindowHint); + + // Set fixed size for the popup + setFixedSize(300, 400); + + // Setup layout + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(5, 5, 5, 5); + layout->setSpacing(5); + + // Search input + searchInput = new QLineEdit(this); + searchInput->setPlaceholderText("Search nodes..."); + searchInput->installEventFilter(this); + connect(searchInput, &QLineEdit::textChanged, this, + &NodeSearchPopup::filterList); + layout->addWidget(searchInput); + + // Item list + itemList = new QListWidget(this); + itemList->setSelectionMode(QAbstractItemView::SingleSelection); + connect(itemList, &QListWidget::itemClicked, + [this](QListWidgetItem* item) { + if (item) { + auto type = (PopupItemType)item->data(Qt::UserRole).toInt(); + auto name = + item->data((int)Roles::LibraryItemName).toString(); + emit itemSelected(name, type, showPosition); + hide(); + } + }); + layout->addWidget(itemList); + + setLayout(layout); +} + +void NodeSearchPopup::setLibrary(Library* lib) +{ + library = lib; + itemList->clear(); + + // Fixed entries: Frame and Comment always appear at the top + auto frameItem = new QListWidgetItem(QIcon(":nodes/frame.png"), "Frame"); + frameItem->setData(Qt::UserRole, (int)PopupItemType::Frame); + frameItem->setData((int)Roles::LibraryItemName, "Frame"); + itemList->addItem(frameItem); + + auto commentItem = new QListWidgetItem(QIcon(":nodes/comment.png"), "Comment"); + commentItem->setData(Qt::UserRole, (int)PopupItemType::Comment); + commentItem->setData((int)Roles::LibraryItemName, "Comment"); + itemList->addItem(commentItem); + + if (lib) { + for (auto& libraryItem : lib->items) { + QListWidgetItem* item = new QListWidgetItem; + // Show the official title; the internal type name the graph needs + // travels in LibraryItemName (see itemName() below). + item->setText(libraryItem.displayName); + item->setIcon(libraryItem.icon); + item->setData(Qt::UserRole, (int)PopupItemType::Node); + item->setData((int)Roles::LibraryItemName, libraryItem.name); + itemList->addItem(item); + } + } + + if (itemList->count() > 0) { + itemList->setCurrentRow(0); + } +} + +void NodeSearchPopup::show(const QPoint& pos) +{ + showPosition = pos; + + // Clear search and reset filter + searchInput->clear(); + filterList(""); + + // Select first item + if (itemList->count() > 0) { + itemList->setCurrentRow(0); + } + + // Position the popup at the cursor + move(pos); + + // Show the popup + QFrame::show(); + + // Focus the search input + searchInput->setFocus(); +} + +void NodeSearchPopup::hide() +{ + QFrame::hide(); + searchInput->clear(); +} + +void NodeSearchPopup::filterList(const QString& text) +{ + QString searchText = text.toLower(); + + int visibleCount = 0; + for (int i = 0; i < itemList->count(); i++) { + QListWidgetItem* item = itemList->item(i); + // Match the visible title as well as the type name, so searching + // "gradientnoise" still finds "Gradient Noise". + QString title = item->text().toLower(); + QString typeName = + item->data((int)Roles::LibraryItemName).toString().toLower(); + + bool matches = searchText.isEmpty() || title.contains(searchText) || + typeName.contains(searchText); + item->setHidden(!matches); + + if (matches && visibleCount == 0) { + // Select the first visible item + itemList->setCurrentItem(item); + } + + if (matches) + visibleCount++; + } +} + +void NodeSearchPopup::selectItem(int index) +{ + if (index >= 0 && index < itemList->count()) { + itemList->setCurrentRow(index); + } +} + +QString NodeSearchPopup::getSelectedItemName() const +{ + auto currentItem = itemList->currentItem(); + if (currentItem && !currentItem->isHidden()) { + return currentItem->data((int)Roles::LibraryItemName).toString(); + } + return QString(); +} + +PopupItemType NodeSearchPopup::getSelectedItemType() const +{ + auto currentItem = itemList->currentItem(); + if (currentItem && !currentItem->isHidden()) { + return (PopupItemType)currentItem->data(Qt::UserRole).toInt(); + } + return PopupItemType::Node; +} + +bool NodeSearchPopup::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == searchInput && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + + // Handle arrow keys to navigate the list + if (keyEvent->key() == Qt::Key_Up) { + // Navigate up in the list (only through visible items) + int currentRow = itemList->currentRow(); + for (int i = currentRow - 1; i >= 0; i--) { + if (!itemList->item(i)->isHidden()) { + itemList->setCurrentRow(i); + break; + } + } + return true; + } + else if (keyEvent->key() == Qt::Key_Down) { + // Navigate down in the list (only through visible items) + int currentRow = itemList->currentRow(); + for (int i = currentRow + 1; i < itemList->count(); i++) { + if (!itemList->item(i)->isHidden()) { + itemList->setCurrentRow(i); + break; + } + } + return true; + } + else if (keyEvent->key() == Qt::Key_Return || + keyEvent->key() == Qt::Key_Enter) { + // Add the selected item + QString itemName = getSelectedItemName(); + if (!itemName.isEmpty()) { + emit itemSelected(itemName, getSelectedItemType(), showPosition); + hide(); + } + return true; + } + else if (keyEvent->key() == Qt::Key_Escape) { + // Close the popup + hide(); + return true; + } + } + + return QFrame::eventFilter(obj, event); +} + +void NodeSearchPopup::keyPressEvent(QKeyEvent* event) +{ + switch (event->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: { + // Add the selected item + QString itemName = getSelectedItemName(); + if (!itemName.isEmpty()) { + emit itemSelected(itemName, getSelectedItemType(), showPosition); + hide(); + } + event->accept(); + break; + } + case Qt::Key_Escape: + // Close the popup + hide(); + event->accept(); + break; + case Qt::Key_Up: { + // Navigate up in the list (only through visible items) + int currentRow = itemList->currentRow(); + for (int i = currentRow - 1; i >= 0; i--) { + if (!itemList->item(i)->isHidden()) { + itemList->setCurrentRow(i); + break; + } + } + event->accept(); + break; + } + case Qt::Key_Down: { + // Navigate down in the list (only through visible items) + int currentRow = itemList->currentRow(); + for (int i = currentRow + 1; i < itemList->count(); i++) { + if (!itemList->item(i)->isHidden()) { + itemList->setCurrentRow(i); + break; + } + } + event->accept(); + break; + } + default: + QFrame::keyPressEvent(event); + break; + } +} diff --git a/src/texturelab/widgets/nodesearchpopup.h b/src/texturelab/widgets/nodesearchpopup.h new file mode 100644 index 00000000..29d0b008 --- /dev/null +++ b/src/texturelab/widgets/nodesearchpopup.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +class Library; +class QLineEdit; + +enum class PopupItemType { Node, Frame, Comment }; + +class NodeSearchPopup : public QFrame { + Q_OBJECT + +public: + NodeSearchPopup(QWidget* parent = nullptr); + + void setLibrary(Library* lib); + void show(const QPoint& pos); + void hide(); + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; + +private: + void filterList(const QString& text); + void selectItem(int index); + QString getSelectedItemName() const; + PopupItemType getSelectedItemType() const; + + Library* library; + QLineEdit* searchInput; + QListWidget* itemList; + QPoint showPosition; + +signals: + void itemSelected(const QString& itemName, PopupItemType type, + const QPoint& position); +}; diff --git a/src/texturelab/widgets/properties/accordionwidget.cpp b/src/texturelab/widgets/properties/accordionwidget.cpp new file mode 100644 index 00000000..1609299d --- /dev/null +++ b/src/texturelab/widgets/properties/accordionwidget.cpp @@ -0,0 +1,47 @@ +#include "accordionwidget.h" + +#include +#include + +AccordionWidget::AccordionWidget(const QString& title, bool startCollapsed, + QWidget* parent) + : QWidget(parent), _title(title), _collapsed(startCollapsed) +{ + auto* outerLayout = new QVBoxLayout(this); + outerLayout->setContentsMargins(0, 0, 0, 2); + outerLayout->setSpacing(0); + this->setLayout(outerLayout); + + headerButton = new QPushButton(this); + headerButton->setObjectName("AccordionHeader"); // styled in app.qss.in + headerButton->setFlat(true); + headerButton->setCursor(Qt::PointingHandCursor); + outerLayout->addWidget(headerButton); + + contentWidget = new QWidget(this); + contentLayout = new QVBoxLayout(contentWidget); + contentLayout->setContentsMargins(0, 0, 0, 0); + contentLayout->setSpacing(0); + contentWidget->setLayout(contentLayout); + outerLayout->addWidget(contentWidget); + + updateHeader(); + contentWidget->setVisible(!_collapsed); + + connect(headerButton, &QPushButton::clicked, this, [this]() { + _collapsed = !_collapsed; + updateHeader(); + contentWidget->setVisible(!_collapsed); + }); +} + +void AccordionWidget::updateHeader() +{ + QString arrow = _collapsed ? " ▶ " : " ▼ "; + headerButton->setText(arrow + _title); +} + +void AccordionWidget::addWidget(QWidget* widget) +{ + contentLayout->addWidget(widget); +} diff --git a/src/texturelab/widgets/properties/accordionwidget.h b/src/texturelab/widgets/properties/accordionwidget.h new file mode 100644 index 00000000..1455094f --- /dev/null +++ b/src/texturelab/widgets/properties/accordionwidget.h @@ -0,0 +1,22 @@ +#pragma once +#include + +class QVBoxLayout; +class QPushButton; + +class AccordionWidget : public QWidget { + Q_OBJECT + + QString _title; + QPushButton* headerButton; + QWidget* contentWidget; + QVBoxLayout* contentLayout; + bool _collapsed; + + void updateHeader(); + +public: + AccordionWidget(const QString& title, bool startCollapsed = false, + QWidget* parent = nullptr); + void addWidget(QWidget* widget); +}; diff --git a/src/texturelab/widgets/properties/curvepropwidget.cpp b/src/texturelab/widgets/properties/curvepropwidget.cpp new file mode 100644 index 00000000..af8e8cef --- /dev/null +++ b/src/texturelab/widgets/properties/curvepropwidget.cpp @@ -0,0 +1,512 @@ +#include "curvepropwidget.h" + +#include "thememanager.h" +#include "tokens.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// ============================================================================ +// Metrics (colours are theme-driven; see CurveCanvas::refreshColors) +// ============================================================================ + +static constexpr int CANVAS_PAD = 8; // px padding inside canvas +static constexpr float ANCHOR_R = 5.0f; +static constexpr float ANCHOR_R_HL = 6.0f; +static constexpr float HANDLE_R = 3.0f; + +// ============================================================================ +// CurveCanvas +// ============================================================================ + +CurveCanvas::CurveCanvas(QWidget* parent) : QWidget(parent) +{ + setMouseTracking(true); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + setMinimumSize(200, 200); + // Keep square + QSizePolicy sp = sizePolicy(); + sp.setHeightForWidth(true); + setSizePolicy(sp); + + refreshColors(); + connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this, [this]() { + refreshColors(); + update(); + }); +} + +void CurveCanvas::refreshColors() +{ + const Theme& t = ThemeManager::instance().theme(); + colBg = t.color(Tokens::CurveBg); + colGrid = t.color(Tokens::CurveGrid); + colIdentity = t.color(Tokens::CurveIdentity); + colCurve = t.color(Tokens::CurveLine); + colAnchorDef = t.color(Tokens::CurveAnchor); + colAnchorHov = t.color(Tokens::CurveAnchorHover); + colAnchorSel = t.color(Tokens::CurveAnchorSelect); + colHandleLine = t.color(Tokens::CurveHandleLine); + colHandleDot = t.color(Tokens::CurveHandleDot); + colHandleHov = t.color(Tokens::CurveHandleHover); + colHandleCor = t.color(Tokens::CurveHandleCorner); +} + +int CurveCanvas::heightForWidth(int w) const { return w; } +bool CurveCanvas::hasHeightForWidth() const { return true; } + +void CurveCanvas::setCurve(const Curve& c) +{ + curve = c; + update(); +} + +// --------------------------------------------------------------------------- +// Coordinate helpers +// --------------------------------------------------------------------------- + +QPointF CurveCanvas::toWidget(float x, float y) const +{ + int pad = CANVAS_PAD; + float W = width() - 2 * pad; + float H = height() - 2 * pad; + // y is flipped: y=0 → bottom, y=1 → top + return QPointF(pad + x * W, pad + (1.0f - y) * H); +} + +QPointF CurveCanvas::toCurveSpace(QPointF p) const +{ + int pad = CANVAS_PAD; + float W = width() - 2 * pad; + float H = height() - 2 * pad; + float x = (p.x() - pad) / W; + float y = 1.0f - (p.y() - pad) / H; + return QPointF(qBound(0.0, x, 1.0), qBound(0.0, y, 1.0)); +} + +// --------------------------------------------------------------------------- +// Hit testing +// --------------------------------------------------------------------------- + +int CurveCanvas::hitTestAnchor(QPointF pos, float radiusPx) const +{ + for (int i = 0; i < curve.points.size(); i++) { + QPointF wp = toWidget(curve.points[i].x, curve.points[i].y); + if (QLineF(pos, wp).length() <= radiusPx) + return i; + } + return -1; +} + +int CurveCanvas::hitTestHandle(QPointF pos, bool& outLeft, float radiusPx) const +{ + if (selectedPoint < 0 || selectedPoint >= curve.points.size()) + return -1; + + const CurvePoint& pt = curve.points[selectedPoint]; + + QPointF lhW = toWidget(pt.x + pt.lx, pt.y + pt.ly); + QPointF rhW = toWidget(pt.x + pt.rx, pt.y + pt.ry); + + if (QLineF(pos, lhW).length() <= radiusPx) { outLeft = true; return selectedPoint; } + if (QLineF(pos, rhW).length() <= radiusPx) { outLeft = false; return selectedPoint; } + return -1; +} + +int CurveCanvas::hitTestCurvePath(QPointF pos, float tolerancePx) const +{ + if (curve.points.size() < 2) return -1; + + // Sample the curve path and find closest point + const int STEPS = 200; + float minDist = tolerancePx; + int bestSeg = -1; + float bestX = 0.0f; + + for (int i = 0; i < curve.points.size() - 1; i++) { + const CurvePoint& a = curve.points[i]; + const CurvePoint& b = curve.points[i + 1]; + + for (int s = 0; s <= STEPS; s++) { + float t = s / (float)STEPS; + float mt = 1.0f - t; + + float px = mt*mt*mt*a.x + 3*mt*mt*t*(a.x+a.rx) + + 3*mt*t*t*(b.x+b.lx) + t*t*t*b.x; + float py = mt*mt*mt*a.y + 3*mt*mt*t*(a.y+a.ry) + + 3*mt*t*t*(b.y+b.ly) + t*t*t*b.y; + + QPointF wp = toWidget(px, py); + float d = (float)QLineF(pos, wp).length(); + if (d < minDist) { + minDist = d; + bestSeg = i; + bestX = px; + } + } + } + (void)bestX; + return bestSeg; // segment index — caller inserts a point at mouse x +} + +// --------------------------------------------------------------------------- +// Paint +// --------------------------------------------------------------------------- + +void CurveCanvas::paintEvent(QPaintEvent*) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + drawGrid(p); + drawIdentityLine(p); + drawCurvePath(p); + drawHandles(p); + drawAnchors(p); +} + +void CurveCanvas::drawGrid(QPainter& p) +{ + p.fillRect(rect(), colBg); + + QPen pen(colGrid, 1); + p.setPen(pen); + + for (int i = 0; i <= 4; i++) { + float t = i / 4.0f; + QPointF a = toWidget(t, 0.0f); + QPointF b = toWidget(t, 1.0f); + p.drawLine(a, b); + + a = toWidget(0.0f, t); + b = toWidget(1.0f, t); + p.drawLine(a, b); + } +} + +void CurveCanvas::drawIdentityLine(QPainter& p) +{ + QPen pen(colIdentity, 1, Qt::DashLine); + p.setPen(pen); + p.drawLine(toWidget(0, 0), toWidget(1, 1)); +} + +void CurveCanvas::drawCurvePath(QPainter& p) +{ + if (curve.points.size() < 2) return; + + QPainterPath path; + path.moveTo(toWidget(curve.points[0].x, curve.points[0].y)); + + for (int i = 0; i < curve.points.size() - 1; i++) { + const CurvePoint& a = curve.points[i]; + const CurvePoint& b = curve.points[i + 1]; + + QPointF cp1 = toWidget(a.x + a.rx, a.y + a.ry); + QPointF cp2 = toWidget(b.x + b.lx, b.y + b.ly); + QPointF end = toWidget(b.x, b.y); + path.cubicTo(cp1, cp2, end); + } + + QPen pen(colCurve, 1.5f); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawPath(path); +} + +void CurveCanvas::drawHandles(QPainter& p) +{ + if (selectedPoint < 0 || selectedPoint >= curve.points.size()) return; + + const CurvePoint& pt = curve.points[selectedPoint]; + QPointF anchor = toWidget(pt.x, pt.y); + QPointF lh = toWidget(pt.x + pt.lx, pt.y + pt.ly); + QPointF rh = toWidget(pt.x + pt.rx, pt.y + pt.ry); + + QColor dotColor = pt.smooth ? colHandleDot : colHandleCor; + + // Lines from anchor to handles + QPen linePen(colHandleLine, 1); + p.setPen(linePen); + p.drawLine(anchor, lh); + p.drawLine(anchor, rh); + + // Handle dots + auto drawHandle = [&](QPointF pos, bool isHovered) { + QColor c = isHovered ? colHandleHov : dotColor; + p.setPen(QPen(c, 1)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(pos, HANDLE_R, HANDLE_R); + }; + + bool lhHovered = (hoveredHandle == selectedPoint && hoveredHandleLeft); + bool rhHovered = (hoveredHandle == selectedPoint && !hoveredHandleLeft); + drawHandle(lh, lhHovered); + drawHandle(rh, rhHovered); +} + +void CurveCanvas::drawAnchors(QPainter& p) +{ + for (int i = 0; i < curve.points.size(); i++) { + const CurvePoint& pt = curve.points[i]; + QPointF wp = toWidget(pt.x, pt.y); + + float r; + QColor fill; + + if (i == selectedPoint) { + r = ANCHOR_R_HL; + fill = colAnchorSel; + // ring + p.setPen(QPen(colAnchorSel, 1)); + p.setBrush(Qt::NoBrush); + p.drawEllipse(wp, r + 2, r + 2); + } else if (i == hoveredPoint) { + r = ANCHOR_R_HL; + fill = colAnchorHov; + } else { + r = ANCHOR_R; + fill = colAnchorDef; + } + + p.setPen(Qt::NoPen); + p.setBrush(fill); + p.drawEllipse(wp, r, r); + } +} + +// --------------------------------------------------------------------------- +// Mouse events +// --------------------------------------------------------------------------- + +void CurveCanvas::mousePressEvent(QMouseEvent* event) +{ + altHeld = event->modifiers() & Qt::AltModifier; + + if (event->button() == Qt::LeftButton) { + QPointF pos = event->position(); + + // 1. Hit test handle (only when a point is selected) + bool handleLeft = false; + int hi = hitTestHandle(pos, handleLeft); + if (hi >= 0) { + dragTarget = handleLeft ? CurveDragTarget::LeftHandle + : CurveDragTarget::RightHandle; + dragIndex = hi; + dragHandleLeft = handleLeft; + setCursor(Qt::ClosedHandCursor); + return; + } + + // 2. Hit test anchor + int ai = hitTestAnchor(pos); + if (ai >= 0) { + selectedPoint = ai; + dragTarget = CurveDragTarget::Anchor; + dragIndex = ai; + setCursor(Qt::ClosedHandCursor); + update(); + return; + } + + // 3. Hit test curve path → add point on curve + QPointF cs = toCurveSpace(pos); + int pathSeg = hitTestCurvePath(pos); + if (pathSeg >= 0) { + // Snap y to existing curve at this x + curve.addPoint((float)cs.x(), (float)cs.y()); + selectedPoint = -1; + // Find the newly inserted point + for (int i = 0; i < curve.points.size(); i++) { + if (qAbs(curve.points[i].x - (float)cs.x()) < 0.01f) { + selectedPoint = i; + break; + } + } + emit curveChanged(curve); + update(); + return; + } + + // 4. Empty area → add new point + if (curve.points.size() < CURVE_MAX_POINTS) { + curve.addPoint((float)cs.x(), (float)cs.y()); + selectedPoint = -1; + for (int i = 0; i < curve.points.size(); i++) { + if (qAbs(curve.points[i].x - (float)cs.x()) < 0.01f) { + selectedPoint = i; + break; + } + } + emit curveChanged(curve); + update(); + } + } else if (event->button() == Qt::LeftButton) { + // Deselect on background click (handled above via fall-through) + selectedPoint = -1; + update(); + } +} + +void CurveCanvas::mouseMoveEvent(QMouseEvent* event) +{ + QPointF pos = event->position(); + altHeld = event->modifiers() & Qt::AltModifier; + + if (dragTarget == CurveDragTarget::Anchor && dragIndex >= 0) { + QPointF cs = toCurveSpace(pos); + curve.moveAnchor(dragIndex, (float)cs.x(), (float)cs.y()); + emit curveChanged(curve); + emit anchorDragging(dragIndex); + update(); + return; + } + + if ((dragTarget == CurveDragTarget::LeftHandle || + dragTarget == CurveDragTarget::RightHandle) && dragIndex >= 0) + { + // Break symmetry if Alt held + if (altHeld) curve.points[dragIndex].smooth = false; + + QPointF cs = toCurveSpace(pos); + const CurvePoint& pt = curve.points[dragIndex]; + float dx = (float)cs.x() - pt.x; + float dy = (float)cs.y() - pt.y; + + bool isLeft = (dragTarget == CurveDragTarget::LeftHandle); + // Compute delta from current handle position + float curHx = isLeft ? pt.lx : pt.rx; + float curHy = isLeft ? pt.ly : pt.ry; + curve.moveHandle(dragIndex, isLeft, dx - curHx, dy - curHy); + emit curveChanged(curve); + update(); + return; + } + + // Hover detection + int prevHovAnchor = hoveredPoint; + int prevHovHandle = hoveredHandle; + hoveredPoint = hitTestAnchor(pos); + bool hl = false; + hoveredHandle = hitTestHandle(pos, hl); + hoveredHandleLeft = hl; + + if (hoveredPoint >= 0 || hoveredHandle >= 0) + setCursor(Qt::SizeAllCursor); + else + setCursor(Qt::CrossCursor); + + if (hoveredPoint != prevHovAnchor || hoveredHandle != prevHovHandle) + update(); +} + +void CurveCanvas::mouseReleaseEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + if (dragTarget != CurveDragTarget::None) + emit dragEnded(); + dragTarget = CurveDragTarget::None; + dragIndex = -1; + setCursor(Qt::CrossCursor); + } +} + +void CurveCanvas::contextMenuEvent(QContextMenuEvent* event) +{ + int ai = hitTestAnchor(event->pos()); + if (ai < 0) return; + + QMenu menu(this); + QAction* removeAct = menu.addAction("Remove Point"); + removeAct->setEnabled(ai > 0 && ai < curve.points.size() - 1); + + QAction* chosen = menu.exec(event->globalPos()); + if (chosen == removeAct) { + curve.removePoint(ai); + if (selectedPoint == ai) selectedPoint = -1; + else if (selectedPoint > ai) selectedPoint--; + emit curveChanged(curve); + update(); + } +} + +void CurveCanvas::leaveEvent(QEvent*) +{ + hoveredPoint = -1; + hoveredHandle = -1; + update(); +} + +// ============================================================================ +// CurvePropWidget +// ============================================================================ + +CurvePropWidget::CurvePropWidget(CurveProp* prop, QWidget* parent) + : QWidget(parent), prop(prop) +{ + auto* vLayout = new QVBoxLayout(this); + vLayout->setContentsMargins(0, 0, 0, 4); + vLayout->setSpacing(4); + + // Label row with Reset button + auto* headerRow = new QHBoxLayout(); + headerRow->setContentsMargins(0, 0, 0, 0); + + auto* label = new QLabel(prop->displayName, this); + resetBtn = new QPushButton("Reset", this); + resetBtn->setFixedWidth(50); + resetBtn->setFixedHeight(20); + resetBtn->setProperty("size", "small"); // styled in app.qss.in + + headerRow->addWidget(label); + headerRow->addStretch(); + headerRow->addWidget(resetBtn); + vLayout->addLayout(headerRow); + + // Canvas + canvas = new CurveCanvas(this); + canvas->setCurve(prop->value); + vLayout->addWidget(canvas); + + // Readout label + readout = new QLabel(this); + readout->setObjectName("CurveReadout"); // styled in app.qss.in + readout->setVisible(false); + vLayout->addWidget(readout); + + setLayout(vLayout); + + // Connections + connect(canvas, &CurveCanvas::curveChanged, this, [this](const Curve& c) { + this->prop->value = c; + emit valueChanged(c); + }); + + connect(canvas, &CurveCanvas::anchorDragging, this, [this](int idx) { + if (idx >= 0 && idx < this->prop->value.points.size()) { + const CurvePoint& pt = this->prop->value.points[idx]; + readout->setText(QString("In: %1 Out: %2") + .arg(pt.x, 0, 'f', 2) + .arg(pt.y, 0, 'f', 2)); + readout->setVisible(true); + } + }); + + connect(canvas, &CurveCanvas::dragEnded, this, [this]() { + readout->setVisible(false); + }); + + connect(resetBtn, &QPushButton::clicked, this, [this]() { + Curve identity; + this->prop->value = identity; + canvas->setCurve(identity); + readout->setVisible(false); + emit valueChanged(identity); + }); +} diff --git a/src/texturelab/widgets/properties/curvepropwidget.h b/src/texturelab/widgets/properties/curvepropwidget.h new file mode 100644 index 00000000..97a59991 --- /dev/null +++ b/src/texturelab/widgets/properties/curvepropwidget.h @@ -0,0 +1,86 @@ +#pragma once + +#include "../../curve.h" +#include "../../props.h" + +#include +#include +#include +#include + +enum class CurveDragTarget { None, Anchor, LeftHandle, RightHandle }; + +class CurveCanvas : public QWidget { + Q_OBJECT +public: + explicit CurveCanvas(QWidget* parent = nullptr); + + void setCurve(const Curve& curve); + const Curve& getCurve() const { return curve; } + + bool hasHeightForWidth() const override; + int heightForWidth(int w) const override; + +signals: + void curveChanged(const Curve& curve); + void anchorDragging(int index); // emits index while dragging anchor + void dragEnded(); + +protected: + void paintEvent(QPaintEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void contextMenuEvent(QContextMenuEvent* event) override; + void leaveEvent(QEvent* event) override; + +private: + Curve curve; + int selectedPoint = -1; + int hoveredPoint = -1; + int hoveredHandle = -1; // index of point whose handle is hovered + bool hoveredHandleLeft = false; + + CurveDragTarget dragTarget = CurveDragTarget::None; + int dragIndex = -1; + bool dragHandleLeft = false; + QPointF dragStartCurve; // curve-space position at drag start + bool altHeld = false; + + QPointF toWidget(float x, float y) const; + QPointF toCurveSpace(QPointF widgetPos) const; + + int hitTestAnchor(QPointF pos, float radiusPx = 8.0f) const; + // Returns point index; sets outLeft = true if left handle hit + int hitTestHandle(QPointF pos, bool& outLeft, float radiusPx = 8.0f) const; + // Returns point index to insert after if cursor is near the path + int hitTestCurvePath(QPointF pos, float tolerancePx = 6.0f) const; + + void drawGrid(QPainter& p); + void drawIdentityLine(QPainter& p); + void drawCurvePath(QPainter& p); + void drawAnchors(QPainter& p); + void drawHandles(QPainter& p); + + // Theme colors, refreshed from the active theme (surface B: QSS can't reach + // QPainter code). Repopulated on construction and on themeChanged(). + void refreshColors(); + QColor colBg, colGrid, colIdentity, colCurve; + QColor colAnchorDef, colAnchorHov, colAnchorSel; + QColor colHandleLine, colHandleDot, colHandleHov, colHandleCor; +}; + +class CurvePropWidget : public QWidget { + Q_OBJECT +public: + explicit CurvePropWidget(CurveProp* prop, QWidget* parent = nullptr); + +signals: + void valueChanged(const Curve& curve); + +private: + CurveProp* prop; + CurveCanvas* canvas; + QLabel* readout; + QPushButton* resetBtn; +}; diff --git a/src/texturelab/widgets/properties/propertieswidget.cpp b/src/texturelab/widgets/properties/propertieswidget.cpp new file mode 100644 index 00000000..0e6162ac --- /dev/null +++ b/src/texturelab/widgets/properties/propertieswidget.cpp @@ -0,0 +1,428 @@ +#include "propertieswidget.h" +#include "../../models.h" +#include "../../props.h" +#include "../../undo/undocommands.h" +#include "accordionwidget.h" +#include "curvepropwidget.h" +#include "propwidgets.h" + +#include +#include + +PropertiesWidget::PropertiesWidget() : QWidget() +{ + setObjectName("PropertiesPanel"); // for QSS scoping (app.qss.in) + displayMode = PropertyDisplayMode::None; + + textureChannelProp = new EnumProp(); + textureChannelProp->displayName = "Texture Channel"; + textureChannelProp->values = {"None", "Albedo", "Normal", "Metalness", + "Roughness", "Height", "Alpha", "AO"}; + textureChannelProp->setValue(0); + + randomSeedProp = new IntProp(); + randomSeedProp->displayName = "Random Seed"; + randomSeedProp->minValue = 0; + randomSeedProp->maxValue = 50; + randomSeedProp->step = 1; + randomSeedProp->setValue(0); + + auto layout = new QVBoxLayout(this); + layout->addStretch(1); + this->setLayout(layout); +} + +// Helper: push PropertyChangeCommand if undoStack is set; otherwise apply directly. +// The value is applied before calling this (first-redo pattern). +// The renderer must be passed through: marking a node dirty doesn't render it, +// something has to call TextureRenderer::update() to kick the render loop, and +// on undo/redo there's no propertyUpdated signal to do it. +static void pushPropChange(QUndoStack* stack, TextureNodePtr node, + TextureProjectPtr project, + TextureRenderer* renderer, + const QString& propName, + QVariant oldVal, QVariant newVal) +{ + if (stack) + stack->push(new PropertyChangeCommand( + node, project, renderer, propName, oldVal, newVal)); +} + +QVariant PropertiesWidget::takePropBaseline(Prop* prop, + const QVariant& newValue) +{ + QVariant oldValue = propBaselines.value(prop, newValue); + propBaselines[prop] = newValue; + + return oldValue; +} + +void PropertiesWidget::syncPropBaselines() +{ + for (auto it = propBaselines.begin(); it != propBaselines.end(); ++it) + it.value() = it.key()->getValue(); +} + +QWidget* PropertiesWidget::createPropWidget(Prop* prop, + const TextureNodePtr& node) +{ + propBaselines[prop] = prop->getValue(); + + switch (prop->type) { + case PropType::Float: { + auto widget = new FloatPropWidget(); + widget->setProp((FloatProp*)prop); + propWidgets.append(widget); + connect(widget, &FloatPropWidget::valueChanged, [=](double value) { + QVariant oldVal = takePropBaseline(prop, value); + node->setProp(prop->name, value); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, value); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, value); + }); + return widget; + } + case PropType::Bool: { + auto widget = new BoolPropWidget(); + widget->setProp((BoolProp*)prop); + propWidgets.append(widget); + connect(widget, &BoolPropWidget::valueChanged, [=](bool value) { + QVariant oldVal = takePropBaseline(prop, value); + node->setProp(prop->name, value); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, value); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, value); + }); + return widget; + } + case PropType::Int: { + auto widget = new IntPropWidget(); + widget->setProp((IntProp*)prop); + propWidgets.append(widget); + connect(widget, &IntPropWidget::valueChanged, [=](long value) { + QVariant oldVal = takePropBaseline(prop, (int)value); + node->setProp(prop->name, (int)value); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, (int)value); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, (int)value); + }); + return widget; + } + case PropType::Enum: { + auto widget = new EnumPropWidget(); + widget->setProp((EnumProp*)prop); + propWidgets.append(widget); + connect(widget, &EnumPropWidget::valueChanged, [=](int value) { + QVariant oldVal = takePropBaseline(prop, value); + node->setProp(prop->name, value); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, value); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, value); + }); + return widget; + } + case PropType::Color: { + auto widget = new ColorPropWidget(); + widget->setProp((ColorProp*)prop); + propWidgets.append(widget); + connect(widget, &ColorPropWidget::valueChanged, [=](const QColor& value) { + QVariant oldVal = takePropBaseline(prop, value); + node->setProp(prop->name, value); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, value); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, value); + }); + return widget; + } + case PropType::Gradient: { + auto widget = new GradientPropWidget(); + widget->setProp((GradientProp*)prop); + propWidgets.append(widget); + connect(widget, &GradientPropWidget::valueChanged, [=](const Gradient& value) { + QVariant newVal = QVariant::fromValue(value); + QVariant oldVal = takePropBaseline(prop, newVal); + node->setProp(prop->name, newVal); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, newVal); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, newVal); + }); + return widget; + } + case PropType::Image: { + auto widget = new ImagePropWidget(); + widget->setProp((ImageProp*)prop); + propWidgets.append(widget); + connect(widget, &ImagePropWidget::valueChanged, [=](const QImage& value) { + QVariant oldVal = takePropBaseline(prop, value); + node->setProp(prop->name, value); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, value); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, value); + }); + return widget; + } + case PropType::String: { + auto widget = new StringPropWidget(); + widget->setProp((StringProp*)prop); + propWidgets.append(widget); + connect(widget, &StringPropWidget::valueChanged, [=](const QString& value) { + QVariant oldVal = takePropBaseline(prop, value); + node->setProp(prop->name, value); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, value); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, value); + }); + return widget; + } + case PropType::Curve: { + auto widget = new CurvePropWidget((CurveProp*)prop); + propWidgets.append(widget); + connect(widget, &CurvePropWidget::valueChanged, [=](const Curve& value) { + QVariant newVal = QVariant::fromValue(value); + QVariant oldVal = takePropBaseline(prop, newVal); + node->setProp(prop->name, newVal); + project->markNodeAsDirty(node); + emit propertyUpdated(prop->name, newVal); + pushPropChange(undoStack, node, project, renderer, prop->name, oldVal, newVal); + }); + return widget; + } + default: + return nullptr; + } +} + +void PropertiesWidget::setSelectedNode(const TextureNodePtr& node) +{ + qDebug() << "Displaying properties for node: " << node->title; + + // clear current properties first, then assign (clearSelection resets selectedNode) + this->clearSelection(); + this->selectedNode = node; + + auto layout = (QVBoxLayout*)this->layout(); + + this->addBasePropsToLayout(); + + // sort all props by insertion order + QList sortedProps = node->props.values(); + std::sort(sortedProps.begin(), sortedProps.end(), + [](Prop* a, Prop* b) { return a->order < b->order; }); + + // ungrouped props first + for (auto prop : sortedProps) { + if (prop->group != nullptr) + continue; + auto widget = createPropWidget(prop, node); + if (widget) + layout->addWidget(widget); + } + + // then each group as a collapsible accordion + for (auto group : node->propertyGroups) { + auto accordion = + new AccordionWidget(group->name, group->collapsed, this); + for (auto prop : group->props) { + auto widget = createPropWidget(prop, node); + if (widget) + accordion->addWidget(widget); + } + layout->addWidget(accordion); + } + + layout->addStretch(1); +} + +void PropertiesWidget::addBasePropsToLayout() +{ + auto layout = (QVBoxLayout*)this->layout(); + + // texture channel + auto widget = new EnumPropWidget(); + + // determine value of prop + auto& channels = this->project->textureChannels; + int channelVal = 0; + for (auto key : channels.keys()) { + if (channels[key] == selectedNode->id) { + channelVal = (int)key; + break; + } + } + textureChannelProp->setValue(channelVal); + + widget->setProp(textureChannelProp); + propWidgets.append(widget); + + connect(widget, &EnumPropWidget::valueChanged, [=](int value) { + emit this->textureChannelUpdated((TextureChannel)value, + this->selectedNode); + }); + layout->addWidget(widget); + + randomSeedProp->setValue((int)this->selectedNode->randomSeed); + + auto seedWidget = new IntPropWidget(); + seedWidget->setProp(randomSeedProp); + connect(seedWidget, &IntPropWidget::valueChanged, [=](int value) { + long oldSeed = this->selectedNode->randomSeed; + this->selectedNode->randomSeed = value; + this->project->markNodeAsDirty(this->selectedNode); + emit this->propertyUpdated("randomSeed", value); + if (undoStack) + undoStack->push(new RandomSeedChangeCommand( + this->selectedNode, this->project, renderer, oldSeed, value)); + }); + layout->addWidget(seedWidget); +} + +void PropertiesWidget::setSelectedFrame(const FramePtr& frame) +{ + this->clearSelection(); + if (!frame) + return; + + this->selectedFrame = frame; + displayMode = PropertyDisplayMode::Frame; + + auto layout = (QVBoxLayout*)this->layout(); + + auto titleLabel = new QLabel("Frame"); + titleLabel->setObjectName("PropSectionTitle"); // styled in app.qss.in + layout->addWidget(titleLabel); + + auto titleProp = new StringProp(); + titleProp->displayName = "Title"; + titleProp->value = frame->text; + auto titleWidget = new StringPropWidget(); + titleWidget->setProp(titleProp); + propWidgets.append(titleWidget); + + connect(titleWidget, &StringPropWidget::valueChanged, [=](const QString& value) { + if (undoStack) { + QString oldTitle = frame->text; + QColor oldColor = frame->color; + frame->text = value; + emit framePropertyChanged(frame); + undoStack->push(new EditFrameCommand( + frame, scene, oldTitle, oldColor, value, oldColor)); + } else { + frame->text = value; + emit framePropertyChanged(frame); + } + }); + layout->addWidget(titleWidget); + + auto colorProp = new ColorProp(); + colorProp->displayName = "Color"; + colorProp->value = frame->color; + auto colorWidget = new ColorPropWidget(); + colorWidget->setProp(colorProp); + propWidgets.append(colorWidget); + + connect(colorWidget, &ColorPropWidget::valueChanged, [=](const QColor& color) { + if (undoStack) { + QString oldTitle = frame->text; + QColor oldColor = frame->color; + frame->color = color; + emit framePropertyChanged(frame); + undoStack->push(new EditFrameCommand( + frame, scene, oldTitle, oldColor, oldTitle, color)); + } else { + frame->color = color; + emit framePropertyChanged(frame); + } + }); + layout->addWidget(colorWidget); + + layout->addStretch(1); +} + +void PropertiesWidget::setSelectedComment(const CommentPtr& comment) +{ + this->clearSelection(); + if (!comment) + return; + + this->selectedComment = comment; + displayMode = PropertyDisplayMode::Comment; + + auto layout = (QVBoxLayout*)this->layout(); + + auto titleLabel = new QLabel("Comment"); + titleLabel->setObjectName("PropSectionTitle"); // styled in app.qss.in + layout->addWidget(titleLabel); + + auto textProp = new StringProp(); + textProp->displayName = "Text"; + textProp->value = comment->text; + auto textWidget = new StringPropWidget(); + textWidget->setProp(textProp); + textWidget->setMultiline(true); + propWidgets.append(textWidget); + + connect(textWidget, &StringPropWidget::valueChanged, [=](const QString& value) { + if (undoStack) { + QString oldText = comment->text; + comment->text = value; + emit commentPropertyChanged(comment); + undoStack->push(new EditCommentCommand(comment, scene, oldText, value)); + } else { + comment->text = value; + emit commentPropertyChanged(comment); + } + }); + layout->addWidget(textWidget); + + layout->addStretch(1); +} + +void PropertiesWidget::clearSelection() +{ + displayMode = PropertyDisplayMode::None; + selectedNode.clear(); + selectedFrame.clear(); + selectedComment.clear(); + + auto layout = this->layout(); + + // clear widget + for (auto widget : propWidgets) { + widget->hide(); + widget->deleteLater(); + } + + // remove all other items (todo: delete them) + while (layout->count() > 0) { + auto item = layout->itemAt(0); + layout->removeItem(item); + + auto widget = item->widget(); + if (widget) + widget->deleteLater(); + } + + propWidgets.clear(); + // the props these pointed at may belong to a node that's going away + propBaselines.clear(); +} + +void PropertiesWidget::setProject(const TextureProjectPtr& project) +{ + this->project = project; +} + +void PropertiesWidget::setScene(NgScenePtr ngScene) +{ + scene = ngScene; +} + +void PropertiesWidget::setUndoStack(QUndoStack* stack) +{ + undoStack = stack; +} + +void PropertiesWidget::setTextureRenderer(TextureRenderer* renderer) +{ + this->renderer = renderer; +} \ No newline at end of file diff --git a/src/texturelab/widgets/properties/propertieswidget.h b/src/texturelab/widgets/properties/propertieswidget.h new file mode 100644 index 00000000..0cdae997 --- /dev/null +++ b/src/texturelab/widgets/properties/propertieswidget.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class TextureProject; +class TextureNode; +class Comment; +class Frame; +typedef QSharedPointer TextureProjectPtr; +typedef QSharedPointer TextureNodePtr; +typedef QSharedPointer CommentPtr; +typedef QSharedPointer FramePtr; + +namespace nodegraph { class Scene; } +typedef QSharedPointer NgScenePtr; + +class Prop; +class EnumProp; +class IntProp; +class TextureRenderer; + +enum class TextureChannel : int; + +enum class PropertyDisplayMode { None, Node, Frame, Comment }; + +class PropertiesWidget : public QWidget { + + Q_OBJECT + + PropertyDisplayMode displayMode; + QVector propWidgets; + + TextureProjectPtr project; + NgScenePtr scene; + QUndoStack* undoStack = nullptr; + // non-owning; needed so undo/redo of a property change can kick the + // render loop (the live edit path goes through propertyUpdated instead) + TextureRenderer* renderer = nullptr; + TextureNodePtr selectedNode; + FramePtr selectedFrame; + CommentPtr selectedComment; + + // base props + EnumProp* textureChannelProp; + IntProp* randomSeedProp; + + // Value each displayed prop held before the edit in progress. The color, + // gradient, image and curve widgets write prop->value themselves before + // emitting valueChanged, so the prop can't be read back for the undo + // baseline — without this their undo steps record oldValue == newValue + // and undoing them changes nothing. + QHash propBaselines; + +public: + PropertiesWidget(); + + void setSelectedNode(const TextureNodePtr& node); + void setSelectedFrame(const FramePtr& frame); + void setSelectedComment(const CommentPtr& comment); + void clearSelection(); + + void setProject(const TextureProjectPtr& project); + void setScene(NgScenePtr ngScene); + void setUndoStack(QUndoStack* stack); + void setTextureRenderer(TextureRenderer* renderer); + + // Re-reads the undo baselines from the props. Call after undo/redo, which + // changes prop values behind the panel's back. + void syncPropBaselines(); + +private: + void addBasePropsToLayout(); + QWidget* createPropWidget(Prop* prop, const TextureNodePtr& node); + + // Returns the value the prop held before this edit and records newValue + // as the baseline for the next one. + QVariant takePropBaseline(Prop* prop, const QVariant& newValue); + +signals: + void propertyUpdated(const QString& name, const QVariant& value); + void textureChannelUpdated(const TextureChannel& name, + const TextureNodePtr& node); + void framePropertyChanged(const FramePtr& frame); + void commentPropertyChanged(const CommentPtr& comment); +}; \ No newline at end of file diff --git a/src/texturelab/widgets/properties/propwidgets.cpp b/src/texturelab/widgets/properties/propwidgets.cpp new file mode 100644 index 00000000..c130779f --- /dev/null +++ b/src/texturelab/widgets/properties/propwidgets.cpp @@ -0,0 +1,644 @@ +#include "propwidgets.h" +#include "../../models.h" +#include "../../props.h" +#include "colorpicker.h" +#include "gradientpicker.h" +#include "thememanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const int SLIDER_MAX = 1000; + +class NoWheelSlider : public QSlider { +public: + using QSlider::QSlider; + void wheelEvent(QWheelEvent* event) override { event->ignore(); } +}; + +class NoWheelComboBox : public QComboBox { +public: + using QComboBox::QComboBox; + void wheelEvent(QWheelEvent* event) override { event->ignore(); } +}; + +// FLOAT PROP WIDGET +// https://stackoverflow.com/a/19007951 +FloatPropWidget::FloatPropWidget() +{ + prop = nullptr; + updating = false; + + auto vlayout = new QVBoxLayout(this); + this->setLayout(vlayout); + + label = new QLabel(this); + label->setText(""); + vlayout->addWidget(label); + + slider = new NoWheelSlider(Qt::Horizontal, this); + slider->setMinimum(0); + slider->setMaximum(SLIDER_MAX); + slider->setSingleStep(1); + + spinbox = new QDoubleSpinBox(this); + spinbox->setMaximum(std::numeric_limits::max()); + spinbox->setFixedWidth(60); + + auto hbox = new QHBoxLayout(); + hbox->addWidget(slider); + hbox->addWidget(spinbox); + + vlayout->addLayout(hbox); + + this->setFixedHeight(80); + + connect(slider, &QSlider::valueChanged, [=](int val) { + if (updating || !prop) + return; + updating = true; + auto range = prop->maxValue - prop->minValue; + auto finalValue = prop->minValue + range * (val / (double)SLIDER_MAX); + spinbox->setValue(finalValue); + updating = false; + emit valueChanged(finalValue); + }); + + connect(spinbox, &QDoubleSpinBox::valueChanged, [=](double val) { + if (updating || !prop) + return; + updating = true; + auto range = prop->maxValue - prop->minValue; + int sliderVal = + (range > 0) + ? qBound(0, (int)((val - prop->minValue) / range * SLIDER_MAX), + SLIDER_MAX) + : 0; + slider->setValue(sliderVal); + updating = false; + emit valueChanged(val); + }); +} + +void FloatPropWidget::setProp(FloatProp* prop) +{ + this->prop = prop; + updating = true; + + label->setText(prop->displayName); + + spinbox->setMinimum(prop->minValue); + spinbox->setMaximum(std::numeric_limits::max()); + spinbox->setSingleStep(prop->step); + spinbox->setValue(prop->value); + + auto range = prop->maxValue - prop->minValue; + int sliderVal = + (range > 0) + ? qBound(0, + (int)((prop->value - prop->minValue) / range * SLIDER_MAX), + SLIDER_MAX) + : 0; + slider->setValue(sliderVal); + + updating = false; +} + +// INT PROP WIDGET +// https://stackoverflow.com/a/19007951 +IntPropWidget::IntPropWidget() +{ + prop = nullptr; + updating = false; + + auto vlayout = new QVBoxLayout(this); + this->setLayout(vlayout); + + label = new QLabel(this); + label->setText(""); + vlayout->addWidget(label); + + slider = new NoWheelSlider(Qt::Horizontal, this); + slider->setMinimum(0); + slider->setMaximum(SLIDER_MAX); + slider->setSingleStep(1); + + spinbox = new QSpinBox(this); + spinbox->setMaximum(INT_MAX); + spinbox->setFixedWidth(60); + + auto hbox = new QHBoxLayout(); + hbox->addWidget(slider); + hbox->addWidget(spinbox); + + vlayout->addLayout(hbox); + + this->setFixedHeight(80); + + connect(slider, &QSlider::valueChanged, [=](int val) { + if (updating || !prop) + return; + updating = true; + auto range = prop->maxValue - prop->minValue; + long finalValue = + prop->minValue + (long)qRound(range * (val / (double)SLIDER_MAX)); + spinbox->setValue((int)finalValue); + updating = false; + emit valueChanged(finalValue); + }); + + connect(spinbox, &QSpinBox::valueChanged, [=](int val) { + if (updating || !prop) + return; + updating = true; + auto range = prop->maxValue - prop->minValue; + int sliderVal = (range > 0) ? qBound(0, + (int)((val - prop->minValue) / + (double)range * SLIDER_MAX), + SLIDER_MAX) + : 0; + slider->setValue(sliderVal); + updating = false; + emit valueChanged((long)val); + }); +} + +void IntPropWidget::setProp(IntProp* prop) +{ + this->prop = prop; + updating = true; + + label->setText(prop->displayName); + + spinbox->setMinimum((int)prop->minValue); + spinbox->setMaximum(INT_MAX); + spinbox->setSingleStep((int)prop->step); + spinbox->setValue((int)prop->value); + + auto range = prop->maxValue - prop->minValue; + int sliderVal = (range > 0) ? qBound(0, + (int)((prop->value - prop->minValue) / + (double)range * SLIDER_MAX), + SLIDER_MAX) + : 0; + slider->setValue(sliderVal); + + updating = false; +} + +// ENUM PROP WIDGET +// https://stackoverflow.com/a/19007951 +EnumPropWidget::EnumPropWidget() +{ + prop = nullptr; + + auto vlayout = new QVBoxLayout(this); + this->setLayout(vlayout); + + // label + label = new QLabel(this); + label->setText(""); + vlayout->addWidget(label); + + // slider + comboBox = new NoWheelComboBox(this); + vlayout->addWidget(comboBox); + + this->setFixedHeight(80); + + connect(comboBox, &QComboBox::currentIndexChanged, + [=](int val) { emit valueChanged(val); }); +} + +void EnumPropWidget::setProp(EnumProp* prop) +{ + label->setText(prop->displayName); + + for (auto item : prop->values) { + comboBox->addItem(item); + } + + comboBox->setCurrentIndex(prop->index); + + this->prop = prop; +} + +// STRING PROP WIDGET +StringPropWidget::StringPropWidget() +{ + prop = nullptr; + + auto vlayout = new QVBoxLayout(this); + this->setLayout(vlayout); + + label = new QLabel(this); + label->setText(""); + vlayout->addWidget(label); + + lineEdit = new QLineEdit(this); + vlayout->addWidget(lineEdit); + + textEdit = new QPlainTextEdit(this); + textEdit->hide(); + vlayout->addWidget(textEdit); + + this->setFixedHeight(80); + + connect(lineEdit, &QLineEdit::textChanged, + [=](const QString& text) { emit valueChanged(text); }); + + connect(textEdit, &QPlainTextEdit::textChanged, + [=]() { emit valueChanged(textEdit->toPlainText()); }); +} + +void StringPropWidget::setProp(StringProp* prop) +{ + label->setText(prop->displayName); + lineEdit->setText(prop->value); + textEdit->setPlainText(prop->value); + + this->prop = prop; +} + +void StringPropWidget::setMultiline(bool multiline) +{ + if (multiline) { + lineEdit->hide(); + textEdit->show(); + setFixedHeight(120); + } + else { + textEdit->hide(); + lineEdit->show(); + setFixedHeight(80); + } +} + +// BOOL PROP WIDGET +// https://stackoverflow.com/a/19007951 +BoolPropWidget::BoolPropWidget() +{ + prop = nullptr; + + auto vlayout = new QVBoxLayout(this); + this->setLayout(vlayout); + + // label + label = new QLabel(this); + label->setText(""); + vlayout->addWidget(label); + + // slider + button = new QPushButton(this); + vlayout->addWidget(button); + + this->setFixedHeight(80); + + connect(button, &QPushButton::pressed, [=]() { + setValue(!this->value); + emit valueChanged(this->value); + }); +} + +void BoolPropWidget::setProp(BoolProp* prop) +{ + label->setText(prop->displayName); + + setValue(prop->value); +} + +void BoolPropWidget::setValue(bool value) +{ + this->value = value; + if (value) { + button->setText("True"); + } + else { + button->setText("False"); + } +} + +namespace { +// Color swatch that shows a checkerboard behind translucent colors. Shaped +// like the app's spinboxes (radius.sm corners, border.input outline). +class ColorSwatch : public QWidget { +public: + QColor color; + + explicit ColorSwatch(QWidget* parent = nullptr) : QWidget(parent) {} + +protected: + void paintEvent(QPaintEvent*) override + { + const Theme& t = ThemeManager::instance().theme(); + const qreal radius = t.radius("sm"); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QRectF r = QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5); + QPainterPath path; + path.addRoundedRect(r, radius, radius); + + painter.save(); + painter.setClipPath(path); + if (color.alpha() < 255) + drawCheckerboard(painter, rect()); + painter.fillRect(rect(), color); + painter.restore(); + + painter.setPen(QPen(t.color("border.input"), 1)); + painter.setBrush(Qt::NoBrush); + painter.drawPath(path); + } +}; + +// Height of a themed spinbox, so the swatch lines up with float/int props +int spinboxHeight() +{ + QDoubleSpinBox probe; + probe.ensurePolished(); + return probe.sizeHint().height(); +} +} // namespace + +ColorPropWidget::ColorPropWidget() +{ + prop = nullptr; + + auto vlayout = new QVBoxLayout(this); + this->setLayout(vlayout); + + // label + label = new QLabel(this); + label->setText(""); + vlayout->addWidget(label); + + // color preview + colorPreview = new ColorSwatch(this); + colorPreview->setFixedHeight(spinboxHeight()); + colorPreview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + colorPreview->setCursor(Qt::PointingHandCursor); + colorPreview->installEventFilter(this); + vlayout->addWidget(colorPreview); + + this->setFixedHeight(80); +} + +void ColorPropWidget::setProp(ColorProp* prop) +{ + label->setText(prop->displayName); + this->prop = prop; + updateColorPreview(); +} + +void ColorPropWidget::updateColorPreview() +{ + if (prop) { + static_cast(colorPreview)->color = prop->value; + colorPreview->update(); + } +} + +bool ColorPropWidget::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == colorPreview && event->type() == QEvent::MouseButtonPress) { + auto picker = new ColorPicker(); + picker->setColor(prop->value); + + // Position dialog below the colorPreview widget + QPoint globalPos = colorPreview->mapToGlobal(QPoint(0, 0)); + picker->move(globalPos.x(), globalPos.y() + colorPreview->height()); + + connect(picker, &ColorPicker::onColorChanged, this, + [this](const QColor& color) { + if (prop) { + prop->value = color; + updateColorPreview(); + emit valueChanged(color); // signal value changed + } + }); + connect(picker, &ColorPicker::onClosed, picker, + &ColorPicker::deleteLater); + + picker->show(); + + return true; + } + return QWidget::eventFilter(obj, event); +} + +// GRADIENT PROP WIDGET +GradientPropWidget::GradientPropWidget() +{ + prop = nullptr; + + auto vlayout = new QVBoxLayout(this); + this->setLayout(vlayout); + + // label + label = new QLabel(this); + label->setText(""); + vlayout->addWidget(label); + + // gradient preview + gradientPreview = new QWidget(this); + gradientPreview->setFixedHeight(20); + gradientPreview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + gradientPreview->setCursor(Qt::PointingHandCursor); + gradientPreview->installEventFilter(this); + vlayout->addWidget(gradientPreview); + + this->setFixedHeight(80); +} + +void GradientPropWidget::setProp(GradientProp* prop) +{ + label->setText(prop->displayName); + this->prop = prop; + updateGradientPreview(); +} + +void GradientPropWidget::updateGradientPreview() +{ + if (prop) { + // Create a gradient preview using QLinearGradient + QLinearGradient gradient(0, 0, 1, 0); + gradient.setCoordinateMode(QGradient::StretchToDeviceMode); + for (const auto& point : prop->value.points) { + gradient.setColorAt(point.position, point.color); + } + + QPalette palette; + QPixmap pixmap(gradientPreview->width(), gradientPreview->height()); + QPainter painter(&pixmap); + painter.fillRect(pixmap.rect(), gradient); + painter.end(); + + QString styleSheet = QString("border: 1px solid #888;"); + gradientPreview->setStyleSheet(styleSheet); // theme-exempt: dynamic gradient-data swatch + + // Set as background using palette + palette.setBrush(gradientPreview->backgroundRole(), QBrush(pixmap)); + gradientPreview->setAutoFillBackground(true); + gradientPreview->setPalette(palette); + } +} + +bool GradientPropWidget::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == gradientPreview && event->type() == QEvent::MouseButtonPress) { + auto picker = new GradientPickerDialog(); + Gradient initialGradient = prop->value; + picker->setGradient(initialGradient); + + // Position dialog below the gradientPreview widget + QPoint globalPos = gradientPreview->mapToGlobal(QPoint(0, 0)); + picker->move(globalPos.x(), globalPos.y() + gradientPreview->height()); + + connect(picker, &GradientPickerDialog::onGradientChanged, this, + [this](const Gradient& gradient) { + if (prop) { + prop->value = gradient; + updateGradientPreview(); + emit valueChanged(gradient); // signal value changed + } + }); + + connect(picker, &GradientPickerDialog::onGradientAccepted, this, + [this](const Gradient& gradient) { + if (prop) { + prop->value = gradient; + updateGradientPreview(); + emit valueChanged(gradient); // signal value changed + } + }); + + connect(picker, &QDialog::rejected, this, [this, initialGradient]() { + if (prop) { + prop->value = initialGradient; + updateGradientPreview(); + emit valueChanged(initialGradient); // revert to initial value + } + }); + + picker->exec(); + delete picker; + + return true; + } + return QWidget::eventFilter(obj, event); +} + +void GradientPropWidget::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + updateGradientPreview(); +} + +ImagePropWidget::ImagePropWidget() +{ + prop = nullptr; + + auto vlayout = new QVBoxLayout(this); + this->setLayout(vlayout); + + // label + label = new QLabel(this); + label->setText(""); + vlayout->addWidget(label); + + // image preview + imagePreview = new QLabel(this); + imagePreview->setFixedHeight(100); + imagePreview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + imagePreview->setAlignment(Qt::AlignCenter); + imagePreview->setCursor(Qt::PointingHandCursor); + imagePreview->setObjectName("ImagePreview"); // styled in app.qss.in + imagePreview->setText("Click to select image"); + imagePreview->setScaledContents(false); + imagePreview->installEventFilter(this); + vlayout->addWidget(imagePreview); + + // clear button + clearButton = new QPushButton("Clear Image", this); + vlayout->addWidget(clearButton); + + connect(clearButton, &QPushButton::clicked, [this]() { + if (prop) { + prop->value = QImage(); + filePath.clear(); + updateImagePreview(); + emit valueChanged(QImage()); + } + }); + + this->setFixedHeight(180); +} + +void ImagePropWidget::setProp(ImageProp* prop) +{ + label->setText(prop->displayName); + this->prop = prop; + updateImagePreview(); +} + +void ImagePropWidget::updateImagePreview() +{ + if (prop && !prop->value.isNull()) { + QPixmap pixmap = QPixmap::fromImage(prop->value); + imagePreview->setPixmap(pixmap.scaled(imagePreview->size(), + Qt::KeepAspectRatio, + Qt::SmoothTransformation)); + } + else { + imagePreview->setPixmap(QPixmap()); + imagePreview->setText("Click to select image"); + } +} + +bool ImagePropWidget::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == imagePreview && event->type() == QEvent::MouseButtonPress) { + QString fileName = QFileDialog::getOpenFileName( + this, "Select Image", + filePath.isEmpty() ? QDir::homePath() + : QFileInfo(filePath).absolutePath(), + "Image Files (*.png *.jpg *.jpeg *.bmp *.tiff *.tga *.webp);;All " + "Files (*)"); + + if (!fileName.isEmpty()) { + filePath = fileName; + QImage image(fileName); + if (!image.isNull()) { + if (image.format() != QImage::Format_RGBA8888) + image = image.convertToFormat(QImage::Format_RGBA8888); + if (prop) { + prop->value = image; + updateImagePreview(); + emit valueChanged(image); + } + } + } + + return true; + } + return QWidget::eventFilter(obj, event); +} \ No newline at end of file diff --git a/src/texturelab/widgets/properties/propwidgets.h b/src/texturelab/widgets/properties/propwidgets.h new file mode 100644 index 00000000..26e6a3f0 --- /dev/null +++ b/src/texturelab/widgets/properties/propwidgets.h @@ -0,0 +1,164 @@ +#include "../../colorpicker/gradient.h" +#include +#include + +class QLabel; +class QSlider; +class QDoubleSpinBox; +class QSpinBox; +class QComboBox; +class QPushButton; +class QLineEdit; +class QPlainTextEdit; + +struct FloatProp; +struct IntProp; +struct EnumProp; +struct BoolProp; +struct ColorProp; +struct ImageProp; +struct GradientProp; +class StringProp; +class Gradient; + +// https://stackoverflow.com/a/19007951 +class FloatPropWidget : public QWidget { + Q_OBJECT + + QLabel* label; + QSlider* slider; + QDoubleSpinBox* spinbox; + + FloatProp* prop; + bool updating; + +public: + FloatPropWidget(); + void setProp(FloatProp* prop); +signals: + void valueChanged(float); +}; + +class IntPropWidget : public QWidget { + Q_OBJECT + + QLabel* label; + QSlider* slider; + QSpinBox* spinbox; + + IntProp* prop; + bool updating; + +public: + IntPropWidget(); + void setProp(IntProp* prop); +signals: + void valueChanged(long); +}; + +class EnumPropWidget : public QWidget { + Q_OBJECT + + QLabel* label; + QComboBox* comboBox; + + EnumProp* prop; + +public: + EnumPropWidget(); + void setProp(EnumProp* prop); +signals: + void valueChanged(int); +}; + +class StringPropWidget : public QWidget { + Q_OBJECT + + QLabel* label; + QLineEdit* lineEdit; + QPlainTextEdit* textEdit; + + StringProp* prop; + +public: + StringPropWidget(); + void setProp(StringProp* prop); + void setMultiline(bool multiline); +signals: + void valueChanged(QString); +}; + +class BoolPropWidget : public QWidget { + Q_OBJECT + + QLabel* label; + QPushButton* button; + + BoolProp* prop; + bool value; + + void setValue(bool value); + +public: + BoolPropWidget(); + void setProp(BoolProp* prop); +signals: + void valueChanged(bool); +}; + +class ColorPropWidget : public QWidget { + Q_OBJECT + + QLabel* label; + QWidget* colorPreview; + + ColorProp* prop; + + void updateColorPreview(); + +public: + ColorPropWidget(); + void setProp(ColorProp* prop); + bool eventFilter(QObject* obj, QEvent* event) override; +signals: + void valueChanged(QColor); +}; + +class GradientPropWidget : public QWidget { + Q_OBJECT + + QLabel* label; + QWidget* gradientPreview; + + GradientProp* prop; + + void updateGradientPreview(); + +public: + GradientPropWidget(); + void setProp(GradientProp* prop); + bool eventFilter(QObject* obj, QEvent* event) override; + void resizeEvent(QResizeEvent* event) override; +signals: + void valueChanged(Gradient); +}; + +class ImagePropWidget : public QWidget { + Q_OBJECT + + QLabel* label; + QLabel* imagePreview; + QPushButton* clearButton; + + ImageProp* prop; + QString filePath; + + void updateImagePreview(); + +public: + ImagePropWidget(); + void setProp(ImageProp* prop); + bool eventFilter(QObject* obj, QEvent* event) override; +signals: + void valueChanged(QImage); +}; diff --git a/src/texturelab/widgets/view2dwidget.cpp b/src/texturelab/widgets/view2dwidget.cpp new file mode 100644 index 00000000..65733621 --- /dev/null +++ b/src/texturelab/widgets/view2dwidget.cpp @@ -0,0 +1,658 @@ +#include "view2dwidget.h" +#include "thememanager.h" +#include "tokens.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "./graphics/texturerenderer.h" +#include "./models.h" +#include "./utils.h" + +const QColor BackgroundColor(53, 53, 53); +const QColor FineGridColor(60, 60, 60); +const QColor CoarseGridColor(25, 25, 25); + +View2DWidget::View2DWidget() : QMainWindow() +{ + // Create toolbar (compact — see #View2DToolbar in app.qss.in) + toolbar = new QToolBar(this); + toolbar->setObjectName("View2DToolbar"); + toolbar->setMovable(false); + toolbar->setIconSize(QSize(18, 18)); + this->addToolBar(Qt::TopToolBarArea, toolbar); + + // Add save button + QAction* saveAction = + toolbar->addAction(QIcon(":/icons/save.svg"), "Save Texture"); + connect(saveAction, &QAction::triggered, this, + &View2DWidget::saveTextureAsImage); + + // Add copy button + QAction* copyAction = toolbar->addAction(QIcon(":/icons/copy.svg"), + "Copy Texture to Clipboard"); + connect(copyAction, &QAction::triggered, this, + &View2DWidget::copyTextureToClipboard); + + // Add tile toggle button + QAction* tileAction = + toolbar->addAction(QIcon(":/icons/grid.svg"), "Toggle 3x3 Tile View"); + tileAction->setCheckable(true); + connect(tileAction, &QAction::triggered, this, + &View2DWidget::toggleTileView); + + // Add recenter button + QAction* recenterAction = + toolbar->addAction(QIcon(":/icons/crosshair.svg"), "Recenter View"); + connect(recenterAction, &QAction::triggered, this, + &View2DWidget::recenterView); + + graph = new View2DGraph(this); + this->setCentralWidget(graph); +} + +void View2DWidget::setSelectedNode(const TextureNodePtr& node) +{ + this->node = node; + this->graph->setSelectedNode(node); +} + +void View2DWidget::clearSelection() +{ + this->node.reset(); + this->graph->clearSelection(); +} + +void View2DWidget::reRenderNode() +{ + // this->graph->scene()->invalidate(); + this->graph->updatePreview(); +} + +void View2DWidget::setTextureRenderer(TextureRenderer* renderer) +{ + connect(renderer, &TextureRenderer::thumbnailGenerated, + [=](const QString& nodeId, GLint texId, const QPixmap& pixmap) { + if (!!node && node->id == nodeId) { + // this->graph->scene()->invalidate(); + this->graph->updatePreview(); + } + }); +} + +void View2DWidget::saveTextureAsImage() +{ + if (!node) { + return; + } + + QString fileName = QFileDialog::getSaveFileName( + this, tr("Save Texture"), "", tr("PNG Images (*.png);;All Files (*)")); + + if (fileName.isEmpty()) { + return; + } + + // Ensure .png extension + if (!fileName.endsWith(".png", Qt::CaseInsensitive)) { + fileName += ".png"; + } + + // Get the texture ID + GLuint texId = node->textureId(); + if (texId == 0) { + return; + } + + // Bind the texture and get its dimensions + glBindTexture(GL_TEXTURE_2D, texId); + GLint width, height; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); + + // Read texture data + QImage image(width, height, QImage::Format_RGBA8888); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.bits()); + + // Flip image vertically (OpenGL coordinates are bottom-up) + image = image.mirrored(false, true); + + // Save the image + image.save(fileName); +} + +void View2DWidget::toggleTileView() +{ + showTiled = !showTiled; + if (graph && graph->preview) { + graph->preview->setTiled(showTiled); + graph->preview->update(); + } +} + +void View2DWidget::recenterView() +{ + if (graph && graph->preview) { + // Reset transform + graph->resetTransform(); + // Set initial scale + graph->scale(0.3, 0.3); + // Reset scene rect to center on origin + QRectF previewRect = graph->preview->boundingRect(); + graph->setSceneRect(previewRect); + } +} + +void View2DWidget::copyTextureToClipboard() +{ + if (!node) { + return; + } + + // Get the texture ID + GLuint texId = node->textureId(); + if (texId == 0) { + return; + } + + // Bind the texture and get its dimensions + glBindTexture(GL_TEXTURE_2D, texId); + GLint width, height; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); + + // Read texture data + QImage image(width, height, QImage::Format_RGBA8888); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.bits()); + + // Flip image vertically (OpenGL coordinates are bottom-up) + image = image.mirrored(false, true); + + // Copy to clipboard + QClipboard* clipboard = QApplication::clipboard(); + clipboard->setImage(image); + + // Show confirmation toast + showToast("Texture copied to clipboard"); +} + +void View2DWidget::showToast(const QString& message, int duration) +{ + QLabel* toast = new QLabel(message, this); + toast->setObjectName("ViewToast"); // styled in app.qss.in + toast->setAlignment(Qt::AlignCenter); + toast->adjustSize(); + + // Position at bottom center + int x = (width() - toast->width()) / 2; + int y = height() - toast->height() - 50; + toast->move(x, y); + + // Fade in + QGraphicsOpacityEffect* effect = new QGraphicsOpacityEffect(toast); + toast->setGraphicsEffect(effect); + QPropertyAnimation* fadeIn = new QPropertyAnimation(effect, "opacity"); + fadeIn->setDuration(200); + fadeIn->setStartValue(0.0); + fadeIn->setEndValue(1.0); + fadeIn->start(QAbstractAnimation::DeleteWhenStopped); + + toast->show(); + toast->raise(); + + // Fade out and delete + QTimer::singleShot(duration, [toast]() { + QGraphicsOpacityEffect* effect = new QGraphicsOpacityEffect(toast); + toast->setGraphicsEffect(effect); + QPropertyAnimation* fadeOut = new QPropertyAnimation(effect, "opacity"); + fadeOut->setDuration(200); + fadeOut->setStartValue(1.0); + fadeOut->setEndValue(0.0); + fadeOut->start(QAbstractAnimation::DeleteWhenStopped); + QTimer::singleShot(200, toast, &QLabel::deleteLater); + }); +} + +View2DWidget::~View2DWidget() { delete graph; } + +// GRAPH +View2DGraph::View2DGraph(QWidget* parent) : QGraphicsView(parent) +{ + // https://doc.qt.io/qt-6.2/graphicsview.html#opengl-rendering + auto gl = new QOpenGLWidget(); + QSurfaceFormat format; + format.setSamples(4); + gl->setFormat(format); + this->setViewport(gl); + + setDragMode(QGraphicsView::ScrollHandDrag); + setRenderHint(QPainter::Antialiasing); + + setBackgroundBrush(ThemeManager::instance().theme().color(Tokens::View2dBg)); + QObject::connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this, + [this]() { + setBackgroundBrush( + ThemeManager::instance().theme().color(Tokens::View2dBg)); + if (scene()) + scene()->update(); + viewport()->update(); + }); + + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setTransformationAnchor(QGraphicsView::AnchorUnderMouse); + + setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + + _scene = new QGraphicsScene(this); + this->setScene(_scene); + + preview = new NodePreviewGraphicsItem(); + this->_scene->addItem(preview); + // this->fitInView(preview, Qt::KeepAspectRatio); + scale(0.3, 0.3); + preview->hide(); +} + +// view manipulation +void View2DGraph::wheelEvent(QWheelEvent* event) +{ + QPoint delta = event->angleDelta(); + + if (delta.y() == 0) { + event->ignore(); + return; + } + + double const d = delta.y() / std::abs(delta.y()); + + if (d > 0.0) + scaleUp(); + else + scaleDown(); +}; + +void View2DGraph::scaleUp() +{ + double const step = 1.2; + double const factor = std::pow(step, 1.0); + + QTransform t = transform(); + + if (t.m11() > 2.0) + return; + + scale(factor, factor); +} + +void View2DGraph::scaleDown() +{ + double const step = 1.2; + double const factor = std::pow(step, -1.0); + + scale(factor, factor); +} + +// void View2DGraph::keyPressEvent(QKeyEvent* event){}; +// void View2DGraph::keyReleaseEvent(QKeyEvent* event){}; +void View2DGraph::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::MiddleButton) { + _clickPos = event->pos(); + setDragMode(QGraphicsView::NoDrag); + return; + } + QGraphicsView::mousePressEvent(event); +} + +void View2DGraph::mouseMoveEvent(QMouseEvent* event) +{ + if (event->buttons() & Qt::MiddleButton) { + QPointF delta = event->pos() - _clickPos; + qreal s = transform().m11(); + setSceneRect(sceneRect().translated(-delta.x() / s, -delta.y() / s)); + _clickPos = event->pos(); + return; + } + QGraphicsView::mouseMoveEvent(event); +} + +void View2DGraph::mouseReleaseEvent(QMouseEvent* event) +{ + if (event->button() == Qt::MiddleButton) { + setDragMode(QGraphicsView::ScrollHandDrag); + } + QGraphicsView::mouseReleaseEvent(event); +} + +void View2DGraph::setSelectedNode(const TextureNodePtr& node) +{ + this->preview->setNode(node); + this->preview->update(); + // this->scene()->invalidate(); +}; + +void View2DGraph::updatePreview() { this->preview->update(); } + +void View2DGraph::clearSelection() +{ + this->preview->clearNode(); + this->preview->hide(); + this->preview->update(); +}; + +void View2DGraph::drawBackground(QPainter* painter, const QRectF& r) +{ + auto type = painter->paintEngine()->type(); + if (type != QPaintEngine::OpenGL && type != QPaintEngine::OpenGL2) { + qWarning() << "background paint engine needs to be OPENGL!"; + // return; + } + + QGraphicsView::drawBackground(painter, r); + painter->setRenderHint(QPainter::Antialiasing); + + auto drawGrid = [&](double gridStep) { + QRect windowRect = rect(); + QPointF tl = mapToScene(windowRect.topLeft()); + QPointF br = mapToScene(windowRect.bottomRight()); + + double left = std::floor(tl.x() / gridStep - 0.5); + double right = std::floor(br.x() / gridStep + 1.0); + double bottom = std::floor(tl.y() / gridStep - 0.5); + double top = std::floor(br.y() / gridStep + 1.0); + + // vertical lines + for (int xi = int(left); xi <= int(right); ++xi) { + QLineF line(xi * gridStep, bottom * gridStep, xi * gridStep, + top * gridStep); + + painter->drawLine(line); + } + + // horizontal lines + for (int yi = int(bottom); yi <= int(top); ++yi) { + QLineF line(left * gridStep, yi * gridStep, right * gridStep, + yi * gridStep); + painter->drawLine(line); + } + }; + + QBrush bBrush = backgroundBrush(); + + // QPen pfine(FineGridColor, 1.0); + + // painter->setPen(pfine); + // drawGrid(15); + + QPen p(CoarseGridColor, 1.0); + painter->setPen(p); + drawGrid(1000); +}; + +// NODE PREVIEW +NodePreviewGraphicsItem::NodePreviewGraphicsItem() {} + +NodePreviewGraphicsItem::~NodePreviewGraphicsItem() +{ + cleanupGL(); +} + +void NodePreviewGraphicsItem::initializeGL() +{ + if (glInitialized) return; + + QOpenGLContext* ctx = QOpenGLContext::currentContext(); + if (!ctx) return; + + QOpenGLFunctions* f = ctx->functions(); + + // Create shader program + shaderProgram = new QOpenGLShaderProgram(); + + const char* vertexShaderSource = R"( + #version 150 + in vec2 position; + in vec2 texCoord; + out vec2 vTexCoord; + uniform mat4 projectionMatrix; + void main() { + gl_Position = projectionMatrix * vec4(position, 0.0, 1.0); + vTexCoord = texCoord; + } + )"; + + const char* fragmentShaderSource = R"( + #version 150 + in vec2 vTexCoord; + out vec4 fragColor; + uniform sampler2D textureSampler; + uniform vec3 checkerA; + uniform vec3 checkerB; + void main() { + // 16px checkerboard in screen space + vec2 tile = floor(gl_FragCoord.xy / 16.0); + float checker = mod(tile.x + tile.y, 2.0); + vec3 bg = mix(checkerA, checkerB, checker); + + vec4 texColor = texture(textureSampler, vTexCoord); + fragColor = vec4(mix(bg, texColor.rgb, texColor.a), 1.0); + } + )"; + + shaderProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource); + shaderProgram->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource); + shaderProgram->link(); + + // Create VAO and VBO + vao = new QOpenGLVertexArrayObject(); + vao->create(); + + vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->setUsagePattern(QOpenGLBuffer::DynamicDraw); + + glInitialized = true; +} + +void NodePreviewGraphicsItem::cleanupGL() +{ + if (!glInitialized) return; + + delete shaderProgram; + shaderProgram = nullptr; + + if (vbo) { + vbo->destroy(); + delete vbo; + vbo = nullptr; + } + + if (vao) { + vao->destroy(); + delete vao; + vao = nullptr; + } + + glInitialized = false; +} + +QRectF NodePreviewGraphicsItem::boundingRect() const +{ + if (tiled) { + // Center the 3x3 grid around origin + return QRectF(-1500, -1500, 3000, 3000); + } + return QRectF(-500, -500, 1000, 1000); +} + +void NodePreviewGraphicsItem::setNode(const TextureNodePtr& node) +{ + this->node = node; + if (!!node) + this->show(); +} +void NodePreviewGraphicsItem::clearNode() { this->node.reset(); } + +void NodePreviewGraphicsItem::setTiled(bool tiled) +{ + if (this->tiled != tiled) { + prepareGeometryChange(); + this->tiled = tiled; + } +} + +void NodePreviewGraphicsItem::paint(QPainter* painter, + QStyleOptionGraphicsItem const* option, + QWidget* widget) +{ + auto type = painter->paintEngine()->type(); + if (type != QPaintEngine::OpenGL && type != QPaintEngine::OpenGL2) { + qWarning() << "Paint engine needs to be OPENGL!"; + return; + } + + if (!node) return; + + auto rect = boundingRect(); + + painter->beginNativePainting(); + + // Initialize OpenGL resources if needed + initializeGL(); + + if (!glInitialized || !shaderProgram || !vao || !vbo) { + painter->endNativePainting(); + return; + } + + QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); + + // Get the current viewport and create orthographic projection + GLint viewport[4]; + f->glGetIntegerv(GL_VIEWPORT, viewport); + + // Create orthographic projection matrix matching the scene coordinates + // The painter's transform converts scene coords to device coords + QTransform transform = painter->combinedTransform(); + QMatrix4x4 projectionMatrix; + projectionMatrix.ortho(0, viewport[2], viewport[3], 0, -1, 1); + + // Build vertex data + QVector vertices; + + auto addQuad = [&](float x0, float y0, float x1, float y1) { + // Transform scene coordinates to device coordinates + QPointF p0 = transform.map(QPointF(x0, y0)); + QPointF p1 = transform.map(QPointF(x1, y0)); + QPointF p2 = transform.map(QPointF(x1, y1)); + QPointF p3 = transform.map(QPointF(x0, y1)); + + // Two triangles for a quad: position (x,y) + texcoord (u,v) + // Triangle 1 + vertices << p0.x() << p0.y() << 0.0f << 1.0f; + vertices << p1.x() << p1.y() << 1.0f << 1.0f; + vertices << p2.x() << p2.y() << 1.0f << 0.0f; + // Triangle 2 + vertices << p0.x() << p0.y() << 0.0f << 1.0f; + vertices << p2.x() << p2.y() << 1.0f << 0.0f; + vertices << p3.x() << p3.y() << 0.0f << 0.0f; + }; + + if (tiled) { + float tileWidth = rect.width() / 3.0f; + float tileHeight = rect.height() / 3.0f; + + for (int y = 0; y < 3; y++) { + for (int x = 0; x < 3; x++) { + float x0 = rect.x() + x * tileWidth; + float y0 = rect.y() + y * tileHeight; + float x1 = rect.x() + (x + 1) * tileWidth; + float y1 = rect.y() + (y + 1) * tileHeight; + addQuad(x0, y0, x1, y1); + } + } + } else { + addQuad(rect.x(), rect.y(), rect.x() + rect.width(), rect.y() + rect.height()); + } + + // Setup state + f->glDisable(GL_BLEND); + f->glDisable(GL_DEPTH_TEST); + + // Bind shader + shaderProgram->bind(); + shaderProgram->setUniformValue("projectionMatrix", projectionMatrix); + shaderProgram->setUniformValue("textureSampler", 0); + + // Themed checkerboard (matches the node-graph checker); read each paint so it + // follows theme changes / --dev-theme hot-reload. + const Theme& theme = ThemeManager::instance().theme(); + const QColor ca = theme.color(Tokens::CheckerA); + const QColor cb = theme.color(Tokens::CheckerB); + shaderProgram->setUniformValue("checkerA", + QVector3D(ca.redF(), ca.greenF(), ca.blueF())); + shaderProgram->setUniformValue("checkerB", + QVector3D(cb.redF(), cb.greenF(), cb.blueF())); + + // Bind texture + f->glActiveTexture(GL_TEXTURE0); + f->glBindTexture(GL_TEXTURE_2D, node->textureId()); + + // Setup VAO and VBO + vao->bind(); + vbo->bind(); + vbo->allocate(vertices.constData(), vertices.size() * sizeof(GLfloat)); + + // Setup vertex attributes + int positionLoc = shaderProgram->attributeLocation("position"); + int texCoordLoc = shaderProgram->attributeLocation("texCoord"); + + shaderProgram->enableAttributeArray(positionLoc); + shaderProgram->enableAttributeArray(texCoordLoc); + shaderProgram->setAttributeBuffer(positionLoc, GL_FLOAT, 0, 2, 4 * sizeof(GLfloat)); + shaderProgram->setAttributeBuffer(texCoordLoc, GL_FLOAT, 2 * sizeof(GLfloat), 2, 4 * sizeof(GLfloat)); + + // Draw + f->glDrawArrays(GL_TRIANGLES, 0, vertices.size() / 4); + + // Cleanup + shaderProgram->disableAttributeArray(positionLoc); + shaderProgram->disableAttributeArray(texCoordLoc); + vbo->release(); + vao->release(); + shaderProgram->release(); + + f->glEnable(GL_BLEND); + + painter->endNativePainting(); +} \ No newline at end of file diff --git a/src/texturelab/widgets/view2dwidget.h b/src/texturelab/widgets/view2dwidget.h new file mode 100644 index 00000000..8bb7b23f --- /dev/null +++ b/src/texturelab/widgets/view2dwidget.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class QWidget; +class QWheelEvent; +class QKeyEvent; +class QMouseEvent; +class QPainter; +class QWheelEvent; +class QShowEvent; +class QGraphicsScene; +class QStyleOptionGraphicsItem; +class TextureRenderer; + +class TextureProject; +class TextureNode; +typedef QSharedPointer TextureProjectPtr; +typedef QSharedPointer TextureNodePtr; +class View2DGraph; + +class NodePreviewGraphicsItem; + +class View2DWidget : public QMainWindow { + Q_OBJECT + + TextureNodePtr node; + QToolBar* toolbar; + bool showTiled = false; + +public: + View2DWidget(); + + void setSelectedNode(const TextureNodePtr& node); + void clearSelection(); + + void setTextureRenderer(TextureRenderer* renderer); + + void reRenderNode(); + + View2DGraph* graph = nullptr; + + virtual ~View2DWidget(); + +private: + void showToast(const QString& message, int duration = 2000); + +private slots: + void saveTextureAsImage(); + void toggleTileView(); + void recenterView(); + void copyTextureToClipboard(); +}; + +class View2DGraph : public QGraphicsView { + Q_OBJECT + + TextureNodePtr node; + QGraphicsScene* _scene; + +public: + View2DGraph(QWidget* parent); + + void scaleUp(); + void scaleDown(); + + void setSelectedNode(const TextureNodePtr& node); + void updatePreview(); + void clearSelection(); + + NodePreviewGraphicsItem* preview = nullptr; + +protected: + // view manipulation + void wheelEvent(QWheelEvent* event) override; + // void keyPressEvent(QKeyEvent* event) override; + // void keyReleaseEvent(QKeyEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + + void drawBackground(QPainter* painter, const QRectF& r) override; + +private: + QPointF _clickPos; +}; + +class NodePreviewGraphicsItem : public QGraphicsItem { + TextureNodePtr node; + bool tiled = false; + + // Modern OpenGL resources + QOpenGLShaderProgram* shaderProgram = nullptr; + QOpenGLBuffer* vbo = nullptr; + QOpenGLVertexArrayObject* vao = nullptr; + bool glInitialized = false; + +public: + NodePreviewGraphicsItem(); + ~NodePreviewGraphicsItem(); + void setNode(const TextureNodePtr& node); + void clearNode(); + void setTiled(bool tiled); + QRectF boundingRect() const override; + +protected: + void paint(QPainter* painter, QStyleOptionGraphicsItem const* option, + QWidget* widget = 0) override; + +private: + void initializeGL(); + void cleanupGL(); +}; \ No newline at end of file diff --git a/src/texturelab/widgets/view3dwidget.cpp b/src/texturelab/widgets/view3dwidget.cpp new file mode 100644 index 00000000..55bf3c23 --- /dev/null +++ b/src/texturelab/widgets/view3dwidget.cpp @@ -0,0 +1,127 @@ +#include "view3dwidget.h" +#include "viewer3d.h" +#include +#include +#include +#include +#include +#include + +namespace { + +// Available meshes. `modelType` is the id Viewer3D::setModel() switches on. +struct ModelInfo { + QString displayName; + QString modelType; +}; + +const QVector& models() +{ + static const QVector modelList = { + {"Sphere", "sphere"}, + {"Plane (XY)", "plane_xy"}, + {"Plane (YZ)", "plane_yz"}, + {"Plane (XZ)", "plane_xz"}, + {"Cylinder", "cylinder"}, + {"Cube", "cube"}, + {"CubeSphere", "cubesphere"}}; + return modelList; +} + +// Available HDR environments. +// +// `rotation` is a yaw in degrees about the up axis, applied to both the skybox +// and the IBL lookups (Renderer::envRotation). Several of these HDRIs point +// their darkest quarter at the default camera, which sits on -Z, so the model +// opened as a silhouette. Each value was picked by maximising the diffuse +// irradiance on a normal 30 degrees off the camera-facing one, which lands the +// sky's brightest arc behind and to the left of the camera as a key light. +struct EnvInfo { + QString displayName; + QString resourcePath; + float rotation; +}; + +const QVector& environments() +{ + static const QVector envList = { + {"Docklands 01", ":env/docklands_01_1k.hdr", -100.0f}, + {"Golden Bay", ":env/golden_bay_1k.hdr", -85.0f}, + {"Little Paris Eiffel Tower", ":env/little_paris_eiffel_tower_1k.hdr", + -95.0f}, + {"Sepulchral Chapel Basement", ":env/sepulchral_chapel_basement_1k.hdr", + 25.0f}, + {"St Peters Square Night", ":env/st_peters_square_night_1k.hdr", + -95.0f}, + {"Stadium 01", ":env/stadium_01_1k.hdr", -120.0f}, + {"Studio Kontrast 03", ":env/studio_kontrast_03_1k.hdr", 100.0f}, + {"Sunny Rose Garden", ":env/sunny_rose_garden_1k.hdr", -95.0f}, + {"University Workshop", ":env/university_workshop_1k.hdr", 140.0f}, + {"Winter River", ":env/winter_river_1k.hdr", -100.0f}}; + return envList; +} + +// The env the viewer opens on. Kept in sync with kFallbackEnvPath/Rotation in +// viewer3d.cpp, which covers hosts that never call setDefaultEnvironment(). +const EnvInfo& defaultEnvironment() +{ + const QString defaultPath = + QStringLiteral(":env/studio_kontrast_03_1k.hdr"); + for (const EnvInfo& env : environments()) { + if (env.resourcePath == defaultPath) + return env; + } + return environments().first(); +} + +} // namespace + +View3DWidget::View3DWidget() +{ + this->viewer = new Viewer3D(); + this->setCentralWidget(viewer); + + const EnvInfo& defaultEnv = defaultEnvironment(); + this->viewer->setDefaultEnvironment(defaultEnv.resourcePath, + defaultEnv.rotation); + + // Create menu bar + QMenuBar* menuBar = new QMenuBar(this); + this->setMenuBar(menuBar); + + // Model menu. Exclusive like the environment menu below, so the mesh in + // use carries a check mark. The initial check matches the mesh Viewer3D + // builds in its constructor. + QMenu* modelMenu = menuBar->addMenu("Model"); + QActionGroup* modelGroup = new QActionGroup(this); + const QString defaultModelType = QStringLiteral("sphere"); + + for (const ModelInfo& model : models()) { + QAction* modelAction = modelMenu->addAction(model.displayName); + modelAction->setCheckable(true); + modelAction->setChecked(model.modelType == defaultModelType); + modelGroup->addAction(modelAction); + QString modelType = model.modelType; + connect(modelAction, &QAction::triggered, + [this, modelType]() { this->viewer->setModel(modelType); }); + } + + // Environment menu. The entries form an exclusive group so the sky in use + // carries a check mark. + QMenu* envMenu = menuBar->addMenu("Environment"); + QActionGroup* envGroup = new QActionGroup(this); + + for (const EnvInfo& env : environments()) { + QAction* envAction = envMenu->addAction(env.displayName); + envAction->setCheckable(true); + envAction->setChecked(env.resourcePath == defaultEnv.resourcePath); + envGroup->addAction(envAction); + QString path = env.resourcePath; + float rotation = env.rotation; + connect(envAction, &QAction::triggered, [this, path, rotation]() { + this->viewer->loadEnvironment(path, rotation); + }); + } +} + +void View3DWidget::reRender() { this->viewer->reRender(); } \ No newline at end of file diff --git a/src/texturelab/widgets/view3dwidget.h b/src/texturelab/widgets/view3dwidget.h new file mode 100644 index 00000000..e382e3e2 --- /dev/null +++ b/src/texturelab/widgets/view3dwidget.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +class Viewer3D; +class View3DWidget : public QMainWindow { +public: + View3DWidget(); + Viewer3D* viewer; + + void reRender(); +}; \ No newline at end of file diff --git a/src/theme/CMakeLists.txt b/src/theme/CMakeLists.txt new file mode 100644 index 00000000..3fab8498 --- /dev/null +++ b/src/theme/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.10) + +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Widgets REQUIRED) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(THEME_SRCS + theme.cpp + qssbuilder.cpp + thememanager.cpp +) +set(THEME_HEADERS + tokens.h + theme.h + qssbuilder.h + thememanager.h +) + +add_library(theme STATIC ${THEME_SRCS} ${THEME_HEADERS}) + +target_link_libraries(theme PUBLIC Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::Widgets) + +target_include_directories(theme PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +set_target_properties(theme PROPERTIES + AUTOMOC ON + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) diff --git a/src/theme/qssbuilder.cpp b/src/theme/qssbuilder.cpp new file mode 100644 index 00000000..0726c37b --- /dev/null +++ b/src/theme/qssbuilder.cpp @@ -0,0 +1,37 @@ +#include "qssbuilder.h" + +#include "theme.h" + +#include + +QString QssBuilder::build(const QString& templateText, const Theme& theme) +{ + const QHash& vars = theme.qssVars(); + + // Matches {{ token.name }} with optional surrounding whitespace. + static const QRegularExpression re(QStringLiteral("\\{\\{\\s*([^}\\s]+)\\s*\\}\\}")); + + QString out; + out.reserve(templateText.size()); + + qsizetype last = 0; + auto it = re.globalMatch(templateText); + while (it.hasNext()) { + const QRegularExpressionMatch m = it.next(); + out += templateText.mid(last, m.capturedStart() - last); + + const QString key = m.captured(1); + auto found = vars.constFind(key); + if (found != vars.constEnd()) { + out += found.value(); + } + else { + qWarning("QssBuilder: unknown token '{{%s}}' left unsubstituted", + qPrintable(key)); + out += m.captured(0); // leave placeholder so the miss is visible + } + last = m.capturedEnd(); + } + out += templateText.mid(last); + return out; +} diff --git a/src/theme/qssbuilder.h b/src/theme/qssbuilder.h new file mode 100644 index 00000000..ce5478c6 --- /dev/null +++ b/src/theme/qssbuilder.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class Theme; + +// Turns an authored QSS template (with {{token}} placeholders) into a final +// stylesheet string by substituting values from the active theme. +namespace QssBuilder { + +// Substitute every {{token}} in `templateText` with theme.qssVars()[token]. +// Unknown tokens are left as-is and a warning is logged, so a typo is visible +// in the rendered CSS rather than silently blanking a rule. +QString build(const QString& templateText, const Theme& theme); + +} // namespace QssBuilder diff --git a/src/theme/theme.cpp b/src/theme/theme.cpp new file mode 100644 index 00000000..fb13dd04 --- /dev/null +++ b/src/theme/theme.cpp @@ -0,0 +1,11 @@ +#include "theme.h" + +QColor Theme::color(const QString& key) const +{ + auto it = m_colors.constFind(key); + if (it == m_colors.constEnd()) { + qWarning("Theme: unknown color token '%s'", qPrintable(key)); + return QColor(255, 0, 255); // loud magenta = obvious mistake + } + return it.value(); +} diff --git a/src/theme/theme.h b/src/theme/theme.h new file mode 100644 index 00000000..bf4d0f8a --- /dev/null +++ b/src/theme/theme.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +// A resolved, immutable snapshot of one theme. Produced by ThemeManager from a +// theme JSON file (all "@ref" indirection already flattened to concrete values). +// +// Consumers: +// - QssBuilder reads the flat string map (qssVars) for {{token}} substitution. +// - Custom paint code reads typed values via color()/radius()/space()/font(). +// - ThemeManager reads paletteSpec to build a QPalette. +class Theme +{ +public: + Theme() = default; + + // Typed lookups for paint-time code. A missing color returns a loud magenta + // (so a mistyped token is obvious on screen rather than silently black). + QColor color(const QString& key) const; + bool hasColor(const QString& key) const { return m_colors.contains(key); } + int radius(const QString& key) const { return m_ints.value("radius." + key, 0); } + int space(const QString& key) const { return m_ints.value("space." + key, 0); } + int motion(const QString& key) const { return m_ints.value("motion." + key, 0); } + QFont font(const QString& key) const { return m_fonts.value(key); } + + const QString& name() const { return m_name; } + + // Flat name->string map used for QSS {{token}} substitution. Colors are + // "#rrggbb", numbers stringified, font sub-fields as "font.ui.family" etc. + const QHash& qssVars() const { return m_qssVars; } + + // role -> resolved QColor, used to build the QPalette. + const QHash& paletteColors() const { return m_paletteColors; } + +private: + friend class ThemeManager; + + QString m_name; + QHash m_colors; // "bg.window" -> QColor + QHash m_ints; // "radius.sm" / "space.md" / "motion.fast" + QHash m_fonts; // "ui" / "mono" / "title" + QHash m_qssVars; // flat map for template substitution + QHash m_paletteColors; // "window" / "disabled.text" -> QColor +}; diff --git a/src/theme/thememanager.cpp b/src/theme/thememanager.cpp new file mode 100644 index 00000000..6df8db69 --- /dev/null +++ b/src/theme/thememanager.cpp @@ -0,0 +1,272 @@ +#include "thememanager.h" + +#include "qssbuilder.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +// Resolve a JSON string value that may be an "@ref" pointing at another key in +// `colors`. Follows a chain of references with a guard against cycles. +QColor resolveColor(const QString& raw, const QJsonObject& colors) +{ + QString v = raw; + int guard = 0; + while (v.startsWith('@')) { + if (++guard > 32) { + qWarning("ThemeManager: color reference cycle at '%s'", qPrintable(raw)); + return QColor(); + } + const QString ref = v.mid(1); + if (!colors.contains(ref)) { + qWarning("ThemeManager: dangling color reference '@%s'", qPrintable(ref)); + return QColor(); + } + v = colors.value(ref).toString(); + } + QColor c(v); + if (!c.isValid()) + qWarning("ThemeManager: invalid color literal '%s'", qPrintable(v)); + return c; +} + +} // namespace + +ThemeManager& ThemeManager::instance() +{ + static ThemeManager s_instance; + return s_instance; +} + +bool ThemeManager::loadFromResource(const QString& resourcePath) +{ + QFile file(resourcePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("ThemeManager: cannot open theme '%s'", qPrintable(resourcePath)); + return false; + } + + QJsonParseError err{}; + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) { + qWarning("ThemeManager: JSON parse error in '%s': %s", qPrintable(resourcePath), + qPrintable(err.errorString())); + return false; + } + + const QJsonObject root = doc.object(); + const QJsonObject colors = root.value("color").toObject(); + + Theme t; + t.m_name = root.value("meta").toObject().value("name").toString(); + + // --- colors (resolve @refs) --- + for (auto it = colors.constBegin(); it != colors.constEnd(); ++it) { + const QColor c = resolveColor(it.value().toString(), colors); + t.m_colors.insert(it.key(), c); + t.m_qssVars.insert(it.key(), c.name(QColor::HexRgb)); // "#rrggbb" + } + + // --- scalar groups: radius / space / motion --- + const auto loadInts = [&](const char* group) { + const QJsonObject obj = root.value(group).toObject(); + for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { + const int val = it.value().toInt(); + const QString flatKey = QString("%1.%2").arg(group, it.key()); + t.m_ints.insert(flatKey, val); + t.m_qssVars.insert(flatKey, QString::number(val)); + } + }; + loadInts("radius"); + loadInts("space"); + loadInts("motion"); + + // --- fonts --- + const QJsonObject fonts = root.value("font").toObject(); + for (auto it = fonts.constBegin(); it != fonts.constEnd(); ++it) { + const QJsonObject f = it.value().toObject(); + const QString family = f.value("family").toString(); + const int size = f.value("size").toInt(12); + const int weight = f.value("weight").toInt(400); + + QFont font; + // Family may be a CSS-style fallback list; take the first as the primary + // and register the rest as substitute candidates via setFamilies. + QStringList families; + for (const QString& part : family.split(',')) + families << part.trimmed(); + if (!families.isEmpty()) { + font.setFamily(families.first()); + font.setFamilies(families); + } + font.setPixelSize(size); + font.setWeight(QFont::Weight(weight)); + t.m_fonts.insert(it.key(), font); + + t.m_qssVars.insert(QString("font.%1.family").arg(it.key()), family); + t.m_qssVars.insert(QString("font.%1.size").arg(it.key()), QString::number(size)); + t.m_qssVars.insert(QString("font.%1.weight").arg(it.key()), QString::number(weight)); + } + + // --- palette role map --- + const QJsonObject palette = root.value("palette").toObject(); + for (auto it = palette.constBegin(); it != palette.constEnd(); ++it) { + t.m_paletteColors.insert(it.key(), resolveColor(it.value().toString(), colors)); + } + + m_theme = t; + return true; +} + +void ThemeManager::setStyleSheetTemplate(const QString& resourcePath) +{ + QFile file(resourcePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("ThemeManager: cannot open QSS template '%s'", qPrintable(resourcePath)); + m_qssTemplate.clear(); + return; + } + m_qssTemplate = QString::fromUtf8(file.readAll()); +} + +void ThemeManager::setAdsStyleSheetTemplate(const QString& resourcePath) +{ + QFile file(resourcePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("ThemeManager: cannot open ADS QSS template '%s'", qPrintable(resourcePath)); + m_adsTemplate.clear(); + return; + } + m_adsTemplate = QString::fromUtf8(file.readAll()); +} + +QString ThemeManager::adsStyleSheet() const +{ + return QssBuilder::build(m_adsTemplate, m_theme); +} + +QString ThemeManager::appStyleSheet() const +{ + return QssBuilder::build(m_qssTemplate, m_theme); +} + +QPalette ThemeManager::buildPalette() const +{ + const QHash& p = m_theme.paletteColors(); + const auto col = [&](const char* role, QColor fallback) { + return p.value(QString::fromLatin1(role), fallback); + }; + + QPalette pal; + pal.setColor(QPalette::Window, col("window", QColor(53, 53, 53))); + pal.setColor(QPalette::WindowText, col("windowText", Qt::white)); + pal.setColor(QPalette::Base, col("base", QColor(35, 35, 35))); + pal.setColor(QPalette::AlternateBase, col("alternateBase", QColor(53, 53, 53))); + pal.setColor(QPalette::ToolTipBase, col("toolTipBase", QColor(25, 25, 25))); + pal.setColor(QPalette::ToolTipText, col("toolTipText", Qt::white)); + pal.setColor(QPalette::Text, col("text", Qt::white)); + pal.setColor(QPalette::Button, col("button", QColor(53, 53, 53))); + pal.setColor(QPalette::ButtonText, col("buttonText", Qt::white)); + pal.setColor(QPalette::BrightText, col("brightText", Qt::red)); + pal.setColor(QPalette::Link, col("link", QColor(42, 130, 218))); + pal.setColor(QPalette::Highlight, col("highlight", QColor(42, 130, 218))); + pal.setColor(QPalette::HighlightedText, col("highlightedText", Qt::black)); + + const QColor disabled = col("disabled.text", QColor(127, 127, 127)); + pal.setColor(QPalette::Disabled, QPalette::WindowText, + col("disabled.windowText", disabled)); + pal.setColor(QPalette::Disabled, QPalette::Text, disabled); + pal.setColor(QPalette::Disabled, QPalette::ButtonText, + col("disabled.buttonText", disabled)); + pal.setColor(QPalette::Disabled, QPalette::HighlightedText, + col("disabled.highlightedText", disabled)); + pal.setColor(QPalette::Disabled, QPalette::Highlight, + col("disabled.highlight", QColor(80, 80, 80))); + return pal; +} + +void ThemeManager::applyToApplication(QApplication& app) +{ + m_app = &app; + + app.setStyle(QStyleFactory::create("Fusion")); +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + app.styleHints()->setColorScheme(Qt::ColorScheme::Dark); +#endif + + reapply(); +} + +void ThemeManager::reapply() +{ + if (!m_app) + return; + + m_app->setPalette(buildPalette()); + m_app->setStyleSheet(QssBuilder::build(m_qssTemplate, m_theme)); + + emit themeChanged(); +} + +void ThemeManager::enableHotReload(const QString& themeFilePath, const QString& qssFilePath, + const QString& adsFilePath) +{ + m_themePath = themeFilePath; + m_qssPath = qssFilePath; + m_adsPath = adsFilePath; + + if (!m_watcher) { + m_watcher = new QFileSystemWatcher(this); + + // Coalesce bursts of change events (editors often fire several per save) + // into a single reload. + m_reloadTimer = new QTimer(this); + m_reloadTimer->setSingleShot(true); + m_reloadTimer->setInterval(120); + connect(m_reloadTimer, &QTimer::timeout, this, &ThemeManager::reloadFromDisk); + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, + [this](const QString&) { m_reloadTimer->start(); }); + } + + // Initial load from the on-disk source, then start watching. + reloadFromDisk(); +} + +void ThemeManager::reloadFromDisk() +{ + // QFile handles plain filesystem paths as well as ":/..." resources. + const bool ok = loadFromResource(m_themePath); // keeps previous theme if parse fails + setStyleSheetTemplate(m_qssPath); + if (!m_adsPath.isEmpty()) + setAdsStyleSheetTemplate(m_adsPath); + reapply(); // emits themeChanged() -> MainWindow re-applies the ADS sheet + + // --dev-theme feedback. Use fprintf, NOT qInfo/qWarning: the app installs a + // custom Qt message handler that routes logging to Sentry breadcrumbs, which + // would swallow this and defeat the point of a live-tuning loop. + std::fprintf(stderr, "[theme] %s: reloaded from %s\n", + ok ? "ok" : "FAILED (kept previous theme)", qPrintable(m_themePath)); + std::fflush(stderr); + + // Many editors save by writing a temp file and renaming over the original, + // which deletes the inode QFileSystemWatcher was tracking and silently drops + // the watch. Re-add any path the watcher is no longer following. + if (m_watcher) { + const QStringList watched = m_watcher->files(); + for (const QString& p : { m_themePath, m_qssPath, m_adsPath }) { + if (!p.isEmpty() && !watched.contains(p) && QFile::exists(p)) + m_watcher->addPath(p); + } + } +} diff --git a/src/theme/thememanager.h b/src/theme/thememanager.h new file mode 100644 index 00000000..254cb42e --- /dev/null +++ b/src/theme/thememanager.h @@ -0,0 +1,87 @@ +#pragma once + +#include "theme.h" + +#include +#include +#include + +class QApplication; +class QFileSystemWatcher; +class QTimer; + +// Owns the active Theme and applies it to the application. Singleton so paint +// code anywhere can read the current theme and subscribe to themeChanged(). +// +// Phase 0: loads a JSON theme, builds a QPalette identical to the previous +// hand-coded applyDarkTheme(), applies Fusion + palette + (empty) stylesheet. +// Later phases fill in app.qss and thread tokens into custom paint code. +class ThemeManager : public QObject +{ + Q_OBJECT + +public: + static ThemeManager& instance(); + + // Load + resolve a theme JSON (e.g. ":/themes/dark.json"). Returns false and + // keeps the previous theme on parse failure. Does not apply on its own. + bool loadFromResource(const QString& resourcePath); + + // Load the QSS template (e.g. ":/qss/app.qss"). Kept separately so it can be + // re-read on hot-reload without re-parsing the theme. + void setStyleSheetTemplate(const QString& resourcePath); + + // Load the dock-system (ADS) QSS override template. Applied by MainWindow to + // the CDockManager, not to qApp (ADS sets its own sheet on the manager). Kept + // here so it participates in token substitution and hot-reload. + void setAdsStyleSheetTemplate(const QString& resourcePath); + + // Token-substituted ADS override stylesheet. MainWindow appends this to ADS's + // own default sheet. Rebuilds from the current theme on each call. + QString adsStyleSheet() const; + + // The built application stylesheet (same one applied to qApp). MainWindow also + // appends this to the dock-manager sheet: Qt prefers an ancestor widget's + // stylesheet over qApp, so without this the app rules don't reach widgets + // living inside ADS docks (e.g. the properties panel). Rebuilds each call. + QString appStyleSheet() const; + + const Theme& theme() const { return m_theme; } + + // Apply Fusion style, dark color scheme, the built QPalette, and the built + // stylesheet to the given application. Emits themeChanged(). + void applyToApplication(QApplication& app); + + // Rebuild + reapply palette/stylesheet from the current theme (used after a + // hot-reload). No-op if applyToApplication() was never called. + void reapply(); + + QPalette buildPalette() const; + + // Dev convenience: watch the on-disk *source* theme + QSS files and reload + // live on save (no rebuild needed). Pass real filesystem paths, not ":/..." + // resource paths — the compiled-in resources can't be watched. Does an + // initial load from those paths, so in dev the disk files win over the qrc. + void enableHotReload(const QString& themeFilePath, const QString& qssFilePath, + const QString& adsFilePath); + +signals: + void themeChanged(); + +private: + ThemeManager() = default; + + void reloadFromDisk(); + + Theme m_theme; + QString m_qssTemplate; // raw app template text (with {{tokens}}) + QString m_adsTemplate; // raw dock-system (ADS) override template + QApplication* m_app = nullptr; + + // hot-reload (dev only; null unless enableHotReload() was called) + QFileSystemWatcher* m_watcher = nullptr; + QTimer* m_reloadTimer = nullptr; + QString m_themePath; + QString m_qssPath; + QString m_adsPath; +}; diff --git a/src/theme/tokens.h b/src/theme/tokens.h new file mode 100644 index 00000000..ddb87177 --- /dev/null +++ b/src/theme/tokens.h @@ -0,0 +1,75 @@ +#pragma once + +// Canonical token keys. Paint code (node graph, viewports, custom widgets) and +// palette-building should reference these constants instead of hardcoding the +// dotted strings, so a rename is a compile-time break rather than a silent miss. +// +// The string values MUST match the keys under "color" in the theme JSON +// (resources/themes/*.json). +namespace Tokens { + +// --- semantic roles (surface A/B — general chrome) --- +constexpr const char* BgWindow = "bg.window"; +constexpr const char* BgPanel = "bg.panel"; +constexpr const char* BgBase = "bg.base"; +constexpr const char* BgElevated = "bg.elevated"; +constexpr const char* BorderSubtle = "border.subtle"; +constexpr const char* BorderStrong = "border.strong"; +constexpr const char* TextPrimary = "text.primary"; +constexpr const char* TextSecondary = "text.secondary"; +constexpr const char* TextDisabled = "text.disabled"; +constexpr const char* Selection = "selection"; +constexpr const char* Accent = "accent"; + +// --- node graph (surface C) --- +constexpr const char* NodeBg = "node.bg"; +constexpr const char* NodeBorder = "node.border"; +constexpr const char* NodeBorderHover = "node.border.hover"; +constexpr const char* NodeBorderSelect = "node.border.select"; +constexpr const char* NodeTitle = "node.title"; +constexpr const char* NodeChannel = "node.channel"; +constexpr const char* SocketFill = "socket.fill"; +constexpr const char* Wire = "wire"; +constexpr const char* WireDragging = "wire.dragging"; +constexpr const char* WireSelected = "wire.selected"; +constexpr const char* GridBg = "grid.bg"; +constexpr const char* GridFine = "grid.fine"; +constexpr const char* GridCoarse = "grid.coarse"; +constexpr const char* CheckerA = "checker.a"; +constexpr const char* CheckerB = "checker.b"; +constexpr const char* FrameSelect = "frame.select"; +constexpr const char* CommentFill = "comment.fill"; +constexpr const char* CommentText = "comment.text"; + +// --- launcher / project manager (surface B) --- +constexpr const char* LauncherCard = "launcher.card"; +constexpr const char* LauncherCardHover = "launcher.card.hover"; +constexpr const char* LauncherCardBorder = "launcher.card.border"; +constexpr const char* LauncherThumbBg = "launcher.thumb.bg"; +constexpr const char* LauncherStar = "launcher.star"; +constexpr const char* LauncherBadge = "launcher.badge"; +constexpr const char* LauncherPipOff = "launcher.pip.off"; +constexpr const char* LauncherPipOn = "launcher.pip.on"; +constexpr const char* LauncherOpenDot = "launcher.open.dot"; + +// --- 2D viewport (surface B/D) --- +constexpr const char* View2dBg = "view2d.bg"; + +// --- 3D viewport (surface D) --- +constexpr const char* View3dClear = "view3d.clear"; +constexpr const char* View3dGrid = "view3d.grid"; + +// --- curve editor (surface B) --- +constexpr const char* CurveBg = "curve.bg"; +constexpr const char* CurveGrid = "curve.grid"; +constexpr const char* CurveIdentity = "curve.identity"; +constexpr const char* CurveLine = "curve.line"; +constexpr const char* CurveAnchor = "curve.anchor"; +constexpr const char* CurveAnchorHover = "curve.anchor.hover"; +constexpr const char* CurveAnchorSelect = "curve.anchor.select"; +constexpr const char* CurveHandleLine = "curve.handle.line"; +constexpr const char* CurveHandleDot = "curve.handle.dot"; +constexpr const char* CurveHandleHover = "curve.handle.hover"; +constexpr const char* CurveHandleCorner = "curve.handle.corner"; + +} // namespace Tokens diff --git a/src/unobserve.ts b/src/unobserve.ts deleted file mode 100644 index 6b4a4858..00000000 --- a/src/unobserve.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Vue } from "vue-property-decorator"; - -const Observer = new Vue().$data.__ob__.constructor; - -// prevent Vue from auto-binding to large objects -// causes app to crash when cpu-based nodes with large -// arrays get observed -// https://github.com/rpkilby/vue-nonreactive/blob/master/vue-nonreactive.js -export function unobserve(obj): any { - // unobserve pr - if (obj) obj.__ob__ = new Observer({}); - - return obj; -} diff --git a/src/utils/inspectelement.ts b/src/utils/inspectelement.ts deleted file mode 100644 index 138b307f..00000000 --- a/src/utils/inspectelement.ts +++ /dev/null @@ -1,27 +0,0 @@ -const remote = require("@electron/remote"); -const Menu = remote.Menu; -const MenuItem = remote.MenuItem; - -let rightClickPosition = null; -if (process.env.NODE_ENV != "production") { - const menu = new remote.Menu(); - const menuItem = new MenuItem({ - label: "Inspect Element", - click: () => { - remote - .getCurrentWindow() - .webContents.inspectElement(rightClickPosition.x, rightClickPosition.y); - } - }); - menu.append(menuItem); - - window.addEventListener( - "contextmenu", - e => { - e.preventDefault(); - rightClickPosition = { x: e.x, y: e.y }; - menu.popup(); - }, - false - ); -} diff --git a/src/utils/propgroupcache.ts b/src/utils/propgroupcache.ts deleted file mode 100644 index 01a73c44..00000000 --- a/src/utils/propgroupcache.ts +++ /dev/null @@ -1,58 +0,0 @@ -export class PropGroupCache { - // private static _instance; - // public static get instance() { - // if (PropCache._instance == null) PropCache._instance = new PropCache(); - - // return PropCache._instance; - // } - - static states: Map> = new Map< - string, - Map - >(); - - static scrollLevels: Map = new Map(); - - static getCollapseState( - nodeType: string, - groupName: string, - defaultValue: boolean - ): boolean { - const states = PropGroupCache.states; - - if (!states.has(nodeType)) return defaultValue; - - const nodeStates = states.get(nodeType); - - if (!nodeStates.has(groupName)) return defaultValue; - - return nodeStates.get(groupName); - } - - static setCollapseState( - nodeType: string, - groupName: string, - val: boolean - ): void { - const states = PropGroupCache.states; - - let nodeStates: Map = null; - if (states.has(nodeType)) nodeStates = states.get(nodeType); - else nodeStates = new Map(); - - nodeStates.set(groupName, val); - - states.set(nodeType, nodeStates); - } - - static setScrollLevel(nodeName: string, level: number) { - PropGroupCache.scrollLevels.set(nodeName, level); - } - - static getScrollLevel(nodeName: string, defaultLevel: number): number { - if (PropGroupCache.scrollLevels.has(nodeName)) - return PropGroupCache.scrollLevels.get(nodeName); - - return defaultLevel; - } -} diff --git a/src/viewer3d/CMakeLists.txt b/src/viewer3d/CMakeLists.txt new file mode 100644 index 00000000..dddc814d --- /dev/null +++ b/src/viewer3d/CMakeLists.txt @@ -0,0 +1,104 @@ +cmake_minimum_required(VERSION 3.10) + + +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} 5.5 COMPONENTS Core Gui Widgets OpenGLWidgets REQUIRED) +find_package(OpenGL REQUIRED) +if (UNIX AND NOT APPLE) + include_directories(${Qt${QT_VERSION_MAJOR}Gui_PRIVATE_INCLUDE_DIRS}) +endif() + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(SRCS + # main.cpp + viewer3d.cpp + iblsampler.cpp + shadercache.cpp + gltf/gltf.cpp + geometry/sphere.cpp + geometry/plane.cpp + geometry/cylinder.cpp + geometry/cube.cpp + geometry/skydome.cpp + renderer/renderer.cpp +) +set(HEADERS + viewer3d.h + iblsampler.h + shadercache.h + geometry/geometry.h + renderer/renderer.h + gltf/json.hpp + gltf/tiny_gltf.h + gltf/stb_image.h +) + +set(RESOURCES + ./viewerassets.qrc) + +# library +add_library(viewer3d STATIC ${SRCS} ${HEADERS} ${RESOURCES}) +target_link_libraries(viewer3d PRIVATE Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::OpenGLWidgets + Qt${QT_VERSION_MAJOR}::Widgets + OpenGL::GL + theme) +target_include_directories(viewer3d PRIVATE ${CMAKE_SOURCE_DIR}/src/theme) + + +set_target_properties(viewer3d PROPERTIES + AUTOMOC ON + AUTORCC ON + CXX_EXTENSIONS OFF + EXPORT_NAME "3d Viewer" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +if(QT_VERSION_MAJOR STREQUAL "5") + set_target_properties(viewer3d PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON) +elseif(QT_VERSION_MAJOR STREQUAL "6") + set_target_properties(viewer3d PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) +endif() + +# EXE for testing purposes +add_executable(viewer3d_app + ${HEADERS} + ${SRCS} + main.cpp + ${RESOURCES} + ) +target_link_libraries(viewer3d_app PRIVATE Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Gui + Qt${QT_VERSION_MAJOR}::OpenGLWidgets + Qt${QT_VERSION_MAJOR}::Widgets + theme) +target_include_directories(viewer3d_app PRIVATE ${CMAKE_SOURCE_DIR}/src/theme) + + +set_target_properties(viewer3d_app PROPERTIES + AUTOMOC ON + AUTORCC ON + CXX_EXTENSIONS OFF + EXPORT_NAME "Viewer Sample App" + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +if(QT_VERSION_MAJOR STREQUAL "5") + set_target_properties(viewer3d_app PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON) +elseif(QT_VERSION_MAJOR STREQUAL "6") + set_target_properties(viewer3d_app PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) +endif() \ No newline at end of file diff --git a/src/viewer3d/README.md b/src/viewer3d/README.md new file mode 100644 index 00000000..f6a4de53 --- /dev/null +++ b/src/viewer3d/README.md @@ -0,0 +1,21 @@ +reference: +https://github.com/SaschaWillems/Vulkan-glTF-PBR +https://github.com/syoyo/tinygltf/tree/release/examples/glview + +PBR shader reference: +https://github.com/KhronosGroup/glTF-Sample-Renderer/tree/main/source/Renderer/shaders + +original branch: +https://github.com/KhronosGroup/glTF-Sample-Renderer/blob/47a191931461a6f2e14de48d6da0f0eb6ec2d147/source/Renderer/shaders/pbr.frag + +https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Viewer/47a191931461a6f2e14de48d6da0f0eb6ec2d147/source/Renderer/shaders/material_info.glsl + + +Editor that uses same gltf renderer: +- https://www.gltfeditor.com/ + +steps: +load gltf model with tangents +load hdr texture +generate hdr textures +pass textures to shaders diff --git a/src/viewer3d/assets/animation.glsl b/src/viewer3d/assets/animation.glsl new file mode 100644 index 00000000..7b40fcde --- /dev/null +++ b/src/viewer3d/assets/animation.glsl @@ -0,0 +1,211 @@ +#ifdef HAS_MORPH_TARGETS +uniform highp sampler2DArray u_MorphTargetsSampler; +#endif + +#ifdef USE_MORPHING +uniform float u_morphWeights[WEIGHT_COUNT]; +#endif + +#ifdef HAS_JOINTS_0_VEC4 +in vec4 a_joints_0; +#endif + +#ifdef HAS_JOINTS_1_VEC4 +in vec4 a_joints_1; +#endif + +#ifdef HAS_WEIGHTS_0_VEC4 +in vec4 a_weights_0; +#endif + +#ifdef HAS_WEIGHTS_1_VEC4 +in vec4 a_weights_1; +#endif + +#ifdef USE_SKINNING +uniform sampler2D u_jointsSampler; +#endif + +#ifdef USE_SKINNING + +mat4 getMatrixFromTexture(sampler2D s, int index) +{ + mat4 result = mat4(1); + int texSize = textureSize(s, 0)[0]; + int pixelIndex = index * 4; + for (int i = 0; i < 4; ++i) + { + int x = (pixelIndex + i) % texSize; + //Rounding mode of integers is undefined: + //https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf (section 12.33) + int y = (pixelIndex + i - x) / texSize; + result[i] = texelFetch(s, ivec2(x,y), 0); + } + return result; +} + +mat4 getSkinningMatrix() +{ + mat4 skin = mat4(0); + +#if defined(HAS_WEIGHTS_0_VEC4) && defined(HAS_JOINTS_0_VEC4) + skin += + a_weights_0.x * getMatrixFromTexture(u_jointsSampler, int(a_joints_0.x) * 2) + + a_weights_0.y * getMatrixFromTexture(u_jointsSampler, int(a_joints_0.y) * 2) + + a_weights_0.z * getMatrixFromTexture(u_jointsSampler, int(a_joints_0.z) * 2) + + a_weights_0.w * getMatrixFromTexture(u_jointsSampler, int(a_joints_0.w) * 2); +#endif + +#if defined(HAS_WEIGHTS_1_VEC4) && defined(HAS_JOINTS_1_VEC4) + skin += + a_weights_1.x * getMatrixFromTexture(u_jointsSampler, int(a_joints_1.x) * 2) + + a_weights_1.y * getMatrixFromTexture(u_jointsSampler, int(a_joints_1.y) * 2) + + a_weights_1.z * getMatrixFromTexture(u_jointsSampler, int(a_joints_1.z) * 2) + + a_weights_1.w * getMatrixFromTexture(u_jointsSampler, int(a_joints_1.w) * 2); +#endif + if (skin == mat4(0)) { + return mat4(1); + } + return skin; +} + + +mat4 getSkinningNormalMatrix() +{ + mat4 skin = mat4(0); + +#if defined(HAS_WEIGHTS_0_VEC4) && defined(HAS_JOINTS_0_VEC4) + skin += + a_weights_0.x * getMatrixFromTexture(u_jointsSampler, int(a_joints_0.x) * 2 + 1) + + a_weights_0.y * getMatrixFromTexture(u_jointsSampler, int(a_joints_0.y) * 2 + 1) + + a_weights_0.z * getMatrixFromTexture(u_jointsSampler, int(a_joints_0.z) * 2 + 1) + + a_weights_0.w * getMatrixFromTexture(u_jointsSampler, int(a_joints_0.w) * 2 + 1); +#endif + +#if defined(HAS_WEIGHTS_1_VEC4) && defined(HAS_JOINTS_1_VEC4) + skin += + a_weights_1.x * getMatrixFromTexture(u_jointsSampler, int(a_joints_1.x) * 2 + 1) + + a_weights_1.y * getMatrixFromTexture(u_jointsSampler, int(a_joints_1.y) * 2 + 1) + + a_weights_1.z * getMatrixFromTexture(u_jointsSampler, int(a_joints_1.z) * 2 + 1) + + a_weights_1.w * getMatrixFromTexture(u_jointsSampler, int(a_joints_1.w) * 2 + 1); +#endif + if (skin == mat4(0)) { + return mat4(1); + } + return skin; +} + +#endif // !USE_SKINNING + + +#ifdef USE_MORPHING + +#ifdef HAS_MORPH_TARGETS +vec4 getDisplacement(int vertexID, int targetIndex, int texSize) +{ + int x = vertexID % texSize; + //Rounding mode of integers is undefined: + //https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf (section 12.33) + int y = (vertexID - x) / texSize; + return texelFetch(u_MorphTargetsSampler, ivec3(x, y, targetIndex), 0); +} +#endif + + +vec4 getTargetPosition(int vertexID) +{ + vec4 pos = vec4(0); +#ifdef HAS_MORPH_TARGET_POSITION + int texSize = textureSize(u_MorphTargetsSampler, 0)[0]; + for(int i = 0; i < WEIGHT_COUNT; i++) + { + vec4 displacement = getDisplacement(vertexID, MORPH_TARGET_POSITION_OFFSET + i, texSize); + pos += u_morphWeights[i] * displacement; + } +#endif + + return pos; +} + +vec3 getTargetNormal(int vertexID) +{ + vec3 normal = vec3(0); + +#ifdef HAS_MORPH_TARGET_NORMAL + int texSize = textureSize(u_MorphTargetsSampler, 0)[0]; + for(int i = 0; i < WEIGHT_COUNT; i++) + { + vec3 displacement = getDisplacement(vertexID, MORPH_TARGET_NORMAL_OFFSET + i, texSize).xyz; + normal += u_morphWeights[i] * displacement; + } +#endif + + return normal; +} + + +vec3 getTargetTangent(int vertexID) +{ + vec3 tangent = vec3(0); + +#ifdef HAS_MORPH_TARGET_TANGENT + int texSize = textureSize(u_MorphTargetsSampler, 0)[0]; + for(int i = 0; i < WEIGHT_COUNT; i++) + { + vec3 displacement = getDisplacement(vertexID, MORPH_TARGET_TANGENT_OFFSET + i, texSize).xyz; + tangent += u_morphWeights[i] * displacement; + } +#endif + + return tangent; +} + +vec2 getTargetTexCoord0(int vertexID) +{ + vec2 uv = vec2(0); + +#ifdef HAS_MORPH_TARGET_TEXCOORD_0 + int texSize = textureSize(u_MorphTargetsSampler, 0)[0]; + for(int i = 0; i < WEIGHT_COUNT; i++) + { + vec2 displacement = getDisplacement(vertexID, MORPH_TARGET_TEXCOORD_0_OFFSET + i, texSize).xy; + uv += u_morphWeights[i] * displacement; + } +#endif + + return uv; +} + +vec2 getTargetTexCoord1(int vertexID) +{ + vec2 uv = vec2(0); + +#ifdef HAS_MORPH_TARGET_TEXCOORD_1 + int texSize = textureSize(u_MorphTargetsSampler, 0)[0]; + for(int i = 0; i < WEIGHT_COUNT; i++) + { + vec2 displacement = getDisplacement(vertexID, MORPH_TARGET_TEXCOORD_1_OFFSET + i, texSize).xy; + uv += u_morphWeights[i] * displacement; + } +#endif + + return uv; +} + +vec4 getTargetColor0(int vertexID) +{ + vec4 color = vec4(0); + +#ifdef HAS_MORPH_TARGET_COLOR_0 + int texSize = textureSize(u_MorphTargetsSampler, 0)[0]; + for(int i = 0; i < WEIGHT_COUNT; i++) + { + vec4 displacement = getDisplacement(vertexID, MORPH_TARGET_COLOR_0_OFFSET + i, texSize); + color += u_morphWeights[i] * displacement; + } +#endif + + return color; +} + +#endif // !USE_MORPHING \ No newline at end of file diff --git a/src/viewer3d/assets/brdf.glsl b/src/viewer3d/assets/brdf.glsl new file mode 100644 index 00000000..da16c3d3 --- /dev/null +++ b/src/viewer3d/assets/brdf.glsl @@ -0,0 +1,213 @@ +// +// Fresnel +// +// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html +// https://github.com/wdas/brdf/tree/master/src/brdfs +// https://google.github.io/filament/Filament.md.html +// + +// The following equation models the Fresnel reflectance term of the spec equation (aka F()) +// Implementation of fresnel from [4], Equation 15 +vec3 F_Schlick(vec3 f0, vec3 f90, float VdotH) +{ + return f0 + (f90 - f0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0); +} + +float F_Schlick(float f0, float f90, float VdotH) +{ + float x = clamp(1.0 - VdotH, 0.0, 1.0); + float x2 = x * x; + float x5 = x * x2 * x2; + return f0 + (f90 - f0) * x5; +} + +float F_Schlick(float f0, float VdotH) +{ + float f90 = 1.0; //clamp(50.0 * f0, 0.0, 1.0); + return F_Schlick(f0, f90, VdotH); +} + +vec3 F_Schlick(vec3 f0, float f90, float VdotH) +{ + float x = clamp(1.0 - VdotH, 0.0, 1.0); + float x2 = x * x; + float x5 = x * x2 * x2; + return f0 + (f90 - f0) * x5; +} + +vec3 F_Schlick(vec3 f0, float VdotH) +{ + float f90 = 1.0; //clamp(dot(f0, vec3(50.0 * 0.33)), 0.0, 1.0); + return F_Schlick(f0, f90, VdotH); +} + +vec3 Schlick_to_F0(vec3 f, vec3 f90, float VdotH) { + float x = clamp(1.0 - VdotH, 0.0, 1.0); + float x2 = x * x; + float x5 = clamp(x * x2 * x2, 0.0, 0.9999); + + return (f - f90 * x5) / (1.0 - x5); +} + +float Schlick_to_F0(float f, float f90, float VdotH) { + float x = clamp(1.0 - VdotH, 0.0, 1.0); + float x2 = x * x; + float x5 = clamp(x * x2 * x2, 0.0, 0.9999); + + return (f - f90 * x5) / (1.0 - x5); +} + +vec3 Schlick_to_F0(vec3 f, float VdotH) { + return Schlick_to_F0(f, vec3(1.0), VdotH); +} + +float Schlick_to_F0(float f, float VdotH) { + return Schlick_to_F0(f, 1.0, VdotH); +} + + +// Smith Joint GGX +// Note: Vis = G / (4 * NdotL * NdotV) +// see Eric Heitz. 2014. Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs. Journal of Computer Graphics Techniques, 3 +// see Real-Time Rendering. Page 331 to 336. +// see https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/geometricshadowing(specularg) +float V_GGX(float NdotL, float NdotV, float alphaRoughness) +{ + float alphaRoughnessSq = alphaRoughness * alphaRoughness; + + float GGXV = NdotL * sqrt(NdotV * NdotV * (1.0 - alphaRoughnessSq) + alphaRoughnessSq); + float GGXL = NdotV * sqrt(NdotL * NdotL * (1.0 - alphaRoughnessSq) + alphaRoughnessSq); + + float GGX = GGXV + GGXL; + if (GGX > 0.0) + { + return 0.5 / GGX; + } + return 0.0; +} + + +// The following equation(s) model the distribution of microfacet normals across the area being drawn (aka D()) +// Implementation from "Average Irregularity Representation of a Roughened Surface for Ray Reflection" by T. S. Trowbridge, and K. P. Reitz +// Follows the distribution function recommended in the SIGGRAPH 2013 course notes from EPIC Games [1], Equation 3. +float D_GGX(float NdotH, float alphaRoughness) +{ + float alphaRoughnessSq = alphaRoughness * alphaRoughness; + float f = (NdotH * NdotH) * (alphaRoughnessSq - 1.0) + 1.0; + return alphaRoughnessSq / (M_PI * f * f); +} + + +float lambdaSheenNumericHelper(float x, float alphaG) +{ + float oneMinusAlphaSq = (1.0 - alphaG) * (1.0 - alphaG); + float a = mix(21.5473, 25.3245, oneMinusAlphaSq); + float b = mix(3.82987, 3.32435, oneMinusAlphaSq); + float c = mix(0.19823, 0.16801, oneMinusAlphaSq); + float d = mix(-1.97760, -1.27393, oneMinusAlphaSq); + float e = mix(-4.32054, -4.85967, oneMinusAlphaSq); + return a / (1.0 + b * pow(x, c)) + d * x + e; +} + + +float lambdaSheen(float cosTheta, float alphaG) +{ + if (abs(cosTheta) < 0.5) + { + return exp(lambdaSheenNumericHelper(cosTheta, alphaG)); + } + else + { + return exp(2.0 * lambdaSheenNumericHelper(0.5, alphaG) - lambdaSheenNumericHelper(1.0 - cosTheta, alphaG)); + } +} + + +float V_Sheen(float NdotL, float NdotV, float sheenRoughness) +{ + sheenRoughness = max(sheenRoughness, 0.000001); //clamp (0,1] + float alphaG = sheenRoughness * sheenRoughness; + + return clamp(1.0 / ((1.0 + lambdaSheen(NdotV, alphaG) + lambdaSheen(NdotL, alphaG)) * + (4.0 * NdotV * NdotL)), 0.0, 1.0); +} + + +//Sheen implementation------------------------------------------------------------------------------------- +// See https://github.com/sebavan/glTF/tree/KHR_materials_sheen/extensions/2.0/Khronos/KHR_materials_sheen + +// Estevez and Kulla http://www.aconty.com/pdf/s2017_pbs_imageworks_sheen.pdf +float D_Charlie(float sheenRoughness, float NdotH) +{ + sheenRoughness = max(sheenRoughness, 0.000001); //clamp (0,1] + float alphaG = sheenRoughness * sheenRoughness; + float invR = 1.0 / alphaG; + float cos2h = NdotH * NdotH; + float sin2h = 1.0 - cos2h; + return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * M_PI); +} + + +//https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#acknowledgments AppendixB +vec3 BRDF_lambertian(vec3 diffuseColor) +{ + // see https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/ + return (diffuseColor / M_PI); +} + +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#acknowledgments AppendixB +vec3 BRDF_specularGGX(float alphaRoughness, float NdotL, float NdotV, float NdotH) +{ + float Vis = V_GGX(NdotL, NdotV, alphaRoughness); + float D = D_GGX(NdotH, alphaRoughness); + + return vec3(Vis * D); +} + + +#ifdef MATERIAL_ANISOTROPY +// GGX Distribution Anisotropic (Same as Babylon.js) +// https://blog.selfshadow.com/publications/s2012-shading-course/burley/s2012_pbs_disney_brdf_notes_v3.pdf Addenda +float D_GGX_anisotropic(float NdotH, float TdotH, float BdotH, float anisotropy, float at, float ab) +{ + float a2 = at * ab; + vec3 f = vec3(ab * TdotH, at * BdotH, a2 * NdotH); + float w2 = a2 / dot(f, f); + return a2 * w2 * w2 / M_PI; +} + +// GGX Mask/Shadowing Anisotropic (Same as Babylon.js - smithVisibility_GGXCorrelated_Anisotropic) +// Heitz http://jcgt.org/published/0003/02/03/paper.pdf +float V_GGX_anisotropic(float NdotL, float NdotV, float BdotV, float TdotV, float TdotL, float BdotL, float at, float ab) +{ + float GGXV = NdotL * length(vec3(at * TdotV, ab * BdotV, NdotV)); + float GGXL = NdotV * length(vec3(at * TdotL, ab * BdotL, NdotL)); + float v = 0.5 / (GGXV + GGXL); + return clamp(v, 0.0, 1.0); +} + +vec3 BRDF_specularGGXAnisotropy(float alphaRoughness, float anisotropy, vec3 n, vec3 v, vec3 l, vec3 h, vec3 t, vec3 b) +{ + // Roughness along the anisotropy bitangent is the material roughness, while the tangent roughness increases with anisotropy. + float at = mix(alphaRoughness, 1.0, anisotropy * anisotropy); + float ab = clamp(alphaRoughness, 0.001, 1.0); + + float NdotL = clamp(dot(n, l), 0.0, 1.0); + float NdotH = clamp(dot(n, h), 0.001, 1.0); + float NdotV = dot(n, v); + + float V = V_GGX_anisotropic(NdotL, NdotV, dot(b, v), dot(t, v), dot(t, l), dot(b, l), at, ab); + float D = D_GGX_anisotropic(NdotH, dot(t, h), dot(b, h), anisotropy, at, ab); + + return vec3(V * D); +} +#endif + + +// f_sheen +vec3 BRDF_specularSheen(vec3 sheenColor, float sheenRoughness, float NdotL, float NdotV, float NdotH) +{ + float sheenDistribution = D_Charlie(sheenRoughness, NdotH); + float sheenVisibility = V_Sheen(NdotL, NdotV, sheenRoughness); + return sheenColor * sheenDistribution * sheenVisibility; +} diff --git a/src/viewer3d/assets/brick.jpg b/src/viewer3d/assets/brick.jpg new file mode 100644 index 00000000..4889f518 Binary files /dev/null and b/src/viewer3d/assets/brick.jpg differ diff --git a/src/viewer3d/assets/cube.gltf b/src/viewer3d/assets/cube.gltf new file mode 100644 index 00000000..7bac5473 --- /dev/null +++ b/src/viewer3d/assets/cube.gltf @@ -0,0 +1,134 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v3.3.32", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube" + } + ], + "materials" : [ + { + "doubleSided" : true, + "name" : "Material", + "pbrMetallicRoughness" : { + "baseColorFactor" : [ + 0.800000011920929, + 0.800000011920929, + 0.800000011920929, + 1 + ], + "metallicFactor" : 0, + "roughnessFactor" : 0.5 + } + } + ], + "meshes" : [ + { + "name" : "Cube", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TANGENT" : 2, + "TEXCOORD_0" : 3 + }, + "indices" : 4, + "material" : 0 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC4" + }, + { + "bufferView" : 3, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 4, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 384, + "byteOffset" : 576, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 960, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 1152, + "target" : 34963 + } + ], + "buffers" : [ + { + "byteLength" : 1224, + "uri" : "data:application/octet-stream;base64,AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIA/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAPwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAPwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIC/AACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAAAAAAIA/AACAPwAAAAAAAACAAACAvwAAgD8AAAAAAAAAgAAAgD8AAAAAAAAAAAAAgL8AAIA/AACAPwAAAAAAAACAAACAvwAAgD8AAAAAAAAAgAAAgL8AAAAAAAAAAAAAgL8AAIA/AACAPwAAAAAAAACAAACAPwAAgD8AAAAAAAAAgAAAgD8AAAAAAAAAAAAAgL8AAIA/AACAPwAAAAAAAACAAACAvwAAgD8AAAAAAAAAgAAAgD8AAAAAAAAAAAAAgL8AAIA/AAAAAAAAAAAAAIC/AACAvwAAgD8AAAAAAAAAgAAAgL8AAIA/AAAAAAAAAIAAAIA/AAAAAAAAAAAAAIC/AACAvwAAgD8AAAAAAAAAgAAAgL8AAIA/AAAAAAAAAIAAAIC/AAAAAAAAAAAAAIC/AACAvwAAgD8AAAAAAAAAgAAAgD8AAIA/AAAAAAAAAIAAAIA/AAAAAAAAAAAAAIC/AACAvwAAgD8AAAAAAAAAgAAAgL8AAIA/AAAAAAAAAIAAAIA/AACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AACAPwAAgD8AAIA/AACAPwAAAAAAAIA/AACAPwAAAAAAAAAAAACAPwAAgD8AAIA/AACAPwAAAAAAAIA/AACAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AQAOABQAAQAUAAcACgAGABMACgATABcAFQASAAwAFQAMAA8AEAADAAkAEAAJABYABQACAAgABQAIAAsAEQANAAAAEQAAAAQA" + } + ] +} diff --git a/src/viewer3d/assets/fullscreen.vert b/src/viewer3d/assets/fullscreen.vert new file mode 100644 index 00000000..a6e21156 --- /dev/null +++ b/src/viewer3d/assets/fullscreen.vert @@ -0,0 +1,19 @@ +in vec3 a_pos; +in vec2 a_texCoord; + +out vec2 v_texCoord; + +void main() +{ + //v_texCoord = a_texCoord*vec2(1,1); + // v_texCoord = (a_texCoord - vec2(0.5)) * vec2(2.0); + + // v_texCoord = a_texCoord * vec2(2.0); + // gl_Position = vec4(a_pos,1); + + float x = float((gl_VertexID & 1) << 2); + float y = float((gl_VertexID & 2) << 1); + v_texCoord.x = x * 0.5; + v_texCoord.y = y * 0.5; + gl_Position = vec4(x - 1.0, y - 1.0, 0, 1); +} \ No newline at end of file diff --git a/src/viewer3d/assets/functions.glsl b/src/viewer3d/assets/functions.glsl new file mode 100644 index 00000000..b9179a0f --- /dev/null +++ b/src/viewer3d/assets/functions.glsl @@ -0,0 +1,93 @@ +const float M_PI = 3.141592653589793; + + +in vec3 v_Position; + + +#ifdef HAS_NORMAL_VEC3 +#ifdef HAS_TANGENT_VEC4 +in mat3 v_TBN; +#else +in vec3 v_Normal; +#endif +#endif + + +#ifdef HAS_COLOR_0_VEC3 +in vec3 v_Color; +#endif +#ifdef HAS_COLOR_0_VEC4 +in vec4 v_Color; +#endif + + +vec4 getVertexColor() +{ + vec4 color = vec4(1.0); + +#ifdef HAS_COLOR_0_VEC3 + color.rgb = v_Color.rgb; +#endif +#ifdef HAS_COLOR_0_VEC4 + color = v_Color; +#endif + + return color; +} + + +struct NormalInfo { + vec3 ng; // Geometry normal + vec3 t; // Geometry tangent + vec3 b; // Geometry bitangent + vec3 n; // Shading normal + vec3 ntex; // Normal from texture, scaling is accounted for. +}; + + +float clampedDot(vec3 x, vec3 y) +{ + return clamp(dot(x, y), 0.0, 1.0); +} + + +float max3(vec3 v) +{ + return max(max(v.x, v.y), v.z); +} + + +float sq(float t) +{ + return t * t; +} + +vec2 sq(vec2 t) +{ + return t * t; +} + +vec3 sq(vec3 t) +{ + return t * t; +} + +vec4 sq(vec4 t) +{ + return t * t; +} + + +float applyIorToRoughness(float roughness, float ior) +{ + // Scale roughness with IOR so that an IOR of 1.0 results in no microfacet refraction and + // an IOR of 1.5 results in the default amount of microfacet refraction. + return roughness * clamp(ior * 2.0 - 2.0, 0.0, 1.0); +} + +vec3 rgb_mix(vec3 base, vec3 layer, vec3 rgb_alpha) +{ + float rgb_alpha_max = max(rgb_alpha.r, max(rgb_alpha.g, rgb_alpha.b)); + return (1.0 - rgb_alpha_max) * base + rgb_alpha * layer; +} + diff --git a/src/viewer3d/assets/ibl.glsl b/src/viewer3d/assets/ibl.glsl new file mode 100644 index 00000000..1e750838 --- /dev/null +++ b/src/viewer3d/assets/ibl.glsl @@ -0,0 +1,160 @@ +uniform float u_EnvIntensity; + +vec3 getDiffuseLight(vec3 n) +{ + vec4 textureSample = texture(u_LambertianEnvSampler, u_EnvRotation * n); + textureSample.rgb *= u_EnvIntensity; + return textureSample.rgb; +} + + +vec4 getSpecularSample(vec3 reflection, float lod) +{ + vec4 textureSample = textureLod(u_GGXEnvSampler, u_EnvRotation * reflection, lod); + textureSample.rgb *= u_EnvIntensity; + return textureSample; +} + + +vec4 getSheenSample(vec3 reflection, float lod) +{ + vec4 textureSample = textureLod(u_CharlieEnvSampler, u_EnvRotation * reflection, lod); + textureSample.rgb *= u_EnvIntensity; + return textureSample; +} + +vec3 getIBLGGXFresnel(vec3 n, vec3 v, float roughness, vec3 F0, float specularWeight) +{ + // see https://bruop.github.io/ibl/#single_scattering_results at Single Scattering Results + // Roughness dependent fresnel, from Fdez-Aguera + float NdotV = clampedDot(n, v); + vec2 brdfSamplePoint = clamp(vec2(NdotV, roughness), vec2(0.0, 0.0), vec2(1.0, 1.0)); + vec2 f_ab = texture(u_GGXLUT, brdfSamplePoint).rg; + vec3 Fr = max(vec3(1.0 - roughness), F0) - F0; + vec3 k_S = F0 + Fr * pow(1.0 - NdotV, 5.0); + vec3 FssEss = specularWeight * (k_S * f_ab.x + f_ab.y); + + // Multiple scattering, from Fdez-Aguera + float Ems = (1.0 - (f_ab.x + f_ab.y)); + vec3 F_avg = specularWeight * (F0 + (1.0 - F0) / 21.0); + vec3 FmsEms = Ems * FssEss * F_avg / (1.0 - F_avg * Ems); + + return FssEss + FmsEms; +} + +vec3 getIBLRadianceGGX(vec3 n, vec3 v, float roughness) +{ + float NdotV = clampedDot(n, v); + float lod = roughness * float(u_MipCount - 1); + vec3 reflection = normalize(reflect(-v, n)); + vec4 specularSample = getSpecularSample(reflection, lod); + + vec3 specularLight = specularSample.rgb; + + return specularLight; +} + + +#ifdef MATERIAL_TRANSMISSION +vec3 getTransmissionSample(vec2 fragCoord, float roughness, float ior) +{ + float framebufferLod = log2(float(u_TransmissionFramebufferSize.x)) * applyIorToRoughness(roughness, ior); + vec3 transmittedLight = textureLod(u_TransmissionFramebufferSampler, fragCoord.xy, framebufferLod).rgb; + + return transmittedLight; +} +#endif + + +#ifdef MATERIAL_TRANSMISSION +vec3 getIBLVolumeRefraction(vec3 n, vec3 v, float perceptualRoughness, vec3 baseColor, vec3 f0, vec3 f90, + vec3 position, mat4 modelMatrix, mat4 viewMatrix, mat4 projMatrix, float ior, float thickness, vec3 attenuationColor, float attenuationDistance, float dispersion) +{ +#ifdef MATERIAL_DISPERSION + // Dispersion will spread out the ior values for each r,g,b channel + float halfSpread = (ior - 1.0) * 0.025 * dispersion; + vec3 iors = vec3(ior - halfSpread, ior, ior + halfSpread); + + vec3 transmittedLight; + float transmissionRayLength; + for (int i = 0; i < 3; i++) + { + vec3 transmissionRay = getVolumeTransmissionRay(n, v, thickness, iors[i], modelMatrix); + // TODO: taking length of blue ray, ideally we would take the length of the green ray. For now overwriting seems ok + transmissionRayLength = length(transmissionRay); + vec3 refractedRayExit = position + transmissionRay; + + // Project refracted vector on the framebuffer, while mapping to normalized device coordinates. + vec4 ndcPos = projMatrix * viewMatrix * vec4(refractedRayExit, 1.0); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + + // Sample framebuffer to get pixel the refracted ray hits for this color channel. + transmittedLight[i] = getTransmissionSample(refractionCoords, perceptualRoughness, iors[i])[i]; + } +#else + vec3 transmissionRay = getVolumeTransmissionRay(n, v, thickness, ior, modelMatrix); + float transmissionRayLength = length(transmissionRay); + vec3 refractedRayExit = position + transmissionRay; + + // Project refracted vector on the framebuffer, while mapping to normalized device coordinates. + vec4 ndcPos = projMatrix * viewMatrix * vec4(refractedRayExit, 1.0); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + + // Sample framebuffer to get pixel the refracted ray hits. + vec3 transmittedLight = getTransmissionSample(refractionCoords, perceptualRoughness, ior); + +#endif // MATERIAL_DISPERSION + vec3 attenuatedColor = applyVolumeAttenuation(transmittedLight, transmissionRayLength, attenuationColor, attenuationDistance); + + // Sample GGX LUT to get the specular component. + float NdotV = clampedDot(n, v); + vec2 brdfSamplePoint = clamp(vec2(NdotV, perceptualRoughness), vec2(0.0, 0.0), vec2(1.0, 1.0)); + vec2 brdf = texture(u_GGXLUT, brdfSamplePoint).rg; + vec3 specularColor = f0 * brdf.x + f90 * brdf.y; + + return (1.0 - specularColor) * attenuatedColor * baseColor; +} +#endif + + +#ifdef MATERIAL_ANISOTROPY +vec3 getIBLRadianceAnisotropy(vec3 n, vec3 v, float roughness, float anisotropy, vec3 anisotropyDirection) +{ + float NdotV = clampedDot(n, v); + + float tangentRoughness = mix(roughness, 1.0, anisotropy * anisotropy); + vec3 anisotropicTangent = cross(anisotropyDirection, v); + vec3 anisotropicNormal = cross(anisotropicTangent, anisotropyDirection); + float bendFactor = 1.0 - anisotropy * (1.0 - roughness); + float bendFactorPow4 = bendFactor * bendFactor * bendFactor * bendFactor; + vec3 bentNormal = normalize(mix(anisotropicNormal, n, bendFactorPow4)); + + float lod = roughness * float(u_MipCount - 1); + vec3 reflection = normalize(reflect(-v, bentNormal)); + + vec4 specularSample = getSpecularSample(reflection, lod); + + vec3 specularLight = specularSample.rgb; + + return specularLight; +} +#endif + + +vec3 getIBLRadianceCharlie(vec3 n, vec3 v, float sheenRoughness, vec3 sheenColor) +{ + float NdotV = clampedDot(n, v); + float lod = sheenRoughness * float(u_MipCount - 1); + vec3 reflection = normalize(reflect(-v, n)); + + vec2 brdfSamplePoint = clamp(vec2(NdotV, sheenRoughness), vec2(0.0, 0.0), vec2(1.0, 1.0)); + float brdf = texture(u_CharlieLUT, brdfSamplePoint).b; + vec4 sheenSample = getSheenSample(reflection, lod); + + vec3 sheenLight = sheenSample.rgb; + return sheenLight * sheenColor * brdf; +} diff --git a/src/viewer3d/assets/ibl_filtering.frag b/src/viewer3d/assets/ibl_filtering.frag new file mode 100644 index 00000000..5be79442 --- /dev/null +++ b/src/viewer3d/assets/ibl_filtering.frag @@ -0,0 +1,472 @@ +//#version 450 +//#extension GL_ARB_separate_shader_objects : enable + +//precision highp float; +#define MATH_PI 3.1415926535897932384626433832795 +//#define MATH_INV_PI (1.0 / MATH_PI) + +uniform samplerCube uCubeMap; + +// enum +const int cLambertian = 0; +const int cGGX = 1; +const int cCharlie = 2; + + +//layout(push_constant) uniform FilterParameters { +uniform float u_roughness; +uniform int u_sampleCount; +uniform int u_width; +uniform float u_lodBias; +uniform int u_distribution; // enum +uniform int u_currentFace; +uniform int u_isGeneratingLUT; + +// 0: Byte Target Texture (normalized) +// 1: Float Target Texture +uniform int u_floatTexture; + +uniform float u_intensityScale; + +//layout (location = 0) in vec2 inUV; +in vec2 v_texCoord; + + +out vec4 fragmentColor; + +//layout(location = 6) out vec3 outLUT; + + +vec3 uvToXYZ(int face, vec2 uv) +{ + if(face == 0) + return vec3( 1.f, uv.y, -uv.x); + + else if(face == 1) + return vec3( -1.f, uv.y, uv.x); + + else if(face == 2) + return vec3( +uv.x, -1.f, +uv.y); + + else if(face == 3) + return vec3( +uv.x, 1.f, -uv.y); + + else if(face == 4) + return vec3( +uv.x, uv.y, 1.f); + + else {//if(face == 5) + return vec3( -uv.x, +uv.y, -1.f);} +} + +vec2 dirToUV(vec3 dir) +{ + return vec2( + 0.5f + 0.5f * atan(dir.z, dir.x) / MATH_PI, + 1.f - acos(dir.y) / MATH_PI); +} + +float saturate(float v) +{ + return clamp(v, 0.0f, 1.0f); +} + +// Hammersley Points on the Hemisphere +// CC BY 3.0 (Holger Dammertz) +// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html +// with adapted interface +float radicalInverse_VdC(uint bits) +{ + bits = (bits << 16u) | (bits >> 16u); + bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); + bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); + bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); + bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); + return float(bits) * 2.3283064365386963e-10; // / 0x100000000 +} + +// hammersley2d describes a sequence of points in the 2d unit square [0,1)^2 +// that can be used for quasi Monte Carlo integration +vec2 hammersley2d(int i, int N) { + return vec2(float(i)/float(N), radicalInverse_VdC(uint(i))); +} + +// Hemisphere Sample + +// TBN generates a tangent bitangent normal coordinate frame from the normal +// (the normal must be normalized) +mat3 generateTBN(vec3 normal) +{ + vec3 bitangent = vec3(0.0, 1.0, 0.0); + + float NdotUp = dot(normal, vec3(0.0, 1.0, 0.0)); + float epsilon = 0.0000001; + if (1.0 - abs(NdotUp) <= epsilon) + { + // Sampling +Y or -Y, so we need a more robust bitangent. + if (NdotUp > 0.0) + { + bitangent = vec3(0.0, 0.0, 1.0); + } + else + { + bitangent = vec3(0.0, 0.0, -1.0); + } + } + + vec3 tangent = normalize(cross(bitangent, normal)); + bitangent = cross(normal, tangent); + + return mat3(tangent, bitangent, normal); +} + +struct MicrofacetDistributionSample +{ + float pdf; + float cosTheta; + float sinTheta; + float phi; +}; + +float D_GGX(float NdotH, float roughness) { + float a = NdotH * roughness; + float k = roughness / (1.0 - NdotH * NdotH + a * a); + return k * k * (1.0 / MATH_PI); +} + +// GGX microfacet distribution +// https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.html +// This implementation is based on https://bruop.github.io/ibl/, +// https://www.tobias-franke.eu/log/2014/03/30/notes_on_importance_sampling.html +// and https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch20.html +MicrofacetDistributionSample GGX(vec2 xi, float roughness) +{ + MicrofacetDistributionSample ggx; + + // evaluate sampling equations + float alpha = roughness * roughness; + ggx.cosTheta = saturate(sqrt((1.0 - xi.y) / (1.0 + (alpha * alpha - 1.0) * xi.y))); + ggx.sinTheta = sqrt(1.0 - ggx.cosTheta * ggx.cosTheta); + ggx.phi = 2.0 * MATH_PI * xi.x; + + // evaluate GGX pdf (for half vector) + ggx.pdf = D_GGX(ggx.cosTheta, alpha); + + // Apply the Jacobian to obtain a pdf that is parameterized by l + // see https://bruop.github.io/ibl/ + // Typically you'd have the following: + // float pdf = D_GGX(NoH, roughness) * NoH / (4.0 * VoH); + // but since V = N => VoH == NoH + ggx.pdf /= 4.0; + + return ggx; +} + +// NDF +float D_Ashikhmin(float NdotH, float roughness) +{ + float alpha = roughness * roughness; + // Ashikhmin 2007, "Distribution-based BRDFs" + float a2 = alpha * alpha; + float cos2h = NdotH * NdotH; + float sin2h = 1.0 - cos2h; + float sin4h = sin2h * sin2h; + float cot2 = -cos2h / (a2 * sin2h); + return 1.0 / (MATH_PI * (4.0 * a2 + 1.0) * sin4h) * (4.0 * exp(cot2) + sin4h); +} + +// NDF +float D_Charlie(float sheenRoughness, float NdotH) +{ + sheenRoughness = max(sheenRoughness, 0.000001); //clamp (0,1] + float invR = 1.0 / sheenRoughness; + float cos2h = NdotH * NdotH; + float sin2h = 1.0 - cos2h; + return (2.0 + invR) * pow(sin2h, invR * 0.5) / (2.0 * MATH_PI); +} + + +MicrofacetDistributionSample Charlie(vec2 xi, float roughness) +{ + MicrofacetDistributionSample charlie; + + float alpha = roughness * roughness; + charlie.sinTheta = pow(xi.y, alpha / (2.0*alpha + 1.0)); + charlie.cosTheta = sqrt(1.0 - charlie.sinTheta * charlie.sinTheta); + charlie.phi = 2.0 * MATH_PI * xi.x; + + // evaluate Charlie pdf (for half vector) + charlie.pdf = D_Charlie(alpha, charlie.cosTheta); + + // Apply the Jacobian to obtain a pdf that is parameterized by l + charlie.pdf /= 4.0; + + return charlie; +} + +MicrofacetDistributionSample Lambertian(vec2 xi, float roughness) +{ + MicrofacetDistributionSample lambertian; + + // Cosine weighted hemisphere sampling + // http://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations.html#Cosine-WeightedHemisphereSampling + lambertian.cosTheta = sqrt(1.0 - xi.y); + lambertian.sinTheta = sqrt(xi.y); // equivalent to `sqrt(1.0 - cosTheta*cosTheta)`; + lambertian.phi = 2.0 * MATH_PI * xi.x; + + lambertian.pdf = lambertian.cosTheta / MATH_PI; // evaluation for solid angle, therefore drop the sinTheta + + return lambertian; +} + + +// getImportanceSample returns an importance sample direction with pdf in the .w component +vec4 getImportanceSample(int sampleIndex, vec3 N, float roughness) +{ + // generate a quasi monte carlo point in the unit square [0.1)^2 + vec2 xi = hammersley2d(sampleIndex, u_sampleCount); + + MicrofacetDistributionSample importanceSample; + + // generate the points on the hemisphere with a fitting mapping for + // the distribution (e.g. lambertian uses a cosine importance) + if(u_distribution == cLambertian) + { + importanceSample = Lambertian(xi, roughness); + } + else if(u_distribution == cGGX) + { + // Trowbridge-Reitz / GGX microfacet model (Walter et al) + // https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.html + importanceSample = GGX(xi, roughness); + } + else if(u_distribution == cCharlie) + { + importanceSample = Charlie(xi, roughness); + } + + // transform the hemisphere sample to the normal coordinate frame + // i.e. rotate the hemisphere to the normal direction + vec3 localSpaceDirection = normalize(vec3( + importanceSample.sinTheta * cos(importanceSample.phi), + importanceSample.sinTheta * sin(importanceSample.phi), + importanceSample.cosTheta + )); + mat3 TBN = generateTBN(N); + vec3 direction = TBN * localSpaceDirection; + + return vec4(direction, importanceSample.pdf); +} + +// Mipmap Filtered Samples (GPU Gems 3, 20.4) +// https://developer.nvidia.com/gpugems/gpugems3/part-iii-rendering/chapter-20-gpu-based-importance-sampling +// https://cgg.mff.cuni.cz/~jaroslav/papers/2007-sketch-fis/Final_sap_0073.pdf +float computeLod(float pdf) +{ + // // Solid angle of current sample -- bigger for less likely samples + // float omegaS = 1.0 / (float(u_sampleCount) * pdf); + // // Solid angle of texel + // // note: the factor of 4.0 * MATH_PI + // float omegaP = 4.0 * MATH_PI / (6.0 * float(u_width) * float(u_width)); + // // Mip level is determined by the ratio of our sample's solid angle to a texel's solid angle + // // note that 0.5 * log2 is equivalent to log4 + // float lod = 0.5 * log2(omegaS / omegaP); + + // babylon introduces a factor of K (=4) to the solid angle ratio + // this helps to avoid undersampling the environment map + // this does not appear in the original formulation by Jaroslav Krivanek and Mark Colbert + // log4(4) == 1 + // lod += 1.0; + + // We achieved good results by using the original formulation from Krivanek & Colbert adapted to cubemaps + + // https://cgg.mff.cuni.cz/~jaroslav/papers/2007-sketch-fis/Final_sap_0073.pdf + float lod = 0.5 * log2( 6.0 * float(u_width) * float(u_width) / (float(u_sampleCount) * pdf)); + + + return lod; +} + +vec3 filterColor(vec3 N) +{ + //return textureLod(uCubeMap, N, 3.0).rgb; + vec3 color = vec3(0.f); + float weight = 0.0f; + + for(int i = 0; i < u_sampleCount; ++i) + { + vec4 importanceSample = getImportanceSample(i, N, u_roughness); + + vec3 H = vec3(importanceSample.xyz); + float pdf = importanceSample.w; + + // mipmap filtered samples (GPU Gems 3, 20.4) + float lod = computeLod(pdf); + + // apply the bias to the lod + lod += u_lodBias; + + if(u_distribution == cLambertian) + { + // sample lambertian at a lower resolution to avoid fireflies + vec3 lambertian = textureLod(uCubeMap, H, lod).rgb * u_intensityScale; + + //// the below operations cancel each other out + // lambertian *= NdotH; // lamberts law + // lambertian /= pdf; // invert bias from importance sampling + // lambertian /= MATH_PI; // convert irradiance to radiance https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/ + + color += lambertian; + } + else if(u_distribution == cGGX || u_distribution == cCharlie) + { + // Note: reflect takes incident vector. + vec3 V = N; + vec3 L = normalize(reflect(-V, H)); + float NdotL = dot(N, L); + + if (NdotL > 0.0) + { + if(u_roughness == 0.0) + { + // without this the roughness=0 lod is too high + lod = u_lodBias; + } + vec3 sampleColor = textureLod(uCubeMap, L, lod).rgb * u_intensityScale; + color += sampleColor * NdotL; + weight += NdotL; + } + } + } + + if(weight != 0.0f) + { + color /= weight; + } + else + { + color /= float(u_sampleCount); + } + + return color.rgb ; +} + +// From the filament docs. Geometric Shadowing function +// https://google.github.io/filament/Filament.html#toc4.4.2 +float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) { + float a2 = pow(roughness, 4.0); + float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2); + float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2); + return 0.5 / (GGXV + GGXL); +} + +// https://github.com/google/filament/blob/master/shaders/src/brdf.fs#L136 +float V_Ashikhmin(float NdotL, float NdotV) +{ + return clamp(1.0 / (4.0 * (NdotL + NdotV - NdotL * NdotV)), 0.0, 1.0); +} + +// Compute LUT for GGX distribution. +// See https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf +vec3 LUT(float NdotV, float roughness) +{ + // Compute spherical view vector: (sin(phi), 0, cos(phi)) + vec3 V = vec3(sqrt(1.0 - NdotV * NdotV), 0.0, NdotV); + + // The macro surface normal just points up. + vec3 N = vec3(0.0, 0.0, 1.0); + + // To make the LUT independant from the material's F0, which is part of the Fresnel term + // when substituted by Schlick's approximation, we factor it out of the integral, + // yielding to the form: F0 * I1 + I2 + // I1 and I2 are slighlty different in the Fresnel term, but both only depend on + // NoL and roughness, so they are both numerically integrated and written into two channels. + float A = 0.0; + float B = 0.0; + float C = 0.0; + + for(int i = 0; i < u_sampleCount; ++i) + { + // Importance sampling, depending on the distribution. + vec4 importanceSample = getImportanceSample(i, N, roughness); + vec3 H = importanceSample.xyz; + // float pdf = importanceSample.w; + vec3 L = normalize(reflect(-V, H)); + + float NdotL = saturate(L.z); + float NdotH = saturate(H.z); + float VdotH = saturate(dot(V, H)); + if (NdotL > 0.0) + { + if (u_distribution == cGGX) + { + // LUT for GGX distribution. + + // Taken from: https://bruop.github.io/ibl + // Shadertoy: https://www.shadertoy.com/view/3lXXDB + // Terms besides V are from the GGX PDF we're dividing by. + float V_pdf = V_SmithGGXCorrelated(NdotV, NdotL, roughness) * VdotH * NdotL / NdotH; + float Fc = pow(1.0 - VdotH, 5.0); + A += (1.0 - Fc) * V_pdf; + B += Fc * V_pdf; + C += 0.0; + } + + if (u_distribution == cCharlie) + { + // LUT for Charlie distribution. + float sheenDistribution = D_Charlie(roughness, NdotH); + float sheenVisibility = V_Ashikhmin(NdotL, NdotV); + + A += 0.0; + B += 0.0; + C += sheenVisibility * sheenDistribution * NdotL * VdotH; + } + } + } + + // The PDF is simply pdf(v, h) -> NDF * . + // To parametrize the PDF over l, use the Jacobian transform, yielding to: pdf(v, l) -> NDF * / 4 + // Since the BRDF divide through the PDF to be normalized, the 4 can be pulled out of the integral. + return vec3(4.0 * A, 4.0 * B, 4.0 * 2.0 * MATH_PI * C) / float(u_sampleCount); +} + + + +// entry point +void main() +{ + vec3 color = vec3(0); + + if(u_isGeneratingLUT == 0) + { + vec2 newUV = v_texCoord ; + + newUV = newUV*2.0-1.0; + + vec3 scan = uvToXYZ(u_currentFace, newUV); + + vec3 direction = normalize(scan); + direction.y = -direction.y; + + color = filterColor(direction); + } + else + { + color = LUT(v_texCoord.x, v_texCoord.y); + fragmentColor.rgb = color; + fragmentColor.a = 1.0; + return; + } + + fragmentColor.a = 1.0; + + if(u_floatTexture == 0) + { + float maxV = max(max(color.r,color.g),color.b); + color /= u_intensityScale; + color = clamp(color, 0.0f, 1.0f); + } + + fragmentColor.rgb = color; +} + diff --git a/src/viewer3d/assets/iridiscence.glsl b/src/viewer3d/assets/iridiscence.glsl new file mode 100644 index 00000000..5bf3d797 --- /dev/null +++ b/src/viewer3d/assets/iridiscence.glsl @@ -0,0 +1,100 @@ +// XYZ to sRGB color space +const mat3 XYZ_TO_REC709 = mat3( + 3.2404542, -0.9692660, 0.0556434, + -1.5371385, 1.8760108, -0.2040259, + -0.4985314, 0.0415560, 1.0572252 +); + +// Assume air interface for top +// Note: We don't handle the case fresnel0 == 1 +vec3 Fresnel0ToIor(vec3 fresnel0) { + vec3 sqrtF0 = sqrt(fresnel0); + return (vec3(1.0) + sqrtF0) / (vec3(1.0) - sqrtF0); +} + +// Conversion FO/IOR +vec3 IorToFresnel0(vec3 transmittedIor, float incidentIor) { + return sq((transmittedIor - vec3(incidentIor)) / (transmittedIor + vec3(incidentIor))); +} + +// ior is a value between 1.0 and 3.0. 1.0 is air interface +float IorToFresnel0(float transmittedIor, float incidentIor) { + return sq((transmittedIor - incidentIor) / (transmittedIor + incidentIor)); +} + +// Fresnel equations for dielectric/dielectric interfaces. +// Ref: https://belcour.github.io/blog/research/2017/05/01/brdf-thin-film.html +// Evaluation XYZ sensitivity curves in Fourier space +vec3 evalSensitivity(float OPD, vec3 shift) { + float phase = 2.0 * M_PI * OPD * 1.0e-9; + vec3 val = vec3(5.4856e-13, 4.4201e-13, 5.2481e-13); + vec3 pos = vec3(1.6810e+06, 1.7953e+06, 2.2084e+06); + vec3 var = vec3(4.3278e+09, 9.3046e+09, 6.6121e+09); + + vec3 xyz = val * sqrt(2.0 * M_PI * var) * cos(pos * phase + shift) * exp(-sq(phase) * var); + xyz.x += 9.7470e-14 * sqrt(2.0 * M_PI * 4.5282e+09) * cos(2.2399e+06 * phase + shift[0]) * exp(-4.5282e+09 * sq(phase)); + xyz /= 1.0685e-7; + + vec3 srgb = XYZ_TO_REC709 * xyz; + return srgb; +} + +vec3 evalIridescence(float outsideIOR, float eta2, float cosTheta1, float thinFilmThickness, vec3 baseF0) { + vec3 I; + + // Force iridescenceIor -> outsideIOR when thinFilmThickness -> 0.0 + float iridescenceIor = mix(outsideIOR, eta2, smoothstep(0.0, 0.03, thinFilmThickness)); + // Evaluate the cosTheta on the base layer (Snell law) + float sinTheta2Sq = sq(outsideIOR / iridescenceIor) * (1.0 - sq(cosTheta1)); + + // Handle TIR: + float cosTheta2Sq = 1.0 - sinTheta2Sq; + if (cosTheta2Sq < 0.0) { + return vec3(1.0); + } + + float cosTheta2 = sqrt(cosTheta2Sq); + + // First interface + float R0 = IorToFresnel0(iridescenceIor, outsideIOR); + float R12 = F_Schlick(R0, cosTheta1); + float R21 = R12; + float T121 = 1.0 - R12; + float phi12 = 0.0; + if (iridescenceIor < outsideIOR) phi12 = M_PI; + float phi21 = M_PI - phi12; + + // Second interface + vec3 baseIOR = Fresnel0ToIor(clamp(baseF0, 0.0, 0.9999)); // guard against 1.0 + vec3 R1 = IorToFresnel0(baseIOR, iridescenceIor); + vec3 R23 = F_Schlick(R1, cosTheta2); + vec3 phi23 = vec3(0.0); + if (baseIOR[0] < iridescenceIor) phi23[0] = M_PI; + if (baseIOR[1] < iridescenceIor) phi23[1] = M_PI; + if (baseIOR[2] < iridescenceIor) phi23[2] = M_PI; + + // Phase shift + float OPD = 2.0 * iridescenceIor * thinFilmThickness * cosTheta2; + vec3 phi = vec3(phi21) + phi23; + + // Compound terms + vec3 R123 = clamp(R12 * R23, 1e-5, 0.9999); + vec3 r123 = sqrt(R123); + vec3 Rs = sq(T121) * R23 / (vec3(1.0) - R123); + + // Reflectance term for m = 0 (DC term amplitude) + vec3 C0 = R12 + Rs; + I = C0; + + // Reflectance term for m > 0 (pairs of diracs) + vec3 Cm = Rs - T121; + for (int m = 1; m <= 2; ++m) + { + Cm *= r123; + vec3 Sm = 2.0 * evalSensitivity(float(m) * OPD, float(m) * phi); + I += Cm * Sm; + } + + // Since out of gamut colors might be produced, negative color values are clamped to 0. + return max(I, vec3(0.0)); +} diff --git a/src/viewer3d/assets/material_info.glsl b/src/viewer3d/assets/material_info.glsl new file mode 100644 index 00000000..9b867326 --- /dev/null +++ b/src/viewer3d/assets/material_info.glsl @@ -0,0 +1,452 @@ +// Metallic Roughness +uniform float u_MetallicFactor; +uniform float u_RoughnessFactor; +uniform vec4 u_BaseColorFactor; + +// Specular Glossiness +uniform vec3 u_SpecularFactor; +uniform vec4 u_DiffuseFactor; +uniform float u_GlossinessFactor; + +// Sheen +uniform float u_SheenRoughnessFactor; +uniform vec3 u_SheenColorFactor; + +// Clearcoat +uniform float u_ClearcoatFactor; +uniform float u_ClearcoatRoughnessFactor; + +// Specular +uniform vec3 u_KHR_materials_specular_specularColorFactor; +uniform float u_KHR_materials_specular_specularFactor; + +// Transmission +uniform float u_TransmissionFactor; + +// Volume +uniform float u_ThicknessFactor; +uniform vec3 u_AttenuationColor; +uniform float u_AttenuationDistance; + +// Iridescence +uniform float u_IridescenceFactor; +uniform float u_IridescenceIor; +uniform float u_IridescenceThicknessMinimum; +uniform float u_IridescenceThicknessMaximum; + +// Diffuse Transmission +uniform float u_DiffuseTransmissionFactor; +uniform vec3 u_DiffuseTransmissionColorFactor; + +// Emissive Strength +uniform float u_EmissiveStrength; + +// IOR +uniform float u_Ior; + +// Anisotropy +uniform vec3 u_Anisotropy; + +// Dispersion +uniform float u_Dispersion; + +// Alpha mode +uniform float u_AlphaCutoff; + +uniform vec3 u_Camera; + +#ifdef MATERIAL_TRANSMISSION +uniform ivec2 u_ScreenSize; +#endif + +uniform mat4 u_ModelMatrix; +uniform mat4 u_ViewMatrix; +uniform mat4 u_ProjectionMatrix; + + +struct MaterialInfo +{ + float ior; + float perceptualRoughness; // roughness value, as authored by the model creator (input to shader) + vec3 f0_dielectric; + + float alphaRoughness; // roughness mapped to a more linear change in the roughness (proposed by [2]) + + float fresnel_w; + + vec3 f90; // reflectance color at grazing angle + vec3 f90_dielectric; + float metallic; + + vec3 baseColor; + + float sheenRoughnessFactor; + vec3 sheenColorFactor; + + vec3 clearcoatF0; + vec3 clearcoatF90; + float clearcoatFactor; + vec3 clearcoatNormal; + float clearcoatRoughness; + + // KHR_materials_specular + float specularWeight; // product of specularFactor and specularTexture.a + + float transmissionFactor; + + float thickness; + vec3 attenuationColor; + float attenuationDistance; + + // KHR_materials_iridescence + float iridescenceFactor; + float iridescenceIor; + float iridescenceThickness; + + float diffuseTransmissionFactor; + vec3 diffuseTransmissionColorFactor; + + // KHR_materials_anisotropy + vec3 anisotropicT; + vec3 anisotropicB; + float anisotropyStrength; + + // KHR_materials_dispersion + float dispersion; +}; + + +// Get normal, tangent and bitangent vectors. +NormalInfo getNormalInfo(vec3 v) +{ + vec2 UV = getNormalUV(); + vec2 uv_dx = dFdx(UV); + vec2 uv_dy = dFdy(UV); + + if (length(uv_dx) <= 1e-2) { + uv_dx = vec2(1.0, 0.0); + } + + if (length(uv_dy) <= 1e-2) { + uv_dy = vec2(0.0, 1.0); + } + + vec3 t_ = (uv_dy.t * dFdx(v_Position) - uv_dx.t * dFdy(v_Position)) / + (uv_dx.s * uv_dy.t - uv_dy.s * uv_dx.t); + + vec3 n, t, b, ng; + + // Compute geometrical TBN: +#ifdef HAS_NORMAL_VEC3 +#ifdef HAS_TANGENT_VEC4 + // Trivial TBN computation, present as vertex attribute. + // Normalize eigenvectors as matrix is linearly interpolated. + t = normalize(v_TBN[0]); + b = normalize(v_TBN[1]); + ng = normalize(v_TBN[2]); +#else + // Normals are either present as vertex attributes or approximated. + ng = normalize(v_Normal); + t = normalize(t_ - ng * dot(ng, t_)); + b = cross(ng, t); +#endif +#else + ng = normalize(cross(dFdx(v_Position), dFdy(v_Position))); + t = normalize(t_ - ng * dot(ng, t_)); + b = cross(ng, t); +#endif + +#ifndef NOT_TRIANGLE + // For a back-facing surface, the tangential basis vectors are negated. + if (gl_FrontFacing == false) + { + t *= -1.0; + b *= -1.0; + ng *= -1.0; + } +#endif + + // Compute normals: + NormalInfo info; + info.ng = ng; +#ifdef HAS_NORMAL_MAP + info.ntex = texture(u_NormalSampler, UV).rgb * 2.0 - vec3(1.0); + info.ntex *= vec3(u_NormalScale, u_NormalScale, 1.0); + info.ntex = normalize(info.ntex); + info.n = normalize(mat3(t, b, ng) * info.ntex); +#else + info.n = ng; +#endif + info.t = t; + info.b = b; + return info; +} + + +#ifdef MATERIAL_CLEARCOAT +vec3 getClearcoatNormal(NormalInfo normalInfo) +{ +#ifdef HAS_CLEARCOAT_NORMAL_MAP + vec3 n = texture(u_ClearcoatNormalSampler, getClearcoatNormalUV()).rgb * 2.0 - vec3(1.0); + n *= vec3(u_ClearcoatNormalScale, u_ClearcoatNormalScale, 1.0); + n = mat3(normalInfo.t, normalInfo.b, normalInfo.ng) * normalize(n); + return n; +#else + return normalInfo.ng; +#endif +} +#endif + + +vec4 getBaseColor() +{ + vec4 baseColor = vec4(1); + +#if defined(MATERIAL_SPECULARGLOSSINESS) + baseColor = u_DiffuseFactor; +#elif defined(MATERIAL_METALLICROUGHNESS) + baseColor = u_BaseColorFactor; +#endif + +#if defined(MATERIAL_SPECULARGLOSSINESS) && defined(HAS_DIFFUSE_MAP) + vec4 diffuseMap = texture(u_DiffuseSampler, getDiffuseUV()); + // Base color textures are in sRGB space, convert to linear for PBR calculations + baseColor *= sRGBToLinear(diffuseMap); +#elif defined(MATERIAL_METALLICROUGHNESS) && defined(HAS_BASE_COLOR_MAP) + vec4 baseColorMap = texture(u_BaseColorSampler, getBaseColorUV()); + // Base color textures are in sRGB space, convert to linear for PBR calculations + baseColor *= sRGBToLinear(baseColorMap); + //baseColor *= baseColorMap; +#endif + +#ifdef HAS_ALPHA_MAP + // Separate grayscale alpha/opacity map, independent of the base color map's own alpha. + baseColor.a *= texture(u_AlphaSampler, getAlphaUV()).r; +#endif + + return baseColor * getVertexColor(); +} + + +#ifdef MATERIAL_SPECULARGLOSSINESS +MaterialInfo getSpecularGlossinessInfo(MaterialInfo info) +{ + info.f0_dielectric = u_SpecularFactor; + info.perceptualRoughness = u_GlossinessFactor; + +#ifdef HAS_SPECULAR_GLOSSINESS_MAP + vec4 sgSample = texture(u_SpecularGlossinessSampler, getSpecularGlossinessUV()); + info.perceptualRoughness *= sgSample.a ; // glossiness to roughness + info.f0_dielectric *= sgSample.rgb; // specular +#endif // ! HAS_SPECULAR_GLOSSINESS_MAP + + info.perceptualRoughness = 1.0 - info.perceptualRoughness; // 1 - glossiness + return info; +} +#endif + + +#ifdef MATERIAL_METALLICROUGHNESS +MaterialInfo getMetallicRoughnessInfo(MaterialInfo info) +{ + info.metallic = u_MetallicFactor; + info.perceptualRoughness = u_RoughnessFactor; + +#ifdef HAS_METALLIC_ROUGHNESS_MAP + // Roughness is stored in the 'g' channel, metallic is stored in the 'b' channel. + // This layout intentionally reserves the 'r' channel for (optional) occlusion map data + vec4 mrSample = texture(u_MetallicRoughnessSampler, getMetallicRoughnessUV()); + info.perceptualRoughness *= mrSample.g; + info.metallic *= mrSample.b; +#endif + +// split maps into separate textures (use same UV for them for now) +#ifdef HAS_ROUGHNESS_MAP + vec4 rSample = texture(u_RoughnessSampler, getRoughnessUV()); + info.perceptualRoughness *= rSample.r; +#endif + +#ifdef HAS_METALNESS_MAP + vec4 mSample = texture(u_MetalnessSampler, getMetalnessUV()); + info.metallic *= mSample.r; +#endif + return info; +} +#endif + + +#ifdef MATERIAL_SHEEN +MaterialInfo getSheenInfo(MaterialInfo info) +{ + info.sheenColorFactor = u_SheenColorFactor; + info.sheenRoughnessFactor = u_SheenRoughnessFactor; + +#ifdef HAS_SHEEN_COLOR_MAP + vec4 sheenColorSample = texture(u_SheenColorSampler, getSheenColorUV()); + info.sheenColorFactor *= sheenColorSample.rgb; +#endif + +#ifdef HAS_SHEEN_ROUGHNESS_MAP + vec4 sheenRoughnessSample = texture(u_SheenRoughnessSampler, getSheenRoughnessUV()); + info.sheenRoughnessFactor *= sheenRoughnessSample.a; +#endif + return info; +} +#endif + + +#ifdef MATERIAL_SPECULAR +MaterialInfo getSpecularInfo(MaterialInfo info) +{ + vec4 specularTexture = vec4(1.0); +#ifdef HAS_SPECULAR_MAP + specularTexture.a = texture(u_SpecularSampler, getSpecularUV()).a; +#endif +#ifdef HAS_SPECULAR_COLOR_MAP + specularTexture.rgb = texture(u_SpecularColorSampler, getSpecularColorUV()).rgb; +#endif + + info.f0_dielectric = min(info.f0_dielectric * u_KHR_materials_specular_specularColorFactor * specularTexture.rgb, vec3(1.0)); + info.specularWeight = u_KHR_materials_specular_specularFactor * specularTexture.a; + info.f90_dielectric = vec3(info.specularWeight); + return info; +} +#endif + + +#ifdef MATERIAL_TRANSMISSION +MaterialInfo getTransmissionInfo(MaterialInfo info) +{ + info.transmissionFactor = u_TransmissionFactor; + +#ifdef HAS_TRANSMISSION_MAP + vec4 transmissionSample = texture(u_TransmissionSampler, getTransmissionUV()); + info.transmissionFactor *= transmissionSample.r; +#endif + +#ifdef MATERIAL_DISPERSION + info.dispersion = u_Dispersion; +#else + info.dispersion = 0.0; +#endif + return info; +} +#endif + +#ifdef MATERIAL_VOLUME +MaterialInfo getVolumeInfo(MaterialInfo info) +{ + info.thickness = u_ThicknessFactor; + info.attenuationColor = u_AttenuationColor; + info.attenuationDistance = u_AttenuationDistance; + +#ifdef HAS_THICKNESS_MAP + vec4 thicknessSample = texture(u_ThicknessSampler, getThicknessUV()); + info.thickness *= thicknessSample.g; +#endif + return info; +} +#endif + + +#ifdef MATERIAL_IRIDESCENCE +MaterialInfo getIridescenceInfo(MaterialInfo info) +{ + info.iridescenceFactor = u_IridescenceFactor; + info.iridescenceIor = u_IridescenceIor; + info.iridescenceThickness = u_IridescenceThicknessMaximum; + + #ifdef HAS_IRIDESCENCE_MAP + info.iridescenceFactor *= texture(u_IridescenceSampler, getIridescenceUV()).r; + #endif + + #ifdef HAS_IRIDESCENCE_THICKNESS_MAP + float thicknessSampled = texture(u_IridescenceThicknessSampler, getIridescenceThicknessUV()).g; + float thickness = mix(u_IridescenceThicknessMinimum, u_IridescenceThicknessMaximum, thicknessSampled); + info.iridescenceThickness = thickness; + #endif + + return info; +} +#endif + + +#ifdef MATERIAL_DIFFUSE_TRANSMISSION +MaterialInfo getDiffuseTransmissionInfo(MaterialInfo info) +{ + info.diffuseTransmissionFactor = u_DiffuseTransmissionFactor; + info.diffuseTransmissionColorFactor = u_DiffuseTransmissionColorFactor; + + #ifdef HAS_DIFFUSE_TRANSMISSION_MAP + info.diffuseTransmissionFactor *= texture(u_DiffuseTransmissionSampler, getDiffuseTransmissionUV()).a; + #endif + + #ifdef HAS_DIFFUSE_TRANSMISSION_COLOR_MAP + info.diffuseTransmissionColorFactor *= texture(u_DiffuseTransmissionColorSampler, getDiffuseTransmissionColorUV()).rgb; + #endif + + return info; +} +#endif + + +#ifdef MATERIAL_CLEARCOAT +MaterialInfo getClearCoatInfo(MaterialInfo info, NormalInfo normalInfo) +{ + info.clearcoatFactor = u_ClearcoatFactor; + info.clearcoatRoughness = u_ClearcoatRoughnessFactor; + info.clearcoatF0 = vec3(pow((info.ior - 1.0) / (info.ior + 1.0), 2.0)); + info.clearcoatF90 = vec3(1.0); + +#ifdef HAS_CLEARCOAT_MAP + vec4 clearcoatSample = texture(u_ClearcoatSampler, getClearcoatUV()); + info.clearcoatFactor *= clearcoatSample.r; +#endif + +#ifdef HAS_CLEARCOAT_ROUGHNESS_MAP + vec4 clearcoatSampleRoughness = texture(u_ClearcoatRoughnessSampler, getClearcoatRoughnessUV()); + info.clearcoatRoughness *= clearcoatSampleRoughness.g; +#endif + + info.clearcoatNormal = getClearcoatNormal(normalInfo); + info.clearcoatRoughness = clamp(info.clearcoatRoughness, 0.0, 1.0); + return info; +} +#endif + + +#ifdef MATERIAL_IOR +MaterialInfo getIorInfo(MaterialInfo info) +{ + info.f0_dielectric = vec3(pow(( u_Ior - 1.0) / (u_Ior + 1.0), 2.0)); + info.ior = u_Ior; + return info; +} +#endif + +#ifdef MATERIAL_ANISOTROPY +MaterialInfo getAnisotropyInfo(MaterialInfo info, NormalInfo normalInfo) +{ + vec2 direction = vec2(1.0, 0.0); + float strengthFactor = 1.0; +#ifdef HAS_ANISOTROPY_MAP + vec3 anisotropySample = texture(u_AnisotropySampler, getAnisotropyUV()).xyz; + direction = anisotropySample.xy * 2.0 - vec2(1.0); + strengthFactor = anisotropySample.z; +#endif + vec2 directionRotation = u_Anisotropy.xy; // cos(theta), sin(theta) + mat2 rotationMatrix = mat2(directionRotation.x, directionRotation.y, -directionRotation.y, directionRotation.x); + direction = rotationMatrix * direction.xy; + + info.anisotropicT = mat3(normalInfo.t, normalInfo.b, normalInfo.n) * normalize(vec3(direction, 0.0)); + info.anisotropicB = cross(normalInfo.ng, info.anisotropicT); + info.anisotropyStrength = clamp(u_Anisotropy.z * strengthFactor, 0.0, 1.0); + return info; +} +#endif + + +float albedoSheenScalingLUT(float NdotV, float sheenRoughnessFactor) +{ + return texture(u_SheenELUT, vec2(NdotV, sheenRoughnessFactor)).r; +} diff --git a/src/viewer3d/assets/panorama.hdr b/src/viewer3d/assets/panorama.hdr new file mode 100644 index 00000000..d005ad69 Binary files /dev/null and b/src/viewer3d/assets/panorama.hdr differ diff --git a/src/viewer3d/assets/panorama_to_cubemap.frag b/src/viewer3d/assets/panorama_to_cubemap.frag new file mode 100644 index 00000000..f29a573f --- /dev/null +++ b/src/viewer3d/assets/panorama_to_cubemap.frag @@ -0,0 +1,61 @@ +#define MATH_PI 3.1415926535897932384626433832795 +#define MATH_INV_PI (1.0 / MATH_PI) + +//precision highp float; + + +in vec2 v_texCoord; +out vec4 fragmentColor; + + +uniform int u_currentFace; + +//uniform sampler2D u_inputTexture; +uniform sampler2D u_panorama; + +vec3 uvToXYZ(int face, vec2 uv) +{ + if(face == 0) + return vec3( 1.f, uv.y, -uv.x); + + else if(face == 1) + return vec3( -1.f, uv.y, uv.x); + + else if(face == 2) + return vec3( +uv.x, -1.f, +uv.y); + + else if(face == 3) + return vec3( +uv.x, 1.f, -uv.y); + + else if(face == 4) + return vec3( +uv.x, uv.y, 1.f); + + else //if(face == 5) + { return vec3( -uv.x, +uv.y, -1.f);} +} + +vec2 dirToUV(vec3 dir) +{ + return vec2( + 0.5f + 0.5f * atan(dir.z, dir.x) / MATH_PI, + 1.f - acos(dir.y) / MATH_PI); +} + +vec3 panoramaToCubeMap(int face, vec2 texCoord) +{ + vec2 texCoordNew = texCoord*2.0-1.0; + vec3 scan = uvToXYZ(face, texCoordNew); + vec3 direction = normalize(scan); + vec2 src = dirToUV(direction); + + return texture(u_panorama, src).rgb; +} + + + +void main(void) +{ + fragmentColor = vec4(0.0, 0.0, 0.0, 1.0); + + fragmentColor.rgb = panoramaToCubeMap(u_currentFace, v_texCoord); +} diff --git a/src/viewer3d/assets/pbr.frag b/src/viewer3d/assets/pbr.frag new file mode 100644 index 00000000..f0e7d23c --- /dev/null +++ b/src/viewer3d/assets/pbr.frag @@ -0,0 +1,524 @@ +// +// This fragment shader defines a reference implementation for Physically Based Shading of +// a microfacet surface material defined by a glTF model. +// +// References: +// [1] Real Shading in Unreal Engine 4 +// http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf +// [2] Physically Based Shading at Disney +// http://blog.selfshadow.com/publications/s2012-shading-course/burley/s2012_pbs_disney_brdf_notes_v3.pdf +// [3] README.md - Environment Maps +// https://github.com/KhronosGroup/glTF-WebGL-PBR/#environment-maps +// [4] "An Inexpensive BRDF Model for Physically based Rendering" by Christophe Schlick +// https://www.cs.virginia.edu/~jdl/bib/appearance/analytic%20models/schlick94b.pdf +// [5] "KHR_materials_clearcoat" +// https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat + +//precision highp float; + + +#include +#include +#include +#include +#include +#include +#include + +#ifdef MATERIAL_IRIDESCENCE +#include +#endif + + +out vec4 g_finalColor; + + +void main() +{ + vec4 baseColor = getBaseColor(); + +#if ALPHAMODE == ALPHAMODE_OPAQUE + baseColor.a = 1.0; +#endif + vec3 color = vec3(0); + + vec3 v = normalize(u_Camera - v_Position); + NormalInfo normalInfo = getNormalInfo(v); + vec3 n = normalInfo.n; + vec3 t = normalInfo.t; + vec3 b = normalInfo.b; + + float NdotV = clampedDot(n, v); + float TdotV = clampedDot(t, v); + float BdotV = clampedDot(b, v); + + MaterialInfo materialInfo; + materialInfo.baseColor = baseColor.rgb; + + // The default index of refraction of 1.5 yields a dielectric normal incidence reflectance of 0.04. + materialInfo.ior = 1.5; + materialInfo.f0_dielectric = vec3(0.04); + materialInfo.specularWeight = 1.0; + + // Anything less than 2% is physically impossible and is instead considered to be shadowing. Compare to "Real-Time-Rendering" 4th editon on page 325. + materialInfo.f90 = vec3(1.0); + materialInfo.f90_dielectric = materialInfo.f90; + + // If the MR debug output is selected, we have to enforce evaluation of the non-iridescence BRDF functions. +#if DEBUG == DEBUG_METALLIC_ROUGHNESS +#undef MATERIAL_IRIDESCENCE +#endif + +#ifdef MATERIAL_IOR + materialInfo = getIorInfo(materialInfo); +#endif + +#ifdef MATERIAL_SPECULARGLOSSINESS + materialInfo = getSpecularGlossinessInfo(materialInfo); +#endif + +#ifdef MATERIAL_METALLICROUGHNESS + materialInfo = getMetallicRoughnessInfo(materialInfo); +#endif + +#ifdef MATERIAL_SHEEN + materialInfo = getSheenInfo(materialInfo); +#endif + +#ifdef MATERIAL_CLEARCOAT + materialInfo = getClearCoatInfo(materialInfo, normalInfo); +#endif + +#ifdef MATERIAL_SPECULAR + materialInfo = getSpecularInfo(materialInfo); +#endif + +#ifdef MATERIAL_TRANSMISSION + materialInfo = getTransmissionInfo(materialInfo); +#endif + +#ifdef MATERIAL_VOLUME + materialInfo = getVolumeInfo(materialInfo); +#endif + +#ifdef MATERIAL_IRIDESCENCE + materialInfo = getIridescenceInfo(materialInfo); +#endif + +#ifdef MATERIAL_DIFFUSE_TRANSMISSION + materialInfo = getDiffuseTransmissionInfo(materialInfo); +#endif + +#ifdef MATERIAL_ANISOTROPY + materialInfo = getAnisotropyInfo(materialInfo, normalInfo); +#endif + + materialInfo.perceptualRoughness = clamp(materialInfo.perceptualRoughness, 0.0, 1.0); + materialInfo.metallic = clamp(materialInfo.metallic, 0.0, 1.0); + + // Roughness is authored as perceptual roughness; as is convention, + // convert to material roughness by squaring the perceptual roughness. + materialInfo.alphaRoughness = materialInfo.perceptualRoughness * materialInfo.perceptualRoughness; + + + // LIGHTING + vec3 f_specular_dielectric = vec3(0.0); + vec3 f_specular_metal = vec3(0.0); + vec3 f_diffuse = vec3(0.0); + vec3 f_dielectric_brdf_ibl = vec3(0.0); + vec3 f_metal_brdf_ibl = vec3(0.0); + vec3 f_emissive = vec3(0.0); + vec3 clearcoat_brdf = vec3(0.0); + vec3 f_sheen = vec3(0.0); + vec3 f_specular_transmission = vec3(0.0); + vec3 f_diffuse_transmission = vec3(0.0); + + float clearcoatFactor = 0.0; + vec3 clearcoatFresnel = vec3(0); + + float albedoSheenScaling = 1.0; + float diffuseTransmissionThickness = 1.0; + +#ifdef MATERIAL_IRIDESCENCE + vec3 iridescenceFresnel_dielectric = evalIridescence(1.0, materialInfo.iridescenceIor, NdotV, materialInfo.iridescenceThickness, materialInfo.f0_dielectric); + vec3 iridescenceFresnel_metallic = evalIridescence(1.0, materialInfo.iridescenceIor, NdotV, materialInfo.iridescenceThickness, baseColor.rgb); + + if (materialInfo.iridescenceThickness == 0.0) { + materialInfo.iridescenceFactor = 0.0; + } +#endif + +#ifdef MATERIAL_DIFFUSE_TRANSMISSION +#ifdef MATERIAL_VOLUME + diffuseTransmissionThickness = materialInfo.thickness * + (length(vec3(u_ModelMatrix[0].xyz)) + length(vec3(u_ModelMatrix[1].xyz)) + length(vec3(u_ModelMatrix[2].xyz))) / 3.0; +#endif +#endif + +#ifdef MATERIAL_CLEARCOAT + clearcoatFactor = materialInfo.clearcoatFactor; + clearcoatFresnel = F_Schlick(materialInfo.clearcoatF0, materialInfo.clearcoatF90, clampedDot(materialInfo.clearcoatNormal, v)); +#endif + + // Calculate lighting contribution from image based lighting source (IBL) + +#if defined(USE_IBL) || defined(MATERIAL_TRANSMISSION) + + f_diffuse = getDiffuseLight(n) * baseColor.rgb ; + +#ifdef MATERIAL_DIFFUSE_TRANSMISSION + vec3 diffuseTransmissionIBL = getDiffuseLight(-n) * materialInfo.diffuseTransmissionColorFactor; +#ifdef MATERIAL_VOLUME + diffuseTransmissionIBL = applyVolumeAttenuation(diffuseTransmissionIBL, diffuseTransmissionThickness, materialInfo.attenuationColor, materialInfo.attenuationDistance); +#endif + f_diffuse = mix(f_diffuse, diffuseTransmissionIBL, materialInfo.diffuseTransmissionFactor); +#endif + + +#if defined(MATERIAL_TRANSMISSION) + f_specular_transmission = getIBLVolumeRefraction( + n, v, + materialInfo.perceptualRoughness, + baseColor.rgb, materialInfo.f0_dielectric, materialInfo.f90, + v_Position, u_ModelMatrix, u_ViewMatrix, u_ProjectionMatrix, + materialInfo.ior, materialInfo.thickness, materialInfo.attenuationColor, materialInfo.attenuationDistance, materialInfo.dispersion); + f_diffuse = mix(f_diffuse, f_specular_transmission, materialInfo.transmissionFactor); +#endif + +#ifdef MATERIAL_ANISOTROPY + f_specular_metal = getIBLRadianceAnisotropy(n, v, materialInfo.perceptualRoughness, materialInfo.anisotropyStrength, materialInfo.anisotropicB); + f_specular_dielectric = f_specular_metal; +#else + f_specular_metal = getIBLRadianceGGX(n, v, materialInfo.perceptualRoughness); + f_specular_dielectric = f_specular_metal; +#endif + + // Calculate fresnel mix for IBL + + vec3 f_metal_fresnel_ibl = getIBLGGXFresnel(n, v, materialInfo.perceptualRoughness, baseColor.rgb, 1.0); + f_metal_brdf_ibl = f_metal_fresnel_ibl * f_specular_metal; + + vec3 f_dielectric_fresnel_ibl = getIBLGGXFresnel(n, v, materialInfo.perceptualRoughness, materialInfo.f0_dielectric, materialInfo.specularWeight); + f_dielectric_brdf_ibl = mix(f_diffuse, f_specular_dielectric, f_dielectric_fresnel_ibl); + +#ifdef MATERIAL_IRIDESCENCE + f_metal_brdf_ibl = mix(f_metal_brdf_ibl, f_specular_metal * iridescenceFresnel_metallic, materialInfo.iridescenceFactor); + f_dielectric_brdf_ibl = mix(f_dielectric_brdf_ibl, rgb_mix(f_diffuse, f_specular_dielectric, iridescenceFresnel_dielectric), materialInfo.iridescenceFactor); +#endif + + +#ifdef MATERIAL_CLEARCOAT + clearcoat_brdf = getIBLRadianceGGX(materialInfo.clearcoatNormal, v, materialInfo.clearcoatRoughness); +#endif + +#ifdef MATERIAL_SHEEN + f_sheen = getIBLRadianceCharlie(n, v, materialInfo.sheenRoughnessFactor, materialInfo.sheenColorFactor); + albedoSheenScaling = 1.0 - max3(materialInfo.sheenColorFactor) * albedoSheenScalingLUT(NdotV, materialInfo.sheenRoughnessFactor); +#endif + + color = mix(f_dielectric_brdf_ibl, f_metal_brdf_ibl, materialInfo.metallic); + color = f_sheen + color * albedoSheenScaling; + color = mix(color, clearcoat_brdf, clearcoatFactor * clearcoatFresnel); + +#ifdef HAS_OCCLUSION_MAP + float ao = 1.0; + ao = texture(u_OcclusionSampler, getOcclusionUV()).r; + color = color * (1.0 + u_OcclusionStrength * (ao - 1.0)); +#endif + +#endif //end USE_IBL + + f_diffuse = vec3(0.0); + f_specular_dielectric = vec3(0.0); + f_specular_metal = vec3(0.0); + vec3 f_dielectric_brdf = vec3(0.0); + vec3 f_metal_brdf = vec3(0.0); + +#ifdef USE_PUNCTUAL + for (int i = 0; i < LIGHT_COUNT; ++i) + { + Light light = u_Lights[i]; + + vec3 pointToLight; + if (light.type != LightType_Directional) + { + pointToLight = light.position - v_Position; + } + else + { + pointToLight = -light.direction; + } + + // BSTF + vec3 l = normalize(pointToLight); // Direction from surface point to light + vec3 h = normalize(l + v); // Direction of the vector between l and v, called halfway vector + float NdotL = clampedDot(n, l); + float NdotV = clampedDot(n, v); + float NdotH = clampedDot(n, h); + float LdotH = clampedDot(l, h); + float VdotH = clampedDot(v, h); + + vec3 dielectric_fresnel = F_Schlick(materialInfo.f0_dielectric * materialInfo.specularWeight, materialInfo.f90_dielectric, abs(VdotH)); + vec3 metal_fresnel = F_Schlick(baseColor.rgb, vec3(1.0), abs(VdotH)); + + vec3 lightIntensity = getLighIntensity(light, pointToLight); + + vec3 l_diffuse = lightIntensity * NdotL * BRDF_lambertian(baseColor.rgb); + vec3 l_specular_dielectric = vec3(0.0); + vec3 l_specular_metal = vec3(0.0); + vec3 l_dielectric_brdf = vec3(0.0); + vec3 l_metal_brdf = vec3(0.0); + vec3 l_clearcoat_brdf = vec3(0.0); + vec3 l_sheen = vec3(0.0); + float l_albedoSheenScaling = 1.0; + + + + +#ifdef MATERIAL_DIFFUSE_TRANSMISSION + vec3 diffuse_btdf = lightIntensity * clampedDot(-n, l) * BRDF_lambertian(materialInfo.diffuseTransmissionColorFactor); + +#ifdef MATERIAL_VOLUME + diffuse_btdf = applyVolumeAttenuation(diffuse_btdf, diffuseTransmissionThickness, materialInfo.attenuationColor, materialInfo.attenuationDistance); +#endif + l_diffuse = mix(l_diffuse, diffuse_btdf, materialInfo.diffuseTransmissionFactor); +#endif // MATERIAL_DIFFUSE_TRANSMISSION + + // BTDF (Bidirectional Transmittance Distribution Function) +#ifdef MATERIAL_TRANSMISSION + // If the light ray travels through the geometry, use the point it exits the geometry again. + // That will change the angle to the light source, if the material refracts the light ray. + vec3 transmissionRay = getVolumeTransmissionRay(n, v, materialInfo.thickness, materialInfo.ior, u_ModelMatrix); + pointToLight -= transmissionRay; + l = normalize(pointToLight); + + vec3 transmittedLight = lightIntensity * getPunctualRadianceTransmission(n, v, l, materialInfo.alphaRoughness, materialInfo.f0_dielectric, materialInfo.f90, baseColor.rgb, materialInfo.ior); + +#ifdef MATERIAL_VOLUME + transmittedLight = applyVolumeAttenuation(transmittedLight, length(transmissionRay), materialInfo.attenuationColor, materialInfo.attenuationDistance); +#endif + l_diffuse = mix(l_diffuse, transmittedLight, materialInfo.transmissionFactor); +#endif + + // Calculation of analytical light + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#acknowledgments AppendixB + vec3 intensity = getLighIntensity(light, pointToLight); + +#ifdef MATERIAL_ANISOTROPY + l_specular_metal = intensity * NdotL * BRDF_specularGGXAnisotropy(materialInfo.alphaRoughness, materialInfo.anisotropyStrength, n, v, l, h, materialInfo.anisotropicT, materialInfo.anisotropicB); + l_specular_dielectric = l_specular_metal; +#else + l_specular_metal = intensity * NdotL * BRDF_specularGGX(materialInfo.alphaRoughness, NdotL, NdotV, NdotH); + l_specular_dielectric = l_specular_metal; +#endif + + l_metal_brdf = metal_fresnel * l_specular_metal; + l_dielectric_brdf = mix(l_diffuse, l_specular_dielectric, dielectric_fresnel); // Do we need to handle vec3 fresnel here? + +#ifdef MATERIAL_IRIDESCENCE + l_metal_brdf = mix(l_metal_brdf, l_specular_metal * iridescenceFresnel_metallic, materialInfo.iridescenceFactor); + l_dielectric_brdf = mix(l_dielectric_brdf, rgb_mix(l_diffuse, l_specular_dielectric, iridescenceFresnel_dielectric), materialInfo.iridescenceFactor); +#endif + +#ifdef MATERIAL_CLEARCOAT + l_clearcoat_brdf = intensity * getPunctualRadianceClearCoat(materialInfo.clearcoatNormal, v, l, h, VdotH, + materialInfo.clearcoatF0, materialInfo.clearcoatF90, materialInfo.clearcoatRoughness); +#endif + +#ifdef MATERIAL_SHEEN + l_sheen = intensity * getPunctualRadianceSheen(materialInfo.sheenColorFactor, materialInfo.sheenRoughnessFactor, NdotL, NdotV, NdotH); + l_albedoSheenScaling = min(1.0 - max3(materialInfo.sheenColorFactor) * albedoSheenScalingLUT(NdotV, materialInfo.sheenRoughnessFactor), + 1.0 - max3(materialInfo.sheenColorFactor) * albedoSheenScalingLUT(NdotL, materialInfo.sheenRoughnessFactor)); +#endif + + vec3 l_color = mix(l_dielectric_brdf, l_metal_brdf, materialInfo.metallic); + l_color = l_sheen + l_color * l_albedoSheenScaling; + l_color = mix(l_color, l_clearcoat_brdf, clearcoatFactor * clearcoatFresnel); + color += l_color; + } +#endif // USE_PUNCTUAL + + f_emissive = u_EmissiveFactor; +#ifdef MATERIAL_EMISSIVE_STRENGTH + f_emissive *= u_EmissiveStrength; +#endif +#ifdef HAS_EMISSIVE_MAP + f_emissive *= texture(u_EmissiveSampler, getEmissiveUV()).rgb; +#endif + + +#ifdef MATERIAL_UNLIT + color = baseColor.rgb; +#elif defined(NOT_TRIANGLE) && !defined(HAS_NORMAL_VEC3) + //Points or Lines with no NORMAL attribute SHOULD be rendered without lighting and instead use the sum of the base color value and the emissive value. + color = f_emissive + baseColor.rgb; +#else + color = f_emissive * (1.0 - clearcoatFactor * clearcoatFresnel) + color; +#endif + +#if DEBUG == DEBUG_NONE + +#if ALPHAMODE == ALPHAMODE_MASK + // Late discard to avoid sampling artifacts. See https://github.com/KhronosGroup/glTF-Sample-Viewer/issues/267 + if (baseColor.a < u_AlphaCutoff) + { + discard; + } + baseColor.a = 1.0; +#endif + +#ifdef LINEAR_OUTPUT + g_finalColor = vec4(color.rgb, baseColor.a); +#else + g_finalColor = vec4(toneMap(color), baseColor.a); +#endif + +#else + // In case of missing data for a debug view, render a checkerboard. + g_finalColor = vec4(1.0); + { + float frequency = 0.02; + float gray = 0.9; + + vec2 v1 = step(0.5, fract(frequency * gl_FragCoord.xy)); + vec2 v2 = step(0.5, vec2(1.0) - fract(frequency * gl_FragCoord.xy)); + g_finalColor.rgb *= gray + v1.x * v1.y + v2.x * v2.y; + } +#endif + + // Debug views: + + // Generic: +#if DEBUG == DEBUG_UV_0 && defined(HAS_TEXCOORD_0_VEC2) + g_finalColor.rgb = vec3(v_texcoord_0, 0); +#endif +#if DEBUG == DEBUG_UV_1 && defined(HAS_TEXCOORD_1_VEC2) + g_finalColor.rgb = vec3(v_texcoord_1, 0); +#endif +#if DEBUG == DEBUG_NORMAL_TEXTURE && defined(HAS_NORMAL_MAP) + g_finalColor.rgb = (normalInfo.ntex + 1.0) / 2.0; +#endif +#if DEBUG == DEBUG_NORMAL_SHADING + g_finalColor.rgb = (n + 1.0) / 2.0; +#endif +#if DEBUG == DEBUG_NORMAL_GEOMETRY + g_finalColor.rgb = (normalInfo.ng + 1.0) / 2.0; +#endif +#if DEBUG == DEBUG_TANGENT + g_finalColor.rgb = (normalInfo.t + 1.0) / 2.0; +#endif +#if DEBUG == DEBUG_BITANGENT + g_finalColor.rgb = (normalInfo.b + 1.0) / 2.0; +#endif +#if DEBUG == DEBUG_ALPHA + g_finalColor.rgb = vec3(baseColor.a); +#endif +#if DEBUG == DEBUG_OCCLUSION && defined(HAS_OCCLUSION_MAP) + g_finalColor.rgb = vec3(ao); +#endif +#if DEBUG == DEBUG_EMISSIVE + g_finalColor.rgb = linearTosRGB(f_emissive); +#endif + + // MR: +#ifdef MATERIAL_METALLICROUGHNESS +#if DEBUG == DEBUG_METALLIC + g_finalColor.rgb = vec3(materialInfo.metallic); +#endif +#if DEBUG == DEBUG_ROUGHNESS + g_finalColor.rgb = vec3(materialInfo.perceptualRoughness); +#endif +#if DEBUG == DEBUG_BASE_COLOR + g_finalColor.rgb = linearTosRGB(materialInfo.baseColor); +#endif +#endif + + // Clearcoat: +#ifdef MATERIAL_CLEARCOAT +#if DEBUG == DEBUG_CLEARCOAT_FACTOR + g_finalColor.rgb = vec3(materialInfo.clearcoatFactor); +#endif +#if DEBUG == DEBUG_CLEARCOAT_ROUGHNESS + g_finalColor.rgb = vec3(materialInfo.clearcoatRoughness); +#endif +#if DEBUG == DEBUG_CLEARCOAT_NORMAL + g_finalColor.rgb = (materialInfo.clearcoatNormal + vec3(1)) / 2.0; +#endif +#endif + + // Sheen: +#ifdef MATERIAL_SHEEN +#if DEBUG == DEBUG_SHEEN_COLOR + g_finalColor.rgb = materialInfo.sheenColorFactor; +#endif +#if DEBUG == DEBUG_SHEEN_ROUGHNESS + g_finalColor.rgb = vec3(materialInfo.sheenRoughnessFactor); +#endif +#endif + + // Specular: +#ifdef MATERIAL_SPECULAR +#if DEBUG == DEBUG_SPECULAR_FACTOR + g_finalColor.rgb = vec3(materialInfo.specularWeight); +#endif + +#if DEBUG == DEBUG_SPECULAR_COLOR +vec3 specularTexture = vec3(1.0); +#ifdef HAS_SPECULAR_COLOR_MAP + specularTexture.rgb = texture(u_SpecularColorSampler, getSpecularColorUV()).rgb; +#endif + g_finalColor.rgb = u_KHR_materials_specular_specularColorFactor * specularTexture.rgb; +#endif +#endif + + // Transmission, Volume: +#ifdef MATERIAL_TRANSMISSION +#if DEBUG == DEBUG_TRANSMISSION_FACTOR + g_finalColor.rgb = vec3(materialInfo.transmissionFactor); +#endif +#endif +#ifdef MATERIAL_VOLUME +#if DEBUG == DEBUG_VOLUME_THICKNESS + g_finalColor.rgb = vec3(materialInfo.thickness / u_ThicknessFactor); +#endif +#endif + + // Iridescence: +#ifdef MATERIAL_IRIDESCENCE +#if DEBUG == DEBUG_IRIDESCENCE_FACTOR + g_finalColor.rgb = vec3(materialInfo.iridescenceFactor); +#endif +#if DEBUG == DEBUG_IRIDESCENCE_THICKNESS + g_finalColor.rgb = vec3(materialInfo.iridescenceThickness / 1200.0); +#endif +#endif + + // Anisotropy: +#ifdef MATERIAL_ANISOTROPY +#if DEBUG == DEBUG_ANISOTROPIC_STRENGTH + g_finalColor.rgb = vec3(materialInfo.anisotropyStrength); +#endif +#if DEBUG == DEBUG_ANISOTROPIC_DIRECTION + vec2 direction = vec2(1.0, 0.0); +#ifdef HAS_ANISOTROPY_MAP + direction = texture(u_AnisotropySampler, getAnisotropyUV()).xy; + direction = direction * 2.0 - vec2(1.0); // [0, 1] -> [-1, 1] +#endif + vec2 directionRotation = u_Anisotropy.xy; // cos(theta), sin(theta) + mat2 rotationMatrix = mat2(directionRotation.x, directionRotation.y, -directionRotation.y, directionRotation.x); + direction = (direction + vec2(1.0)) * 0.5; // [-1, 1] -> [0, 1] + + g_finalColor.rgb = vec3(direction, 0.0); +#endif +#endif + + // Diffuse Transmission: +#ifdef MATERIAL_DIFFUSE_TRANSMISSION +#if DEBUG == DEBUG_DIFFUSE_TRANSMISSION_FACTOR + g_finalColor.rgb = linearTosRGB(vec3(materialInfo.diffuseTransmissionFactor)); +#endif +#if DEBUG == DEBUG_DIFFUSE_TRANSMISSION_COLOR_FACTOR + g_finalColor.rgb = linearTosRGB(materialInfo.diffuseTransmissionColorFactor); +#endif +#endif +} diff --git a/src/viewer3d/assets/primitive.vert b/src/viewer3d/assets/primitive.vert new file mode 100644 index 00000000..fe668d90 --- /dev/null +++ b/src/viewer3d/assets/primitive.vert @@ -0,0 +1,167 @@ +#include + + +uniform mat4 u_ViewProjectionMatrix; +uniform mat4 u_ModelMatrix; +uniform mat4 u_NormalMatrix; + +#ifdef HAS_HEIGHT_MAP +uniform sampler2D u_HeightSampler; +uniform float u_HeightScale; +#endif + + +in vec3 a_position; +out vec3 v_Position; + +#ifdef HAS_NORMAL_VEC3 +in vec3 a_normal; +#endif + +#ifdef HAS_NORMAL_VEC3 +#ifdef HAS_TANGENT_VEC4 +in vec4 a_tangent; +out mat3 v_TBN; +#else +out vec3 v_Normal; +#endif +#endif + +#ifdef HAS_TEXCOORD_0_VEC2 +in vec2 a_texcoord_0; +#endif + +#ifdef HAS_TEXCOORD_1_VEC2 +in vec2 a_texcoord_1; +#endif + +out vec2 v_texcoord_0; +out vec2 v_texcoord_1; + +#ifdef HAS_COLOR_0_VEC3 +in vec3 a_color_0; +out vec3 v_Color; +#endif + +#ifdef HAS_COLOR_0_VEC4 +in vec4 a_color_0; +out vec4 v_Color; +#endif + + +vec4 getPosition() +{ + vec4 pos = vec4(a_position, 1.0); + +#ifdef USE_MORPHING + pos += getTargetPosition(gl_VertexID); +#endif + +#ifdef USE_SKINNING + pos = getSkinningMatrix() * pos; +#endif + + return pos; +} + + +#ifdef HAS_NORMAL_VEC3 +vec3 getNormal() +{ + vec3 normal = a_normal; + +#ifdef USE_MORPHING + normal += getTargetNormal(gl_VertexID); +#endif + +#ifdef USE_SKINNING + normal = mat3(getSkinningNormalMatrix()) * normal; +#endif + + return normalize(normal); +} +#endif + +#ifdef HAS_NORMAL_VEC3 +#ifdef HAS_TANGENT_VEC4 +vec3 getTangent() +{ + vec3 tangent = a_tangent.xyz; + +#ifdef USE_MORPHING + tangent += getTargetTangent(gl_VertexID); +#endif + +#ifdef USE_SKINNING + tangent = mat3(getSkinningMatrix()) * tangent; +#endif + + return normalize(tangent); +} +#endif +#endif + + +void main() +{ + gl_PointSize = 1.0f; + + vec4 localPos = getPosition(); + +#ifdef HAS_HEIGHT_MAP +#ifdef HAS_TEXCOORD_0_VEC2 + // Sample height map and displace along normal + float height = texture(u_HeightSampler, a_texcoord_0).r; + vec3 displacedPos = localPos.xyz + getNormal() * height * u_HeightScale; + localPos = vec4(displacedPos, 1.0); +#endif +#endif + + vec4 pos = u_ModelMatrix * localPos; + v_Position = vec3(pos.xyz) / pos.w; + +#ifdef HAS_NORMAL_VEC3 +#ifdef HAS_TANGENT_VEC4 + vec3 tangent = getTangent(); + vec3 normalW = normalize(vec3(u_NormalMatrix * vec4(getNormal(), 0.0))); + vec3 tangentW = normalize(vec3(u_ModelMatrix * vec4(tangent, 0.0))); + vec3 bitangentW = cross(normalW, tangentW) * a_tangent.w; + v_TBN = mat3(tangentW, bitangentW, normalW); +#else + v_Normal = normalize(vec3(u_NormalMatrix * vec4(getNormal(), 0.0))); +#endif +#endif + + v_texcoord_0 = vec2(0.0, 0.0); + v_texcoord_1 = vec2(0.0, 0.0); + +#ifdef HAS_TEXCOORD_0_VEC2 + v_texcoord_0 = a_texcoord_0; +#endif + +#ifdef HAS_TEXCOORD_1_VEC2 + v_texcoord_1 = a_texcoord_1; +#endif + +#ifdef USE_MORPHING + v_texcoord_0 += getTargetTexCoord0(gl_VertexID); + v_texcoord_1 += getTargetTexCoord1(gl_VertexID); +#endif + + +#if defined(HAS_COLOR_0_VEC3) + v_Color = a_color_0; +#if defined(USE_MORPHING) + v_Color = clamp(v_Color + getTargetColor0(gl_VertexID).xyz, 0.0f, 1.0f); +#endif +#endif + +#if defined(HAS_COLOR_0_VEC4) + v_Color = a_color_0; +#if defined(USE_MORPHING) + v_Color = clamp(v_Color + getTargetColor0(gl_VertexID), 0.0f, 1.0f); +#endif +#endif + + gl_Position = u_ViewProjectionMatrix * pos; +} \ No newline at end of file diff --git a/src/viewer3d/assets/punctual.glsl b/src/viewer3d/assets/punctual.glsl new file mode 100644 index 00000000..25345ccf --- /dev/null +++ b/src/viewer3d/assets/punctual.glsl @@ -0,0 +1,141 @@ +// KHR_lights_punctual extension. +// see https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual +struct Light +{ + vec3 direction; + float range; + + vec3 color; + float intensity; + + vec3 position; + float innerConeCos; + + float outerConeCos; + int type; +}; + + +const int LightType_Directional = 0; +const int LightType_Point = 1; +const int LightType_Spot = 2; + + +#ifdef USE_PUNCTUAL +uniform Light u_Lights[LIGHT_COUNT + 1]; //Array [0] is not allowed +#endif + + +// https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/README.md#range-property +float getRangeAttenuation(float range, float distance) +{ + if (range <= 0.0) + { + // negative range means unlimited + return 1.0 / pow(distance, 2.0); + } + return max(min(1.0 - pow(distance / range, 4.0), 1.0), 0.0) / pow(distance, 2.0); +} + + +// https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/README.md#inner-and-outer-cone-angles +float getSpotAttenuation(vec3 pointToLight, vec3 spotDirection, float outerConeCos, float innerConeCos) +{ + float actualCos = dot(normalize(spotDirection), normalize(-pointToLight)); + if (actualCos > outerConeCos) + { + if (actualCos < innerConeCos) + { + float angularAttenuation = (actualCos - outerConeCos) / (innerConeCos - outerConeCos); + return angularAttenuation * angularAttenuation; + } + return 1.0; + } + return 0.0; +} + + +vec3 getLighIntensity(Light light, vec3 pointToLight) +{ + float rangeAttenuation = 1.0; + float spotAttenuation = 1.0; + + if (light.type != LightType_Directional) + { + rangeAttenuation = getRangeAttenuation(light.range, length(pointToLight)); + } + if (light.type == LightType_Spot) + { + spotAttenuation = getSpotAttenuation(pointToLight, light.direction, light.outerConeCos, light.innerConeCos); + } + + return rangeAttenuation * spotAttenuation * light.intensity * light.color; +} + + +vec3 getPunctualRadianceTransmission(vec3 normal, vec3 view, vec3 pointToLight, float alphaRoughness, + vec3 f0, vec3 f90, vec3 baseColor, float ior) +{ + float transmissionRougness = applyIorToRoughness(alphaRoughness, ior); + + vec3 n = normalize(normal); // Outward direction of surface point + vec3 v = normalize(view); // Direction from surface point to view + vec3 l = normalize(pointToLight); + vec3 l_mirror = normalize(l + 2.0*n*dot(-l, n)); // Mirror light reflection vector on surface + vec3 h = normalize(l_mirror + v); // Halfway vector between transmission light vector and v + + float D = D_GGX(clamp(dot(n, h), 0.0, 1.0), transmissionRougness); + vec3 F = F_Schlick(f0, f90, clamp(dot(v, h), 0.0, 1.0)); + float Vis = V_GGX(clamp(dot(n, l_mirror), 0.0, 1.0), clamp(dot(n, v), 0.0, 1.0), transmissionRougness); + + // Transmission BTDF + return (1.0 - F) * baseColor * D * Vis; +} + + +vec3 getPunctualRadianceClearCoat(vec3 clearcoatNormal, vec3 v, vec3 l, vec3 h, float VdotH, vec3 f0, vec3 f90, float clearcoatRoughness) +{ + float NdotL = clampedDot(clearcoatNormal, l); + float NdotV = clampedDot(clearcoatNormal, v); + float NdotH = clampedDot(clearcoatNormal, h); + return NdotL * BRDF_specularGGX(clearcoatRoughness * clearcoatRoughness, NdotL, NdotV, NdotH); +} + + +vec3 getPunctualRadianceSheen(vec3 sheenColor, float sheenRoughness, float NdotL, float NdotV, float NdotH) +{ + return NdotL * BRDF_specularSheen(sheenColor, sheenRoughness, NdotL, NdotV, NdotH); +} + + +// Compute attenuated light as it travels through a volume. +vec3 applyVolumeAttenuation(vec3 radiance, float transmissionDistance, vec3 attenuationColor, float attenuationDistance) +{ + if (attenuationDistance == 0.0) + { + // Attenuation distance is +∞ (which we indicate by zero), i.e. the transmitted color is not attenuated at all. + return radiance; + } + else + { + // Compute light attenuation using Beer's law. + vec3 transmittance = pow(attenuationColor, vec3(transmissionDistance / attenuationDistance)); + return transmittance * radiance; + } +} + + +vec3 getVolumeTransmissionRay(vec3 n, vec3 v, float thickness, float ior, mat4 modelMatrix) +{ + // Direction of refracted light. + vec3 refractionVector = refract(-v, normalize(n), 1.0 / ior); + + // Compute rotation-independant scaling of the model matrix. + vec3 modelScale; + modelScale.x = length(vec3(modelMatrix[0].xyz)); + modelScale.y = length(vec3(modelMatrix[1].xyz)); + modelScale.z = length(vec3(modelMatrix[2].xyz)); + + // The thickness is specified in local space. + return normalize(refractionVector) * thickness * modelScale; +} diff --git a/src/viewer3d/assets/skybox.frag b/src/viewer3d/assets/skybox.frag new file mode 100644 index 00000000..f9c2b643 --- /dev/null +++ b/src/viewer3d/assets/skybox.frag @@ -0,0 +1,20 @@ +#version 330 core + +in vec3 v_texCoord; + +out vec4 fragColor; + +uniform samplerCube u_environmentMap; + +void main() +{ + vec3 envColor = texture(u_environmentMap, v_texCoord).rgb; + + // Apply simple tone mapping + envColor = envColor / (envColor + vec3(1.0)); + + // Gamma correction + envColor = pow(envColor, vec3(1.0/2.2)); + + fragColor = vec4(envColor, 1.0); +} diff --git a/src/viewer3d/assets/skybox.vert b/src/viewer3d/assets/skybox.vert new file mode 100644 index 00000000..e63b48ee --- /dev/null +++ b/src/viewer3d/assets/skybox.vert @@ -0,0 +1,24 @@ +#version 330 core + +in vec3 a_position; + +out vec3 v_texCoord; + +uniform mat4 u_modelMatrix; +uniform mat4 u_viewMatrix; +uniform mat4 u_projectionMatrix; +// Yaw about the up axis, matching u_EnvRotation in the pbr shader so the +// background and the image based lighting stay in sync. +uniform mat3 u_envRotation; + +void main() +{ + v_texCoord = u_envRotation * a_position; + + // Remove translation from view matrix + mat4 rotView = mat4(mat3(u_viewMatrix)); + vec4 clipPos = u_projectionMatrix * rotView * vec4(a_position, 1.0); + + // Set z to w so that after perspective divide, z will be 1.0 (max depth) + gl_Position = clipPos.xyww; +} diff --git a/src/viewer3d/assets/textures.glsl b/src/viewer3d/assets/textures.glsl new file mode 100644 index 00000000..09649c0c --- /dev/null +++ b/src/viewer3d/assets/textures.glsl @@ -0,0 +1,443 @@ +// IBL + + +uniform int u_MipCount; +uniform samplerCube u_LambertianEnvSampler; +uniform samplerCube u_GGXEnvSampler; +uniform sampler2D u_GGXLUT; +uniform samplerCube u_CharlieEnvSampler; +uniform sampler2D u_CharlieLUT; +uniform sampler2D u_SheenELUT; +uniform mat3 u_EnvRotation; + + +// General Material + + +uniform sampler2D u_NormalSampler; +uniform float u_NormalScale; +uniform int u_NormalUVSet; +uniform mat3 u_NormalUVTransform; + +uniform vec3 u_EmissiveFactor; +uniform sampler2D u_EmissiveSampler; +uniform int u_EmissiveUVSet; +uniform mat3 u_EmissiveUVTransform; + +uniform sampler2D u_OcclusionSampler; +uniform int u_OcclusionUVSet; +uniform float u_OcclusionStrength; +uniform mat3 u_OcclusionUVTransform; + + +in vec2 v_texcoord_0; +in vec2 v_texcoord_1; + + +vec2 getNormalUV() +{ + vec3 uv = vec3(u_NormalUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_NORMAL_UV_TRANSFORM + uv = u_NormalUVTransform * uv; +#endif + + return uv.xy; +} + + +vec2 getEmissiveUV() +{ + vec3 uv = vec3(u_EmissiveUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_EMISSIVE_UV_TRANSFORM + uv = u_EmissiveUVTransform * uv; +#endif + + return uv.xy; +} + + +vec2 getOcclusionUV() +{ + vec3 uv = vec3(u_OcclusionUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_OCCLUSION_UV_TRANSFORM + uv = u_OcclusionUVTransform * uv; +#endif + + return uv.xy; +} + + +// Metallic Roughness Material + + +#ifdef MATERIAL_METALLICROUGHNESS + +uniform sampler2D u_BaseColorSampler; +uniform int u_BaseColorUVSet; +uniform mat3 u_BaseColorUVTransform; + +uniform sampler2D u_MetallicRoughnessSampler; +uniform int u_MetallicRoughnessUVSet; +uniform mat3 u_MetallicRoughnessUVTransform; + +uniform sampler2D u_MetalnessSampler; +uniform int u_MetalnessUVSet; +uniform mat3 u_MetalnessUVTransform; + +uniform sampler2D u_RoughnessSampler; +uniform int u_RoughnessUVSet; +uniform mat3 u_RoughnessUVTransform; + +uniform sampler2D u_AlphaSampler; +uniform int u_AlphaUVSet; +uniform mat3 u_AlphaUVTransform; + +vec2 getBaseColorUV() +{ + vec3 uv = vec3(u_BaseColorUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_BASECOLOR_UV_TRANSFORM + uv = u_BaseColorUVTransform * uv; +#endif + + return uv.xy; +} + +vec2 getMetallicRoughnessUV() +{ + vec3 uv = vec3(u_MetallicRoughnessUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_METALLICROUGHNESS_UV_TRANSFORM + uv = u_MetallicRoughnessUVTransform * uv; +#endif + + return uv.xy; +} + +vec2 getMetalnessUV() +{ + vec3 uv = vec3(u_MetalnessUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_METALNESS_UV_TRANSFORM + uv = u_MetalnessUVTransform * uv; +#endif + + return uv.xy; +} + +vec2 getRoughnessUV() +{ + vec3 uv = vec3(u_RoughnessUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_ROUGHNESS_UV_TRANSFORM + uv = u_RoughnessUVTransform * uv; +#endif + + return uv.xy; +} + +vec2 getAlphaUV() +{ + vec3 uv = vec3(u_AlphaUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_ALPHA_UV_TRANSFORM + uv = u_AlphaUVTransform * uv; +#endif + + return uv.xy; +} + +#endif + + +// Specular Glossiness Material + + +#ifdef MATERIAL_SPECULARGLOSSINESS + +uniform sampler2D u_DiffuseSampler; +uniform int u_DiffuseUVSet; +uniform mat3 u_DiffuseUVTransform; + +uniform sampler2D u_SpecularGlossinessSampler; +uniform int u_SpecularGlossinessUVSet; +uniform mat3 u_SpecularGlossinessUVTransform; + + +vec2 getSpecularGlossinessUV() +{ + vec3 uv = vec3(u_SpecularGlossinessUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_SPECULARGLOSSINESS_UV_TRANSFORM + uv = u_SpecularGlossinessUVTransform * uv; +#endif + + return uv.xy; +} + +vec2 getDiffuseUV() +{ + vec3 uv = vec3(u_DiffuseUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); + +#ifdef HAS_DIFFUSE_UV_TRANSFORM + uv = u_DiffuseUVTransform * uv; +#endif + + return uv.xy; +} + +#endif + + +// Clearcoat Material + + +#ifdef MATERIAL_CLEARCOAT + +uniform sampler2D u_ClearcoatSampler; +uniform int u_ClearcoatUVSet; +uniform mat3 u_ClearcoatUVTransform; + +uniform sampler2D u_ClearcoatRoughnessSampler; +uniform int u_ClearcoatRoughnessUVSet; +uniform mat3 u_ClearcoatRoughnessUVTransform; + +uniform sampler2D u_ClearcoatNormalSampler; +uniform int u_ClearcoatNormalUVSet; +uniform mat3 u_ClearcoatNormalUVTransform; +uniform float u_ClearcoatNormalScale; + + +vec2 getClearcoatUV() +{ + vec3 uv = vec3(u_ClearcoatUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_CLEARCOAT_UV_TRANSFORM + uv = u_ClearcoatUVTransform * uv; +#endif + return uv.xy; +} + +vec2 getClearcoatRoughnessUV() +{ + vec3 uv = vec3(u_ClearcoatRoughnessUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_CLEARCOATROUGHNESS_UV_TRANSFORM + uv = u_ClearcoatRoughnessUVTransform * uv; +#endif + return uv.xy; +} + +vec2 getClearcoatNormalUV() +{ + vec3 uv = vec3(u_ClearcoatNormalUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_CLEARCOATNORMAL_UV_TRANSFORM + uv = u_ClearcoatNormalUVTransform * uv; +#endif + return uv.xy; +} + +#endif + + +// Sheen Material + + +#ifdef MATERIAL_SHEEN + +uniform sampler2D u_SheenColorSampler; +uniform int u_SheenColorUVSet; +uniform mat3 u_SheenColorUVTransform; +uniform sampler2D u_SheenRoughnessSampler; +uniform int u_SheenRoughnessUVSet; +uniform mat3 u_SheenRoughnessUVTransform; + + +vec2 getSheenColorUV() +{ + vec3 uv = vec3(u_SheenColorUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_SHEENCOLOR_UV_TRANSFORM + uv = u_SheenColorUVTransform * uv; +#endif + return uv.xy; +} + +vec2 getSheenRoughnessUV() +{ + vec3 uv = vec3(u_SheenRoughnessUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_SHEENROUGHNESS_UV_TRANSFORM + uv = u_SheenRoughnessUVTransform * uv; +#endif + return uv.xy; +} + +#endif + + +// Specular Material + + +#ifdef MATERIAL_SPECULAR + +uniform sampler2D u_SpecularSampler; +uniform int u_SpecularUVSet; +uniform mat3 u_SpecularUVTransform; +uniform sampler2D u_SpecularColorSampler; +uniform int u_SpecularColorUVSet; +uniform mat3 u_SpecularColorUVTransform; + + +vec2 getSpecularUV() +{ + vec3 uv = vec3(u_SpecularUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_SPECULAR_UV_TRANSFORM + uv = u_SpecularUVTransform * uv; +#endif + return uv.xy; +} + +vec2 getSpecularColorUV() +{ + vec3 uv = vec3(u_SpecularColorUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_SPECULARCOLOR_UV_TRANSFORM + uv = u_SpecularColorUVTransform * uv; +#endif + return uv.xy; +} + +#endif + + +// Transmission Material + + +#ifdef MATERIAL_TRANSMISSION + +uniform sampler2D u_TransmissionSampler; +uniform int u_TransmissionUVSet; +uniform mat3 u_TransmissionUVTransform; +uniform sampler2D u_TransmissionFramebufferSampler; +uniform ivec2 u_TransmissionFramebufferSize; + + +vec2 getTransmissionUV() +{ + vec3 uv = vec3(u_TransmissionUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_TRANSMISSION_UV_TRANSFORM + uv = u_TransmissionUVTransform * uv; +#endif + return uv.xy; +} + +#endif + + +// Volume Material + + +#ifdef MATERIAL_VOLUME + +uniform sampler2D u_ThicknessSampler; +uniform int u_ThicknessUVSet; +uniform mat3 u_ThicknessUVTransform; + + +vec2 getThicknessUV() +{ + vec3 uv = vec3(u_ThicknessUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_THICKNESS_UV_TRANSFORM + uv = u_ThicknessUVTransform * uv; +#endif + return uv.xy; +} + +#endif + + +// Iridescence + + +#ifdef MATERIAL_IRIDESCENCE + +uniform sampler2D u_IridescenceSampler; +uniform int u_IridescenceUVSet; +uniform mat3 u_IridescenceUVTransform; + +uniform sampler2D u_IridescenceThicknessSampler; +uniform int u_IridescenceThicknessUVSet; +uniform mat3 u_IridescenceThicknessUVTransform; + + +vec2 getIridescenceUV() +{ + vec3 uv = vec3(u_IridescenceUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_IRIDESCENCE_UV_TRANSFORM + uv = u_IridescenceUVTransform * uv; +#endif + return uv.xy; +} + +vec2 getIridescenceThicknessUV() +{ + vec3 uv = vec3(u_IridescenceThicknessUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_IRIDESCENCETHICKNESS_UV_TRANSFORM + uv = u_IridescenceThicknessUVTransform * uv; +#endif + return uv.xy; +} + +#endif + + +// Diffuse Transmission + +#ifdef MATERIAL_DIFFUSE_TRANSMISSION + +uniform sampler2D u_DiffuseTransmissionSampler; +uniform int u_DiffuseTransmissionUVSet; +uniform mat3 u_DiffuseTransmissionUVTransform; + +uniform sampler2D u_DiffuseTransmissionColorSampler; +uniform int u_DiffuseTransmissionColorUVSet; +uniform mat3 u_DiffuseTransmissionColorUVTransform; + + +vec2 getDiffuseTransmissionUV() +{ + vec3 uv = vec3(u_DiffuseTransmissionUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_DIFFUSETRANSMISSION_UV_TRANSFORM + uv = u_DiffuseTransmissionUVTransform * uv; +#endif + return uv.xy; +} + +vec2 getDiffuseTransmissionColorUV() +{ + vec3 uv = vec3(u_DiffuseTransmissionColorUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_DIFFUSETRANSMISSIONCOLOR_UV_TRANSFORM + uv = u_DiffuseTransmissionColorUVTransform * uv; +#endif + return uv.xy; +} + +#endif + +// Anisotropy + +#ifdef MATERIAL_ANISOTROPY + +uniform sampler2D u_AnisotropySampler; +uniform int u_AnisotropyUVSet; +uniform mat3 u_AnisotropyUVTransform; + +vec2 getAnisotropyUV() +{ + vec3 uv = vec3(u_AnisotropyUVSet < 1 ? v_texcoord_0 : v_texcoord_1, 1.0); +#ifdef HAS_ANISOTROPY_UV_TRANSFORM + uv = u_AnisotropyUVTransform * uv; +#endif + return uv.xy; +} + +#endif diff --git a/src/viewer3d/assets/tonemapping.glsl b/src/viewer3d/assets/tonemapping.glsl new file mode 100644 index 00000000..30d39afa --- /dev/null +++ b/src/viewer3d/assets/tonemapping.glsl @@ -0,0 +1,135 @@ +uniform float u_Exposure; + + +const float GAMMA = 2.2; +const float INV_GAMMA = 1.0 / GAMMA; + + +// sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT +const mat3 ACESInputMat = mat3 +( + 0.59719, 0.07600, 0.02840, + 0.35458, 0.90834, 0.13383, + 0.04823, 0.01566, 0.83777 +); + + +// ODT_SAT => XYZ => D60_2_D65 => sRGB +const mat3 ACESOutputMat = mat3 +( + 1.60475, -0.10208, -0.00327, + -0.53108, 1.10813, -0.07276, + -0.07367, -0.00605, 1.07602 +); + + +// linear to sRGB approximation +// see http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html +vec3 linearTosRGB(vec3 color) +{ + return pow(color, vec3(INV_GAMMA)); +} + + +// sRGB to linear approximation +// see http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html +vec3 sRGBToLinear(vec3 srgbIn) +{ + return vec3(pow(srgbIn.xyz, vec3(GAMMA))); +} + + +vec4 sRGBToLinear(vec4 srgbIn) +{ + return vec4(sRGBToLinear(srgbIn.xyz), srgbIn.w); +} + + +// ACES tone map (faster approximation) +// see: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/ +vec3 toneMapACES_Narkowicz(vec3 color) +{ + const float A = 2.51; + const float B = 0.03; + const float C = 2.43; + const float D = 0.59; + const float E = 0.14; + return clamp((color * (A * color + B)) / (color * (C * color + D) + E), 0.0, 1.0); +} + + +// ACES filmic tone map approximation +// see https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl +vec3 RRTAndODTFit(vec3 color) +{ + vec3 a = color * (color + 0.0245786) - 0.000090537; + vec3 b = color * (0.983729 * color + 0.4329510) + 0.238081; + return a / b; +} + + +// tone mapping +vec3 toneMapACES_Hill(vec3 color) +{ + color = ACESInputMat * color; + + // Apply RRT and ODT + color = RRTAndODTFit(color); + + color = ACESOutputMat * color; + + // Clamp to [0, 1] + color = clamp(color, 0.0, 1.0); + + return color; +} + +// Khronos PBR neutral tone mapping +#ifdef TONEMAP_KHR_PBR_NEUTRAL +vec3 toneMap_KhronosPbrNeutral( vec3 color ) +{ + const float startCompression = 0.8 - 0.04; + const float desaturation = 0.15; + + float x = min(color.r, min(color.g, color.b)); + float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; + color -= offset; + + float peak = max(color.r, max(color.g, color.b)); + if (peak < startCompression) return color; + + const float d = 1. - startCompression; + float newPeak = 1. - d * d / (peak + d - startCompression); + color *= newPeak / peak; + + float g = 1. - 1. / (desaturation * (peak - newPeak) + 1.); + return mix(color, newPeak * vec3(1, 1, 1), g); +} +#endif + +vec3 toneMap(vec3 color) +{ + color *= u_Exposure; + +#ifdef TONEMAP_ACES_NARKOWICZ + color = toneMapACES_Narkowicz(color); +#endif + +#ifdef TONEMAP_ACES_HILL + color = toneMapACES_Hill(color); +#endif + +#ifdef TONEMAP_ACES_HILL_EXPOSURE_BOOST + // boost exposure as discussed in https://github.com/mrdoob/three.js/pull/19621 + // this factor is based on the exposure correction of Krzysztof Narkowicz in his + // implemetation of ACES tone mapping + color /= 0.6; + color = toneMapACES_Hill(color); +#endif + +#ifdef TONEMAP_KHR_PBR_NEUTRAL + color = toneMap_KhronosPbrNeutral(color); +#endif + + return linearTosRGB(color); +} diff --git a/src/viewer3d/geometry/cube.cpp b/src/viewer3d/geometry/cube.cpp new file mode 100644 index 00000000..ff3fef8e --- /dev/null +++ b/src/viewer3d/geometry/cube.cpp @@ -0,0 +1,157 @@ +#include "../renderer/renderer.h" +#include "geometry.h" +#include +#include +#include +#include +#include + +#define BUFFER_OFFSET(i) ((char*)NULL + (i)) + +Mesh* createCube(QOpenGLFunctions* gl, float width, float height, float depth, + int widthSegments, int heightSegments, int depthSegments) +{ + widthSegments = std::max(1, widthSegments); + heightSegments = std::max(1, heightSegments); + depthSegments = std::max(1, depthSegments); + + // Interleaved layout per vertex: pos(3) normal(3) uv(2) tangent(4) = 12 floats + static constexpr int kFloatsPerVertex = 12; + static constexpr int kStride = kFloatsPerVertex * sizeof(float); + + const int wS = widthSegments, hS = heightSegments, dS = depthSegments; + const int totalVertices = 2 * ((dS+1)*(hS+1) + (wS+1)*(dS+1) + (wS+1)*(hS+1)); + const int totalIndices = 12 * (dS*hS + wS*dS + wS*hS); + + std::vector interleaved; + std::vector indices; + interleaved.reserve(totalVertices * kFloatsPerVertex); + indices.reserve(totalIndices); + + int vertexOffset = 0; + + // u, v, w are axis indices (0=x,1=y,2=z) that map the face's local axes + // onto world space. udir/vdir flip the winding. depth is the face offset. + auto buildPlane = [&](int u, int v, int w, int udir, int vdir, + float faceW, float faceH, float faceD, + int gridX, int gridY) + { + const float segW = faceW / gridX; + const float segH = faceH / gridY; + const float halfW = faceW / 2.0f; + const float halfH = faceH / 2.0f; + const float halfD = faceD / 2.0f; + const int gridX1 = gridX + 1; + const int gridY1 = gridY + 1; + + // Normal and tangent are constant across the face — compute once. + float norm[3] = {}; + norm[w] = faceD > 0.0f ? 1.0f : -1.0f; + + float tang[3] = {}; + tang[u] = (float)udir; + + for (int iy = 0; iy < gridY1; iy++) { + const float y = iy * segH - halfH; + + for (int ix = 0; ix < gridX1; ix++) { + const float x = ix * segW - halfW; + + // position + float pos[3] = {}; + pos[u] = x * udir; + pos[v] = y * vdir; + pos[w] = halfD; + interleaved.push_back(pos[0]); + interleaved.push_back(pos[1]); + interleaved.push_back(pos[2]); + + // normal + interleaved.push_back(norm[0]); + interleaved.push_back(norm[1]); + interleaved.push_back(norm[2]); + + // uv + interleaved.push_back(ix / (float)gridX); + interleaved.push_back(1.0f - (iy / (float)gridY)); + + // tangent + interleaved.push_back(tang[0]); + interleaved.push_back(tang[1]); + interleaved.push_back(tang[2]); + interleaved.push_back(1.0f); + } + } + + for (int iy = 0; iy < gridY; iy++) { + for (int ix = 0; ix < gridX; ix++) { + const unsigned int a = vertexOffset + ix + gridX1 * iy; + const unsigned int b = vertexOffset + ix + gridX1 * (iy + 1); + const unsigned int c = vertexOffset + (ix + 1) + gridX1 * (iy + 1); + const unsigned int d = vertexOffset + (ix + 1) + gridX1 * iy; + + indices.push_back(a); + indices.push_back(b); + indices.push_back(d); + + indices.push_back(b); + indices.push_back(c); + indices.push_back(d); + } + } + + vertexOffset += gridX1 * gridY1; + }; + + buildPlane(2, 1, 0, -1, -1, depth, height, width, depthSegments, heightSegments); // px + buildPlane(2, 1, 0, 1, -1, depth, height, -width, depthSegments, heightSegments); // nx + buildPlane(0, 2, 1, 1, 1, width, depth, height, widthSegments, depthSegments); // py + buildPlane(0, 2, 1, 1, -1, width, depth, -height, widthSegments, depthSegments); // ny + buildPlane(0, 1, 2, 1, -1, width, height, depth, widthSegments, heightSegments); // pz + buildPlane(0, 1, 2, -1, -1, width, height, -depth, widthSegments, heightSegments); // nz + + QOpenGLVertexArrayObject* vao = new QOpenGLVertexArrayObject(); + vao->create(); + vao->bind(); + + auto vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(interleaved.data(), (int)(interleaved.size() * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(0)); + + gl->glEnableVertexAttribArray((int)VertexUsage::Normal); + gl->glVertexAttribPointer((int)VertexUsage::Normal, 3, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(3 * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(6 * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::Tangent); + gl->glVertexAttribPointer((int)VertexUsage::Tangent, 4, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(8 * sizeof(float))); + + vao->release(); + + auto ibo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer); + ibo->create(); + ibo->bind(); + ibo->setUsagePattern(QOpenGLBuffer::StaticDraw); + ibo->allocate(indices.data(), (int)(indices.size() * sizeof(unsigned int))); + + auto mesh = new Mesh(); + mesh->vao = vao; + mesh->meshType = MeshType::Generated; + mesh->indexBuffer = ibo; + mesh->numElements = (int)indices.size(); + mesh->indexByteOffset = 0; + mesh->indexType = GL_UNSIGNED_INT; + mesh->primitiveMode = GL_TRIANGLES; + + return mesh; +} diff --git a/src/viewer3d/geometry/cylinder.cpp b/src/viewer3d/geometry/cylinder.cpp new file mode 100644 index 00000000..1f4adac5 --- /dev/null +++ b/src/viewer3d/geometry/cylinder.cpp @@ -0,0 +1,172 @@ +#include "../renderer/renderer.h" +#include "geometry.h" +#include +#include +#include +#include +#include +#include + +#define BUFFER_OFFSET(i) ((char*)NULL + (i)) + +Mesh* createCylinder(QOpenGLFunctions* gl, float radiusTop, float radiusBottom, + float height, int radialSegments, int heightSegments, + float bevelRadius, int bevelSegments, + float uvScaleU, float uvScaleV) +{ + radialSegments = std::max(3, radialSegments); + heightSegments = std::max(1, heightSegments); + bevelSegments = std::max(1, bevelSegments); + bevelRadius = std::min({bevelRadius, radiusTop, radiusBottom, height / 2.0f}); + + // Interleaved layout per vertex: pos(3) normal(3) uv(2) tangent(4) = 12 floats + static constexpr int kFloatsPerVertex = 12; + static constexpr int kStride = kFloatsPerVertex * sizeof(float); + + // Ring layout top→bottom: + // top bevel: bevelSegments+1 rings (0 .. bevelSegments) + // torso: heightSegments rings (bevelSegments+1 .. bevelSegments+heightSegments) + // bottom bevel: bevelSegments rings (bevelSegments+heightSegments+1 .. 2*bevelSegments+heightSegments) + // Junction rings are owned by the preceding section; the next section skips ring 0. + const int totalRings = 2 * bevelSegments + heightSegments + 1; + const int colCount = radialSegments + 1; + const int totalVertices = totalRings * colCount; + const int totalIndices = (totalRings - 1) * radialSegments * 6; + + std::vector interleaved; + std::vector indices; + interleaved.reserve(totalVertices * kFloatsPerVertex); + indices.reserve(totalIndices); + + // Precompute sin/cos per column — reused by all sections. + std::vector sinT(colCount), cosT(colCount); + for (int x = 0; x < colCount; x++) { + const float theta = (x / (float)radialSegments) * (float)(M_PI * 2.0); + sinT[x] = std::sin(theta); + cosT[x] = std::cos(theta); + } + + const float halfHeight = height / 2.0f; + + // Slope for the torso normal (constant; 0 for a true cylinder). + const float slope = std::atan2(radiusBottom - radiusTop, height); + const float cosSlope = std::cos(slope); + const float sinSlope = std::sin(slope); + + // Push one full ring. normR = outward radial scale, normY = vertical normal component. + int ringIndex = 0; + auto pushRing = [&](float r, float y, float normR, float normY) { + const float globalV = ringIndex / (float)(totalRings - 1); + for (int x = 0; x < colCount; x++) { + const float s = sinT[x], c = cosT[x]; + + interleaved.push_back(r * s); + interleaved.push_back(y); + interleaved.push_back(r * c); + + interleaved.push_back(normR * s); + interleaved.push_back(normY); + interleaved.push_back(normR * c); + + interleaved.push_back((x / (float)radialSegments) * uvScaleU); + interleaved.push_back((1.0f - globalV) * uvScaleV); + + interleaved.push_back(c); + interleaved.push_back(0.0f); + interleaved.push_back(-s); + interleaved.push_back(1.0f); + } + ringIndex++; + }; + + // ---- Top bevel (rings 0..bevelSegments) ---- + // phi=PI/2 → top rim; phi=0 → torso junction. + for (int i = 0; i <= bevelSegments; i++) { + const float phi = (float)(bevelSegments - i) / bevelSegments * (float)(M_PI / 2.0); + const float r = (radiusTop - bevelRadius) + bevelRadius * std::cos(phi); + const float y = (halfHeight - bevelRadius) + bevelRadius * std::sin(phi); + pushRing(r, y, std::cos(phi), std::sin(phi)); + } + + // ---- Torso (rings bevelSegments+1..bevelSegments+heightSegments) ---- + // Skip j=0 — that junction ring was already pushed by the top bevel. + for (int j = 1; j <= heightSegments; j++) { + const float v = j / (float)heightSegments; + const float r = v * (radiusBottom - radiusTop) + radiusTop; + const float y = (halfHeight - bevelRadius) - v * (height - 2.0f * bevelRadius); + pushRing(r, y, cosSlope, sinSlope); + } + + // ---- Bottom bevel (rings bevelSegments+heightSegments+1..2*bevelSegments+heightSegments) ---- + // Skip k=0 — that junction ring was already pushed by the torso. + // phi=0 → torso junction; phi=PI/2 → bottom rim. + for (int k = 1; k <= bevelSegments; k++) { + const float phi = (float)k / bevelSegments * (float)(M_PI / 2.0); + const float r = (radiusBottom - bevelRadius) + bevelRadius * std::cos(phi); + const float y = -(halfHeight - bevelRadius) - bevelRadius * std::sin(phi); + pushRing(r, y, std::cos(phi), -std::sin(phi)); + } + + // ---- Indices: connect every adjacent pair of rings ---- + for (int r = 0; r < totalRings - 1; r++) { + for (int x = 0; x < radialSegments; x++) { + const unsigned int a = r * colCount + x; + const unsigned int b = (r + 1) * colCount + x; + const unsigned int c = (r + 1) * colCount + x + 1; + const unsigned int d = r * colCount + x + 1; + + indices.push_back(a); + indices.push_back(b); + indices.push_back(d); + + indices.push_back(b); + indices.push_back(c); + indices.push_back(d); + } + } + + QOpenGLVertexArrayObject* vao = new QOpenGLVertexArrayObject(); + vao->create(); + vao->bind(); + + auto vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(interleaved.data(), (int)(interleaved.size() * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(0)); + + gl->glEnableVertexAttribArray((int)VertexUsage::Normal); + gl->glVertexAttribPointer((int)VertexUsage::Normal, 3, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(3 * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(6 * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::Tangent); + gl->glVertexAttribPointer((int)VertexUsage::Tangent, 4, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(8 * sizeof(float))); + + vao->release(); + + auto ibo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer); + ibo->create(); + ibo->bind(); + ibo->setUsagePattern(QOpenGLBuffer::StaticDraw); + ibo->allocate(indices.data(), (int)(indices.size() * sizeof(unsigned int))); + + auto mesh = new Mesh(); + mesh->vao = vao; + mesh->meshType = MeshType::Generated; + mesh->indexBuffer = ibo; + mesh->numElements = (int)indices.size(); + mesh->indexByteOffset = 0; + mesh->indexType = GL_UNSIGNED_INT; + mesh->primitiveMode = GL_TRIANGLES; + + return mesh; +} diff --git a/src/viewer3d/geometry/geometry.h b/src/viewer3d/geometry/geometry.h new file mode 100644 index 00000000..6abc3296 --- /dev/null +++ b/src/viewer3d/geometry/geometry.h @@ -0,0 +1,48 @@ +#pragma once +#include + +class Mesh; +class QOpenGLFunctions; + +enum class PlaneOrientation { + XY, // Normal facing Z + YZ, // Normal facing X + XZ // Normal facing Y +}; + +// https://github.com/mrdoob/three.js/blob/master/src/geometries/SphereGeometry.js +Mesh* createSphere(QOpenGLFunctions* gl, float radius = 1, + int widthSegments = 32, int heightSegments = 16, + float phiStart = 0, float phiLength = M_PI * 2, + float thetaStart = 0, float thetaLength = M_PI); + +// Create a subdivided plane mesh with normals, tangents, and UVs +// https://github.com/mrdoob/three.js/blob/master/src/geometries/PlaneGeometry.js +Mesh* createPlane(QOpenGLFunctions* gl, float width = 1, float height = 1, + int widthSegments = 1, int heightSegments = 1, + PlaneOrientation orientation = PlaneOrientation::XY); + +// Create a cylinder mesh with normals, tangents, and UVs +// https://github.com/mrdoob/three.js/blob/master/src/geometries/CylinderGeometry.js +Mesh* createCylinder(QOpenGLFunctions* gl, float radiusTop = 1, + float radiusBottom = 1, float height = 1, + int radialSegments = 32, int heightSegments = 1, + float bevelRadius = 0.1f, int bevelSegments = 4, + float uvScaleU = 3.0f, float uvScaleV = 1.0f); + +// Create a subdivided cube mesh with normals, tangents, and UVs +// https://github.com/mrdoob/three.js/blob/master/src/geometries/BoxGeometry.js +Mesh* createCube(QOpenGLFunctions* gl, float width = 1, float height = 1, + float depth = 1, int widthSegments = 1, int heightSegments = 1, + int depthSegments = 1); + +// Create a skydome (inverted sphere) for rendering environment maps +Mesh* createSkydome(QOpenGLFunctions* gl, float radius = 100, + int widthSegments = 32, int heightSegments = 16); + +// other sources: +// https://www.danielsieger.com/blog/2021/03/27/generating-spheres.html +// https://schneide.blog/2016/07/15/generating-an-icosphere-in-c/ +// https://github.com/caosdoar/spheres/blob/master/src/spheres.cpp +// VERY GOOD: +// http://www.songho.ca/opengl/gl_sphere.html \ No newline at end of file diff --git a/src/viewer3d/geometry/plane.cpp b/src/viewer3d/geometry/plane.cpp new file mode 100644 index 00000000..b254592d --- /dev/null +++ b/src/viewer3d/geometry/plane.cpp @@ -0,0 +1,157 @@ +#include "../renderer/renderer.h" +#include "geometry.h" +#include +#include +#include +#include +#include + +#define BUFFER_OFFSET(i) ((char*)NULL + (i)) + +Mesh* createPlane(QOpenGLFunctions* gl, float width, float height, + int widthSegments, int heightSegments, + PlaneOrientation orientation) +{ + widthSegments = std::max(1, widthSegments); + heightSegments = std::max(1, heightSegments); + + const float width_half = width / 2.0f; + const float height_half = height / 2.0f; + + const int gridX1 = widthSegments + 1; + const int gridY1 = heightSegments + 1; + + const float segment_width = width / widthSegments; + const float segment_height = height / heightSegments; + + const int vertexCount = gridX1 * gridY1; + + // Interleaved layout per vertex: pos(3) normal(3) uv(2) tangent(4) = 12 floats + static constexpr int kFloatsPerVertex = 12; + static constexpr int kStride = kFloatsPerVertex * sizeof(float); + + std::vector interleaved; + interleaved.reserve(vertexCount * kFloatsPerVertex); + + std::vector indices; + indices.reserve(widthSegments * heightSegments * 6); + + // Precompute orientation-dependent constants to keep the inner loop branch-free. + float nx, ny, nz; + float tx, ty, tz; + const bool flipV = (orientation != PlaneOrientation::XZ); + + if (orientation == PlaneOrientation::XY) { + nx = 0.0f; ny = 0.0f; nz = -1.0f; + tx = 1.0f; ty = 0.0f; tz = 0.0f; + } else if (orientation == PlaneOrientation::YZ) { + nx = -1.0f; ny = 0.0f; nz = 0.0f; + tx = 0.0f; ty = 0.0f; tz = 1.0f; + } else { // XZ + nx = 0.0f; ny = 1.0f; nz = 0.0f; + tx = 1.0f; ty = 0.0f; tz = 0.0f; + } + + for (int iy = 0; iy < gridY1; iy++) { + const float v = iy * segment_height - height_half; + + for (int ix = 0; ix < gridX1; ix++) { + const float u = ix * segment_width - width_half; + + // position + if (orientation == PlaneOrientation::XY) { + interleaved.push_back(u); + interleaved.push_back(-v); + interleaved.push_back(0.0f); + } else if (orientation == PlaneOrientation::YZ) { + interleaved.push_back(0.0f); + interleaved.push_back(-v); + interleaved.push_back(u); + } else { + interleaved.push_back(u); + interleaved.push_back(0.0f); + interleaved.push_back(v); + } + + // normal + interleaved.push_back(nx); + interleaved.push_back(ny); + interleaved.push_back(nz); + + // uv + const float uvx = ix / (float)widthSegments; + const float uvy = flipV ? 1.0f - (iy / (float)heightSegments) + : iy / (float)heightSegments; + interleaved.push_back(uvx); + interleaved.push_back(uvy); + + // tangent + interleaved.push_back(tx); + interleaved.push_back(ty); + interleaved.push_back(tz); + interleaved.push_back(1.0f); + } + } + + for (int iy = 0; iy < heightSegments; iy++) { + for (int ix = 0; ix < widthSegments; ix++) { + const unsigned int a = ix + gridX1 * iy; + const unsigned int b = ix + gridX1 * (iy + 1); + const unsigned int c = (ix + 1) + gridX1 * (iy + 1); + const unsigned int d = (ix + 1) + gridX1 * iy; + + indices.push_back(a); + indices.push_back(b); + indices.push_back(d); + + indices.push_back(b); + indices.push_back(c); + indices.push_back(d); + } + } + + QOpenGLVertexArrayObject* vao = new QOpenGLVertexArrayObject(); + vao->create(); + vao->bind(); + + auto vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(interleaved.data(), (int)(interleaved.size() * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(0)); + + gl->glEnableVertexAttribArray((int)VertexUsage::Normal); + gl->glVertexAttribPointer((int)VertexUsage::Normal, 3, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(3 * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(6 * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::Tangent); + gl->glVertexAttribPointer((int)VertexUsage::Tangent, 4, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(8 * sizeof(float))); + + vao->release(); + + auto ibo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer); + ibo->create(); + ibo->bind(); + ibo->setUsagePattern(QOpenGLBuffer::StaticDraw); + ibo->allocate(indices.data(), (int)(indices.size() * sizeof(unsigned int))); + + auto mesh = new Mesh(); + mesh->vao = vao; + mesh->meshType = MeshType::Generated; + mesh->indexBuffer = ibo; + mesh->numElements = (int)indices.size(); + mesh->indexByteOffset = 0; + mesh->indexType = GL_UNSIGNED_INT; + mesh->primitiveMode = GL_TRIANGLES; + + return mesh; +} diff --git a/src/viewer3d/geometry/skydome.cpp b/src/viewer3d/geometry/skydome.cpp new file mode 100644 index 00000000..750e28c4 --- /dev/null +++ b/src/viewer3d/geometry/skydome.cpp @@ -0,0 +1,155 @@ +#include "../renderer/renderer.h" +#include "geometry.h" +#include +#include +#include +#include +#include +#include + +#define BUFFER_OFFSET(i) ((char*)NULL + (i)) + +Mesh* createSkydome(QOpenGLFunctions* gl, float radius, int widthSegments, + int heightSegments) +{ + widthSegments = std::max(3, (int)std::floor(widthSegments)); + heightSegments = std::max(2, (int)std::floor(heightSegments)); + + int index = 0; + QVector> grid; + + // buffers + QVector indices; + QVector vertices; + QVector normals; + QVector tangents; + QVector uvs; + + // Generate vertices for an inverted sphere (skydome) + for (int iy = 0; iy <= heightSegments; iy++) { + QVector verticesRow; + + float v = iy / (float)heightSegments; + float theta = v * M_PI; + + for (int ix = 0; ix <= widthSegments; ix++) { + float u = ix / (float)widthSegments; + float phi = u * M_PI * 2.0f; + + // Vertex position (standard sphere) + float x = -radius * std::cos(phi) * std::sin(theta); + float y = radius * std::cos(theta); + float z = radius * std::sin(phi) * std::sin(theta); + + vertices.append(x); + vertices.append(y); + vertices.append(z); + + // Normal pointing inward (inverted) + QVector3D normal(-x, -y, -z); + normal.normalize(); + normals.append(normal.x()); + normals.append(normal.y()); + normals.append(normal.z()); + + // Tangent + QVector3D tangent(std::sin(phi), 0.0f, std::cos(phi)); + tangent.normalize(); + tangents.append(tangent.x()); + tangents.append(tangent.y()); + tangents.append(tangent.z()); + tangents.append(1.0f); + + // UV coordinates + uvs.append(u); + uvs.append(v); + + verticesRow.append(index++); + } + + grid.append(verticesRow); + } + + // Generate indices (reversed winding order for inverted sphere) + for (int iy = 0; iy < heightSegments; iy++) { + for (int ix = 0; ix < widthSegments; ix++) { + unsigned int a = grid[iy][ix + 1]; + unsigned int b = grid[iy][ix]; + unsigned int c = grid[iy + 1][ix]; + unsigned int d = grid[iy + 1][ix + 1]; + + // Reversed winding order compared to normal sphere + if (iy != 0) + indices.append({b, a, d}); + if (iy != heightSegments - 1) + indices.append({c, b, d}); + } + } + + // Build OpenGL buffers + QOpenGLVertexArrayObject* vao = new QOpenGLVertexArrayObject(); + vao->create(); + vao->bind(); + + QOpenGLBuffer* vbo; + + // Position buffer + vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(vertices.data(), vertices.length() * sizeof(float)); + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, GL_FALSE, + 3 * sizeof(float), BUFFER_OFFSET(0)); + + // Normal buffer + vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(normals.data(), normals.length() * sizeof(float)); + gl->glEnableVertexAttribArray((int)VertexUsage::Normal); + gl->glVertexAttribPointer((int)VertexUsage::Normal, 3, GL_FLOAT, GL_FALSE, + 3 * sizeof(float), BUFFER_OFFSET(0)); + + // UV buffer + vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(uvs.data(), uvs.length() * sizeof(float)); + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, + GL_FALSE, 2 * sizeof(float), BUFFER_OFFSET(0)); + + // Tangent buffer + vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(tangents.data(), tangents.length() * sizeof(float)); + gl->glEnableVertexAttribArray((int)VertexUsage::Tangent); + gl->glVertexAttribPointer((int)VertexUsage::Tangent, 4, GL_FLOAT, GL_FALSE, + 4 * sizeof(float), BUFFER_OFFSET(0)); + + vao->release(); + + // Index buffer + auto ibo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer); + ibo->create(); + ibo->bind(); + ibo->setUsagePattern(QOpenGLBuffer::StaticDraw); + ibo->allocate(indices.data(), indices.length() * sizeof(unsigned int)); + + auto mesh = new Mesh(); + mesh->vao = vao; + mesh->meshType = MeshType::Generated; + mesh->indexBuffer = ibo; + mesh->numElements = indices.count(); + mesh->indexByteOffset = 0; + mesh->indexType = GL_UNSIGNED_INT; + mesh->primitiveMode = GL_TRIANGLES; + + return mesh; +} diff --git a/src/viewer3d/geometry/sphere.cpp b/src/viewer3d/geometry/sphere.cpp new file mode 100644 index 00000000..1d59e65a --- /dev/null +++ b/src/viewer3d/geometry/sphere.cpp @@ -0,0 +1,162 @@ +#include "../renderer/renderer.h" +#include "geometry.h" +#include +#include +#include +#include +#include +#include + +#define BUFFER_OFFSET(i) ((char*)NULL + (i)) + +Mesh* createSphere(QOpenGLFunctions* gl, float radius, int widthSegments, + int heightSegments, float phiStart, float phiLength, + float thetaStart, float thetaLength) +{ + const float uvScaleX = 2.0f; + const float uvScaleY = 1.0f; + + widthSegments = std::max(3, widthSegments); + heightSegments = std::max(2, heightSegments); + + const double thetaEnd = std::min((double)thetaStart + thetaLength, M_PI); + + const int ringCount = heightSegments + 1; + const int colCount = widthSegments + 1; + const int vertexCount = ringCount * colCount; + + // Precompute trig per column (phi) and per ring (theta) to avoid + // redundant sin/cos calls inside the double loop. + std::vector sinPhi(colCount), cosPhi(colCount); + for (int ix = 0; ix < colCount; ix++) { + float phi = phiStart + (ix / (float)widthSegments) * phiLength; + sinPhi[ix] = std::sin(phi); + cosPhi[ix] = std::cos(phi); + } + std::vector sinTheta(ringCount), cosTheta(ringCount); + for (int iy = 0; iy < ringCount; iy++) { + float theta = thetaStart + (iy / (float)heightSegments) * thetaLength; + sinTheta[iy] = std::sin(theta); + cosTheta[iy] = std::cos(theta); + } + + // Interleaved layout per vertex: pos(3) normal(3) uv(2) tangent(4) = 12 floats + static constexpr int kFloatsPerVertex = 12; + static constexpr int kStride = kFloatsPerVertex * sizeof(float); + + std::vector interleaved; + interleaved.reserve(vertexCount * kFloatsPerVertex); + + // Upper-bound index count: 6 per quad + std::vector indices; + indices.reserve(widthSegments * heightSegments * 6); + + for (int iy = 0; iy < ringCount; iy++) { + const float v = iy / (float)heightSegments; + const float sinT = sinTheta[iy]; + const float cosT = cosTheta[iy]; + + float uOffset = 0.0f; + if (iy == 0 && thetaStart == 0.0f) + uOffset = 0.5f / widthSegments; + else if (iy == heightSegments && thetaEnd == M_PI) + uOffset = -0.5f / widthSegments; + + for (int ix = 0; ix < colCount; ix++) { + const float u = ix / (float)widthSegments; + const float sP = sinPhi[ix]; + const float cP = cosPhi[ix]; + + // Position + const float x = -radius * cP * sinT; + const float y = radius * cosT; + const float z = radius * sP * sinT; + + interleaved.push_back(x); + interleaved.push_back(y); + interleaved.push_back(z); + + // Normal = position / radius (already unit length for a sphere) + interleaved.push_back(x / radius); + interleaved.push_back(y / radius); + interleaved.push_back(z / radius); + + // UV + interleaved.push_back((u + uOffset) * uvScaleX); + interleaved.push_back((1.0f - v) * uvScaleY); + + // Tangent = d(pos)/d(phi) normalized = (sin(phi), 0, cos(phi), 1) + // Already unit length: sqrt(sin²+cos²) = 1, no normalize needed. + interleaved.push_back(sP); + interleaved.push_back(0.0f); + interleaved.push_back(cP); + interleaved.push_back(1.0f); + } + } + + // Build indices with flat grid math — no 2D intermediate vector needed. + for (int iy = 0; iy < heightSegments; iy++) { + for (int ix = 0; ix < widthSegments; ix++) { + const unsigned int a = iy * colCount + ix + 1; + const unsigned int b = iy * colCount + ix; + const unsigned int c = (iy + 1) * colCount + ix; + const unsigned int d = (iy + 1) * colCount + ix + 1; + + if (iy != 0 || thetaStart > 0.0f) { + indices.push_back(a); + indices.push_back(b); + indices.push_back(d); + } + if (iy != heightSegments - 1 || thetaEnd < M_PI) { + indices.push_back(b); + indices.push_back(c); + indices.push_back(d); + } + } + } + + QOpenGLVertexArrayObject* vao = new QOpenGLVertexArrayObject(); + vao->create(); + vao->bind(); + + auto vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(interleaved.data(), (int)(interleaved.size() * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(0)); + + gl->glEnableVertexAttribArray((int)VertexUsage::Normal); + gl->glVertexAttribPointer((int)VertexUsage::Normal, 3, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(3 * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(6 * sizeof(float))); + + gl->glEnableVertexAttribArray((int)VertexUsage::Tangent); + gl->glVertexAttribPointer((int)VertexUsage::Tangent, 4, GL_FLOAT, GL_FALSE, + kStride, BUFFER_OFFSET(8 * sizeof(float))); + + vao->release(); + + auto ibo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer); + ibo->create(); + ibo->bind(); + ibo->setUsagePattern(QOpenGLBuffer::StaticDraw); + ibo->allocate(indices.data(), (int)(indices.size() * sizeof(unsigned int))); + + auto mesh = new Mesh(); + mesh->vao = vao; + mesh->meshType = MeshType::Generated; + mesh->indexBuffer = ibo; + mesh->numElements = (int)indices.size(); + mesh->indexByteOffset = 0; + mesh->indexType = GL_UNSIGNED_INT; + mesh->primitiveMode = GL_TRIANGLES; + + return mesh; +} diff --git a/src/components/ExportMenu.vue b/src/viewer3d/gltf/gltf.cpp similarity index 100% rename from src/components/ExportMenu.vue rename to src/viewer3d/gltf/gltf.cpp diff --git a/src/viewer3d/gltf/json.hpp b/src/viewer3d/gltf/json.hpp new file mode 100644 index 00000000..26684068 --- /dev/null +++ b/src/viewer3d/gltf/json.hpp @@ -0,0 +1,26753 @@ +/* + __ _____ _____ _____ + __| | __| | | | JSON for Modern C++ +| | |__ | | | | | | version 3.10.4 +|_____|_____|_____|_|___| https://github.com/nlohmann/json + +Licensed under the MIT License . +SPDX-License-Identifier: MIT +Copyright (c) 2013-2019 Niels Lohmann . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#ifndef INCLUDE_NLOHMANN_JSON_HPP_ +#define INCLUDE_NLOHMANN_JSON_HPP_ + +#define NLOHMANN_JSON_VERSION_MAJOR 3 +#define NLOHMANN_JSON_VERSION_MINOR 10 +#define NLOHMANN_JSON_VERSION_PATCH 4 + +#include // all_of, find, for_each +#include // nullptr_t, ptrdiff_t, size_t +#include // hash, less +#include // initializer_list +#ifndef JSON_NO_IO + #include // istream, ostream +#endif // JSON_NO_IO +#include // random_access_iterator_tag +#include // unique_ptr +#include // accumulate +#include // string, stoi, to_string +#include // declval, forward, move, pair, swap +#include // vector + +// #include + + +#include +#include + +// #include + + +#include // transform +#include // array +#include // forward_list +#include // inserter, front_inserter, end +#include // map +#include // string +#include // tuple, make_tuple +#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible +#include // unordered_map +#include // pair, declval +#include // valarray + +// #include + + +#include // exception +#include // runtime_error +#include // to_string +#include // vector + +// #include + + +#include // array +#include // size_t +#include // uint8_t +#include // string + +namespace nlohmann +{ +namespace detail +{ +/////////////////////////// +// JSON type enumeration // +/////////////////////////// + +/*! +@brief the JSON type enumeration + +This enumeration collects the different JSON types. It is internally used to +distinguish the stored values, and the functions @ref basic_json::is_null(), +@ref basic_json::is_object(), @ref basic_json::is_array(), +@ref basic_json::is_string(), @ref basic_json::is_boolean(), +@ref basic_json::is_number() (with @ref basic_json::is_number_integer(), +@ref basic_json::is_number_unsigned(), and @ref basic_json::is_number_float()), +@ref basic_json::is_discarded(), @ref basic_json::is_primitive(), and +@ref basic_json::is_structured() rely on it. + +@note There are three enumeration entries (number_integer, number_unsigned, and +number_float), because the library distinguishes these three types for numbers: +@ref basic_json::number_unsigned_t is used for unsigned integers, +@ref basic_json::number_integer_t is used for signed integers, and +@ref basic_json::number_float_t is used for floating-point numbers or to +approximate integers which do not fit in the limits of their respective type. + +@sa see @ref basic_json::basic_json(const value_t value_type) -- create a JSON +value with the default value for a given type + +@since version 1.0.0 +*/ +enum class value_t : std::uint8_t +{ + null, ///< null value + object, ///< object (unordered set of name/value pairs) + array, ///< array (ordered collection of values) + string, ///< string value + boolean, ///< boolean value + number_integer, ///< number value (signed integer) + number_unsigned, ///< number value (unsigned integer) + number_float, ///< number value (floating-point) + binary, ///< binary array (ordered collection of bytes) + discarded ///< discarded by the parser callback function +}; + +/*! +@brief comparison operator for JSON types + +Returns an ordering that is similar to Python: +- order: null < boolean < number < object < array < string < binary +- furthermore, each type is not smaller than itself +- discarded values are not comparable +- binary is represented as a b"" string in python and directly comparable to a + string; however, making a binary array directly comparable with a string would + be surprising behavior in a JSON file. + +@since version 1.0.0 +*/ +inline bool operator<(const value_t lhs, const value_t rhs) noexcept +{ + static constexpr std::array order = {{ + 0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */, + 1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */, + 6 /* binary */ + } + }; + + const auto l_index = static_cast(lhs); + const auto r_index = static_cast(rhs); + return l_index < order.size() && r_index < order.size() && order[l_index] < order[r_index]; +} +} // namespace detail +} // namespace nlohmann + +// #include + + +#include +// #include + + +#include // declval, pair +// #include + + +/* Hedley - https://nemequ.github.io/hedley + * Created by Evan Nemerson + * + * To the extent possible under law, the author(s) have dedicated all + * copyright and related and neighboring rights to this software to + * the public domain worldwide. This software is distributed without + * any warranty. + * + * For details, see . + * SPDX-License-Identifier: CC0-1.0 + */ + +#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 15) +#if defined(JSON_HEDLEY_VERSION) + #undef JSON_HEDLEY_VERSION +#endif +#define JSON_HEDLEY_VERSION 15 + +#if defined(JSON_HEDLEY_STRINGIFY_EX) + #undef JSON_HEDLEY_STRINGIFY_EX +#endif +#define JSON_HEDLEY_STRINGIFY_EX(x) #x + +#if defined(JSON_HEDLEY_STRINGIFY) + #undef JSON_HEDLEY_STRINGIFY +#endif +#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x) + +#if defined(JSON_HEDLEY_CONCAT_EX) + #undef JSON_HEDLEY_CONCAT_EX +#endif +#define JSON_HEDLEY_CONCAT_EX(a,b) a##b + +#if defined(JSON_HEDLEY_CONCAT) + #undef JSON_HEDLEY_CONCAT +#endif +#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b) + +#if defined(JSON_HEDLEY_CONCAT3_EX) + #undef JSON_HEDLEY_CONCAT3_EX +#endif +#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c + +#if defined(JSON_HEDLEY_CONCAT3) + #undef JSON_HEDLEY_CONCAT3 +#endif +#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c) + +#if defined(JSON_HEDLEY_VERSION_ENCODE) + #undef JSON_HEDLEY_VERSION_ENCODE +#endif +#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision)) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR) + #undef JSON_HEDLEY_VERSION_DECODE_MAJOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR) + #undef JSON_HEDLEY_VERSION_DECODE_MINOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION) + #undef JSON_HEDLEY_VERSION_DECODE_REVISION +#endif +#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000) + +#if defined(JSON_HEDLEY_GNUC_VERSION) + #undef JSON_HEDLEY_GNUC_VERSION +#endif +#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#elif defined(__GNUC__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0) +#endif + +#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK) + #undef JSON_HEDLEY_GNUC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GNUC_VERSION) + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION) + #undef JSON_HEDLEY_MSVC_VERSION +#endif +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100) +#elif defined(_MSC_FULL_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10) +#elif defined(_MSC_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK) + #undef JSON_HEDLEY_MSVC_VERSION_CHECK +#endif +#if !defined(JSON_HEDLEY_MSVC_VERSION) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0) +#elif defined(_MSC_VER) && (_MSC_VER >= 1400) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) +#elif defined(_MSC_VER) && (_MSC_VER >= 1200) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) +#else + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION) + #undef JSON_HEDLEY_INTEL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE) +#elif defined(__INTEL_COMPILER) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_VERSION) + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #undef JSON_HEDLEY_INTEL_CL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && defined(__ICL) + #define JSON_HEDLEY_INTEL_CL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER, __INTEL_COMPILER_UPDATE, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_CL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_CL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION) + #undef JSON_HEDLEY_PGI_VERSION +#endif +#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) + #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION_CHECK) + #undef JSON_HEDLEY_PGI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PGI_VERSION) + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #undef JSON_HEDLEY_SUNPRO_VERSION +#endif +#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10) +#elif defined(__SUNPRO_C) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf) +#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10) +#elif defined(__SUNPRO_CC) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK) + #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION +#endif +#if defined(__EMSCRIPTEN__) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION) + #undef JSON_HEDLEY_ARM_VERSION +#endif +#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100) +#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION_CHECK) + #undef JSON_HEDLEY_ARM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_ARM_VERSION) + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION) + #undef JSON_HEDLEY_IBM_VERSION +#endif +#if defined(__ibmxl__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__) +#elif defined(__xlC__) && defined(__xlC_ver__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) +#elif defined(__xlC__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION_CHECK) + #undef JSON_HEDLEY_IBM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IBM_VERSION) + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_VERSION) + #undef JSON_HEDLEY_TI_VERSION +#endif +#if \ + defined(__TI_COMPILER_VERSION__) && \ + ( \ + defined(__TMS470__) || defined(__TI_ARM__) || \ + defined(__MSP430__) || \ + defined(__TMS320C2000__) \ + ) +#if (__TI_COMPILER_VERSION__ >= 16000000) + #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif +#endif + +#if defined(JSON_HEDLEY_TI_VERSION_CHECK) + #undef JSON_HEDLEY_TI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_VERSION) + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #undef JSON_HEDLEY_TI_CL2000_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__) + #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #undef JSON_HEDLEY_TI_CL430_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__) + #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #undef JSON_HEDLEY_TI_ARMCL_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__)) + #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK) + #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #undef JSON_HEDLEY_TI_CL6X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__) + #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #undef JSON_HEDLEY_TI_CL7X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__) + #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #undef JSON_HEDLEY_TI_CLPRU_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__) + #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION) + #undef JSON_HEDLEY_CRAY_VERSION +#endif +#if defined(_CRAYC) + #if defined(_RELEASE_PATCHLEVEL) + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL) + #else + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK) + #undef JSON_HEDLEY_CRAY_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION) + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION) + #undef JSON_HEDLEY_IAR_VERSION +#endif +#if defined(__IAR_SYSTEMS_ICC__) + #if __VER__ > 1000 + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000)) + #else + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(__VER__ / 100, __VER__ % 100, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION_CHECK) + #undef JSON_HEDLEY_IAR_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IAR_VERSION) + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION) + #undef JSON_HEDLEY_TINYC_VERSION +#endif +#if defined(__TINYC__) + #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK) + #undef JSON_HEDLEY_TINYC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION) + #undef JSON_HEDLEY_DMC_VERSION +#endif +#if defined(__DMC__) + #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION_CHECK) + #undef JSON_HEDLEY_DMC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_DMC_VERSION) + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #undef JSON_HEDLEY_COMPCERT_VERSION +#endif +#if defined(__COMPCERT_VERSION__) + #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK) + #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION) + #undef JSON_HEDLEY_PELLES_VERSION +#endif +#if defined(__POCC__) + #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK) + #undef JSON_HEDLEY_PELLES_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PELLES_VERSION) + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #undef JSON_HEDLEY_MCST_LCC_VERSION +#endif +#if defined(__LCC__) && defined(__LCC_MINOR__) + #define JSON_HEDLEY_MCST_LCC_VERSION JSON_HEDLEY_VERSION_ENCODE(__LCC__ / 100, __LCC__ % 100, __LCC_MINOR__) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION_CHECK) + #undef JSON_HEDLEY_MCST_LCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_MCST_LCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION) + #undef JSON_HEDLEY_GCC_VERSION +#endif +#if \ + defined(JSON_HEDLEY_GNUC_VERSION) && \ + !defined(__clang__) && \ + !defined(JSON_HEDLEY_INTEL_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_ARM_VERSION) && \ + !defined(JSON_HEDLEY_CRAY_VERSION) && \ + !defined(JSON_HEDLEY_TI_VERSION) && \ + !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL430_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \ + !defined(__COMPCERT__) && \ + !defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_ATTRIBUTE +#endif +#if \ + defined(__has_attribute) && \ + ( \ + (!defined(JSON_HEDLEY_IAR_VERSION) || JSON_HEDLEY_IAR_VERSION_CHECK(8,5,9)) \ + ) +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute) +#else +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE +#endif +#if \ + defined(__has_cpp_attribute) && \ + defined(__cplusplus) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS +#endif +#if !defined(__cplusplus) || !defined(__has_cpp_attribute) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#elif \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \ + (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_BUILTIN) + #undef JSON_HEDLEY_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_HAS_BUILTIN(builtin) __has_builtin(builtin) +#else + #define JSON_HEDLEY_HAS_BUILTIN(builtin) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_BUILTIN) + #undef JSON_HEDLEY_GNUC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_BUILTIN) + #undef JSON_HEDLEY_GCC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_FEATURE) + #undef JSON_HEDLEY_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_HAS_FEATURE(feature) __has_feature(feature) +#else + #define JSON_HEDLEY_HAS_FEATURE(feature) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_FEATURE) + #undef JSON_HEDLEY_GNUC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_FEATURE) + #undef JSON_HEDLEY_GCC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_EXTENSION) + #undef JSON_HEDLEY_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_HAS_EXTENSION(extension) __has_extension(extension) +#else + #define JSON_HEDLEY_HAS_EXTENSION(extension) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_EXTENSION) + #undef JSON_HEDLEY_GNUC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_EXTENSION) + #undef JSON_HEDLEY_GCC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_WARNING) + #undef JSON_HEDLEY_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_HAS_WARNING(warning) __has_warning(warning) +#else + #define JSON_HEDLEY_HAS_WARNING(warning) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_WARNING) + #undef JSON_HEDLEY_GNUC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_WARNING) + #undef JSON_HEDLEY_GCC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + defined(__clang__) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,17) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(8,0,0) || \ + (JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) && defined(__C99_PRAGMA_OPERATOR)) + #define JSON_HEDLEY_PRAGMA(value) _Pragma(#value) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_PRAGMA(value) __pragma(value) +#else + #define JSON_HEDLEY_PRAGMA(value) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_PUSH) + #undef JSON_HEDLEY_DIAGNOSTIC_PUSH +#endif +#if defined(JSON_HEDLEY_DIAGNOSTIC_POP) + #undef JSON_HEDLEY_DIAGNOSTIC_POP +#endif +#if defined(__clang__) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH __pragma(warning(push)) + #define JSON_HEDLEY_DIAGNOSTIC_POP __pragma(warning(pop)) +#elif JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("pop") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("diag_push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("diag_pop") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_PUSH + #define JSON_HEDLEY_DIAGNOSTIC_POP +#endif + +/* JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat") +# if JSON_HEDLEY_HAS_WARNING("-Wc++17-extensions") +# if JSON_HEDLEY_HAS_WARNING("-Wc++1z-extensions") +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + _Pragma("clang diagnostic ignored \"-Wc++1z-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# endif +#endif +#if !defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(x) x +#endif + +#if defined(JSON_HEDLEY_CONST_CAST) + #undef JSON_HEDLEY_CONST_CAST +#endif +#if defined(__cplusplus) +# define JSON_HEDLEY_CONST_CAST(T, expr) (const_cast(expr)) +#elif \ + JSON_HEDLEY_HAS_WARNING("-Wcast-qual") || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_CONST_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_CONST_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_REINTERPRET_CAST) + #undef JSON_HEDLEY_REINTERPRET_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) (reinterpret_cast(expr)) +#else + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_STATIC_CAST) + #undef JSON_HEDLEY_STATIC_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_STATIC_CAST(T, expr) (static_cast(expr)) +#else + #define JSON_HEDLEY_STATIC_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_CPP_CAST) + #undef JSON_HEDLEY_CPP_CAST +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wold-style-cast") +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wold-style-cast\"") \ + ((T) (expr)) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# elif JSON_HEDLEY_IAR_VERSION_CHECK(8,3,0) +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("diag_suppress=Pe137") \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_CPP_CAST(T, expr) ((T) (expr)) +# endif +#else +# define JSON_HEDLEY_CPP_CAST(T, expr) (expr) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wdeprecated-declarations") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warning(disable:1478 1786)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:1478 1786)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1216,1444,1445") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:4996)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1291,1718") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && !defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,E_DEPRECATED_ATT,E_DEPRECATED_ATT_MESS)") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,symdeprecated,symdeprecated2)") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress=Pe1444,Pe1215") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warn(disable:2241)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("warning(disable:161)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:161)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 1675") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:4068)) +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(16,9,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress=Pe161") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 161") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-attributes") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("clang diagnostic ignored \"-Wunknown-attributes\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("warning(disable:1292)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:1292)) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:5030)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097,1098") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("error_messages(off,attrskipunsup)") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1173") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress=Pe1097") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wcast-qual") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("clang diagnostic ignored \"-Wcast-qual\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("warning(disable:2203 2331)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunused-function") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("clang diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("GCC diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(1,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION __pragma(warning(disable:4505)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("diag_suppress 3142") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif + +#if defined(JSON_HEDLEY_DEPRECATED) + #undef JSON_HEDLEY_DEPRECATED +#endif +#if defined(JSON_HEDLEY_DEPRECATED_FOR) + #undef JSON_HEDLEY_DEPRECATED_FOR +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated("Since " # since)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated("Since " #since "; use " #replacement)) +#elif \ + (JSON_HEDLEY_HAS_EXTENSION(attribute_deprecated_with_message) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__("Since " #since))) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__("Since " #since "; use " #replacement))) +#elif defined(__cplusplus) && (__cplusplus >= 201402L) + #define JSON_HEDLEY_DEPRECATED(since) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since)]]) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since "; use " #replacement)]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(deprecated) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_PELLES_VERSION_CHECK(6,50,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DEPRECATED(since) _Pragma("deprecated") + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) _Pragma("deprecated") +#else + #define JSON_HEDLEY_DEPRECATED(since) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) +#endif + +#if defined(JSON_HEDLEY_UNAVAILABLE) + #undef JSON_HEDLEY_UNAVAILABLE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warning) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNAVAILABLE(available_since) __attribute__((__warning__("Not available until " #available_since))) +#else + #define JSON_HEDLEY_UNAVAILABLE(available_since) +#endif + +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT +#endif +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT_MSG) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warn_unused_result) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) __attribute__((__warn_unused_result__)) +#elif (JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard(msg)]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) +#elif defined(_Check_return_) /* SAL */ + #define JSON_HEDLEY_WARN_UNUSED_RESULT _Check_return_ + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) _Check_return_ +#else + #define JSON_HEDLEY_WARN_UNUSED_RESULT + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) +#endif + +#if defined(JSON_HEDLEY_SENTINEL) + #undef JSON_HEDLEY_SENTINEL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(sentinel) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_SENTINEL(position) __attribute__((__sentinel__(position))) +#else + #define JSON_HEDLEY_SENTINEL(position) +#endif + +#if defined(JSON_HEDLEY_NO_RETURN) + #undef JSON_HEDLEY_NO_RETURN +#endif +#if JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NO_RETURN __noreturn +#elif \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + #define JSON_HEDLEY_NO_RETURN _Noreturn +#elif defined(__cplusplus) && (__cplusplus >= 201103L) + #define JSON_HEDLEY_NO_RETURN JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[noreturn]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(noreturn) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,2,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_NO_RETURN _Pragma("does_not_return") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NO_RETURN _Pragma("FUNC_NEVER_RETURNS;") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NO_RETURN __attribute((noreturn)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#else + #define JSON_HEDLEY_NO_RETURN +#endif + +#if defined(JSON_HEDLEY_NO_ESCAPE) + #undef JSON_HEDLEY_NO_ESCAPE +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(noescape) + #define JSON_HEDLEY_NO_ESCAPE __attribute__((__noescape__)) +#else + #define JSON_HEDLEY_NO_ESCAPE +#endif + +#if defined(JSON_HEDLEY_UNREACHABLE) + #undef JSON_HEDLEY_UNREACHABLE +#endif +#if defined(JSON_HEDLEY_UNREACHABLE_RETURN) + #undef JSON_HEDLEY_UNREACHABLE_RETURN +#endif +#if defined(JSON_HEDLEY_ASSUME) + #undef JSON_HEDLEY_ASSUME +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_ASSUME(expr) __assume(expr) +#elif JSON_HEDLEY_HAS_BUILTIN(__builtin_assume) + #define JSON_HEDLEY_ASSUME(expr) __builtin_assume(expr) +#elif \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #if defined(__cplusplus) + #define JSON_HEDLEY_ASSUME(expr) std::_nassert(expr) + #else + #define JSON_HEDLEY_ASSUME(expr) _nassert(expr) + #endif +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_unreachable) && (!defined(JSON_HEDLEY_ARM_VERSION))) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,5) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(10,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNREACHABLE() __builtin_unreachable() +#elif defined(JSON_HEDLEY_ASSUME) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif +#if !defined(JSON_HEDLEY_ASSUME) + #if defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, ((expr) ? 1 : (JSON_HEDLEY_UNREACHABLE(), 1))) + #else + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, expr) + #endif +#endif +#if defined(JSON_HEDLEY_UNREACHABLE) + #if \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (JSON_HEDLEY_STATIC_CAST(void, JSON_HEDLEY_ASSUME(0)), (value)) + #else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) JSON_HEDLEY_UNREACHABLE() + #endif +#else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (value) +#endif +#if !defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif + +JSON_HEDLEY_DIAGNOSTIC_PUSH +#if JSON_HEDLEY_HAS_WARNING("-Wpedantic") + #pragma clang diagnostic ignored "-Wpedantic" +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat-pedantic") && defined(__cplusplus) + #pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif +#if JSON_HEDLEY_GCC_HAS_WARNING("-Wvariadic-macros",4,0,0) + #if defined(__clang__) + #pragma clang diagnostic ignored "-Wvariadic-macros" + #elif defined(JSON_HEDLEY_GCC_VERSION) + #pragma GCC diagnostic ignored "-Wvariadic-macros" + #endif +#endif +#if defined(JSON_HEDLEY_NON_NULL) + #undef JSON_HEDLEY_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NON_NULL(...) __attribute__((__nonnull__(__VA_ARGS__))) +#else + #define JSON_HEDLEY_NON_NULL(...) +#endif +JSON_HEDLEY_DIAGNOSTIC_POP + +#if defined(JSON_HEDLEY_PRINTF_FORMAT) + #undef JSON_HEDLEY_PRINTF_FORMAT +#endif +#if defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && !defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(ms_printf, string_idx, first_to_check))) +#elif defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(gnu_printf, string_idx, first_to_check))) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(format) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(__printf__, string_idx, first_to_check))) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(6,0,0) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __declspec(vaformat(printf,string_idx,first_to_check)) +#else + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) +#endif + +#if defined(JSON_HEDLEY_CONSTEXPR) + #undef JSON_HEDLEY_CONSTEXPR +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_CONSTEXPR JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(constexpr) + #endif +#endif +#if !defined(JSON_HEDLEY_CONSTEXPR) + #define JSON_HEDLEY_CONSTEXPR +#endif + +#if defined(JSON_HEDLEY_PREDICT) + #undef JSON_HEDLEY_PREDICT +#endif +#if defined(JSON_HEDLEY_LIKELY) + #undef JSON_HEDLEY_LIKELY +#endif +#if defined(JSON_HEDLEY_UNLIKELY) + #undef JSON_HEDLEY_UNLIKELY +#endif +#if defined(JSON_HEDLEY_UNPREDICTABLE) + #undef JSON_HEDLEY_UNPREDICTABLE +#endif +#if JSON_HEDLEY_HAS_BUILTIN(__builtin_unpredictable) + #define JSON_HEDLEY_UNPREDICTABLE(expr) __builtin_unpredictable((expr)) +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect_with_probability) && !defined(JSON_HEDLEY_PGI_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(9,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, value, probability) __builtin_expect_with_probability( (expr), (value), (probability)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) __builtin_expect_with_probability(!!(expr), 1 , (probability)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) __builtin_expect_with_probability(!!(expr), 0 , (probability)) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect (!!(expr), 1 ) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect (!!(expr), 0 ) +#elif \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,27) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, expected, probability) \ + (((probability) >= 0.9) ? __builtin_expect((expr), (expected)) : (JSON_HEDLEY_STATIC_CAST(void, expected), (expr))) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 1) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 0) : !!(expr))); \ + })) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 0) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 1) : !!(expr))); \ + })) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect(!!(expr), 1) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +#else +# define JSON_HEDLEY_PREDICT(expr, expected, probability) (JSON_HEDLEY_STATIC_CAST(void, expected), (expr)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_LIKELY(expr) (!!(expr)) +# define JSON_HEDLEY_UNLIKELY(expr) (!!(expr)) +#endif +#if !defined(JSON_HEDLEY_UNPREDICTABLE) + #define JSON_HEDLEY_UNPREDICTABLE(expr) JSON_HEDLEY_PREDICT(expr, 1, 0.5) +#endif + +#if defined(JSON_HEDLEY_MALLOC) + #undef JSON_HEDLEY_MALLOC +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(malloc) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_MALLOC __attribute__((__malloc__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_MALLOC _Pragma("returns_new_memory") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_MALLOC __declspec(restrict) +#else + #define JSON_HEDLEY_MALLOC +#endif + +#if defined(JSON_HEDLEY_PURE) + #undef JSON_HEDLEY_PURE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(pure) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,96,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PURE __attribute__((__pure__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) +# define JSON_HEDLEY_PURE _Pragma("does_not_write_global_data") +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) \ + ) +# define JSON_HEDLEY_PURE _Pragma("FUNC_IS_PURE;") +#else +# define JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_CONST) + #undef JSON_HEDLEY_CONST +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(const) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_CONST __attribute__((__const__)) +#elif \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_CONST _Pragma("no_side_effect") +#else + #define JSON_HEDLEY_CONST JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_RESTRICT) + #undef JSON_HEDLEY_RESTRICT +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT restrict +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,4) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + defined(__clang__) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RESTRICT __restrict +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,3,0) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT _Restrict +#else + #define JSON_HEDLEY_RESTRICT +#endif + +#if defined(JSON_HEDLEY_INLINE) + #undef JSON_HEDLEY_INLINE +#endif +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + (defined(__cplusplus) && (__cplusplus >= 199711L)) + #define JSON_HEDLEY_INLINE inline +#elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(6,2,0) + #define JSON_HEDLEY_INLINE __inline__ +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,1,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_INLINE __inline +#else + #define JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_ALWAYS_INLINE) + #undef JSON_HEDLEY_ALWAYS_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(always_inline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) +# define JSON_HEDLEY_ALWAYS_INLINE __attribute__((__always_inline__)) JSON_HEDLEY_INLINE +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_ALWAYS_INLINE __forceinline +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) \ + ) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("FUNC_ALWAYS_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("inline=forced") +#else +# define JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_NEVER_INLINE) + #undef JSON_HEDLEY_NEVER_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(noinline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute__((__noinline__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(10,2,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("noinline") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("FUNC_CANNOT_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("inline=never") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute((noinline)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#else + #define JSON_HEDLEY_NEVER_INLINE +#endif + +#if defined(JSON_HEDLEY_PRIVATE) + #undef JSON_HEDLEY_PRIVATE +#endif +#if defined(JSON_HEDLEY_PUBLIC) + #undef JSON_HEDLEY_PUBLIC +#endif +#if defined(JSON_HEDLEY_IMPORT) + #undef JSON_HEDLEY_IMPORT +#endif +#if defined(_WIN32) || defined(__CYGWIN__) +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC __declspec(dllexport) +# define JSON_HEDLEY_IMPORT __declspec(dllimport) +#else +# if \ + JSON_HEDLEY_HAS_ATTRIBUTE(visibility) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + ( \ + defined(__TI_EABI__) && \ + ( \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) \ + ) \ + ) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PRIVATE __attribute__((__visibility__("hidden"))) +# define JSON_HEDLEY_PUBLIC __attribute__((__visibility__("default"))) +# else +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC +# endif +# define JSON_HEDLEY_IMPORT extern +#endif + +#if defined(JSON_HEDLEY_NO_THROW) + #undef JSON_HEDLEY_NO_THROW +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nothrow) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_THROW __attribute__((__nothrow__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NO_THROW __declspec(nothrow) +#else + #define JSON_HEDLEY_NO_THROW +#endif + +#if defined(JSON_HEDLEY_FALL_THROUGH) + #undef JSON_HEDLEY_FALL_THROUGH +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(fallthrough) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_FALL_THROUGH __attribute__((__fallthrough__)) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(clang,fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[clang::fallthrough]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[fallthrough]]) +#elif defined(__fallthrough) /* SAL */ + #define JSON_HEDLEY_FALL_THROUGH __fallthrough +#else + #define JSON_HEDLEY_FALL_THROUGH +#endif + +#if defined(JSON_HEDLEY_RETURNS_NON_NULL) + #undef JSON_HEDLEY_RETURNS_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(returns_nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RETURNS_NON_NULL __attribute__((__returns_nonnull__)) +#elif defined(_Ret_notnull_) /* SAL */ + #define JSON_HEDLEY_RETURNS_NON_NULL _Ret_notnull_ +#else + #define JSON_HEDLEY_RETURNS_NON_NULL +#endif + +#if defined(JSON_HEDLEY_ARRAY_PARAM) + #undef JSON_HEDLEY_ARRAY_PARAM +#endif +#if \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ + !defined(__STDC_NO_VLA__) && \ + !defined(__cplusplus) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_ARRAY_PARAM(name) (name) +#else + #define JSON_HEDLEY_ARRAY_PARAM(name) +#endif + +#if defined(JSON_HEDLEY_IS_CONSTANT) + #undef JSON_HEDLEY_IS_CONSTANT +#endif +#if defined(JSON_HEDLEY_REQUIRE_CONSTEXPR) + #undef JSON_HEDLEY_REQUIRE_CONSTEXPR +#endif +/* JSON_HEDLEY_IS_CONSTEXPR_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #undef JSON_HEDLEY_IS_CONSTEXPR_ +#endif +#if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_constant_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,19) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) && !defined(__cplusplus)) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_IS_CONSTANT(expr) __builtin_constant_p(expr) +#endif +#if !defined(__cplusplus) +# if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_types_compatible_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,24) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0)), int*) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((intptr_t) ((expr) * 0)) : (int*) 0)), int*) +#endif +# elif \ + ( \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(JSON_HEDLEY_SUNPRO_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION)) || \ + (JSON_HEDLEY_HAS_EXTENSION(c_generic_selections) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,3,0) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0), int*: 1, void*: 0) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((intptr_t) * 0) : (int*) 0), int*: 1, void*: 0) +#endif +# elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + defined(JSON_HEDLEY_INTEL_VERSION) || \ + defined(JSON_HEDLEY_TINYC_VERSION) || \ + defined(JSON_HEDLEY_TI_ARMCL_VERSION) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(18,12,0) || \ + defined(JSON_HEDLEY_TI_CL2000_VERSION) || \ + defined(JSON_HEDLEY_TI_CL6X_VERSION) || \ + defined(JSON_HEDLEY_TI_CL7X_VERSION) || \ + defined(JSON_HEDLEY_TI_CLPRU_VERSION) || \ + defined(__clang__) +# define JSON_HEDLEY_IS_CONSTEXPR_(expr) ( \ + sizeof(void) != \ + sizeof(*( \ + 1 ? \ + ((void*) ((expr) * 0L) ) : \ +((struct { char v[sizeof(void) * 2]; } *) 1) \ + ) \ + ) \ + ) +# endif +#endif +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) JSON_HEDLEY_IS_CONSTEXPR_(expr) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (JSON_HEDLEY_IS_CONSTEXPR_(expr) ? (expr) : (-1)) +#else + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) (0) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (expr) +#endif + +#if defined(JSON_HEDLEY_BEGIN_C_DECLS) + #undef JSON_HEDLEY_BEGIN_C_DECLS +#endif +#if defined(JSON_HEDLEY_END_C_DECLS) + #undef JSON_HEDLEY_END_C_DECLS +#endif +#if defined(JSON_HEDLEY_C_DECL) + #undef JSON_HEDLEY_C_DECL +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_BEGIN_C_DECLS extern "C" { + #define JSON_HEDLEY_END_C_DECLS } + #define JSON_HEDLEY_C_DECL extern "C" +#else + #define JSON_HEDLEY_BEGIN_C_DECLS + #define JSON_HEDLEY_END_C_DECLS + #define JSON_HEDLEY_C_DECL +#endif + +#if defined(JSON_HEDLEY_STATIC_ASSERT) + #undef JSON_HEDLEY_STATIC_ASSERT +#endif +#if \ + !defined(__cplusplus) && ( \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) || \ + (JSON_HEDLEY_HAS_FEATURE(c_static_assert) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(6,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + defined(_Static_assert) \ + ) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) _Static_assert(expr, message) +#elif \ + (defined(__cplusplus) && (__cplusplus >= 201103L)) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(16,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(static_assert(expr, message)) +#else +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) +#endif + +#if defined(JSON_HEDLEY_NULL) + #undef JSON_HEDLEY_NULL +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_NULL JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(nullptr) + #elif defined(NULL) + #define JSON_HEDLEY_NULL NULL + #else + #define JSON_HEDLEY_NULL JSON_HEDLEY_STATIC_CAST(void*, 0) + #endif +#elif defined(NULL) + #define JSON_HEDLEY_NULL NULL +#else + #define JSON_HEDLEY_NULL ((void*) 0) +#endif + +#if defined(JSON_HEDLEY_MESSAGE) + #undef JSON_HEDLEY_MESSAGE +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_MESSAGE(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(message msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message msg) +#elif JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(_CRI message msg) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_WARNING) + #undef JSON_HEDLEY_WARNING +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_WARNING(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(clang warning msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,8,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(GCC warning msg) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_REQUIRE) + #undef JSON_HEDLEY_REQUIRE +#endif +#if defined(JSON_HEDLEY_REQUIRE_MSG) + #undef JSON_HEDLEY_REQUIRE_MSG +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(diagnose_if) +# if JSON_HEDLEY_HAS_WARNING("-Wgcc-compat") +# define JSON_HEDLEY_REQUIRE(expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), #expr, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), msg, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_REQUIRE(expr) __attribute__((diagnose_if(!(expr), #expr, "error"))) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) __attribute__((diagnose_if(!(expr), msg, "error"))) +# endif +#else +# define JSON_HEDLEY_REQUIRE(expr) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) +#endif + +#if defined(JSON_HEDLEY_FLAGS) + #undef JSON_HEDLEY_FLAGS +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(flag_enum) && (!defined(__cplusplus) || JSON_HEDLEY_HAS_WARNING("-Wbitfield-enum-conversion")) + #define JSON_HEDLEY_FLAGS __attribute__((__flag_enum__)) +#else + #define JSON_HEDLEY_FLAGS +#endif + +#if defined(JSON_HEDLEY_FLAGS_CAST) + #undef JSON_HEDLEY_FLAGS_CAST +#endif +#if JSON_HEDLEY_INTEL_VERSION_CHECK(19,0,0) +# define JSON_HEDLEY_FLAGS_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("warning(disable:188)") \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_FLAGS_CAST(T, expr) JSON_HEDLEY_STATIC_CAST(T, expr) +#endif + +#if defined(JSON_HEDLEY_EMPTY_BASES) + #undef JSON_HEDLEY_EMPTY_BASES +#endif +#if \ + (JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,23918) && !JSON_HEDLEY_MSVC_VERSION_CHECK(20,0,0)) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_EMPTY_BASES __declspec(empty_bases) +#else + #define JSON_HEDLEY_EMPTY_BASES +#endif + +/* Remaining macros are deprecated. */ + +#if defined(JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK +#endif +#if defined(__clang__) + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) (0) +#else + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_CLANG_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_BUILTIN) + #undef JSON_HEDLEY_CLANG_HAS_BUILTIN +#endif +#define JSON_HEDLEY_CLANG_HAS_BUILTIN(builtin) JSON_HEDLEY_HAS_BUILTIN(builtin) + +#if defined(JSON_HEDLEY_CLANG_HAS_FEATURE) + #undef JSON_HEDLEY_CLANG_HAS_FEATURE +#endif +#define JSON_HEDLEY_CLANG_HAS_FEATURE(feature) JSON_HEDLEY_HAS_FEATURE(feature) + +#if defined(JSON_HEDLEY_CLANG_HAS_EXTENSION) + #undef JSON_HEDLEY_CLANG_HAS_EXTENSION +#endif +#define JSON_HEDLEY_CLANG_HAS_EXTENSION(extension) JSON_HEDLEY_HAS_EXTENSION(extension) + +#if defined(JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_DECLSPEC_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_WARNING) + #undef JSON_HEDLEY_CLANG_HAS_WARNING +#endif +#define JSON_HEDLEY_CLANG_HAS_WARNING(warning) JSON_HEDLEY_HAS_WARNING(warning) + +#endif /* !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < X) */ + +// #include + + +#include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +template struct make_void +{ + using type = void; +}; +template using void_t = typename make_void::type; +} // namespace detail +} // namespace nlohmann + + +// https://en.cppreference.com/w/cpp/experimental/is_detected +namespace nlohmann +{ +namespace detail +{ +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; +}; + +template class Op, + class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; + +template class Op, class... Args> +using is_detected = typename detector::value_t; + +template class Op, class... Args> +struct is_detected_lazy : is_detected { }; + +template class Op, class... Args> +using detected_t = typename detector::type; + +template class Op, class... Args> +using detected_or = detector; + +template class Op, class... Args> +using detected_or_t = typename detected_or::type; + +template class Op, class... Args> +using is_detected_exact = std::is_same>; + +template class Op, class... Args> +using is_detected_convertible = + std::is_convertible, To>; +} // namespace detail +} // namespace nlohmann + + +// This file contains all internal macro definitions +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// C++ language standard detection +// if the user manually specified the used c++ version this is skipped +#if !defined(JSON_HAS_CPP_20) && !defined(JSON_HAS_CPP_17) && !defined(JSON_HAS_CPP_14) && !defined(JSON_HAS_CPP_11) + #if (defined(__cplusplus) && __cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) + #define JSON_HAS_CPP_20 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 + #endif + // the cpp 11 flag is always specified because it is the minimal required version + #define JSON_HAS_CPP_11 +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdocumentation" + #pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#endif + +// allow to disable exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #include + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +// allow to override assert +#if !defined(JSON_ASSERT) + #include // assert + #define JSON_ASSERT(x) assert(x) +#endif + +// allow to access some private functions (needed by the test suite) +#if defined(JSON_TESTS_PRIVATE) + #define JSON_PRIVATE_UNLESS_TESTED public +#else + #define JSON_PRIVATE_UNLESS_TESTED private +#endif + +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM +@since version 3.4.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + j = ((it != std::end(m)) ? it : std::begin(m))->second; \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [&j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + e = ((it != std::end(m)) ? it : std::begin(m))->first; \ + } + +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. + +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer, \ + class BinaryType> + +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json + +// Macros to simplify conversion from/to types + +#define NLOHMANN_JSON_EXPAND( x ) x +#define NLOHMANN_JSON_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME +#define NLOHMANN_JSON_PASTE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_GET_MACRO(__VA_ARGS__, \ + NLOHMANN_JSON_PASTE64, \ + NLOHMANN_JSON_PASTE63, \ + NLOHMANN_JSON_PASTE62, \ + NLOHMANN_JSON_PASTE61, \ + NLOHMANN_JSON_PASTE60, \ + NLOHMANN_JSON_PASTE59, \ + NLOHMANN_JSON_PASTE58, \ + NLOHMANN_JSON_PASTE57, \ + NLOHMANN_JSON_PASTE56, \ + NLOHMANN_JSON_PASTE55, \ + NLOHMANN_JSON_PASTE54, \ + NLOHMANN_JSON_PASTE53, \ + NLOHMANN_JSON_PASTE52, \ + NLOHMANN_JSON_PASTE51, \ + NLOHMANN_JSON_PASTE50, \ + NLOHMANN_JSON_PASTE49, \ + NLOHMANN_JSON_PASTE48, \ + NLOHMANN_JSON_PASTE47, \ + NLOHMANN_JSON_PASTE46, \ + NLOHMANN_JSON_PASTE45, \ + NLOHMANN_JSON_PASTE44, \ + NLOHMANN_JSON_PASTE43, \ + NLOHMANN_JSON_PASTE42, \ + NLOHMANN_JSON_PASTE41, \ + NLOHMANN_JSON_PASTE40, \ + NLOHMANN_JSON_PASTE39, \ + NLOHMANN_JSON_PASTE38, \ + NLOHMANN_JSON_PASTE37, \ + NLOHMANN_JSON_PASTE36, \ + NLOHMANN_JSON_PASTE35, \ + NLOHMANN_JSON_PASTE34, \ + NLOHMANN_JSON_PASTE33, \ + NLOHMANN_JSON_PASTE32, \ + NLOHMANN_JSON_PASTE31, \ + NLOHMANN_JSON_PASTE30, \ + NLOHMANN_JSON_PASTE29, \ + NLOHMANN_JSON_PASTE28, \ + NLOHMANN_JSON_PASTE27, \ + NLOHMANN_JSON_PASTE26, \ + NLOHMANN_JSON_PASTE25, \ + NLOHMANN_JSON_PASTE24, \ + NLOHMANN_JSON_PASTE23, \ + NLOHMANN_JSON_PASTE22, \ + NLOHMANN_JSON_PASTE21, \ + NLOHMANN_JSON_PASTE20, \ + NLOHMANN_JSON_PASTE19, \ + NLOHMANN_JSON_PASTE18, \ + NLOHMANN_JSON_PASTE17, \ + NLOHMANN_JSON_PASTE16, \ + NLOHMANN_JSON_PASTE15, \ + NLOHMANN_JSON_PASTE14, \ + NLOHMANN_JSON_PASTE13, \ + NLOHMANN_JSON_PASTE12, \ + NLOHMANN_JSON_PASTE11, \ + NLOHMANN_JSON_PASTE10, \ + NLOHMANN_JSON_PASTE9, \ + NLOHMANN_JSON_PASTE8, \ + NLOHMANN_JSON_PASTE7, \ + NLOHMANN_JSON_PASTE6, \ + NLOHMANN_JSON_PASTE5, \ + NLOHMANN_JSON_PASTE4, \ + NLOHMANN_JSON_PASTE3, \ + NLOHMANN_JSON_PASTE2, \ + NLOHMANN_JSON_PASTE1)(__VA_ARGS__)) +#define NLOHMANN_JSON_PASTE2(func, v1) func(v1) +#define NLOHMANN_JSON_PASTE3(func, v1, v2) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE2(func, v2) +#define NLOHMANN_JSON_PASTE4(func, v1, v2, v3) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE3(func, v2, v3) +#define NLOHMANN_JSON_PASTE5(func, v1, v2, v3, v4) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE4(func, v2, v3, v4) +#define NLOHMANN_JSON_PASTE6(func, v1, v2, v3, v4, v5) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE5(func, v2, v3, v4, v5) +#define NLOHMANN_JSON_PASTE7(func, v1, v2, v3, v4, v5, v6) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE6(func, v2, v3, v4, v5, v6) +#define NLOHMANN_JSON_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE7(func, v2, v3, v4, v5, v6, v7) +#define NLOHMANN_JSON_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE8(func, v2, v3, v4, v5, v6, v7, v8) +#define NLOHMANN_JSON_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9) +#define NLOHMANN_JSON_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10) +#define NLOHMANN_JSON_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) +#define NLOHMANN_JSON_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) +#define NLOHMANN_JSON_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) +#define NLOHMANN_JSON_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) +#define NLOHMANN_JSON_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) +#define NLOHMANN_JSON_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) +#define NLOHMANN_JSON_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) +#define NLOHMANN_JSON_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) +#define NLOHMANN_JSON_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) +#define NLOHMANN_JSON_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) +#define NLOHMANN_JSON_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) +#define NLOHMANN_JSON_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) +#define NLOHMANN_JSON_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) +#define NLOHMANN_JSON_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) +#define NLOHMANN_JSON_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) +#define NLOHMANN_JSON_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) +#define NLOHMANN_JSON_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) +#define NLOHMANN_JSON_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) +#define NLOHMANN_JSON_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) +#define NLOHMANN_JSON_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) +#define NLOHMANN_JSON_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) +#define NLOHMANN_JSON_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) +#define NLOHMANN_JSON_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) +#define NLOHMANN_JSON_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) +#define NLOHMANN_JSON_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) +#define NLOHMANN_JSON_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) +#define NLOHMANN_JSON_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) +#define NLOHMANN_JSON_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) +#define NLOHMANN_JSON_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) +#define NLOHMANN_JSON_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) +#define NLOHMANN_JSON_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) +#define NLOHMANN_JSON_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) +#define NLOHMANN_JSON_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) +#define NLOHMANN_JSON_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) +#define NLOHMANN_JSON_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) +#define NLOHMANN_JSON_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) +#define NLOHMANN_JSON_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) +#define NLOHMANN_JSON_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) +#define NLOHMANN_JSON_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) +#define NLOHMANN_JSON_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) +#define NLOHMANN_JSON_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) +#define NLOHMANN_JSON_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) +#define NLOHMANN_JSON_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) +#define NLOHMANN_JSON_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) +#define NLOHMANN_JSON_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) +#define NLOHMANN_JSON_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) +#define NLOHMANN_JSON_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) +#define NLOHMANN_JSON_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) +#define NLOHMANN_JSON_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) +#define NLOHMANN_JSON_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) +#define NLOHMANN_JSON_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) +#define NLOHMANN_JSON_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) +#define NLOHMANN_JSON_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) + +#define NLOHMANN_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.v1; +#define NLOHMANN_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.v1); + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE +@since version 3.9.0 +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE +@since version 3.9.0 +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + + +// inspired from https://stackoverflow.com/a/26745591 +// allows to call any std function as if (e.g. with begin): +// using std::begin; begin(x); +// +// it allows using the detected idiom to retrieve the return type +// of such an expression +#define NLOHMANN_CAN_CALL_STD_FUNC_IMPL(std_name) \ + namespace detail { \ + using std::std_name; \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + } \ + \ + namespace detail2 { \ + struct std_name##_tag \ + { \ + }; \ + \ + template \ + std_name##_tag std_name(T&&...); \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + \ + template \ + struct would_call_std_##std_name \ + { \ + static constexpr auto const value = ::nlohmann::detail:: \ + is_detected_exact::value; \ + }; \ + } /* namespace detail2 */ \ + \ + template \ + struct would_call_std_##std_name : detail2::would_call_std_##std_name \ + { \ + } + +#ifndef JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_USE_IMPLICIT_CONVERSIONS 1 +#endif + +#if JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_EXPLICIT +#else + #define JSON_EXPLICIT explicit +#endif + +#ifndef JSON_DIAGNOSTICS + #define JSON_DIAGNOSTICS 0 +#endif + + +namespace nlohmann +{ +namespace detail +{ + +/*! +@brief replace all occurrences of a substring by another string + +@param[in,out] s the string to manipulate; changed so that all + occurrences of @a f are replaced with @a t +@param[in] f the substring to replace with @a t +@param[in] t the string to replace @a f + +@pre The search string @a f must not be empty. **This precondition is +enforced with an assertion.** + +@since version 2.0.0 +*/ +inline void replace_substring(std::string& s, const std::string& f, + const std::string& t) +{ + JSON_ASSERT(!f.empty()); + for (auto pos = s.find(f); // find first occurrence of f + pos != std::string::npos; // make sure f was found + s.replace(pos, f.size(), t), // replace with t, and + pos = s.find(f, pos + t.size())) // find next occurrence of f + {} +} + +/*! + * @brief string escaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to escape + * @return escaped string + * + * Note the order of escaping "~" to "~0" and "/" to "~1" is important. + */ +inline std::string escape(std::string s) +{ + replace_substring(s, "~", "~0"); + replace_substring(s, "/", "~1"); + return s; +} + +/*! + * @brief string unescaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to unescape + * @return unescaped string + * + * Note the order of escaping "~1" to "/" and "~0" to "~" is important. + */ +static void unescape(std::string& s) +{ + replace_substring(s, "~1", "/"); + replace_substring(s, "~0", "~"); +} + +} // namespace detail +} // namespace nlohmann + +// #include + + +#include // size_t + +namespace nlohmann +{ +namespace detail +{ +/// struct to capture the start position of the current token +struct position_t +{ + /// the total number of characters read + std::size_t chars_read_total = 0; + /// the number of characters read in the current line + std::size_t chars_read_current_line = 0; + /// the number of lines read + std::size_t lines_read = 0; + + /// conversion to size_t to preserve SAX interface + constexpr operator size_t() const + { + return chars_read_total; + } +}; + +} // namespace detail +} // namespace nlohmann + +// #include + + +namespace nlohmann +{ +namespace detail +{ +//////////////// +// exceptions // +//////////////// + +/*! +@brief general exception of the @ref basic_json class + +This class is an extension of `std::exception` objects with a member @a id for +exception ids. It is used as the base class for all exceptions thrown by the +@ref basic_json class. This class can hence be used as "wildcard" to catch +exceptions. + +Subclasses: +- @ref parse_error for exceptions indicating a parse error +- @ref invalid_iterator for exceptions indicating errors with iterators +- @ref type_error for exceptions indicating executing a member function with + a wrong type +- @ref out_of_range for exceptions indicating access out of the defined range +- @ref other_error for exceptions indicating other library errors + +@internal +@note To have nothrow-copy-constructible exceptions, we internally use + `std::runtime_error` which can cope with arbitrary-length error messages. + Intermediate strings are built with static functions and then passed to + the actual constructor. +@endinternal + +@liveexample{The following code shows how arbitrary library exceptions can be +caught.,exception} + +@since version 3.0.0 +*/ +class exception : public std::exception +{ + public: + /// returns the explanatory string + const char* what() const noexcept override + { + return m.what(); + } + + /// the id of the exception + const int id; // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) + + protected: + JSON_HEDLEY_NON_NULL(3) + exception(int id_, const char* what_arg) : id(id_), m(what_arg) {} + + static std::string name(const std::string& ename, int id_) + { + return "[json.exception." + ename + "." + std::to_string(id_) + "] "; + } + + template + static std::string diagnostics(const BasicJsonType& leaf_element) + { +#if JSON_DIAGNOSTICS + std::vector tokens; + for (const auto* current = &leaf_element; current->m_parent != nullptr; current = current->m_parent) + { + switch (current->m_parent->type()) + { + case value_t::array: + { + for (std::size_t i = 0; i < current->m_parent->m_value.array->size(); ++i) + { + if (¤t->m_parent->m_value.array->operator[](i) == current) + { + tokens.emplace_back(std::to_string(i)); + break; + } + } + break; + } + + case value_t::object: + { + for (const auto& element : *current->m_parent->m_value.object) + { + if (&element.second == current) + { + tokens.emplace_back(element.first.c_str()); + break; + } + } + break; + } + + case value_t::null: // LCOV_EXCL_LINE + case value_t::string: // LCOV_EXCL_LINE + case value_t::boolean: // LCOV_EXCL_LINE + case value_t::number_integer: // LCOV_EXCL_LINE + case value_t::number_unsigned: // LCOV_EXCL_LINE + case value_t::number_float: // LCOV_EXCL_LINE + case value_t::binary: // LCOV_EXCL_LINE + case value_t::discarded: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + break; // LCOV_EXCL_LINE + } + } + + if (tokens.empty()) + { + return ""; + } + + return "(" + std::accumulate(tokens.rbegin(), tokens.rend(), std::string{}, + [](const std::string & a, const std::string & b) + { + return a + "/" + detail::escape(b); + }) + ") "; +#else + static_cast(leaf_element); + return ""; +#endif + } + + private: + /// an exception object as storage for error messages + std::runtime_error m; +}; + +/*! +@brief exception indicating a parse error + +This exception is thrown by the library when a parse error occurs. Parse errors +can occur during the deserialization of JSON text, CBOR, MessagePack, as well +as when using JSON Patch. + +Member @a byte holds the byte index of the last read character in the input +file. + +Exceptions have ids 1xx. + +name / id | example message | description +------------------------------ | --------------- | ------------------------- +json.exception.parse_error.101 | parse error at 2: unexpected end of input; expected string literal | This error indicates a syntax error while deserializing a JSON text. The error message describes that an unexpected token (character) was encountered, and the member @a byte indicates the error position. +json.exception.parse_error.102 | parse error at 14: missing or wrong low surrogate | JSON uses the `\uxxxx` format to describe Unicode characters. Code points above above 0xFFFF are split into two `\uxxxx` entries ("surrogate pairs"). This error indicates that the surrogate pair is incomplete or contains an invalid code point. +json.exception.parse_error.103 | parse error: code points above 0x10FFFF are invalid | Unicode supports code points up to 0x10FFFF. Code points above 0x10FFFF are invalid. +json.exception.parse_error.104 | parse error: JSON patch must be an array of objects | [RFC 6902](https://tools.ietf.org/html/rfc6902) requires a JSON Patch document to be a JSON document that represents an array of objects. +json.exception.parse_error.105 | parse error: operation must have string member 'op' | An operation of a JSON Patch document must contain exactly one "op" member, whose value indicates the operation to perform. Its value must be one of "add", "remove", "replace", "move", "copy", or "test"; other values are errors. +json.exception.parse_error.106 | parse error: array index '01' must not begin with '0' | An array index in a JSON Pointer ([RFC 6901](https://tools.ietf.org/html/rfc6901)) may be `0` or any number without a leading `0`. +json.exception.parse_error.107 | parse error: JSON pointer must be empty or begin with '/' - was: 'foo' | A JSON Pointer must be a Unicode string containing a sequence of zero or more reference tokens, each prefixed by a `/` character. +json.exception.parse_error.108 | parse error: escape character '~' must be followed with '0' or '1' | In a JSON Pointer, only `~0` and `~1` are valid escape sequences. +json.exception.parse_error.109 | parse error: array index 'one' is not a number | A JSON Pointer array index must be a number. +json.exception.parse_error.110 | parse error at 1: cannot read 2 bytes from vector | When parsing CBOR or MessagePack, the byte vector ends before the complete value has been read. +json.exception.parse_error.112 | parse error at 1: error reading CBOR; last byte: 0xF8 | Not all types of CBOR or MessagePack are supported. This exception occurs if an unsupported byte was read. +json.exception.parse_error.113 | parse error at 2: expected a CBOR string; last byte: 0x98 | While parsing a map key, a value that is not a string has been read. +json.exception.parse_error.114 | parse error: Unsupported BSON record type 0x0F | The parsing of the corresponding BSON record type is not implemented (yet). +json.exception.parse_error.115 | parse error at byte 5: syntax error while parsing UBJSON high-precision number: invalid number text: 1A | A UBJSON high-precision number could not be parsed. + +@note For an input with n bytes, 1 is the index of the first character and n+1 + is the index of the terminating null byte or the end of file. This also + holds true when reading a byte vector (CBOR or MessagePack). + +@liveexample{The following code shows how a `parse_error` exception can be +caught.,parse_error} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class parse_error : public exception +{ + public: + /*! + @brief create a parse error exception + @param[in] id_ the id of the exception + @param[in] pos the position where the error occurred (or with + chars_read_total=0 if the position cannot be + determined) + @param[in] what_arg the explanatory string + @return parse_error object + */ + template + static parse_error create(int id_, const position_t& pos, const std::string& what_arg, const BasicJsonType& context) + { + std::string w = exception::name("parse_error", id_) + "parse error" + + position_string(pos) + ": " + exception::diagnostics(context) + what_arg; + return parse_error(id_, pos.chars_read_total, w.c_str()); + } + + template + static parse_error create(int id_, std::size_t byte_, const std::string& what_arg, const BasicJsonType& context) + { + std::string w = exception::name("parse_error", id_) + "parse error" + + (byte_ != 0 ? (" at byte " + std::to_string(byte_)) : "") + + ": " + exception::diagnostics(context) + what_arg; + return parse_error(id_, byte_, w.c_str()); + } + + /*! + @brief byte index of the parse error + + The byte index of the last read character in the input file. + + @note For an input with n bytes, 1 is the index of the first character and + n+1 is the index of the terminating null byte or the end of file. + This also holds true when reading a byte vector (CBOR or MessagePack). + */ + const std::size_t byte; + + private: + parse_error(int id_, std::size_t byte_, const char* what_arg) + : exception(id_, what_arg), byte(byte_) {} + + static std::string position_string(const position_t& pos) + { + return " at line " + std::to_string(pos.lines_read + 1) + + ", column " + std::to_string(pos.chars_read_current_line); + } +}; + +/*! +@brief exception indicating errors with iterators + +This exception is thrown if iterators passed to a library function do not match +the expected semantics. + +Exceptions have ids 2xx. + +name / id | example message | description +----------------------------------- | --------------- | ------------------------- +json.exception.invalid_iterator.201 | iterators are not compatible | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. +json.exception.invalid_iterator.202 | iterator does not fit current value | In an erase or insert function, the passed iterator @a pos does not belong to the JSON value for which the function was called. It hence does not define a valid position for the deletion/insertion. +json.exception.invalid_iterator.203 | iterators do not fit current value | Either iterator passed to function @ref erase(IteratorType first, IteratorType last) does not belong to the JSON value from which values shall be erased. It hence does not define a valid range to delete values from. +json.exception.invalid_iterator.204 | iterators out of range | When an iterator range for a primitive type (number, boolean, or string) is passed to a constructor or an erase function, this range has to be exactly (@ref begin(), @ref end()), because this is the only way the single stored value is expressed. All other ranges are invalid. +json.exception.invalid_iterator.205 | iterator out of range | When an iterator for a primitive type (number, boolean, or string) is passed to an erase function, the iterator has to be the @ref begin() iterator, because it is the only way to address the stored value. All other iterators are invalid. +json.exception.invalid_iterator.206 | cannot construct with iterators from null | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) belong to a JSON null value and hence to not define a valid range. +json.exception.invalid_iterator.207 | cannot use key() for non-object iterators | The key() member function can only be used on iterators belonging to a JSON object, because other types do not have a concept of a key. +json.exception.invalid_iterator.208 | cannot use operator[] for object iterators | The operator[] to specify a concrete offset cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. +json.exception.invalid_iterator.209 | cannot use offsets with object iterators | The offset operators (+, -, +=, -=) cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. +json.exception.invalid_iterator.210 | iterators do not fit | The iterator range passed to the insert function are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. +json.exception.invalid_iterator.211 | passed iterators may not belong to container | The iterator range passed to the insert function must not be a subrange of the container to insert to. +json.exception.invalid_iterator.212 | cannot compare iterators of different containers | When two iterators are compared, they must belong to the same container. +json.exception.invalid_iterator.213 | cannot compare order of object iterators | The order of object iterators cannot be compared, because JSON objects are unordered. +json.exception.invalid_iterator.214 | cannot get value | Cannot get value for iterator: Either the iterator belongs to a null value or it is an iterator to a primitive type (number, boolean, or string), but the iterator is different to @ref begin(). + +@liveexample{The following code shows how an `invalid_iterator` exception can be +caught.,invalid_iterator} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class invalid_iterator : public exception +{ + public: + template + static invalid_iterator create(int id_, const std::string& what_arg, const BasicJsonType& context) + { + std::string w = exception::name("invalid_iterator", id_) + exception::diagnostics(context) + what_arg; + return invalid_iterator(id_, w.c_str()); + } + + private: + JSON_HEDLEY_NON_NULL(3) + invalid_iterator(int id_, const char* what_arg) + : exception(id_, what_arg) {} +}; + +/*! +@brief exception indicating executing a member function with a wrong type + +This exception is thrown in case of a type error; that is, a library function is +executed on a JSON value whose type does not match the expected semantics. + +Exceptions have ids 3xx. + +name / id | example message | description +----------------------------- | --------------- | ------------------------- +json.exception.type_error.301 | cannot create object from initializer list | To create an object from an initializer list, the initializer list must consist only of a list of pairs whose first element is a string. When this constraint is violated, an array is created instead. +json.exception.type_error.302 | type must be object, but is array | During implicit or explicit value conversion, the JSON type must be compatible to the target type. For instance, a JSON string can only be converted into string types, but not into numbers or boolean types. +json.exception.type_error.303 | incompatible ReferenceType for get_ref, actual type is object | To retrieve a reference to a value stored in a @ref basic_json object with @ref get_ref, the type of the reference must match the value type. For instance, for a JSON array, the @a ReferenceType must be @ref array_t &. +json.exception.type_error.304 | cannot use at() with string | The @ref at() member functions can only be executed for certain JSON types. +json.exception.type_error.305 | cannot use operator[] with string | The @ref operator[] member functions can only be executed for certain JSON types. +json.exception.type_error.306 | cannot use value() with string | The @ref value() member functions can only be executed for certain JSON types. +json.exception.type_error.307 | cannot use erase() with string | The @ref erase() member functions can only be executed for certain JSON types. +json.exception.type_error.308 | cannot use push_back() with string | The @ref push_back() and @ref operator+= member functions can only be executed for certain JSON types. +json.exception.type_error.309 | cannot use insert() with | The @ref insert() member functions can only be executed for certain JSON types. +json.exception.type_error.310 | cannot use swap() with number | The @ref swap() member functions can only be executed for certain JSON types. +json.exception.type_error.311 | cannot use emplace_back() with string | The @ref emplace_back() member function can only be executed for certain JSON types. +json.exception.type_error.312 | cannot use update() with string | The @ref update() member functions can only be executed for certain JSON types. +json.exception.type_error.313 | invalid value to unflatten | The @ref unflatten function converts an object whose keys are JSON Pointers back into an arbitrary nested JSON value. The JSON Pointers must not overlap, because then the resulting value would not be well defined. +json.exception.type_error.314 | only objects can be unflattened | The @ref unflatten function only works for an object whose keys are JSON Pointers. +json.exception.type_error.315 | values in object must be primitive | The @ref unflatten function only works for an object whose keys are JSON Pointers and whose values are primitive. +json.exception.type_error.316 | invalid UTF-8 byte at index 10: 0x7E | The @ref dump function only works with UTF-8 encoded strings; that is, if you assign a `std::string` to a JSON value, make sure it is UTF-8 encoded. | +json.exception.type_error.317 | JSON value cannot be serialized to requested format | The dynamic type of the object cannot be represented in the requested serialization format (e.g. a raw `true` or `null` JSON object cannot be serialized to BSON) | + +@liveexample{The following code shows how a `type_error` exception can be +caught.,type_error} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref out_of_range for exceptions indicating access out of the defined range +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class type_error : public exception +{ + public: + template + static type_error create(int id_, const std::string& what_arg, const BasicJsonType& context) + { + std::string w = exception::name("type_error", id_) + exception::diagnostics(context) + what_arg; + return type_error(id_, w.c_str()); + } + + private: + JSON_HEDLEY_NON_NULL(3) + type_error(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; + +/*! +@brief exception indicating access out of the defined range + +This exception is thrown in case a library function is called on an input +parameter that exceeds the expected range, for instance in case of array +indices or nonexisting object keys. + +Exceptions have ids 4xx. + +name / id | example message | description +------------------------------- | --------------- | ------------------------- +json.exception.out_of_range.401 | array index 3 is out of range | The provided array index @a i is larger than @a size-1. +json.exception.out_of_range.402 | array index '-' (3) is out of range | The special array index `-` in a JSON Pointer never describes a valid element of the array, but the index past the end. That is, it can only be used to add elements at this position, but not to read it. +json.exception.out_of_range.403 | key 'foo' not found | The provided key was not found in the JSON object. +json.exception.out_of_range.404 | unresolved reference token 'foo' | A reference token in a JSON Pointer could not be resolved. +json.exception.out_of_range.405 | JSON pointer has no parent | The JSON Patch operations 'remove' and 'add' can not be applied to the root element of the JSON value. +json.exception.out_of_range.406 | number overflow parsing '10E1000' | A parsed number could not be stored as without changing it to NaN or INF. +json.exception.out_of_range.407 | number overflow serializing '9223372036854775808' | UBJSON and BSON only support integer numbers up to 9223372036854775807. (until version 3.8.0) | +json.exception.out_of_range.408 | excessive array size: 8658170730974374167 | The size (following `#`) of an UBJSON array or object exceeds the maximal capacity. | +json.exception.out_of_range.409 | BSON key cannot contain code point U+0000 (at byte 2) | Key identifiers to be serialized to BSON cannot contain code point U+0000, since the key is stored as zero-terminated c-string | + +@liveexample{The following code shows how an `out_of_range` exception can be +caught.,out_of_range} + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref other_error for exceptions indicating other library errors + +@since version 3.0.0 +*/ +class out_of_range : public exception +{ + public: + template + static out_of_range create(int id_, const std::string& what_arg, const BasicJsonType& context) + { + std::string w = exception::name("out_of_range", id_) + exception::diagnostics(context) + what_arg; + return out_of_range(id_, w.c_str()); + } + + private: + JSON_HEDLEY_NON_NULL(3) + out_of_range(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; + +/*! +@brief exception indicating other library errors + +This exception is thrown in case of errors that cannot be classified with the +other exception types. + +Exceptions have ids 5xx. + +name / id | example message | description +------------------------------ | --------------- | ------------------------- +json.exception.other_error.501 | unsuccessful: {"op":"test","path":"/baz", "value":"bar"} | A JSON Patch operation 'test' failed. The unsuccessful operation is also printed. + +@sa - @ref exception for the base class of the library exceptions +@sa - @ref parse_error for exceptions indicating a parse error +@sa - @ref invalid_iterator for exceptions indicating errors with iterators +@sa - @ref type_error for exceptions indicating executing a member function with + a wrong type +@sa - @ref out_of_range for exceptions indicating access out of the defined range + +@liveexample{The following code shows how an `other_error` exception can be +caught.,other_error} + +@since version 3.0.0 +*/ +class other_error : public exception +{ + public: + template + static other_error create(int id_, const std::string& what_arg, const BasicJsonType& context) + { + std::string w = exception::name("other_error", id_) + exception::diagnostics(context) + what_arg; + return other_error(id_, w.c_str()); + } + + private: + JSON_HEDLEY_NON_NULL(3) + other_error(int id_, const char* what_arg) : exception(id_, what_arg) {} +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + + +#include // size_t +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#include // index_sequence, make_index_sequence, index_sequence_for + +// #include + + +namespace nlohmann +{ +namespace detail +{ + +template +using uncvref_t = typename std::remove_cv::type>::type; + +#ifdef JSON_HAS_CPP_14 + +// the following utilities are natively available in C++14 +using std::enable_if_t; +using std::index_sequence; +using std::make_index_sequence; +using std::index_sequence_for; + +#else + +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +// The following code is taken from https://github.com/abseil/abseil-cpp/blob/10cb35e459f5ecca5b2ff107635da0bfa41011b4/absl/utility/utility.h +// which is part of Google Abseil (https://github.com/abseil/abseil-cpp), licensed under the Apache License 2.0. + +//// START OF CODE FROM GOOGLE ABSEIL + +// integer_sequence +// +// Class template representing a compile-time integer sequence. An instantiation +// of `integer_sequence` has a sequence of integers encoded in its +// type through its template arguments (which is a common need when +// working with C++11 variadic templates). `absl::integer_sequence` is designed +// to be a drop-in replacement for C++14's `std::integer_sequence`. +// +// Example: +// +// template< class T, T... Ints > +// void user_function(integer_sequence); +// +// int main() +// { +// // user_function's `T` will be deduced to `int` and `Ints...` +// // will be deduced to `0, 1, 2, 3, 4`. +// user_function(make_integer_sequence()); +// } +template +struct integer_sequence +{ + using value_type = T; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; + +// index_sequence +// +// A helper template for an `integer_sequence` of `size_t`, +// `absl::index_sequence` is designed to be a drop-in replacement for C++14's +// `std::index_sequence`. +template +using index_sequence = integer_sequence; + +namespace utility_internal +{ + +template +struct Extend; + +// Note that SeqSize == sizeof...(Ints). It's passed explicitly for efficiency. +template +struct Extend, SeqSize, 0> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)... >; +}; + +template +struct Extend, SeqSize, 1> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)..., 2 * SeqSize >; +}; + +// Recursion helper for 'make_integer_sequence'. +// 'Gen::type' is an alias for 'integer_sequence'. +template +struct Gen +{ + using type = + typename Extend < typename Gen < T, N / 2 >::type, N / 2, N % 2 >::type; +}; + +template +struct Gen +{ + using type = integer_sequence; +}; + +} // namespace utility_internal + +// Compile-time sequences of integers + +// make_integer_sequence +// +// This template alias is equivalent to +// `integer_sequence`, and is designed to be a drop-in +// replacement for C++14's `std::make_integer_sequence`. +template +using make_integer_sequence = typename utility_internal::Gen::type; + +// make_index_sequence +// +// This template alias is equivalent to `index_sequence<0, 1, ..., N-1>`, +// and is designed to be a drop-in replacement for C++14's +// `std::make_index_sequence`. +template +using make_index_sequence = make_integer_sequence; + +// index_sequence_for +// +// Converts a typename pack into an index sequence of the same length, and +// is designed to be a drop-in replacement for C++14's +// `std::index_sequence_for()` +template +using index_sequence_for = make_index_sequence; + +//// END OF CODE FROM GOOGLE ABSEIL + +#endif + +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; + +// taken from ranges-v3 +template +struct static_const +{ + static constexpr T value{}; +}; + +template +constexpr T static_const::value; + +} // namespace detail +} // namespace nlohmann + +// #include + + +namespace nlohmann +{ +namespace detail +{ +// dispatching helper struct +template struct identity_tag {}; +} // namespace detail +} // namespace nlohmann + +// #include + + +#include // numeric_limits +#include // false_type, is_constructible, is_integral, is_same, true_type +#include // declval +#include // tuple + +// #include + + +// #include + + +#include // random_access_iterator_tag + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +template +struct iterator_types {}; + +template +struct iterator_types < + It, + void_t> +{ + using difference_type = typename It::difference_type; + using value_type = typename It::value_type; + using pointer = typename It::pointer; + using reference = typename It::reference; + using iterator_category = typename It::iterator_category; +}; + +// This is required as some compilers implement std::iterator_traits in a way that +// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. +template +struct iterator_traits +{ +}; + +template +struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> + : iterator_types +{ +}; + +template +struct iterator_traits::value>> +{ + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; +}; +} // namespace detail +} // namespace nlohmann + +// #include + + +// #include + + +namespace nlohmann +{ +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(begin); +} // namespace nlohmann + +// #include + + +// #include + + +namespace nlohmann +{ +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(end); +} // namespace nlohmann + +// #include + +// #include + +// #include +#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ +#define INCLUDE_NLOHMANN_JSON_FWD_HPP_ + +#include // int64_t, uint64_t +#include // map +#include // allocator +#include // string +#include // vector + +/*! +@brief namespace for Niels Lohmann +@see https://github.com/nlohmann +@since version 1.0.0 +*/ +namespace nlohmann +{ +/*! +@brief default JSONSerializer template argument + +This serializer ignores the template arguments and uses ADL +([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) +for serialization. +*/ +template +struct adl_serializer; + +template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer, + class BinaryType = std::vector> +class basic_json; + +/*! +@brief JSON Pointer + +A JSON pointer defines a string syntax for identifying a specific value +within a JSON document. It can be used with functions `at` and +`operator[]`. Furthermore, JSON pointers are the base for JSON patches. + +@sa [RFC 6901](https://tools.ietf.org/html/rfc6901) + +@since version 2.0.0 +*/ +template +class json_pointer; + +/*! +@brief default JSON class + +This type is the default specialization of the @ref basic_json class which +uses the standard template types. + +@since version 1.0.0 +*/ +using json = basic_json<>; + +template +struct ordered_map; + +/*! +@brief ordered JSON class + +This type preserves the insertion order of object keys. + +@since version 3.9.0 +*/ +using ordered_json = basic_json; + +} // namespace nlohmann + +#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + +namespace nlohmann +{ +/*! +@brief detail namespace with internal helper functions + +This namespace collects functions that should not be exposed, +implementations of some @ref basic_json methods, and meta-programming helpers. + +@since version 2.1.0 +*/ +namespace detail +{ +///////////// +// helpers // +///////////// + +// Note to maintainers: +// +// Every trait in this file expects a non CV-qualified type. +// The only exceptions are in the 'aliases for detected' section +// (i.e. those of the form: decltype(T::member_function(std::declval()))) +// +// In this case, T has to be properly CV-qualified to constraint the function arguments +// (e.g. to_json(BasicJsonType&, const T&)) + +template struct is_basic_json : std::false_type {}; + +NLOHMANN_BASIC_JSON_TPL_DECLARATION +struct is_basic_json : std::true_type {}; + +////////////////////// +// json_ref helpers // +////////////////////// + +template +class json_ref; + +template +struct is_json_ref : std::false_type {}; + +template +struct is_json_ref> : std::true_type {}; + +////////////////////////// +// aliases for detected // +////////////////////////// + +template +using mapped_type_t = typename T::mapped_type; + +template +using key_type_t = typename T::key_type; + +template +using value_type_t = typename T::value_type; + +template +using difference_type_t = typename T::difference_type; + +template +using pointer_t = typename T::pointer; + +template +using reference_t = typename T::reference; + +template +using iterator_category_t = typename T::iterator_category; + +template +using to_json_function = decltype(T::to_json(std::declval()...)); + +template +using from_json_function = decltype(T::from_json(std::declval()...)); + +template +using get_template_function = decltype(std::declval().template get()); + +// trait checking if JSONSerializer::from_json(json const&, udt&) exists +template +struct has_from_json : std::false_type {}; + +// trait checking if j.get is valid +// use this trait instead of std::is_constructible or std::is_convertible, +// both rely on, or make use of implicit conversions, and thus fail when T +// has several constructors/operator= (see https://github.com/nlohmann/json/issues/958) +template +struct is_getable +{ + static constexpr bool value = is_detected::value; +}; + +template +struct has_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if JSONSerializer::from_json(json const&) exists +// this overload is used for non-default-constructible user-defined-types +template +struct has_non_default_from_json : std::false_type {}; + +template +struct has_non_default_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if BasicJsonType::json_serializer::to_json exists +// Do not evaluate the trait when T is a basic_json type, to avoid template instantiation infinite recursion. +template +struct has_to_json : std::false_type {}; + +template +struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + + +/////////////////// +// is_ functions // +/////////////////// + +// https://en.cppreference.com/w/cpp/types/conjunction +template struct conjunction : std::true_type { }; +template struct conjunction : B1 { }; +template +struct conjunction +: std::conditional, B1>::type {}; + +// https://en.cppreference.com/w/cpp/types/negation +template struct negation : std::integral_constant < bool, !B::value > { }; + +// Reimplementation of is_constructible and is_default_constructible, due to them being broken for +// std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). +// This causes compile errors in e.g. clang 3.5 or gcc 4.9. +template +struct is_default_constructible : std::is_default_constructible {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + + +template +struct is_constructible : std::is_constructible {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + + +template +struct is_iterator_traits : std::false_type {}; + +template +struct is_iterator_traits> +{ + private: + using traits = iterator_traits; + + public: + static constexpr auto value = + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value; +}; + +template +struct is_range +{ + private: + using t_ref = typename std::add_lvalue_reference::type; + + using iterator = detected_t; + using sentinel = detected_t; + + // to be 100% correct, it should use https://en.cppreference.com/w/cpp/iterator/input_or_output_iterator + // and https://en.cppreference.com/w/cpp/iterator/sentinel_for + // but reimplementing these would be too much work, as a lot of other concepts are used underneath + static constexpr auto is_iterator_begin = + is_iterator_traits>::value; + + public: + static constexpr bool value = !std::is_same::value && !std::is_same::value && is_iterator_begin; +}; + +template +using iterator_t = enable_if_t::value, result_of_begin())>>; + +template +using range_value_t = value_type_t>>; + +// The following implementation of is_complete_type is taken from +// https://blogs.msdn.microsoft.com/vcblog/2015/12/02/partial-support-for-expression-sfinae-in-vs-2015-update-1/ +// and is written by Xiang Fan who agreed to using it in this library. + +template +struct is_complete_type : std::false_type {}; + +template +struct is_complete_type : std::true_type {}; + +template +struct is_compatible_object_type_impl : std::false_type {}; + +template +struct is_compatible_object_type_impl < + BasicJsonType, CompatibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + // macOS's is_constructible does not play well with nonesuch... + static constexpr bool value = + is_constructible::value && + is_constructible::value; +}; + +template +struct is_compatible_object_type + : is_compatible_object_type_impl {}; + +template +struct is_constructible_object_type_impl : std::false_type {}; + +template +struct is_constructible_object_type_impl < + BasicJsonType, ConstructibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + static constexpr bool value = + (is_default_constructible::value && + (std::is_move_assignable::value || + std::is_copy_assignable::value) && + (is_constructible::value && + std::is_same < + typename object_t::mapped_type, + typename ConstructibleObjectType::mapped_type >::value)) || + (has_from_json::value || + has_non_default_from_json < + BasicJsonType, + typename ConstructibleObjectType::mapped_type >::value); +}; + +template +struct is_constructible_object_type + : is_constructible_object_type_impl {}; + +template +struct is_compatible_string_type +{ + static constexpr auto value = + is_constructible::value; +}; + +template +struct is_constructible_string_type +{ + static constexpr auto value = + is_constructible::value; +}; + +template +struct is_compatible_array_type_impl : std::false_type {}; + +template +struct is_compatible_array_type_impl < + BasicJsonType, CompatibleArrayType, + enable_if_t < + is_detected::value&& + is_iterator_traits>>::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 + !std::is_same>::value >> +{ + static constexpr bool value = + is_constructible>::value; +}; + +template +struct is_compatible_array_type + : is_compatible_array_type_impl {}; + +template +struct is_constructible_array_type_impl : std::false_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t::value >> + : std::true_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t < !std::is_same::value&& + !is_compatible_string_type::value&& + is_default_constructible::value&& +(std::is_move_assignable::value || + std::is_copy_assignable::value)&& +is_detected::value&& +is_iterator_traits>>::value&& +is_detected::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 +!std::is_same>::value&& + is_complete_type < + detected_t>::value >> +{ + using value_type = range_value_t; + + static constexpr bool value = + std::is_same::value || + has_from_json::value || + has_non_default_from_json < + BasicJsonType, + value_type >::value; +}; + +template +struct is_constructible_array_type + : is_constructible_array_type_impl {}; + +template +struct is_compatible_integer_type_impl : std::false_type {}; + +template +struct is_compatible_integer_type_impl < + RealIntegerType, CompatibleNumberIntegerType, + enable_if_t < std::is_integral::value&& + std::is_integral::value&& + !std::is_same::value >> +{ + // is there an assert somewhere on overflows? + using RealLimits = std::numeric_limits; + using CompatibleLimits = std::numeric_limits; + + static constexpr auto value = + is_constructible::value && + CompatibleLimits::is_integer && + RealLimits::is_signed == CompatibleLimits::is_signed; +}; + +template +struct is_compatible_integer_type + : is_compatible_integer_type_impl {}; + +template +struct is_compatible_type_impl: std::false_type {}; + +template +struct is_compatible_type_impl < + BasicJsonType, CompatibleType, + enable_if_t::value >> +{ + static constexpr bool value = + has_to_json::value; +}; + +template +struct is_compatible_type + : is_compatible_type_impl {}; + +template +struct is_constructible_tuple : std::false_type {}; + +template +struct is_constructible_tuple> : conjunction...> {}; + +// a naive helper to check if a type is an ordered_map (exploits the fact that +// ordered_map inherits capacity() from std::vector) +template +struct is_ordered_map +{ + using one = char; + + struct two + { + char x[2]; // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + }; + + template static one test( decltype(&C::capacity) ) ; + template static two test(...); + + enum { value = sizeof(test(nullptr)) == sizeof(char) }; // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) +}; + +// to avoid useless casts (see https://github.com/nlohmann/json/issues/2893#issuecomment-889152324) +template < typename T, typename U, enable_if_t < !std::is_same::value, int > = 0 > +T conditional_static_cast(U value) +{ + return static_cast(value); +} + +template::value, int> = 0> +T conditional_static_cast(U value) +{ + return value; +} + +} // namespace detail +} // namespace nlohmann + +// #include + + +#ifdef JSON_HAS_CPP_17 + #include +#endif + +namespace nlohmann +{ +namespace detail +{ +template +void from_json(const BasicJsonType& j, typename std::nullptr_t& n) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_null())) + { + JSON_THROW(type_error::create(302, "type must be null, but is " + std::string(j.type_name()), j)); + } + n = nullptr; +} + +// overloads for basic_json template parameters +template < typename BasicJsonType, typename ArithmeticType, + enable_if_t < std::is_arithmetic::value&& + !std::is_same::value, + int > = 0 > +void get_arithmetic_value(const BasicJsonType& j, ArithmeticType& val) +{ + switch (static_cast(j)) + { + case value_t::number_unsigned: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::number_integer: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::number_float: + { + val = static_cast(*j.template get_ptr()); + break; + } + + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::boolean: + case value_t::binary: + case value_t::discarded: + default: + JSON_THROW(type_error::create(302, "type must be number, but is " + std::string(j.type_name()), j)); + } +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::boolean_t& b) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_boolean())) + { + JSON_THROW(type_error::create(302, "type must be boolean, but is " + std::string(j.type_name()), j)); + } + b = *j.template get_ptr(); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::string_t& s) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_string())) + { + JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name()), j)); + } + s = *j.template get_ptr(); +} + +template < + typename BasicJsonType, typename ConstructibleStringType, + enable_if_t < + is_constructible_string_type::value&& + !std::is_same::value, + int > = 0 > +void from_json(const BasicJsonType& j, ConstructibleStringType& s) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_string())) + { + JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name()), j)); + } + + s = *j.template get_ptr(); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::number_float_t& val) +{ + get_arithmetic_value(j, val); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::number_unsigned_t& val) +{ + get_arithmetic_value(j, val); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::number_integer_t& val) +{ + get_arithmetic_value(j, val); +} + +template::value, int> = 0> +void from_json(const BasicJsonType& j, EnumType& e) +{ + typename std::underlying_type::type val; + get_arithmetic_value(j, val); + e = static_cast(val); +} + +// forward_list doesn't have an insert method +template::value, int> = 0> +void from_json(const BasicJsonType& j, std::forward_list& l) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + l.clear(); + std::transform(j.rbegin(), j.rend(), + std::front_inserter(l), [](const BasicJsonType & i) + { + return i.template get(); + }); +} + +// valarray doesn't have an insert method +template::value, int> = 0> +void from_json(const BasicJsonType& j, std::valarray& l) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + l.resize(j.size()); + std::transform(j.begin(), j.end(), std::begin(l), + [](const BasicJsonType & elem) + { + return elem.template get(); + }); +} + +template +auto from_json(const BasicJsonType& j, T (&arr)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) +-> decltype(j.template get(), void()) +{ + for (std::size_t i = 0; i < N; ++i) + { + arr[i] = j.at(i).template get(); + } +} + +template +void from_json_array_impl(const BasicJsonType& j, typename BasicJsonType::array_t& arr, priority_tag<3> /*unused*/) +{ + arr = *j.template get_ptr(); +} + +template +auto from_json_array_impl(const BasicJsonType& j, std::array& arr, + priority_tag<2> /*unused*/) +-> decltype(j.template get(), void()) +{ + for (std::size_t i = 0; i < N; ++i) + { + arr[i] = j.at(i).template get(); + } +} + +template::value, + int> = 0> +auto from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, priority_tag<1> /*unused*/) +-> decltype( + arr.reserve(std::declval()), + j.template get(), + void()) +{ + using std::end; + + ConstructibleArrayType ret; + ret.reserve(j.size()); + std::transform(j.begin(), j.end(), + std::inserter(ret, end(ret)), [](const BasicJsonType & i) + { + // get() returns *this, this won't call a from_json + // method when value_type is BasicJsonType + return i.template get(); + }); + arr = std::move(ret); +} + +template::value, + int> = 0> +void from_json_array_impl(const BasicJsonType& j, ConstructibleArrayType& arr, + priority_tag<0> /*unused*/) +{ + using std::end; + + ConstructibleArrayType ret; + std::transform( + j.begin(), j.end(), std::inserter(ret, end(ret)), + [](const BasicJsonType & i) + { + // get() returns *this, this won't call a from_json + // method when value_type is BasicJsonType + return i.template get(); + }); + arr = std::move(ret); +} + +template < typename BasicJsonType, typename ConstructibleArrayType, + enable_if_t < + is_constructible_array_type::value&& + !is_constructible_object_type::value&& + !is_constructible_string_type::value&& + !std::is_same::value&& + !is_basic_json::value, + int > = 0 > +auto from_json(const BasicJsonType& j, ConstructibleArrayType& arr) +-> decltype(from_json_array_impl(j, arr, priority_tag<3> {}), +j.template get(), +void()) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + + from_json_array_impl(j, arr, priority_tag<3> {}); +} + +template < typename BasicJsonType, typename T, std::size_t... Idx > +std::array from_json_inplace_array_impl(BasicJsonType&& j, + identity_tag> /*unused*/, index_sequence /*unused*/) +{ + return { { std::forward(j).at(Idx).template get()... } }; +} + +template < typename BasicJsonType, typename T, std::size_t N > +auto from_json(BasicJsonType&& j, identity_tag> tag) +-> decltype(from_json_inplace_array_impl(std::forward(j), tag, make_index_sequence {})) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + + return from_json_inplace_array_impl(std::forward(j), tag, make_index_sequence {}); +} + +template +void from_json(const BasicJsonType& j, typename BasicJsonType::binary_t& bin) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_binary())) + { + JSON_THROW(type_error::create(302, "type must be binary, but is " + std::string(j.type_name()), j)); + } + + bin = *j.template get_ptr(); +} + +template::value, int> = 0> +void from_json(const BasicJsonType& j, ConstructibleObjectType& obj) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_object())) + { + JSON_THROW(type_error::create(302, "type must be object, but is " + std::string(j.type_name()), j)); + } + + ConstructibleObjectType ret; + const auto* inner_object = j.template get_ptr(); + using value_type = typename ConstructibleObjectType::value_type; + std::transform( + inner_object->begin(), inner_object->end(), + std::inserter(ret, ret.begin()), + [](typename BasicJsonType::object_t::value_type const & p) + { + return value_type(p.first, p.second.template get()); + }); + obj = std::move(ret); +} + +// overload for arithmetic types, not chosen for basic_json template arguments +// (BooleanType, etc..); note: Is it really necessary to provide explicit +// overloads for boolean_t etc. in case of a custom BooleanType which is not +// an arithmetic type? +template < typename BasicJsonType, typename ArithmeticType, + enable_if_t < + std::is_arithmetic::value&& + !std::is_same::value&& + !std::is_same::value&& + !std::is_same::value&& + !std::is_same::value, + int > = 0 > +void from_json(const BasicJsonType& j, ArithmeticType& val) +{ + switch (static_cast(j)) + { + case value_t::number_unsigned: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::number_integer: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::number_float: + { + val = static_cast(*j.template get_ptr()); + break; + } + case value_t::boolean: + { + val = static_cast(*j.template get_ptr()); + break; + } + + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::string: + case value_t::binary: + case value_t::discarded: + default: + JSON_THROW(type_error::create(302, "type must be number, but is " + std::string(j.type_name()), j)); + } +} + +template +std::tuple from_json_tuple_impl_base(BasicJsonType&& j, index_sequence /*unused*/) +{ + return std::make_tuple(std::forward(j).at(Idx).template get()...); +} + +template < typename BasicJsonType, class A1, class A2 > +std::pair from_json_tuple_impl(BasicJsonType&& j, identity_tag> /*unused*/, priority_tag<0> /*unused*/) +{ + return {std::forward(j).at(0).template get(), + std::forward(j).at(1).template get()}; +} + +template +void from_json_tuple_impl(BasicJsonType&& j, std::pair& p, priority_tag<1> /*unused*/) +{ + p = from_json_tuple_impl(std::forward(j), identity_tag> {}, priority_tag<0> {}); +} + +template +std::tuple from_json_tuple_impl(BasicJsonType&& j, identity_tag> /*unused*/, priority_tag<2> /*unused*/) +{ + return from_json_tuple_impl_base(std::forward(j), index_sequence_for {}); +} + +template +void from_json_tuple_impl(BasicJsonType&& j, std::tuple& t, priority_tag<3> /*unused*/) +{ + t = from_json_tuple_impl_base(std::forward(j), index_sequence_for {}); +} + +template +auto from_json(BasicJsonType&& j, TupleRelated&& t) +-> decltype(from_json_tuple_impl(std::forward(j), std::forward(t), priority_tag<3> {})) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + + return from_json_tuple_impl(std::forward(j), std::forward(t), priority_tag<3> {}); +} + +template < typename BasicJsonType, typename Key, typename Value, typename Compare, typename Allocator, + typename = enable_if_t < !std::is_constructible < + typename BasicJsonType::string_t, Key >::value >> +void from_json(const BasicJsonType& j, std::map& m) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + m.clear(); + for (const auto& p : j) + { + if (JSON_HEDLEY_UNLIKELY(!p.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(p.type_name()), j)); + } + m.emplace(p.at(0).template get(), p.at(1).template get()); + } +} + +template < typename BasicJsonType, typename Key, typename Value, typename Hash, typename KeyEqual, typename Allocator, + typename = enable_if_t < !std::is_constructible < + typename BasicJsonType::string_t, Key >::value >> +void from_json(const BasicJsonType& j, std::unordered_map& m) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(j.type_name()), j)); + } + m.clear(); + for (const auto& p : j) + { + if (JSON_HEDLEY_UNLIKELY(!p.is_array())) + { + JSON_THROW(type_error::create(302, "type must be array, but is " + std::string(p.type_name()), j)); + } + m.emplace(p.at(0).template get(), p.at(1).template get()); + } +} + +#ifdef JSON_HAS_CPP_17 +template +void from_json(const BasicJsonType& j, std::filesystem::path& p) +{ + if (JSON_HEDLEY_UNLIKELY(!j.is_string())) + { + JSON_THROW(type_error::create(302, "type must be string, but is " + std::string(j.type_name()), j)); + } + p = *j.template get_ptr(); +} +#endif + +struct from_json_fn +{ + template + auto operator()(const BasicJsonType& j, T&& val) const + noexcept(noexcept(from_json(j, std::forward(val)))) + -> decltype(from_json(j, std::forward(val))) + { + return from_json(j, std::forward(val)); + } +}; +} // namespace detail + +/// namespace to hold default `from_json` function +/// to see why this is required: +/// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html +namespace // NOLINT(cert-dcl59-cpp,fuchsia-header-anon-namespaces,google-build-namespaces) +{ +constexpr const auto& from_json = detail::static_const::value; // NOLINT(misc-definitions-in-headers) +} // namespace +} // namespace nlohmann + +// #include + + +#include // copy +#include // begin, end +#include // string +#include // tuple, get +#include // is_same, is_constructible, is_floating_point, is_enum, underlying_type +#include // move, forward, declval, pair +#include // valarray +#include // vector + +// #include + +// #include + + +#include // size_t +#include // input_iterator_tag +#include // string, to_string +#include // tuple_size, get, tuple_element +#include // move + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +template +void int_to_string( string_type& target, std::size_t value ) +{ + // For ADL + using std::to_string; + target = to_string(value); +} +template class iteration_proxy_value +{ + public: + using difference_type = std::ptrdiff_t; + using value_type = iteration_proxy_value; + using pointer = value_type * ; + using reference = value_type & ; + using iterator_category = std::input_iterator_tag; + using string_type = typename std::remove_cv< typename std::remove_reference().key() ) >::type >::type; + + private: + /// the iterator + IteratorType anchor; + /// an index for arrays (used to create key names) + std::size_t array_index = 0; + /// last stringified array index + mutable std::size_t array_index_last = 0; + /// a string representation of the array index + mutable string_type array_index_str = "0"; + /// an empty string (to return a reference for primitive values) + const string_type empty_str{}; + + public: + explicit iteration_proxy_value(IteratorType it) noexcept + : anchor(std::move(it)) + {} + + /// dereference operator (needed for range-based for) + iteration_proxy_value& operator*() + { + return *this; + } + + /// increment operator (needed for range-based for) + iteration_proxy_value& operator++() + { + ++anchor; + ++array_index; + + return *this; + } + + /// equality operator (needed for InputIterator) + bool operator==(const iteration_proxy_value& o) const + { + return anchor == o.anchor; + } + + /// inequality operator (needed for range-based for) + bool operator!=(const iteration_proxy_value& o) const + { + return anchor != o.anchor; + } + + /// return key of the iterator + const string_type& key() const + { + JSON_ASSERT(anchor.m_object != nullptr); + + switch (anchor.m_object->type()) + { + // use integer array index as key + case value_t::array: + { + if (array_index != array_index_last) + { + int_to_string( array_index_str, array_index ); + array_index_last = array_index; + } + return array_index_str; + } + + // use key from the object + case value_t::object: + return anchor.key(); + + // use an empty key for all primitive types + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return empty_str; + } + } + + /// return value of the iterator + typename IteratorType::reference value() const + { + return anchor.value(); + } +}; + +/// proxy class for the items() function +template class iteration_proxy +{ + private: + /// the container to iterate + typename IteratorType::reference container; + + public: + /// construct iteration proxy from a container + explicit iteration_proxy(typename IteratorType::reference cont) noexcept + : container(cont) {} + + /// return iterator begin (needed for range-based for) + iteration_proxy_value begin() noexcept + { + return iteration_proxy_value(container.begin()); + } + + /// return iterator end (needed for range-based for) + iteration_proxy_value end() noexcept + { + return iteration_proxy_value(container.end()); + } +}; +// Structured Bindings Support +// For further reference see https://blog.tartanllama.xyz/structured-bindings/ +// And see https://github.com/nlohmann/json/pull/1391 +template = 0> +auto get(const nlohmann::detail::iteration_proxy_value& i) -> decltype(i.key()) +{ + return i.key(); +} +// Structured Bindings Support +// For further reference see https://blog.tartanllama.xyz/structured-bindings/ +// And see https://github.com/nlohmann/json/pull/1391 +template = 0> +auto get(const nlohmann::detail::iteration_proxy_value& i) -> decltype(i.value()) +{ + return i.value(); +} +} // namespace detail +} // namespace nlohmann + +// The Addition to the STD Namespace is required to add +// Structured Bindings Support to the iteration_proxy_value class +// For further reference see https://blog.tartanllama.xyz/structured-bindings/ +// And see https://github.com/nlohmann/json/pull/1391 +namespace std +{ +#if defined(__clang__) + // Fix: https://github.com/nlohmann/json/issues/1401 + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wmismatched-tags" +#endif +template +class tuple_size<::nlohmann::detail::iteration_proxy_value> + : public std::integral_constant {}; + +template +class tuple_element> +{ + public: + using type = decltype( + get(std::declval < + ::nlohmann::detail::iteration_proxy_value> ())); +}; +#if defined(__clang__) + #pragma clang diagnostic pop +#endif +} // namespace std + +// #include + +// #include + +// #include + + +#ifdef JSON_HAS_CPP_17 + #include +#endif + +namespace nlohmann +{ +namespace detail +{ +////////////////// +// constructors // +////////////////// + +/* + * Note all external_constructor<>::construct functions need to call + * j.m_value.destroy(j.m_type) to avoid a memory leak in case j contains an + * allocated value (e.g., a string). See bug issue + * https://github.com/nlohmann/json/issues/2865 for more information. + */ + +template struct external_constructor; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, typename BasicJsonType::boolean_t b) noexcept + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::boolean; + j.m_value = b; + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, const typename BasicJsonType::string_t& s) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::string; + j.m_value = s; + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, typename BasicJsonType::string_t&& s) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::string; + j.m_value = std::move(s); + j.assert_invariant(); + } + + template < typename BasicJsonType, typename CompatibleStringType, + enable_if_t < !std::is_same::value, + int > = 0 > + static void construct(BasicJsonType& j, const CompatibleStringType& str) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::string; + j.m_value.string = j.template create(str); + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, const typename BasicJsonType::binary_t& b) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::binary; + j.m_value = typename BasicJsonType::binary_t(b); + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, typename BasicJsonType::binary_t&& b) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::binary; + j.m_value = typename BasicJsonType::binary_t(std::move(b)); + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, typename BasicJsonType::number_float_t val) noexcept + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::number_float; + j.m_value = val; + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, typename BasicJsonType::number_unsigned_t val) noexcept + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::number_unsigned; + j.m_value = val; + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, typename BasicJsonType::number_integer_t val) noexcept + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::number_integer; + j.m_value = val; + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, const typename BasicJsonType::array_t& arr) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::array; + j.m_value = arr; + j.set_parents(); + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, typename BasicJsonType::array_t&& arr) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::array; + j.m_value = std::move(arr); + j.set_parents(); + j.assert_invariant(); + } + + template < typename BasicJsonType, typename CompatibleArrayType, + enable_if_t < !std::is_same::value, + int > = 0 > + static void construct(BasicJsonType& j, const CompatibleArrayType& arr) + { + using std::begin; + using std::end; + + j.m_value.destroy(j.m_type); + j.m_type = value_t::array; + j.m_value.array = j.template create(begin(arr), end(arr)); + j.set_parents(); + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, const std::vector& arr) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::array; + j.m_value = value_t::array; + j.m_value.array->reserve(arr.size()); + for (const bool x : arr) + { + j.m_value.array->push_back(x); + j.set_parent(j.m_value.array->back()); + } + j.assert_invariant(); + } + + template::value, int> = 0> + static void construct(BasicJsonType& j, const std::valarray& arr) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::array; + j.m_value = value_t::array; + j.m_value.array->resize(arr.size()); + if (arr.size() > 0) + { + std::copy(std::begin(arr), std::end(arr), j.m_value.array->begin()); + } + j.set_parents(); + j.assert_invariant(); + } +}; + +template<> +struct external_constructor +{ + template + static void construct(BasicJsonType& j, const typename BasicJsonType::object_t& obj) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::object; + j.m_value = obj; + j.set_parents(); + j.assert_invariant(); + } + + template + static void construct(BasicJsonType& j, typename BasicJsonType::object_t&& obj) + { + j.m_value.destroy(j.m_type); + j.m_type = value_t::object; + j.m_value = std::move(obj); + j.set_parents(); + j.assert_invariant(); + } + + template < typename BasicJsonType, typename CompatibleObjectType, + enable_if_t < !std::is_same::value, int > = 0 > + static void construct(BasicJsonType& j, const CompatibleObjectType& obj) + { + using std::begin; + using std::end; + + j.m_value.destroy(j.m_type); + j.m_type = value_t::object; + j.m_value.object = j.template create(begin(obj), end(obj)); + j.set_parents(); + j.assert_invariant(); + } +}; + +///////////// +// to_json // +///////////// + +template::value, int> = 0> +void to_json(BasicJsonType& j, T b) noexcept +{ + external_constructor::construct(j, b); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, const CompatibleString& s) +{ + external_constructor::construct(j, s); +} + +template +void to_json(BasicJsonType& j, typename BasicJsonType::string_t&& s) +{ + external_constructor::construct(j, std::move(s)); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, FloatType val) noexcept +{ + external_constructor::construct(j, static_cast(val)); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, CompatibleNumberUnsignedType val) noexcept +{ + external_constructor::construct(j, static_cast(val)); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, CompatibleNumberIntegerType val) noexcept +{ + external_constructor::construct(j, static_cast(val)); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, EnumType e) noexcept +{ + using underlying_type = typename std::underlying_type::type; + external_constructor::construct(j, static_cast(e)); +} + +template +void to_json(BasicJsonType& j, const std::vector& e) +{ + external_constructor::construct(j, e); +} + +template < typename BasicJsonType, typename CompatibleArrayType, + enable_if_t < is_compatible_array_type::value&& + !is_compatible_object_type::value&& + !is_compatible_string_type::value&& + !std::is_same::value&& + !is_basic_json::value, + int > = 0 > +void to_json(BasicJsonType& j, const CompatibleArrayType& arr) +{ + external_constructor::construct(j, arr); +} + +template +void to_json(BasicJsonType& j, const typename BasicJsonType::binary_t& bin) +{ + external_constructor::construct(j, bin); +} + +template::value, int> = 0> +void to_json(BasicJsonType& j, const std::valarray& arr) +{ + external_constructor::construct(j, std::move(arr)); +} + +template +void to_json(BasicJsonType& j, typename BasicJsonType::array_t&& arr) +{ + external_constructor::construct(j, std::move(arr)); +} + +template < typename BasicJsonType, typename CompatibleObjectType, + enable_if_t < is_compatible_object_type::value&& !is_basic_json::value, int > = 0 > +void to_json(BasicJsonType& j, const CompatibleObjectType& obj) +{ + external_constructor::construct(j, obj); +} + +template +void to_json(BasicJsonType& j, typename BasicJsonType::object_t&& obj) +{ + external_constructor::construct(j, std::move(obj)); +} + +template < + typename BasicJsonType, typename T, std::size_t N, + enable_if_t < !std::is_constructible::value, // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + int > = 0 > +void to_json(BasicJsonType& j, const T(&arr)[N]) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) +{ + external_constructor::construct(j, arr); +} + +template < typename BasicJsonType, typename T1, typename T2, enable_if_t < std::is_constructible::value&& std::is_constructible::value, int > = 0 > +void to_json(BasicJsonType& j, const std::pair& p) +{ + j = { p.first, p.second }; +} + +// for https://github.com/nlohmann/json/pull/1134 +template>::value, int> = 0> +void to_json(BasicJsonType& j, const T& b) +{ + j = { {b.key(), b.value()} }; +} + +template +void to_json_tuple_impl(BasicJsonType& j, const Tuple& t, index_sequence /*unused*/) +{ + j = { std::get(t)... }; +} + +template::value, int > = 0> +void to_json(BasicJsonType& j, const T& t) +{ + to_json_tuple_impl(j, t, make_index_sequence::value> {}); +} + +#ifdef JSON_HAS_CPP_17 +template +void to_json(BasicJsonType& j, const std::filesystem::path& p) +{ + j = p.string(); +} +#endif + +struct to_json_fn +{ + template + auto operator()(BasicJsonType& j, T&& val) const noexcept(noexcept(to_json(j, std::forward(val)))) + -> decltype(to_json(j, std::forward(val)), void()) + { + return to_json(j, std::forward(val)); + } +}; +} // namespace detail + +/// namespace to hold default `to_json` function +/// to see why this is required: +/// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4381.html +namespace // NOLINT(cert-dcl59-cpp,fuchsia-header-anon-namespaces,google-build-namespaces) +{ +constexpr const auto& to_json = detail::static_const::value; // NOLINT(misc-definitions-in-headers) +} // namespace +} // namespace nlohmann + +// #include + +// #include + + +namespace nlohmann +{ + +template +struct adl_serializer +{ + /*! + @brief convert a JSON value to any value type + + This function is usually called by the `get()` function of the + @ref basic_json class (either explicit or via conversion operators). + + @note This function is chosen for default-constructible value types. + + @param[in] j JSON value to read from + @param[in,out] val value to write to + */ + template + static auto from_json(BasicJsonType && j, TargetType& val) noexcept( + noexcept(::nlohmann::from_json(std::forward(j), val))) + -> decltype(::nlohmann::from_json(std::forward(j), val), void()) + { + ::nlohmann::from_json(std::forward(j), val); + } + + /*! + @brief convert a JSON value to any value type + + This function is usually called by the `get()` function of the + @ref basic_json class (either explicit or via conversion operators). + + @note This function is chosen for value types which are not default-constructible. + + @param[in] j JSON value to read from + + @return copy of the JSON value, converted to @a ValueType + */ + template + static auto from_json(BasicJsonType && j) noexcept( + noexcept(::nlohmann::from_json(std::forward(j), detail::identity_tag {}))) + -> decltype(::nlohmann::from_json(std::forward(j), detail::identity_tag {})) + { + return ::nlohmann::from_json(std::forward(j), detail::identity_tag {}); + } + + /*! + @brief convert any value type to a JSON value + + This function is usually called by the constructors of the @ref basic_json + class. + + @param[in,out] j JSON value to write to + @param[in] val value to read from + */ + template + static auto to_json(BasicJsonType& j, TargetType && val) noexcept( + noexcept(::nlohmann::to_json(j, std::forward(val)))) + -> decltype(::nlohmann::to_json(j, std::forward(val)), void()) + { + ::nlohmann::to_json(j, std::forward(val)); + } +}; +} // namespace nlohmann + +// #include + + +#include // uint8_t, uint64_t +#include // tie +#include // move + +namespace nlohmann +{ + +/*! +@brief an internal type for a backed binary type + +This type extends the template parameter @a BinaryType provided to `basic_json` +with a subtype used by BSON and MessagePack. This type exists so that the user +does not have to specify a type themselves with a specific naming scheme in +order to override the binary type. + +@tparam BinaryType container to store bytes (`std::vector` by + default) + +@since version 3.8.0; changed type of subtypes to std::uint64_t in 3.10.0. +*/ +template +class byte_container_with_subtype : public BinaryType +{ + public: + /// the type of the underlying container + using container_type = BinaryType; + /// the type of the subtype + using subtype_type = std::uint64_t; + + byte_container_with_subtype() noexcept(noexcept(container_type())) + : container_type() + {} + + byte_container_with_subtype(const container_type& b) noexcept(noexcept(container_type(b))) + : container_type(b) + {} + + byte_container_with_subtype(container_type&& b) noexcept(noexcept(container_type(std::move(b)))) + : container_type(std::move(b)) + {} + + byte_container_with_subtype(const container_type& b, subtype_type subtype_) noexcept(noexcept(container_type(b))) + : container_type(b) + , m_subtype(subtype_) + , m_has_subtype(true) + {} + + byte_container_with_subtype(container_type&& b, subtype_type subtype_) noexcept(noexcept(container_type(std::move(b)))) + : container_type(std::move(b)) + , m_subtype(subtype_) + , m_has_subtype(true) + {} + + bool operator==(const byte_container_with_subtype& rhs) const + { + return std::tie(static_cast(*this), m_subtype, m_has_subtype) == + std::tie(static_cast(rhs), rhs.m_subtype, rhs.m_has_subtype); + } + + bool operator!=(const byte_container_with_subtype& rhs) const + { + return !(rhs == *this); + } + + /*! + @brief sets the binary subtype + + Sets the binary subtype of the value, also flags a binary JSON value as + having a subtype, which has implications for serialization. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @sa see @ref subtype() -- return the binary subtype + @sa see @ref clear_subtype() -- clears the binary subtype + @sa see @ref has_subtype() -- returns whether or not the binary value has a + subtype + + @since version 3.8.0 + */ + void set_subtype(subtype_type subtype_) noexcept + { + m_subtype = subtype_; + m_has_subtype = true; + } + + /*! + @brief return the binary subtype + + Returns the numerical subtype of the value if it has a subtype. If it does + not have a subtype, this function will return subtype_type(-1) as a sentinel + value. + + @return the numerical subtype of the binary value + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @sa see @ref set_subtype() -- sets the binary subtype + @sa see @ref clear_subtype() -- clears the binary subtype + @sa see @ref has_subtype() -- returns whether or not the binary value has a + subtype + + @since version 3.8.0; fixed return value to properly return + subtype_type(-1) as documented in version 3.10.0 + */ + constexpr subtype_type subtype() const noexcept + { + return m_has_subtype ? m_subtype : subtype_type(-1); + } + + /*! + @brief return whether the value has a subtype + + @return whether the value has a subtype + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @sa see @ref subtype() -- return the binary subtype + @sa see @ref set_subtype() -- sets the binary subtype + @sa see @ref clear_subtype() -- clears the binary subtype + + @since version 3.8.0 + */ + constexpr bool has_subtype() const noexcept + { + return m_has_subtype; + } + + /*! + @brief clears the binary subtype + + Clears the binary subtype and flags the value as not having a subtype, which + has implications for serialization; for instance MessagePack will prefer the + bin family over the ext family. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @sa see @ref subtype() -- return the binary subtype + @sa see @ref set_subtype() -- sets the binary subtype + @sa see @ref has_subtype() -- returns whether or not the binary value has a + subtype + + @since version 3.8.0 + */ + void clear_subtype() noexcept + { + m_subtype = 0; + m_has_subtype = false; + } + + private: + subtype_type m_subtype = 0; + bool m_has_subtype = false; +}; + +} // namespace nlohmann + +// #include + +// #include + +// #include + +// #include + + +#include // uint8_t +#include // size_t +#include // hash + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ + +// boost::hash_combine +inline std::size_t combine(std::size_t seed, std::size_t h) noexcept +{ + seed ^= h + 0x9e3779b9 + (seed << 6U) + (seed >> 2U); + return seed; +} + +/*! +@brief hash a JSON value + +The hash function tries to rely on std::hash where possible. Furthermore, the +type of the JSON value is taken into account to have different hash values for +null, 0, 0U, and false, etc. + +@tparam BasicJsonType basic_json specialization +@param j JSON value to hash +@return hash value of j +*/ +template +std::size_t hash(const BasicJsonType& j) +{ + using string_t = typename BasicJsonType::string_t; + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + + const auto type = static_cast(j.type()); + switch (j.type()) + { + case BasicJsonType::value_t::null: + case BasicJsonType::value_t::discarded: + { + return combine(type, 0); + } + + case BasicJsonType::value_t::object: + { + auto seed = combine(type, j.size()); + for (const auto& element : j.items()) + { + const auto h = std::hash {}(element.key()); + seed = combine(seed, h); + seed = combine(seed, hash(element.value())); + } + return seed; + } + + case BasicJsonType::value_t::array: + { + auto seed = combine(type, j.size()); + for (const auto& element : j) + { + seed = combine(seed, hash(element)); + } + return seed; + } + + case BasicJsonType::value_t::string: + { + const auto h = std::hash {}(j.template get_ref()); + return combine(type, h); + } + + case BasicJsonType::value_t::boolean: + { + const auto h = std::hash {}(j.template get()); + return combine(type, h); + } + + case BasicJsonType::value_t::number_integer: + { + const auto h = std::hash {}(j.template get()); + return combine(type, h); + } + + case BasicJsonType::value_t::number_unsigned: + { + const auto h = std::hash {}(j.template get()); + return combine(type, h); + } + + case BasicJsonType::value_t::number_float: + { + const auto h = std::hash {}(j.template get()); + return combine(type, h); + } + + case BasicJsonType::value_t::binary: + { + auto seed = combine(type, j.get_binary().size()); + const auto h = std::hash {}(j.get_binary().has_subtype()); + seed = combine(seed, h); + seed = combine(seed, static_cast(j.get_binary().subtype())); + for (const auto byte : j.get_binary()) + { + seed = combine(seed, std::hash {}(byte)); + } + return seed; + } + + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + return 0; // LCOV_EXCL_LINE + } +} + +} // namespace detail +} // namespace nlohmann + +// #include + + +#include // generate_n +#include // array +#include // ldexp +#include // size_t +#include // uint8_t, uint16_t, uint32_t, uint64_t +#include // snprintf +#include // memcpy +#include // back_inserter +#include // numeric_limits +#include // char_traits, string +#include // make_pair, move +#include // vector + +// #include + +// #include + + +#include // array +#include // size_t +#include // strlen +#include // begin, end, iterator_traits, random_access_iterator_tag, distance, next +#include // shared_ptr, make_shared, addressof +#include // accumulate +#include // string, char_traits +#include // enable_if, is_base_of, is_pointer, is_integral, remove_pointer +#include // pair, declval + +#ifndef JSON_NO_IO + #include // FILE * + #include // istream +#endif // JSON_NO_IO + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +/// the supported input formats +enum class input_format_t { json, cbor, msgpack, ubjson, bson }; + +//////////////////// +// input adapters // +//////////////////// + +#ifndef JSON_NO_IO +/*! +Input adapter for stdio file access. This adapter read only 1 byte and do not use any + buffer. This adapter is a very low level adapter. +*/ +class file_input_adapter +{ + public: + using char_type = char; + + JSON_HEDLEY_NON_NULL(2) + explicit file_input_adapter(std::FILE* f) noexcept + : m_file(f) + {} + + // make class move-only + file_input_adapter(const file_input_adapter&) = delete; + file_input_adapter(file_input_adapter&&) noexcept = default; + file_input_adapter& operator=(const file_input_adapter&) = delete; + file_input_adapter& operator=(file_input_adapter&&) = delete; + ~file_input_adapter() = default; + + std::char_traits::int_type get_character() noexcept + { + return std::fgetc(m_file); + } + + private: + /// the file pointer to read from + std::FILE* m_file; +}; + + +/*! +Input adapter for a (caching) istream. Ignores a UFT Byte Order Mark at +beginning of input. Does not support changing the underlying std::streambuf +in mid-input. Maintains underlying std::istream and std::streambuf to support +subsequent use of standard std::istream operations to process any input +characters following those used in parsing the JSON input. Clears the +std::istream flags; any input errors (e.g., EOF) will be detected by the first +subsequent call for input from the std::istream. +*/ +class input_stream_adapter +{ + public: + using char_type = char; + + ~input_stream_adapter() + { + // clear stream flags; we use underlying streambuf I/O, do not + // maintain ifstream flags, except eof + if (is != nullptr) + { + is->clear(is->rdstate() & std::ios::eofbit); + } + } + + explicit input_stream_adapter(std::istream& i) + : is(&i), sb(i.rdbuf()) + {} + + // delete because of pointer members + input_stream_adapter(const input_stream_adapter&) = delete; + input_stream_adapter& operator=(input_stream_adapter&) = delete; + input_stream_adapter& operator=(input_stream_adapter&&) = delete; + + input_stream_adapter(input_stream_adapter&& rhs) noexcept + : is(rhs.is), sb(rhs.sb) + { + rhs.is = nullptr; + rhs.sb = nullptr; + } + + // std::istream/std::streambuf use std::char_traits::to_int_type, to + // ensure that std::char_traits::eof() and the character 0xFF do not + // end up as the same value, eg. 0xFFFFFFFF. + std::char_traits::int_type get_character() + { + auto res = sb->sbumpc(); + // set eof manually, as we don't use the istream interface. + if (JSON_HEDLEY_UNLIKELY(res == std::char_traits::eof())) + { + is->clear(is->rdstate() | std::ios::eofbit); + } + return res; + } + + private: + /// the associated input stream + std::istream* is = nullptr; + std::streambuf* sb = nullptr; +}; +#endif // JSON_NO_IO + +// General-purpose iterator-based adapter. It might not be as fast as +// theoretically possible for some containers, but it is extremely versatile. +template +class iterator_input_adapter +{ + public: + using char_type = typename std::iterator_traits::value_type; + + iterator_input_adapter(IteratorType first, IteratorType last) + : current(std::move(first)), end(std::move(last)) + {} + + typename std::char_traits::int_type get_character() + { + if (JSON_HEDLEY_LIKELY(current != end)) + { + auto result = std::char_traits::to_int_type(*current); + std::advance(current, 1); + return result; + } + + return std::char_traits::eof(); + } + + private: + IteratorType current; + IteratorType end; + + template + friend struct wide_string_input_helper; + + bool empty() const + { + return current == end; + } +}; + + +template +struct wide_string_input_helper; + +template +struct wide_string_input_helper +{ + // UTF-32 + static void fill_buffer(BaseInputAdapter& input, + std::array::int_type, 4>& utf8_bytes, + size_t& utf8_bytes_index, + size_t& utf8_bytes_filled) + { + utf8_bytes_index = 0; + + if (JSON_HEDLEY_UNLIKELY(input.empty())) + { + utf8_bytes[0] = std::char_traits::eof(); + utf8_bytes_filled = 1; + } + else + { + // get the current character + const auto wc = input.get_character(); + + // UTF-32 to UTF-8 encoding + if (wc < 0x80) + { + utf8_bytes[0] = static_cast::int_type>(wc); + utf8_bytes_filled = 1; + } + else if (wc <= 0x7FF) + { + utf8_bytes[0] = static_cast::int_type>(0xC0u | ((static_cast(wc) >> 6u) & 0x1Fu)); + utf8_bytes[1] = static_cast::int_type>(0x80u | (static_cast(wc) & 0x3Fu)); + utf8_bytes_filled = 2; + } + else if (wc <= 0xFFFF) + { + utf8_bytes[0] = static_cast::int_type>(0xE0u | ((static_cast(wc) >> 12u) & 0x0Fu)); + utf8_bytes[1] = static_cast::int_type>(0x80u | ((static_cast(wc) >> 6u) & 0x3Fu)); + utf8_bytes[2] = static_cast::int_type>(0x80u | (static_cast(wc) & 0x3Fu)); + utf8_bytes_filled = 3; + } + else if (wc <= 0x10FFFF) + { + utf8_bytes[0] = static_cast::int_type>(0xF0u | ((static_cast(wc) >> 18u) & 0x07u)); + utf8_bytes[1] = static_cast::int_type>(0x80u | ((static_cast(wc) >> 12u) & 0x3Fu)); + utf8_bytes[2] = static_cast::int_type>(0x80u | ((static_cast(wc) >> 6u) & 0x3Fu)); + utf8_bytes[3] = static_cast::int_type>(0x80u | (static_cast(wc) & 0x3Fu)); + utf8_bytes_filled = 4; + } + else + { + // unknown character + utf8_bytes[0] = static_cast::int_type>(wc); + utf8_bytes_filled = 1; + } + } + } +}; + +template +struct wide_string_input_helper +{ + // UTF-16 + static void fill_buffer(BaseInputAdapter& input, + std::array::int_type, 4>& utf8_bytes, + size_t& utf8_bytes_index, + size_t& utf8_bytes_filled) + { + utf8_bytes_index = 0; + + if (JSON_HEDLEY_UNLIKELY(input.empty())) + { + utf8_bytes[0] = std::char_traits::eof(); + utf8_bytes_filled = 1; + } + else + { + // get the current character + const auto wc = input.get_character(); + + // UTF-16 to UTF-8 encoding + if (wc < 0x80) + { + utf8_bytes[0] = static_cast::int_type>(wc); + utf8_bytes_filled = 1; + } + else if (wc <= 0x7FF) + { + utf8_bytes[0] = static_cast::int_type>(0xC0u | ((static_cast(wc) >> 6u))); + utf8_bytes[1] = static_cast::int_type>(0x80u | (static_cast(wc) & 0x3Fu)); + utf8_bytes_filled = 2; + } + else if (0xD800 > wc || wc >= 0xE000) + { + utf8_bytes[0] = static_cast::int_type>(0xE0u | ((static_cast(wc) >> 12u))); + utf8_bytes[1] = static_cast::int_type>(0x80u | ((static_cast(wc) >> 6u) & 0x3Fu)); + utf8_bytes[2] = static_cast::int_type>(0x80u | (static_cast(wc) & 0x3Fu)); + utf8_bytes_filled = 3; + } + else + { + if (JSON_HEDLEY_UNLIKELY(!input.empty())) + { + const auto wc2 = static_cast(input.get_character()); + const auto charcode = 0x10000u + (((static_cast(wc) & 0x3FFu) << 10u) | (wc2 & 0x3FFu)); + utf8_bytes[0] = static_cast::int_type>(0xF0u | (charcode >> 18u)); + utf8_bytes[1] = static_cast::int_type>(0x80u | ((charcode >> 12u) & 0x3Fu)); + utf8_bytes[2] = static_cast::int_type>(0x80u | ((charcode >> 6u) & 0x3Fu)); + utf8_bytes[3] = static_cast::int_type>(0x80u | (charcode & 0x3Fu)); + utf8_bytes_filled = 4; + } + else + { + utf8_bytes[0] = static_cast::int_type>(wc); + utf8_bytes_filled = 1; + } + } + } + } +}; + +// Wraps another input apdater to convert wide character types into individual bytes. +template +class wide_string_input_adapter +{ + public: + using char_type = char; + + wide_string_input_adapter(BaseInputAdapter base) + : base_adapter(base) {} + + typename std::char_traits::int_type get_character() noexcept + { + // check if buffer needs to be filled + if (utf8_bytes_index == utf8_bytes_filled) + { + fill_buffer(); + + JSON_ASSERT(utf8_bytes_filled > 0); + JSON_ASSERT(utf8_bytes_index == 0); + } + + // use buffer + JSON_ASSERT(utf8_bytes_filled > 0); + JSON_ASSERT(utf8_bytes_index < utf8_bytes_filled); + return utf8_bytes[utf8_bytes_index++]; + } + + private: + BaseInputAdapter base_adapter; + + template + void fill_buffer() + { + wide_string_input_helper::fill_buffer(base_adapter, utf8_bytes, utf8_bytes_index, utf8_bytes_filled); + } + + /// a buffer for UTF-8 bytes + std::array::int_type, 4> utf8_bytes = {{0, 0, 0, 0}}; + + /// index to the utf8_codes array for the next valid byte + std::size_t utf8_bytes_index = 0; + /// number of valid bytes in the utf8_codes array + std::size_t utf8_bytes_filled = 0; +}; + + +template +struct iterator_input_adapter_factory +{ + using iterator_type = IteratorType; + using char_type = typename std::iterator_traits::value_type; + using adapter_type = iterator_input_adapter; + + static adapter_type create(IteratorType first, IteratorType last) + { + return adapter_type(std::move(first), std::move(last)); + } +}; + +template +struct is_iterator_of_multibyte +{ + using value_type = typename std::iterator_traits::value_type; + enum + { + value = sizeof(value_type) > 1 + }; +}; + +template +struct iterator_input_adapter_factory::value>> +{ + using iterator_type = IteratorType; + using char_type = typename std::iterator_traits::value_type; + using base_adapter_type = iterator_input_adapter; + using adapter_type = wide_string_input_adapter; + + static adapter_type create(IteratorType first, IteratorType last) + { + return adapter_type(base_adapter_type(std::move(first), std::move(last))); + } +}; + +// General purpose iterator-based input +template +typename iterator_input_adapter_factory::adapter_type input_adapter(IteratorType first, IteratorType last) +{ + using factory_type = iterator_input_adapter_factory; + return factory_type::create(first, last); +} + +// Convenience shorthand from container to iterator +// Enables ADL on begin(container) and end(container) +// Encloses the using declarations in namespace for not to leak them to outside scope + +namespace container_input_adapter_factory_impl +{ + +using std::begin; +using std::end; + +template +struct container_input_adapter_factory {}; + +template +struct container_input_adapter_factory< ContainerType, + void_t()), end(std::declval()))>> + { + using adapter_type = decltype(input_adapter(begin(std::declval()), end(std::declval()))); + + static adapter_type create(const ContainerType& container) +{ + return input_adapter(begin(container), end(container)); +} + }; + +} // namespace container_input_adapter_factory_impl + +template +typename container_input_adapter_factory_impl::container_input_adapter_factory::adapter_type input_adapter(const ContainerType& container) +{ + return container_input_adapter_factory_impl::container_input_adapter_factory::create(container); +} + +#ifndef JSON_NO_IO +// Special cases with fast paths +inline file_input_adapter input_adapter(std::FILE* file) +{ + return file_input_adapter(file); +} + +inline input_stream_adapter input_adapter(std::istream& stream) +{ + return input_stream_adapter(stream); +} + +inline input_stream_adapter input_adapter(std::istream&& stream) +{ + return input_stream_adapter(stream); +} +#endif // JSON_NO_IO + +using contiguous_bytes_input_adapter = decltype(input_adapter(std::declval(), std::declval())); + +// Null-delimited strings, and the like. +template < typename CharT, + typename std::enable_if < + std::is_pointer::value&& + !std::is_array::value&& + std::is_integral::type>::value&& + sizeof(typename std::remove_pointer::type) == 1, + int >::type = 0 > +contiguous_bytes_input_adapter input_adapter(CharT b) +{ + auto length = std::strlen(reinterpret_cast(b)); + const auto* ptr = reinterpret_cast(b); + return input_adapter(ptr, ptr + length); +} + +template +auto input_adapter(T (&array)[N]) -> decltype(input_adapter(array, array + N)) // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) +{ + return input_adapter(array, array + N); +} + +// This class only handles inputs of input_buffer_adapter type. +// It's required so that expressions like {ptr, len} can be implicitely casted +// to the correct adapter. +class span_input_adapter +{ + public: + template < typename CharT, + typename std::enable_if < + std::is_pointer::value&& + std::is_integral::type>::value&& + sizeof(typename std::remove_pointer::type) == 1, + int >::type = 0 > + span_input_adapter(CharT b, std::size_t l) + : ia(reinterpret_cast(b), reinterpret_cast(b) + l) {} + + template::iterator_category, std::random_access_iterator_tag>::value, + int>::type = 0> + span_input_adapter(IteratorType first, IteratorType last) + : ia(input_adapter(first, last)) {} + + contiguous_bytes_input_adapter&& get() + { + return std::move(ia); // NOLINT(hicpp-move-const-arg,performance-move-const-arg) + } + + private: + contiguous_bytes_input_adapter ia; +}; +} // namespace detail +} // namespace nlohmann + +// #include + + +#include +#include // string +#include // move +#include // vector + +// #include + +// #include + + +namespace nlohmann +{ + +/*! +@brief SAX interface + +This class describes the SAX interface used by @ref nlohmann::json::sax_parse. +Each function is called in different situations while the input is parsed. The +boolean return value informs the parser whether to continue processing the +input. +*/ +template +struct json_sax +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + + /*! + @brief a null value was read + @return whether parsing should proceed + */ + virtual bool null() = 0; + + /*! + @brief a boolean value was read + @param[in] val boolean value + @return whether parsing should proceed + */ + virtual bool boolean(bool val) = 0; + + /*! + @brief an integer number was read + @param[in] val integer value + @return whether parsing should proceed + */ + virtual bool number_integer(number_integer_t val) = 0; + + /*! + @brief an unsigned integer number was read + @param[in] val unsigned integer value + @return whether parsing should proceed + */ + virtual bool number_unsigned(number_unsigned_t val) = 0; + + /*! + @brief an floating-point number was read + @param[in] val floating-point value + @param[in] s raw token value + @return whether parsing should proceed + */ + virtual bool number_float(number_float_t val, const string_t& s) = 0; + + /*! + @brief a string was read + @param[in] val string value + @return whether parsing should proceed + @note It is safe to move the passed string. + */ + virtual bool string(string_t& val) = 0; + + /*! + @brief a binary string was read + @param[in] val binary value + @return whether parsing should proceed + @note It is safe to move the passed binary. + */ + virtual bool binary(binary_t& val) = 0; + + /*! + @brief the beginning of an object was read + @param[in] elements number of object elements or -1 if unknown + @return whether parsing should proceed + @note binary formats may report the number of elements + */ + virtual bool start_object(std::size_t elements) = 0; + + /*! + @brief an object key was read + @param[in] val object key + @return whether parsing should proceed + @note It is safe to move the passed string. + */ + virtual bool key(string_t& val) = 0; + + /*! + @brief the end of an object was read + @return whether parsing should proceed + */ + virtual bool end_object() = 0; + + /*! + @brief the beginning of an array was read + @param[in] elements number of array elements or -1 if unknown + @return whether parsing should proceed + @note binary formats may report the number of elements + */ + virtual bool start_array(std::size_t elements) = 0; + + /*! + @brief the end of an array was read + @return whether parsing should proceed + */ + virtual bool end_array() = 0; + + /*! + @brief a parse error occurred + @param[in] position the position in the input where the error occurs + @param[in] last_token the last read token + @param[in] ex an exception object describing the error + @return whether parsing should proceed (must return false) + */ + virtual bool parse_error(std::size_t position, + const std::string& last_token, + const detail::exception& ex) = 0; + + json_sax() = default; + json_sax(const json_sax&) = default; + json_sax(json_sax&&) noexcept = default; + json_sax& operator=(const json_sax&) = default; + json_sax& operator=(json_sax&&) noexcept = default; + virtual ~json_sax() = default; +}; + + +namespace detail +{ +/*! +@brief SAX implementation to create a JSON value from SAX events + +This class implements the @ref json_sax interface and processes the SAX events +to create a JSON value which makes it basically a DOM parser. The structure or +hierarchy of the JSON value is managed by the stack `ref_stack` which contains +a pointer to the respective array or object for each recursion depth. + +After successful parsing, the value that is passed by reference to the +constructor contains the parsed value. + +@tparam BasicJsonType the JSON type +*/ +template +class json_sax_dom_parser +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + + /*! + @param[in,out] r reference to a JSON value that is manipulated while + parsing + @param[in] allow_exceptions_ whether parse errors yield exceptions + */ + explicit json_sax_dom_parser(BasicJsonType& r, const bool allow_exceptions_ = true) + : root(r), allow_exceptions(allow_exceptions_) + {} + + // make class move-only + json_sax_dom_parser(const json_sax_dom_parser&) = delete; + json_sax_dom_parser(json_sax_dom_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + json_sax_dom_parser& operator=(const json_sax_dom_parser&) = delete; + json_sax_dom_parser& operator=(json_sax_dom_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + ~json_sax_dom_parser() = default; + + bool null() + { + handle_value(nullptr); + return true; + } + + bool boolean(bool val) + { + handle_value(val); + return true; + } + + bool number_integer(number_integer_t val) + { + handle_value(val); + return true; + } + + bool number_unsigned(number_unsigned_t val) + { + handle_value(val); + return true; + } + + bool number_float(number_float_t val, const string_t& /*unused*/) + { + handle_value(val); + return true; + } + + bool string(string_t& val) + { + handle_value(val); + return true; + } + + bool binary(binary_t& val) + { + handle_value(std::move(val)); + return true; + } + + bool start_object(std::size_t len) + { + ref_stack.push_back(handle_value(BasicJsonType::value_t::object)); + + if (JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) && len > ref_stack.back()->max_size())) + { + JSON_THROW(out_of_range::create(408, "excessive object size: " + std::to_string(len), *ref_stack.back())); + } + + return true; + } + + bool key(string_t& val) + { + // add null at given key and store the reference for later + object_element = &(ref_stack.back()->m_value.object->operator[](val)); + return true; + } + + bool end_object() + { + ref_stack.back()->set_parents(); + ref_stack.pop_back(); + return true; + } + + bool start_array(std::size_t len) + { + ref_stack.push_back(handle_value(BasicJsonType::value_t::array)); + + if (JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) && len > ref_stack.back()->max_size())) + { + JSON_THROW(out_of_range::create(408, "excessive array size: " + std::to_string(len), *ref_stack.back())); + } + + return true; + } + + bool end_array() + { + ref_stack.back()->set_parents(); + ref_stack.pop_back(); + return true; + } + + template + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, + const Exception& ex) + { + errored = true; + static_cast(ex); + if (allow_exceptions) + { + JSON_THROW(ex); + } + return false; + } + + constexpr bool is_errored() const + { + return errored; + } + + private: + /*! + @invariant If the ref stack is empty, then the passed value will be the new + root. + @invariant If the ref stack contains a value, then it is an array or an + object to which we can add elements + */ + template + JSON_HEDLEY_RETURNS_NON_NULL + BasicJsonType* handle_value(Value&& v) + { + if (ref_stack.empty()) + { + root = BasicJsonType(std::forward(v)); + return &root; + } + + JSON_ASSERT(ref_stack.back()->is_array() || ref_stack.back()->is_object()); + + if (ref_stack.back()->is_array()) + { + ref_stack.back()->m_value.array->emplace_back(std::forward(v)); + return &(ref_stack.back()->m_value.array->back()); + } + + JSON_ASSERT(ref_stack.back()->is_object()); + JSON_ASSERT(object_element); + *object_element = BasicJsonType(std::forward(v)); + return object_element; + } + + /// the parsed JSON value + BasicJsonType& root; + /// stack to model hierarchy of values + std::vector ref_stack {}; + /// helper to hold the reference for the next object element + BasicJsonType* object_element = nullptr; + /// whether a syntax error occurred + bool errored = false; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; +}; + +template +class json_sax_dom_callback_parser +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using parser_callback_t = typename BasicJsonType::parser_callback_t; + using parse_event_t = typename BasicJsonType::parse_event_t; + + json_sax_dom_callback_parser(BasicJsonType& r, + const parser_callback_t cb, + const bool allow_exceptions_ = true) + : root(r), callback(cb), allow_exceptions(allow_exceptions_) + { + keep_stack.push_back(true); + } + + // make class move-only + json_sax_dom_callback_parser(const json_sax_dom_callback_parser&) = delete; + json_sax_dom_callback_parser(json_sax_dom_callback_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + json_sax_dom_callback_parser& operator=(const json_sax_dom_callback_parser&) = delete; + json_sax_dom_callback_parser& operator=(json_sax_dom_callback_parser&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + ~json_sax_dom_callback_parser() = default; + + bool null() + { + handle_value(nullptr); + return true; + } + + bool boolean(bool val) + { + handle_value(val); + return true; + } + + bool number_integer(number_integer_t val) + { + handle_value(val); + return true; + } + + bool number_unsigned(number_unsigned_t val) + { + handle_value(val); + return true; + } + + bool number_float(number_float_t val, const string_t& /*unused*/) + { + handle_value(val); + return true; + } + + bool string(string_t& val) + { + handle_value(val); + return true; + } + + bool binary(binary_t& val) + { + handle_value(std::move(val)); + return true; + } + + bool start_object(std::size_t len) + { + // check callback for object start + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::object_start, discarded); + keep_stack.push_back(keep); + + auto val = handle_value(BasicJsonType::value_t::object, true); + ref_stack.push_back(val.second); + + // check object limit + if (ref_stack.back() && JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) && len > ref_stack.back()->max_size())) + { + JSON_THROW(out_of_range::create(408, "excessive object size: " + std::to_string(len), *ref_stack.back())); + } + + return true; + } + + bool key(string_t& val) + { + BasicJsonType k = BasicJsonType(val); + + // check callback for key + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::key, k); + key_keep_stack.push_back(keep); + + // add discarded value at given key and store the reference for later + if (keep && ref_stack.back()) + { + object_element = &(ref_stack.back()->m_value.object->operator[](val) = discarded); + } + + return true; + } + + bool end_object() + { + if (ref_stack.back()) + { + if (!callback(static_cast(ref_stack.size()) - 1, parse_event_t::object_end, *ref_stack.back())) + { + // discard object + *ref_stack.back() = discarded; + } + else + { + ref_stack.back()->set_parents(); + } + } + + JSON_ASSERT(!ref_stack.empty()); + JSON_ASSERT(!keep_stack.empty()); + ref_stack.pop_back(); + keep_stack.pop_back(); + + if (!ref_stack.empty() && ref_stack.back() && ref_stack.back()->is_structured()) + { + // remove discarded value + for (auto it = ref_stack.back()->begin(); it != ref_stack.back()->end(); ++it) + { + if (it->is_discarded()) + { + ref_stack.back()->erase(it); + break; + } + } + } + + return true; + } + + bool start_array(std::size_t len) + { + const bool keep = callback(static_cast(ref_stack.size()), parse_event_t::array_start, discarded); + keep_stack.push_back(keep); + + auto val = handle_value(BasicJsonType::value_t::array, true); + ref_stack.push_back(val.second); + + // check array limit + if (ref_stack.back() && JSON_HEDLEY_UNLIKELY(len != std::size_t(-1) && len > ref_stack.back()->max_size())) + { + JSON_THROW(out_of_range::create(408, "excessive array size: " + std::to_string(len), *ref_stack.back())); + } + + return true; + } + + bool end_array() + { + bool keep = true; + + if (ref_stack.back()) + { + keep = callback(static_cast(ref_stack.size()) - 1, parse_event_t::array_end, *ref_stack.back()); + if (keep) + { + ref_stack.back()->set_parents(); + } + else + { + // discard array + *ref_stack.back() = discarded; + } + } + + JSON_ASSERT(!ref_stack.empty()); + JSON_ASSERT(!keep_stack.empty()); + ref_stack.pop_back(); + keep_stack.pop_back(); + + // remove discarded value + if (!keep && !ref_stack.empty() && ref_stack.back()->is_array()) + { + ref_stack.back()->m_value.array->pop_back(); + } + + return true; + } + + template + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, + const Exception& ex) + { + errored = true; + static_cast(ex); + if (allow_exceptions) + { + JSON_THROW(ex); + } + return false; + } + + constexpr bool is_errored() const + { + return errored; + } + + private: + /*! + @param[in] v value to add to the JSON value we build during parsing + @param[in] skip_callback whether we should skip calling the callback + function; this is required after start_array() and + start_object() SAX events, because otherwise we would call the + callback function with an empty array or object, respectively. + + @invariant If the ref stack is empty, then the passed value will be the new + root. + @invariant If the ref stack contains a value, then it is an array or an + object to which we can add elements + + @return pair of boolean (whether value should be kept) and pointer (to the + passed value in the ref_stack hierarchy; nullptr if not kept) + */ + template + std::pair handle_value(Value&& v, const bool skip_callback = false) + { + JSON_ASSERT(!keep_stack.empty()); + + // do not handle this value if we know it would be added to a discarded + // container + if (!keep_stack.back()) + { + return {false, nullptr}; + } + + // create value + auto value = BasicJsonType(std::forward(v)); + + // check callback + const bool keep = skip_callback || callback(static_cast(ref_stack.size()), parse_event_t::value, value); + + // do not handle this value if we just learnt it shall be discarded + if (!keep) + { + return {false, nullptr}; + } + + if (ref_stack.empty()) + { + root = std::move(value); + return {true, &root}; + } + + // skip this value if we already decided to skip the parent + // (https://github.com/nlohmann/json/issues/971#issuecomment-413678360) + if (!ref_stack.back()) + { + return {false, nullptr}; + } + + // we now only expect arrays and objects + JSON_ASSERT(ref_stack.back()->is_array() || ref_stack.back()->is_object()); + + // array + if (ref_stack.back()->is_array()) + { + ref_stack.back()->m_value.array->emplace_back(std::move(value)); + return {true, &(ref_stack.back()->m_value.array->back())}; + } + + // object + JSON_ASSERT(ref_stack.back()->is_object()); + // check if we should store an element for the current key + JSON_ASSERT(!key_keep_stack.empty()); + const bool store_element = key_keep_stack.back(); + key_keep_stack.pop_back(); + + if (!store_element) + { + return {false, nullptr}; + } + + JSON_ASSERT(object_element); + *object_element = std::move(value); + return {true, object_element}; + } + + /// the parsed JSON value + BasicJsonType& root; + /// stack to model hierarchy of values + std::vector ref_stack {}; + /// stack to manage which values to keep + std::vector keep_stack {}; + /// stack to manage which object keys to keep + std::vector key_keep_stack {}; + /// helper to hold the reference for the next object element + BasicJsonType* object_element = nullptr; + /// whether a syntax error occurred + bool errored = false; + /// callback function + const parser_callback_t callback = nullptr; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; + /// a discarded value for the callback + BasicJsonType discarded = BasicJsonType::value_t::discarded; +}; + +template +class json_sax_acceptor +{ + public: + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + + bool null() + { + return true; + } + + bool boolean(bool /*unused*/) + { + return true; + } + + bool number_integer(number_integer_t /*unused*/) + { + return true; + } + + bool number_unsigned(number_unsigned_t /*unused*/) + { + return true; + } + + bool number_float(number_float_t /*unused*/, const string_t& /*unused*/) + { + return true; + } + + bool string(string_t& /*unused*/) + { + return true; + } + + bool binary(binary_t& /*unused*/) + { + return true; + } + + bool start_object(std::size_t /*unused*/ = std::size_t(-1)) + { + return true; + } + + bool key(string_t& /*unused*/) + { + return true; + } + + bool end_object() + { + return true; + } + + bool start_array(std::size_t /*unused*/ = std::size_t(-1)) + { + return true; + } + + bool end_array() + { + return true; + } + + bool parse_error(std::size_t /*unused*/, const std::string& /*unused*/, const detail::exception& /*unused*/) + { + return false; + } +}; +} // namespace detail + +} // namespace nlohmann + +// #include + + +#include // array +#include // localeconv +#include // size_t +#include // snprintf +#include // strtof, strtod, strtold, strtoll, strtoull +#include // initializer_list +#include // char_traits, string +#include // move +#include // vector + +// #include + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +/////////// +// lexer // +/////////// + +template +class lexer_base +{ + public: + /// token types for the parser + enum class token_type + { + uninitialized, ///< indicating the scanner is uninitialized + literal_true, ///< the `true` literal + literal_false, ///< the `false` literal + literal_null, ///< the `null` literal + value_string, ///< a string -- use get_string() for actual value + value_unsigned, ///< an unsigned integer -- use get_number_unsigned() for actual value + value_integer, ///< a signed integer -- use get_number_integer() for actual value + value_float, ///< an floating point number -- use get_number_float() for actual value + begin_array, ///< the character for array begin `[` + begin_object, ///< the character for object begin `{` + end_array, ///< the character for array end `]` + end_object, ///< the character for object end `}` + name_separator, ///< the name separator `:` + value_separator, ///< the value separator `,` + parse_error, ///< indicating a parse error + end_of_input, ///< indicating the end of the input buffer + literal_or_value ///< a literal or the begin of a value (only for diagnostics) + }; + + /// return name of values of type token_type (only used for errors) + JSON_HEDLEY_RETURNS_NON_NULL + JSON_HEDLEY_CONST + static const char* token_type_name(const token_type t) noexcept + { + switch (t) + { + case token_type::uninitialized: + return ""; + case token_type::literal_true: + return "true literal"; + case token_type::literal_false: + return "false literal"; + case token_type::literal_null: + return "null literal"; + case token_type::value_string: + return "string literal"; + case token_type::value_unsigned: + case token_type::value_integer: + case token_type::value_float: + return "number literal"; + case token_type::begin_array: + return "'['"; + case token_type::begin_object: + return "'{'"; + case token_type::end_array: + return "']'"; + case token_type::end_object: + return "'}'"; + case token_type::name_separator: + return "':'"; + case token_type::value_separator: + return "','"; + case token_type::parse_error: + return ""; + case token_type::end_of_input: + return "end of input"; + case token_type::literal_or_value: + return "'[', '{', or a literal"; + // LCOV_EXCL_START + default: // catch non-enum values + return "unknown token"; + // LCOV_EXCL_STOP + } + } +}; +/*! +@brief lexical analysis + +This class organizes the lexical analysis during JSON deserialization. +*/ +template +class lexer : public lexer_base +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using char_type = typename InputAdapterType::char_type; + using char_int_type = typename std::char_traits::int_type; + + public: + using token_type = typename lexer_base::token_type; + + explicit lexer(InputAdapterType&& adapter, bool ignore_comments_ = false) noexcept + : ia(std::move(adapter)) + , ignore_comments(ignore_comments_) + , decimal_point_char(static_cast(get_decimal_point())) + {} + + // delete because of pointer members + lexer(const lexer&) = delete; + lexer(lexer&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + lexer& operator=(lexer&) = delete; + lexer& operator=(lexer&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + ~lexer() = default; + + private: + ///////////////////// + // locales + ///////////////////// + + /// return the locale-dependent decimal point + JSON_HEDLEY_PURE + static char get_decimal_point() noexcept + { + const auto* loc = localeconv(); + JSON_ASSERT(loc != nullptr); + return (loc->decimal_point == nullptr) ? '.' : *(loc->decimal_point); + } + + ///////////////////// + // scan functions + ///////////////////// + + /*! + @brief get codepoint from 4 hex characters following `\u` + + For input "\u c1 c2 c3 c4" the codepoint is: + (c1 * 0x1000) + (c2 * 0x0100) + (c3 * 0x0010) + c4 + = (c1 << 12) + (c2 << 8) + (c3 << 4) + (c4 << 0) + + Furthermore, the possible characters '0'..'9', 'A'..'F', and 'a'..'f' + must be converted to the integers 0x0..0x9, 0xA..0xF, 0xA..0xF, resp. The + conversion is done by subtracting the offset (0x30, 0x37, and 0x57) + between the ASCII value of the character and the desired integer value. + + @return codepoint (0x0000..0xFFFF) or -1 in case of an error (e.g. EOF or + non-hex character) + */ + int get_codepoint() + { + // this function only makes sense after reading `\u` + JSON_ASSERT(current == 'u'); + int codepoint = 0; + + const auto factors = { 12u, 8u, 4u, 0u }; + for (const auto factor : factors) + { + get(); + + if (current >= '0' && current <= '9') + { + codepoint += static_cast((static_cast(current) - 0x30u) << factor); + } + else if (current >= 'A' && current <= 'F') + { + codepoint += static_cast((static_cast(current) - 0x37u) << factor); + } + else if (current >= 'a' && current <= 'f') + { + codepoint += static_cast((static_cast(current) - 0x57u) << factor); + } + else + { + return -1; + } + } + + JSON_ASSERT(0x0000 <= codepoint && codepoint <= 0xFFFF); + return codepoint; + } + + /*! + @brief check if the next byte(s) are inside a given range + + Adds the current byte and, for each passed range, reads a new byte and + checks if it is inside the range. If a violation was detected, set up an + error message and return false. Otherwise, return true. + + @param[in] ranges list of integers; interpreted as list of pairs of + inclusive lower and upper bound, respectively + + @pre The passed list @a ranges must have 2, 4, or 6 elements; that is, + 1, 2, or 3 pairs. This precondition is enforced by an assertion. + + @return true if and only if no range violation was detected + */ + bool next_byte_in_range(std::initializer_list ranges) + { + JSON_ASSERT(ranges.size() == 2 || ranges.size() == 4 || ranges.size() == 6); + add(current); + + for (auto range = ranges.begin(); range != ranges.end(); ++range) + { + get(); + if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) + { + add(current); + } + else + { + error_message = "invalid string: ill-formed UTF-8 byte"; + return false; + } + } + + return true; + } + + /*! + @brief scan a string literal + + This function scans a string according to Sect. 7 of RFC 8259. While + scanning, bytes are escaped and copied into buffer token_buffer. Then the + function returns successfully, token_buffer is *not* null-terminated (as it + may contain \0 bytes), and token_buffer.size() is the number of bytes in the + string. + + @return token_type::value_string if string could be successfully scanned, + token_type::parse_error otherwise + + @note In case of errors, variable error_message contains a textual + description. + */ + token_type scan_string() + { + // reset token_buffer (ignore opening quote) + reset(); + + // we entered the function by reading an open quote + JSON_ASSERT(current == '\"'); + + while (true) + { + // get next character + switch (get()) + { + // end of file while parsing string + case std::char_traits::eof(): + { + error_message = "invalid string: missing closing quote"; + return token_type::parse_error; + } + + // closing quote + case '\"': + { + return token_type::value_string; + } + + // escapes + case '\\': + { + switch (get()) + { + // quotation mark + case '\"': + add('\"'); + break; + // reverse solidus + case '\\': + add('\\'); + break; + // solidus + case '/': + add('/'); + break; + // backspace + case 'b': + add('\b'); + break; + // form feed + case 'f': + add('\f'); + break; + // line feed + case 'n': + add('\n'); + break; + // carriage return + case 'r': + add('\r'); + break; + // tab + case 't': + add('\t'); + break; + + // unicode escapes + case 'u': + { + const int codepoint1 = get_codepoint(); + int codepoint = codepoint1; // start with codepoint1 + + if (JSON_HEDLEY_UNLIKELY(codepoint1 == -1)) + { + error_message = "invalid string: '\\u' must be followed by 4 hex digits"; + return token_type::parse_error; + } + + // check if code point is a high surrogate + if (0xD800 <= codepoint1 && codepoint1 <= 0xDBFF) + { + // expect next \uxxxx entry + if (JSON_HEDLEY_LIKELY(get() == '\\' && get() == 'u')) + { + const int codepoint2 = get_codepoint(); + + if (JSON_HEDLEY_UNLIKELY(codepoint2 == -1)) + { + error_message = "invalid string: '\\u' must be followed by 4 hex digits"; + return token_type::parse_error; + } + + // check if codepoint2 is a low surrogate + if (JSON_HEDLEY_LIKELY(0xDC00 <= codepoint2 && codepoint2 <= 0xDFFF)) + { + // overwrite codepoint + codepoint = static_cast( + // high surrogate occupies the most significant 22 bits + (static_cast(codepoint1) << 10u) + // low surrogate occupies the least significant 15 bits + + static_cast(codepoint2) + // there is still the 0xD800, 0xDC00 and 0x10000 noise + // in the result so we have to subtract with: + // (0xD800 << 10) + DC00 - 0x10000 = 0x35FDC00 + - 0x35FDC00u); + } + else + { + error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF"; + return token_type::parse_error; + } + } + else + { + error_message = "invalid string: surrogate U+D800..U+DBFF must be followed by U+DC00..U+DFFF"; + return token_type::parse_error; + } + } + else + { + if (JSON_HEDLEY_UNLIKELY(0xDC00 <= codepoint1 && codepoint1 <= 0xDFFF)) + { + error_message = "invalid string: surrogate U+DC00..U+DFFF must follow U+D800..U+DBFF"; + return token_type::parse_error; + } + } + + // result of the above calculation yields a proper codepoint + JSON_ASSERT(0x00 <= codepoint && codepoint <= 0x10FFFF); + + // translate codepoint into bytes + if (codepoint < 0x80) + { + // 1-byte characters: 0xxxxxxx (ASCII) + add(static_cast(codepoint)); + } + else if (codepoint <= 0x7FF) + { + // 2-byte characters: 110xxxxx 10xxxxxx + add(static_cast(0xC0u | (static_cast(codepoint) >> 6u))); + add(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + else if (codepoint <= 0xFFFF) + { + // 3-byte characters: 1110xxxx 10xxxxxx 10xxxxxx + add(static_cast(0xE0u | (static_cast(codepoint) >> 12u))); + add(static_cast(0x80u | ((static_cast(codepoint) >> 6u) & 0x3Fu))); + add(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + else + { + // 4-byte characters: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + add(static_cast(0xF0u | (static_cast(codepoint) >> 18u))); + add(static_cast(0x80u | ((static_cast(codepoint) >> 12u) & 0x3Fu))); + add(static_cast(0x80u | ((static_cast(codepoint) >> 6u) & 0x3Fu))); + add(static_cast(0x80u | (static_cast(codepoint) & 0x3Fu))); + } + + break; + } + + // other characters after escape + default: + error_message = "invalid string: forbidden character after backslash"; + return token_type::parse_error; + } + + break; + } + + // invalid control characters + case 0x00: + { + error_message = "invalid string: control character U+0000 (NUL) must be escaped to \\u0000"; + return token_type::parse_error; + } + + case 0x01: + { + error_message = "invalid string: control character U+0001 (SOH) must be escaped to \\u0001"; + return token_type::parse_error; + } + + case 0x02: + { + error_message = "invalid string: control character U+0002 (STX) must be escaped to \\u0002"; + return token_type::parse_error; + } + + case 0x03: + { + error_message = "invalid string: control character U+0003 (ETX) must be escaped to \\u0003"; + return token_type::parse_error; + } + + case 0x04: + { + error_message = "invalid string: control character U+0004 (EOT) must be escaped to \\u0004"; + return token_type::parse_error; + } + + case 0x05: + { + error_message = "invalid string: control character U+0005 (ENQ) must be escaped to \\u0005"; + return token_type::parse_error; + } + + case 0x06: + { + error_message = "invalid string: control character U+0006 (ACK) must be escaped to \\u0006"; + return token_type::parse_error; + } + + case 0x07: + { + error_message = "invalid string: control character U+0007 (BEL) must be escaped to \\u0007"; + return token_type::parse_error; + } + + case 0x08: + { + error_message = "invalid string: control character U+0008 (BS) must be escaped to \\u0008 or \\b"; + return token_type::parse_error; + } + + case 0x09: + { + error_message = "invalid string: control character U+0009 (HT) must be escaped to \\u0009 or \\t"; + return token_type::parse_error; + } + + case 0x0A: + { + error_message = "invalid string: control character U+000A (LF) must be escaped to \\u000A or \\n"; + return token_type::parse_error; + } + + case 0x0B: + { + error_message = "invalid string: control character U+000B (VT) must be escaped to \\u000B"; + return token_type::parse_error; + } + + case 0x0C: + { + error_message = "invalid string: control character U+000C (FF) must be escaped to \\u000C or \\f"; + return token_type::parse_error; + } + + case 0x0D: + { + error_message = "invalid string: control character U+000D (CR) must be escaped to \\u000D or \\r"; + return token_type::parse_error; + } + + case 0x0E: + { + error_message = "invalid string: control character U+000E (SO) must be escaped to \\u000E"; + return token_type::parse_error; + } + + case 0x0F: + { + error_message = "invalid string: control character U+000F (SI) must be escaped to \\u000F"; + return token_type::parse_error; + } + + case 0x10: + { + error_message = "invalid string: control character U+0010 (DLE) must be escaped to \\u0010"; + return token_type::parse_error; + } + + case 0x11: + { + error_message = "invalid string: control character U+0011 (DC1) must be escaped to \\u0011"; + return token_type::parse_error; + } + + case 0x12: + { + error_message = "invalid string: control character U+0012 (DC2) must be escaped to \\u0012"; + return token_type::parse_error; + } + + case 0x13: + { + error_message = "invalid string: control character U+0013 (DC3) must be escaped to \\u0013"; + return token_type::parse_error; + } + + case 0x14: + { + error_message = "invalid string: control character U+0014 (DC4) must be escaped to \\u0014"; + return token_type::parse_error; + } + + case 0x15: + { + error_message = "invalid string: control character U+0015 (NAK) must be escaped to \\u0015"; + return token_type::parse_error; + } + + case 0x16: + { + error_message = "invalid string: control character U+0016 (SYN) must be escaped to \\u0016"; + return token_type::parse_error; + } + + case 0x17: + { + error_message = "invalid string: control character U+0017 (ETB) must be escaped to \\u0017"; + return token_type::parse_error; + } + + case 0x18: + { + error_message = "invalid string: control character U+0018 (CAN) must be escaped to \\u0018"; + return token_type::parse_error; + } + + case 0x19: + { + error_message = "invalid string: control character U+0019 (EM) must be escaped to \\u0019"; + return token_type::parse_error; + } + + case 0x1A: + { + error_message = "invalid string: control character U+001A (SUB) must be escaped to \\u001A"; + return token_type::parse_error; + } + + case 0x1B: + { + error_message = "invalid string: control character U+001B (ESC) must be escaped to \\u001B"; + return token_type::parse_error; + } + + case 0x1C: + { + error_message = "invalid string: control character U+001C (FS) must be escaped to \\u001C"; + return token_type::parse_error; + } + + case 0x1D: + { + error_message = "invalid string: control character U+001D (GS) must be escaped to \\u001D"; + return token_type::parse_error; + } + + case 0x1E: + { + error_message = "invalid string: control character U+001E (RS) must be escaped to \\u001E"; + return token_type::parse_error; + } + + case 0x1F: + { + error_message = "invalid string: control character U+001F (US) must be escaped to \\u001F"; + return token_type::parse_error; + } + + // U+0020..U+007F (except U+0022 (quote) and U+005C (backspace)) + case 0x20: + case 0x21: + case 0x23: + case 0x24: + case 0x25: + case 0x26: + case 0x27: + case 0x28: + case 0x29: + case 0x2A: + case 0x2B: + case 0x2C: + case 0x2D: + case 0x2E: + case 0x2F: + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + case 0x48: + case 0x49: + case 0x4A: + case 0x4B: + case 0x4C: + case 0x4D: + case 0x4E: + case 0x4F: + case 0x50: + case 0x51: + case 0x52: + case 0x53: + case 0x54: + case 0x55: + case 0x56: + case 0x57: + case 0x58: + case 0x59: + case 0x5A: + case 0x5B: + case 0x5D: + case 0x5E: + case 0x5F: + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + case 0x68: + case 0x69: + case 0x6A: + case 0x6B: + case 0x6C: + case 0x6D: + case 0x6E: + case 0x6F: + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + case 0x78: + case 0x79: + case 0x7A: + case 0x7B: + case 0x7C: + case 0x7D: + case 0x7E: + case 0x7F: + { + add(current); + break; + } + + // U+0080..U+07FF: bytes C2..DF 80..BF + case 0xC2: + case 0xC3: + case 0xC4: + case 0xC5: + case 0xC6: + case 0xC7: + case 0xC8: + case 0xC9: + case 0xCA: + case 0xCB: + case 0xCC: + case 0xCD: + case 0xCE: + case 0xCF: + case 0xD0: + case 0xD1: + case 0xD2: + case 0xD3: + case 0xD4: + case 0xD5: + case 0xD6: + case 0xD7: + case 0xD8: + case 0xD9: + case 0xDA: + case 0xDB: + case 0xDC: + case 0xDD: + case 0xDE: + case 0xDF: + { + if (JSON_HEDLEY_UNLIKELY(!next_byte_in_range({0x80, 0xBF}))) + { + return token_type::parse_error; + } + break; + } + + // U+0800..U+0FFF: bytes E0 A0..BF 80..BF + case 0xE0: + { + if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0xA0, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+1000..U+CFFF: bytes E1..EC 80..BF 80..BF + // U+E000..U+FFFF: bytes EE..EF 80..BF 80..BF + case 0xE1: + case 0xE2: + case 0xE3: + case 0xE4: + case 0xE5: + case 0xE6: + case 0xE7: + case 0xE8: + case 0xE9: + case 0xEA: + case 0xEB: + case 0xEC: + case 0xEE: + case 0xEF: + { + if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+D000..U+D7FF: bytes ED 80..9F 80..BF + case 0xED: + { + if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x9F, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+10000..U+3FFFF F0 90..BF 80..BF 80..BF + case 0xF0: + { + if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x90, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF + case 0xF1: + case 0xF2: + case 0xF3: + { + if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0xBF, 0x80, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // U+100000..U+10FFFF F4 80..8F 80..BF 80..BF + case 0xF4: + { + if (JSON_HEDLEY_UNLIKELY(!(next_byte_in_range({0x80, 0x8F, 0x80, 0xBF, 0x80, 0xBF})))) + { + return token_type::parse_error; + } + break; + } + + // remaining bytes (80..C1 and F5..FF) are ill-formed + default: + { + error_message = "invalid string: ill-formed UTF-8 byte"; + return token_type::parse_error; + } + } + } + } + + /*! + * @brief scan a comment + * @return whether comment could be scanned successfully + */ + bool scan_comment() + { + switch (get()) + { + // single-line comments skip input until a newline or EOF is read + case '/': + { + while (true) + { + switch (get()) + { + case '\n': + case '\r': + case std::char_traits::eof(): + case '\0': + return true; + + default: + break; + } + } + } + + // multi-line comments skip input until */ is read + case '*': + { + while (true) + { + switch (get()) + { + case std::char_traits::eof(): + case '\0': + { + error_message = "invalid comment; missing closing '*/'"; + return false; + } + + case '*': + { + switch (get()) + { + case '/': + return true; + + default: + { + unget(); + continue; + } + } + } + + default: + continue; + } + } + } + + // unexpected character after reading '/' + default: + { + error_message = "invalid comment; expecting '/' or '*' after '/'"; + return false; + } + } + } + + JSON_HEDLEY_NON_NULL(2) + static void strtof(float& f, const char* str, char** endptr) noexcept + { + f = std::strtof(str, endptr); + } + + JSON_HEDLEY_NON_NULL(2) + static void strtof(double& f, const char* str, char** endptr) noexcept + { + f = std::strtod(str, endptr); + } + + JSON_HEDLEY_NON_NULL(2) + static void strtof(long double& f, const char* str, char** endptr) noexcept + { + f = std::strtold(str, endptr); + } + + /*! + @brief scan a number literal + + This function scans a string according to Sect. 6 of RFC 8259. + + The function is realized with a deterministic finite state machine derived + from the grammar described in RFC 8259. Starting in state "init", the + input is read and used to determined the next state. Only state "done" + accepts the number. State "error" is a trap state to model errors. In the + table below, "anything" means any character but the ones listed before. + + state | 0 | 1-9 | e E | + | - | . | anything + ---------|----------|----------|----------|---------|---------|----------|----------- + init | zero | any1 | [error] | [error] | minus | [error] | [error] + minus | zero | any1 | [error] | [error] | [error] | [error] | [error] + zero | done | done | exponent | done | done | decimal1 | done + any1 | any1 | any1 | exponent | done | done | decimal1 | done + decimal1 | decimal2 | decimal2 | [error] | [error] | [error] | [error] | [error] + decimal2 | decimal2 | decimal2 | exponent | done | done | done | done + exponent | any2 | any2 | [error] | sign | sign | [error] | [error] + sign | any2 | any2 | [error] | [error] | [error] | [error] | [error] + any2 | any2 | any2 | done | done | done | done | done + + The state machine is realized with one label per state (prefixed with + "scan_number_") and `goto` statements between them. The state machine + contains cycles, but any cycle can be left when EOF is read. Therefore, + the function is guaranteed to terminate. + + During scanning, the read bytes are stored in token_buffer. This string is + then converted to a signed integer, an unsigned integer, or a + floating-point number. + + @return token_type::value_unsigned, token_type::value_integer, or + token_type::value_float if number could be successfully scanned, + token_type::parse_error otherwise + + @note The scanner is independent of the current locale. Internally, the + locale's decimal point is used instead of `.` to work with the + locale-dependent converters. + */ + token_type scan_number() // lgtm [cpp/use-of-goto] + { + // reset token_buffer to store the number's bytes + reset(); + + // the type of the parsed number; initially set to unsigned; will be + // changed if minus sign, decimal point or exponent is read + token_type number_type = token_type::value_unsigned; + + // state (init): we just found out we need to scan a number + switch (current) + { + case '-': + { + add(current); + goto scan_number_minus; + } + + case '0': + { + add(current); + goto scan_number_zero; + } + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any1; + } + + // all other characters are rejected outside scan_number() + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + +scan_number_minus: + // state: we just parsed a leading minus sign + number_type = token_type::value_integer; + switch (get()) + { + case '0': + { + add(current); + goto scan_number_zero; + } + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any1; + } + + default: + { + error_message = "invalid number; expected digit after '-'"; + return token_type::parse_error; + } + } + +scan_number_zero: + // state: we just parse a zero (maybe with a leading minus sign) + switch (get()) + { + case '.': + { + add(decimal_point_char); + goto scan_number_decimal1; + } + + case 'e': + case 'E': + { + add(current); + goto scan_number_exponent; + } + + default: + goto scan_number_done; + } + +scan_number_any1: + // state: we just parsed a number 0-9 (maybe with a leading minus sign) + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any1; + } + + case '.': + { + add(decimal_point_char); + goto scan_number_decimal1; + } + + case 'e': + case 'E': + { + add(current); + goto scan_number_exponent; + } + + default: + goto scan_number_done; + } + +scan_number_decimal1: + // state: we just parsed a decimal point + number_type = token_type::value_float; + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_decimal2; + } + + default: + { + error_message = "invalid number; expected digit after '.'"; + return token_type::parse_error; + } + } + +scan_number_decimal2: + // we just parsed at least one number after a decimal point + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_decimal2; + } + + case 'e': + case 'E': + { + add(current); + goto scan_number_exponent; + } + + default: + goto scan_number_done; + } + +scan_number_exponent: + // we just parsed an exponent + number_type = token_type::value_float; + switch (get()) + { + case '+': + case '-': + { + add(current); + goto scan_number_sign; + } + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any2; + } + + default: + { + error_message = + "invalid number; expected '+', '-', or digit after exponent"; + return token_type::parse_error; + } + } + +scan_number_sign: + // we just parsed an exponent sign + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any2; + } + + default: + { + error_message = "invalid number; expected digit after exponent sign"; + return token_type::parse_error; + } + } + +scan_number_any2: + // we just parsed a number after the exponent or exponent sign + switch (get()) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + { + add(current); + goto scan_number_any2; + } + + default: + goto scan_number_done; + } + +scan_number_done: + // unget the character after the number (we only read it to know that + // we are done scanning a number) + unget(); + + char* endptr = nullptr; // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + errno = 0; + + // try to parse integers first and fall back to floats + if (number_type == token_type::value_unsigned) + { + const auto x = std::strtoull(token_buffer.data(), &endptr, 10); + + // we checked the number format before + JSON_ASSERT(endptr == token_buffer.data() + token_buffer.size()); + + if (errno == 0) + { + value_unsigned = static_cast(x); + if (value_unsigned == x) + { + return token_type::value_unsigned; + } + } + } + else if (number_type == token_type::value_integer) + { + const auto x = std::strtoll(token_buffer.data(), &endptr, 10); + + // we checked the number format before + JSON_ASSERT(endptr == token_buffer.data() + token_buffer.size()); + + if (errno == 0) + { + value_integer = static_cast(x); + if (value_integer == x) + { + return token_type::value_integer; + } + } + } + + // this code is reached if we parse a floating-point number or if an + // integer conversion above failed + strtof(value_float, token_buffer.data(), &endptr); + + // we checked the number format before + JSON_ASSERT(endptr == token_buffer.data() + token_buffer.size()); + + return token_type::value_float; + } + + /*! + @param[in] literal_text the literal text to expect + @param[in] length the length of the passed literal text + @param[in] return_type the token type to return on success + */ + JSON_HEDLEY_NON_NULL(2) + token_type scan_literal(const char_type* literal_text, const std::size_t length, + token_type return_type) + { + JSON_ASSERT(std::char_traits::to_char_type(current) == literal_text[0]); + for (std::size_t i = 1; i < length; ++i) + { + if (JSON_HEDLEY_UNLIKELY(std::char_traits::to_char_type(get()) != literal_text[i])) + { + error_message = "invalid literal"; + return token_type::parse_error; + } + } + return return_type; + } + + ///////////////////// + // input management + ///////////////////// + + /// reset token_buffer; current character is beginning of token + void reset() noexcept + { + token_buffer.clear(); + token_string.clear(); + token_string.push_back(std::char_traits::to_char_type(current)); + } + + /* + @brief get next character from the input + + This function provides the interface to the used input adapter. It does + not throw in case the input reached EOF, but returns a + `std::char_traits::eof()` in that case. Stores the scanned characters + for use in error messages. + + @return character read from the input + */ + char_int_type get() + { + ++position.chars_read_total; + ++position.chars_read_current_line; + + if (next_unget) + { + // just reset the next_unget variable and work with current + next_unget = false; + } + else + { + current = ia.get_character(); + } + + if (JSON_HEDLEY_LIKELY(current != std::char_traits::eof())) + { + token_string.push_back(std::char_traits::to_char_type(current)); + } + + if (current == '\n') + { + ++position.lines_read; + position.chars_read_current_line = 0; + } + + return current; + } + + /*! + @brief unget current character (read it again on next get) + + We implement unget by setting variable next_unget to true. The input is not + changed - we just simulate ungetting by modifying chars_read_total, + chars_read_current_line, and token_string. The next call to get() will + behave as if the unget character is read again. + */ + void unget() + { + next_unget = true; + + --position.chars_read_total; + + // in case we "unget" a newline, we have to also decrement the lines_read + if (position.chars_read_current_line == 0) + { + if (position.lines_read > 0) + { + --position.lines_read; + } + } + else + { + --position.chars_read_current_line; + } + + if (JSON_HEDLEY_LIKELY(current != std::char_traits::eof())) + { + JSON_ASSERT(!token_string.empty()); + token_string.pop_back(); + } + } + + /// add a character to token_buffer + void add(char_int_type c) + { + token_buffer.push_back(static_cast(c)); + } + + public: + ///////////////////// + // value getters + ///////////////////// + + /// return integer value + constexpr number_integer_t get_number_integer() const noexcept + { + return value_integer; + } + + /// return unsigned integer value + constexpr number_unsigned_t get_number_unsigned() const noexcept + { + return value_unsigned; + } + + /// return floating-point value + constexpr number_float_t get_number_float() const noexcept + { + return value_float; + } + + /// return current string value (implicitly resets the token; useful only once) + string_t& get_string() + { + return token_buffer; + } + + ///////////////////// + // diagnostics + ///////////////////// + + /// return position of last read token + constexpr position_t get_position() const noexcept + { + return position; + } + + /// return the last read token (for errors only). Will never contain EOF + /// (an arbitrary value that is not a valid char value, often -1), because + /// 255 may legitimately occur. May contain NUL, which should be escaped. + std::string get_token_string() const + { + // escape control characters + std::string result; + for (const auto c : token_string) + { + if (static_cast(c) <= '\x1F') + { + // escape control characters + std::array cs{{}}; + (std::snprintf)(cs.data(), cs.size(), "", static_cast(c)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + result += cs.data(); + } + else + { + // add character as is + result.push_back(static_cast(c)); + } + } + + return result; + } + + /// return syntax error message + JSON_HEDLEY_RETURNS_NON_NULL + constexpr const char* get_error_message() const noexcept + { + return error_message; + } + + ///////////////////// + // actual scanner + ///////////////////// + + /*! + @brief skip the UTF-8 byte order mark + @return true iff there is no BOM or the correct BOM has been skipped + */ + bool skip_bom() + { + if (get() == 0xEF) + { + // check if we completely parse the BOM + return get() == 0xBB && get() == 0xBF; + } + + // the first character is not the beginning of the BOM; unget it to + // process is later + unget(); + return true; + } + + void skip_whitespace() + { + do + { + get(); + } + while (current == ' ' || current == '\t' || current == '\n' || current == '\r'); + } + + token_type scan() + { + // initially, skip the BOM + if (position.chars_read_total == 0 && !skip_bom()) + { + error_message = "invalid BOM; must be 0xEF 0xBB 0xBF if given"; + return token_type::parse_error; + } + + // read next character and ignore whitespace + skip_whitespace(); + + // ignore comments + while (ignore_comments && current == '/') + { + if (!scan_comment()) + { + return token_type::parse_error; + } + + // skip following whitespace + skip_whitespace(); + } + + switch (current) + { + // structural characters + case '[': + return token_type::begin_array; + case ']': + return token_type::end_array; + case '{': + return token_type::begin_object; + case '}': + return token_type::end_object; + case ':': + return token_type::name_separator; + case ',': + return token_type::value_separator; + + // literals + case 't': + { + std::array true_literal = {{char_type('t'), char_type('r'), char_type('u'), char_type('e')}}; + return scan_literal(true_literal.data(), true_literal.size(), token_type::literal_true); + } + case 'f': + { + std::array false_literal = {{char_type('f'), char_type('a'), char_type('l'), char_type('s'), char_type('e')}}; + return scan_literal(false_literal.data(), false_literal.size(), token_type::literal_false); + } + case 'n': + { + std::array null_literal = {{char_type('n'), char_type('u'), char_type('l'), char_type('l')}}; + return scan_literal(null_literal.data(), null_literal.size(), token_type::literal_null); + } + + // string + case '\"': + return scan_string(); + + // number + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return scan_number(); + + // end of input (the null byte is needed when parsing from + // string literals) + case '\0': + case std::char_traits::eof(): + return token_type::end_of_input; + + // error + default: + error_message = "invalid literal"; + return token_type::parse_error; + } + } + + private: + /// input adapter + InputAdapterType ia; + + /// whether comments should be ignored (true) or signaled as errors (false) + const bool ignore_comments = false; + + /// the current character + char_int_type current = std::char_traits::eof(); + + /// whether the next get() call should just return current + bool next_unget = false; + + /// the start position of the current token + position_t position {}; + + /// raw input token string (for error messages) + std::vector token_string {}; + + /// buffer for variable-length tokens (numbers, strings) + string_t token_buffer {}; + + /// a description of occurred lexer errors + const char* error_message = ""; + + // number values + number_integer_t value_integer = 0; + number_unsigned_t value_unsigned = 0; + number_float_t value_float = 0; + + /// the decimal point + const char_int_type decimal_point_char = '.'; +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + + +#include // size_t +#include // declval +#include // string + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +template +using null_function_t = decltype(std::declval().null()); + +template +using boolean_function_t = + decltype(std::declval().boolean(std::declval())); + +template +using number_integer_function_t = + decltype(std::declval().number_integer(std::declval())); + +template +using number_unsigned_function_t = + decltype(std::declval().number_unsigned(std::declval())); + +template +using number_float_function_t = decltype(std::declval().number_float( + std::declval(), std::declval())); + +template +using string_function_t = + decltype(std::declval().string(std::declval())); + +template +using binary_function_t = + decltype(std::declval().binary(std::declval())); + +template +using start_object_function_t = + decltype(std::declval().start_object(std::declval())); + +template +using key_function_t = + decltype(std::declval().key(std::declval())); + +template +using end_object_function_t = decltype(std::declval().end_object()); + +template +using start_array_function_t = + decltype(std::declval().start_array(std::declval())); + +template +using end_array_function_t = decltype(std::declval().end_array()); + +template +using parse_error_function_t = decltype(std::declval().parse_error( + std::declval(), std::declval(), + std::declval())); + +template +struct is_sax +{ + private: + static_assert(is_basic_json::value, + "BasicJsonType must be of type basic_json<...>"); + + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using exception_t = typename BasicJsonType::exception; + + public: + static constexpr bool value = + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value && + is_detected_exact::value; +}; + +template +struct is_sax_static_asserts +{ + private: + static_assert(is_basic_json::value, + "BasicJsonType must be of type basic_json<...>"); + + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using exception_t = typename BasicJsonType::exception; + + public: + static_assert(is_detected_exact::value, + "Missing/invalid function: bool null()"); + static_assert(is_detected_exact::value, + "Missing/invalid function: bool boolean(bool)"); + static_assert(is_detected_exact::value, + "Missing/invalid function: bool boolean(bool)"); + static_assert( + is_detected_exact::value, + "Missing/invalid function: bool number_integer(number_integer_t)"); + static_assert( + is_detected_exact::value, + "Missing/invalid function: bool number_unsigned(number_unsigned_t)"); + static_assert(is_detected_exact::value, + "Missing/invalid function: bool number_float(number_float_t, const string_t&)"); + static_assert( + is_detected_exact::value, + "Missing/invalid function: bool string(string_t&)"); + static_assert( + is_detected_exact::value, + "Missing/invalid function: bool binary(binary_t&)"); + static_assert(is_detected_exact::value, + "Missing/invalid function: bool start_object(std::size_t)"); + static_assert(is_detected_exact::value, + "Missing/invalid function: bool key(string_t&)"); + static_assert(is_detected_exact::value, + "Missing/invalid function: bool end_object()"); + static_assert(is_detected_exact::value, + "Missing/invalid function: bool start_array(std::size_t)"); + static_assert(is_detected_exact::value, + "Missing/invalid function: bool end_array()"); + static_assert( + is_detected_exact::value, + "Missing/invalid function: bool parse_error(std::size_t, const " + "std::string&, const exception&)"); +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ + +/// how to treat CBOR tags +enum class cbor_tag_handler_t +{ + error, ///< throw a parse_error exception in case of a tag + ignore, ///< ignore tags + store ///< store tags as binary type +}; + +/*! +@brief determine system byte order + +@return true if and only if system's byte order is little endian + +@note from https://stackoverflow.com/a/1001328/266378 +*/ +static inline bool little_endianess(int num = 1) noexcept +{ + return *reinterpret_cast(&num) == 1; +} + + +/////////////////// +// binary reader // +/////////////////// + +/*! +@brief deserialization of CBOR, MessagePack, and UBJSON values +*/ +template> +class binary_reader +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using json_sax_t = SAX; + using char_type = typename InputAdapterType::char_type; + using char_int_type = typename std::char_traits::int_type; + + public: + /*! + @brief create a binary reader + + @param[in] adapter input adapter to read from + */ + explicit binary_reader(InputAdapterType&& adapter) noexcept : ia(std::move(adapter)) + { + (void)detail::is_sax_static_asserts {}; + } + + // make class move-only + binary_reader(const binary_reader&) = delete; + binary_reader(binary_reader&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + binary_reader& operator=(const binary_reader&) = delete; + binary_reader& operator=(binary_reader&&) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor) + ~binary_reader() = default; + + /*! + @param[in] format the binary format to parse + @param[in] sax_ a SAX event processor + @param[in] strict whether to expect the input to be consumed completed + @param[in] tag_handler how to treat CBOR tags + + @return whether parsing was successful + */ + JSON_HEDLEY_NON_NULL(3) + bool sax_parse(const input_format_t format, + json_sax_t* sax_, + const bool strict = true, + const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) + { + sax = sax_; + bool result = false; + + switch (format) + { + case input_format_t::bson: + result = parse_bson_internal(); + break; + + case input_format_t::cbor: + result = parse_cbor_internal(true, tag_handler); + break; + + case input_format_t::msgpack: + result = parse_msgpack_internal(); + break; + + case input_format_t::ubjson: + result = parse_ubjson_internal(); + break; + + case input_format_t::json: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + + // strict mode: next byte must be EOF + if (result && strict) + { + if (format == input_format_t::ubjson) + { + get_ignore_noop(); + } + else + { + get(); + } + + if (JSON_HEDLEY_UNLIKELY(current != std::char_traits::eof())) + { + return sax->parse_error(chars_read, get_token_string(), + parse_error::create(110, chars_read, exception_message(format, "expected end of input; last byte: 0x" + get_token_string(), "value"), BasicJsonType())); + } + } + + return result; + } + + private: + ////////// + // BSON // + ////////// + + /*! + @brief Reads in a BSON-object and passes it to the SAX-parser. + @return whether a valid BSON-value was passed to the SAX parser + */ + bool parse_bson_internal() + { + std::int32_t document_size{}; + get_number(input_format_t::bson, document_size); + + if (JSON_HEDLEY_UNLIKELY(!sax->start_object(std::size_t(-1)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/false))) + { + return false; + } + + return sax->end_object(); + } + + /*! + @brief Parses a C-style string from the BSON input. + @param[in,out] result A reference to the string variable where the read + string is to be stored. + @return `true` if the \x00-byte indicating the end of the string was + encountered before the EOF; false` indicates an unexpected EOF. + */ + bool get_bson_cstr(string_t& result) + { + auto out = std::back_inserter(result); + while (true) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bson, "cstring"))) + { + return false; + } + if (current == 0x00) + { + return true; + } + *out++ = static_cast(current); + } + } + + /*! + @brief Parses a zero-terminated string of length @a len from the BSON + input. + @param[in] len The length (including the zero-byte at the end) of the + string to be read. + @param[in,out] result A reference to the string variable where the read + string is to be stored. + @tparam NumberType The type of the length @a len + @pre len >= 1 + @return `true` if the string was successfully parsed + */ + template + bool get_bson_string(const NumberType len, string_t& result) + { + if (JSON_HEDLEY_UNLIKELY(len < 1)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::bson, "string length must be at least 1, is " + std::to_string(len), "string"), BasicJsonType())); + } + + return get_string(input_format_t::bson, len - static_cast(1), result) && get() != std::char_traits::eof(); + } + + /*! + @brief Parses a byte array input of length @a len from the BSON input. + @param[in] len The length of the byte array to be read. + @param[in,out] result A reference to the binary variable where the read + array is to be stored. + @tparam NumberType The type of the length @a len + @pre len >= 0 + @return `true` if the byte array was successfully parsed + */ + template + bool get_bson_binary(const NumberType len, binary_t& result) + { + if (JSON_HEDLEY_UNLIKELY(len < 0)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::bson, "byte array length cannot be negative, is " + std::to_string(len), "binary"), BasicJsonType())); + } + + // All BSON binary values have a subtype + std::uint8_t subtype{}; + get_number(input_format_t::bson, subtype); + result.set_subtype(subtype); + + return get_binary(input_format_t::bson, len, result); + } + + /*! + @brief Read a BSON document element of the given @a element_type. + @param[in] element_type The BSON element type, c.f. http://bsonspec.org/spec.html + @param[in] element_type_parse_position The position in the input stream, + where the `element_type` was read. + @warning Not all BSON element types are supported yet. An unsupported + @a element_type will give rise to a parse_error.114: + Unsupported BSON record type 0x... + @return whether a valid BSON-object/array was passed to the SAX parser + */ + bool parse_bson_element_internal(const char_int_type element_type, + const std::size_t element_type_parse_position) + { + switch (element_type) + { + case 0x01: // double + { + double number{}; + return get_number(input_format_t::bson, number) && sax->number_float(static_cast(number), ""); + } + + case 0x02: // string + { + std::int32_t len{}; + string_t value; + return get_number(input_format_t::bson, len) && get_bson_string(len, value) && sax->string(value); + } + + case 0x03: // object + { + return parse_bson_internal(); + } + + case 0x04: // array + { + return parse_bson_array(); + } + + case 0x05: // binary + { + std::int32_t len{}; + binary_t value; + return get_number(input_format_t::bson, len) && get_bson_binary(len, value) && sax->binary(value); + } + + case 0x08: // boolean + { + return sax->boolean(get() != 0); + } + + case 0x0A: // null + { + return sax->null(); + } + + case 0x10: // int32 + { + std::int32_t value{}; + return get_number(input_format_t::bson, value) && sax->number_integer(value); + } + + case 0x12: // int64 + { + std::int64_t value{}; + return get_number(input_format_t::bson, value) && sax->number_integer(value); + } + + default: // anything else not supported (yet) + { + std::array cr{{}}; + (std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(element_type)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + return sax->parse_error(element_type_parse_position, std::string(cr.data()), parse_error::create(114, element_type_parse_position, "Unsupported BSON record type 0x" + std::string(cr.data()), BasicJsonType())); + } + } + } + + /*! + @brief Read a BSON element list (as specified in the BSON-spec) + + The same binary layout is used for objects and arrays, hence it must be + indicated with the argument @a is_array which one is expected + (true --> array, false --> object). + + @param[in] is_array Determines if the element list being read is to be + treated as an object (@a is_array == false), or as an + array (@a is_array == true). + @return whether a valid BSON-object/array was passed to the SAX parser + */ + bool parse_bson_element_list(const bool is_array) + { + string_t key; + + while (auto element_type = get()) + { + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bson, "element list"))) + { + return false; + } + + const std::size_t element_type_parse_position = chars_read; + if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key))) + { + return false; + } + + if (!is_array && !sax->key(key)) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_internal(element_type, element_type_parse_position))) + { + return false; + } + + // get_bson_cstr only appends + key.clear(); + } + + return true; + } + + /*! + @brief Reads an array from the BSON input and passes it to the SAX-parser. + @return whether a valid BSON-array was passed to the SAX parser + */ + bool parse_bson_array() + { + std::int32_t document_size{}; + get_number(input_format_t::bson, document_size); + + if (JSON_HEDLEY_UNLIKELY(!sax->start_array(std::size_t(-1)))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/true))) + { + return false; + } + + return sax->end_array(); + } + + ////////// + // CBOR // + ////////// + + /*! + @param[in] get_char whether a new character should be retrieved from the + input (true) or whether the last read character should + be considered instead (false) + @param[in] tag_handler how CBOR tags should be treated + + @return whether a valid CBOR value was passed to the SAX parser + */ + bool parse_cbor_internal(const bool get_char, + const cbor_tag_handler_t tag_handler) + { + switch (get_char ? get() : current) + { + // EOF + case std::char_traits::eof(): + return unexpect_eof(input_format_t::cbor, "value"); + + // Integer 0x00..0x17 (0..23) + case 0x00: + case 0x01: + case 0x02: + case 0x03: + case 0x04: + case 0x05: + case 0x06: + case 0x07: + case 0x08: + case 0x09: + case 0x0A: + case 0x0B: + case 0x0C: + case 0x0D: + case 0x0E: + case 0x0F: + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + return sax->number_unsigned(static_cast(current)); + + case 0x18: // Unsigned integer (one-byte uint8_t follows) + { + std::uint8_t number{}; + return get_number(input_format_t::cbor, number) && sax->number_unsigned(number); + } + + case 0x19: // Unsigned integer (two-byte uint16_t follows) + { + std::uint16_t number{}; + return get_number(input_format_t::cbor, number) && sax->number_unsigned(number); + } + + case 0x1A: // Unsigned integer (four-byte uint32_t follows) + { + std::uint32_t number{}; + return get_number(input_format_t::cbor, number) && sax->number_unsigned(number); + } + + case 0x1B: // Unsigned integer (eight-byte uint64_t follows) + { + std::uint64_t number{}; + return get_number(input_format_t::cbor, number) && sax->number_unsigned(number); + } + + // Negative integer -1-0x00..-1-0x17 (-1..-24) + case 0x20: + case 0x21: + case 0x22: + case 0x23: + case 0x24: + case 0x25: + case 0x26: + case 0x27: + case 0x28: + case 0x29: + case 0x2A: + case 0x2B: + case 0x2C: + case 0x2D: + case 0x2E: + case 0x2F: + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + return sax->number_integer(static_cast(0x20 - 1 - current)); + + case 0x38: // Negative integer (one-byte uint8_t follows) + { + std::uint8_t number{}; + return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); + } + + case 0x39: // Negative integer -1-n (two-byte uint16_t follows) + { + std::uint16_t number{}; + return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); + } + + case 0x3A: // Negative integer -1-n (four-byte uint32_t follows) + { + std::uint32_t number{}; + return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) - number); + } + + case 0x3B: // Negative integer -1-n (eight-byte uint64_t follows) + { + std::uint64_t number{}; + return get_number(input_format_t::cbor, number) && sax->number_integer(static_cast(-1) + - static_cast(number)); + } + + // Binary data (0x00..0x17 bytes follow) + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + case 0x48: + case 0x49: + case 0x4A: + case 0x4B: + case 0x4C: + case 0x4D: + case 0x4E: + case 0x4F: + case 0x50: + case 0x51: + case 0x52: + case 0x53: + case 0x54: + case 0x55: + case 0x56: + case 0x57: + case 0x58: // Binary data (one-byte uint8_t for n follows) + case 0x59: // Binary data (two-byte uint16_t for n follow) + case 0x5A: // Binary data (four-byte uint32_t for n follow) + case 0x5B: // Binary data (eight-byte uint64_t for n follow) + case 0x5F: // Binary data (indefinite length) + { + binary_t b; + return get_cbor_binary(b) && sax->binary(b); + } + + // UTF-8 string (0x00..0x17 bytes follow) + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + case 0x68: + case 0x69: + case 0x6A: + case 0x6B: + case 0x6C: + case 0x6D: + case 0x6E: + case 0x6F: + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + case 0x78: // UTF-8 string (one-byte uint8_t for n follows) + case 0x79: // UTF-8 string (two-byte uint16_t for n follow) + case 0x7A: // UTF-8 string (four-byte uint32_t for n follow) + case 0x7B: // UTF-8 string (eight-byte uint64_t for n follow) + case 0x7F: // UTF-8 string (indefinite length) + { + string_t s; + return get_cbor_string(s) && sax->string(s); + } + + // array (0x00..0x17 data items follow) + case 0x80: + case 0x81: + case 0x82: + case 0x83: + case 0x84: + case 0x85: + case 0x86: + case 0x87: + case 0x88: + case 0x89: + case 0x8A: + case 0x8B: + case 0x8C: + case 0x8D: + case 0x8E: + case 0x8F: + case 0x90: + case 0x91: + case 0x92: + case 0x93: + case 0x94: + case 0x95: + case 0x96: + case 0x97: + return get_cbor_array(static_cast(static_cast(current) & 0x1Fu), tag_handler); + + case 0x98: // array (one-byte uint8_t for n follows) + { + std::uint8_t len{}; + return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast(len), tag_handler); + } + + case 0x99: // array (two-byte uint16_t for n follow) + { + std::uint16_t len{}; + return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast(len), tag_handler); + } + + case 0x9A: // array (four-byte uint32_t for n follow) + { + std::uint32_t len{}; + return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast(len), tag_handler); + } + + case 0x9B: // array (eight-byte uint64_t for n follow) + { + std::uint64_t len{}; + return get_number(input_format_t::cbor, len) && get_cbor_array(detail::conditional_static_cast(len), tag_handler); + } + + case 0x9F: // array (indefinite length) + return get_cbor_array(std::size_t(-1), tag_handler); + + // map (0x00..0x17 pairs of data items follow) + case 0xA0: + case 0xA1: + case 0xA2: + case 0xA3: + case 0xA4: + case 0xA5: + case 0xA6: + case 0xA7: + case 0xA8: + case 0xA9: + case 0xAA: + case 0xAB: + case 0xAC: + case 0xAD: + case 0xAE: + case 0xAF: + case 0xB0: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB4: + case 0xB5: + case 0xB6: + case 0xB7: + return get_cbor_object(static_cast(static_cast(current) & 0x1Fu), tag_handler); + + case 0xB8: // map (one-byte uint8_t for n follows) + { + std::uint8_t len{}; + return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast(len), tag_handler); + } + + case 0xB9: // map (two-byte uint16_t for n follow) + { + std::uint16_t len{}; + return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast(len), tag_handler); + } + + case 0xBA: // map (four-byte uint32_t for n follow) + { + std::uint32_t len{}; + return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast(len), tag_handler); + } + + case 0xBB: // map (eight-byte uint64_t for n follow) + { + std::uint64_t len{}; + return get_number(input_format_t::cbor, len) && get_cbor_object(detail::conditional_static_cast(len), tag_handler); + } + + case 0xBF: // map (indefinite length) + return get_cbor_object(std::size_t(-1), tag_handler); + + case 0xC6: // tagged item + case 0xC7: + case 0xC8: + case 0xC9: + case 0xCA: + case 0xCB: + case 0xCC: + case 0xCD: + case 0xCE: + case 0xCF: + case 0xD0: + case 0xD1: + case 0xD2: + case 0xD3: + case 0xD4: + case 0xD8: // tagged item (1 bytes follow) + case 0xD9: // tagged item (2 bytes follow) + case 0xDA: // tagged item (4 bytes follow) + case 0xDB: // tagged item (8 bytes follow) + { + switch (tag_handler) + { + case cbor_tag_handler_t::error: + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::cbor, "invalid byte: 0x" + last_token, "value"), BasicJsonType())); + } + + case cbor_tag_handler_t::ignore: + { + // ignore binary subtype + switch (current) + { + case 0xD8: + { + std::uint8_t subtype_to_ignore{}; + get_number(input_format_t::cbor, subtype_to_ignore); + break; + } + case 0xD9: + { + std::uint16_t subtype_to_ignore{}; + get_number(input_format_t::cbor, subtype_to_ignore); + break; + } + case 0xDA: + { + std::uint32_t subtype_to_ignore{}; + get_number(input_format_t::cbor, subtype_to_ignore); + break; + } + case 0xDB: + { + std::uint64_t subtype_to_ignore{}; + get_number(input_format_t::cbor, subtype_to_ignore); + break; + } + default: + break; + } + return parse_cbor_internal(true, tag_handler); + } + + case cbor_tag_handler_t::store: + { + binary_t b; + // use binary subtype and store in binary container + switch (current) + { + case 0xD8: + { + std::uint8_t subtype{}; + get_number(input_format_t::cbor, subtype); + b.set_subtype(detail::conditional_static_cast(subtype)); + break; + } + case 0xD9: + { + std::uint16_t subtype{}; + get_number(input_format_t::cbor, subtype); + b.set_subtype(detail::conditional_static_cast(subtype)); + break; + } + case 0xDA: + { + std::uint32_t subtype{}; + get_number(input_format_t::cbor, subtype); + b.set_subtype(detail::conditional_static_cast(subtype)); + break; + } + case 0xDB: + { + std::uint64_t subtype{}; + get_number(input_format_t::cbor, subtype); + b.set_subtype(detail::conditional_static_cast(subtype)); + break; + } + default: + return parse_cbor_internal(true, tag_handler); + } + get(); + return get_cbor_binary(b) && sax->binary(b); + } + + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + return false; // LCOV_EXCL_LINE + } + } + + case 0xF4: // false + return sax->boolean(false); + + case 0xF5: // true + return sax->boolean(true); + + case 0xF6: // null + return sax->null(); + + case 0xF9: // Half-Precision Float (two-byte IEEE 754) + { + const auto byte1_raw = get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::cbor, "number"))) + { + return false; + } + const auto byte2_raw = get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::cbor, "number"))) + { + return false; + } + + const auto byte1 = static_cast(byte1_raw); + const auto byte2 = static_cast(byte2_raw); + + // code from RFC 7049, Appendix D, Figure 3: + // As half-precision floating-point numbers were only added + // to IEEE 754 in 2008, today's programming platforms often + // still only have limited support for them. It is very + // easy to include at least decoding support for them even + // without such support. An example of a small decoder for + // half-precision floating-point numbers in the C language + // is shown in Fig. 3. + const auto half = static_cast((byte1 << 8u) + byte2); + const double val = [&half] + { + const int exp = (half >> 10u) & 0x1Fu; + const unsigned int mant = half & 0x3FFu; + JSON_ASSERT(0 <= exp&& exp <= 32); + JSON_ASSERT(mant <= 1024); + switch (exp) + { + case 0: + return std::ldexp(mant, -24); + case 31: + return (mant == 0) + ? std::numeric_limits::infinity() + : std::numeric_limits::quiet_NaN(); + default: + return std::ldexp(mant + 1024, exp - 25); + } + }(); + return sax->number_float((half & 0x8000u) != 0 + ? static_cast(-val) + : static_cast(val), ""); + } + + case 0xFA: // Single-Precision Float (four-byte IEEE 754) + { + float number{}; + return get_number(input_format_t::cbor, number) && sax->number_float(static_cast(number), ""); + } + + case 0xFB: // Double-Precision Float (eight-byte IEEE 754) + { + double number{}; + return get_number(input_format_t::cbor, number) && sax->number_float(static_cast(number), ""); + } + + default: // anything else (0xFF is handled inside the other types) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::cbor, "invalid byte: 0x" + last_token, "value"), BasicJsonType())); + } + } + } + + /*! + @brief reads a CBOR string + + This function first reads starting bytes to determine the expected + string length and then copies this number of bytes into a string. + Additionally, CBOR's strings with indefinite lengths are supported. + + @param[out] result created string + + @return whether string creation completed + */ + bool get_cbor_string(string_t& result) + { + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::cbor, "string"))) + { + return false; + } + + switch (current) + { + // UTF-8 string (0x00..0x17 bytes follow) + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + case 0x68: + case 0x69: + case 0x6A: + case 0x6B: + case 0x6C: + case 0x6D: + case 0x6E: + case 0x6F: + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + { + return get_string(input_format_t::cbor, static_cast(current) & 0x1Fu, result); + } + + case 0x78: // UTF-8 string (one-byte uint8_t for n follows) + { + std::uint8_t len{}; + return get_number(input_format_t::cbor, len) && get_string(input_format_t::cbor, len, result); + } + + case 0x79: // UTF-8 string (two-byte uint16_t for n follow) + { + std::uint16_t len{}; + return get_number(input_format_t::cbor, len) && get_string(input_format_t::cbor, len, result); + } + + case 0x7A: // UTF-8 string (four-byte uint32_t for n follow) + { + std::uint32_t len{}; + return get_number(input_format_t::cbor, len) && get_string(input_format_t::cbor, len, result); + } + + case 0x7B: // UTF-8 string (eight-byte uint64_t for n follow) + { + std::uint64_t len{}; + return get_number(input_format_t::cbor, len) && get_string(input_format_t::cbor, len, result); + } + + case 0x7F: // UTF-8 string (indefinite length) + { + while (get() != 0xFF) + { + string_t chunk; + if (!get_cbor_string(chunk)) + { + return false; + } + result.append(chunk); + } + return true; + } + + default: + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::cbor, "expected length specification (0x60-0x7B) or indefinite string type (0x7F); last byte: 0x" + last_token, "string"), BasicJsonType())); + } + } + } + + /*! + @brief reads a CBOR byte array + + This function first reads starting bytes to determine the expected + byte array length and then copies this number of bytes into the byte array. + Additionally, CBOR's byte arrays with indefinite lengths are supported. + + @param[out] result created byte array + + @return whether byte array creation completed + */ + bool get_cbor_binary(binary_t& result) + { + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::cbor, "binary"))) + { + return false; + } + + switch (current) + { + // Binary data (0x00..0x17 bytes follow) + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + case 0x48: + case 0x49: + case 0x4A: + case 0x4B: + case 0x4C: + case 0x4D: + case 0x4E: + case 0x4F: + case 0x50: + case 0x51: + case 0x52: + case 0x53: + case 0x54: + case 0x55: + case 0x56: + case 0x57: + { + return get_binary(input_format_t::cbor, static_cast(current) & 0x1Fu, result); + } + + case 0x58: // Binary data (one-byte uint8_t for n follows) + { + std::uint8_t len{}; + return get_number(input_format_t::cbor, len) && + get_binary(input_format_t::cbor, len, result); + } + + case 0x59: // Binary data (two-byte uint16_t for n follow) + { + std::uint16_t len{}; + return get_number(input_format_t::cbor, len) && + get_binary(input_format_t::cbor, len, result); + } + + case 0x5A: // Binary data (four-byte uint32_t for n follow) + { + std::uint32_t len{}; + return get_number(input_format_t::cbor, len) && + get_binary(input_format_t::cbor, len, result); + } + + case 0x5B: // Binary data (eight-byte uint64_t for n follow) + { + std::uint64_t len{}; + return get_number(input_format_t::cbor, len) && + get_binary(input_format_t::cbor, len, result); + } + + case 0x5F: // Binary data (indefinite length) + { + while (get() != 0xFF) + { + binary_t chunk; + if (!get_cbor_binary(chunk)) + { + return false; + } + result.insert(result.end(), chunk.begin(), chunk.end()); + } + return true; + } + + default: + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::cbor, "expected length specification (0x40-0x5B) or indefinite binary array type (0x5F); last byte: 0x" + last_token, "binary"), BasicJsonType())); + } + } + } + + /*! + @param[in] len the length of the array or std::size_t(-1) for an + array of indefinite size + @param[in] tag_handler how CBOR tags should be treated + @return whether array creation completed + */ + bool get_cbor_array(const std::size_t len, + const cbor_tag_handler_t tag_handler) + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len))) + { + return false; + } + + if (len != std::size_t(-1)) + { + for (std::size_t i = 0; i < len; ++i) + { + if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler))) + { + return false; + } + } + } + else + { + while (get() != 0xFF) + { + if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(false, tag_handler))) + { + return false; + } + } + } + + return sax->end_array(); + } + + /*! + @param[in] len the length of the object or std::size_t(-1) for an + object of indefinite size + @param[in] tag_handler how CBOR tags should be treated + @return whether object creation completed + */ + bool get_cbor_object(const std::size_t len, + const cbor_tag_handler_t tag_handler) + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_object(len))) + { + return false; + } + + if (len != 0) + { + string_t key; + if (len != std::size_t(-1)) + { + for (std::size_t i = 0; i < len; ++i) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler))) + { + return false; + } + key.clear(); + } + } + else + { + while (get() != 0xFF) + { + if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler))) + { + return false; + } + key.clear(); + } + } + } + + return sax->end_object(); + } + + ///////////// + // MsgPack // + ///////////// + + /*! + @return whether a valid MessagePack value was passed to the SAX parser + */ + bool parse_msgpack_internal() + { + switch (get()) + { + // EOF + case std::char_traits::eof(): + return unexpect_eof(input_format_t::msgpack, "value"); + + // positive fixint + case 0x00: + case 0x01: + case 0x02: + case 0x03: + case 0x04: + case 0x05: + case 0x06: + case 0x07: + case 0x08: + case 0x09: + case 0x0A: + case 0x0B: + case 0x0C: + case 0x0D: + case 0x0E: + case 0x0F: + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + case 0x18: + case 0x19: + case 0x1A: + case 0x1B: + case 0x1C: + case 0x1D: + case 0x1E: + case 0x1F: + case 0x20: + case 0x21: + case 0x22: + case 0x23: + case 0x24: + case 0x25: + case 0x26: + case 0x27: + case 0x28: + case 0x29: + case 0x2A: + case 0x2B: + case 0x2C: + case 0x2D: + case 0x2E: + case 0x2F: + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3A: + case 0x3B: + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + case 0x48: + case 0x49: + case 0x4A: + case 0x4B: + case 0x4C: + case 0x4D: + case 0x4E: + case 0x4F: + case 0x50: + case 0x51: + case 0x52: + case 0x53: + case 0x54: + case 0x55: + case 0x56: + case 0x57: + case 0x58: + case 0x59: + case 0x5A: + case 0x5B: + case 0x5C: + case 0x5D: + case 0x5E: + case 0x5F: + case 0x60: + case 0x61: + case 0x62: + case 0x63: + case 0x64: + case 0x65: + case 0x66: + case 0x67: + case 0x68: + case 0x69: + case 0x6A: + case 0x6B: + case 0x6C: + case 0x6D: + case 0x6E: + case 0x6F: + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + case 0x78: + case 0x79: + case 0x7A: + case 0x7B: + case 0x7C: + case 0x7D: + case 0x7E: + case 0x7F: + return sax->number_unsigned(static_cast(current)); + + // fixmap + case 0x80: + case 0x81: + case 0x82: + case 0x83: + case 0x84: + case 0x85: + case 0x86: + case 0x87: + case 0x88: + case 0x89: + case 0x8A: + case 0x8B: + case 0x8C: + case 0x8D: + case 0x8E: + case 0x8F: + return get_msgpack_object(static_cast(static_cast(current) & 0x0Fu)); + + // fixarray + case 0x90: + case 0x91: + case 0x92: + case 0x93: + case 0x94: + case 0x95: + case 0x96: + case 0x97: + case 0x98: + case 0x99: + case 0x9A: + case 0x9B: + case 0x9C: + case 0x9D: + case 0x9E: + case 0x9F: + return get_msgpack_array(static_cast(static_cast(current) & 0x0Fu)); + + // fixstr + case 0xA0: + case 0xA1: + case 0xA2: + case 0xA3: + case 0xA4: + case 0xA5: + case 0xA6: + case 0xA7: + case 0xA8: + case 0xA9: + case 0xAA: + case 0xAB: + case 0xAC: + case 0xAD: + case 0xAE: + case 0xAF: + case 0xB0: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB4: + case 0xB5: + case 0xB6: + case 0xB7: + case 0xB8: + case 0xB9: + case 0xBA: + case 0xBB: + case 0xBC: + case 0xBD: + case 0xBE: + case 0xBF: + case 0xD9: // str 8 + case 0xDA: // str 16 + case 0xDB: // str 32 + { + string_t s; + return get_msgpack_string(s) && sax->string(s); + } + + case 0xC0: // nil + return sax->null(); + + case 0xC2: // false + return sax->boolean(false); + + case 0xC3: // true + return sax->boolean(true); + + case 0xC4: // bin 8 + case 0xC5: // bin 16 + case 0xC6: // bin 32 + case 0xC7: // ext 8 + case 0xC8: // ext 16 + case 0xC9: // ext 32 + case 0xD4: // fixext 1 + case 0xD5: // fixext 2 + case 0xD6: // fixext 4 + case 0xD7: // fixext 8 + case 0xD8: // fixext 16 + { + binary_t b; + return get_msgpack_binary(b) && sax->binary(b); + } + + case 0xCA: // float 32 + { + float number{}; + return get_number(input_format_t::msgpack, number) && sax->number_float(static_cast(number), ""); + } + + case 0xCB: // float 64 + { + double number{}; + return get_number(input_format_t::msgpack, number) && sax->number_float(static_cast(number), ""); + } + + case 0xCC: // uint 8 + { + std::uint8_t number{}; + return get_number(input_format_t::msgpack, number) && sax->number_unsigned(number); + } + + case 0xCD: // uint 16 + { + std::uint16_t number{}; + return get_number(input_format_t::msgpack, number) && sax->number_unsigned(number); + } + + case 0xCE: // uint 32 + { + std::uint32_t number{}; + return get_number(input_format_t::msgpack, number) && sax->number_unsigned(number); + } + + case 0xCF: // uint 64 + { + std::uint64_t number{}; + return get_number(input_format_t::msgpack, number) && sax->number_unsigned(number); + } + + case 0xD0: // int 8 + { + std::int8_t number{}; + return get_number(input_format_t::msgpack, number) && sax->number_integer(number); + } + + case 0xD1: // int 16 + { + std::int16_t number{}; + return get_number(input_format_t::msgpack, number) && sax->number_integer(number); + } + + case 0xD2: // int 32 + { + std::int32_t number{}; + return get_number(input_format_t::msgpack, number) && sax->number_integer(number); + } + + case 0xD3: // int 64 + { + std::int64_t number{}; + return get_number(input_format_t::msgpack, number) && sax->number_integer(number); + } + + case 0xDC: // array 16 + { + std::uint16_t len{}; + return get_number(input_format_t::msgpack, len) && get_msgpack_array(static_cast(len)); + } + + case 0xDD: // array 32 + { + std::uint32_t len{}; + return get_number(input_format_t::msgpack, len) && get_msgpack_array(static_cast(len)); + } + + case 0xDE: // map 16 + { + std::uint16_t len{}; + return get_number(input_format_t::msgpack, len) && get_msgpack_object(static_cast(len)); + } + + case 0xDF: // map 32 + { + std::uint32_t len{}; + return get_number(input_format_t::msgpack, len) && get_msgpack_object(static_cast(len)); + } + + // negative fixint + case 0xE0: + case 0xE1: + case 0xE2: + case 0xE3: + case 0xE4: + case 0xE5: + case 0xE6: + case 0xE7: + case 0xE8: + case 0xE9: + case 0xEA: + case 0xEB: + case 0xEC: + case 0xED: + case 0xEE: + case 0xEF: + case 0xF0: + case 0xF1: + case 0xF2: + case 0xF3: + case 0xF4: + case 0xF5: + case 0xF6: + case 0xF7: + case 0xF8: + case 0xF9: + case 0xFA: + case 0xFB: + case 0xFC: + case 0xFD: + case 0xFE: + case 0xFF: + return sax->number_integer(static_cast(current)); + + default: // anything else + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::msgpack, "invalid byte: 0x" + last_token, "value"), BasicJsonType())); + } + } + } + + /*! + @brief reads a MessagePack string + + This function first reads starting bytes to determine the expected + string length and then copies this number of bytes into a string. + + @param[out] result created string + + @return whether string creation completed + */ + bool get_msgpack_string(string_t& result) + { + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::msgpack, "string"))) + { + return false; + } + + switch (current) + { + // fixstr + case 0xA0: + case 0xA1: + case 0xA2: + case 0xA3: + case 0xA4: + case 0xA5: + case 0xA6: + case 0xA7: + case 0xA8: + case 0xA9: + case 0xAA: + case 0xAB: + case 0xAC: + case 0xAD: + case 0xAE: + case 0xAF: + case 0xB0: + case 0xB1: + case 0xB2: + case 0xB3: + case 0xB4: + case 0xB5: + case 0xB6: + case 0xB7: + case 0xB8: + case 0xB9: + case 0xBA: + case 0xBB: + case 0xBC: + case 0xBD: + case 0xBE: + case 0xBF: + { + return get_string(input_format_t::msgpack, static_cast(current) & 0x1Fu, result); + } + + case 0xD9: // str 8 + { + std::uint8_t len{}; + return get_number(input_format_t::msgpack, len) && get_string(input_format_t::msgpack, len, result); + } + + case 0xDA: // str 16 + { + std::uint16_t len{}; + return get_number(input_format_t::msgpack, len) && get_string(input_format_t::msgpack, len, result); + } + + case 0xDB: // str 32 + { + std::uint32_t len{}; + return get_number(input_format_t::msgpack, len) && get_string(input_format_t::msgpack, len, result); + } + + default: + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::msgpack, "expected length specification (0xA0-0xBF, 0xD9-0xDB); last byte: 0x" + last_token, "string"), BasicJsonType())); + } + } + } + + /*! + @brief reads a MessagePack byte array + + This function first reads starting bytes to determine the expected + byte array length and then copies this number of bytes into a byte array. + + @param[out] result created byte array + + @return whether byte array creation completed + */ + bool get_msgpack_binary(binary_t& result) + { + // helper function to set the subtype + auto assign_and_return_true = [&result](std::int8_t subtype) + { + result.set_subtype(static_cast(subtype)); + return true; + }; + + switch (current) + { + case 0xC4: // bin 8 + { + std::uint8_t len{}; + return get_number(input_format_t::msgpack, len) && + get_binary(input_format_t::msgpack, len, result); + } + + case 0xC5: // bin 16 + { + std::uint16_t len{}; + return get_number(input_format_t::msgpack, len) && + get_binary(input_format_t::msgpack, len, result); + } + + case 0xC6: // bin 32 + { + std::uint32_t len{}; + return get_number(input_format_t::msgpack, len) && + get_binary(input_format_t::msgpack, len, result); + } + + case 0xC7: // ext 8 + { + std::uint8_t len{}; + std::int8_t subtype{}; + return get_number(input_format_t::msgpack, len) && + get_number(input_format_t::msgpack, subtype) && + get_binary(input_format_t::msgpack, len, result) && + assign_and_return_true(subtype); + } + + case 0xC8: // ext 16 + { + std::uint16_t len{}; + std::int8_t subtype{}; + return get_number(input_format_t::msgpack, len) && + get_number(input_format_t::msgpack, subtype) && + get_binary(input_format_t::msgpack, len, result) && + assign_and_return_true(subtype); + } + + case 0xC9: // ext 32 + { + std::uint32_t len{}; + std::int8_t subtype{}; + return get_number(input_format_t::msgpack, len) && + get_number(input_format_t::msgpack, subtype) && + get_binary(input_format_t::msgpack, len, result) && + assign_and_return_true(subtype); + } + + case 0xD4: // fixext 1 + { + std::int8_t subtype{}; + return get_number(input_format_t::msgpack, subtype) && + get_binary(input_format_t::msgpack, 1, result) && + assign_and_return_true(subtype); + } + + case 0xD5: // fixext 2 + { + std::int8_t subtype{}; + return get_number(input_format_t::msgpack, subtype) && + get_binary(input_format_t::msgpack, 2, result) && + assign_and_return_true(subtype); + } + + case 0xD6: // fixext 4 + { + std::int8_t subtype{}; + return get_number(input_format_t::msgpack, subtype) && + get_binary(input_format_t::msgpack, 4, result) && + assign_and_return_true(subtype); + } + + case 0xD7: // fixext 8 + { + std::int8_t subtype{}; + return get_number(input_format_t::msgpack, subtype) && + get_binary(input_format_t::msgpack, 8, result) && + assign_and_return_true(subtype); + } + + case 0xD8: // fixext 16 + { + std::int8_t subtype{}; + return get_number(input_format_t::msgpack, subtype) && + get_binary(input_format_t::msgpack, 16, result) && + assign_and_return_true(subtype); + } + + default: // LCOV_EXCL_LINE + return false; // LCOV_EXCL_LINE + } + } + + /*! + @param[in] len the length of the array + @return whether array creation completed + */ + bool get_msgpack_array(const std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len))) + { + return false; + } + + for (std::size_t i = 0; i < len; ++i) + { + if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal())) + { + return false; + } + } + + return sax->end_array(); + } + + /*! + @param[in] len the length of the object + @return whether object creation completed + */ + bool get_msgpack_object(const std::size_t len) + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_object(len))) + { + return false; + } + + string_t key; + for (std::size_t i = 0; i < len; ++i) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!get_msgpack_string(key) || !sax->key(key))) + { + return false; + } + + if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal())) + { + return false; + } + key.clear(); + } + + return sax->end_object(); + } + + //////////// + // UBJSON // + //////////// + + /*! + @param[in] get_char whether a new character should be retrieved from the + input (true, default) or whether the last read + character should be considered instead + + @return whether a valid UBJSON value was passed to the SAX parser + */ + bool parse_ubjson_internal(const bool get_char = true) + { + return get_ubjson_value(get_char ? get_ignore_noop() : current); + } + + /*! + @brief reads a UBJSON string + + This function is either called after reading the 'S' byte explicitly + indicating a string, or in case of an object key where the 'S' byte can be + left out. + + @param[out] result created string + @param[in] get_char whether a new character should be retrieved from the + input (true, default) or whether the last read + character should be considered instead + + @return whether string creation completed + */ + bool get_ubjson_string(string_t& result, const bool get_char = true) + { + if (get_char) + { + get(); // TODO(niels): may we ignore N here? + } + + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "value"))) + { + return false; + } + + switch (current) + { + case 'U': + { + std::uint8_t len{}; + return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result); + } + + case 'i': + { + std::int8_t len{}; + return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result); + } + + case 'I': + { + std::int16_t len{}; + return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result); + } + + case 'l': + { + std::int32_t len{}; + return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result); + } + + case 'L': + { + std::int64_t len{}; + return get_number(input_format_t::ubjson, len) && get_string(input_format_t::ubjson, len, result); + } + + default: + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "expected length type specification (U, i, I, l, L); last byte: 0x" + last_token, "string"), BasicJsonType())); + } + } + + /*! + @param[out] result determined size + @return whether size determination completed + */ + bool get_ubjson_size_value(std::size_t& result) + { + switch (get_ignore_noop()) + { + case 'U': + { + std::uint8_t number{}; + if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + case 'i': + { + std::int8_t number{}; + if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); // NOLINT(bugprone-signed-char-misuse,cert-str34-c): number is not a char + return true; + } + + case 'I': + { + std::int16_t number{}; + if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + case 'l': + { + std::int32_t number{}; + if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + case 'L': + { + std::int64_t number{}; + if (JSON_HEDLEY_UNLIKELY(!get_number(input_format_t::ubjson, number))) + { + return false; + } + result = static_cast(number); + return true; + } + + default: + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "expected length type specification (U, i, I, l, L) after '#'; last byte: 0x" + last_token, "size"), BasicJsonType())); + } + } + } + + /*! + @brief determine the type and size for a container + + In the optimized UBJSON format, a type and a size can be provided to allow + for a more compact representation. + + @param[out] result pair of the size and the type + + @return whether pair creation completed + */ + bool get_ubjson_size_type(std::pair& result) + { + result.first = string_t::npos; // size + result.second = 0; // type + + get_ignore_noop(); + + if (current == '$') + { + result.second = get(); // must not ignore 'N', because 'N' maybe the type + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "type"))) + { + return false; + } + + get_ignore_noop(); + if (JSON_HEDLEY_UNLIKELY(current != '#')) + { + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "value"))) + { + return false; + } + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::ubjson, "expected '#' after type information; last byte: 0x" + last_token, "size"), BasicJsonType())); + } + + return get_ubjson_size_value(result.first); + } + + if (current == '#') + { + return get_ubjson_size_value(result.first); + } + + return true; + } + + /*! + @param prefix the previously read or set type prefix + @return whether value creation completed + */ + bool get_ubjson_value(const char_int_type prefix) + { + switch (prefix) + { + case std::char_traits::eof(): // EOF + return unexpect_eof(input_format_t::ubjson, "value"); + + case 'T': // true + return sax->boolean(true); + case 'F': // false + return sax->boolean(false); + + case 'Z': // null + return sax->null(); + + case 'U': + { + std::uint8_t number{}; + return get_number(input_format_t::ubjson, number) && sax->number_unsigned(number); + } + + case 'i': + { + std::int8_t number{}; + return get_number(input_format_t::ubjson, number) && sax->number_integer(number); + } + + case 'I': + { + std::int16_t number{}; + return get_number(input_format_t::ubjson, number) && sax->number_integer(number); + } + + case 'l': + { + std::int32_t number{}; + return get_number(input_format_t::ubjson, number) && sax->number_integer(number); + } + + case 'L': + { + std::int64_t number{}; + return get_number(input_format_t::ubjson, number) && sax->number_integer(number); + } + + case 'd': + { + float number{}; + return get_number(input_format_t::ubjson, number) && sax->number_float(static_cast(number), ""); + } + + case 'D': + { + double number{}; + return get_number(input_format_t::ubjson, number) && sax->number_float(static_cast(number), ""); + } + + case 'H': + { + return get_ubjson_high_precision_number(); + } + + case 'C': // char + { + get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "char"))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(current > 127)) + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(113, chars_read, exception_message(input_format_t::ubjson, "byte after 'C' must be in range 0x00..0x7F; last byte: 0x" + last_token, "char"), BasicJsonType())); + } + string_t s(1, static_cast(current)); + return sax->string(s); + } + + case 'S': // string + { + string_t s; + return get_ubjson_string(s) && sax->string(s); + } + + case '[': // array + return get_ubjson_array(); + + case '{': // object + return get_ubjson_object(); + + default: // anything else + { + auto last_token = get_token_string(); + return sax->parse_error(chars_read, last_token, parse_error::create(112, chars_read, exception_message(input_format_t::ubjson, "invalid byte: 0x" + last_token, "value"), BasicJsonType())); + } + } + } + + /*! + @return whether array creation completed + */ + bool get_ubjson_array() + { + std::pair size_and_type; + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_size_type(size_and_type))) + { + return false; + } + + if (size_and_type.first != string_t::npos) + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_array(size_and_type.first))) + { + return false; + } + + if (size_and_type.second != 0) + { + if (size_and_type.second != 'N') + { + for (std::size_t i = 0; i < size_and_type.first; ++i) + { + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second))) + { + return false; + } + } + } + } + else + { + for (std::size_t i = 0; i < size_and_type.first; ++i) + { + if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal())) + { + return false; + } + } + } + } + else + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_array(std::size_t(-1)))) + { + return false; + } + + while (current != ']') + { + if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal(false))) + { + return false; + } + get_ignore_noop(); + } + } + + return sax->end_array(); + } + + /*! + @return whether object creation completed + */ + bool get_ubjson_object() + { + std::pair size_and_type; + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_size_type(size_and_type))) + { + return false; + } + + string_t key; + if (size_and_type.first != string_t::npos) + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_object(size_and_type.first))) + { + return false; + } + + if (size_and_type.second != 0) + { + for (std::size_t i = 0; i < size_and_type.first; ++i) + { + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second))) + { + return false; + } + key.clear(); + } + } + else + { + for (std::size_t i = 0; i < size_and_type.first; ++i) + { + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal())) + { + return false; + } + key.clear(); + } + } + } + else + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_object(std::size_t(-1)))) + { + return false; + } + + while (current != '}') + { + if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key, false) || !sax->key(key))) + { + return false; + } + if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal())) + { + return false; + } + get_ignore_noop(); + key.clear(); + } + } + + return sax->end_object(); + } + + // Note, no reader for UBJSON binary types is implemented because they do + // not exist + + bool get_ubjson_high_precision_number() + { + // get size of following number string + std::size_t size{}; + auto res = get_ubjson_size_value(size); + if (JSON_HEDLEY_UNLIKELY(!res)) + { + return res; + } + + // get number string + std::vector number_vector; + for (std::size_t i = 0; i < size; ++i) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::ubjson, "number"))) + { + return false; + } + number_vector.push_back(static_cast(current)); + } + + // parse number string + using ia_type = decltype(detail::input_adapter(number_vector)); + auto number_lexer = detail::lexer(detail::input_adapter(number_vector), false); + const auto result_number = number_lexer.scan(); + const auto number_string = number_lexer.get_token_string(); + const auto result_remainder = number_lexer.scan(); + + using token_type = typename detail::lexer_base::token_type; + + if (JSON_HEDLEY_UNLIKELY(result_remainder != token_type::end_of_input)) + { + return sax->parse_error(chars_read, number_string, parse_error::create(115, chars_read, exception_message(input_format_t::ubjson, "invalid number text: " + number_lexer.get_token_string(), "high-precision number"), BasicJsonType())); + } + + switch (result_number) + { + case token_type::value_integer: + return sax->number_integer(number_lexer.get_number_integer()); + case token_type::value_unsigned: + return sax->number_unsigned(number_lexer.get_number_unsigned()); + case token_type::value_float: + return sax->number_float(number_lexer.get_number_float(), std::move(number_string)); + case token_type::uninitialized: + case token_type::literal_true: + case token_type::literal_false: + case token_type::literal_null: + case token_type::value_string: + case token_type::begin_array: + case token_type::begin_object: + case token_type::end_array: + case token_type::end_object: + case token_type::name_separator: + case token_type::value_separator: + case token_type::parse_error: + case token_type::end_of_input: + case token_type::literal_or_value: + default: + return sax->parse_error(chars_read, number_string, parse_error::create(115, chars_read, exception_message(input_format_t::ubjson, "invalid number text: " + number_lexer.get_token_string(), "high-precision number"), BasicJsonType())); + } + } + + /////////////////////// + // Utility functions // + /////////////////////// + + /*! + @brief get next character from the input + + This function provides the interface to the used input adapter. It does + not throw in case the input reached EOF, but returns a -'ve valued + `std::char_traits::eof()` in that case. + + @return character read from the input + */ + char_int_type get() + { + ++chars_read; + return current = ia.get_character(); + } + + /*! + @return character read from the input after ignoring all 'N' entries + */ + char_int_type get_ignore_noop() + { + do + { + get(); + } + while (current == 'N'); + + return current; + } + + /* + @brief read a number from the input + + @tparam NumberType the type of the number + @param[in] format the current format (for diagnostics) + @param[out] result number of type @a NumberType + + @return whether conversion completed + + @note This function needs to respect the system's endianess, because + bytes in CBOR, MessagePack, and UBJSON are stored in network order + (big endian) and therefore need reordering on little endian systems. + */ + template + bool get_number(const input_format_t format, NumberType& result) + { + // step 1: read input into array with system's byte order + std::array vec{}; + for (std::size_t i = 0; i < sizeof(NumberType); ++i) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "number"))) + { + return false; + } + + // reverse byte order prior to conversion if necessary + if (is_little_endian != InputIsLittleEndian) + { + vec[sizeof(NumberType) - i - 1] = static_cast(current); + } + else + { + vec[i] = static_cast(current); // LCOV_EXCL_LINE + } + } + + // step 2: convert array into number of type T and return + std::memcpy(&result, vec.data(), sizeof(NumberType)); + return true; + } + + /*! + @brief create a string by reading characters from the input + + @tparam NumberType the type of the number + @param[in] format the current format (for diagnostics) + @param[in] len number of characters to read + @param[out] result string created by reading @a len bytes + + @return whether string creation completed + + @note We can not reserve @a len bytes for the result, because @a len + may be too large. Usually, @ref unexpect_eof() detects the end of + the input before we run out of string memory. + */ + template + bool get_string(const input_format_t format, + const NumberType len, + string_t& result) + { + bool success = true; + for (NumberType i = 0; i < len; i++) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "string"))) + { + success = false; + break; + } + result.push_back(static_cast(current)); + } + return success; + } + + /*! + @brief create a byte array by reading bytes from the input + + @tparam NumberType the type of the number + @param[in] format the current format (for diagnostics) + @param[in] len number of bytes to read + @param[out] result byte array created by reading @a len bytes + + @return whether byte array creation completed + + @note We can not reserve @a len bytes for the result, because @a len + may be too large. Usually, @ref unexpect_eof() detects the end of + the input before we run out of memory. + */ + template + bool get_binary(const input_format_t format, + const NumberType len, + binary_t& result) + { + bool success = true; + for (NumberType i = 0; i < len; i++) + { + get(); + if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(format, "binary"))) + { + success = false; + break; + } + result.push_back(static_cast(current)); + } + return success; + } + + /*! + @param[in] format the current format (for diagnostics) + @param[in] context further context information (for diagnostics) + @return whether the last read character is not EOF + */ + JSON_HEDLEY_NON_NULL(3) + bool unexpect_eof(const input_format_t format, const char* context) const + { + if (JSON_HEDLEY_UNLIKELY(current == std::char_traits::eof())) + { + return sax->parse_error(chars_read, "", + parse_error::create(110, chars_read, exception_message(format, "unexpected end of input", context), BasicJsonType())); + } + return true; + } + + /*! + @return a string representation of the last read byte + */ + std::string get_token_string() const + { + std::array cr{{}}; + (std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(current)); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + return std::string{cr.data()}; + } + + /*! + @param[in] format the current format + @param[in] detail a detailed error message + @param[in] context further context information + @return a message string to use in the parse_error exceptions + */ + std::string exception_message(const input_format_t format, + const std::string& detail, + const std::string& context) const + { + std::string error_msg = "syntax error while parsing "; + + switch (format) + { + case input_format_t::cbor: + error_msg += "CBOR"; + break; + + case input_format_t::msgpack: + error_msg += "MessagePack"; + break; + + case input_format_t::ubjson: + error_msg += "UBJSON"; + break; + + case input_format_t::bson: + error_msg += "BSON"; + break; + + case input_format_t::json: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + + return error_msg + " " + context + ": " + detail; + } + + private: + /// input adapter + InputAdapterType ia; + + /// the current character + char_int_type current = std::char_traits::eof(); + + /// the number of characters read + std::size_t chars_read = 0; + + /// whether we can assume little endianess + const bool is_little_endian = little_endianess(); + + /// the SAX parser + json_sax_t* sax = nullptr; +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + +// #include + + +#include // isfinite +#include // uint8_t +#include // function +#include // string +#include // move +#include // vector + +// #include + +// #include + +// #include + +// #include + +// #include + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +//////////// +// parser // +//////////// + +enum class parse_event_t : std::uint8_t +{ + /// the parser read `{` and started to process a JSON object + object_start, + /// the parser read `}` and finished processing a JSON object + object_end, + /// the parser read `[` and started to process a JSON array + array_start, + /// the parser read `]` and finished processing a JSON array + array_end, + /// the parser read a key of a value in an object + key, + /// the parser finished reading a JSON value + value +}; + +template +using parser_callback_t = + std::function; + +/*! +@brief syntax analysis + +This class implements a recursive descent parser. +*/ +template +class parser +{ + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using number_float_t = typename BasicJsonType::number_float_t; + using string_t = typename BasicJsonType::string_t; + using lexer_t = lexer; + using token_type = typename lexer_t::token_type; + + public: + /// a parser reading from an input adapter + explicit parser(InputAdapterType&& adapter, + const parser_callback_t cb = nullptr, + const bool allow_exceptions_ = true, + const bool skip_comments = false) + : callback(cb) + , m_lexer(std::move(adapter), skip_comments) + , allow_exceptions(allow_exceptions_) + { + // read first token + get_token(); + } + + /*! + @brief public parser interface + + @param[in] strict whether to expect the last token to be EOF + @param[in,out] result parsed JSON value + + @throw parse_error.101 in case of an unexpected token + @throw parse_error.102 if to_unicode fails or surrogate error + @throw parse_error.103 if to_unicode fails + */ + void parse(const bool strict, BasicJsonType& result) + { + if (callback) + { + json_sax_dom_callback_parser sdp(result, callback, allow_exceptions); + sax_parse_internal(&sdp); + + // in strict mode, input must be completely read + if (strict && (get_token() != token_type::end_of_input)) + { + sdp.parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + exception_message(token_type::end_of_input, "value"), BasicJsonType())); + } + + // in case of an error, return discarded value + if (sdp.is_errored()) + { + result = value_t::discarded; + return; + } + + // set top-level value to null if it was discarded by the callback + // function + if (result.is_discarded()) + { + result = nullptr; + } + } + else + { + json_sax_dom_parser sdp(result, allow_exceptions); + sax_parse_internal(&sdp); + + // in strict mode, input must be completely read + if (strict && (get_token() != token_type::end_of_input)) + { + sdp.parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_of_input, "value"), BasicJsonType())); + } + + // in case of an error, return discarded value + if (sdp.is_errored()) + { + result = value_t::discarded; + return; + } + } + + result.assert_invariant(); + } + + /*! + @brief public accept interface + + @param[in] strict whether to expect the last token to be EOF + @return whether the input is a proper JSON text + */ + bool accept(const bool strict = true) + { + json_sax_acceptor sax_acceptor; + return sax_parse(&sax_acceptor, strict); + } + + template + JSON_HEDLEY_NON_NULL(2) + bool sax_parse(SAX* sax, const bool strict = true) + { + (void)detail::is_sax_static_asserts {}; + const bool result = sax_parse_internal(sax); + + // strict mode: next byte must be EOF + if (result && strict && (get_token() != token_type::end_of_input)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_of_input, "value"), BasicJsonType())); + } + + return result; + } + + private: + template + JSON_HEDLEY_NON_NULL(2) + bool sax_parse_internal(SAX* sax) + { + // stack to remember the hierarchy of structured values we are parsing + // true = array; false = object + std::vector states; + // value to avoid a goto (see comment where set to true) + bool skip_to_state_evaluation = false; + + while (true) + { + if (!skip_to_state_evaluation) + { + // invariant: get_token() was called before each iteration + switch (last_token) + { + case token_type::begin_object: + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_object(std::size_t(-1)))) + { + return false; + } + + // closing } -> we are done + if (get_token() == token_type::end_object) + { + if (JSON_HEDLEY_UNLIKELY(!sax->end_object())) + { + return false; + } + break; + } + + // parse key + if (JSON_HEDLEY_UNLIKELY(last_token != token_type::value_string)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::value_string, "object key"), BasicJsonType())); + } + if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string()))) + { + return false; + } + + // parse separator (:) + if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::name_separator)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::name_separator, "object separator"), BasicJsonType())); + } + + // remember we are now inside an object + states.push_back(false); + + // parse values + get_token(); + continue; + } + + case token_type::begin_array: + { + if (JSON_HEDLEY_UNLIKELY(!sax->start_array(std::size_t(-1)))) + { + return false; + } + + // closing ] -> we are done + if (get_token() == token_type::end_array) + { + if (JSON_HEDLEY_UNLIKELY(!sax->end_array())) + { + return false; + } + break; + } + + // remember we are now inside an array + states.push_back(true); + + // parse values (no need to call get_token) + continue; + } + + case token_type::value_float: + { + const auto res = m_lexer.get_number_float(); + + if (JSON_HEDLEY_UNLIKELY(!std::isfinite(res))) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + out_of_range::create(406, "number overflow parsing '" + m_lexer.get_token_string() + "'", BasicJsonType())); + } + + if (JSON_HEDLEY_UNLIKELY(!sax->number_float(res, m_lexer.get_string()))) + { + return false; + } + + break; + } + + case token_type::literal_false: + { + if (JSON_HEDLEY_UNLIKELY(!sax->boolean(false))) + { + return false; + } + break; + } + + case token_type::literal_null: + { + if (JSON_HEDLEY_UNLIKELY(!sax->null())) + { + return false; + } + break; + } + + case token_type::literal_true: + { + if (JSON_HEDLEY_UNLIKELY(!sax->boolean(true))) + { + return false; + } + break; + } + + case token_type::value_integer: + { + if (JSON_HEDLEY_UNLIKELY(!sax->number_integer(m_lexer.get_number_integer()))) + { + return false; + } + break; + } + + case token_type::value_string: + { + if (JSON_HEDLEY_UNLIKELY(!sax->string(m_lexer.get_string()))) + { + return false; + } + break; + } + + case token_type::value_unsigned: + { + if (JSON_HEDLEY_UNLIKELY(!sax->number_unsigned(m_lexer.get_number_unsigned()))) + { + return false; + } + break; + } + + case token_type::parse_error: + { + // using "uninitialized" to avoid "expected" message + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::uninitialized, "value"), BasicJsonType())); + } + + case token_type::uninitialized: + case token_type::end_array: + case token_type::end_object: + case token_type::name_separator: + case token_type::value_separator: + case token_type::end_of_input: + case token_type::literal_or_value: + default: // the last token was unexpected + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), BasicJsonType())); + } + } + } + else + { + skip_to_state_evaluation = false; + } + + // we reached this line after we successfully parsed a value + if (states.empty()) + { + // empty stack: we reached the end of the hierarchy: done + return true; + } + + if (states.back()) // array + { + // comma -> next value + if (get_token() == token_type::value_separator) + { + // parse a new value + get_token(); + continue; + } + + // closing ] + if (JSON_HEDLEY_LIKELY(last_token == token_type::end_array)) + { + if (JSON_HEDLEY_UNLIKELY(!sax->end_array())) + { + return false; + } + + // We are done with this array. Before we can parse a + // new value, we need to evaluate the new state first. + // By setting skip_to_state_evaluation to false, we + // are effectively jumping to the beginning of this if. + JSON_ASSERT(!states.empty()); + states.pop_back(); + skip_to_state_evaluation = true; + continue; + } + + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_array, "array"), BasicJsonType())); + } + + // states.back() is false -> object + + // comma -> next value + if (get_token() == token_type::value_separator) + { + // parse key + if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::value_string)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::value_string, "object key"), BasicJsonType())); + } + + if (JSON_HEDLEY_UNLIKELY(!sax->key(m_lexer.get_string()))) + { + return false; + } + + // parse separator (:) + if (JSON_HEDLEY_UNLIKELY(get_token() != token_type::name_separator)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::name_separator, "object separator"), BasicJsonType())); + } + + // parse values + get_token(); + continue; + } + + // closing } + if (JSON_HEDLEY_LIKELY(last_token == token_type::end_object)) + { + if (JSON_HEDLEY_UNLIKELY(!sax->end_object())) + { + return false; + } + + // We are done with this object. Before we can parse a + // new value, we need to evaluate the new state first. + // By setting skip_to_state_evaluation to false, we + // are effectively jumping to the beginning of this if. + JSON_ASSERT(!states.empty()); + states.pop_back(); + skip_to_state_evaluation = true; + continue; + } + + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::end_object, "object"), BasicJsonType())); + } + } + + /// get next token from lexer + token_type get_token() + { + return last_token = m_lexer.scan(); + } + + std::string exception_message(const token_type expected, const std::string& context) + { + std::string error_msg = "syntax error "; + + if (!context.empty()) + { + error_msg += "while parsing " + context + " "; + } + + error_msg += "- "; + + if (last_token == token_type::parse_error) + { + error_msg += std::string(m_lexer.get_error_message()) + "; last read: '" + + m_lexer.get_token_string() + "'"; + } + else + { + error_msg += "unexpected " + std::string(lexer_t::token_type_name(last_token)); + } + + if (expected != token_type::uninitialized) + { + error_msg += "; expected " + std::string(lexer_t::token_type_name(expected)); + } + + return error_msg; + } + + private: + /// callback function + const parser_callback_t callback = nullptr; + /// the type of the last read token + token_type last_token = token_type::uninitialized; + /// the lexer + lexer_t m_lexer; + /// whether to throw exceptions in case of errors + const bool allow_exceptions = true; +}; + +} // namespace detail +} // namespace nlohmann + +// #include + + +// #include + + +#include // ptrdiff_t +#include // numeric_limits + +// #include + + +namespace nlohmann +{ +namespace detail +{ +/* +@brief an iterator for primitive JSON types + +This class models an iterator for primitive JSON types (boolean, number, +string). It's only purpose is to allow the iterator/const_iterator classes +to "iterate" over primitive values. Internally, the iterator is modeled by +a `difference_type` variable. Value begin_value (`0`) models the begin, +end_value (`1`) models past the end. +*/ +class primitive_iterator_t +{ + private: + using difference_type = std::ptrdiff_t; + static constexpr difference_type begin_value = 0; + static constexpr difference_type end_value = begin_value + 1; + + JSON_PRIVATE_UNLESS_TESTED: + /// iterator as signed integer type + difference_type m_it = (std::numeric_limits::min)(); + + public: + constexpr difference_type get_value() const noexcept + { + return m_it; + } + + /// set iterator to a defined beginning + void set_begin() noexcept + { + m_it = begin_value; + } + + /// set iterator to a defined past the end + void set_end() noexcept + { + m_it = end_value; + } + + /// return whether the iterator can be dereferenced + constexpr bool is_begin() const noexcept + { + return m_it == begin_value; + } + + /// return whether the iterator is at end + constexpr bool is_end() const noexcept + { + return m_it == end_value; + } + + friend constexpr bool operator==(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept + { + return lhs.m_it == rhs.m_it; + } + + friend constexpr bool operator<(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept + { + return lhs.m_it < rhs.m_it; + } + + primitive_iterator_t operator+(difference_type n) noexcept + { + auto result = *this; + result += n; + return result; + } + + friend constexpr difference_type operator-(primitive_iterator_t lhs, primitive_iterator_t rhs) noexcept + { + return lhs.m_it - rhs.m_it; + } + + primitive_iterator_t& operator++() noexcept + { + ++m_it; + return *this; + } + + primitive_iterator_t const operator++(int) noexcept // NOLINT(readability-const-return-type) + { + auto result = *this; + ++m_it; + return result; + } + + primitive_iterator_t& operator--() noexcept + { + --m_it; + return *this; + } + + primitive_iterator_t const operator--(int) noexcept // NOLINT(readability-const-return-type) + { + auto result = *this; + --m_it; + return result; + } + + primitive_iterator_t& operator+=(difference_type n) noexcept + { + m_it += n; + return *this; + } + + primitive_iterator_t& operator-=(difference_type n) noexcept + { + m_it -= n; + return *this; + } +}; +} // namespace detail +} // namespace nlohmann + + +namespace nlohmann +{ +namespace detail +{ +/*! +@brief an iterator value + +@note This structure could easily be a union, but MSVC currently does not allow +unions members with complex constructors, see https://github.com/nlohmann/json/pull/105. +*/ +template struct internal_iterator +{ + /// iterator for JSON objects + typename BasicJsonType::object_t::iterator object_iterator {}; + /// iterator for JSON arrays + typename BasicJsonType::array_t::iterator array_iterator {}; + /// generic iterator for all other types + primitive_iterator_t primitive_iterator {}; +}; +} // namespace detail +} // namespace nlohmann + +// #include + + +#include // iterator, random_access_iterator_tag, bidirectional_iterator_tag, advance, next +#include // conditional, is_const, remove_const + +// #include + +// #include + +// #include + +// #include + +// #include + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +// forward declare, to be able to friend it later on +template class iteration_proxy; +template class iteration_proxy_value; + +/*! +@brief a template for a bidirectional iterator for the @ref basic_json class +This class implements a both iterators (iterator and const_iterator) for the +@ref basic_json class. +@note An iterator is called *initialized* when a pointer to a JSON value has + been set (e.g., by a constructor or a copy assignment). If the iterator is + default-constructed, it is *uninitialized* and most methods are undefined. + **The library uses assertions to detect calls on uninitialized iterators.** +@requirement The class satisfies the following concept requirements: +- +[BidirectionalIterator](https://en.cppreference.com/w/cpp/named_req/BidirectionalIterator): + The iterator that can be moved can be moved in both directions (i.e. + incremented and decremented). +@since version 1.0.0, simplified in version 2.0.9, change to bidirectional + iterators in version 3.0.0 (see https://github.com/nlohmann/json/issues/593) +*/ +template +class iter_impl +{ + /// the iterator with BasicJsonType of different const-ness + using other_iter_impl = iter_impl::value, typename std::remove_const::type, const BasicJsonType>::type>; + /// allow basic_json to access private members + friend other_iter_impl; + friend BasicJsonType; + friend iteration_proxy; + friend iteration_proxy_value; + + using object_t = typename BasicJsonType::object_t; + using array_t = typename BasicJsonType::array_t; + // make sure BasicJsonType is basic_json or const basic_json + static_assert(is_basic_json::type>::value, + "iter_impl only accepts (const) basic_json"); + + public: + + /// The std::iterator class template (used as a base class to provide typedefs) is deprecated in C++17. + /// The C++ Standard has never required user-defined iterators to derive from std::iterator. + /// A user-defined iterator should provide publicly accessible typedefs named + /// iterator_category, value_type, difference_type, pointer, and reference. + /// Note that value_type is required to be non-const, even for constant iterators. + using iterator_category = std::bidirectional_iterator_tag; + + /// the type of the values when the iterator is dereferenced + using value_type = typename BasicJsonType::value_type; + /// a type to represent differences between iterators + using difference_type = typename BasicJsonType::difference_type; + /// defines a pointer to the type iterated over (value_type) + using pointer = typename std::conditional::value, + typename BasicJsonType::const_pointer, + typename BasicJsonType::pointer>::type; + /// defines a reference to the type iterated over (value_type) + using reference = + typename std::conditional::value, + typename BasicJsonType::const_reference, + typename BasicJsonType::reference>::type; + + iter_impl() = default; + ~iter_impl() = default; + iter_impl(iter_impl&&) noexcept = default; + iter_impl& operator=(iter_impl&&) noexcept = default; + + /*! + @brief constructor for a given JSON instance + @param[in] object pointer to a JSON object for this iterator + @pre object != nullptr + @post The iterator is initialized; i.e. `m_object != nullptr`. + */ + explicit iter_impl(pointer object) noexcept : m_object(object) + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + m_it.object_iterator = typename object_t::iterator(); + break; + } + + case value_t::array: + { + m_it.array_iterator = typename array_t::iterator(); + break; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + m_it.primitive_iterator = primitive_iterator_t(); + break; + } + } + } + + /*! + @note The conventional copy constructor and copy assignment are implicitly + defined. Combined with the following converting constructor and + assignment, they support: (1) copy from iterator to iterator, (2) + copy from const iterator to const iterator, and (3) conversion from + iterator to const iterator. However conversion from const iterator + to iterator is not defined. + */ + + /*! + @brief const copy constructor + @param[in] other const iterator to copy from + @note This copy constructor had to be defined explicitly to circumvent a bug + occurring on msvc v19.0 compiler (VS 2015) debug build. For more + information refer to: https://github.com/nlohmann/json/issues/1608 + */ + iter_impl(const iter_impl& other) noexcept + : m_object(other.m_object), m_it(other.m_it) + {} + + /*! + @brief converting assignment + @param[in] other const iterator to copy from + @return const/non-const iterator + @note It is not checked whether @a other is initialized. + */ + iter_impl& operator=(const iter_impl& other) noexcept + { + if (&other != this) + { + m_object = other.m_object; + m_it = other.m_it; + } + return *this; + } + + /*! + @brief converting constructor + @param[in] other non-const iterator to copy from + @note It is not checked whether @a other is initialized. + */ + iter_impl(const iter_impl::type>& other) noexcept + : m_object(other.m_object), m_it(other.m_it) + {} + + /*! + @brief converting assignment + @param[in] other non-const iterator to copy from + @return const/non-const iterator + @note It is not checked whether @a other is initialized. + */ + iter_impl& operator=(const iter_impl::type>& other) noexcept // NOLINT(cert-oop54-cpp) + { + m_object = other.m_object; + m_it = other.m_it; + return *this; + } + + JSON_PRIVATE_UNLESS_TESTED: + /*! + @brief set the iterator to the first value + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + void set_begin() noexcept + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + m_it.object_iterator = m_object->m_value.object->begin(); + break; + } + + case value_t::array: + { + m_it.array_iterator = m_object->m_value.array->begin(); + break; + } + + case value_t::null: + { + // set to end so begin()==end() is true: null is empty + m_it.primitive_iterator.set_end(); + break; + } + + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + m_it.primitive_iterator.set_begin(); + break; + } + } + } + + /*! + @brief set the iterator past the last value + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + void set_end() noexcept + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + m_it.object_iterator = m_object->m_value.object->end(); + break; + } + + case value_t::array: + { + m_it.array_iterator = m_object->m_value.array->end(); + break; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + m_it.primitive_iterator.set_end(); + break; + } + } + } + + public: + /*! + @brief return a reference to the value pointed to by the iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + reference operator*() const + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + JSON_ASSERT(m_it.object_iterator != m_object->m_value.object->end()); + return m_it.object_iterator->second; + } + + case value_t::array: + { + JSON_ASSERT(m_it.array_iterator != m_object->m_value.array->end()); + return *m_it.array_iterator; + } + + case value_t::null: + JSON_THROW(invalid_iterator::create(214, "cannot get value", *m_object)); + + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) + { + return *m_object; + } + + JSON_THROW(invalid_iterator::create(214, "cannot get value", *m_object)); + } + } + } + + /*! + @brief dereference the iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + pointer operator->() const + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + JSON_ASSERT(m_it.object_iterator != m_object->m_value.object->end()); + return &(m_it.object_iterator->second); + } + + case value_t::array: + { + JSON_ASSERT(m_it.array_iterator != m_object->m_value.array->end()); + return &*m_it.array_iterator; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.is_begin())) + { + return m_object; + } + + JSON_THROW(invalid_iterator::create(214, "cannot get value", *m_object)); + } + } + } + + /*! + @brief post-increment (it++) + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl const operator++(int) // NOLINT(readability-const-return-type) + { + auto result = *this; + ++(*this); + return result; + } + + /*! + @brief pre-increment (++it) + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl& operator++() + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + std::advance(m_it.object_iterator, 1); + break; + } + + case value_t::array: + { + std::advance(m_it.array_iterator, 1); + break; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + ++m_it.primitive_iterator; + break; + } + } + + return *this; + } + + /*! + @brief post-decrement (it--) + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl const operator--(int) // NOLINT(readability-const-return-type) + { + auto result = *this; + --(*this); + return result; + } + + /*! + @brief pre-decrement (--it) + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl& operator--() + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + { + std::advance(m_it.object_iterator, -1); + break; + } + + case value_t::array: + { + std::advance(m_it.array_iterator, -1); + break; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + --m_it.primitive_iterator; + break; + } + } + + return *this; + } + + /*! + @brief comparison: equal + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + template < typename IterImpl, detail::enable_if_t < (std::is_same::value || std::is_same::value), std::nullptr_t > = nullptr > + bool operator==(const IterImpl& other) const + { + // if objects are not the same, the comparison is undefined + if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) + { + JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers", *m_object)); + } + + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + return (m_it.object_iterator == other.m_it.object_iterator); + + case value_t::array: + return (m_it.array_iterator == other.m_it.array_iterator); + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return (m_it.primitive_iterator == other.m_it.primitive_iterator); + } + } + + /*! + @brief comparison: not equal + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + template < typename IterImpl, detail::enable_if_t < (std::is_same::value || std::is_same::value), std::nullptr_t > = nullptr > + bool operator!=(const IterImpl& other) const + { + return !operator==(other); + } + + /*! + @brief comparison: smaller + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator<(const iter_impl& other) const + { + // if objects are not the same, the comparison is undefined + if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object)) + { + JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers", *m_object)); + } + + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + JSON_THROW(invalid_iterator::create(213, "cannot compare order of object iterators", *m_object)); + + case value_t::array: + return (m_it.array_iterator < other.m_it.array_iterator); + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return (m_it.primitive_iterator < other.m_it.primitive_iterator); + } + } + + /*! + @brief comparison: less than or equal + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator<=(const iter_impl& other) const + { + return !other.operator < (*this); + } + + /*! + @brief comparison: greater than + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator>(const iter_impl& other) const + { + return !operator<=(other); + } + + /*! + @brief comparison: greater than or equal + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + bool operator>=(const iter_impl& other) const + { + return !operator<(other); + } + + /*! + @brief add to iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl& operator+=(difference_type i) + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators", *m_object)); + + case value_t::array: + { + std::advance(m_it.array_iterator, i); + break; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + m_it.primitive_iterator += i; + break; + } + } + + return *this; + } + + /*! + @brief subtract from iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl& operator-=(difference_type i) + { + return operator+=(-i); + } + + /*! + @brief add to iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl operator+(difference_type i) const + { + auto result = *this; + result += i; + return result; + } + + /*! + @brief addition of distance and iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + friend iter_impl operator+(difference_type i, const iter_impl& it) + { + auto result = it; + result += i; + return result; + } + + /*! + @brief subtract from iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + iter_impl operator-(difference_type i) const + { + auto result = *this; + result -= i; + return result; + } + + /*! + @brief return difference + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + difference_type operator-(const iter_impl& other) const + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators", *m_object)); + + case value_t::array: + return m_it.array_iterator - other.m_it.array_iterator; + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return m_it.primitive_iterator - other.m_it.primitive_iterator; + } + } + + /*! + @brief access to successor + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + reference operator[](difference_type n) const + { + JSON_ASSERT(m_object != nullptr); + + switch (m_object->m_type) + { + case value_t::object: + JSON_THROW(invalid_iterator::create(208, "cannot use operator[] for object iterators", *m_object)); + + case value_t::array: + return *std::next(m_it.array_iterator, n); + + case value_t::null: + JSON_THROW(invalid_iterator::create(214, "cannot get value", *m_object)); + + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + if (JSON_HEDLEY_LIKELY(m_it.primitive_iterator.get_value() == -n)) + { + return *m_object; + } + + JSON_THROW(invalid_iterator::create(214, "cannot get value", *m_object)); + } + } + } + + /*! + @brief return the key of an object iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + const typename object_t::key_type& key() const + { + JSON_ASSERT(m_object != nullptr); + + if (JSON_HEDLEY_LIKELY(m_object->is_object())) + { + return m_it.object_iterator->first; + } + + JSON_THROW(invalid_iterator::create(207, "cannot use key() for non-object iterators", *m_object)); + } + + /*! + @brief return the value of an iterator + @pre The iterator is initialized; i.e. `m_object != nullptr`. + */ + reference value() const + { + return operator*(); + } + + JSON_PRIVATE_UNLESS_TESTED: + /// associated JSON instance + pointer m_object = nullptr; + /// the actual iterator of the associated instance + internal_iterator::type> m_it {}; +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + + +#include // ptrdiff_t +#include // reverse_iterator +#include // declval + +namespace nlohmann +{ +namespace detail +{ +////////////////////// +// reverse_iterator // +////////////////////// + +/*! +@brief a template for a reverse iterator class + +@tparam Base the base iterator type to reverse. Valid types are @ref +iterator (to create @ref reverse_iterator) and @ref const_iterator (to +create @ref const_reverse_iterator). + +@requirement The class satisfies the following concept requirements: +- +[BidirectionalIterator](https://en.cppreference.com/w/cpp/named_req/BidirectionalIterator): + The iterator that can be moved can be moved in both directions (i.e. + incremented and decremented). +- [OutputIterator](https://en.cppreference.com/w/cpp/named_req/OutputIterator): + It is possible to write to the pointed-to element (only if @a Base is + @ref iterator). + +@since version 1.0.0 +*/ +template +class json_reverse_iterator : public std::reverse_iterator +{ + public: + using difference_type = std::ptrdiff_t; + /// shortcut to the reverse iterator adapter + using base_iterator = std::reverse_iterator; + /// the reference type for the pointed-to element + using reference = typename Base::reference; + + /// create reverse iterator from iterator + explicit json_reverse_iterator(const typename base_iterator::iterator_type& it) noexcept + : base_iterator(it) {} + + /// create reverse iterator from base class + explicit json_reverse_iterator(const base_iterator& it) noexcept : base_iterator(it) {} + + /// post-increment (it++) + json_reverse_iterator const operator++(int) // NOLINT(readability-const-return-type) + { + return static_cast(base_iterator::operator++(1)); + } + + /// pre-increment (++it) + json_reverse_iterator& operator++() + { + return static_cast(base_iterator::operator++()); + } + + /// post-decrement (it--) + json_reverse_iterator const operator--(int) // NOLINT(readability-const-return-type) + { + return static_cast(base_iterator::operator--(1)); + } + + /// pre-decrement (--it) + json_reverse_iterator& operator--() + { + return static_cast(base_iterator::operator--()); + } + + /// add to iterator + json_reverse_iterator& operator+=(difference_type i) + { + return static_cast(base_iterator::operator+=(i)); + } + + /// add to iterator + json_reverse_iterator operator+(difference_type i) const + { + return static_cast(base_iterator::operator+(i)); + } + + /// subtract from iterator + json_reverse_iterator operator-(difference_type i) const + { + return static_cast(base_iterator::operator-(i)); + } + + /// return difference + difference_type operator-(const json_reverse_iterator& other) const + { + return base_iterator(*this) - base_iterator(other); + } + + /// access to successor + reference operator[](difference_type n) const + { + return *(this->operator+(n)); + } + + /// return the key of an object iterator + auto key() const -> decltype(std::declval().key()) + { + auto it = --this->base(); + return it.key(); + } + + /// return the value of an iterator + reference value() const + { + auto it = --this->base(); + return it.operator * (); + } +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + + +#include // all_of +#include // isdigit +#include // max +#include // accumulate +#include // string +#include // move +#include // vector + +// #include + +// #include + +// #include + +// #include + + +namespace nlohmann +{ +template +class json_pointer +{ + // allow basic_json to access private members + NLOHMANN_BASIC_JSON_TPL_DECLARATION + friend class basic_json; + + public: + /*! + @brief create JSON pointer + + Create a JSON pointer according to the syntax described in + [Section 3 of RFC6901](https://tools.ietf.org/html/rfc6901#section-3). + + @param[in] s string representing the JSON pointer; if omitted, the empty + string is assumed which references the whole JSON value + + @throw parse_error.107 if the given JSON pointer @a s is nonempty and does + not begin with a slash (`/`); see example below + + @throw parse_error.108 if a tilde (`~`) in the given JSON pointer @a s is + not followed by `0` (representing `~`) or `1` (representing `/`); see + example below + + @liveexample{The example shows the construction several valid JSON pointers + as well as the exceptional behavior.,json_pointer} + + @since version 2.0.0 + */ + explicit json_pointer(const std::string& s = "") + : reference_tokens(split(s)) + {} + + /*! + @brief return a string representation of the JSON pointer + + @invariant For each JSON pointer `ptr`, it holds: + @code {.cpp} + ptr == json_pointer(ptr.to_string()); + @endcode + + @return a string representation of the JSON pointer + + @liveexample{The example shows the result of `to_string`.,json_pointer__to_string} + + @since version 2.0.0 + */ + std::string to_string() const + { + return std::accumulate(reference_tokens.begin(), reference_tokens.end(), + std::string{}, + [](const std::string & a, const std::string & b) + { + return a + "/" + detail::escape(b); + }); + } + + /// @copydoc to_string() + operator std::string() const + { + return to_string(); + } + + /*! + @brief append another JSON pointer at the end of this JSON pointer + + @param[in] ptr JSON pointer to append + @return JSON pointer with @a ptr appended + + @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add} + + @complexity Linear in the length of @a ptr. + + @sa see @ref operator/=(std::string) to append a reference token + @sa see @ref operator/=(std::size_t) to append an array index + @sa see @ref operator/(const json_pointer&, const json_pointer&) for a binary operator + + @since version 3.6.0 + */ + json_pointer& operator/=(const json_pointer& ptr) + { + reference_tokens.insert(reference_tokens.end(), + ptr.reference_tokens.begin(), + ptr.reference_tokens.end()); + return *this; + } + + /*! + @brief append an unescaped reference token at the end of this JSON pointer + + @param[in] token reference token to append + @return JSON pointer with @a token appended without escaping @a token + + @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add} + + @complexity Amortized constant. + + @sa see @ref operator/=(const json_pointer&) to append a JSON pointer + @sa see @ref operator/=(std::size_t) to append an array index + @sa see @ref operator/(const json_pointer&, std::size_t) for a binary operator + + @since version 3.6.0 + */ + json_pointer& operator/=(std::string token) + { + push_back(std::move(token)); + return *this; + } + + /*! + @brief append an array index at the end of this JSON pointer + + @param[in] array_idx array index to append + @return JSON pointer with @a array_idx appended + + @liveexample{The example shows the usage of `operator/=`.,json_pointer__operator_add} + + @complexity Amortized constant. + + @sa see @ref operator/=(const json_pointer&) to append a JSON pointer + @sa see @ref operator/=(std::string) to append a reference token + @sa see @ref operator/(const json_pointer&, std::string) for a binary operator + + @since version 3.6.0 + */ + json_pointer& operator/=(std::size_t array_idx) + { + return *this /= std::to_string(array_idx); + } + + /*! + @brief create a new JSON pointer by appending the right JSON pointer at the end of the left JSON pointer + + @param[in] lhs JSON pointer + @param[in] rhs JSON pointer + @return a new JSON pointer with @a rhs appended to @a lhs + + @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary} + + @complexity Linear in the length of @a lhs and @a rhs. + + @sa see @ref operator/=(const json_pointer&) to append a JSON pointer + + @since version 3.6.0 + */ + friend json_pointer operator/(const json_pointer& lhs, + const json_pointer& rhs) + { + return json_pointer(lhs) /= rhs; + } + + /*! + @brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer + + @param[in] ptr JSON pointer + @param[in] token reference token + @return a new JSON pointer with unescaped @a token appended to @a ptr + + @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary} + + @complexity Linear in the length of @a ptr. + + @sa see @ref operator/=(std::string) to append a reference token + + @since version 3.6.0 + */ + friend json_pointer operator/(const json_pointer& ptr, std::string token) // NOLINT(performance-unnecessary-value-param) + { + return json_pointer(ptr) /= std::move(token); + } + + /*! + @brief create a new JSON pointer by appending the array-index-token at the end of the JSON pointer + + @param[in] ptr JSON pointer + @param[in] array_idx array index + @return a new JSON pointer with @a array_idx appended to @a ptr + + @liveexample{The example shows the usage of `operator/`.,json_pointer__operator_add_binary} + + @complexity Linear in the length of @a ptr. + + @sa see @ref operator/=(std::size_t) to append an array index + + @since version 3.6.0 + */ + friend json_pointer operator/(const json_pointer& ptr, std::size_t array_idx) + { + return json_pointer(ptr) /= array_idx; + } + + /*! + @brief returns the parent of this JSON pointer + + @return parent of this JSON pointer; in case this JSON pointer is the root, + the root itself is returned + + @complexity Linear in the length of the JSON pointer. + + @liveexample{The example shows the result of `parent_pointer` for different + JSON Pointers.,json_pointer__parent_pointer} + + @since version 3.6.0 + */ + json_pointer parent_pointer() const + { + if (empty()) + { + return *this; + } + + json_pointer res = *this; + res.pop_back(); + return res; + } + + /*! + @brief remove last reference token + + @pre not `empty()` + + @liveexample{The example shows the usage of `pop_back`.,json_pointer__pop_back} + + @complexity Constant. + + @throw out_of_range.405 if JSON pointer has no parent + + @since version 3.6.0 + */ + void pop_back() + { + if (JSON_HEDLEY_UNLIKELY(empty())) + { + JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", BasicJsonType())); + } + + reference_tokens.pop_back(); + } + + /*! + @brief return last reference token + + @pre not `empty()` + @return last reference token + + @liveexample{The example shows the usage of `back`.,json_pointer__back} + + @complexity Constant. + + @throw out_of_range.405 if JSON pointer has no parent + + @since version 3.6.0 + */ + const std::string& back() const + { + if (JSON_HEDLEY_UNLIKELY(empty())) + { + JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", BasicJsonType())); + } + + return reference_tokens.back(); + } + + /*! + @brief append an unescaped token at the end of the reference pointer + + @param[in] token token to add + + @complexity Amortized constant. + + @liveexample{The example shows the result of `push_back` for different + JSON Pointers.,json_pointer__push_back} + + @since version 3.6.0 + */ + void push_back(const std::string& token) + { + reference_tokens.push_back(token); + } + + /// @copydoc push_back(const std::string&) + void push_back(std::string&& token) + { + reference_tokens.push_back(std::move(token)); + } + + /*! + @brief return whether pointer points to the root document + + @return true iff the JSON pointer points to the root document + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @liveexample{The example shows the result of `empty` for different JSON + Pointers.,json_pointer__empty} + + @since version 3.6.0 + */ + bool empty() const noexcept + { + return reference_tokens.empty(); + } + + private: + /*! + @param[in] s reference token to be converted into an array index + + @return integer representation of @a s + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index begins not with a digit + @throw out_of_range.404 if string @a s could not be converted to an integer + @throw out_of_range.410 if an array index exceeds size_type + */ + static typename BasicJsonType::size_type array_index(const std::string& s) + { + using size_type = typename BasicJsonType::size_type; + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && s[0] == '0')) + { + JSON_THROW(detail::parse_error::create(106, 0, "array index '" + s + "' must not begin with '0'", BasicJsonType())); + } + + // error condition (cf. RFC 6901, Sect. 4) + if (JSON_HEDLEY_UNLIKELY(s.size() > 1 && !(s[0] >= '1' && s[0] <= '9'))) + { + JSON_THROW(detail::parse_error::create(109, 0, "array index '" + s + "' is not a number", BasicJsonType())); + } + + std::size_t processed_chars = 0; + unsigned long long res = 0; // NOLINT(runtime/int) + JSON_TRY + { + res = std::stoull(s, &processed_chars); + } + JSON_CATCH(std::out_of_range&) + { + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'", BasicJsonType())); + } + + // check if the string was completely read + if (JSON_HEDLEY_UNLIKELY(processed_chars != s.size())) + { + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + s + "'", BasicJsonType())); + } + + // only triggered on special platforms (like 32bit), see also + // https://github.com/nlohmann/json/pull/2203 + if (res >= static_cast((std::numeric_limits::max)())) // NOLINT(runtime/int) + { + JSON_THROW(detail::out_of_range::create(410, "array index " + s + " exceeds size_type", BasicJsonType())); // LCOV_EXCL_LINE + } + + return static_cast(res); + } + + JSON_PRIVATE_UNLESS_TESTED: + json_pointer top() const + { + if (JSON_HEDLEY_UNLIKELY(empty())) + { + JSON_THROW(detail::out_of_range::create(405, "JSON pointer has no parent", BasicJsonType())); + } + + json_pointer result = *this; + result.reference_tokens = {reference_tokens[0]}; + return result; + } + + private: + /*! + @brief create and return a reference to the pointed to value + + @complexity Linear in the number of reference tokens. + + @throw parse_error.109 if array index is not a number + @throw type_error.313 if value cannot be unflattened + */ + BasicJsonType& get_and_create(BasicJsonType& j) const + { + auto* result = &j; + + // in case no reference tokens exist, return a reference to the JSON value + // j which will be overwritten by a primitive value + for (const auto& reference_token : reference_tokens) + { + switch (result->type()) + { + case detail::value_t::null: + { + if (reference_token == "0") + { + // start a new array if reference token is 0 + result = &result->operator[](0); + } + else + { + // start a new object otherwise + result = &result->operator[](reference_token); + } + break; + } + + case detail::value_t::object: + { + // create an entry in the object + result = &result->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + // create an entry in the array + result = &result->operator[](array_index(reference_token)); + break; + } + + /* + The following code is only reached if there exists a reference + token _and_ the current value is primitive. In this case, we have + an error situation, because primitive values may only occur as + single value; that is, with an empty list of reference tokens. + */ + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", j)); + } + } + + return *result; + } + + /*! + @brief return a reference to the pointed to value + + @note This version does not throw if a value is not present, but tries to + create nested values instead. For instance, calling this function + with pointer `"/this/that"` on a null value is equivalent to calling + `operator[]("this").operator[]("that")` on that value, effectively + changing the null value to an object. + + @param[in] ptr a JSON value + + @return reference to the JSON value pointed to by the JSON pointer + + @complexity Linear in the length of the JSON pointer. + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.404 if the JSON pointer can not be resolved + */ + BasicJsonType& get_unchecked(BasicJsonType* ptr) const + { + for (const auto& reference_token : reference_tokens) + { + // convert null values to arrays or objects before continuing + if (ptr->is_null()) + { + // check if reference token is a number + const bool nums = + std::all_of(reference_token.begin(), reference_token.end(), + [](const unsigned char x) + { + return std::isdigit(x); + }); + + // change value to array for numbers or "-" or to object otherwise + *ptr = (nums || reference_token == "-") + ? detail::value_t::array + : detail::value_t::object; + } + + switch (ptr->type()) + { + case detail::value_t::object: + { + // use unchecked object access + ptr = &ptr->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + if (reference_token == "-") + { + // explicitly treat "-" as index beyond the end + ptr = &ptr->operator[](ptr->m_value.array->size()); + } + else + { + // convert array index to number; unchecked access + ptr = &ptr->operator[](array_index(reference_token)); + } + break; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'", *ptr)); + } + } + + return *ptr; + } + + /*! + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.402 if the array index '-' is used + @throw out_of_range.404 if the JSON pointer can not be resolved + */ + BasicJsonType& get_checked(BasicJsonType* ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + // note: at performs range check + ptr = &ptr->at(reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + JSON_THROW(detail::out_of_range::create(402, + "array index '-' (" + std::to_string(ptr->m_value.array->size()) + + ") is out of range", *ptr)); + } + + // note: at performs range check + ptr = &ptr->at(array_index(reference_token)); + break; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'", *ptr)); + } + } + + return *ptr; + } + + /*! + @brief return a const reference to the pointed to value + + @param[in] ptr a JSON value + + @return const reference to the JSON value pointed to by the JSON + pointer + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.402 if the array index '-' is used + @throw out_of_range.404 if the JSON pointer can not be resolved + */ + const BasicJsonType& get_unchecked(const BasicJsonType* ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + // use unchecked object access + ptr = &ptr->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" cannot be used for const access + JSON_THROW(detail::out_of_range::create(402, "array index '-' (" + std::to_string(ptr->m_value.array->size()) + ") is out of range", *ptr)); + } + + // use unchecked array access + ptr = &ptr->operator[](array_index(reference_token)); + break; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'", *ptr)); + } + } + + return *ptr; + } + + /*! + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.402 if the array index '-' is used + @throw out_of_range.404 if the JSON pointer can not be resolved + */ + const BasicJsonType& get_checked(const BasicJsonType* ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + // note: at performs range check + ptr = &ptr->at(reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + JSON_THROW(detail::out_of_range::create(402, + "array index '-' (" + std::to_string(ptr->m_value.array->size()) + + ") is out of range", *ptr)); + } + + // note: at performs range check + ptr = &ptr->at(array_index(reference_token)); + break; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + JSON_THROW(detail::out_of_range::create(404, "unresolved reference token '" + reference_token + "'", *ptr)); + } + } + + return *ptr; + } + + /*! + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + */ + bool contains(const BasicJsonType* ptr) const + { + for (const auto& reference_token : reference_tokens) + { + switch (ptr->type()) + { + case detail::value_t::object: + { + if (!ptr->contains(reference_token)) + { + // we did not find the key in the object + return false; + } + + ptr = &ptr->operator[](reference_token); + break; + } + + case detail::value_t::array: + { + if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) + { + // "-" always fails the range check + return false; + } + if (JSON_HEDLEY_UNLIKELY(reference_token.size() == 1 && !("0" <= reference_token && reference_token <= "9"))) + { + // invalid char + return false; + } + if (JSON_HEDLEY_UNLIKELY(reference_token.size() > 1)) + { + if (JSON_HEDLEY_UNLIKELY(!('1' <= reference_token[0] && reference_token[0] <= '9'))) + { + // first char should be between '1' and '9' + return false; + } + for (std::size_t i = 1; i < reference_token.size(); i++) + { + if (JSON_HEDLEY_UNLIKELY(!('0' <= reference_token[i] && reference_token[i] <= '9'))) + { + // other char should be between '0' and '9' + return false; + } + } + } + + const auto idx = array_index(reference_token); + if (idx >= ptr->size()) + { + // index out of range + return false; + } + + ptr = &ptr->operator[](idx); + break; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + { + // we do not expect primitive values if there is still a + // reference token to process + return false; + } + } + } + + // no reference token left means we found a primitive value + return true; + } + + /*! + @brief split the string input to reference tokens + + @note This function is only called by the json_pointer constructor. + All exceptions below are documented there. + + @throw parse_error.107 if the pointer is not empty or begins with '/' + @throw parse_error.108 if character '~' is not followed by '0' or '1' + */ + static std::vector split(const std::string& reference_string) + { + std::vector result; + + // special case: empty reference string -> no reference tokens + if (reference_string.empty()) + { + return result; + } + + // check if nonempty reference string begins with slash + if (JSON_HEDLEY_UNLIKELY(reference_string[0] != '/')) + { + JSON_THROW(detail::parse_error::create(107, 1, "JSON pointer must be empty or begin with '/' - was: '" + reference_string + "'", BasicJsonType())); + } + + // extract the reference tokens: + // - slash: position of the last read slash (or end of string) + // - start: position after the previous slash + for ( + // search for the first slash after the first character + std::size_t slash = reference_string.find_first_of('/', 1), + // set the beginning of the first reference token + start = 1; + // we can stop if start == 0 (if slash == std::string::npos) + start != 0; + // set the beginning of the next reference token + // (will eventually be 0 if slash == std::string::npos) + start = (slash == std::string::npos) ? 0 : slash + 1, + // find next slash + slash = reference_string.find_first_of('/', start)) + { + // use the text between the beginning of the reference token + // (start) and the last slash (slash). + auto reference_token = reference_string.substr(start, slash - start); + + // check reference tokens are properly escaped + for (std::size_t pos = reference_token.find_first_of('~'); + pos != std::string::npos; + pos = reference_token.find_first_of('~', pos + 1)) + { + JSON_ASSERT(reference_token[pos] == '~'); + + // ~ must be followed by 0 or 1 + if (JSON_HEDLEY_UNLIKELY(pos == reference_token.size() - 1 || + (reference_token[pos + 1] != '0' && + reference_token[pos + 1] != '1'))) + { + JSON_THROW(detail::parse_error::create(108, 0, "escape character '~' must be followed with '0' or '1'", BasicJsonType())); + } + } + + // finally, store the reference token + detail::unescape(reference_token); + result.push_back(reference_token); + } + + return result; + } + + private: + /*! + @param[in] reference_string the reference string to the current value + @param[in] value the value to consider + @param[in,out] result the result object to insert values to + + @note Empty objects or arrays are flattened to `null`. + */ + static void flatten(const std::string& reference_string, + const BasicJsonType& value, + BasicJsonType& result) + { + switch (value.type()) + { + case detail::value_t::array: + { + if (value.m_value.array->empty()) + { + // flatten empty array as null + result[reference_string] = nullptr; + } + else + { + // iterate array and use index as reference string + for (std::size_t i = 0; i < value.m_value.array->size(); ++i) + { + flatten(reference_string + "/" + std::to_string(i), + value.m_value.array->operator[](i), result); + } + } + break; + } + + case detail::value_t::object: + { + if (value.m_value.object->empty()) + { + // flatten empty object as null + result[reference_string] = nullptr; + } + else + { + // iterate object and use keys as reference string + for (const auto& element : *value.m_value.object) + { + flatten(reference_string + "/" + detail::escape(element.first), element.second, result); + } + } + break; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + { + // add primitive value with its reference string + result[reference_string] = value; + break; + } + } + } + + /*! + @param[in] value flattened JSON + + @return unflattened JSON + + @throw parse_error.109 if array index is not a number + @throw type_error.314 if value is not an object + @throw type_error.315 if object values are not primitive + @throw type_error.313 if value cannot be unflattened + */ + static BasicJsonType + unflatten(const BasicJsonType& value) + { + if (JSON_HEDLEY_UNLIKELY(!value.is_object())) + { + JSON_THROW(detail::type_error::create(314, "only objects can be unflattened", value)); + } + + BasicJsonType result; + + // iterate the JSON object values + for (const auto& element : *value.m_value.object) + { + if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive())) + { + JSON_THROW(detail::type_error::create(315, "values in object must be primitive", element.second)); + } + + // assign value to reference pointed to by JSON pointer; Note that if + // the JSON pointer is "" (i.e., points to the whole value), function + // get_and_create returns a reference to result itself. An assignment + // will then create a primitive value. + json_pointer(element.first).get_and_create(result) = element.second; + } + + return result; + } + + /*! + @brief compares two JSON pointers for equality + + @param[in] lhs JSON pointer to compare + @param[in] rhs JSON pointer to compare + @return whether @a lhs is equal to @a rhs + + @complexity Linear in the length of the JSON pointer + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + */ + friend bool operator==(json_pointer const& lhs, + json_pointer const& rhs) noexcept + { + return lhs.reference_tokens == rhs.reference_tokens; + } + + /*! + @brief compares two JSON pointers for inequality + + @param[in] lhs JSON pointer to compare + @param[in] rhs JSON pointer to compare + @return whether @a lhs is not equal @a rhs + + @complexity Linear in the length of the JSON pointer + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + */ + friend bool operator!=(json_pointer const& lhs, + json_pointer const& rhs) noexcept + { + return !(lhs == rhs); + } + + /// the reference tokens + std::vector reference_tokens; +}; +} // namespace nlohmann + +// #include + + +#include +#include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +template +class json_ref +{ + public: + using value_type = BasicJsonType; + + json_ref(value_type&& value) + : owned_value(std::move(value)) + {} + + json_ref(const value_type& value) + : value_ref(&value) + {} + + json_ref(std::initializer_list init) + : owned_value(init) + {} + + template < + class... Args, + enable_if_t::value, int> = 0 > + json_ref(Args && ... args) + : owned_value(std::forward(args)...) + {} + + // class should be movable only + json_ref(json_ref&&) noexcept = default; + json_ref(const json_ref&) = delete; + json_ref& operator=(const json_ref&) = delete; + json_ref& operator=(json_ref&&) = delete; + ~json_ref() = default; + + value_type moved_or_copied() const + { + if (value_ref == nullptr) + { + return std::move(owned_value); + } + return *value_ref; + } + + value_type const& operator*() const + { + return value_ref ? *value_ref : owned_value; + } + + value_type const* operator->() const + { + return &** this; + } + + private: + mutable value_type owned_value = nullptr; + value_type const* value_ref = nullptr; +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + +// #include + +// #include + +// #include + + +#include // reverse +#include // array +#include // isnan, isinf +#include // uint8_t, uint16_t, uint32_t, uint64_t +#include // memcpy +#include // numeric_limits +#include // string +#include // move + +// #include + +// #include + +// #include + + +#include // copy +#include // size_t +#include // back_inserter +#include // shared_ptr, make_shared +#include // basic_string +#include // vector + +#ifndef JSON_NO_IO + #include // streamsize + #include // basic_ostream +#endif // JSON_NO_IO + +// #include + + +namespace nlohmann +{ +namespace detail +{ +/// abstract output adapter interface +template struct output_adapter_protocol +{ + virtual void write_character(CharType c) = 0; + virtual void write_characters(const CharType* s, std::size_t length) = 0; + virtual ~output_adapter_protocol() = default; + + output_adapter_protocol() = default; + output_adapter_protocol(const output_adapter_protocol&) = default; + output_adapter_protocol(output_adapter_protocol&&) noexcept = default; + output_adapter_protocol& operator=(const output_adapter_protocol&) = default; + output_adapter_protocol& operator=(output_adapter_protocol&&) noexcept = default; +}; + +/// a type to simplify interfaces +template +using output_adapter_t = std::shared_ptr>; + +/// output adapter for byte vectors +template> +class output_vector_adapter : public output_adapter_protocol +{ + public: + explicit output_vector_adapter(std::vector& vec) noexcept + : v(vec) + {} + + void write_character(CharType c) override + { + v.push_back(c); + } + + JSON_HEDLEY_NON_NULL(2) + void write_characters(const CharType* s, std::size_t length) override + { + std::copy(s, s + length, std::back_inserter(v)); + } + + private: + std::vector& v; +}; + +#ifndef JSON_NO_IO +/// output adapter for output streams +template +class output_stream_adapter : public output_adapter_protocol +{ + public: + explicit output_stream_adapter(std::basic_ostream& s) noexcept + : stream(s) + {} + + void write_character(CharType c) override + { + stream.put(c); + } + + JSON_HEDLEY_NON_NULL(2) + void write_characters(const CharType* s, std::size_t length) override + { + stream.write(s, static_cast(length)); + } + + private: + std::basic_ostream& stream; +}; +#endif // JSON_NO_IO + +/// output adapter for basic_string +template> +class output_string_adapter : public output_adapter_protocol +{ + public: + explicit output_string_adapter(StringType& s) noexcept + : str(s) + {} + + void write_character(CharType c) override + { + str.push_back(c); + } + + JSON_HEDLEY_NON_NULL(2) + void write_characters(const CharType* s, std::size_t length) override + { + str.append(s, length); + } + + private: + StringType& str; +}; + +template> +class output_adapter +{ + public: + template> + output_adapter(std::vector& vec) + : oa(std::make_shared>(vec)) {} + +#ifndef JSON_NO_IO + output_adapter(std::basic_ostream& s) + : oa(std::make_shared>(s)) {} +#endif // JSON_NO_IO + + output_adapter(StringType& s) + : oa(std::make_shared>(s)) {} + + operator output_adapter_t() + { + return oa; + } + + private: + output_adapter_t oa = nullptr; +}; +} // namespace detail +} // namespace nlohmann + + +namespace nlohmann +{ +namespace detail +{ +/////////////////// +// binary writer // +/////////////////// + +/*! +@brief serialization to CBOR and MessagePack values +*/ +template +class binary_writer +{ + using string_t = typename BasicJsonType::string_t; + using binary_t = typename BasicJsonType::binary_t; + using number_float_t = typename BasicJsonType::number_float_t; + + public: + /*! + @brief create a binary writer + + @param[in] adapter output adapter to write to + */ + explicit binary_writer(output_adapter_t adapter) : oa(std::move(adapter)) + { + JSON_ASSERT(oa); + } + + /*! + @param[in] j JSON value to serialize + @pre j.type() == value_t::object + */ + void write_bson(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::object: + { + write_bson_object(*j.m_value.object); + break; + } + + case value_t::null: + case value_t::array: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + JSON_THROW(type_error::create(317, "to serialize to BSON, top-level type must be object, but is " + std::string(j.type_name()), j)); + } + } + } + + /*! + @param[in] j JSON value to serialize + */ + void write_cbor(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::null: + { + oa->write_character(to_char_type(0xF6)); + break; + } + + case value_t::boolean: + { + oa->write_character(j.m_value.boolean + ? to_char_type(0xF5) + : to_char_type(0xF4)); + break; + } + + case value_t::number_integer: + { + if (j.m_value.number_integer >= 0) + { + // CBOR does not differentiate between positive signed + // integers and unsigned integers. Therefore, we used the + // code from the value_t::number_unsigned case here. + if (j.m_value.number_integer <= 0x17) + { + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_integer <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x18)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_integer <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x19)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_integer <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x1A)); + write_number(static_cast(j.m_value.number_integer)); + } + else + { + oa->write_character(to_char_type(0x1B)); + write_number(static_cast(j.m_value.number_integer)); + } + } + else + { + // The conversions below encode the sign in the first + // byte, and the value is converted to a positive number. + const auto positive_number = -1 - j.m_value.number_integer; + if (j.m_value.number_integer >= -24) + { + write_number(static_cast(0x20 + positive_number)); + } + else if (positive_number <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x38)); + write_number(static_cast(positive_number)); + } + else if (positive_number <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x39)); + write_number(static_cast(positive_number)); + } + else if (positive_number <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x3A)); + write_number(static_cast(positive_number)); + } + else + { + oa->write_character(to_char_type(0x3B)); + write_number(static_cast(positive_number)); + } + } + break; + } + + case value_t::number_unsigned: + { + if (j.m_value.number_unsigned <= 0x17) + { + write_number(static_cast(j.m_value.number_unsigned)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x18)); + write_number(static_cast(j.m_value.number_unsigned)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x19)); + write_number(static_cast(j.m_value.number_unsigned)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x1A)); + write_number(static_cast(j.m_value.number_unsigned)); + } + else + { + oa->write_character(to_char_type(0x1B)); + write_number(static_cast(j.m_value.number_unsigned)); + } + break; + } + + case value_t::number_float: + { + if (std::isnan(j.m_value.number_float)) + { + // NaN is 0xf97e00 in CBOR + oa->write_character(to_char_type(0xF9)); + oa->write_character(to_char_type(0x7E)); + oa->write_character(to_char_type(0x00)); + } + else if (std::isinf(j.m_value.number_float)) + { + // Infinity is 0xf97c00, -Infinity is 0xf9fc00 + oa->write_character(to_char_type(0xf9)); + oa->write_character(j.m_value.number_float > 0 ? to_char_type(0x7C) : to_char_type(0xFC)); + oa->write_character(to_char_type(0x00)); + } + else + { + write_compact_float(j.m_value.number_float, detail::input_format_t::cbor); + } + break; + } + + case value_t::string: + { + // step 1: write control byte and the string length + const auto N = j.m_value.string->size(); + if (N <= 0x17) + { + write_number(static_cast(0x60 + N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x78)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x79)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x7A)); + write_number(static_cast(N)); + } + // LCOV_EXCL_START + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x7B)); + write_number(static_cast(N)); + } + // LCOV_EXCL_STOP + + // step 2: write the string + oa->write_characters( + reinterpret_cast(j.m_value.string->c_str()), + j.m_value.string->size()); + break; + } + + case value_t::array: + { + // step 1: write control byte and the array size + const auto N = j.m_value.array->size(); + if (N <= 0x17) + { + write_number(static_cast(0x80 + N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x98)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x99)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x9A)); + write_number(static_cast(N)); + } + // LCOV_EXCL_START + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x9B)); + write_number(static_cast(N)); + } + // LCOV_EXCL_STOP + + // step 2: write each element + for (const auto& el : *j.m_value.array) + { + write_cbor(el); + } + break; + } + + case value_t::binary: + { + if (j.m_value.binary->has_subtype()) + { + if (j.m_value.binary->subtype() <= (std::numeric_limits::max)()) + { + write_number(static_cast(0xd8)); + write_number(static_cast(j.m_value.binary->subtype())); + } + else if (j.m_value.binary->subtype() <= (std::numeric_limits::max)()) + { + write_number(static_cast(0xd9)); + write_number(static_cast(j.m_value.binary->subtype())); + } + else if (j.m_value.binary->subtype() <= (std::numeric_limits::max)()) + { + write_number(static_cast(0xda)); + write_number(static_cast(j.m_value.binary->subtype())); + } + else if (j.m_value.binary->subtype() <= (std::numeric_limits::max)()) + { + write_number(static_cast(0xdb)); + write_number(static_cast(j.m_value.binary->subtype())); + } + } + + // step 1: write control byte and the binary array size + const auto N = j.m_value.binary->size(); + if (N <= 0x17) + { + write_number(static_cast(0x40 + N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x58)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x59)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x5A)); + write_number(static_cast(N)); + } + // LCOV_EXCL_START + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0x5B)); + write_number(static_cast(N)); + } + // LCOV_EXCL_STOP + + // step 2: write each element + oa->write_characters( + reinterpret_cast(j.m_value.binary->data()), + N); + + break; + } + + case value_t::object: + { + // step 1: write control byte and the object size + const auto N = j.m_value.object->size(); + if (N <= 0x17) + { + write_number(static_cast(0xA0 + N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0xB8)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0xB9)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0xBA)); + write_number(static_cast(N)); + } + // LCOV_EXCL_START + else if (N <= (std::numeric_limits::max)()) + { + oa->write_character(to_char_type(0xBB)); + write_number(static_cast(N)); + } + // LCOV_EXCL_STOP + + // step 2: write each element + for (const auto& el : *j.m_value.object) + { + write_cbor(el.first); + write_cbor(el.second); + } + break; + } + + case value_t::discarded: + default: + break; + } + } + + /*! + @param[in] j JSON value to serialize + */ + void write_msgpack(const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::null: // nil + { + oa->write_character(to_char_type(0xC0)); + break; + } + + case value_t::boolean: // true and false + { + oa->write_character(j.m_value.boolean + ? to_char_type(0xC3) + : to_char_type(0xC2)); + break; + } + + case value_t::number_integer: + { + if (j.m_value.number_integer >= 0) + { + // MessagePack does not differentiate between positive + // signed integers and unsigned integers. Therefore, we used + // the code from the value_t::number_unsigned case here. + if (j.m_value.number_unsigned < 128) + { + // positive fixnum + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + // uint 8 + oa->write_character(to_char_type(0xCC)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + // uint 16 + oa->write_character(to_char_type(0xCD)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + // uint 32 + oa->write_character(to_char_type(0xCE)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + // uint 64 + oa->write_character(to_char_type(0xCF)); + write_number(static_cast(j.m_value.number_integer)); + } + } + else + { + if (j.m_value.number_integer >= -32) + { + // negative fixnum + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 8 + oa->write_character(to_char_type(0xD0)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 16 + oa->write_character(to_char_type(0xD1)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 32 + oa->write_character(to_char_type(0xD2)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_value.number_integer <= (std::numeric_limits::max)()) + { + // int 64 + oa->write_character(to_char_type(0xD3)); + write_number(static_cast(j.m_value.number_integer)); + } + } + break; + } + + case value_t::number_unsigned: + { + if (j.m_value.number_unsigned < 128) + { + // positive fixnum + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + // uint 8 + oa->write_character(to_char_type(0xCC)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + // uint 16 + oa->write_character(to_char_type(0xCD)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + // uint 32 + oa->write_character(to_char_type(0xCE)); + write_number(static_cast(j.m_value.number_integer)); + } + else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + { + // uint 64 + oa->write_character(to_char_type(0xCF)); + write_number(static_cast(j.m_value.number_integer)); + } + break; + } + + case value_t::number_float: + { + write_compact_float(j.m_value.number_float, detail::input_format_t::msgpack); + break; + } + + case value_t::string: + { + // step 1: write control byte and the string length + const auto N = j.m_value.string->size(); + if (N <= 31) + { + // fixstr + write_number(static_cast(0xA0 | N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 8 + oa->write_character(to_char_type(0xD9)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 16 + oa->write_character(to_char_type(0xDA)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // str 32 + oa->write_character(to_char_type(0xDB)); + write_number(static_cast(N)); + } + + // step 2: write the string + oa->write_characters( + reinterpret_cast(j.m_value.string->c_str()), + j.m_value.string->size()); + break; + } + + case value_t::array: + { + // step 1: write control byte and the array size + const auto N = j.m_value.array->size(); + if (N <= 15) + { + // fixarray + write_number(static_cast(0x90 | N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // array 16 + oa->write_character(to_char_type(0xDC)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // array 32 + oa->write_character(to_char_type(0xDD)); + write_number(static_cast(N)); + } + + // step 2: write each element + for (const auto& el : *j.m_value.array) + { + write_msgpack(el); + } + break; + } + + case value_t::binary: + { + // step 0: determine if the binary type has a set subtype to + // determine whether or not to use the ext or fixext types + const bool use_ext = j.m_value.binary->has_subtype(); + + // step 1: write control byte and the byte string length + const auto N = j.m_value.binary->size(); + if (N <= (std::numeric_limits::max)()) + { + std::uint8_t output_type{}; + bool fixed = true; + if (use_ext) + { + switch (N) + { + case 1: + output_type = 0xD4; // fixext 1 + break; + case 2: + output_type = 0xD5; // fixext 2 + break; + case 4: + output_type = 0xD6; // fixext 4 + break; + case 8: + output_type = 0xD7; // fixext 8 + break; + case 16: + output_type = 0xD8; // fixext 16 + break; + default: + output_type = 0xC7; // ext 8 + fixed = false; + break; + } + + } + else + { + output_type = 0xC4; // bin 8 + fixed = false; + } + + oa->write_character(to_char_type(output_type)); + if (!fixed) + { + write_number(static_cast(N)); + } + } + else if (N <= (std::numeric_limits::max)()) + { + std::uint8_t output_type = use_ext + ? 0xC8 // ext 16 + : 0xC5; // bin 16 + + oa->write_character(to_char_type(output_type)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + std::uint8_t output_type = use_ext + ? 0xC9 // ext 32 + : 0xC6; // bin 32 + + oa->write_character(to_char_type(output_type)); + write_number(static_cast(N)); + } + + // step 1.5: if this is an ext type, write the subtype + if (use_ext) + { + write_number(static_cast(j.m_value.binary->subtype())); + } + + // step 2: write the byte string + oa->write_characters( + reinterpret_cast(j.m_value.binary->data()), + N); + + break; + } + + case value_t::object: + { + // step 1: write control byte and the object size + const auto N = j.m_value.object->size(); + if (N <= 15) + { + // fixmap + write_number(static_cast(0x80 | (N & 0xF))); + } + else if (N <= (std::numeric_limits::max)()) + { + // map 16 + oa->write_character(to_char_type(0xDE)); + write_number(static_cast(N)); + } + else if (N <= (std::numeric_limits::max)()) + { + // map 32 + oa->write_character(to_char_type(0xDF)); + write_number(static_cast(N)); + } + + // step 2: write each element + for (const auto& el : *j.m_value.object) + { + write_msgpack(el.first); + write_msgpack(el.second); + } + break; + } + + case value_t::discarded: + default: + break; + } + } + + /*! + @param[in] j JSON value to serialize + @param[in] use_count whether to use '#' prefixes (optimized format) + @param[in] use_type whether to use '$' prefixes (optimized format) + @param[in] add_prefix whether prefixes need to be used for this value + */ + void write_ubjson(const BasicJsonType& j, const bool use_count, + const bool use_type, const bool add_prefix = true) + { + switch (j.type()) + { + case value_t::null: + { + if (add_prefix) + { + oa->write_character(to_char_type('Z')); + } + break; + } + + case value_t::boolean: + { + if (add_prefix) + { + oa->write_character(j.m_value.boolean + ? to_char_type('T') + : to_char_type('F')); + } + break; + } + + case value_t::number_integer: + { + write_number_with_ubjson_prefix(j.m_value.number_integer, add_prefix); + break; + } + + case value_t::number_unsigned: + { + write_number_with_ubjson_prefix(j.m_value.number_unsigned, add_prefix); + break; + } + + case value_t::number_float: + { + write_number_with_ubjson_prefix(j.m_value.number_float, add_prefix); + break; + } + + case value_t::string: + { + if (add_prefix) + { + oa->write_character(to_char_type('S')); + } + write_number_with_ubjson_prefix(j.m_value.string->size(), true); + oa->write_characters( + reinterpret_cast(j.m_value.string->c_str()), + j.m_value.string->size()); + break; + } + + case value_t::array: + { + if (add_prefix) + { + oa->write_character(to_char_type('[')); + } + + bool prefix_required = true; + if (use_type && !j.m_value.array->empty()) + { + JSON_ASSERT(use_count); + const CharType first_prefix = ubjson_prefix(j.front()); + const bool same_prefix = std::all_of(j.begin() + 1, j.end(), + [this, first_prefix](const BasicJsonType & v) + { + return ubjson_prefix(v) == first_prefix; + }); + + if (same_prefix) + { + prefix_required = false; + oa->write_character(to_char_type('$')); + oa->write_character(first_prefix); + } + } + + if (use_count) + { + oa->write_character(to_char_type('#')); + write_number_with_ubjson_prefix(j.m_value.array->size(), true); + } + + for (const auto& el : *j.m_value.array) + { + write_ubjson(el, use_count, use_type, prefix_required); + } + + if (!use_count) + { + oa->write_character(to_char_type(']')); + } + + break; + } + + case value_t::binary: + { + if (add_prefix) + { + oa->write_character(to_char_type('[')); + } + + if (use_type && !j.m_value.binary->empty()) + { + JSON_ASSERT(use_count); + oa->write_character(to_char_type('$')); + oa->write_character('U'); + } + + if (use_count) + { + oa->write_character(to_char_type('#')); + write_number_with_ubjson_prefix(j.m_value.binary->size(), true); + } + + if (use_type) + { + oa->write_characters( + reinterpret_cast(j.m_value.binary->data()), + j.m_value.binary->size()); + } + else + { + for (size_t i = 0; i < j.m_value.binary->size(); ++i) + { + oa->write_character(to_char_type('U')); + oa->write_character(j.m_value.binary->data()[i]); + } + } + + if (!use_count) + { + oa->write_character(to_char_type(']')); + } + + break; + } + + case value_t::object: + { + if (add_prefix) + { + oa->write_character(to_char_type('{')); + } + + bool prefix_required = true; + if (use_type && !j.m_value.object->empty()) + { + JSON_ASSERT(use_count); + const CharType first_prefix = ubjson_prefix(j.front()); + const bool same_prefix = std::all_of(j.begin(), j.end(), + [this, first_prefix](const BasicJsonType & v) + { + return ubjson_prefix(v) == first_prefix; + }); + + if (same_prefix) + { + prefix_required = false; + oa->write_character(to_char_type('$')); + oa->write_character(first_prefix); + } + } + + if (use_count) + { + oa->write_character(to_char_type('#')); + write_number_with_ubjson_prefix(j.m_value.object->size(), true); + } + + for (const auto& el : *j.m_value.object) + { + write_number_with_ubjson_prefix(el.first.size(), true); + oa->write_characters( + reinterpret_cast(el.first.c_str()), + el.first.size()); + write_ubjson(el.second, use_count, use_type, prefix_required); + } + + if (!use_count) + { + oa->write_character(to_char_type('}')); + } + + break; + } + + case value_t::discarded: + default: + break; + } + } + + private: + ////////// + // BSON // + ////////// + + /*! + @return The size of a BSON document entry header, including the id marker + and the entry name size (and its null-terminator). + */ + static std::size_t calc_bson_entry_header_size(const string_t& name, const BasicJsonType& j) + { + const auto it = name.find(static_cast(0)); + if (JSON_HEDLEY_UNLIKELY(it != BasicJsonType::string_t::npos)) + { + JSON_THROW(out_of_range::create(409, "BSON key cannot contain code point U+0000 (at byte " + std::to_string(it) + ")", j)); + static_cast(j); + } + + return /*id*/ 1ul + name.size() + /*zero-terminator*/1u; + } + + /*! + @brief Writes the given @a element_type and @a name to the output adapter + */ + void write_bson_entry_header(const string_t& name, + const std::uint8_t element_type) + { + oa->write_character(to_char_type(element_type)); // boolean + oa->write_characters( + reinterpret_cast(name.c_str()), + name.size() + 1u); + } + + /*! + @brief Writes a BSON element with key @a name and boolean value @a value + */ + void write_bson_boolean(const string_t& name, + const bool value) + { + write_bson_entry_header(name, 0x08); + oa->write_character(value ? to_char_type(0x01) : to_char_type(0x00)); + } + + /*! + @brief Writes a BSON element with key @a name and double value @a value + */ + void write_bson_double(const string_t& name, + const double value) + { + write_bson_entry_header(name, 0x01); + write_number(value); + } + + /*! + @return The size of the BSON-encoded string in @a value + */ + static std::size_t calc_bson_string_size(const string_t& value) + { + return sizeof(std::int32_t) + value.size() + 1ul; + } + + /*! + @brief Writes a BSON element with key @a name and string value @a value + */ + void write_bson_string(const string_t& name, + const string_t& value) + { + write_bson_entry_header(name, 0x02); + + write_number(static_cast(value.size() + 1ul)); + oa->write_characters( + reinterpret_cast(value.c_str()), + value.size() + 1); + } + + /*! + @brief Writes a BSON element with key @a name and null value + */ + void write_bson_null(const string_t& name) + { + write_bson_entry_header(name, 0x0A); + } + + /*! + @return The size of the BSON-encoded integer @a value + */ + static std::size_t calc_bson_integer_size(const std::int64_t value) + { + return (std::numeric_limits::min)() <= value && value <= (std::numeric_limits::max)() + ? sizeof(std::int32_t) + : sizeof(std::int64_t); + } + + /*! + @brief Writes a BSON element with key @a name and integer @a value + */ + void write_bson_integer(const string_t& name, + const std::int64_t value) + { + if ((std::numeric_limits::min)() <= value && value <= (std::numeric_limits::max)()) + { + write_bson_entry_header(name, 0x10); // int32 + write_number(static_cast(value)); + } + else + { + write_bson_entry_header(name, 0x12); // int64 + write_number(static_cast(value)); + } + } + + /*! + @return The size of the BSON-encoded unsigned integer in @a j + */ + static constexpr std::size_t calc_bson_unsigned_size(const std::uint64_t value) noexcept + { + return (value <= static_cast((std::numeric_limits::max)())) + ? sizeof(std::int32_t) + : sizeof(std::int64_t); + } + + /*! + @brief Writes a BSON element with key @a name and unsigned @a value + */ + void write_bson_unsigned(const string_t& name, + const BasicJsonType& j) + { + if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + { + write_bson_entry_header(name, 0x10 /* int32 */); + write_number(static_cast(j.m_value.number_unsigned)); + } + else if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + { + write_bson_entry_header(name, 0x12 /* int64 */); + write_number(static_cast(j.m_value.number_unsigned)); + } + else + { + JSON_THROW(out_of_range::create(407, "integer number " + std::to_string(j.m_value.number_unsigned) + " cannot be represented by BSON as it does not fit int64", j)); + } + } + + /*! + @brief Writes a BSON element with key @a name and object @a value + */ + void write_bson_object_entry(const string_t& name, + const typename BasicJsonType::object_t& value) + { + write_bson_entry_header(name, 0x03); // object + write_bson_object(value); + } + + /*! + @return The size of the BSON-encoded array @a value + */ + static std::size_t calc_bson_array_size(const typename BasicJsonType::array_t& value) + { + std::size_t array_index = 0ul; + + const std::size_t embedded_document_size = std::accumulate(std::begin(value), std::end(value), std::size_t(0), [&array_index](std::size_t result, const typename BasicJsonType::array_t::value_type & el) + { + return result + calc_bson_element_size(std::to_string(array_index++), el); + }); + + return sizeof(std::int32_t) + embedded_document_size + 1ul; + } + + /*! + @return The size of the BSON-encoded binary array @a value + */ + static std::size_t calc_bson_binary_size(const typename BasicJsonType::binary_t& value) + { + return sizeof(std::int32_t) + value.size() + 1ul; + } + + /*! + @brief Writes a BSON element with key @a name and array @a value + */ + void write_bson_array(const string_t& name, + const typename BasicJsonType::array_t& value) + { + write_bson_entry_header(name, 0x04); // array + write_number(static_cast(calc_bson_array_size(value))); + + std::size_t array_index = 0ul; + + for (const auto& el : value) + { + write_bson_element(std::to_string(array_index++), el); + } + + oa->write_character(to_char_type(0x00)); + } + + /*! + @brief Writes a BSON element with key @a name and binary value @a value + */ + void write_bson_binary(const string_t& name, + const binary_t& value) + { + write_bson_entry_header(name, 0x05); + + write_number(static_cast(value.size())); + write_number(value.has_subtype() ? static_cast(value.subtype()) : std::uint8_t(0x00)); + + oa->write_characters(reinterpret_cast(value.data()), value.size()); + } + + /*! + @brief Calculates the size necessary to serialize the JSON value @a j with its @a name + @return The calculated size for the BSON document entry for @a j with the given @a name. + */ + static std::size_t calc_bson_element_size(const string_t& name, + const BasicJsonType& j) + { + const auto header_size = calc_bson_entry_header_size(name, j); + switch (j.type()) + { + case value_t::object: + return header_size + calc_bson_object_size(*j.m_value.object); + + case value_t::array: + return header_size + calc_bson_array_size(*j.m_value.array); + + case value_t::binary: + return header_size + calc_bson_binary_size(*j.m_value.binary); + + case value_t::boolean: + return header_size + 1ul; + + case value_t::number_float: + return header_size + 8ul; + + case value_t::number_integer: + return header_size + calc_bson_integer_size(j.m_value.number_integer); + + case value_t::number_unsigned: + return header_size + calc_bson_unsigned_size(j.m_value.number_unsigned); + + case value_t::string: + return header_size + calc_bson_string_size(*j.m_value.string); + + case value_t::null: + return header_size + 0ul; + + // LCOV_EXCL_START + case value_t::discarded: + default: + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) + return 0ul; + // LCOV_EXCL_STOP + } + } + + /*! + @brief Serializes the JSON value @a j to BSON and associates it with the + key @a name. + @param name The name to associate with the JSON entity @a j within the + current BSON document + */ + void write_bson_element(const string_t& name, + const BasicJsonType& j) + { + switch (j.type()) + { + case value_t::object: + return write_bson_object_entry(name, *j.m_value.object); + + case value_t::array: + return write_bson_array(name, *j.m_value.array); + + case value_t::binary: + return write_bson_binary(name, *j.m_value.binary); + + case value_t::boolean: + return write_bson_boolean(name, j.m_value.boolean); + + case value_t::number_float: + return write_bson_double(name, j.m_value.number_float); + + case value_t::number_integer: + return write_bson_integer(name, j.m_value.number_integer); + + case value_t::number_unsigned: + return write_bson_unsigned(name, j); + + case value_t::string: + return write_bson_string(name, *j.m_value.string); + + case value_t::null: + return write_bson_null(name); + + // LCOV_EXCL_START + case value_t::discarded: + default: + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) + return; + // LCOV_EXCL_STOP + } + } + + /*! + @brief Calculates the size of the BSON serialization of the given + JSON-object @a j. + @param[in] value JSON value to serialize + @pre value.type() == value_t::object + */ + static std::size_t calc_bson_object_size(const typename BasicJsonType::object_t& value) + { + std::size_t document_size = std::accumulate(value.begin(), value.end(), std::size_t(0), + [](size_t result, const typename BasicJsonType::object_t::value_type & el) + { + return result += calc_bson_element_size(el.first, el.second); + }); + + return sizeof(std::int32_t) + document_size + 1ul; + } + + /*! + @param[in] value JSON value to serialize + @pre value.type() == value_t::object + */ + void write_bson_object(const typename BasicJsonType::object_t& value) + { + write_number(static_cast(calc_bson_object_size(value))); + + for (const auto& el : value) + { + write_bson_element(el.first, el.second); + } + + oa->write_character(to_char_type(0x00)); + } + + ////////// + // CBOR // + ////////// + + static constexpr CharType get_cbor_float_prefix(float /*unused*/) + { + return to_char_type(0xFA); // Single-Precision Float + } + + static constexpr CharType get_cbor_float_prefix(double /*unused*/) + { + return to_char_type(0xFB); // Double-Precision Float + } + + ///////////// + // MsgPack // + ///////////// + + static constexpr CharType get_msgpack_float_prefix(float /*unused*/) + { + return to_char_type(0xCA); // float 32 + } + + static constexpr CharType get_msgpack_float_prefix(double /*unused*/) + { + return to_char_type(0xCB); // float 64 + } + + //////////// + // UBJSON // + //////////// + + // UBJSON: write number (floating point) + template::value, int>::type = 0> + void write_number_with_ubjson_prefix(const NumberType n, + const bool add_prefix) + { + if (add_prefix) + { + oa->write_character(get_ubjson_float_prefix(n)); + } + write_number(n); + } + + // UBJSON: write number (unsigned integer) + template::value, int>::type = 0> + void write_number_with_ubjson_prefix(const NumberType n, + const bool add_prefix) + { + if (n <= static_cast((std::numeric_limits::max)())) + { + if (add_prefix) + { + oa->write_character(to_char_type('i')); // int8 + } + write_number(static_cast(n)); + } + else if (n <= (std::numeric_limits::max)()) + { + if (add_prefix) + { + oa->write_character(to_char_type('U')); // uint8 + } + write_number(static_cast(n)); + } + else if (n <= static_cast((std::numeric_limits::max)())) + { + if (add_prefix) + { + oa->write_character(to_char_type('I')); // int16 + } + write_number(static_cast(n)); + } + else if (n <= static_cast((std::numeric_limits::max)())) + { + if (add_prefix) + { + oa->write_character(to_char_type('l')); // int32 + } + write_number(static_cast(n)); + } + else if (n <= static_cast((std::numeric_limits::max)())) + { + if (add_prefix) + { + oa->write_character(to_char_type('L')); // int64 + } + write_number(static_cast(n)); + } + else + { + if (add_prefix) + { + oa->write_character(to_char_type('H')); // high-precision number + } + + const auto number = BasicJsonType(n).dump(); + write_number_with_ubjson_prefix(number.size(), true); + for (std::size_t i = 0; i < number.size(); ++i) + { + oa->write_character(to_char_type(static_cast(number[i]))); + } + } + } + + // UBJSON: write number (signed integer) + template < typename NumberType, typename std::enable_if < + std::is_signed::value&& + !std::is_floating_point::value, int >::type = 0 > + void write_number_with_ubjson_prefix(const NumberType n, + const bool add_prefix) + { + if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) + { + if (add_prefix) + { + oa->write_character(to_char_type('i')); // int8 + } + write_number(static_cast(n)); + } + else if (static_cast((std::numeric_limits::min)()) <= n && n <= static_cast((std::numeric_limits::max)())) + { + if (add_prefix) + { + oa->write_character(to_char_type('U')); // uint8 + } + write_number(static_cast(n)); + } + else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) + { + if (add_prefix) + { + oa->write_character(to_char_type('I')); // int16 + } + write_number(static_cast(n)); + } + else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) + { + if (add_prefix) + { + oa->write_character(to_char_type('l')); // int32 + } + write_number(static_cast(n)); + } + else if ((std::numeric_limits::min)() <= n && n <= (std::numeric_limits::max)()) + { + if (add_prefix) + { + oa->write_character(to_char_type('L')); // int64 + } + write_number(static_cast(n)); + } + // LCOV_EXCL_START + else + { + if (add_prefix) + { + oa->write_character(to_char_type('H')); // high-precision number + } + + const auto number = BasicJsonType(n).dump(); + write_number_with_ubjson_prefix(number.size(), true); + for (std::size_t i = 0; i < number.size(); ++i) + { + oa->write_character(to_char_type(static_cast(number[i]))); + } + } + // LCOV_EXCL_STOP + } + + /*! + @brief determine the type prefix of container values + */ + CharType ubjson_prefix(const BasicJsonType& j) const noexcept + { + switch (j.type()) + { + case value_t::null: + return 'Z'; + + case value_t::boolean: + return j.m_value.boolean ? 'T' : 'F'; + + case value_t::number_integer: + { + if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + { + return 'i'; + } + if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + { + return 'U'; + } + if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + { + return 'I'; + } + if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + { + return 'l'; + } + if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + { + return 'L'; + } + // anything else is treated as high-precision number + return 'H'; // LCOV_EXCL_LINE + } + + case value_t::number_unsigned: + { + if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + { + return 'i'; + } + if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + { + return 'U'; + } + if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + { + return 'I'; + } + if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + { + return 'l'; + } + if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + { + return 'L'; + } + // anything else is treated as high-precision number + return 'H'; // LCOV_EXCL_LINE + } + + case value_t::number_float: + return get_ubjson_float_prefix(j.m_value.number_float); + + case value_t::string: + return 'S'; + + case value_t::array: // fallthrough + case value_t::binary: + return '['; + + case value_t::object: + return '{'; + + case value_t::discarded: + default: // discarded values + return 'N'; + } + } + + static constexpr CharType get_ubjson_float_prefix(float /*unused*/) + { + return 'd'; // float 32 + } + + static constexpr CharType get_ubjson_float_prefix(double /*unused*/) + { + return 'D'; // float 64 + } + + /////////////////////// + // Utility functions // + /////////////////////// + + /* + @brief write a number to output input + @param[in] n number of type @a NumberType + @tparam NumberType the type of the number + @tparam OutputIsLittleEndian Set to true if output data is + required to be little endian + + @note This function needs to respect the system's endianess, because bytes + in CBOR, MessagePack, and UBJSON are stored in network order (big + endian) and therefore need reordering on little endian systems. + */ + template + void write_number(const NumberType n) + { + // step 1: write number to array of length NumberType + std::array vec{}; + std::memcpy(vec.data(), &n, sizeof(NumberType)); + + // step 2: write array to output (with possible reordering) + if (is_little_endian != OutputIsLittleEndian) + { + // reverse byte order prior to conversion if necessary + std::reverse(vec.begin(), vec.end()); + } + + oa->write_characters(vec.data(), sizeof(NumberType)); + } + + void write_compact_float(const number_float_t n, detail::input_format_t format) + { +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + if (static_cast(n) >= static_cast(std::numeric_limits::lowest()) && + static_cast(n) <= static_cast((std::numeric_limits::max)()) && + static_cast(static_cast(n)) == static_cast(n)) + { + oa->write_character(format == detail::input_format_t::cbor + ? get_cbor_float_prefix(static_cast(n)) + : get_msgpack_float_prefix(static_cast(n))); + write_number(static_cast(n)); + } + else + { + oa->write_character(format == detail::input_format_t::cbor + ? get_cbor_float_prefix(n) + : get_msgpack_float_prefix(n)); + write_number(n); + } +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + } + + public: + // The following to_char_type functions are implement the conversion + // between uint8_t and CharType. In case CharType is not unsigned, + // such a conversion is required to allow values greater than 128. + // See for a discussion. + template < typename C = CharType, + enable_if_t < std::is_signed::value && std::is_signed::value > * = nullptr > + static constexpr CharType to_char_type(std::uint8_t x) noexcept + { + return *reinterpret_cast(&x); + } + + template < typename C = CharType, + enable_if_t < std::is_signed::value && std::is_unsigned::value > * = nullptr > + static CharType to_char_type(std::uint8_t x) noexcept + { + static_assert(sizeof(std::uint8_t) == sizeof(CharType), "size of CharType must be equal to std::uint8_t"); + static_assert(std::is_trivial::value, "CharType must be trivial"); + CharType result; + std::memcpy(&result, &x, sizeof(x)); + return result; + } + + template::value>* = nullptr> + static constexpr CharType to_char_type(std::uint8_t x) noexcept + { + return x; + } + + template < typename InputCharType, typename C = CharType, + enable_if_t < + std::is_signed::value && + std::is_signed::value && + std::is_same::type>::value + > * = nullptr > + static constexpr CharType to_char_type(InputCharType x) noexcept + { + return x; + } + + private: + /// whether we can assume little endianess + const bool is_little_endian = little_endianess(); + + /// the output + output_adapter_t oa = nullptr; +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + + +#include // reverse, remove, fill, find, none_of +#include // array +#include // localeconv, lconv +#include // labs, isfinite, isnan, signbit +#include // size_t, ptrdiff_t +#include // uint8_t +#include // snprintf +#include // numeric_limits +#include // string, char_traits +#include // is_same +#include // move + +// #include + + +#include // array +#include // signbit, isfinite +#include // intN_t, uintN_t +#include // memcpy, memmove +#include // numeric_limits +#include // conditional + +// #include + + +namespace nlohmann +{ +namespace detail +{ + +/*! +@brief implements the Grisu2 algorithm for binary to decimal floating-point +conversion. + +This implementation is a slightly modified version of the reference +implementation which may be obtained from +http://florian.loitsch.com/publications (bench.tar.gz). + +The code is distributed under the MIT license, Copyright (c) 2009 Florian Loitsch. + +For a detailed description of the algorithm see: + +[1] Loitsch, "Printing Floating-Point Numbers Quickly and Accurately with + Integers", Proceedings of the ACM SIGPLAN 2010 Conference on Programming + Language Design and Implementation, PLDI 2010 +[2] Burger, Dybvig, "Printing Floating-Point Numbers Quickly and Accurately", + Proceedings of the ACM SIGPLAN 1996 Conference on Programming Language + Design and Implementation, PLDI 1996 +*/ +namespace dtoa_impl +{ + +template +Target reinterpret_bits(const Source source) +{ + static_assert(sizeof(Target) == sizeof(Source), "size mismatch"); + + Target target; + std::memcpy(&target, &source, sizeof(Source)); + return target; +} + +struct diyfp // f * 2^e +{ + static constexpr int kPrecision = 64; // = q + + std::uint64_t f = 0; + int e = 0; + + constexpr diyfp(std::uint64_t f_, int e_) noexcept : f(f_), e(e_) {} + + /*! + @brief returns x - y + @pre x.e == y.e and x.f >= y.f + */ + static diyfp sub(const diyfp& x, const diyfp& y) noexcept + { + JSON_ASSERT(x.e == y.e); + JSON_ASSERT(x.f >= y.f); + + return {x.f - y.f, x.e}; + } + + /*! + @brief returns x * y + @note The result is rounded. (Only the upper q bits are returned.) + */ + static diyfp mul(const diyfp& x, const diyfp& y) noexcept + { + static_assert(kPrecision == 64, "internal error"); + + // Computes: + // f = round((x.f * y.f) / 2^q) + // e = x.e + y.e + q + + // Emulate the 64-bit * 64-bit multiplication: + // + // p = u * v + // = (u_lo + 2^32 u_hi) (v_lo + 2^32 v_hi) + // = (u_lo v_lo ) + 2^32 ((u_lo v_hi ) + (u_hi v_lo )) + 2^64 (u_hi v_hi ) + // = (p0 ) + 2^32 ((p1 ) + (p2 )) + 2^64 (p3 ) + // = (p0_lo + 2^32 p0_hi) + 2^32 ((p1_lo + 2^32 p1_hi) + (p2_lo + 2^32 p2_hi)) + 2^64 (p3 ) + // = (p0_lo ) + 2^32 (p0_hi + p1_lo + p2_lo ) + 2^64 (p1_hi + p2_hi + p3) + // = (p0_lo ) + 2^32 (Q ) + 2^64 (H ) + // = (p0_lo ) + 2^32 (Q_lo + 2^32 Q_hi ) + 2^64 (H ) + // + // (Since Q might be larger than 2^32 - 1) + // + // = (p0_lo + 2^32 Q_lo) + 2^64 (Q_hi + H) + // + // (Q_hi + H does not overflow a 64-bit int) + // + // = p_lo + 2^64 p_hi + + const std::uint64_t u_lo = x.f & 0xFFFFFFFFu; + const std::uint64_t u_hi = x.f >> 32u; + const std::uint64_t v_lo = y.f & 0xFFFFFFFFu; + const std::uint64_t v_hi = y.f >> 32u; + + const std::uint64_t p0 = u_lo * v_lo; + const std::uint64_t p1 = u_lo * v_hi; + const std::uint64_t p2 = u_hi * v_lo; + const std::uint64_t p3 = u_hi * v_hi; + + const std::uint64_t p0_hi = p0 >> 32u; + const std::uint64_t p1_lo = p1 & 0xFFFFFFFFu; + const std::uint64_t p1_hi = p1 >> 32u; + const std::uint64_t p2_lo = p2 & 0xFFFFFFFFu; + const std::uint64_t p2_hi = p2 >> 32u; + + std::uint64_t Q = p0_hi + p1_lo + p2_lo; + + // The full product might now be computed as + // + // p_hi = p3 + p2_hi + p1_hi + (Q >> 32) + // p_lo = p0_lo + (Q << 32) + // + // But in this particular case here, the full p_lo is not required. + // Effectively we only need to add the highest bit in p_lo to p_hi (and + // Q_hi + 1 does not overflow). + + Q += std::uint64_t{1} << (64u - 32u - 1u); // round, ties up + + const std::uint64_t h = p3 + p2_hi + p1_hi + (Q >> 32u); + + return {h, x.e + y.e + 64}; + } + + /*! + @brief normalize x such that the significand is >= 2^(q-1) + @pre x.f != 0 + */ + static diyfp normalize(diyfp x) noexcept + { + JSON_ASSERT(x.f != 0); + + while ((x.f >> 63u) == 0) + { + x.f <<= 1u; + x.e--; + } + + return x; + } + + /*! + @brief normalize x such that the result has the exponent E + @pre e >= x.e and the upper e - x.e bits of x.f must be zero. + */ + static diyfp normalize_to(const diyfp& x, const int target_exponent) noexcept + { + const int delta = x.e - target_exponent; + + JSON_ASSERT(delta >= 0); + JSON_ASSERT(((x.f << delta) >> delta) == x.f); + + return {x.f << delta, target_exponent}; + } +}; + +struct boundaries +{ + diyfp w; + diyfp minus; + diyfp plus; +}; + +/*! +Compute the (normalized) diyfp representing the input number 'value' and its +boundaries. + +@pre value must be finite and positive +*/ +template +boundaries compute_boundaries(FloatType value) +{ + JSON_ASSERT(std::isfinite(value)); + JSON_ASSERT(value > 0); + + // Convert the IEEE representation into a diyfp. + // + // If v is denormal: + // value = 0.F * 2^(1 - bias) = ( F) * 2^(1 - bias - (p-1)) + // If v is normalized: + // value = 1.F * 2^(E - bias) = (2^(p-1) + F) * 2^(E - bias - (p-1)) + + static_assert(std::numeric_limits::is_iec559, + "internal error: dtoa_short requires an IEEE-754 floating-point implementation"); + + constexpr int kPrecision = std::numeric_limits::digits; // = p (includes the hidden bit) + constexpr int kBias = std::numeric_limits::max_exponent - 1 + (kPrecision - 1); + constexpr int kMinExp = 1 - kBias; + constexpr std::uint64_t kHiddenBit = std::uint64_t{1} << (kPrecision - 1); // = 2^(p-1) + + using bits_type = typename std::conditional::type; + + const auto bits = static_cast(reinterpret_bits(value)); + const std::uint64_t E = bits >> (kPrecision - 1); + const std::uint64_t F = bits & (kHiddenBit - 1); + + const bool is_denormal = E == 0; + const diyfp v = is_denormal + ? diyfp(F, kMinExp) + : diyfp(F + kHiddenBit, static_cast(E) - kBias); + + // Compute the boundaries m- and m+ of the floating-point value + // v = f * 2^e. + // + // Determine v- and v+, the floating-point predecessor and successor if v, + // respectively. + // + // v- = v - 2^e if f != 2^(p-1) or e == e_min (A) + // = v - 2^(e-1) if f == 2^(p-1) and e > e_min (B) + // + // v+ = v + 2^e + // + // Let m- = (v- + v) / 2 and m+ = (v + v+) / 2. All real numbers _strictly_ + // between m- and m+ round to v, regardless of how the input rounding + // algorithm breaks ties. + // + // ---+-------------+-------------+-------------+-------------+--- (A) + // v- m- v m+ v+ + // + // -----------------+------+------+-------------+-------------+--- (B) + // v- m- v m+ v+ + + const bool lower_boundary_is_closer = F == 0 && E > 1; + const diyfp m_plus = diyfp(2 * v.f + 1, v.e - 1); + const diyfp m_minus = lower_boundary_is_closer + ? diyfp(4 * v.f - 1, v.e - 2) // (B) + : diyfp(2 * v.f - 1, v.e - 1); // (A) + + // Determine the normalized w+ = m+. + const diyfp w_plus = diyfp::normalize(m_plus); + + // Determine w- = m- such that e_(w-) = e_(w+). + const diyfp w_minus = diyfp::normalize_to(m_minus, w_plus.e); + + return {diyfp::normalize(v), w_minus, w_plus}; +} + +// Given normalized diyfp w, Grisu needs to find a (normalized) cached +// power-of-ten c, such that the exponent of the product c * w = f * 2^e lies +// within a certain range [alpha, gamma] (Definition 3.2 from [1]) +// +// alpha <= e = e_c + e_w + q <= gamma +// +// or +// +// f_c * f_w * 2^alpha <= f_c 2^(e_c) * f_w 2^(e_w) * 2^q +// <= f_c * f_w * 2^gamma +// +// Since c and w are normalized, i.e. 2^(q-1) <= f < 2^q, this implies +// +// 2^(q-1) * 2^(q-1) * 2^alpha <= c * w * 2^q < 2^q * 2^q * 2^gamma +// +// or +// +// 2^(q - 2 + alpha) <= c * w < 2^(q + gamma) +// +// The choice of (alpha,gamma) determines the size of the table and the form of +// the digit generation procedure. Using (alpha,gamma)=(-60,-32) works out well +// in practice: +// +// The idea is to cut the number c * w = f * 2^e into two parts, which can be +// processed independently: An integral part p1, and a fractional part p2: +// +// f * 2^e = ( (f div 2^-e) * 2^-e + (f mod 2^-e) ) * 2^e +// = (f div 2^-e) + (f mod 2^-e) * 2^e +// = p1 + p2 * 2^e +// +// The conversion of p1 into decimal form requires a series of divisions and +// modulos by (a power of) 10. These operations are faster for 32-bit than for +// 64-bit integers, so p1 should ideally fit into a 32-bit integer. This can be +// achieved by choosing +// +// -e >= 32 or e <= -32 := gamma +// +// In order to convert the fractional part +// +// p2 * 2^e = p2 / 2^-e = d[-1] / 10^1 + d[-2] / 10^2 + ... +// +// into decimal form, the fraction is repeatedly multiplied by 10 and the digits +// d[-i] are extracted in order: +// +// (10 * p2) div 2^-e = d[-1] +// (10 * p2) mod 2^-e = d[-2] / 10^1 + ... +// +// The multiplication by 10 must not overflow. It is sufficient to choose +// +// 10 * p2 < 16 * p2 = 2^4 * p2 <= 2^64. +// +// Since p2 = f mod 2^-e < 2^-e, +// +// -e <= 60 or e >= -60 := alpha + +constexpr int kAlpha = -60; +constexpr int kGamma = -32; + +struct cached_power // c = f * 2^e ~= 10^k +{ + std::uint64_t f; + int e; + int k; +}; + +/*! +For a normalized diyfp w = f * 2^e, this function returns a (normalized) cached +power-of-ten c = f_c * 2^e_c, such that the exponent of the product w * c +satisfies (Definition 3.2 from [1]) + + alpha <= e_c + e + q <= gamma. +*/ +inline cached_power get_cached_power_for_binary_exponent(int e) +{ + // Now + // + // alpha <= e_c + e + q <= gamma (1) + // ==> f_c * 2^alpha <= c * 2^e * 2^q + // + // and since the c's are normalized, 2^(q-1) <= f_c, + // + // ==> 2^(q - 1 + alpha) <= c * 2^(e + q) + // ==> 2^(alpha - e - 1) <= c + // + // If c were an exact power of ten, i.e. c = 10^k, one may determine k as + // + // k = ceil( log_10( 2^(alpha - e - 1) ) ) + // = ceil( (alpha - e - 1) * log_10(2) ) + // + // From the paper: + // "In theory the result of the procedure could be wrong since c is rounded, + // and the computation itself is approximated [...]. In practice, however, + // this simple function is sufficient." + // + // For IEEE double precision floating-point numbers converted into + // normalized diyfp's w = f * 2^e, with q = 64, + // + // e >= -1022 (min IEEE exponent) + // -52 (p - 1) + // -52 (p - 1, possibly normalize denormal IEEE numbers) + // -11 (normalize the diyfp) + // = -1137 + // + // and + // + // e <= +1023 (max IEEE exponent) + // -52 (p - 1) + // -11 (normalize the diyfp) + // = 960 + // + // This binary exponent range [-1137,960] results in a decimal exponent + // range [-307,324]. One does not need to store a cached power for each + // k in this range. For each such k it suffices to find a cached power + // such that the exponent of the product lies in [alpha,gamma]. + // This implies that the difference of the decimal exponents of adjacent + // table entries must be less than or equal to + // + // floor( (gamma - alpha) * log_10(2) ) = 8. + // + // (A smaller distance gamma-alpha would require a larger table.) + + // NB: + // Actually this function returns c, such that -60 <= e_c + e + 64 <= -34. + + constexpr int kCachedPowersMinDecExp = -300; + constexpr int kCachedPowersDecStep = 8; + + static constexpr std::array kCachedPowers = + { + { + { 0xAB70FE17C79AC6CA, -1060, -300 }, + { 0xFF77B1FCBEBCDC4F, -1034, -292 }, + { 0xBE5691EF416BD60C, -1007, -284 }, + { 0x8DD01FAD907FFC3C, -980, -276 }, + { 0xD3515C2831559A83, -954, -268 }, + { 0x9D71AC8FADA6C9B5, -927, -260 }, + { 0xEA9C227723EE8BCB, -901, -252 }, + { 0xAECC49914078536D, -874, -244 }, + { 0x823C12795DB6CE57, -847, -236 }, + { 0xC21094364DFB5637, -821, -228 }, + { 0x9096EA6F3848984F, -794, -220 }, + { 0xD77485CB25823AC7, -768, -212 }, + { 0xA086CFCD97BF97F4, -741, -204 }, + { 0xEF340A98172AACE5, -715, -196 }, + { 0xB23867FB2A35B28E, -688, -188 }, + { 0x84C8D4DFD2C63F3B, -661, -180 }, + { 0xC5DD44271AD3CDBA, -635, -172 }, + { 0x936B9FCEBB25C996, -608, -164 }, + { 0xDBAC6C247D62A584, -582, -156 }, + { 0xA3AB66580D5FDAF6, -555, -148 }, + { 0xF3E2F893DEC3F126, -529, -140 }, + { 0xB5B5ADA8AAFF80B8, -502, -132 }, + { 0x87625F056C7C4A8B, -475, -124 }, + { 0xC9BCFF6034C13053, -449, -116 }, + { 0x964E858C91BA2655, -422, -108 }, + { 0xDFF9772470297EBD, -396, -100 }, + { 0xA6DFBD9FB8E5B88F, -369, -92 }, + { 0xF8A95FCF88747D94, -343, -84 }, + { 0xB94470938FA89BCF, -316, -76 }, + { 0x8A08F0F8BF0F156B, -289, -68 }, + { 0xCDB02555653131B6, -263, -60 }, + { 0x993FE2C6D07B7FAC, -236, -52 }, + { 0xE45C10C42A2B3B06, -210, -44 }, + { 0xAA242499697392D3, -183, -36 }, + { 0xFD87B5F28300CA0E, -157, -28 }, + { 0xBCE5086492111AEB, -130, -20 }, + { 0x8CBCCC096F5088CC, -103, -12 }, + { 0xD1B71758E219652C, -77, -4 }, + { 0x9C40000000000000, -50, 4 }, + { 0xE8D4A51000000000, -24, 12 }, + { 0xAD78EBC5AC620000, 3, 20 }, + { 0x813F3978F8940984, 30, 28 }, + { 0xC097CE7BC90715B3, 56, 36 }, + { 0x8F7E32CE7BEA5C70, 83, 44 }, + { 0xD5D238A4ABE98068, 109, 52 }, + { 0x9F4F2726179A2245, 136, 60 }, + { 0xED63A231D4C4FB27, 162, 68 }, + { 0xB0DE65388CC8ADA8, 189, 76 }, + { 0x83C7088E1AAB65DB, 216, 84 }, + { 0xC45D1DF942711D9A, 242, 92 }, + { 0x924D692CA61BE758, 269, 100 }, + { 0xDA01EE641A708DEA, 295, 108 }, + { 0xA26DA3999AEF774A, 322, 116 }, + { 0xF209787BB47D6B85, 348, 124 }, + { 0xB454E4A179DD1877, 375, 132 }, + { 0x865B86925B9BC5C2, 402, 140 }, + { 0xC83553C5C8965D3D, 428, 148 }, + { 0x952AB45CFA97A0B3, 455, 156 }, + { 0xDE469FBD99A05FE3, 481, 164 }, + { 0xA59BC234DB398C25, 508, 172 }, + { 0xF6C69A72A3989F5C, 534, 180 }, + { 0xB7DCBF5354E9BECE, 561, 188 }, + { 0x88FCF317F22241E2, 588, 196 }, + { 0xCC20CE9BD35C78A5, 614, 204 }, + { 0x98165AF37B2153DF, 641, 212 }, + { 0xE2A0B5DC971F303A, 667, 220 }, + { 0xA8D9D1535CE3B396, 694, 228 }, + { 0xFB9B7CD9A4A7443C, 720, 236 }, + { 0xBB764C4CA7A44410, 747, 244 }, + { 0x8BAB8EEFB6409C1A, 774, 252 }, + { 0xD01FEF10A657842C, 800, 260 }, + { 0x9B10A4E5E9913129, 827, 268 }, + { 0xE7109BFBA19C0C9D, 853, 276 }, + { 0xAC2820D9623BF429, 880, 284 }, + { 0x80444B5E7AA7CF85, 907, 292 }, + { 0xBF21E44003ACDD2D, 933, 300 }, + { 0x8E679C2F5E44FF8F, 960, 308 }, + { 0xD433179D9C8CB841, 986, 316 }, + { 0x9E19DB92B4E31BA9, 1013, 324 }, + } + }; + + // This computation gives exactly the same results for k as + // k = ceil((kAlpha - e - 1) * 0.30102999566398114) + // for |e| <= 1500, but doesn't require floating-point operations. + // NB: log_10(2) ~= 78913 / 2^18 + JSON_ASSERT(e >= -1500); + JSON_ASSERT(e <= 1500); + const int f = kAlpha - e - 1; + const int k = (f * 78913) / (1 << 18) + static_cast(f > 0); + + const int index = (-kCachedPowersMinDecExp + k + (kCachedPowersDecStep - 1)) / kCachedPowersDecStep; + JSON_ASSERT(index >= 0); + JSON_ASSERT(static_cast(index) < kCachedPowers.size()); + + const cached_power cached = kCachedPowers[static_cast(index)]; + JSON_ASSERT(kAlpha <= cached.e + e + 64); + JSON_ASSERT(kGamma >= cached.e + e + 64); + + return cached; +} + +/*! +For n != 0, returns k, such that pow10 := 10^(k-1) <= n < 10^k. +For n == 0, returns 1 and sets pow10 := 1. +*/ +inline int find_largest_pow10(const std::uint32_t n, std::uint32_t& pow10) +{ + // LCOV_EXCL_START + if (n >= 1000000000) + { + pow10 = 1000000000; + return 10; + } + // LCOV_EXCL_STOP + if (n >= 100000000) + { + pow10 = 100000000; + return 9; + } + if (n >= 10000000) + { + pow10 = 10000000; + return 8; + } + if (n >= 1000000) + { + pow10 = 1000000; + return 7; + } + if (n >= 100000) + { + pow10 = 100000; + return 6; + } + if (n >= 10000) + { + pow10 = 10000; + return 5; + } + if (n >= 1000) + { + pow10 = 1000; + return 4; + } + if (n >= 100) + { + pow10 = 100; + return 3; + } + if (n >= 10) + { + pow10 = 10; + return 2; + } + + pow10 = 1; + return 1; +} + +inline void grisu2_round(char* buf, int len, std::uint64_t dist, std::uint64_t delta, + std::uint64_t rest, std::uint64_t ten_k) +{ + JSON_ASSERT(len >= 1); + JSON_ASSERT(dist <= delta); + JSON_ASSERT(rest <= delta); + JSON_ASSERT(ten_k > 0); + + // <--------------------------- delta ----> + // <---- dist ---------> + // --------------[------------------+-------------------]-------------- + // M- w M+ + // + // ten_k + // <------> + // <---- rest ----> + // --------------[------------------+----+--------------]-------------- + // w V + // = buf * 10^k + // + // ten_k represents a unit-in-the-last-place in the decimal representation + // stored in buf. + // Decrement buf by ten_k while this takes buf closer to w. + + // The tests are written in this order to avoid overflow in unsigned + // integer arithmetic. + + while (rest < dist + && delta - rest >= ten_k + && (rest + ten_k < dist || dist - rest > rest + ten_k - dist)) + { + JSON_ASSERT(buf[len - 1] != '0'); + buf[len - 1]--; + rest += ten_k; + } +} + +/*! +Generates V = buffer * 10^decimal_exponent, such that M- <= V <= M+. +M- and M+ must be normalized and share the same exponent -60 <= e <= -32. +*/ +inline void grisu2_digit_gen(char* buffer, int& length, int& decimal_exponent, + diyfp M_minus, diyfp w, diyfp M_plus) +{ + static_assert(kAlpha >= -60, "internal error"); + static_assert(kGamma <= -32, "internal error"); + + // Generates the digits (and the exponent) of a decimal floating-point + // number V = buffer * 10^decimal_exponent in the range [M-, M+]. The diyfp's + // w, M- and M+ share the same exponent e, which satisfies alpha <= e <= gamma. + // + // <--------------------------- delta ----> + // <---- dist ---------> + // --------------[------------------+-------------------]-------------- + // M- w M+ + // + // Grisu2 generates the digits of M+ from left to right and stops as soon as + // V is in [M-,M+]. + + JSON_ASSERT(M_plus.e >= kAlpha); + JSON_ASSERT(M_plus.e <= kGamma); + + std::uint64_t delta = diyfp::sub(M_plus, M_minus).f; // (significand of (M+ - M-), implicit exponent is e) + std::uint64_t dist = diyfp::sub(M_plus, w ).f; // (significand of (M+ - w ), implicit exponent is e) + + // Split M+ = f * 2^e into two parts p1 and p2 (note: e < 0): + // + // M+ = f * 2^e + // = ((f div 2^-e) * 2^-e + (f mod 2^-e)) * 2^e + // = ((p1 ) * 2^-e + (p2 )) * 2^e + // = p1 + p2 * 2^e + + const diyfp one(std::uint64_t{1} << -M_plus.e, M_plus.e); + + auto p1 = static_cast(M_plus.f >> -one.e); // p1 = f div 2^-e (Since -e >= 32, p1 fits into a 32-bit int.) + std::uint64_t p2 = M_plus.f & (one.f - 1); // p2 = f mod 2^-e + + // 1) + // + // Generate the digits of the integral part p1 = d[n-1]...d[1]d[0] + + JSON_ASSERT(p1 > 0); + + std::uint32_t pow10{}; + const int k = find_largest_pow10(p1, pow10); + + // 10^(k-1) <= p1 < 10^k, pow10 = 10^(k-1) + // + // p1 = (p1 div 10^(k-1)) * 10^(k-1) + (p1 mod 10^(k-1)) + // = (d[k-1] ) * 10^(k-1) + (p1 mod 10^(k-1)) + // + // M+ = p1 + p2 * 2^e + // = d[k-1] * 10^(k-1) + (p1 mod 10^(k-1)) + p2 * 2^e + // = d[k-1] * 10^(k-1) + ((p1 mod 10^(k-1)) * 2^-e + p2) * 2^e + // = d[k-1] * 10^(k-1) + ( rest) * 2^e + // + // Now generate the digits d[n] of p1 from left to right (n = k-1,...,0) + // + // p1 = d[k-1]...d[n] * 10^n + d[n-1]...d[0] + // + // but stop as soon as + // + // rest * 2^e = (d[n-1]...d[0] * 2^-e + p2) * 2^e <= delta * 2^e + + int n = k; + while (n > 0) + { + // Invariants: + // M+ = buffer * 10^n + (p1 + p2 * 2^e) (buffer = 0 for n = k) + // pow10 = 10^(n-1) <= p1 < 10^n + // + const std::uint32_t d = p1 / pow10; // d = p1 div 10^(n-1) + const std::uint32_t r = p1 % pow10; // r = p1 mod 10^(n-1) + // + // M+ = buffer * 10^n + (d * 10^(n-1) + r) + p2 * 2^e + // = (buffer * 10 + d) * 10^(n-1) + (r + p2 * 2^e) + // + JSON_ASSERT(d <= 9); + buffer[length++] = static_cast('0' + d); // buffer := buffer * 10 + d + // + // M+ = buffer * 10^(n-1) + (r + p2 * 2^e) + // + p1 = r; + n--; + // + // M+ = buffer * 10^n + (p1 + p2 * 2^e) + // pow10 = 10^n + // + + // Now check if enough digits have been generated. + // Compute + // + // p1 + p2 * 2^e = (p1 * 2^-e + p2) * 2^e = rest * 2^e + // + // Note: + // Since rest and delta share the same exponent e, it suffices to + // compare the significands. + const std::uint64_t rest = (std::uint64_t{p1} << -one.e) + p2; + if (rest <= delta) + { + // V = buffer * 10^n, with M- <= V <= M+. + + decimal_exponent += n; + + // We may now just stop. But instead look if the buffer could be + // decremented to bring V closer to w. + // + // pow10 = 10^n is now 1 ulp in the decimal representation V. + // The rounding procedure works with diyfp's with an implicit + // exponent of e. + // + // 10^n = (10^n * 2^-e) * 2^e = ulp * 2^e + // + const std::uint64_t ten_n = std::uint64_t{pow10} << -one.e; + grisu2_round(buffer, length, dist, delta, rest, ten_n); + + return; + } + + pow10 /= 10; + // + // pow10 = 10^(n-1) <= p1 < 10^n + // Invariants restored. + } + + // 2) + // + // The digits of the integral part have been generated: + // + // M+ = d[k-1]...d[1]d[0] + p2 * 2^e + // = buffer + p2 * 2^e + // + // Now generate the digits of the fractional part p2 * 2^e. + // + // Note: + // No decimal point is generated: the exponent is adjusted instead. + // + // p2 actually represents the fraction + // + // p2 * 2^e + // = p2 / 2^-e + // = d[-1] / 10^1 + d[-2] / 10^2 + ... + // + // Now generate the digits d[-m] of p1 from left to right (m = 1,2,...) + // + // p2 * 2^e = d[-1]d[-2]...d[-m] * 10^-m + // + 10^-m * (d[-m-1] / 10^1 + d[-m-2] / 10^2 + ...) + // + // using + // + // 10^m * p2 = ((10^m * p2) div 2^-e) * 2^-e + ((10^m * p2) mod 2^-e) + // = ( d) * 2^-e + ( r) + // + // or + // 10^m * p2 * 2^e = d + r * 2^e + // + // i.e. + // + // M+ = buffer + p2 * 2^e + // = buffer + 10^-m * (d + r * 2^e) + // = (buffer * 10^m + d) * 10^-m + 10^-m * r * 2^e + // + // and stop as soon as 10^-m * r * 2^e <= delta * 2^e + + JSON_ASSERT(p2 > delta); + + int m = 0; + for (;;) + { + // Invariant: + // M+ = buffer * 10^-m + 10^-m * (d[-m-1] / 10 + d[-m-2] / 10^2 + ...) * 2^e + // = buffer * 10^-m + 10^-m * (p2 ) * 2^e + // = buffer * 10^-m + 10^-m * (1/10 * (10 * p2) ) * 2^e + // = buffer * 10^-m + 10^-m * (1/10 * ((10*p2 div 2^-e) * 2^-e + (10*p2 mod 2^-e)) * 2^e + // + JSON_ASSERT(p2 <= (std::numeric_limits::max)() / 10); + p2 *= 10; + const std::uint64_t d = p2 >> -one.e; // d = (10 * p2) div 2^-e + const std::uint64_t r = p2 & (one.f - 1); // r = (10 * p2) mod 2^-e + // + // M+ = buffer * 10^-m + 10^-m * (1/10 * (d * 2^-e + r) * 2^e + // = buffer * 10^-m + 10^-m * (1/10 * (d + r * 2^e)) + // = (buffer * 10 + d) * 10^(-m-1) + 10^(-m-1) * r * 2^e + // + JSON_ASSERT(d <= 9); + buffer[length++] = static_cast('0' + d); // buffer := buffer * 10 + d + // + // M+ = buffer * 10^(-m-1) + 10^(-m-1) * r * 2^e + // + p2 = r; + m++; + // + // M+ = buffer * 10^-m + 10^-m * p2 * 2^e + // Invariant restored. + + // Check if enough digits have been generated. + // + // 10^-m * p2 * 2^e <= delta * 2^e + // p2 * 2^e <= 10^m * delta * 2^e + // p2 <= 10^m * delta + delta *= 10; + dist *= 10; + if (p2 <= delta) + { + break; + } + } + + // V = buffer * 10^-m, with M- <= V <= M+. + + decimal_exponent -= m; + + // 1 ulp in the decimal representation is now 10^-m. + // Since delta and dist are now scaled by 10^m, we need to do the + // same with ulp in order to keep the units in sync. + // + // 10^m * 10^-m = 1 = 2^-e * 2^e = ten_m * 2^e + // + const std::uint64_t ten_m = one.f; + grisu2_round(buffer, length, dist, delta, p2, ten_m); + + // By construction this algorithm generates the shortest possible decimal + // number (Loitsch, Theorem 6.2) which rounds back to w. + // For an input number of precision p, at least + // + // N = 1 + ceil(p * log_10(2)) + // + // decimal digits are sufficient to identify all binary floating-point + // numbers (Matula, "In-and-Out conversions"). + // This implies that the algorithm does not produce more than N decimal + // digits. + // + // N = 17 for p = 53 (IEEE double precision) + // N = 9 for p = 24 (IEEE single precision) +} + +/*! +v = buf * 10^decimal_exponent +len is the length of the buffer (number of decimal digits) +The buffer must be large enough, i.e. >= max_digits10. +*/ +JSON_HEDLEY_NON_NULL(1) +inline void grisu2(char* buf, int& len, int& decimal_exponent, + diyfp m_minus, diyfp v, diyfp m_plus) +{ + JSON_ASSERT(m_plus.e == m_minus.e); + JSON_ASSERT(m_plus.e == v.e); + + // --------(-----------------------+-----------------------)-------- (A) + // m- v m+ + // + // --------------------(-----------+-----------------------)-------- (B) + // m- v m+ + // + // First scale v (and m- and m+) such that the exponent is in the range + // [alpha, gamma]. + + const cached_power cached = get_cached_power_for_binary_exponent(m_plus.e); + + const diyfp c_minus_k(cached.f, cached.e); // = c ~= 10^-k + + // The exponent of the products is = v.e + c_minus_k.e + q and is in the range [alpha,gamma] + const diyfp w = diyfp::mul(v, c_minus_k); + const diyfp w_minus = diyfp::mul(m_minus, c_minus_k); + const diyfp w_plus = diyfp::mul(m_plus, c_minus_k); + + // ----(---+---)---------------(---+---)---------------(---+---)---- + // w- w w+ + // = c*m- = c*v = c*m+ + // + // diyfp::mul rounds its result and c_minus_k is approximated too. w, w- and + // w+ are now off by a small amount. + // In fact: + // + // w - v * 10^k < 1 ulp + // + // To account for this inaccuracy, add resp. subtract 1 ulp. + // + // --------+---[---------------(---+---)---------------]---+-------- + // w- M- w M+ w+ + // + // Now any number in [M-, M+] (bounds included) will round to w when input, + // regardless of how the input rounding algorithm breaks ties. + // + // And digit_gen generates the shortest possible such number in [M-, M+]. + // Note that this does not mean that Grisu2 always generates the shortest + // possible number in the interval (m-, m+). + const diyfp M_minus(w_minus.f + 1, w_minus.e); + const diyfp M_plus (w_plus.f - 1, w_plus.e ); + + decimal_exponent = -cached.k; // = -(-k) = k + + grisu2_digit_gen(buf, len, decimal_exponent, M_minus, w, M_plus); +} + +/*! +v = buf * 10^decimal_exponent +len is the length of the buffer (number of decimal digits) +The buffer must be large enough, i.e. >= max_digits10. +*/ +template +JSON_HEDLEY_NON_NULL(1) +void grisu2(char* buf, int& len, int& decimal_exponent, FloatType value) +{ + static_assert(diyfp::kPrecision >= std::numeric_limits::digits + 3, + "internal error: not enough precision"); + + JSON_ASSERT(std::isfinite(value)); + JSON_ASSERT(value > 0); + + // If the neighbors (and boundaries) of 'value' are always computed for double-precision + // numbers, all float's can be recovered using strtod (and strtof). However, the resulting + // decimal representations are not exactly "short". + // + // The documentation for 'std::to_chars' (https://en.cppreference.com/w/cpp/utility/to_chars) + // says "value is converted to a string as if by std::sprintf in the default ("C") locale" + // and since sprintf promotes float's to double's, I think this is exactly what 'std::to_chars' + // does. + // On the other hand, the documentation for 'std::to_chars' requires that "parsing the + // representation using the corresponding std::from_chars function recovers value exactly". That + // indicates that single precision floating-point numbers should be recovered using + // 'std::strtof'. + // + // NB: If the neighbors are computed for single-precision numbers, there is a single float + // (7.0385307e-26f) which can't be recovered using strtod. The resulting double precision + // value is off by 1 ulp. +#if 0 + const boundaries w = compute_boundaries(static_cast(value)); +#else + const boundaries w = compute_boundaries(value); +#endif + + grisu2(buf, len, decimal_exponent, w.minus, w.w, w.plus); +} + +/*! +@brief appends a decimal representation of e to buf +@return a pointer to the element following the exponent. +@pre -1000 < e < 1000 +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* append_exponent(char* buf, int e) +{ + JSON_ASSERT(e > -1000); + JSON_ASSERT(e < 1000); + + if (e < 0) + { + e = -e; + *buf++ = '-'; + } + else + { + *buf++ = '+'; + } + + auto k = static_cast(e); + if (k < 10) + { + // Always print at least two digits in the exponent. + // This is for compatibility with printf("%g"). + *buf++ = '0'; + *buf++ = static_cast('0' + k); + } + else if (k < 100) + { + *buf++ = static_cast('0' + k / 10); + k %= 10; + *buf++ = static_cast('0' + k); + } + else + { + *buf++ = static_cast('0' + k / 100); + k %= 100; + *buf++ = static_cast('0' + k / 10); + k %= 10; + *buf++ = static_cast('0' + k); + } + + return buf; +} + +/*! +@brief prettify v = buf * 10^decimal_exponent + +If v is in the range [10^min_exp, 10^max_exp) it will be printed in fixed-point +notation. Otherwise it will be printed in exponential notation. + +@pre min_exp < 0 +@pre max_exp > 0 +*/ +JSON_HEDLEY_NON_NULL(1) +JSON_HEDLEY_RETURNS_NON_NULL +inline char* format_buffer(char* buf, int len, int decimal_exponent, + int min_exp, int max_exp) +{ + JSON_ASSERT(min_exp < 0); + JSON_ASSERT(max_exp > 0); + + const int k = len; + const int n = len + decimal_exponent; + + // v = buf * 10^(n-k) + // k is the length of the buffer (number of decimal digits) + // n is the position of the decimal point relative to the start of the buffer. + + if (k <= n && n <= max_exp) + { + // digits[000] + // len <= max_exp + 2 + + std::memset(buf + k, '0', static_cast(n) - static_cast(k)); + // Make it look like a floating-point number (#362, #378) + buf[n + 0] = '.'; + buf[n + 1] = '0'; + return buf + (static_cast(n) + 2); + } + + if (0 < n && n <= max_exp) + { + // dig.its + // len <= max_digits10 + 1 + + JSON_ASSERT(k > n); + + std::memmove(buf + (static_cast(n) + 1), buf + n, static_cast(k) - static_cast(n)); + buf[n] = '.'; + return buf + (static_cast(k) + 1U); + } + + if (min_exp < n && n <= 0) + { + // 0.[000]digits + // len <= 2 + (-min_exp - 1) + max_digits10 + + std::memmove(buf + (2 + static_cast(-n)), buf, static_cast(k)); + buf[0] = '0'; + buf[1] = '.'; + std::memset(buf + 2, '0', static_cast(-n)); + return buf + (2U + static_cast(-n) + static_cast(k)); + } + + if (k == 1) + { + // dE+123 + // len <= 1 + 5 + + buf += 1; + } + else + { + // d.igitsE+123 + // len <= max_digits10 + 1 + 5 + + std::memmove(buf + 2, buf + 1, static_cast(k) - 1); + buf[1] = '.'; + buf += 1 + static_cast(k); + } + + *buf++ = 'e'; + return append_exponent(buf, n - 1); +} + +} // namespace dtoa_impl + +/*! +@brief generates a decimal representation of the floating-point number value in [first, last). + +The format of the resulting decimal representation is similar to printf's %g +format. Returns an iterator pointing past-the-end of the decimal representation. + +@note The input number must be finite, i.e. NaN's and Inf's are not supported. +@note The buffer must be large enough. +@note The result is NOT null-terminated. +*/ +template +JSON_HEDLEY_NON_NULL(1, 2) +JSON_HEDLEY_RETURNS_NON_NULL +char* to_chars(char* first, const char* last, FloatType value) +{ + static_cast(last); // maybe unused - fix warning + JSON_ASSERT(std::isfinite(value)); + + // Use signbit(value) instead of (value < 0) since signbit works for -0. + if (std::signbit(value)) + { + value = -value; + *first++ = '-'; + } + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + if (value == 0) // +-0 + { + *first++ = '0'; + // Make it look like a floating-point number (#362, #378) + *first++ = '.'; + *first++ = '0'; + return first; + } +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + + JSON_ASSERT(last - first >= std::numeric_limits::max_digits10); + + // Compute v = buffer * 10^decimal_exponent. + // The decimal digits are stored in the buffer, which needs to be interpreted + // as an unsigned decimal integer. + // len is the length of the buffer, i.e. the number of decimal digits. + int len = 0; + int decimal_exponent = 0; + dtoa_impl::grisu2(first, len, decimal_exponent, value); + + JSON_ASSERT(len <= std::numeric_limits::max_digits10); + + // Format the buffer like printf("%.*g", prec, value) + constexpr int kMinExp = -4; + // Use digits10 here to increase compatibility with version 2. + constexpr int kMaxExp = std::numeric_limits::digits10; + + JSON_ASSERT(last - first >= kMaxExp + 2); + JSON_ASSERT(last - first >= 2 + (-kMinExp - 1) + std::numeric_limits::max_digits10); + JSON_ASSERT(last - first >= std::numeric_limits::max_digits10 + 6); + + return dtoa_impl::format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp); +} + +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + +// #include + +// #include + +// #include + +// #include + + +namespace nlohmann +{ +namespace detail +{ +/////////////////// +// serialization // +/////////////////// + +/// how to treat decoding errors +enum class error_handler_t +{ + strict, ///< throw a type_error exception in case of invalid UTF-8 + replace, ///< replace invalid UTF-8 sequences with U+FFFD + ignore ///< ignore invalid UTF-8 sequences +}; + +template +class serializer +{ + using string_t = typename BasicJsonType::string_t; + using number_float_t = typename BasicJsonType::number_float_t; + using number_integer_t = typename BasicJsonType::number_integer_t; + using number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using binary_char_t = typename BasicJsonType::binary_t::value_type; + static constexpr std::uint8_t UTF8_ACCEPT = 0; + static constexpr std::uint8_t UTF8_REJECT = 1; + + public: + /*! + @param[in] s output stream to serialize to + @param[in] ichar indentation character to use + @param[in] error_handler_ how to react on decoding errors + */ + serializer(output_adapter_t s, const char ichar, + error_handler_t error_handler_ = error_handler_t::strict) + : o(std::move(s)) + , loc(std::localeconv()) + , thousands_sep(loc->thousands_sep == nullptr ? '\0' : std::char_traits::to_char_type(* (loc->thousands_sep))) + , decimal_point(loc->decimal_point == nullptr ? '\0' : std::char_traits::to_char_type(* (loc->decimal_point))) + , indent_char(ichar) + , indent_string(512, indent_char) + , error_handler(error_handler_) + {} + + // delete because of pointer members + serializer(const serializer&) = delete; + serializer& operator=(const serializer&) = delete; + serializer(serializer&&) = delete; + serializer& operator=(serializer&&) = delete; + ~serializer() = default; + + /*! + @brief internal implementation of the serialization function + + This function is called by the public member function dump and organizes + the serialization internally. The indentation level is propagated as + additional parameter. In case of arrays and objects, the function is + called recursively. + + - strings and object keys are escaped using `escape_string()` + - integer numbers are converted implicitly via `operator<<` + - floating-point numbers are converted to a string using `"%g"` format + - binary values are serialized as objects containing the subtype and the + byte array + + @param[in] val value to serialize + @param[in] pretty_print whether the output shall be pretty-printed + @param[in] ensure_ascii If @a ensure_ascii is true, all non-ASCII characters + in the output are escaped with `\uXXXX` sequences, and the result consists + of ASCII characters only. + @param[in] indent_step the indent level + @param[in] current_indent the current indent level (only used internally) + */ + void dump(const BasicJsonType& val, + const bool pretty_print, + const bool ensure_ascii, + const unsigned int indent_step, + const unsigned int current_indent = 0) + { + switch (val.m_type) + { + case value_t::object: + { + if (val.m_value.object->empty()) + { + o->write_characters("{}", 2); + return; + } + + if (pretty_print) + { + o->write_characters("{\n", 2); + + // variable to hold indentation for recursive calls + const auto new_indent = current_indent + indent_step; + if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) + { + indent_string.resize(indent_string.size() * 2, ' '); + } + + // first n-1 elements + auto i = val.m_value.object->cbegin(); + for (std::size_t cnt = 0; cnt < val.m_value.object->size() - 1; ++cnt, ++i) + { + o->write_characters(indent_string.c_str(), new_indent); + o->write_character('\"'); + dump_escaped(i->first, ensure_ascii); + o->write_characters("\": ", 3); + dump(i->second, true, ensure_ascii, indent_step, new_indent); + o->write_characters(",\n", 2); + } + + // last element + JSON_ASSERT(i != val.m_value.object->cend()); + JSON_ASSERT(std::next(i) == val.m_value.object->cend()); + o->write_characters(indent_string.c_str(), new_indent); + o->write_character('\"'); + dump_escaped(i->first, ensure_ascii); + o->write_characters("\": ", 3); + dump(i->second, true, ensure_ascii, indent_step, new_indent); + + o->write_character('\n'); + o->write_characters(indent_string.c_str(), current_indent); + o->write_character('}'); + } + else + { + o->write_character('{'); + + // first n-1 elements + auto i = val.m_value.object->cbegin(); + for (std::size_t cnt = 0; cnt < val.m_value.object->size() - 1; ++cnt, ++i) + { + o->write_character('\"'); + dump_escaped(i->first, ensure_ascii); + o->write_characters("\":", 2); + dump(i->second, false, ensure_ascii, indent_step, current_indent); + o->write_character(','); + } + + // last element + JSON_ASSERT(i != val.m_value.object->cend()); + JSON_ASSERT(std::next(i) == val.m_value.object->cend()); + o->write_character('\"'); + dump_escaped(i->first, ensure_ascii); + o->write_characters("\":", 2); + dump(i->second, false, ensure_ascii, indent_step, current_indent); + + o->write_character('}'); + } + + return; + } + + case value_t::array: + { + if (val.m_value.array->empty()) + { + o->write_characters("[]", 2); + return; + } + + if (pretty_print) + { + o->write_characters("[\n", 2); + + // variable to hold indentation for recursive calls + const auto new_indent = current_indent + indent_step; + if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) + { + indent_string.resize(indent_string.size() * 2, ' '); + } + + // first n-1 elements + for (auto i = val.m_value.array->cbegin(); + i != val.m_value.array->cend() - 1; ++i) + { + o->write_characters(indent_string.c_str(), new_indent); + dump(*i, true, ensure_ascii, indent_step, new_indent); + o->write_characters(",\n", 2); + } + + // last element + JSON_ASSERT(!val.m_value.array->empty()); + o->write_characters(indent_string.c_str(), new_indent); + dump(val.m_value.array->back(), true, ensure_ascii, indent_step, new_indent); + + o->write_character('\n'); + o->write_characters(indent_string.c_str(), current_indent); + o->write_character(']'); + } + else + { + o->write_character('['); + + // first n-1 elements + for (auto i = val.m_value.array->cbegin(); + i != val.m_value.array->cend() - 1; ++i) + { + dump(*i, false, ensure_ascii, indent_step, current_indent); + o->write_character(','); + } + + // last element + JSON_ASSERT(!val.m_value.array->empty()); + dump(val.m_value.array->back(), false, ensure_ascii, indent_step, current_indent); + + o->write_character(']'); + } + + return; + } + + case value_t::string: + { + o->write_character('\"'); + dump_escaped(*val.m_value.string, ensure_ascii); + o->write_character('\"'); + return; + } + + case value_t::binary: + { + if (pretty_print) + { + o->write_characters("{\n", 2); + + // variable to hold indentation for recursive calls + const auto new_indent = current_indent + indent_step; + if (JSON_HEDLEY_UNLIKELY(indent_string.size() < new_indent)) + { + indent_string.resize(indent_string.size() * 2, ' '); + } + + o->write_characters(indent_string.c_str(), new_indent); + + o->write_characters("\"bytes\": [", 10); + + if (!val.m_value.binary->empty()) + { + for (auto i = val.m_value.binary->cbegin(); + i != val.m_value.binary->cend() - 1; ++i) + { + dump_integer(*i); + o->write_characters(", ", 2); + } + dump_integer(val.m_value.binary->back()); + } + + o->write_characters("],\n", 3); + o->write_characters(indent_string.c_str(), new_indent); + + o->write_characters("\"subtype\": ", 11); + if (val.m_value.binary->has_subtype()) + { + dump_integer(val.m_value.binary->subtype()); + } + else + { + o->write_characters("null", 4); + } + o->write_character('\n'); + o->write_characters(indent_string.c_str(), current_indent); + o->write_character('}'); + } + else + { + o->write_characters("{\"bytes\":[", 10); + + if (!val.m_value.binary->empty()) + { + for (auto i = val.m_value.binary->cbegin(); + i != val.m_value.binary->cend() - 1; ++i) + { + dump_integer(*i); + o->write_character(','); + } + dump_integer(val.m_value.binary->back()); + } + + o->write_characters("],\"subtype\":", 12); + if (val.m_value.binary->has_subtype()) + { + dump_integer(val.m_value.binary->subtype()); + o->write_character('}'); + } + else + { + o->write_characters("null}", 5); + } + } + return; + } + + case value_t::boolean: + { + if (val.m_value.boolean) + { + o->write_characters("true", 4); + } + else + { + o->write_characters("false", 5); + } + return; + } + + case value_t::number_integer: + { + dump_integer(val.m_value.number_integer); + return; + } + + case value_t::number_unsigned: + { + dump_integer(val.m_value.number_unsigned); + return; + } + + case value_t::number_float: + { + dump_float(val.m_value.number_float); + return; + } + + case value_t::discarded: + { + o->write_characters("", 11); + return; + } + + case value_t::null: + { + o->write_characters("null", 4); + return; + } + + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + } + + JSON_PRIVATE_UNLESS_TESTED: + /*! + @brief dump escaped string + + Escape a string by replacing certain special characters by a sequence of an + escape character (backslash) and another character and other control + characters by a sequence of "\u" followed by a four-digit hex + representation. The escaped string is written to output stream @a o. + + @param[in] s the string to escape + @param[in] ensure_ascii whether to escape non-ASCII characters with + \uXXXX sequences + + @complexity Linear in the length of string @a s. + */ + void dump_escaped(const string_t& s, const bool ensure_ascii) + { + std::uint32_t codepoint{}; + std::uint8_t state = UTF8_ACCEPT; + std::size_t bytes = 0; // number of bytes written to string_buffer + + // number of bytes written at the point of the last valid byte + std::size_t bytes_after_last_accept = 0; + std::size_t undumped_chars = 0; + + for (std::size_t i = 0; i < s.size(); ++i) + { + const auto byte = static_cast(s[i]); + + switch (decode(state, codepoint, byte)) + { + case UTF8_ACCEPT: // decode found a new code point + { + switch (codepoint) + { + case 0x08: // backspace + { + string_buffer[bytes++] = '\\'; + string_buffer[bytes++] = 'b'; + break; + } + + case 0x09: // horizontal tab + { + string_buffer[bytes++] = '\\'; + string_buffer[bytes++] = 't'; + break; + } + + case 0x0A: // newline + { + string_buffer[bytes++] = '\\'; + string_buffer[bytes++] = 'n'; + break; + } + + case 0x0C: // formfeed + { + string_buffer[bytes++] = '\\'; + string_buffer[bytes++] = 'f'; + break; + } + + case 0x0D: // carriage return + { + string_buffer[bytes++] = '\\'; + string_buffer[bytes++] = 'r'; + break; + } + + case 0x22: // quotation mark + { + string_buffer[bytes++] = '\\'; + string_buffer[bytes++] = '\"'; + break; + } + + case 0x5C: // reverse solidus + { + string_buffer[bytes++] = '\\'; + string_buffer[bytes++] = '\\'; + break; + } + + default: + { + // escape control characters (0x00..0x1F) or, if + // ensure_ascii parameter is used, non-ASCII characters + if ((codepoint <= 0x1F) || (ensure_ascii && (codepoint >= 0x7F))) + { + if (codepoint <= 0xFFFF) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + (std::snprintf)(string_buffer.data() + bytes, 7, "\\u%04x", + static_cast(codepoint)); + bytes += 6; + } + else + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + (std::snprintf)(string_buffer.data() + bytes, 13, "\\u%04x\\u%04x", + static_cast(0xD7C0u + (codepoint >> 10u)), + static_cast(0xDC00u + (codepoint & 0x3FFu))); + bytes += 12; + } + } + else + { + // copy byte to buffer (all previous bytes + // been copied have in default case above) + string_buffer[bytes++] = s[i]; + } + break; + } + } + + // write buffer and reset index; there must be 13 bytes + // left, as this is the maximal number of bytes to be + // written ("\uxxxx\uxxxx\0") for one code point + if (string_buffer.size() - bytes < 13) + { + o->write_characters(string_buffer.data(), bytes); + bytes = 0; + } + + // remember the byte position of this accept + bytes_after_last_accept = bytes; + undumped_chars = 0; + break; + } + + case UTF8_REJECT: // decode found invalid UTF-8 byte + { + switch (error_handler) + { + case error_handler_t::strict: + { + std::string sn(9, '\0'); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + (std::snprintf)(&sn[0], sn.size(), "%.2X", byte); + JSON_THROW(type_error::create(316, "invalid UTF-8 byte at index " + std::to_string(i) + ": 0x" + sn, BasicJsonType())); + } + + case error_handler_t::ignore: + case error_handler_t::replace: + { + // in case we saw this character the first time, we + // would like to read it again, because the byte + // may be OK for itself, but just not OK for the + // previous sequence + if (undumped_chars > 0) + { + --i; + } + + // reset length buffer to the last accepted index; + // thus removing/ignoring the invalid characters + bytes = bytes_after_last_accept; + + if (error_handler == error_handler_t::replace) + { + // add a replacement character + if (ensure_ascii) + { + string_buffer[bytes++] = '\\'; + string_buffer[bytes++] = 'u'; + string_buffer[bytes++] = 'f'; + string_buffer[bytes++] = 'f'; + string_buffer[bytes++] = 'f'; + string_buffer[bytes++] = 'd'; + } + else + { + string_buffer[bytes++] = detail::binary_writer::to_char_type('\xEF'); + string_buffer[bytes++] = detail::binary_writer::to_char_type('\xBF'); + string_buffer[bytes++] = detail::binary_writer::to_char_type('\xBD'); + } + + // write buffer and reset index; there must be 13 bytes + // left, as this is the maximal number of bytes to be + // written ("\uxxxx\uxxxx\0") for one code point + if (string_buffer.size() - bytes < 13) + { + o->write_characters(string_buffer.data(), bytes); + bytes = 0; + } + + bytes_after_last_accept = bytes; + } + + undumped_chars = 0; + + // continue processing the string + state = UTF8_ACCEPT; + break; + } + + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + break; + } + + default: // decode found yet incomplete multi-byte code point + { + if (!ensure_ascii) + { + // code point will not be escaped - copy byte to buffer + string_buffer[bytes++] = s[i]; + } + ++undumped_chars; + break; + } + } + } + + // we finished processing the string + if (JSON_HEDLEY_LIKELY(state == UTF8_ACCEPT)) + { + // write buffer + if (bytes > 0) + { + o->write_characters(string_buffer.data(), bytes); + } + } + else + { + // we finish reading, but do not accept: string was incomplete + switch (error_handler) + { + case error_handler_t::strict: + { + std::string sn(9, '\0'); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + (std::snprintf)(&sn[0], sn.size(), "%.2X", static_cast(s.back())); + JSON_THROW(type_error::create(316, "incomplete UTF-8 string; last byte: 0x" + sn, BasicJsonType())); + } + + case error_handler_t::ignore: + { + // write all accepted bytes + o->write_characters(string_buffer.data(), bytes_after_last_accept); + break; + } + + case error_handler_t::replace: + { + // write all accepted bytes + o->write_characters(string_buffer.data(), bytes_after_last_accept); + // add a replacement character + if (ensure_ascii) + { + o->write_characters("\\ufffd", 6); + } + else + { + o->write_characters("\xEF\xBF\xBD", 3); + } + break; + } + + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + } + } + + private: + /*! + @brief count digits + + Count the number of decimal (base 10) digits for an input unsigned integer. + + @param[in] x unsigned integer number to count its digits + @return number of decimal digits + */ + inline unsigned int count_digits(number_unsigned_t x) noexcept + { + unsigned int n_digits = 1; + for (;;) + { + if (x < 10) + { + return n_digits; + } + if (x < 100) + { + return n_digits + 1; + } + if (x < 1000) + { + return n_digits + 2; + } + if (x < 10000) + { + return n_digits + 3; + } + x = x / 10000u; + n_digits += 4; + } + } + + /*! + @brief dump an integer + + Dump a given integer to output stream @a o. Works internally with + @a number_buffer. + + @param[in] x integer number (signed or unsigned) to dump + @tparam NumberType either @a number_integer_t or @a number_unsigned_t + */ + template < typename NumberType, detail::enable_if_t < + std::is_integral::value || + std::is_same::value || + std::is_same::value || + std::is_same::value, + int > = 0 > + void dump_integer(NumberType x) + { + static constexpr std::array, 100> digits_to_99 + { + { + {{'0', '0'}}, {{'0', '1'}}, {{'0', '2'}}, {{'0', '3'}}, {{'0', '4'}}, {{'0', '5'}}, {{'0', '6'}}, {{'0', '7'}}, {{'0', '8'}}, {{'0', '9'}}, + {{'1', '0'}}, {{'1', '1'}}, {{'1', '2'}}, {{'1', '3'}}, {{'1', '4'}}, {{'1', '5'}}, {{'1', '6'}}, {{'1', '7'}}, {{'1', '8'}}, {{'1', '9'}}, + {{'2', '0'}}, {{'2', '1'}}, {{'2', '2'}}, {{'2', '3'}}, {{'2', '4'}}, {{'2', '5'}}, {{'2', '6'}}, {{'2', '7'}}, {{'2', '8'}}, {{'2', '9'}}, + {{'3', '0'}}, {{'3', '1'}}, {{'3', '2'}}, {{'3', '3'}}, {{'3', '4'}}, {{'3', '5'}}, {{'3', '6'}}, {{'3', '7'}}, {{'3', '8'}}, {{'3', '9'}}, + {{'4', '0'}}, {{'4', '1'}}, {{'4', '2'}}, {{'4', '3'}}, {{'4', '4'}}, {{'4', '5'}}, {{'4', '6'}}, {{'4', '7'}}, {{'4', '8'}}, {{'4', '9'}}, + {{'5', '0'}}, {{'5', '1'}}, {{'5', '2'}}, {{'5', '3'}}, {{'5', '4'}}, {{'5', '5'}}, {{'5', '6'}}, {{'5', '7'}}, {{'5', '8'}}, {{'5', '9'}}, + {{'6', '0'}}, {{'6', '1'}}, {{'6', '2'}}, {{'6', '3'}}, {{'6', '4'}}, {{'6', '5'}}, {{'6', '6'}}, {{'6', '7'}}, {{'6', '8'}}, {{'6', '9'}}, + {{'7', '0'}}, {{'7', '1'}}, {{'7', '2'}}, {{'7', '3'}}, {{'7', '4'}}, {{'7', '5'}}, {{'7', '6'}}, {{'7', '7'}}, {{'7', '8'}}, {{'7', '9'}}, + {{'8', '0'}}, {{'8', '1'}}, {{'8', '2'}}, {{'8', '3'}}, {{'8', '4'}}, {{'8', '5'}}, {{'8', '6'}}, {{'8', '7'}}, {{'8', '8'}}, {{'8', '9'}}, + {{'9', '0'}}, {{'9', '1'}}, {{'9', '2'}}, {{'9', '3'}}, {{'9', '4'}}, {{'9', '5'}}, {{'9', '6'}}, {{'9', '7'}}, {{'9', '8'}}, {{'9', '9'}}, + } + }; + + // special case for "0" + if (x == 0) + { + o->write_character('0'); + return; + } + + // use a pointer to fill the buffer + auto buffer_ptr = number_buffer.begin(); // NOLINT(llvm-qualified-auto,readability-qualified-auto,cppcoreguidelines-pro-type-vararg,hicpp-vararg) + + const bool is_negative = std::is_signed::value && !(x >= 0); // see issue #755 + number_unsigned_t abs_value; + + unsigned int n_chars{}; + + if (is_negative) + { + *buffer_ptr = '-'; + abs_value = remove_sign(static_cast(x)); + + // account one more byte for the minus sign + n_chars = 1 + count_digits(abs_value); + } + else + { + abs_value = static_cast(x); + n_chars = count_digits(abs_value); + } + + // spare 1 byte for '\0' + JSON_ASSERT(n_chars < number_buffer.size() - 1); + + // jump to the end to generate the string from backward + // so we later avoid reversing the result + buffer_ptr += n_chars; + + // Fast int2ascii implementation inspired by "Fastware" talk by Andrei Alexandrescu + // See: https://www.youtube.com/watch?v=o4-CwDo2zpg + while (abs_value >= 100) + { + const auto digits_index = static_cast((abs_value % 100)); + abs_value /= 100; + *(--buffer_ptr) = digits_to_99[digits_index][1]; + *(--buffer_ptr) = digits_to_99[digits_index][0]; + } + + if (abs_value >= 10) + { + const auto digits_index = static_cast(abs_value); + *(--buffer_ptr) = digits_to_99[digits_index][1]; + *(--buffer_ptr) = digits_to_99[digits_index][0]; + } + else + { + *(--buffer_ptr) = static_cast('0' + abs_value); + } + + o->write_characters(number_buffer.data(), n_chars); + } + + /*! + @brief dump a floating-point number + + Dump a given floating-point number to output stream @a o. Works internally + with @a number_buffer. + + @param[in] x floating-point number to dump + */ + void dump_float(number_float_t x) + { + // NaN / inf + if (!std::isfinite(x)) + { + o->write_characters("null", 4); + return; + } + + // If number_float_t is an IEEE-754 single or double precision number, + // use the Grisu2 algorithm to produce short numbers which are + // guaranteed to round-trip, using strtof and strtod, resp. + // + // NB: The test below works if == . + static constexpr bool is_ieee_single_or_double + = (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 24 && std::numeric_limits::max_exponent == 128) || + (std::numeric_limits::is_iec559 && std::numeric_limits::digits == 53 && std::numeric_limits::max_exponent == 1024); + + dump_float(x, std::integral_constant()); + } + + void dump_float(number_float_t x, std::true_type /*is_ieee_single_or_double*/) + { + auto* begin = number_buffer.data(); + auto* end = ::nlohmann::detail::to_chars(begin, begin + number_buffer.size(), x); + + o->write_characters(begin, static_cast(end - begin)); + } + + void dump_float(number_float_t x, std::false_type /*is_ieee_single_or_double*/) + { + // get number of digits for a float -> text -> float round-trip + static constexpr auto d = std::numeric_limits::max_digits10; + + // the actual conversion + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg,hicpp-vararg) + std::ptrdiff_t len = (std::snprintf)(number_buffer.data(), number_buffer.size(), "%.*g", d, x); + + // negative value indicates an error + JSON_ASSERT(len > 0); + // check if buffer was large enough + JSON_ASSERT(static_cast(len) < number_buffer.size()); + + // erase thousands separator + if (thousands_sep != '\0') + { + // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::remove returns an iterator, see https://github.com/nlohmann/json/issues/3081 + const auto end = std::remove(number_buffer.begin(), number_buffer.begin() + len, thousands_sep); + std::fill(end, number_buffer.end(), '\0'); + JSON_ASSERT((end - number_buffer.begin()) <= len); + len = (end - number_buffer.begin()); + } + + // convert decimal point to '.' + if (decimal_point != '\0' && decimal_point != '.') + { + // NOLINTNEXTLINE(readability-qualified-auto,llvm-qualified-auto): std::find returns an iterator, see https://github.com/nlohmann/json/issues/3081 + const auto dec_pos = std::find(number_buffer.begin(), number_buffer.end(), decimal_point); + if (dec_pos != number_buffer.end()) + { + *dec_pos = '.'; + } + } + + o->write_characters(number_buffer.data(), static_cast(len)); + + // determine if need to append ".0" + const bool value_is_int_like = + std::none_of(number_buffer.begin(), number_buffer.begin() + len + 1, + [](char c) + { + return c == '.' || c == 'e'; + }); + + if (value_is_int_like) + { + o->write_characters(".0", 2); + } + } + + /*! + @brief check whether a string is UTF-8 encoded + + The function checks each byte of a string whether it is UTF-8 encoded. The + result of the check is stored in the @a state parameter. The function must + be called initially with state 0 (accept). State 1 means the string must + be rejected, because the current byte is not allowed. If the string is + completely processed, but the state is non-zero, the string ended + prematurely; that is, the last byte indicated more bytes should have + followed. + + @param[in,out] state the state of the decoding + @param[in,out] codep codepoint (valid only if resulting state is UTF8_ACCEPT) + @param[in] byte next byte to decode + @return new state + + @note The function has been edited: a std::array is used. + + @copyright Copyright (c) 2008-2009 Bjoern Hoehrmann + @sa http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + */ + static std::uint8_t decode(std::uint8_t& state, std::uint32_t& codep, const std::uint8_t byte) noexcept + { + static const std::array utf8d = + { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5F + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7F + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9F + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // A0..BF + 8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C0..DF + 0xA, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // E0..EF + 0xB, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // F0..FF + 0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2 + 1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4 + 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6 + 1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // s7..s8 + } + }; + + JSON_ASSERT(byte < utf8d.size()); + const std::uint8_t type = utf8d[byte]; + + codep = (state != UTF8_ACCEPT) + ? (byte & 0x3fu) | (codep << 6u) + : (0xFFu >> type) & (byte); + + std::size_t index = 256u + static_cast(state) * 16u + static_cast(type); + JSON_ASSERT(index < 400); + state = utf8d[index]; + return state; + } + + /* + * Overload to make the compiler happy while it is instantiating + * dump_integer for number_unsigned_t. + * Must never be called. + */ + number_unsigned_t remove_sign(number_unsigned_t x) + { + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + return x; // LCOV_EXCL_LINE + } + + /* + * Helper function for dump_integer + * + * This function takes a negative signed integer and returns its absolute + * value as unsigned integer. The plus/minus shuffling is necessary as we can + * not directly remove the sign of an arbitrary signed integer as the + * absolute values of INT_MIN and INT_MAX are usually not the same. See + * #1708 for details. + */ + inline number_unsigned_t remove_sign(number_integer_t x) noexcept + { + JSON_ASSERT(x < 0 && x < (std::numeric_limits::max)()); // NOLINT(misc-redundant-expression) + return static_cast(-(x + 1)) + 1; + } + + private: + /// the output of the serializer + output_adapter_t o = nullptr; + + /// a (hopefully) large enough character buffer + std::array number_buffer{{}}; + + /// the locale + const std::lconv* loc = nullptr; + /// the locale's thousand separator character + const char thousands_sep = '\0'; + /// the locale's decimal point character + const char decimal_point = '\0'; + + /// string buffer + std::array string_buffer{{}}; + + /// the indentation character + const char indent_char; + /// the indentation string + string_t indent_string; + + /// error_handler how to react on decoding errors + const error_handler_t error_handler; +}; +} // namespace detail +} // namespace nlohmann + +// #include + +// #include + +// #include + + +#include // less +#include // initializer_list +#include // input_iterator_tag, iterator_traits +#include // allocator +#include // for out_of_range +#include // enable_if, is_convertible +#include // pair +#include // vector + +// #include + + +namespace nlohmann +{ + +/// ordered_map: a minimal map-like container that preserves insertion order +/// for use within nlohmann::basic_json +template , + class Allocator = std::allocator>> + struct ordered_map : std::vector, Allocator> +{ + using key_type = Key; + using mapped_type = T; + using Container = std::vector, Allocator>; + using typename Container::iterator; + using typename Container::const_iterator; + using typename Container::size_type; + using typename Container::value_type; + + // Explicit constructors instead of `using Container::Container` + // otherwise older compilers choke on it (GCC <= 5.5, xcode <= 9.4) + ordered_map(const Allocator& alloc = Allocator()) : Container{alloc} {} + template + ordered_map(It first, It last, const Allocator& alloc = Allocator()) + : Container{first, last, alloc} {} + ordered_map(std::initializer_list init, const Allocator& alloc = Allocator() ) + : Container{init, alloc} {} + + std::pair emplace(const key_type& key, T&& t) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (it->first == key) + { + return {it, false}; + } + } + Container::emplace_back(key, t); + return {--this->end(), true}; + } + + T& operator[](const Key& key) + { + return emplace(key, T{}).first->second; + } + + const T& operator[](const Key& key) const + { + return at(key); + } + + T& at(const Key& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (it->first == key) + { + return it->second; + } + } + + JSON_THROW(std::out_of_range("key not found")); + } + + const T& at(const Key& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (it->first == key) + { + return it->second; + } + } + + JSON_THROW(std::out_of_range("key not found")); + } + + size_type erase(const Key& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (it->first == key) + { + // Since we cannot move const Keys, re-construct them in place + for (auto next = it; ++next != this->end(); ++it) + { + it->~value_type(); // Destroy but keep allocation + new (&*it) value_type{std::move(*next)}; + } + Container::pop_back(); + return 1; + } + } + return 0; + } + + iterator erase(iterator pos) + { + auto it = pos; + + // Since we cannot move const Keys, re-construct them in place + for (auto next = it; ++next != this->end(); ++it) + { + it->~value_type(); // Destroy but keep allocation + new (&*it) value_type{std::move(*next)}; + } + Container::pop_back(); + return pos; + } + + size_type count(const Key& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (it->first == key) + { + return 1; + } + } + return 0; + } + + iterator find(const Key& key) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (it->first == key) + { + return it; + } + } + return Container::end(); + } + + const_iterator find(const Key& key) const + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (it->first == key) + { + return it; + } + } + return Container::end(); + } + + std::pair insert( value_type&& value ) + { + return emplace(value.first, std::move(value.second)); + } + + std::pair insert( const value_type& value ) + { + for (auto it = this->begin(); it != this->end(); ++it) + { + if (it->first == value.first) + { + return {it, false}; + } + } + Container::push_back(value); + return {--this->end(), true}; + } + + template + using require_input_iter = typename std::enable_if::iterator_category, + std::input_iterator_tag>::value>::type; + + template> + void insert(InputIt first, InputIt last) + { + for (auto it = first; it != last; ++it) + { + insert(*it); + } + } +}; + +} // namespace nlohmann + + +#if defined(JSON_HAS_CPP_17) + #include +#endif + +/*! +@brief namespace for Niels Lohmann +@see https://github.com/nlohmann +@since version 1.0.0 +*/ +namespace nlohmann +{ + +/*! +@brief a class to store JSON values + +@tparam ObjectType type for JSON objects (`std::map` by default; will be used +in @ref object_t) +@tparam ArrayType type for JSON arrays (`std::vector` by default; will be used +in @ref array_t) +@tparam StringType type for JSON strings and object keys (`std::string` by +default; will be used in @ref string_t) +@tparam BooleanType type for JSON booleans (`bool` by default; will be used +in @ref boolean_t) +@tparam NumberIntegerType type for JSON integer numbers (`int64_t` by +default; will be used in @ref number_integer_t) +@tparam NumberUnsignedType type for JSON unsigned integer numbers (@c +`uint64_t` by default; will be used in @ref number_unsigned_t) +@tparam NumberFloatType type for JSON floating-point numbers (`double` by +default; will be used in @ref number_float_t) +@tparam BinaryType type for packed binary data for compatibility with binary +serialization formats (`std::vector` by default; will be used in +@ref binary_t) +@tparam AllocatorType type of the allocator to use (`std::allocator` by +default) +@tparam JSONSerializer the serializer to resolve internal calls to `to_json()` +and `from_json()` (@ref adl_serializer by default) + +@requirement The class satisfies the following concept requirements: +- Basic + - [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible): + JSON values can be default constructed. The result will be a JSON null + value. + - [MoveConstructible](https://en.cppreference.com/w/cpp/named_req/MoveConstructible): + A JSON value can be constructed from an rvalue argument. + - [CopyConstructible](https://en.cppreference.com/w/cpp/named_req/CopyConstructible): + A JSON value can be copy-constructed from an lvalue expression. + - [MoveAssignable](https://en.cppreference.com/w/cpp/named_req/MoveAssignable): + A JSON value van be assigned from an rvalue argument. + - [CopyAssignable](https://en.cppreference.com/w/cpp/named_req/CopyAssignable): + A JSON value can be copy-assigned from an lvalue expression. + - [Destructible](https://en.cppreference.com/w/cpp/named_req/Destructible): + JSON values can be destructed. +- Layout + - [StandardLayoutType](https://en.cppreference.com/w/cpp/named_req/StandardLayoutType): + JSON values have + [standard layout](https://en.cppreference.com/w/cpp/language/data_members#Standard_layout): + All non-static data members are private and standard layout types, the + class has no virtual functions or (virtual) base classes. +- Library-wide + - [EqualityComparable](https://en.cppreference.com/w/cpp/named_req/EqualityComparable): + JSON values can be compared with `==`, see @ref + operator==(const_reference,const_reference). + - [LessThanComparable](https://en.cppreference.com/w/cpp/named_req/LessThanComparable): + JSON values can be compared with `<`, see @ref + operator<(const_reference,const_reference). + - [Swappable](https://en.cppreference.com/w/cpp/named_req/Swappable): + Any JSON lvalue or rvalue of can be swapped with any lvalue or rvalue of + other compatible types, using unqualified function call @ref swap(). + - [NullablePointer](https://en.cppreference.com/w/cpp/named_req/NullablePointer): + JSON values can be compared against `std::nullptr_t` objects which are used + to model the `null` value. +- Container + - [Container](https://en.cppreference.com/w/cpp/named_req/Container): + JSON values can be used like STL containers and provide iterator access. + - [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer); + JSON values can be used like STL containers and provide reverse iterator + access. + +@invariant The member variables @a m_value and @a m_type have the following +relationship: +- If `m_type == value_t::object`, then `m_value.object != nullptr`. +- If `m_type == value_t::array`, then `m_value.array != nullptr`. +- If `m_type == value_t::string`, then `m_value.string != nullptr`. +The invariants are checked by member function assert_invariant(). + +@internal +@note ObjectType trick from https://stackoverflow.com/a/9860911 +@endinternal + +@see [RFC 8259: The JavaScript Object Notation (JSON) Data Interchange +Format](https://tools.ietf.org/html/rfc8259) + +@since version 1.0.0 + +@nosubgrouping +*/ +NLOHMANN_BASIC_JSON_TPL_DECLARATION +class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions) +{ + private: + template friend struct detail::external_constructor; + friend ::nlohmann::json_pointer; + + template + friend class ::nlohmann::detail::parser; + friend ::nlohmann::detail::serializer; + template + friend class ::nlohmann::detail::iter_impl; + template + friend class ::nlohmann::detail::binary_writer; + template + friend class ::nlohmann::detail::binary_reader; + template + friend class ::nlohmann::detail::json_sax_dom_parser; + template + friend class ::nlohmann::detail::json_sax_dom_callback_parser; + friend class ::nlohmann::detail::exception; + + /// workaround type for MSVC + using basic_json_t = NLOHMANN_BASIC_JSON_TPL; + + JSON_PRIVATE_UNLESS_TESTED: + // convenience aliases for types residing in namespace detail; + using lexer = ::nlohmann::detail::lexer_base; + + template + static ::nlohmann::detail::parser parser( + InputAdapterType adapter, + detail::parser_callback_tcb = nullptr, + const bool allow_exceptions = true, + const bool ignore_comments = false + ) + { + return ::nlohmann::detail::parser(std::move(adapter), + std::move(cb), allow_exceptions, ignore_comments); + } + + private: + using primitive_iterator_t = ::nlohmann::detail::primitive_iterator_t; + template + using internal_iterator = ::nlohmann::detail::internal_iterator; + template + using iter_impl = ::nlohmann::detail::iter_impl; + template + using iteration_proxy = ::nlohmann::detail::iteration_proxy; + template using json_reverse_iterator = ::nlohmann::detail::json_reverse_iterator; + + template + using output_adapter_t = ::nlohmann::detail::output_adapter_t; + + template + using binary_reader = ::nlohmann::detail::binary_reader; + template using binary_writer = ::nlohmann::detail::binary_writer; + + JSON_PRIVATE_UNLESS_TESTED: + using serializer = ::nlohmann::detail::serializer; + + public: + using value_t = detail::value_t; + /// JSON Pointer, see @ref nlohmann::json_pointer + using json_pointer = ::nlohmann::json_pointer; + template + using json_serializer = JSONSerializer; + /// how to treat decoding errors + using error_handler_t = detail::error_handler_t; + /// how to treat CBOR tags + using cbor_tag_handler_t = detail::cbor_tag_handler_t; + /// helper type for initializer lists of basic_json values + using initializer_list_t = std::initializer_list>; + + using input_format_t = detail::input_format_t; + /// SAX interface type, see @ref nlohmann::json_sax + using json_sax_t = json_sax; + + //////////////// + // exceptions // + //////////////// + + /// @name exceptions + /// Classes to implement user-defined exceptions. + /// @{ + + /// @copydoc detail::exception + using exception = detail::exception; + /// @copydoc detail::parse_error + using parse_error = detail::parse_error; + /// @copydoc detail::invalid_iterator + using invalid_iterator = detail::invalid_iterator; + /// @copydoc detail::type_error + using type_error = detail::type_error; + /// @copydoc detail::out_of_range + using out_of_range = detail::out_of_range; + /// @copydoc detail::other_error + using other_error = detail::other_error; + + /// @} + + + ///////////////////// + // container types // + ///////////////////// + + /// @name container types + /// The canonic container types to use @ref basic_json like any other STL + /// container. + /// @{ + + /// the type of elements in a basic_json container + using value_type = basic_json; + + /// the type of an element reference + using reference = value_type&; + /// the type of an element const reference + using const_reference = const value_type&; + + /// a type to represent differences between iterators + using difference_type = std::ptrdiff_t; + /// a type to represent container sizes + using size_type = std::size_t; + + /// the allocator type + using allocator_type = AllocatorType; + + /// the type of an element pointer + using pointer = typename std::allocator_traits::pointer; + /// the type of an element const pointer + using const_pointer = typename std::allocator_traits::const_pointer; + + /// an iterator for a basic_json container + using iterator = iter_impl; + /// a const iterator for a basic_json container + using const_iterator = iter_impl; + /// a reverse iterator for a basic_json container + using reverse_iterator = json_reverse_iterator; + /// a const reverse iterator for a basic_json container + using const_reverse_iterator = json_reverse_iterator; + + /// @} + + + /*! + @brief returns the allocator associated with the container + */ + static allocator_type get_allocator() + { + return allocator_type(); + } + + /*! + @brief returns version information on the library + + This function returns a JSON object with information about the library, + including the version number and information on the platform and compiler. + + @return JSON object holding version information + key | description + ----------- | --------------- + `compiler` | Information on the used compiler. It is an object with the following keys: `c++` (the used C++ standard), `family` (the compiler family; possible values are `clang`, `icc`, `gcc`, `ilecpp`, `msvc`, `pgcpp`, `sunpro`, and `unknown`), and `version` (the compiler version). + `copyright` | The copyright line for the library as string. + `name` | The name of the library as string. + `platform` | The used platform as string. Possible values are `win32`, `linux`, `apple`, `unix`, and `unknown`. + `url` | The URL of the project as string. + `version` | The version of the library. It is an object with the following keys: `major`, `minor`, and `patch` as defined by [Semantic Versioning](http://semver.org), and `string` (the version string). + + @liveexample{The following code shows an example output of the `meta()` + function.,meta} + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @complexity Constant. + + @since 2.1.0 + */ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json meta() + { + basic_json result; + + result["copyright"] = "(C) 2013-2021 Niels Lohmann"; + result["name"] = "JSON for Modern C++"; + result["url"] = "https://github.com/nlohmann/json"; + result["version"]["string"] = + std::to_string(NLOHMANN_JSON_VERSION_MAJOR) + "." + + std::to_string(NLOHMANN_JSON_VERSION_MINOR) + "." + + std::to_string(NLOHMANN_JSON_VERSION_PATCH); + result["version"]["major"] = NLOHMANN_JSON_VERSION_MAJOR; + result["version"]["minor"] = NLOHMANN_JSON_VERSION_MINOR; + result["version"]["patch"] = NLOHMANN_JSON_VERSION_PATCH; + +#ifdef _WIN32 + result["platform"] = "win32"; +#elif defined __linux__ + result["platform"] = "linux"; +#elif defined __APPLE__ + result["platform"] = "apple"; +#elif defined __unix__ + result["platform"] = "unix"; +#else + result["platform"] = "unknown"; +#endif + +#if defined(__ICC) || defined(__INTEL_COMPILER) + result["compiler"] = {{"family", "icc"}, {"version", __INTEL_COMPILER}}; +#elif defined(__clang__) + result["compiler"] = {{"family", "clang"}, {"version", __clang_version__}}; +#elif defined(__GNUC__) || defined(__GNUG__) + result["compiler"] = {{"family", "gcc"}, {"version", std::to_string(__GNUC__) + "." + std::to_string(__GNUC_MINOR__) + "." + std::to_string(__GNUC_PATCHLEVEL__)}}; +#elif defined(__HP_cc) || defined(__HP_aCC) + result["compiler"] = "hp" +#elif defined(__IBMCPP__) + result["compiler"] = {{"family", "ilecpp"}, {"version", __IBMCPP__}}; +#elif defined(_MSC_VER) + result["compiler"] = {{"family", "msvc"}, {"version", _MSC_VER}}; +#elif defined(__PGI) + result["compiler"] = {{"family", "pgcpp"}, {"version", __PGI}}; +#elif defined(__SUNPRO_CC) + result["compiler"] = {{"family", "sunpro"}, {"version", __SUNPRO_CC}}; +#else + result["compiler"] = {{"family", "unknown"}, {"version", "unknown"}}; +#endif + +#ifdef __cplusplus + result["compiler"]["c++"] = std::to_string(__cplusplus); +#else + result["compiler"]["c++"] = "unknown"; +#endif + return result; + } + + + /////////////////////////// + // JSON value data types // + /////////////////////////// + + /// @name JSON value data types + /// The data types to store a JSON value. These types are derived from + /// the template arguments passed to class @ref basic_json. + /// @{ + +#if defined(JSON_HAS_CPP_14) + // Use transparent comparator if possible, combined with perfect forwarding + // on find() and count() calls prevents unnecessary string construction. + using object_comparator_t = std::less<>; +#else + using object_comparator_t = std::less; +#endif + + /*! + @brief a type for an object + + [RFC 8259](https://tools.ietf.org/html/rfc8259) describes JSON objects as follows: + > An object is an unordered collection of zero or more name/value pairs, + > where a name is a string and a value is a string, number, boolean, null, + > object, or array. + + To store objects in C++, a type is defined by the template parameters + described below. + + @tparam ObjectType the container to store objects (e.g., `std::map` or + `std::unordered_map`) + @tparam StringType the type of the keys or names (e.g., `std::string`). + The comparison function `std::less` is used to order elements + inside the container. + @tparam AllocatorType the allocator to use for objects (e.g., + `std::allocator`) + + #### Default type + + With the default values for @a ObjectType (`std::map`), @a StringType + (`std::string`), and @a AllocatorType (`std::allocator`), the default + value for @a object_t is: + + @code {.cpp} + std::map< + std::string, // key_type + basic_json, // value_type + std::less, // key_compare + std::allocator> // allocator_type + > + @endcode + + #### Behavior + + The choice of @a object_t influences the behavior of the JSON class. With + the default type, objects have the following behavior: + + - When all names are unique, objects will be interoperable in the sense + that all software implementations receiving that object will agree on + the name-value mappings. + - When the names within an object are not unique, it is unspecified which + one of the values for a given key will be chosen. For instance, + `{"key": 2, "key": 1}` could be equal to either `{"key": 1}` or + `{"key": 2}`. + - Internally, name/value pairs are stored in lexicographical order of the + names. Objects will also be serialized (see @ref dump) in this order. + For instance, `{"b": 1, "a": 2}` and `{"a": 2, "b": 1}` will be stored + and serialized as `{"a": 2, "b": 1}`. + - When comparing objects, the order of the name/value pairs is irrelevant. + This makes objects interoperable in the sense that they will not be + affected by these differences. For instance, `{"b": 1, "a": 2}` and + `{"a": 2, "b": 1}` will be treated as equal. + + #### Limits + + [RFC 8259](https://tools.ietf.org/html/rfc8259) specifies: + > An implementation may set limits on the maximum depth of nesting. + + In this class, the object's limit of nesting is not explicitly constrained. + However, a maximum depth of nesting may be introduced by the compiler or + runtime environment. A theoretical limit can be queried by calling the + @ref max_size function of a JSON object. + + #### Storage + + Objects are stored as pointers in a @ref basic_json type. That is, for any + access to object values, a pointer of type `object_t*` must be + dereferenced. + + @sa see @ref array_t -- type for an array value + + @since version 1.0.0 + + @note The order name/value pairs are added to the object is *not* + preserved by the library. Therefore, iterating an object may return + name/value pairs in a different order than they were originally stored. In + fact, keys will be traversed in alphabetical order as `std::map` with + `std::less` is used by default. Please note this behavior conforms to [RFC + 8259](https://tools.ietf.org/html/rfc8259), because any order implements the + specified "unordered" nature of JSON objects. + */ + using object_t = ObjectType>>; + + /*! + @brief a type for an array + + [RFC 8259](https://tools.ietf.org/html/rfc8259) describes JSON arrays as follows: + > An array is an ordered sequence of zero or more values. + + To store objects in C++, a type is defined by the template parameters + explained below. + + @tparam ArrayType container type to store arrays (e.g., `std::vector` or + `std::list`) + @tparam AllocatorType allocator to use for arrays (e.g., `std::allocator`) + + #### Default type + + With the default values for @a ArrayType (`std::vector`) and @a + AllocatorType (`std::allocator`), the default value for @a array_t is: + + @code {.cpp} + std::vector< + basic_json, // value_type + std::allocator // allocator_type + > + @endcode + + #### Limits + + [RFC 8259](https://tools.ietf.org/html/rfc8259) specifies: + > An implementation may set limits on the maximum depth of nesting. + + In this class, the array's limit of nesting is not explicitly constrained. + However, a maximum depth of nesting may be introduced by the compiler or + runtime environment. A theoretical limit can be queried by calling the + @ref max_size function of a JSON array. + + #### Storage + + Arrays are stored as pointers in a @ref basic_json type. That is, for any + access to array values, a pointer of type `array_t*` must be dereferenced. + + @sa see @ref object_t -- type for an object value + + @since version 1.0.0 + */ + using array_t = ArrayType>; + + /*! + @brief a type for a string + + [RFC 8259](https://tools.ietf.org/html/rfc8259) describes JSON strings as follows: + > A string is a sequence of zero or more Unicode characters. + + To store objects in C++, a type is defined by the template parameter + described below. Unicode values are split by the JSON class into + byte-sized characters during deserialization. + + @tparam StringType the container to store strings (e.g., `std::string`). + Note this container is used for keys/names in objects, see @ref object_t. + + #### Default type + + With the default values for @a StringType (`std::string`), the default + value for @a string_t is: + + @code {.cpp} + std::string + @endcode + + #### Encoding + + Strings are stored in UTF-8 encoding. Therefore, functions like + `std::string::size()` or `std::string::length()` return the number of + bytes in the string rather than the number of characters or glyphs. + + #### String comparison + + [RFC 8259](https://tools.ietf.org/html/rfc8259) states: + > Software implementations are typically required to test names of object + > members for equality. Implementations that transform the textual + > representation into sequences of Unicode code units and then perform the + > comparison numerically, code unit by code unit, are interoperable in the + > sense that implementations will agree in all cases on equality or + > inequality of two strings. For example, implementations that compare + > strings with escaped characters unconverted may incorrectly find that + > `"a\\b"` and `"a\u005Cb"` are not equal. + + This implementation is interoperable as it does compare strings code unit + by code unit. + + #### Storage + + String values are stored as pointers in a @ref basic_json type. That is, + for any access to string values, a pointer of type `string_t*` must be + dereferenced. + + @since version 1.0.0 + */ + using string_t = StringType; + + /*! + @brief a type for a boolean + + [RFC 8259](https://tools.ietf.org/html/rfc8259) implicitly describes a boolean as a + type which differentiates the two literals `true` and `false`. + + To store objects in C++, a type is defined by the template parameter @a + BooleanType which chooses the type to use. + + #### Default type + + With the default values for @a BooleanType (`bool`), the default value for + @a boolean_t is: + + @code {.cpp} + bool + @endcode + + #### Storage + + Boolean values are stored directly inside a @ref basic_json type. + + @since version 1.0.0 + */ + using boolean_t = BooleanType; + + /*! + @brief a type for a number (integer) + + [RFC 8259](https://tools.ietf.org/html/rfc8259) describes numbers as follows: + > The representation of numbers is similar to that used in most + > programming languages. A number is represented in base 10 using decimal + > digits. It contains an integer component that may be prefixed with an + > optional minus sign, which may be followed by a fraction part and/or an + > exponent part. Leading zeros are not allowed. (...) Numeric values that + > cannot be represented in the grammar below (such as Infinity and NaN) + > are not permitted. + + This description includes both integer and floating-point numbers. + However, C++ allows more precise storage if it is known whether the number + is a signed integer, an unsigned integer or a floating-point number. + Therefore, three different types, @ref number_integer_t, @ref + number_unsigned_t and @ref number_float_t are used. + + To store integer numbers in C++, a type is defined by the template + parameter @a NumberIntegerType which chooses the type to use. + + #### Default type + + With the default values for @a NumberIntegerType (`int64_t`), the default + value for @a number_integer_t is: + + @code {.cpp} + int64_t + @endcode + + #### Default behavior + + - The restrictions about leading zeros is not enforced in C++. Instead, + leading zeros in integer literals lead to an interpretation as octal + number. Internally, the value will be stored as decimal number. For + instance, the C++ integer literal `010` will be serialized to `8`. + During deserialization, leading zeros yield an error. + - Not-a-number (NaN) values will be serialized to `null`. + + #### Limits + + [RFC 8259](https://tools.ietf.org/html/rfc8259) specifies: + > An implementation may set limits on the range and precision of numbers. + + When the default type is used, the maximal integer number that can be + stored is `9223372036854775807` (INT64_MAX) and the minimal integer number + that can be stored is `-9223372036854775808` (INT64_MIN). Integer numbers + that are out of range will yield over/underflow when used in a + constructor. During deserialization, too large or small integer numbers + will be automatically be stored as @ref number_unsigned_t or @ref + number_float_t. + + [RFC 8259](https://tools.ietf.org/html/rfc8259) further states: + > Note that when such software is used, numbers that are integers and are + > in the range \f$[-2^{53}+1, 2^{53}-1]\f$ are interoperable in the sense + > that implementations will agree exactly on their numeric values. + + As this range is a subrange of the exactly supported range [INT64_MIN, + INT64_MAX], this class's integer type is interoperable. + + #### Storage + + Integer number values are stored directly inside a @ref basic_json type. + + @sa see @ref number_float_t -- type for number values (floating-point) + + @sa see @ref number_unsigned_t -- type for number values (unsigned integer) + + @since version 1.0.0 + */ + using number_integer_t = NumberIntegerType; + + /*! + @brief a type for a number (unsigned) + + [RFC 8259](https://tools.ietf.org/html/rfc8259) describes numbers as follows: + > The representation of numbers is similar to that used in most + > programming languages. A number is represented in base 10 using decimal + > digits. It contains an integer component that may be prefixed with an + > optional minus sign, which may be followed by a fraction part and/or an + > exponent part. Leading zeros are not allowed. (...) Numeric values that + > cannot be represented in the grammar below (such as Infinity and NaN) + > are not permitted. + + This description includes both integer and floating-point numbers. + However, C++ allows more precise storage if it is known whether the number + is a signed integer, an unsigned integer or a floating-point number. + Therefore, three different types, @ref number_integer_t, @ref + number_unsigned_t and @ref number_float_t are used. + + To store unsigned integer numbers in C++, a type is defined by the + template parameter @a NumberUnsignedType which chooses the type to use. + + #### Default type + + With the default values for @a NumberUnsignedType (`uint64_t`), the + default value for @a number_unsigned_t is: + + @code {.cpp} + uint64_t + @endcode + + #### Default behavior + + - The restrictions about leading zeros is not enforced in C++. Instead, + leading zeros in integer literals lead to an interpretation as octal + number. Internally, the value will be stored as decimal number. For + instance, the C++ integer literal `010` will be serialized to `8`. + During deserialization, leading zeros yield an error. + - Not-a-number (NaN) values will be serialized to `null`. + + #### Limits + + [RFC 8259](https://tools.ietf.org/html/rfc8259) specifies: + > An implementation may set limits on the range and precision of numbers. + + When the default type is used, the maximal integer number that can be + stored is `18446744073709551615` (UINT64_MAX) and the minimal integer + number that can be stored is `0`. Integer numbers that are out of range + will yield over/underflow when used in a constructor. During + deserialization, too large or small integer numbers will be automatically + be stored as @ref number_integer_t or @ref number_float_t. + + [RFC 8259](https://tools.ietf.org/html/rfc8259) further states: + > Note that when such software is used, numbers that are integers and are + > in the range \f$[-2^{53}+1, 2^{53}-1]\f$ are interoperable in the sense + > that implementations will agree exactly on their numeric values. + + As this range is a subrange (when considered in conjunction with the + number_integer_t type) of the exactly supported range [0, UINT64_MAX], + this class's integer type is interoperable. + + #### Storage + + Integer number values are stored directly inside a @ref basic_json type. + + @sa see @ref number_float_t -- type for number values (floating-point) + @sa see @ref number_integer_t -- type for number values (integer) + + @since version 2.0.0 + */ + using number_unsigned_t = NumberUnsignedType; + + /*! + @brief a type for a number (floating-point) + + [RFC 8259](https://tools.ietf.org/html/rfc8259) describes numbers as follows: + > The representation of numbers is similar to that used in most + > programming languages. A number is represented in base 10 using decimal + > digits. It contains an integer component that may be prefixed with an + > optional minus sign, which may be followed by a fraction part and/or an + > exponent part. Leading zeros are not allowed. (...) Numeric values that + > cannot be represented in the grammar below (such as Infinity and NaN) + > are not permitted. + + This description includes both integer and floating-point numbers. + However, C++ allows more precise storage if it is known whether the number + is a signed integer, an unsigned integer or a floating-point number. + Therefore, three different types, @ref number_integer_t, @ref + number_unsigned_t and @ref number_float_t are used. + + To store floating-point numbers in C++, a type is defined by the template + parameter @a NumberFloatType which chooses the type to use. + + #### Default type + + With the default values for @a NumberFloatType (`double`), the default + value for @a number_float_t is: + + @code {.cpp} + double + @endcode + + #### Default behavior + + - The restrictions about leading zeros is not enforced in C++. Instead, + leading zeros in floating-point literals will be ignored. Internally, + the value will be stored as decimal number. For instance, the C++ + floating-point literal `01.2` will be serialized to `1.2`. During + deserialization, leading zeros yield an error. + - Not-a-number (NaN) values will be serialized to `null`. + + #### Limits + + [RFC 8259](https://tools.ietf.org/html/rfc8259) states: + > This specification allows implementations to set limits on the range and + > precision of numbers accepted. Since software that implements IEEE + > 754-2008 binary64 (double precision) numbers is generally available and + > widely used, good interoperability can be achieved by implementations + > that expect no more precision or range than these provide, in the sense + > that implementations will approximate JSON numbers within the expected + > precision. + + This implementation does exactly follow this approach, as it uses double + precision floating-point numbers. Note values smaller than + `-1.79769313486232e+308` and values greater than `1.79769313486232e+308` + will be stored as NaN internally and be serialized to `null`. + + #### Storage + + Floating-point number values are stored directly inside a @ref basic_json + type. + + @sa see @ref number_integer_t -- type for number values (integer) + + @sa see @ref number_unsigned_t -- type for number values (unsigned integer) + + @since version 1.0.0 + */ + using number_float_t = NumberFloatType; + + /*! + @brief a type for a packed binary type + + This type is a type designed to carry binary data that appears in various + serialized formats, such as CBOR's Major Type 2, MessagePack's bin, and + BSON's generic binary subtype. This type is NOT a part of standard JSON and + exists solely for compatibility with these binary types. As such, it is + simply defined as an ordered sequence of zero or more byte values. + + Additionally, as an implementation detail, the subtype of the binary data is + carried around as a `std::uint8_t`, which is compatible with both of the + binary data formats that use binary subtyping, (though the specific + numbering is incompatible with each other, and it is up to the user to + translate between them). + + [CBOR's RFC 7049](https://tools.ietf.org/html/rfc7049) describes this type + as: + > Major type 2: a byte string. The string's length in bytes is represented + > following the rules for positive integers (major type 0). + + [MessagePack's documentation on the bin type + family](https://github.com/msgpack/msgpack/blob/master/spec.md#bin-format-family) + describes this type as: + > Bin format family stores an byte array in 2, 3, or 5 bytes of extra bytes + > in addition to the size of the byte array. + + [BSON's specifications](http://bsonspec.org/spec.html) describe several + binary types; however, this type is intended to represent the generic binary + type which has the description: + > Generic binary subtype - This is the most commonly used binary subtype and + > should be the 'default' for drivers and tools. + + None of these impose any limitations on the internal representation other + than the basic unit of storage be some type of array whose parts are + decomposable into bytes. + + The default representation of this binary format is a + `std::vector`, which is a very common way to represent a byte + array in modern C++. + + #### Default type + + The default values for @a BinaryType is `std::vector` + + #### Storage + + Binary Arrays are stored as pointers in a @ref basic_json type. That is, + for any access to array values, a pointer of the type `binary_t*` must be + dereferenced. + + #### Notes on subtypes + + - CBOR + - Binary values are represented as byte strings. Subtypes are serialized + as tagged values. + - MessagePack + - If a subtype is given and the binary array contains exactly 1, 2, 4, 8, + or 16 elements, the fixext family (fixext1, fixext2, fixext4, fixext8) + is used. For other sizes, the ext family (ext8, ext16, ext32) is used. + The subtype is then added as singed 8-bit integer. + - If no subtype is given, the bin family (bin8, bin16, bin32) is used. + - BSON + - If a subtype is given, it is used and added as unsigned 8-bit integer. + - If no subtype is given, the generic binary subtype 0x00 is used. + + @sa see @ref binary -- create a binary array + + @since version 3.8.0 + */ + using binary_t = nlohmann::byte_container_with_subtype; + /// @} + + private: + + /// helper for exception-safe object creation + template + JSON_HEDLEY_RETURNS_NON_NULL + static T* create(Args&& ... args) + { + AllocatorType alloc; + using AllocatorTraits = std::allocator_traits>; + + auto deleter = [&](T * obj) + { + AllocatorTraits::deallocate(alloc, obj, 1); + }; + std::unique_ptr obj(AllocatorTraits::allocate(alloc, 1), deleter); + AllocatorTraits::construct(alloc, obj.get(), std::forward(args)...); + JSON_ASSERT(obj != nullptr); + return obj.release(); + } + + //////////////////////// + // JSON value storage // + //////////////////////// + + JSON_PRIVATE_UNLESS_TESTED: + /*! + @brief a JSON value + + The actual storage for a JSON value of the @ref basic_json class. This + union combines the different storage types for the JSON value types + defined in @ref value_t. + + JSON type | value_t type | used type + --------- | --------------- | ------------------------ + object | object | pointer to @ref object_t + array | array | pointer to @ref array_t + string | string | pointer to @ref string_t + boolean | boolean | @ref boolean_t + number | number_integer | @ref number_integer_t + number | number_unsigned | @ref number_unsigned_t + number | number_float | @ref number_float_t + binary | binary | pointer to @ref binary_t + null | null | *no value is stored* + + @note Variable-length types (objects, arrays, and strings) are stored as + pointers. The size of the union should not exceed 64 bits if the default + value types are used. + + @since version 1.0.0 + */ + union json_value + { + /// object (stored with pointer to save storage) + object_t* object; + /// array (stored with pointer to save storage) + array_t* array; + /// string (stored with pointer to save storage) + string_t* string; + /// binary (stored with pointer to save storage) + binary_t* binary; + /// boolean + boolean_t boolean; + /// number (integer) + number_integer_t number_integer; + /// number (unsigned integer) + number_unsigned_t number_unsigned; + /// number (floating-point) + number_float_t number_float; + + /// default constructor (for null values) + json_value() = default; + /// constructor for booleans + json_value(boolean_t v) noexcept : boolean(v) {} + /// constructor for numbers (integer) + json_value(number_integer_t v) noexcept : number_integer(v) {} + /// constructor for numbers (unsigned) + json_value(number_unsigned_t v) noexcept : number_unsigned(v) {} + /// constructor for numbers (floating-point) + json_value(number_float_t v) noexcept : number_float(v) {} + /// constructor for empty values of a given type + json_value(value_t t) + { + switch (t) + { + case value_t::object: + { + object = create(); + break; + } + + case value_t::array: + { + array = create(); + break; + } + + case value_t::string: + { + string = create(""); + break; + } + + case value_t::binary: + { + binary = create(); + break; + } + + case value_t::boolean: + { + boolean = boolean_t(false); + break; + } + + case value_t::number_integer: + { + number_integer = number_integer_t(0); + break; + } + + case value_t::number_unsigned: + { + number_unsigned = number_unsigned_t(0); + break; + } + + case value_t::number_float: + { + number_float = number_float_t(0.0); + break; + } + + case value_t::null: + { + object = nullptr; // silence warning, see #821 + break; + } + + case value_t::discarded: + default: + { + object = nullptr; // silence warning, see #821 + if (JSON_HEDLEY_UNLIKELY(t == value_t::null)) + { + JSON_THROW(other_error::create(500, "961c151d2e87f2686a955a9be24d316f1362bf21 3.10.4", basic_json())); // LCOV_EXCL_LINE + } + break; + } + } + } + + /// constructor for strings + json_value(const string_t& value) + { + string = create(value); + } + + /// constructor for rvalue strings + json_value(string_t&& value) + { + string = create(std::move(value)); + } + + /// constructor for objects + json_value(const object_t& value) + { + object = create(value); + } + + /// constructor for rvalue objects + json_value(object_t&& value) + { + object = create(std::move(value)); + } + + /// constructor for arrays + json_value(const array_t& value) + { + array = create(value); + } + + /// constructor for rvalue arrays + json_value(array_t&& value) + { + array = create(std::move(value)); + } + + /// constructor for binary arrays + json_value(const typename binary_t::container_type& value) + { + binary = create(value); + } + + /// constructor for rvalue binary arrays + json_value(typename binary_t::container_type&& value) + { + binary = create(std::move(value)); + } + + /// constructor for binary arrays (internal type) + json_value(const binary_t& value) + { + binary = create(value); + } + + /// constructor for rvalue binary arrays (internal type) + json_value(binary_t&& value) + { + binary = create(std::move(value)); + } + + void destroy(value_t t) + { + if (t == value_t::array || t == value_t::object) + { + // flatten the current json_value to a heap-allocated stack + std::vector stack; + + // move the top-level items to stack + if (t == value_t::array) + { + stack.reserve(array->size()); + std::move(array->begin(), array->end(), std::back_inserter(stack)); + } + else + { + stack.reserve(object->size()); + for (auto&& it : *object) + { + stack.push_back(std::move(it.second)); + } + } + + while (!stack.empty()) + { + // move the last item to local variable to be processed + basic_json current_item(std::move(stack.back())); + stack.pop_back(); + + // if current_item is array/object, move + // its children to the stack to be processed later + if (current_item.is_array()) + { + std::move(current_item.m_value.array->begin(), current_item.m_value.array->end(), std::back_inserter(stack)); + + current_item.m_value.array->clear(); + } + else if (current_item.is_object()) + { + for (auto&& it : *current_item.m_value.object) + { + stack.push_back(std::move(it.second)); + } + + current_item.m_value.object->clear(); + } + + // it's now safe that current_item get destructed + // since it doesn't have any children + } + } + + switch (t) + { + case value_t::object: + { + AllocatorType alloc; + std::allocator_traits::destroy(alloc, object); + std::allocator_traits::deallocate(alloc, object, 1); + break; + } + + case value_t::array: + { + AllocatorType alloc; + std::allocator_traits::destroy(alloc, array); + std::allocator_traits::deallocate(alloc, array, 1); + break; + } + + case value_t::string: + { + AllocatorType alloc; + std::allocator_traits::destroy(alloc, string); + std::allocator_traits::deallocate(alloc, string, 1); + break; + } + + case value_t::binary: + { + AllocatorType alloc; + std::allocator_traits::destroy(alloc, binary); + std::allocator_traits::deallocate(alloc, binary, 1); + break; + } + + case value_t::null: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::discarded: + default: + { + break; + } + } + } + }; + + private: + /*! + @brief checks the class invariants + + This function asserts the class invariants. It needs to be called at the + end of every constructor to make sure that created objects respect the + invariant. Furthermore, it has to be called each time the type of a JSON + value is changed, because the invariant expresses a relationship between + @a m_type and @a m_value. + + Furthermore, the parent relation is checked for arrays and objects: If + @a check_parents true and the value is an array or object, then the + container's elements must have the current value as parent. + + @param[in] check_parents whether the parent relation should be checked. + The value is true by default and should only be set to false + during destruction of objects when the invariant does not + need to hold. + */ + void assert_invariant(bool check_parents = true) const noexcept + { + JSON_ASSERT(m_type != value_t::object || m_value.object != nullptr); + JSON_ASSERT(m_type != value_t::array || m_value.array != nullptr); + JSON_ASSERT(m_type != value_t::string || m_value.string != nullptr); + JSON_ASSERT(m_type != value_t::binary || m_value.binary != nullptr); + +#if JSON_DIAGNOSTICS + JSON_TRY + { + // cppcheck-suppress assertWithSideEffect + JSON_ASSERT(!check_parents || !is_structured() || std::all_of(begin(), end(), [this](const basic_json & j) + { + return j.m_parent == this; + })); + } + JSON_CATCH(...) {} // LCOV_EXCL_LINE +#endif + static_cast(check_parents); + } + + void set_parents() + { +#if JSON_DIAGNOSTICS + switch (m_type) + { + case value_t::array: + { + for (auto& element : *m_value.array) + { + element.m_parent = this; + } + break; + } + + case value_t::object: + { + for (auto& element : *m_value.object) + { + element.second.m_parent = this; + } + break; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + break; + } +#endif + } + + iterator set_parents(iterator it, typename iterator::difference_type count) + { +#if JSON_DIAGNOSTICS + for (typename iterator::difference_type i = 0; i < count; ++i) + { + (it + i)->m_parent = this; + } +#else + static_cast(count); +#endif + return it; + } + + reference set_parent(reference j, std::size_t old_capacity = std::size_t(-1)) + { +#if JSON_DIAGNOSTICS + if (old_capacity != std::size_t(-1)) + { + // see https://github.com/nlohmann/json/issues/2838 + JSON_ASSERT(type() == value_t::array); + if (JSON_HEDLEY_UNLIKELY(m_value.array->capacity() != old_capacity)) + { + // capacity has changed: update all parents + set_parents(); + return j; + } + } + + // ordered_json uses a vector internally, so pointers could have + // been invalidated; see https://github.com/nlohmann/json/issues/2962 +#ifdef JSON_HEDLEY_MSVC_VERSION +#pragma warning(push ) +#pragma warning(disable : 4127) // ignore warning to replace if with if constexpr +#endif + if (detail::is_ordered_map::value) + { + set_parents(); + return j; + } +#ifdef JSON_HEDLEY_MSVC_VERSION +#pragma warning( pop ) +#endif + + j.m_parent = this; +#else + static_cast(j); + static_cast(old_capacity); +#endif + return j; + } + + public: + ////////////////////////// + // JSON parser callback // + ////////////////////////// + + /*! + @brief parser event types + + The parser callback distinguishes the following events: + - `object_start`: the parser read `{` and started to process a JSON object + - `key`: the parser read a key of a value in an object + - `object_end`: the parser read `}` and finished processing a JSON object + - `array_start`: the parser read `[` and started to process a JSON array + - `array_end`: the parser read `]` and finished processing a JSON array + - `value`: the parser finished reading a JSON value + + @image html callback_events.png "Example when certain parse events are triggered" + + @sa see @ref parser_callback_t for more information and examples + */ + using parse_event_t = detail::parse_event_t; + + /*! + @brief per-element parser callback type + + With a parser callback function, the result of parsing a JSON text can be + influenced. When passed to @ref parse, it is called on certain events + (passed as @ref parse_event_t via parameter @a event) with a set recursion + depth @a depth and context JSON value @a parsed. The return value of the + callback function is a boolean indicating whether the element that emitted + the callback shall be kept or not. + + We distinguish six scenarios (determined by the event type) in which the + callback function can be called. The following table describes the values + of the parameters @a depth, @a event, and @a parsed. + + parameter @a event | description | parameter @a depth | parameter @a parsed + ------------------ | ----------- | ------------------ | ------------------- + parse_event_t::object_start | the parser read `{` and started to process a JSON object | depth of the parent of the JSON object | a JSON value with type discarded + parse_event_t::key | the parser read a key of a value in an object | depth of the currently parsed JSON object | a JSON string containing the key + parse_event_t::object_end | the parser read `}` and finished processing a JSON object | depth of the parent of the JSON object | the parsed JSON object + parse_event_t::array_start | the parser read `[` and started to process a JSON array | depth of the parent of the JSON array | a JSON value with type discarded + parse_event_t::array_end | the parser read `]` and finished processing a JSON array | depth of the parent of the JSON array | the parsed JSON array + parse_event_t::value | the parser finished reading a JSON value | depth of the value | the parsed JSON value + + @image html callback_events.png "Example when certain parse events are triggered" + + Discarding a value (i.e., returning `false`) has different effects + depending on the context in which function was called: + + - Discarded values in structured types are skipped. That is, the parser + will behave as if the discarded value was never read. + - In case a value outside a structured type is skipped, it is replaced + with `null`. This case happens if the top-level element is skipped. + + @param[in] depth the depth of the recursion during parsing + + @param[in] event an event of type parse_event_t indicating the context in + the callback function has been called + + @param[in,out] parsed the current intermediate parse result; note that + writing to this value has no effect for parse_event_t::key events + + @return Whether the JSON value which called the function during parsing + should be kept (`true`) or not (`false`). In the latter case, it is either + skipped completely or replaced by an empty discarded object. + + @sa see @ref parse for examples + + @since version 1.0.0 + */ + using parser_callback_t = detail::parser_callback_t; + + ////////////////// + // constructors // + ////////////////// + + /// @name constructors and destructors + /// Constructors of class @ref basic_json, copy/move constructor, copy + /// assignment, static functions creating objects, and the destructor. + /// @{ + + /*! + @brief create an empty value with a given type + + Create an empty JSON value with a given type. The value will be default + initialized with an empty value which depends on the type: + + Value type | initial value + ----------- | ------------- + null | `null` + boolean | `false` + string | `""` + number | `0` + object | `{}` + array | `[]` + binary | empty array + + @param[in] v the type of the value to create + + @complexity Constant. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @liveexample{The following code shows the constructor for different @ref + value_t values,basic_json__value_t} + + @sa see @ref clear() -- restores the postcondition of this constructor + + @since version 1.0.0 + */ + basic_json(const value_t v) + : m_type(v), m_value(v) + { + assert_invariant(); + } + + /*! + @brief create a null object + + Create a `null` JSON value. It either takes a null pointer as parameter + (explicitly creating `null`) or no parameter (implicitly creating `null`). + The passed null pointer itself is not read -- it is only used to choose + the right constructor. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this constructor never throws + exceptions. + + @liveexample{The following code shows the constructor with and without a + null pointer parameter.,basic_json__nullptr_t} + + @since version 1.0.0 + */ + basic_json(std::nullptr_t = nullptr) noexcept + : basic_json(value_t::null) + { + assert_invariant(); + } + + /*! + @brief create a JSON value + + This is a "catch all" constructor for all compatible JSON types; that is, + types for which a `to_json()` method exists. The constructor forwards the + parameter @a val to that method (to `json_serializer::to_json` method + with `U = uncvref_t`, to be exact). + + Template type @a CompatibleType includes, but is not limited to, the + following types: + - **arrays**: @ref array_t and all kinds of compatible containers such as + `std::vector`, `std::deque`, `std::list`, `std::forward_list`, + `std::array`, `std::valarray`, `std::set`, `std::unordered_set`, + `std::multiset`, and `std::unordered_multiset` with a `value_type` from + which a @ref basic_json value can be constructed. + - **objects**: @ref object_t and all kinds of compatible associative + containers such as `std::map`, `std::unordered_map`, `std::multimap`, + and `std::unordered_multimap` with a `key_type` compatible to + @ref string_t and a `value_type` from which a @ref basic_json value can + be constructed. + - **strings**: @ref string_t, string literals, and all compatible string + containers can be used. + - **numbers**: @ref number_integer_t, @ref number_unsigned_t, + @ref number_float_t, and all convertible number types such as `int`, + `size_t`, `int64_t`, `float` or `double` can be used. + - **boolean**: @ref boolean_t / `bool` can be used. + - **binary**: @ref binary_t / `std::vector` may be used, + unfortunately because string literals cannot be distinguished from binary + character arrays by the C++ type system, all types compatible with `const + char*` will be directed to the string constructor instead. This is both + for backwards compatibility, and due to the fact that a binary type is not + a standard JSON type. + + See the examples below. + + @tparam CompatibleType a type such that: + - @a CompatibleType is not derived from `std::istream`, + - @a CompatibleType is not @ref basic_json (to avoid hijacking copy/move + constructors), + - @a CompatibleType is not a different @ref basic_json type (i.e. with different template arguments) + - @a CompatibleType is not a @ref basic_json nested type (e.g., + @ref json_pointer, @ref iterator, etc ...) + - `json_serializer` has a `to_json(basic_json_t&, CompatibleType&&)` method + + @tparam U = `uncvref_t` + + @param[in] val the value to be forwarded to the respective constructor + + @complexity Usually linear in the size of the passed @a val, also + depending on the implementation of the called `to_json()` + method. + + @exceptionsafety Depends on the called constructor. For types directly + supported by the library (i.e., all types for which no `to_json()` function + was provided), strong guarantee holds: if an exception is thrown, there are + no changes to any JSON value. + + @liveexample{The following code shows the constructor with several + compatible types.,basic_json__CompatibleType} + + @since version 2.1.0 + */ + template < typename CompatibleType, + typename U = detail::uncvref_t, + detail::enable_if_t < + !detail::is_basic_json::value && detail::is_compatible_type::value, int > = 0 > + basic_json(CompatibleType && val) noexcept(noexcept( // NOLINT(bugprone-forwarding-reference-overload,bugprone-exception-escape) + JSONSerializer::to_json(std::declval(), + std::forward(val)))) + { + JSONSerializer::to_json(*this, std::forward(val)); + set_parents(); + assert_invariant(); + } + + /*! + @brief create a JSON value from an existing one + + This is a constructor for existing @ref basic_json types. + It does not hijack copy/move constructors, since the parameter has different + template arguments than the current ones. + + The constructor tries to convert the internal @ref m_value of the parameter. + + @tparam BasicJsonType a type such that: + - @a BasicJsonType is a @ref basic_json type. + - @a BasicJsonType has different template arguments than @ref basic_json_t. + + @param[in] val the @ref basic_json value to be converted. + + @complexity Usually linear in the size of the passed @a val, also + depending on the implementation of the called `to_json()` + method. + + @exceptionsafety Depends on the called constructor. For types directly + supported by the library (i.e., all types for which no `to_json()` function + was provided), strong guarantee holds: if an exception is thrown, there are + no changes to any JSON value. + + @since version 3.2.0 + */ + template < typename BasicJsonType, + detail::enable_if_t < + detail::is_basic_json::value&& !std::is_same::value, int > = 0 > + basic_json(const BasicJsonType& val) + { + using other_boolean_t = typename BasicJsonType::boolean_t; + using other_number_float_t = typename BasicJsonType::number_float_t; + using other_number_integer_t = typename BasicJsonType::number_integer_t; + using other_number_unsigned_t = typename BasicJsonType::number_unsigned_t; + using other_string_t = typename BasicJsonType::string_t; + using other_object_t = typename BasicJsonType::object_t; + using other_array_t = typename BasicJsonType::array_t; + using other_binary_t = typename BasicJsonType::binary_t; + + switch (val.type()) + { + case value_t::boolean: + JSONSerializer::to_json(*this, val.template get()); + break; + case value_t::number_float: + JSONSerializer::to_json(*this, val.template get()); + break; + case value_t::number_integer: + JSONSerializer::to_json(*this, val.template get()); + break; + case value_t::number_unsigned: + JSONSerializer::to_json(*this, val.template get()); + break; + case value_t::string: + JSONSerializer::to_json(*this, val.template get_ref()); + break; + case value_t::object: + JSONSerializer::to_json(*this, val.template get_ref()); + break; + case value_t::array: + JSONSerializer::to_json(*this, val.template get_ref()); + break; + case value_t::binary: + JSONSerializer::to_json(*this, val.template get_ref()); + break; + case value_t::null: + *this = nullptr; + break; + case value_t::discarded: + m_type = value_t::discarded; + break; + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + set_parents(); + assert_invariant(); + } + + /*! + @brief create a container (array or object) from an initializer list + + Creates a JSON value of type array or object from the passed initializer + list @a init. In case @a type_deduction is `true` (default), the type of + the JSON value to be created is deducted from the initializer list @a init + according to the following rules: + + 1. If the list is empty, an empty JSON object value `{}` is created. + 2. If the list consists of pairs whose first element is a string, a JSON + object value is created where the first elements of the pairs are + treated as keys and the second elements are as values. + 3. In all other cases, an array is created. + + The rules aim to create the best fit between a C++ initializer list and + JSON values. The rationale is as follows: + + 1. The empty initializer list is written as `{}` which is exactly an empty + JSON object. + 2. C++ has no way of describing mapped types other than to list a list of + pairs. As JSON requires that keys must be of type string, rule 2 is the + weakest constraint one can pose on initializer lists to interpret them + as an object. + 3. In all other cases, the initializer list could not be interpreted as + JSON object type, so interpreting it as JSON array type is safe. + + With the rules described above, the following JSON values cannot be + expressed by an initializer list: + + - the empty array (`[]`): use @ref array(initializer_list_t) + with an empty initializer list in this case + - arrays whose elements satisfy rule 2: use @ref + array(initializer_list_t) with the same initializer list + in this case + + @note When used without parentheses around an empty initializer list, @ref + basic_json() is called instead of this function, yielding the JSON null + value. + + @param[in] init initializer list with JSON values + + @param[in] type_deduction internal parameter; when set to `true`, the type + of the JSON value is deducted from the initializer list @a init; when set + to `false`, the type provided via @a manual_type is forced. This mode is + used by the functions @ref array(initializer_list_t) and + @ref object(initializer_list_t). + + @param[in] manual_type internal parameter; when @a type_deduction is set + to `false`, the created JSON value will use the provided type (only @ref + value_t::array and @ref value_t::object are valid); when @a type_deduction + is set to `true`, this parameter has no effect + + @throw type_error.301 if @a type_deduction is `false`, @a manual_type is + `value_t::object`, but @a init contains an element which is not a pair + whose first element is a string. In this case, the constructor could not + create an object. If @a type_deduction would have be `true`, an array + would have been created. See @ref object(initializer_list_t) + for an example. + + @complexity Linear in the size of the initializer list @a init. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @liveexample{The example below shows how JSON values are created from + initializer lists.,basic_json__list_init_t} + + @sa see @ref array(initializer_list_t) -- create a JSON array + value from an initializer list + @sa see @ref object(initializer_list_t) -- create a JSON object + value from an initializer list + + @since version 1.0.0 + */ + basic_json(initializer_list_t init, + bool type_deduction = true, + value_t manual_type = value_t::array) + { + // check if each element is an array with two elements whose first + // element is a string + bool is_an_object = std::all_of(init.begin(), init.end(), + [](const detail::json_ref& element_ref) + { + return element_ref->is_array() && element_ref->size() == 2 && (*element_ref)[0].is_string(); + }); + + // adjust type if type deduction is not wanted + if (!type_deduction) + { + // if array is wanted, do not create an object though possible + if (manual_type == value_t::array) + { + is_an_object = false; + } + + // if object is wanted but impossible, throw an exception + if (JSON_HEDLEY_UNLIKELY(manual_type == value_t::object && !is_an_object)) + { + JSON_THROW(type_error::create(301, "cannot create object from initializer list", basic_json())); + } + } + + if (is_an_object) + { + // the initializer list is a list of pairs -> create object + m_type = value_t::object; + m_value = value_t::object; + + for (auto& element_ref : init) + { + auto element = element_ref.moved_or_copied(); + m_value.object->emplace( + std::move(*((*element.m_value.array)[0].m_value.string)), + std::move((*element.m_value.array)[1])); + } + } + else + { + // the initializer list describes an array -> create array + m_type = value_t::array; + m_value.array = create(init.begin(), init.end()); + } + + set_parents(); + assert_invariant(); + } + + /*! + @brief explicitly create a binary array (without subtype) + + Creates a JSON binary array value from a given binary container. Binary + values are part of various binary formats, such as CBOR, MessagePack, and + BSON. This constructor is used to create a value for serialization to those + formats. + + @note Note, this function exists because of the difficulty in correctly + specifying the correct template overload in the standard value ctor, as both + JSON arrays and JSON binary arrays are backed with some form of a + `std::vector`. Because JSON binary arrays are a non-standard extension it + was decided that it would be best to prevent automatic initialization of a + binary array type, for backwards compatibility and so it does not happen on + accident. + + @param[in] init container containing bytes to use as binary type + + @return JSON binary array value + + @complexity Linear in the size of @a init. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @since version 3.8.0 + */ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json binary(const typename binary_t::container_type& init) + { + auto res = basic_json(); + res.m_type = value_t::binary; + res.m_value = init; + return res; + } + + /*! + @brief explicitly create a binary array (with subtype) + + Creates a JSON binary array value from a given binary container. Binary + values are part of various binary formats, such as CBOR, MessagePack, and + BSON. This constructor is used to create a value for serialization to those + formats. + + @note Note, this function exists because of the difficulty in correctly + specifying the correct template overload in the standard value ctor, as both + JSON arrays and JSON binary arrays are backed with some form of a + `std::vector`. Because JSON binary arrays are a non-standard extension it + was decided that it would be best to prevent automatic initialization of a + binary array type, for backwards compatibility and so it does not happen on + accident. + + @param[in] init container containing bytes to use as binary type + @param[in] subtype subtype to use in MessagePack and BSON + + @return JSON binary array value + + @complexity Linear in the size of @a init. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @since version 3.8.0 + */ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json binary(const typename binary_t::container_type& init, typename binary_t::subtype_type subtype) + { + auto res = basic_json(); + res.m_type = value_t::binary; + res.m_value = binary_t(init, subtype); + return res; + } + + /// @copydoc binary(const typename binary_t::container_type&) + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json binary(typename binary_t::container_type&& init) + { + auto res = basic_json(); + res.m_type = value_t::binary; + res.m_value = std::move(init); + return res; + } + + /// @copydoc binary(const typename binary_t::container_type&, typename binary_t::subtype_type) + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json binary(typename binary_t::container_type&& init, typename binary_t::subtype_type subtype) + { + auto res = basic_json(); + res.m_type = value_t::binary; + res.m_value = binary_t(std::move(init), subtype); + return res; + } + + /*! + @brief explicitly create an array from an initializer list + + Creates a JSON array value from a given initializer list. That is, given a + list of values `a, b, c`, creates the JSON value `[a, b, c]`. If the + initializer list is empty, the empty array `[]` is created. + + @note This function is only needed to express two edge cases that cannot + be realized with the initializer list constructor (@ref + basic_json(initializer_list_t, bool, value_t)). These cases + are: + 1. creating an array whose elements are all pairs whose first element is a + string -- in this case, the initializer list constructor would create an + object, taking the first elements as keys + 2. creating an empty array -- passing the empty initializer list to the + initializer list constructor yields an empty object + + @param[in] init initializer list with JSON values to create an array from + (optional) + + @return JSON array value + + @complexity Linear in the size of @a init. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @liveexample{The following code shows an example for the `array` + function.,array} + + @sa see @ref basic_json(initializer_list_t, bool, value_t) -- + create a JSON value from an initializer list + @sa see @ref object(initializer_list_t) -- create a JSON object + value from an initializer list + + @since version 1.0.0 + */ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json array(initializer_list_t init = {}) + { + return basic_json(init, false, value_t::array); + } + + /*! + @brief explicitly create an object from an initializer list + + Creates a JSON object value from a given initializer list. The initializer + lists elements must be pairs, and their first elements must be strings. If + the initializer list is empty, the empty object `{}` is created. + + @note This function is only added for symmetry reasons. In contrast to the + related function @ref array(initializer_list_t), there are + no cases which can only be expressed by this function. That is, any + initializer list @a init can also be passed to the initializer list + constructor @ref basic_json(initializer_list_t, bool, value_t). + + @param[in] init initializer list to create an object from (optional) + + @return JSON object value + + @throw type_error.301 if @a init is not a list of pairs whose first + elements are strings. In this case, no object can be created. When such a + value is passed to @ref basic_json(initializer_list_t, bool, value_t), + an array would have been created from the passed initializer list @a init. + See example below. + + @complexity Linear in the size of @a init. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @liveexample{The following code shows an example for the `object` + function.,object} + + @sa see @ref basic_json(initializer_list_t, bool, value_t) -- + create a JSON value from an initializer list + @sa see @ref array(initializer_list_t) -- create a JSON array + value from an initializer list + + @since version 1.0.0 + */ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json object(initializer_list_t init = {}) + { + return basic_json(init, false, value_t::object); + } + + /*! + @brief construct an array with count copies of given value + + Constructs a JSON array value by creating @a cnt copies of a passed value. + In case @a cnt is `0`, an empty array is created. + + @param[in] cnt the number of JSON copies of @a val to create + @param[in] val the JSON value to copy + + @post `std::distance(begin(),end()) == cnt` holds. + + @complexity Linear in @a cnt. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @liveexample{The following code shows examples for the @ref + basic_json(size_type\, const basic_json&) + constructor.,basic_json__size_type_basic_json} + + @since version 1.0.0 + */ + basic_json(size_type cnt, const basic_json& val) + : m_type(value_t::array) + { + m_value.array = create(cnt, val); + set_parents(); + assert_invariant(); + } + + /*! + @brief construct a JSON container given an iterator range + + Constructs the JSON value with the contents of the range `[first, last)`. + The semantics depends on the different types a JSON value can have: + - In case of a null type, invalid_iterator.206 is thrown. + - In case of other primitive types (number, boolean, or string), @a first + must be `begin()` and @a last must be `end()`. In this case, the value is + copied. Otherwise, invalid_iterator.204 is thrown. + - In case of structured types (array, object), the constructor behaves as + similar versions for `std::vector` or `std::map`; that is, a JSON array + or object is constructed from the values in the range. + + @tparam InputIT an input iterator type (@ref iterator or @ref + const_iterator) + + @param[in] first begin of the range to copy from (included) + @param[in] last end of the range to copy from (excluded) + + @pre Iterators @a first and @a last must be initialized. **This + precondition is enforced with an assertion (see warning).** If + assertions are switched off, a violation of this precondition yields + undefined behavior. + + @pre Range `[first, last)` is valid. Usually, this precondition cannot be + checked efficiently. Only certain edge cases are detected; see the + description of the exceptions below. A violation of this precondition + yields undefined behavior. + + @warning A precondition is enforced with a runtime assertion that will + result in calling `std::abort` if this precondition is not met. + Assertions can be disabled by defining `NDEBUG` at compile time. + See https://en.cppreference.com/w/cpp/error/assert for more + information. + + @throw invalid_iterator.201 if iterators @a first and @a last are not + compatible (i.e., do not belong to the same JSON value). In this case, + the range `[first, last)` is undefined. + @throw invalid_iterator.204 if iterators @a first and @a last belong to a + primitive type (number, boolean, or string), but @a first does not point + to the first element any more. In this case, the range `[first, last)` is + undefined. See example code below. + @throw invalid_iterator.206 if iterators @a first and @a last belong to a + null value. In this case, the range `[first, last)` is undefined. + + @complexity Linear in distance between @a first and @a last. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @liveexample{The example below shows several ways to create JSON values by + specifying a subrange with iterators.,basic_json__InputIt_InputIt} + + @since version 1.0.0 + */ + template < class InputIT, typename std::enable_if < + std::is_same::value || + std::is_same::value, int >::type = 0 > + basic_json(InputIT first, InputIT last) + { + JSON_ASSERT(first.m_object != nullptr); + JSON_ASSERT(last.m_object != nullptr); + + // make sure iterator fits the current value + if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) + { + JSON_THROW(invalid_iterator::create(201, "iterators are not compatible", basic_json())); + } + + // copy type from first iterator + m_type = first.m_object->m_type; + + // check if iterator range is complete for primitive values + switch (m_type) + { + case value_t::boolean: + case value_t::number_float: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::string: + { + if (JSON_HEDLEY_UNLIKELY(!first.m_it.primitive_iterator.is_begin() + || !last.m_it.primitive_iterator.is_end())) + { + JSON_THROW(invalid_iterator::create(204, "iterators out of range", *first.m_object)); + } + break; + } + + case value_t::null: + case value_t::object: + case value_t::array: + case value_t::binary: + case value_t::discarded: + default: + break; + } + + switch (m_type) + { + case value_t::number_integer: + { + m_value.number_integer = first.m_object->m_value.number_integer; + break; + } + + case value_t::number_unsigned: + { + m_value.number_unsigned = first.m_object->m_value.number_unsigned; + break; + } + + case value_t::number_float: + { + m_value.number_float = first.m_object->m_value.number_float; + break; + } + + case value_t::boolean: + { + m_value.boolean = first.m_object->m_value.boolean; + break; + } + + case value_t::string: + { + m_value = *first.m_object->m_value.string; + break; + } + + case value_t::object: + { + m_value.object = create(first.m_it.object_iterator, + last.m_it.object_iterator); + break; + } + + case value_t::array: + { + m_value.array = create(first.m_it.array_iterator, + last.m_it.array_iterator); + break; + } + + case value_t::binary: + { + m_value = *first.m_object->m_value.binary; + break; + } + + case value_t::null: + case value_t::discarded: + default: + JSON_THROW(invalid_iterator::create(206, "cannot construct with iterators from " + std::string(first.m_object->type_name()), *first.m_object)); + } + + set_parents(); + assert_invariant(); + } + + + /////////////////////////////////////// + // other constructors and destructor // + /////////////////////////////////////// + + template, + std::is_same>::value, int> = 0 > + basic_json(const JsonRef& ref) : basic_json(ref.moved_or_copied()) {} + + /*! + @brief copy constructor + + Creates a copy of a given JSON value. + + @param[in] other the JSON value to copy + + @post `*this == other` + + @complexity Linear in the size of @a other. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes to any JSON value. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is linear. + - As postcondition, it holds: `other == basic_json(other)`. + + @liveexample{The following code shows an example for the copy + constructor.,basic_json__basic_json} + + @since version 1.0.0 + */ + basic_json(const basic_json& other) + : m_type(other.m_type) + { + // check of passed value is valid + other.assert_invariant(); + + switch (m_type) + { + case value_t::object: + { + m_value = *other.m_value.object; + break; + } + + case value_t::array: + { + m_value = *other.m_value.array; + break; + } + + case value_t::string: + { + m_value = *other.m_value.string; + break; + } + + case value_t::boolean: + { + m_value = other.m_value.boolean; + break; + } + + case value_t::number_integer: + { + m_value = other.m_value.number_integer; + break; + } + + case value_t::number_unsigned: + { + m_value = other.m_value.number_unsigned; + break; + } + + case value_t::number_float: + { + m_value = other.m_value.number_float; + break; + } + + case value_t::binary: + { + m_value = *other.m_value.binary; + break; + } + + case value_t::null: + case value_t::discarded: + default: + break; + } + + set_parents(); + assert_invariant(); + } + + /*! + @brief move constructor + + Move constructor. Constructs a JSON value with the contents of the given + value @a other using move semantics. It "steals" the resources from @a + other and leaves it as JSON null value. + + @param[in,out] other value to move to this object + + @post `*this` has the same value as @a other before the call. + @post @a other is a JSON null value. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this constructor never throws + exceptions. + + @requirement This function helps `basic_json` satisfying the + [MoveConstructible](https://en.cppreference.com/w/cpp/named_req/MoveConstructible) + requirements. + + @liveexample{The code below shows the move constructor explicitly called + via std::move.,basic_json__moveconstructor} + + @since version 1.0.0 + */ + basic_json(basic_json&& other) noexcept + : m_type(std::move(other.m_type)), + m_value(std::move(other.m_value)) + { + // check that passed value is valid + other.assert_invariant(false); + + // invalidate payload + other.m_type = value_t::null; + other.m_value = {}; + + set_parents(); + assert_invariant(); + } + + /*! + @brief copy assignment + + Copy assignment operator. Copies a JSON value via the "copy and swap" + strategy: It is expressed in terms of the copy constructor, destructor, + and the `swap()` member function. + + @param[in] other value to copy from + + @complexity Linear. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is linear. + + @liveexample{The code below shows and example for the copy assignment. It + creates a copy of value `a` which is then swapped with `b`. Finally\, the + copy of `a` (which is the null value after the swap) is + destroyed.,basic_json__copyassignment} + + @since version 1.0.0 + */ + basic_json& operator=(basic_json other) noexcept ( + std::is_nothrow_move_constructible::value&& + std::is_nothrow_move_assignable::value&& + std::is_nothrow_move_constructible::value&& + std::is_nothrow_move_assignable::value + ) + { + // check that passed value is valid + other.assert_invariant(); + + using std::swap; + swap(m_type, other.m_type); + swap(m_value, other.m_value); + + set_parents(); + assert_invariant(); + return *this; + } + + /*! + @brief destructor + + Destroys the JSON value and frees all allocated memory. + + @complexity Linear. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is linear. + - All stored elements are destroyed and all memory is freed. + + @since version 1.0.0 + */ + ~basic_json() noexcept + { + assert_invariant(false); + m_value.destroy(m_type); + } + + /// @} + + public: + /////////////////////// + // object inspection // + /////////////////////// + + /// @name object inspection + /// Functions to inspect the type of a JSON value. + /// @{ + + /*! + @brief serialization + + Serialization function for JSON values. The function tries to mimic + Python's `json.dumps()` function, and currently supports its @a indent + and @a ensure_ascii parameters. + + @param[in] indent If indent is nonnegative, then array elements and object + members will be pretty-printed with that indent level. An indent level of + `0` will only insert newlines. `-1` (the default) selects the most compact + representation. + @param[in] indent_char The character to use for indentation if @a indent is + greater than `0`. The default is ` ` (space). + @param[in] ensure_ascii If @a ensure_ascii is true, all non-ASCII characters + in the output are escaped with `\uXXXX` sequences, and the result consists + of ASCII characters only. + @param[in] error_handler how to react on decoding errors; there are three + possible values: `strict` (throws and exception in case a decoding error + occurs; default), `replace` (replace invalid UTF-8 sequences with U+FFFD), + and `ignore` (ignore invalid UTF-8 sequences during serialization; all + bytes are copied to the output unchanged). + + @return string containing the serialization of the JSON value + + @throw type_error.316 if a string stored inside the JSON value is not + UTF-8 encoded and @a error_handler is set to strict + + @note Binary values are serialized as object containing two keys: + - "bytes": an array of bytes as integers + - "subtype": the subtype as integer or "null" if the binary has no subtype + + @complexity Linear. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @liveexample{The following example shows the effect of different @a indent\, + @a indent_char\, and @a ensure_ascii parameters to the result of the + serialization.,dump} + + @see https://docs.python.org/2/library/json.html#json.dump + + @since version 1.0.0; indentation character @a indent_char, option + @a ensure_ascii and exceptions added in version 3.0.0; error + handlers added in version 3.4.0; serialization of binary values added + in version 3.8.0. + */ + string_t dump(const int indent = -1, + const char indent_char = ' ', + const bool ensure_ascii = false, + const error_handler_t error_handler = error_handler_t::strict) const + { + string_t result; + serializer s(detail::output_adapter(result), indent_char, error_handler); + + if (indent >= 0) + { + s.dump(*this, true, ensure_ascii, static_cast(indent)); + } + else + { + s.dump(*this, false, ensure_ascii, 0); + } + + return result; + } + + /*! + @brief return the type of the JSON value (explicit) + + Return the type of the JSON value as a value from the @ref value_t + enumeration. + + @return the type of the JSON value + Value type | return value + ------------------------- | ------------------------- + null | value_t::null + boolean | value_t::boolean + string | value_t::string + number (integer) | value_t::number_integer + number (unsigned integer) | value_t::number_unsigned + number (floating-point) | value_t::number_float + object | value_t::object + array | value_t::array + binary | value_t::binary + discarded | value_t::discarded + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `type()` for all JSON + types.,type} + + @sa see @ref operator value_t() -- return the type of the JSON value (implicit) + @sa see @ref type_name() -- return the type as string + + @since version 1.0.0 + */ + constexpr value_t type() const noexcept + { + return m_type; + } + + /*! + @brief return whether type is primitive + + This function returns true if and only if the JSON type is primitive + (string, number, boolean, or null). + + @return `true` if type is primitive (string, number, boolean, or null), + `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_primitive()` for all JSON + types.,is_primitive} + + @sa see @ref is_structured() -- returns whether JSON value is structured + @sa see @ref is_null() -- returns whether JSON value is `null` + @sa see @ref is_string() -- returns whether JSON value is a string + @sa see @ref is_boolean() -- returns whether JSON value is a boolean + @sa see @ref is_number() -- returns whether JSON value is a number + @sa see @ref is_binary() -- returns whether JSON value is a binary array + + @since version 1.0.0 + */ + constexpr bool is_primitive() const noexcept + { + return is_null() || is_string() || is_boolean() || is_number() || is_binary(); + } + + /*! + @brief return whether type is structured + + This function returns true if and only if the JSON type is structured + (array or object). + + @return `true` if type is structured (array or object), `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_structured()` for all JSON + types.,is_structured} + + @sa see @ref is_primitive() -- returns whether value is primitive + @sa see @ref is_array() -- returns whether value is an array + @sa see @ref is_object() -- returns whether value is an object + + @since version 1.0.0 + */ + constexpr bool is_structured() const noexcept + { + return is_array() || is_object(); + } + + /*! + @brief return whether value is null + + This function returns true if and only if the JSON value is null. + + @return `true` if type is null, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_null()` for all JSON + types.,is_null} + + @since version 1.0.0 + */ + constexpr bool is_null() const noexcept + { + return m_type == value_t::null; + } + + /*! + @brief return whether value is a boolean + + This function returns true if and only if the JSON value is a boolean. + + @return `true` if type is boolean, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_boolean()` for all JSON + types.,is_boolean} + + @since version 1.0.0 + */ + constexpr bool is_boolean() const noexcept + { + return m_type == value_t::boolean; + } + + /*! + @brief return whether value is a number + + This function returns true if and only if the JSON value is a number. This + includes both integer (signed and unsigned) and floating-point values. + + @return `true` if type is number (regardless whether integer, unsigned + integer or floating-type), `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_number()` for all JSON + types.,is_number} + + @sa see @ref is_number_integer() -- check if value is an integer or unsigned + integer number + @sa see @ref is_number_unsigned() -- check if value is an unsigned integer + number + @sa see @ref is_number_float() -- check if value is a floating-point number + + @since version 1.0.0 + */ + constexpr bool is_number() const noexcept + { + return is_number_integer() || is_number_float(); + } + + /*! + @brief return whether value is an integer number + + This function returns true if and only if the JSON value is a signed or + unsigned integer number. This excludes floating-point values. + + @return `true` if type is an integer or unsigned integer number, `false` + otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_number_integer()` for all + JSON types.,is_number_integer} + + @sa see @ref is_number() -- check if value is a number + @sa see @ref is_number_unsigned() -- check if value is an unsigned integer + number + @sa see @ref is_number_float() -- check if value is a floating-point number + + @since version 1.0.0 + */ + constexpr bool is_number_integer() const noexcept + { + return m_type == value_t::number_integer || m_type == value_t::number_unsigned; + } + + /*! + @brief return whether value is an unsigned integer number + + This function returns true if and only if the JSON value is an unsigned + integer number. This excludes floating-point and signed integer values. + + @return `true` if type is an unsigned integer number, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_number_unsigned()` for all + JSON types.,is_number_unsigned} + + @sa see @ref is_number() -- check if value is a number + @sa see @ref is_number_integer() -- check if value is an integer or unsigned + integer number + @sa see @ref is_number_float() -- check if value is a floating-point number + + @since version 2.0.0 + */ + constexpr bool is_number_unsigned() const noexcept + { + return m_type == value_t::number_unsigned; + } + + /*! + @brief return whether value is a floating-point number + + This function returns true if and only if the JSON value is a + floating-point number. This excludes signed and unsigned integer values. + + @return `true` if type is a floating-point number, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_number_float()` for all + JSON types.,is_number_float} + + @sa see @ref is_number() -- check if value is number + @sa see @ref is_number_integer() -- check if value is an integer number + @sa see @ref is_number_unsigned() -- check if value is an unsigned integer + number + + @since version 1.0.0 + */ + constexpr bool is_number_float() const noexcept + { + return m_type == value_t::number_float; + } + + /*! + @brief return whether value is an object + + This function returns true if and only if the JSON value is an object. + + @return `true` if type is object, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_object()` for all JSON + types.,is_object} + + @since version 1.0.0 + */ + constexpr bool is_object() const noexcept + { + return m_type == value_t::object; + } + + /*! + @brief return whether value is an array + + This function returns true if and only if the JSON value is an array. + + @return `true` if type is array, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_array()` for all JSON + types.,is_array} + + @since version 1.0.0 + */ + constexpr bool is_array() const noexcept + { + return m_type == value_t::array; + } + + /*! + @brief return whether value is a string + + This function returns true if and only if the JSON value is a string. + + @return `true` if type is string, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_string()` for all JSON + types.,is_string} + + @since version 1.0.0 + */ + constexpr bool is_string() const noexcept + { + return m_type == value_t::string; + } + + /*! + @brief return whether value is a binary array + + This function returns true if and only if the JSON value is a binary array. + + @return `true` if type is binary array, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_binary()` for all JSON + types.,is_binary} + + @since version 3.8.0 + */ + constexpr bool is_binary() const noexcept + { + return m_type == value_t::binary; + } + + /*! + @brief return whether value is discarded + + This function returns true if and only if the JSON value was discarded + during parsing with a callback function (see @ref parser_callback_t). + + @note This function will always be `false` for JSON values after parsing. + That is, discarded values can only occur during parsing, but will be + removed when inside a structured value or replaced by null in other cases. + + @return `true` if type is discarded, `false` otherwise. + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies `is_discarded()` for all JSON + types.,is_discarded} + + @since version 1.0.0 + */ + constexpr bool is_discarded() const noexcept + { + return m_type == value_t::discarded; + } + + /*! + @brief return the type of the JSON value (implicit) + + Implicitly return the type of the JSON value as a value from the @ref + value_t enumeration. + + @return the type of the JSON value + + @complexity Constant. + + @exceptionsafety No-throw guarantee: this member function never throws + exceptions. + + @liveexample{The following code exemplifies the @ref value_t operator for + all JSON types.,operator__value_t} + + @sa see @ref type() -- return the type of the JSON value (explicit) + @sa see @ref type_name() -- return the type as string + + @since version 1.0.0 + */ + constexpr operator value_t() const noexcept + { + return m_type; + } + + /// @} + + private: + ////////////////// + // value access // + ////////////////// + + /// get a boolean (explicit) + boolean_t get_impl(boolean_t* /*unused*/) const + { + if (JSON_HEDLEY_LIKELY(is_boolean())) + { + return m_value.boolean; + } + + JSON_THROW(type_error::create(302, "type must be boolean, but is " + std::string(type_name()), *this)); + } + + /// get a pointer to the value (object) + object_t* get_impl_ptr(object_t* /*unused*/) noexcept + { + return is_object() ? m_value.object : nullptr; + } + + /// get a pointer to the value (object) + constexpr const object_t* get_impl_ptr(const object_t* /*unused*/) const noexcept + { + return is_object() ? m_value.object : nullptr; + } + + /// get a pointer to the value (array) + array_t* get_impl_ptr(array_t* /*unused*/) noexcept + { + return is_array() ? m_value.array : nullptr; + } + + /// get a pointer to the value (array) + constexpr const array_t* get_impl_ptr(const array_t* /*unused*/) const noexcept + { + return is_array() ? m_value.array : nullptr; + } + + /// get a pointer to the value (string) + string_t* get_impl_ptr(string_t* /*unused*/) noexcept + { + return is_string() ? m_value.string : nullptr; + } + + /// get a pointer to the value (string) + constexpr const string_t* get_impl_ptr(const string_t* /*unused*/) const noexcept + { + return is_string() ? m_value.string : nullptr; + } + + /// get a pointer to the value (boolean) + boolean_t* get_impl_ptr(boolean_t* /*unused*/) noexcept + { + return is_boolean() ? &m_value.boolean : nullptr; + } + + /// get a pointer to the value (boolean) + constexpr const boolean_t* get_impl_ptr(const boolean_t* /*unused*/) const noexcept + { + return is_boolean() ? &m_value.boolean : nullptr; + } + + /// get a pointer to the value (integer number) + number_integer_t* get_impl_ptr(number_integer_t* /*unused*/) noexcept + { + return is_number_integer() ? &m_value.number_integer : nullptr; + } + + /// get a pointer to the value (integer number) + constexpr const number_integer_t* get_impl_ptr(const number_integer_t* /*unused*/) const noexcept + { + return is_number_integer() ? &m_value.number_integer : nullptr; + } + + /// get a pointer to the value (unsigned number) + number_unsigned_t* get_impl_ptr(number_unsigned_t* /*unused*/) noexcept + { + return is_number_unsigned() ? &m_value.number_unsigned : nullptr; + } + + /// get a pointer to the value (unsigned number) + constexpr const number_unsigned_t* get_impl_ptr(const number_unsigned_t* /*unused*/) const noexcept + { + return is_number_unsigned() ? &m_value.number_unsigned : nullptr; + } + + /// get a pointer to the value (floating-point number) + number_float_t* get_impl_ptr(number_float_t* /*unused*/) noexcept + { + return is_number_float() ? &m_value.number_float : nullptr; + } + + /// get a pointer to the value (floating-point number) + constexpr const number_float_t* get_impl_ptr(const number_float_t* /*unused*/) const noexcept + { + return is_number_float() ? &m_value.number_float : nullptr; + } + + /// get a pointer to the value (binary) + binary_t* get_impl_ptr(binary_t* /*unused*/) noexcept + { + return is_binary() ? m_value.binary : nullptr; + } + + /// get a pointer to the value (binary) + constexpr const binary_t* get_impl_ptr(const binary_t* /*unused*/) const noexcept + { + return is_binary() ? m_value.binary : nullptr; + } + + /*! + @brief helper function to implement get_ref() + + This function helps to implement get_ref() without code duplication for + const and non-const overloads + + @tparam ThisType will be deduced as `basic_json` or `const basic_json` + + @throw type_error.303 if ReferenceType does not match underlying value + type of the current JSON + */ + template + static ReferenceType get_ref_impl(ThisType& obj) + { + // delegate the call to get_ptr<>() + auto* ptr = obj.template get_ptr::type>(); + + if (JSON_HEDLEY_LIKELY(ptr != nullptr)) + { + return *ptr; + } + + JSON_THROW(type_error::create(303, "incompatible ReferenceType for get_ref, actual type is " + std::string(obj.type_name()), obj)); + } + + public: + /// @name value access + /// Direct access to the stored value of a JSON value. + /// @{ + + /*! + @brief get a pointer value (implicit) + + Implicit pointer access to the internally stored JSON value. No copies are + made. + + @warning Writing data to the pointee of the result yields an undefined + state. + + @tparam PointerType pointer type; must be a pointer to @ref array_t, @ref + object_t, @ref string_t, @ref boolean_t, @ref number_integer_t, + @ref number_unsigned_t, or @ref number_float_t. Enforced by a static + assertion. + + @return pointer to the internally stored JSON value if the requested + pointer type @a PointerType fits to the JSON value; `nullptr` otherwise + + @complexity Constant. + + @liveexample{The example below shows how pointers to internal values of a + JSON value can be requested. Note that no type conversions are made and a + `nullptr` is returned if the value and the requested pointer type does not + match.,get_ptr} + + @since version 1.0.0 + */ + template::value, int>::type = 0> + auto get_ptr() noexcept -> decltype(std::declval().get_impl_ptr(std::declval())) + { + // delegate the call to get_impl_ptr<>() + return get_impl_ptr(static_cast(nullptr)); + } + + /*! + @brief get a pointer value (implicit) + @copydoc get_ptr() + */ + template < typename PointerType, typename std::enable_if < + std::is_pointer::value&& + std::is_const::type>::value, int >::type = 0 > + constexpr auto get_ptr() const noexcept -> decltype(std::declval().get_impl_ptr(std::declval())) + { + // delegate the call to get_impl_ptr<>() const + return get_impl_ptr(static_cast(nullptr)); + } + + private: + /*! + @brief get a value (explicit) + + Explicit type conversion between the JSON value and a compatible value + which is [CopyConstructible](https://en.cppreference.com/w/cpp/named_req/CopyConstructible) + and [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible). + The value is converted by calling the @ref json_serializer + `from_json()` method. + + The function is equivalent to executing + @code {.cpp} + ValueType ret; + JSONSerializer::from_json(*this, ret); + return ret; + @endcode + + This overloads is chosen if: + - @a ValueType is not @ref basic_json, + - @ref json_serializer has a `from_json()` method of the form + `void from_json(const basic_json&, ValueType&)`, and + - @ref json_serializer does not have a `from_json()` method of + the form `ValueType from_json(const basic_json&)` + + @tparam ValueType the returned value type + + @return copy of the JSON value, converted to @a ValueType + + @throw what @ref json_serializer `from_json()` method throws + + @liveexample{The example below shows several conversions from JSON values + to other types. There a few things to note: (1) Floating-point numbers can + be converted to integers\, (2) A JSON array can be converted to a standard + `std::vector`\, (3) A JSON object can be converted to C++ + associative containers such as `std::unordered_map`.,get__ValueType_const} + + @since version 2.1.0 + */ + template < typename ValueType, + detail::enable_if_t < + detail::is_default_constructible::value&& + detail::has_from_json::value, + int > = 0 > + ValueType get_impl(detail::priority_tag<0> /*unused*/) const noexcept(noexcept( + JSONSerializer::from_json(std::declval(), std::declval()))) + { + auto ret = ValueType(); + JSONSerializer::from_json(*this, ret); + return ret; + } + + /*! + @brief get a value (explicit); special case + + Explicit type conversion between the JSON value and a compatible value + which is **not** [CopyConstructible](https://en.cppreference.com/w/cpp/named_req/CopyConstructible) + and **not** [DefaultConstructible](https://en.cppreference.com/w/cpp/named_req/DefaultConstructible). + The value is converted by calling the @ref json_serializer + `from_json()` method. + + The function is equivalent to executing + @code {.cpp} + return JSONSerializer::from_json(*this); + @endcode + + This overloads is chosen if: + - @a ValueType is not @ref basic_json and + - @ref json_serializer has a `from_json()` method of the form + `ValueType from_json(const basic_json&)` + + @note If @ref json_serializer has both overloads of + `from_json()`, this one is chosen. + + @tparam ValueType the returned value type + + @return copy of the JSON value, converted to @a ValueType + + @throw what @ref json_serializer `from_json()` method throws + + @since version 2.1.0 + */ + template < typename ValueType, + detail::enable_if_t < + detail::has_non_default_from_json::value, + int > = 0 > + ValueType get_impl(detail::priority_tag<1> /*unused*/) const noexcept(noexcept( + JSONSerializer::from_json(std::declval()))) + { + return JSONSerializer::from_json(*this); + } + + /*! + @brief get special-case overload + + This overloads converts the current @ref basic_json in a different + @ref basic_json type + + @tparam BasicJsonType == @ref basic_json + + @return a copy of *this, converted into @a BasicJsonType + + @complexity Depending on the implementation of the called `from_json()` + method. + + @since version 3.2.0 + */ + template < typename BasicJsonType, + detail::enable_if_t < + detail::is_basic_json::value, + int > = 0 > + BasicJsonType get_impl(detail::priority_tag<2> /*unused*/) const + { + return *this; + } + + /*! + @brief get special-case overload + + This overloads avoids a lot of template boilerplate, it can be seen as the + identity method + + @tparam BasicJsonType == @ref basic_json + + @return a copy of *this + + @complexity Constant. + + @since version 2.1.0 + */ + template::value, + int> = 0> + basic_json get_impl(detail::priority_tag<3> /*unused*/) const + { + return *this; + } + + /*! + @brief get a pointer value (explicit) + @copydoc get() + */ + template::value, + int> = 0> + constexpr auto get_impl(detail::priority_tag<4> /*unused*/) const noexcept + -> decltype(std::declval().template get_ptr()) + { + // delegate the call to get_ptr + return get_ptr(); + } + + public: + /*! + @brief get a (pointer) value (explicit) + + Performs explicit type conversion between the JSON value and a compatible value if required. + + - If the requested type is a pointer to the internally stored JSON value that pointer is returned. + No copies are made. + + - If the requested type is the current @ref basic_json, or a different @ref basic_json convertible + from the current @ref basic_json. + + - Otherwise the value is converted by calling the @ref json_serializer `from_json()` + method. + + @tparam ValueTypeCV the provided value type + @tparam ValueType the returned value type + + @return copy of the JSON value, converted to @tparam ValueType if necessary + + @throw what @ref json_serializer `from_json()` method throws if conversion is required + + @since version 2.1.0 + */ + template < typename ValueTypeCV, typename ValueType = detail::uncvref_t> +#if defined(JSON_HAS_CPP_14) + constexpr +#endif + auto get() const noexcept( + noexcept(std::declval().template get_impl(detail::priority_tag<4> {}))) + -> decltype(std::declval().template get_impl(detail::priority_tag<4> {})) + { + // we cannot static_assert on ValueTypeCV being non-const, because + // there is support for get(), which is why we + // still need the uncvref + static_assert(!std::is_reference::value, + "get() cannot be used with reference types, you might want to use get_ref()"); + return get_impl(detail::priority_tag<4> {}); + } + + /*! + @brief get a pointer value (explicit) + + Explicit pointer access to the internally stored JSON value. No copies are + made. + + @warning The pointer becomes invalid if the underlying JSON object + changes. + + @tparam PointerType pointer type; must be a pointer to @ref array_t, @ref + object_t, @ref string_t, @ref boolean_t, @ref number_integer_t, + @ref number_unsigned_t, or @ref number_float_t. + + @return pointer to the internally stored JSON value if the requested + pointer type @a PointerType fits to the JSON value; `nullptr` otherwise + + @complexity Constant. + + @liveexample{The example below shows how pointers to internal values of a + JSON value can be requested. Note that no type conversions are made and a + `nullptr` is returned if the value and the requested pointer type does not + match.,get__PointerType} + + @sa see @ref get_ptr() for explicit pointer-member access + + @since version 1.0.0 + */ + template::value, int>::type = 0> + auto get() noexcept -> decltype(std::declval().template get_ptr()) + { + // delegate the call to get_ptr + return get_ptr(); + } + + /*! + @brief get a value (explicit) + + Explicit type conversion between the JSON value and a compatible value. + The value is filled into the input parameter by calling the @ref json_serializer + `from_json()` method. + + The function is equivalent to executing + @code {.cpp} + ValueType v; + JSONSerializer::from_json(*this, v); + @endcode + + This overloads is chosen if: + - @a ValueType is not @ref basic_json, + - @ref json_serializer has a `from_json()` method of the form + `void from_json(const basic_json&, ValueType&)`, and + + @tparam ValueType the input parameter type. + + @return the input parameter, allowing chaining calls. + + @throw what @ref json_serializer `from_json()` method throws + + @liveexample{The example below shows several conversions from JSON values + to other types. There a few things to note: (1) Floating-point numbers can + be converted to integers\, (2) A JSON array can be converted to a standard + `std::vector`\, (3) A JSON object can be converted to C++ + associative containers such as `std::unordered_map`.,get_to} + + @since version 3.3.0 + */ + template < typename ValueType, + detail::enable_if_t < + !detail::is_basic_json::value&& + detail::has_from_json::value, + int > = 0 > + ValueType & get_to(ValueType& v) const noexcept(noexcept( + JSONSerializer::from_json(std::declval(), v))) + { + JSONSerializer::from_json(*this, v); + return v; + } + + // specialization to allow to call get_to with a basic_json value + // see https://github.com/nlohmann/json/issues/2175 + template::value, + int> = 0> + ValueType & get_to(ValueType& v) const + { + v = *this; + return v; + } + + template < + typename T, std::size_t N, + typename Array = T (&)[N], // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + detail::enable_if_t < + detail::has_from_json::value, int > = 0 > + Array get_to(T (&v)[N]) const // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) + noexcept(noexcept(JSONSerializer::from_json( + std::declval(), v))) + { + JSONSerializer::from_json(*this, v); + return v; + } + + /*! + @brief get a reference value (implicit) + + Implicit reference access to the internally stored JSON value. No copies + are made. + + @warning Writing data to the referee of the result yields an undefined + state. + + @tparam ReferenceType reference type; must be a reference to @ref array_t, + @ref object_t, @ref string_t, @ref boolean_t, @ref number_integer_t, or + @ref number_float_t. Enforced by static assertion. + + @return reference to the internally stored JSON value if the requested + reference type @a ReferenceType fits to the JSON value; throws + type_error.303 otherwise + + @throw type_error.303 in case passed type @a ReferenceType is incompatible + with the stored JSON value; see example below + + @complexity Constant. + + @liveexample{The example shows several calls to `get_ref()`.,get_ref} + + @since version 1.1.0 + */ + template::value, int>::type = 0> + ReferenceType get_ref() + { + // delegate call to get_ref_impl + return get_ref_impl(*this); + } + + /*! + @brief get a reference value (implicit) + @copydoc get_ref() + */ + template < typename ReferenceType, typename std::enable_if < + std::is_reference::value&& + std::is_const::type>::value, int >::type = 0 > + ReferenceType get_ref() const + { + // delegate call to get_ref_impl + return get_ref_impl(*this); + } + + /*! + @brief get a value (implicit) + + Implicit type conversion between the JSON value and a compatible value. + The call is realized by calling @ref get() const. + + @tparam ValueType non-pointer type compatible to the JSON value, for + instance `int` for JSON integer numbers, `bool` for JSON booleans, or + `std::vector` types for JSON arrays. The character type of @ref string_t + as well as an initializer list of this type is excluded to avoid + ambiguities as these types implicitly convert to `std::string`. + + @return copy of the JSON value, converted to type @a ValueType + + @throw type_error.302 in case passed type @a ValueType is incompatible + to the JSON value type (e.g., the JSON value is of type boolean, but a + string is requested); see example below + + @complexity Linear in the size of the JSON value. + + @liveexample{The example below shows several conversions from JSON values + to other types. There a few things to note: (1) Floating-point numbers can + be converted to integers\, (2) A JSON array can be converted to a standard + `std::vector`\, (3) A JSON object can be converted to C++ + associative containers such as `std::unordered_map`.,operator__ValueType} + + @since version 1.0.0 + */ + template < typename ValueType, typename std::enable_if < + detail::conjunction < + detail::negation>, + detail::negation>>, + detail::negation>, + detail::negation>, + detail::negation>>, + +#if defined(JSON_HAS_CPP_17) && (defined(__GNUC__) || (defined(_MSC_VER) && _MSC_VER >= 1910 && _MSC_VER <= 1914)) + detail::negation>, +#endif + detail::is_detected_lazy + >::value, int >::type = 0 > + JSON_EXPLICIT operator ValueType() const + { + // delegate the call to get<>() const + return get(); + } + + /*! + @return reference to the binary value + + @throw type_error.302 if the value is not binary + + @sa see @ref is_binary() to check if the value is binary + + @since version 3.8.0 + */ + binary_t& get_binary() + { + if (!is_binary()) + { + JSON_THROW(type_error::create(302, "type must be binary, but is " + std::string(type_name()), *this)); + } + + return *get_ptr(); + } + + /// @copydoc get_binary() + const binary_t& get_binary() const + { + if (!is_binary()) + { + JSON_THROW(type_error::create(302, "type must be binary, but is " + std::string(type_name()), *this)); + } + + return *get_ptr(); + } + + /// @} + + + //////////////////// + // element access // + //////////////////// + + /// @name element access + /// Access to the JSON value. + /// @{ + + /*! + @brief access specified array element with bounds checking + + Returns a reference to the element at specified location @a idx, with + bounds checking. + + @param[in] idx index of the element to access + + @return reference to the element at index @a idx + + @throw type_error.304 if the JSON value is not an array; in this case, + calling `at` with an index makes no sense. See example below. + @throw out_of_range.401 if the index @a idx is out of range of the array; + that is, `idx >= size()`. See example below. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @complexity Constant. + + @since version 1.0.0 + + @liveexample{The example below shows how array elements can be read and + written using `at()`. It also demonstrates the different exceptions that + can be thrown.,at__size_type} + */ + reference at(size_type idx) + { + // at only works for arrays + if (JSON_HEDLEY_LIKELY(is_array())) + { + JSON_TRY + { + return set_parent(m_value.array->at(idx)); + } + JSON_CATCH (std::out_of_range&) + { + // create better exception explanation + JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range", *this)); + } + } + else + { + JSON_THROW(type_error::create(304, "cannot use at() with " + std::string(type_name()), *this)); + } + } + + /*! + @brief access specified array element with bounds checking + + Returns a const reference to the element at specified location @a idx, + with bounds checking. + + @param[in] idx index of the element to access + + @return const reference to the element at index @a idx + + @throw type_error.304 if the JSON value is not an array; in this case, + calling `at` with an index makes no sense. See example below. + @throw out_of_range.401 if the index @a idx is out of range of the array; + that is, `idx >= size()`. See example below. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @complexity Constant. + + @since version 1.0.0 + + @liveexample{The example below shows how array elements can be read using + `at()`. It also demonstrates the different exceptions that can be thrown., + at__size_type_const} + */ + const_reference at(size_type idx) const + { + // at only works for arrays + if (JSON_HEDLEY_LIKELY(is_array())) + { + JSON_TRY + { + return m_value.array->at(idx); + } + JSON_CATCH (std::out_of_range&) + { + // create better exception explanation + JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range", *this)); + } + } + else + { + JSON_THROW(type_error::create(304, "cannot use at() with " + std::string(type_name()), *this)); + } + } + + /*! + @brief access specified object element with bounds checking + + Returns a reference to the element at with specified key @a key, with + bounds checking. + + @param[in] key key of the element to access + + @return reference to the element at key @a key + + @throw type_error.304 if the JSON value is not an object; in this case, + calling `at` with a key makes no sense. See example below. + @throw out_of_range.403 if the key @a key is is not stored in the object; + that is, `find(key) == end()`. See example below. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @complexity Logarithmic in the size of the container. + + @sa see @ref operator[](const typename object_t::key_type&) for unchecked + access by reference + @sa see @ref value() for access by value with a default value + + @since version 1.0.0 + + @liveexample{The example below shows how object elements can be read and + written using `at()`. It also demonstrates the different exceptions that + can be thrown.,at__object_t_key_type} + */ + reference at(const typename object_t::key_type& key) + { + // at only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + JSON_TRY + { + return set_parent(m_value.object->at(key)); + } + JSON_CATCH (std::out_of_range&) + { + // create better exception explanation + JSON_THROW(out_of_range::create(403, "key '" + key + "' not found", *this)); + } + } + else + { + JSON_THROW(type_error::create(304, "cannot use at() with " + std::string(type_name()), *this)); + } + } + + /*! + @brief access specified object element with bounds checking + + Returns a const reference to the element at with specified key @a key, + with bounds checking. + + @param[in] key key of the element to access + + @return const reference to the element at key @a key + + @throw type_error.304 if the JSON value is not an object; in this case, + calling `at` with a key makes no sense. See example below. + @throw out_of_range.403 if the key @a key is is not stored in the object; + that is, `find(key) == end()`. See example below. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @complexity Logarithmic in the size of the container. + + @sa see @ref operator[](const typename object_t::key_type&) for unchecked + access by reference + @sa see @ref value() for access by value with a default value + + @since version 1.0.0 + + @liveexample{The example below shows how object elements can be read using + `at()`. It also demonstrates the different exceptions that can be thrown., + at__object_t_key_type_const} + */ + const_reference at(const typename object_t::key_type& key) const + { + // at only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + JSON_TRY + { + return m_value.object->at(key); + } + JSON_CATCH (std::out_of_range&) + { + // create better exception explanation + JSON_THROW(out_of_range::create(403, "key '" + key + "' not found", *this)); + } + } + else + { + JSON_THROW(type_error::create(304, "cannot use at() with " + std::string(type_name()), *this)); + } + } + + /*! + @brief access specified array element + + Returns a reference to the element at specified location @a idx. + + @note If @a idx is beyond the range of the array (i.e., `idx >= size()`), + then the array is silently filled up with `null` values to make `idx` a + valid reference to the last stored element. + + @param[in] idx index of the element to access + + @return reference to the element at index @a idx + + @throw type_error.305 if the JSON value is not an array or null; in that + cases, using the [] operator with an index makes no sense. + + @complexity Constant if @a idx is in the range of the array. Otherwise + linear in `idx - size()`. + + @liveexample{The example below shows how array elements can be read and + written using `[]` operator. Note the addition of `null` + values.,operatorarray__size_type} + + @since version 1.0.0 + */ + reference operator[](size_type idx) + { + // implicitly convert null value to an empty array + if (is_null()) + { + m_type = value_t::array; + m_value.array = create(); + assert_invariant(); + } + + // operator[] only works for arrays + if (JSON_HEDLEY_LIKELY(is_array())) + { + // fill up array with null values if given idx is outside range + if (idx >= m_value.array->size()) + { +#if JSON_DIAGNOSTICS + // remember array size & capacity before resizing + const auto old_size = m_value.array->size(); + const auto old_capacity = m_value.array->capacity(); +#endif + m_value.array->resize(idx + 1); + +#if JSON_DIAGNOSTICS + if (JSON_HEDLEY_UNLIKELY(m_value.array->capacity() != old_capacity)) + { + // capacity has changed: update all parents + set_parents(); + } + else + { + // set parent for values added above + set_parents(begin() + static_cast(old_size), static_cast(idx + 1 - old_size)); + } +#endif + assert_invariant(); + } + + return m_value.array->operator[](idx); + } + + JSON_THROW(type_error::create(305, "cannot use operator[] with a numeric argument with " + std::string(type_name()), *this)); + } + + /*! + @brief access specified array element + + Returns a const reference to the element at specified location @a idx. + + @param[in] idx index of the element to access + + @return const reference to the element at index @a idx + + @throw type_error.305 if the JSON value is not an array; in that case, + using the [] operator with an index makes no sense. + + @complexity Constant. + + @liveexample{The example below shows how array elements can be read using + the `[]` operator.,operatorarray__size_type_const} + + @since version 1.0.0 + */ + const_reference operator[](size_type idx) const + { + // const operator[] only works for arrays + if (JSON_HEDLEY_LIKELY(is_array())) + { + return m_value.array->operator[](idx); + } + + JSON_THROW(type_error::create(305, "cannot use operator[] with a numeric argument with " + std::string(type_name()), *this)); + } + + /*! + @brief access specified object element + + Returns a reference to the element at with specified key @a key. + + @note If @a key is not found in the object, then it is silently added to + the object and filled with a `null` value to make `key` a valid reference. + In case the value was `null` before, it is converted to an object. + + @param[in] key key of the element to access + + @return reference to the element at key @a key + + @throw type_error.305 if the JSON value is not an object or null; in that + cases, using the [] operator with a key makes no sense. + + @complexity Logarithmic in the size of the container. + + @liveexample{The example below shows how object elements can be read and + written using the `[]` operator.,operatorarray__key_type} + + @sa see @ref at(const typename object_t::key_type&) for access by reference + with range checking + @sa see @ref value() for access by value with a default value + + @since version 1.0.0 + */ + reference operator[](const typename object_t::key_type& key) + { + // implicitly convert null value to an empty object + if (is_null()) + { + m_type = value_t::object; + m_value.object = create(); + assert_invariant(); + } + + // operator[] only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + return set_parent(m_value.object->operator[](key)); + } + + JSON_THROW(type_error::create(305, "cannot use operator[] with a string argument with " + std::string(type_name()), *this)); + } + + /*! + @brief read-only access specified object element + + Returns a const reference to the element at with specified key @a key. No + bounds checking is performed. + + @warning If the element with key @a key does not exist, the behavior is + undefined. + + @param[in] key key of the element to access + + @return const reference to the element at key @a key + + @pre The element with key @a key must exist. **This precondition is + enforced with an assertion.** + + @throw type_error.305 if the JSON value is not an object; in that case, + using the [] operator with a key makes no sense. + + @complexity Logarithmic in the size of the container. + + @liveexample{The example below shows how object elements can be read using + the `[]` operator.,operatorarray__key_type_const} + + @sa see @ref at(const typename object_t::key_type&) for access by reference + with range checking + @sa see @ref value() for access by value with a default value + + @since version 1.0.0 + */ + const_reference operator[](const typename object_t::key_type& key) const + { + // const operator[] only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + JSON_ASSERT(m_value.object->find(key) != m_value.object->end()); + return m_value.object->find(key)->second; + } + + JSON_THROW(type_error::create(305, "cannot use operator[] with a string argument with " + std::string(type_name()), *this)); + } + + /*! + @brief access specified object element + + Returns a reference to the element at with specified key @a key. + + @note If @a key is not found in the object, then it is silently added to + the object and filled with a `null` value to make `key` a valid reference. + In case the value was `null` before, it is converted to an object. + + @param[in] key key of the element to access + + @return reference to the element at key @a key + + @throw type_error.305 if the JSON value is not an object or null; in that + cases, using the [] operator with a key makes no sense. + + @complexity Logarithmic in the size of the container. + + @liveexample{The example below shows how object elements can be read and + written using the `[]` operator.,operatorarray__key_type} + + @sa see @ref at(const typename object_t::key_type&) for access by reference + with range checking + @sa see @ref value() for access by value with a default value + + @since version 1.1.0 + */ + template + JSON_HEDLEY_NON_NULL(2) + reference operator[](T* key) + { + // implicitly convert null to object + if (is_null()) + { + m_type = value_t::object; + m_value = value_t::object; + assert_invariant(); + } + + // at only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + return set_parent(m_value.object->operator[](key)); + } + + JSON_THROW(type_error::create(305, "cannot use operator[] with a string argument with " + std::string(type_name()), *this)); + } + + /*! + @brief read-only access specified object element + + Returns a const reference to the element at with specified key @a key. No + bounds checking is performed. + + @warning If the element with key @a key does not exist, the behavior is + undefined. + + @param[in] key key of the element to access + + @return const reference to the element at key @a key + + @pre The element with key @a key must exist. **This precondition is + enforced with an assertion.** + + @throw type_error.305 if the JSON value is not an object; in that case, + using the [] operator with a key makes no sense. + + @complexity Logarithmic in the size of the container. + + @liveexample{The example below shows how object elements can be read using + the `[]` operator.,operatorarray__key_type_const} + + @sa see @ref at(const typename object_t::key_type&) for access by reference + with range checking + @sa see @ref value() for access by value with a default value + + @since version 1.1.0 + */ + template + JSON_HEDLEY_NON_NULL(2) + const_reference operator[](T* key) const + { + // at only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + JSON_ASSERT(m_value.object->find(key) != m_value.object->end()); + return m_value.object->find(key)->second; + } + + JSON_THROW(type_error::create(305, "cannot use operator[] with a string argument with " + std::string(type_name()), *this)); + } + + /*! + @brief access specified object element with default value + + Returns either a copy of an object's element at the specified key @a key + or a given default value if no element with key @a key exists. + + The function is basically equivalent to executing + @code {.cpp} + try { + return at(key); + } catch(out_of_range) { + return default_value; + } + @endcode + + @note Unlike @ref at(const typename object_t::key_type&), this function + does not throw if the given key @a key was not found. + + @note Unlike @ref operator[](const typename object_t::key_type& key), this + function does not implicitly add an element to the position defined by @a + key. This function is furthermore also applicable to const objects. + + @param[in] key key of the element to access + @param[in] default_value the value to return if @a key is not found + + @tparam ValueType type compatible to JSON values, for instance `int` for + JSON integer numbers, `bool` for JSON booleans, or `std::vector` types for + JSON arrays. Note the type of the expected value at @a key and the default + value @a default_value must be compatible. + + @return copy of the element at key @a key or @a default_value if @a key + is not found + + @throw type_error.302 if @a default_value does not match the type of the + value at @a key + @throw type_error.306 if the JSON value is not an object; in that case, + using `value()` with a key makes no sense. + + @complexity Logarithmic in the size of the container. + + @liveexample{The example below shows how object elements can be queried + with a default value.,basic_json__value} + + @sa see @ref at(const typename object_t::key_type&) for access by reference + with range checking + @sa see @ref operator[](const typename object_t::key_type&) for unchecked + access by reference + + @since version 1.0.0 + */ + // using std::is_convertible in a std::enable_if will fail when using explicit conversions + template < class ValueType, typename std::enable_if < + detail::is_getable::value + && !std::is_same::value, int >::type = 0 > + ValueType value(const typename object_t::key_type& key, const ValueType& default_value) const + { + // at only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + // if key is found, return value and given default value otherwise + const auto it = find(key); + if (it != end()) + { + return it->template get(); + } + + return default_value; + } + + JSON_THROW(type_error::create(306, "cannot use value() with " + std::string(type_name()), *this)); + } + + /*! + @brief overload for a default value of type const char* + @copydoc basic_json::value(const typename object_t::key_type&, const ValueType&) const + */ + string_t value(const typename object_t::key_type& key, const char* default_value) const + { + return value(key, string_t(default_value)); + } + + /*! + @brief access specified object element via JSON Pointer with default value + + Returns either a copy of an object's element at the specified key @a key + or a given default value if no element with key @a key exists. + + The function is basically equivalent to executing + @code {.cpp} + try { + return at(ptr); + } catch(out_of_range) { + return default_value; + } + @endcode + + @note Unlike @ref at(const json_pointer&), this function does not throw + if the given key @a key was not found. + + @param[in] ptr a JSON pointer to the element to access + @param[in] default_value the value to return if @a ptr found no value + + @tparam ValueType type compatible to JSON values, for instance `int` for + JSON integer numbers, `bool` for JSON booleans, or `std::vector` types for + JSON arrays. Note the type of the expected value at @a key and the default + value @a default_value must be compatible. + + @return copy of the element at key @a key or @a default_value if @a key + is not found + + @throw type_error.302 if @a default_value does not match the type of the + value at @a ptr + @throw type_error.306 if the JSON value is not an object; in that case, + using `value()` with a key makes no sense. + + @complexity Logarithmic in the size of the container. + + @liveexample{The example below shows how object elements can be queried + with a default value.,basic_json__value_ptr} + + @sa see @ref operator[](const json_pointer&) for unchecked access by reference + + @since version 2.0.2 + */ + template::value, int>::type = 0> + ValueType value(const json_pointer& ptr, const ValueType& default_value) const + { + // at only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + // if pointer resolves a value, return it or use default value + JSON_TRY + { + return ptr.get_checked(this).template get(); + } + JSON_INTERNAL_CATCH (out_of_range&) + { + return default_value; + } + } + + JSON_THROW(type_error::create(306, "cannot use value() with " + std::string(type_name()), *this)); + } + + /*! + @brief overload for a default value of type const char* + @copydoc basic_json::value(const json_pointer&, ValueType) const + */ + JSON_HEDLEY_NON_NULL(3) + string_t value(const json_pointer& ptr, const char* default_value) const + { + return value(ptr, string_t(default_value)); + } + + /*! + @brief access the first element + + Returns a reference to the first element in the container. For a JSON + container `c`, the expression `c.front()` is equivalent to `*c.begin()`. + + @return In case of a structured type (array or object), a reference to the + first element is returned. In case of number, string, boolean, or binary + values, a reference to the value is returned. + + @complexity Constant. + + @pre The JSON value must not be `null` (would throw `std::out_of_range`) + or an empty array or object (undefined behavior, **guarded by + assertions**). + @post The JSON value remains unchanged. + + @throw invalid_iterator.214 when called on `null` value + + @liveexample{The following code shows an example for `front()`.,front} + + @sa see @ref back() -- access the last element + + @since version 1.0.0 + */ + reference front() + { + return *begin(); + } + + /*! + @copydoc basic_json::front() + */ + const_reference front() const + { + return *cbegin(); + } + + /*! + @brief access the last element + + Returns a reference to the last element in the container. For a JSON + container `c`, the expression `c.back()` is equivalent to + @code {.cpp} + auto tmp = c.end(); + --tmp; + return *tmp; + @endcode + + @return In case of a structured type (array or object), a reference to the + last element is returned. In case of number, string, boolean, or binary + values, a reference to the value is returned. + + @complexity Constant. + + @pre The JSON value must not be `null` (would throw `std::out_of_range`) + or an empty array or object (undefined behavior, **guarded by + assertions**). + @post The JSON value remains unchanged. + + @throw invalid_iterator.214 when called on a `null` value. See example + below. + + @liveexample{The following code shows an example for `back()`.,back} + + @sa see @ref front() -- access the first element + + @since version 1.0.0 + */ + reference back() + { + auto tmp = end(); + --tmp; + return *tmp; + } + + /*! + @copydoc basic_json::back() + */ + const_reference back() const + { + auto tmp = cend(); + --tmp; + return *tmp; + } + + /*! + @brief remove element given an iterator + + Removes the element specified by iterator @a pos. The iterator @a pos must + be valid and dereferenceable. Thus the `end()` iterator (which is valid, + but is not dereferenceable) cannot be used as a value for @a pos. + + If called on a primitive type other than `null`, the resulting JSON value + will be `null`. + + @param[in] pos iterator to the element to remove + @return Iterator following the last removed element. If the iterator @a + pos refers to the last element, the `end()` iterator is returned. + + @tparam IteratorType an @ref iterator or @ref const_iterator + + @post Invalidates iterators and references at or after the point of the + erase, including the `end()` iterator. + + @throw type_error.307 if called on a `null` value; example: `"cannot use + erase() with null"` + @throw invalid_iterator.202 if called on an iterator which does not belong + to the current JSON value; example: `"iterator does not fit current + value"` + @throw invalid_iterator.205 if called on a primitive type with invalid + iterator (i.e., any iterator which is not `begin()`); example: `"iterator + out of range"` + + @complexity The complexity depends on the type: + - objects: amortized constant + - arrays: linear in distance between @a pos and the end of the container + - strings and binary: linear in the length of the member + - other types: constant + + @liveexample{The example shows the result of `erase()` for different JSON + types.,erase__IteratorType} + + @sa see @ref erase(IteratorType, IteratorType) -- removes the elements in + the given range + @sa see @ref erase(const typename object_t::key_type&) -- removes the element + from an object at the given key + @sa see @ref erase(const size_type) -- removes the element from an array at + the given index + + @since version 1.0.0 + */ + template < class IteratorType, typename std::enable_if < + std::is_same::value || + std::is_same::value, int >::type + = 0 > + IteratorType erase(IteratorType pos) + { + // make sure iterator fits the current value + if (JSON_HEDLEY_UNLIKELY(this != pos.m_object)) + { + JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", *this)); + } + + IteratorType result = end(); + + switch (m_type) + { + case value_t::boolean: + case value_t::number_float: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::string: + case value_t::binary: + { + if (JSON_HEDLEY_UNLIKELY(!pos.m_it.primitive_iterator.is_begin())) + { + JSON_THROW(invalid_iterator::create(205, "iterator out of range", *this)); + } + + if (is_string()) + { + AllocatorType alloc; + std::allocator_traits::destroy(alloc, m_value.string); + std::allocator_traits::deallocate(alloc, m_value.string, 1); + m_value.string = nullptr; + } + else if (is_binary()) + { + AllocatorType alloc; + std::allocator_traits::destroy(alloc, m_value.binary); + std::allocator_traits::deallocate(alloc, m_value.binary, 1); + m_value.binary = nullptr; + } + + m_type = value_t::null; + assert_invariant(); + break; + } + + case value_t::object: + { + result.m_it.object_iterator = m_value.object->erase(pos.m_it.object_iterator); + break; + } + + case value_t::array: + { + result.m_it.array_iterator = m_value.array->erase(pos.m_it.array_iterator); + break; + } + + case value_t::null: + case value_t::discarded: + default: + JSON_THROW(type_error::create(307, "cannot use erase() with " + std::string(type_name()), *this)); + } + + return result; + } + + /*! + @brief remove elements given an iterator range + + Removes the element specified by the range `[first; last)`. The iterator + @a first does not need to be dereferenceable if `first == last`: erasing + an empty range is a no-op. + + If called on a primitive type other than `null`, the resulting JSON value + will be `null`. + + @param[in] first iterator to the beginning of the range to remove + @param[in] last iterator past the end of the range to remove + @return Iterator following the last removed element. If the iterator @a + second refers to the last element, the `end()` iterator is returned. + + @tparam IteratorType an @ref iterator or @ref const_iterator + + @post Invalidates iterators and references at or after the point of the + erase, including the `end()` iterator. + + @throw type_error.307 if called on a `null` value; example: `"cannot use + erase() with null"` + @throw invalid_iterator.203 if called on iterators which does not belong + to the current JSON value; example: `"iterators do not fit current value"` + @throw invalid_iterator.204 if called on a primitive type with invalid + iterators (i.e., if `first != begin()` and `last != end()`); example: + `"iterators out of range"` + + @complexity The complexity depends on the type: + - objects: `log(size()) + std::distance(first, last)` + - arrays: linear in the distance between @a first and @a last, plus linear + in the distance between @a last and end of the container + - strings and binary: linear in the length of the member + - other types: constant + + @liveexample{The example shows the result of `erase()` for different JSON + types.,erase__IteratorType_IteratorType} + + @sa see @ref erase(IteratorType) -- removes the element at a given position + @sa see @ref erase(const typename object_t::key_type&) -- removes the element + from an object at the given key + @sa see @ref erase(const size_type) -- removes the element from an array at + the given index + + @since version 1.0.0 + */ + template < class IteratorType, typename std::enable_if < + std::is_same::value || + std::is_same::value, int >::type + = 0 > + IteratorType erase(IteratorType first, IteratorType last) + { + // make sure iterator fits the current value + if (JSON_HEDLEY_UNLIKELY(this != first.m_object || this != last.m_object)) + { + JSON_THROW(invalid_iterator::create(203, "iterators do not fit current value", *this)); + } + + IteratorType result = end(); + + switch (m_type) + { + case value_t::boolean: + case value_t::number_float: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::string: + case value_t::binary: + { + if (JSON_HEDLEY_LIKELY(!first.m_it.primitive_iterator.is_begin() + || !last.m_it.primitive_iterator.is_end())) + { + JSON_THROW(invalid_iterator::create(204, "iterators out of range", *this)); + } + + if (is_string()) + { + AllocatorType alloc; + std::allocator_traits::destroy(alloc, m_value.string); + std::allocator_traits::deallocate(alloc, m_value.string, 1); + m_value.string = nullptr; + } + else if (is_binary()) + { + AllocatorType alloc; + std::allocator_traits::destroy(alloc, m_value.binary); + std::allocator_traits::deallocate(alloc, m_value.binary, 1); + m_value.binary = nullptr; + } + + m_type = value_t::null; + assert_invariant(); + break; + } + + case value_t::object: + { + result.m_it.object_iterator = m_value.object->erase(first.m_it.object_iterator, + last.m_it.object_iterator); + break; + } + + case value_t::array: + { + result.m_it.array_iterator = m_value.array->erase(first.m_it.array_iterator, + last.m_it.array_iterator); + break; + } + + case value_t::null: + case value_t::discarded: + default: + JSON_THROW(type_error::create(307, "cannot use erase() with " + std::string(type_name()), *this)); + } + + return result; + } + + /*! + @brief remove element from a JSON object given a key + + Removes elements from a JSON object with the key value @a key. + + @param[in] key value of the elements to remove + + @return Number of elements removed. If @a ObjectType is the default + `std::map` type, the return value will always be `0` (@a key was not + found) or `1` (@a key was found). + + @post References and iterators to the erased elements are invalidated. + Other references and iterators are not affected. + + @throw type_error.307 when called on a type other than JSON object; + example: `"cannot use erase() with null"` + + @complexity `log(size()) + count(key)` + + @liveexample{The example shows the effect of `erase()`.,erase__key_type} + + @sa see @ref erase(IteratorType) -- removes the element at a given position + @sa see @ref erase(IteratorType, IteratorType) -- removes the elements in + the given range + @sa see @ref erase(const size_type) -- removes the element from an array at + the given index + + @since version 1.0.0 + */ + size_type erase(const typename object_t::key_type& key) + { + // this erase only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + return m_value.object->erase(key); + } + + JSON_THROW(type_error::create(307, "cannot use erase() with " + std::string(type_name()), *this)); + } + + /*! + @brief remove element from a JSON array given an index + + Removes element from a JSON array at the index @a idx. + + @param[in] idx index of the element to remove + + @throw type_error.307 when called on a type other than JSON object; + example: `"cannot use erase() with null"` + @throw out_of_range.401 when `idx >= size()`; example: `"array index 17 + is out of range"` + + @complexity Linear in distance between @a idx and the end of the container. + + @liveexample{The example shows the effect of `erase()`.,erase__size_type} + + @sa see @ref erase(IteratorType) -- removes the element at a given position + @sa see @ref erase(IteratorType, IteratorType) -- removes the elements in + the given range + @sa see @ref erase(const typename object_t::key_type&) -- removes the element + from an object at the given key + + @since version 1.0.0 + */ + void erase(const size_type idx) + { + // this erase only works for arrays + if (JSON_HEDLEY_LIKELY(is_array())) + { + if (JSON_HEDLEY_UNLIKELY(idx >= size())) + { + JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range", *this)); + } + + m_value.array->erase(m_value.array->begin() + static_cast(idx)); + } + else + { + JSON_THROW(type_error::create(307, "cannot use erase() with " + std::string(type_name()), *this)); + } + } + + /// @} + + + //////////// + // lookup // + //////////// + + /// @name lookup + /// @{ + + /*! + @brief find an element in a JSON object + + Finds an element in a JSON object with key equivalent to @a key. If the + element is not found or the JSON value is not an object, end() is + returned. + + @note This method always returns @ref end() when executed on a JSON type + that is not an object. + + @param[in] key key value of the element to search for. + + @return Iterator to an element with key equivalent to @a key. If no such + element is found or the JSON value is not an object, past-the-end (see + @ref end()) iterator is returned. + + @complexity Logarithmic in the size of the JSON object. + + @liveexample{The example shows how `find()` is used.,find__key_type} + + @sa see @ref contains(KeyT&&) const -- checks whether a key exists + + @since version 1.0.0 + */ + template + iterator find(KeyT&& key) + { + auto result = end(); + + if (is_object()) + { + result.m_it.object_iterator = m_value.object->find(std::forward(key)); + } + + return result; + } + + /*! + @brief find an element in a JSON object + @copydoc find(KeyT&&) + */ + template + const_iterator find(KeyT&& key) const + { + auto result = cend(); + + if (is_object()) + { + result.m_it.object_iterator = m_value.object->find(std::forward(key)); + } + + return result; + } + + /*! + @brief returns the number of occurrences of a key in a JSON object + + Returns the number of elements with key @a key. If ObjectType is the + default `std::map` type, the return value will always be `0` (@a key was + not found) or `1` (@a key was found). + + @note This method always returns `0` when executed on a JSON type that is + not an object. + + @param[in] key key value of the element to count + + @return Number of elements with key @a key. If the JSON value is not an + object, the return value will be `0`. + + @complexity Logarithmic in the size of the JSON object. + + @liveexample{The example shows how `count()` is used.,count} + + @since version 1.0.0 + */ + template + size_type count(KeyT&& key) const + { + // return 0 for all nonobject types + return is_object() ? m_value.object->count(std::forward(key)) : 0; + } + + /*! + @brief check the existence of an element in a JSON object + + Check whether an element exists in a JSON object with key equivalent to + @a key. If the element is not found or the JSON value is not an object, + false is returned. + + @note This method always returns false when executed on a JSON type + that is not an object. + + @param[in] key key value to check its existence. + + @return true if an element with specified @a key exists. If no such + element with such key is found or the JSON value is not an object, + false is returned. + + @complexity Logarithmic in the size of the JSON object. + + @liveexample{The following code shows an example for `contains()`.,contains} + + @sa see @ref find(KeyT&&) -- returns an iterator to an object element + @sa see @ref contains(const json_pointer&) const -- checks the existence for a JSON pointer + + @since version 3.6.0 + */ + template < typename KeyT, typename std::enable_if < + !std::is_same::type, json_pointer>::value, int >::type = 0 > + bool contains(KeyT && key) const + { + return is_object() && m_value.object->find(std::forward(key)) != m_value.object->end(); + } + + /*! + @brief check the existence of an element in a JSON object given a JSON pointer + + Check whether the given JSON pointer @a ptr can be resolved in the current + JSON value. + + @note This method can be executed on any JSON value type. + + @param[in] ptr JSON pointer to check its existence. + + @return true if the JSON pointer can be resolved to a stored value, false + otherwise. + + @post If `j.contains(ptr)` returns true, it is safe to call `j[ptr]`. + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + + @complexity Logarithmic in the size of the JSON object. + + @liveexample{The following code shows an example for `contains()`.,contains_json_pointer} + + @sa see @ref contains(KeyT &&) const -- checks the existence of a key + + @since version 3.7.0 + */ + bool contains(const json_pointer& ptr) const + { + return ptr.contains(this); + } + + /// @} + + + /////////////// + // iterators // + /////////////// + + /// @name iterators + /// @{ + + /*! + @brief returns an iterator to the first element + + Returns an iterator to the first element. + + @image html range-begin-end.svg "Illustration from cppreference.com" + + @return iterator to the first element + + @complexity Constant. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is constant. + + @liveexample{The following code shows an example for `begin()`.,begin} + + @sa see @ref cbegin() -- returns a const iterator to the beginning + @sa see @ref end() -- returns an iterator to the end + @sa see @ref cend() -- returns a const iterator to the end + + @since version 1.0.0 + */ + iterator begin() noexcept + { + iterator result(this); + result.set_begin(); + return result; + } + + /*! + @copydoc basic_json::cbegin() + */ + const_iterator begin() const noexcept + { + return cbegin(); + } + + /*! + @brief returns a const iterator to the first element + + Returns a const iterator to the first element. + + @image html range-begin-end.svg "Illustration from cppreference.com" + + @return const iterator to the first element + + @complexity Constant. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is constant. + - Has the semantics of `const_cast(*this).begin()`. + + @liveexample{The following code shows an example for `cbegin()`.,cbegin} + + @sa see @ref begin() -- returns an iterator to the beginning + @sa see @ref end() -- returns an iterator to the end + @sa see @ref cend() -- returns a const iterator to the end + + @since version 1.0.0 + */ + const_iterator cbegin() const noexcept + { + const_iterator result(this); + result.set_begin(); + return result; + } + + /*! + @brief returns an iterator to one past the last element + + Returns an iterator to one past the last element. + + @image html range-begin-end.svg "Illustration from cppreference.com" + + @return iterator one past the last element + + @complexity Constant. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is constant. + + @liveexample{The following code shows an example for `end()`.,end} + + @sa see @ref cend() -- returns a const iterator to the end + @sa see @ref begin() -- returns an iterator to the beginning + @sa see @ref cbegin() -- returns a const iterator to the beginning + + @since version 1.0.0 + */ + iterator end() noexcept + { + iterator result(this); + result.set_end(); + return result; + } + + /*! + @copydoc basic_json::cend() + */ + const_iterator end() const noexcept + { + return cend(); + } + + /*! + @brief returns a const iterator to one past the last element + + Returns a const iterator to one past the last element. + + @image html range-begin-end.svg "Illustration from cppreference.com" + + @return const iterator one past the last element + + @complexity Constant. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is constant. + - Has the semantics of `const_cast(*this).end()`. + + @liveexample{The following code shows an example for `cend()`.,cend} + + @sa see @ref end() -- returns an iterator to the end + @sa see @ref begin() -- returns an iterator to the beginning + @sa see @ref cbegin() -- returns a const iterator to the beginning + + @since version 1.0.0 + */ + const_iterator cend() const noexcept + { + const_iterator result(this); + result.set_end(); + return result; + } + + /*! + @brief returns an iterator to the reverse-beginning + + Returns an iterator to the reverse-beginning; that is, the last element. + + @image html range-rbegin-rend.svg "Illustration from cppreference.com" + + @complexity Constant. + + @requirement This function helps `basic_json` satisfying the + [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer) + requirements: + - The complexity is constant. + - Has the semantics of `reverse_iterator(end())`. + + @liveexample{The following code shows an example for `rbegin()`.,rbegin} + + @sa see @ref crbegin() -- returns a const reverse iterator to the beginning + @sa see @ref rend() -- returns a reverse iterator to the end + @sa see @ref crend() -- returns a const reverse iterator to the end + + @since version 1.0.0 + */ + reverse_iterator rbegin() noexcept + { + return reverse_iterator(end()); + } + + /*! + @copydoc basic_json::crbegin() + */ + const_reverse_iterator rbegin() const noexcept + { + return crbegin(); + } + + /*! + @brief returns an iterator to the reverse-end + + Returns an iterator to the reverse-end; that is, one before the first + element. + + @image html range-rbegin-rend.svg "Illustration from cppreference.com" + + @complexity Constant. + + @requirement This function helps `basic_json` satisfying the + [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer) + requirements: + - The complexity is constant. + - Has the semantics of `reverse_iterator(begin())`. + + @liveexample{The following code shows an example for `rend()`.,rend} + + @sa see @ref crend() -- returns a const reverse iterator to the end + @sa see @ref rbegin() -- returns a reverse iterator to the beginning + @sa see @ref crbegin() -- returns a const reverse iterator to the beginning + + @since version 1.0.0 + */ + reverse_iterator rend() noexcept + { + return reverse_iterator(begin()); + } + + /*! + @copydoc basic_json::crend() + */ + const_reverse_iterator rend() const noexcept + { + return crend(); + } + + /*! + @brief returns a const reverse iterator to the last element + + Returns a const iterator to the reverse-beginning; that is, the last + element. + + @image html range-rbegin-rend.svg "Illustration from cppreference.com" + + @complexity Constant. + + @requirement This function helps `basic_json` satisfying the + [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer) + requirements: + - The complexity is constant. + - Has the semantics of `const_cast(*this).rbegin()`. + + @liveexample{The following code shows an example for `crbegin()`.,crbegin} + + @sa see @ref rbegin() -- returns a reverse iterator to the beginning + @sa see @ref rend() -- returns a reverse iterator to the end + @sa see @ref crend() -- returns a const reverse iterator to the end + + @since version 1.0.0 + */ + const_reverse_iterator crbegin() const noexcept + { + return const_reverse_iterator(cend()); + } + + /*! + @brief returns a const reverse iterator to one before the first + + Returns a const reverse iterator to the reverse-end; that is, one before + the first element. + + @image html range-rbegin-rend.svg "Illustration from cppreference.com" + + @complexity Constant. + + @requirement This function helps `basic_json` satisfying the + [ReversibleContainer](https://en.cppreference.com/w/cpp/named_req/ReversibleContainer) + requirements: + - The complexity is constant. + - Has the semantics of `const_cast(*this).rend()`. + + @liveexample{The following code shows an example for `crend()`.,crend} + + @sa see @ref rend() -- returns a reverse iterator to the end + @sa see @ref rbegin() -- returns a reverse iterator to the beginning + @sa see @ref crbegin() -- returns a const reverse iterator to the beginning + + @since version 1.0.0 + */ + const_reverse_iterator crend() const noexcept + { + return const_reverse_iterator(cbegin()); + } + + public: + /*! + @brief wrapper to access iterator member functions in range-based for + + This function allows to access @ref iterator::key() and @ref + iterator::value() during range-based for loops. In these loops, a + reference to the JSON values is returned, so there is no access to the + underlying iterator. + + For loop without iterator_wrapper: + + @code{cpp} + for (auto it = j_object.begin(); it != j_object.end(); ++it) + { + std::cout << "key: " << it.key() << ", value:" << it.value() << '\n'; + } + @endcode + + Range-based for loop without iterator proxy: + + @code{cpp} + for (auto it : j_object) + { + // "it" is of type json::reference and has no key() member + std::cout << "value: " << it << '\n'; + } + @endcode + + Range-based for loop with iterator proxy: + + @code{cpp} + for (auto it : json::iterator_wrapper(j_object)) + { + std::cout << "key: " << it.key() << ", value:" << it.value() << '\n'; + } + @endcode + + @note When iterating over an array, `key()` will return the index of the + element as string (see example). + + @param[in] ref reference to a JSON value + @return iteration proxy object wrapping @a ref with an interface to use in + range-based for loops + + @liveexample{The following code shows how the wrapper is used,iterator_wrapper} + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @complexity Constant. + + @note The name of this function is not yet final and may change in the + future. + + @deprecated This stream operator is deprecated and will be removed in + future 4.0.0 of the library. Please use @ref items() instead; + that is, replace `json::iterator_wrapper(j)` with `j.items()`. + */ + JSON_HEDLEY_DEPRECATED_FOR(3.1.0, items()) + static iteration_proxy iterator_wrapper(reference ref) noexcept + { + return ref.items(); + } + + /*! + @copydoc iterator_wrapper(reference) + */ + JSON_HEDLEY_DEPRECATED_FOR(3.1.0, items()) + static iteration_proxy iterator_wrapper(const_reference ref) noexcept + { + return ref.items(); + } + + /*! + @brief helper to access iterator member functions in range-based for + + This function allows to access @ref iterator::key() and @ref + iterator::value() during range-based for loops. In these loops, a + reference to the JSON values is returned, so there is no access to the + underlying iterator. + + For loop without `items()` function: + + @code{cpp} + for (auto it = j_object.begin(); it != j_object.end(); ++it) + { + std::cout << "key: " << it.key() << ", value:" << it.value() << '\n'; + } + @endcode + + Range-based for loop without `items()` function: + + @code{cpp} + for (auto it : j_object) + { + // "it" is of type json::reference and has no key() member + std::cout << "value: " << it << '\n'; + } + @endcode + + Range-based for loop with `items()` function: + + @code{cpp} + for (auto& el : j_object.items()) + { + std::cout << "key: " << el.key() << ", value:" << el.value() << '\n'; + } + @endcode + + The `items()` function also allows to use + [structured bindings](https://en.cppreference.com/w/cpp/language/structured_binding) + (C++17): + + @code{cpp} + for (auto& [key, val] : j_object.items()) + { + std::cout << "key: " << key << ", value:" << val << '\n'; + } + @endcode + + @note When iterating over an array, `key()` will return the index of the + element as string (see example). For primitive types (e.g., numbers), + `key()` returns an empty string. + + @warning Using `items()` on temporary objects is dangerous. Make sure the + object's lifetime exeeds the iteration. See + for more + information. + + @return iteration proxy object wrapping @a ref with an interface to use in + range-based for loops + + @liveexample{The following code shows how the function is used.,items} + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @complexity Constant. + + @since version 3.1.0, structured bindings support since 3.5.0. + */ + iteration_proxy items() noexcept + { + return iteration_proxy(*this); + } + + /*! + @copydoc items() + */ + iteration_proxy items() const noexcept + { + return iteration_proxy(*this); + } + + /// @} + + + ////////////// + // capacity // + ////////////// + + /// @name capacity + /// @{ + + /*! + @brief checks whether the container is empty. + + Checks if a JSON value has no elements (i.e. whether its @ref size is `0`). + + @return The return value depends on the different types and is + defined as follows: + Value type | return value + ----------- | ------------- + null | `true` + boolean | `false` + string | `false` + number | `false` + binary | `false` + object | result of function `object_t::empty()` + array | result of function `array_t::empty()` + + @liveexample{The following code uses `empty()` to check if a JSON + object contains any elements.,empty} + + @complexity Constant, as long as @ref array_t and @ref object_t satisfy + the Container concept; that is, their `empty()` functions have constant + complexity. + + @iterators No changes. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @note This function does not return whether a string stored as JSON value + is empty - it returns whether the JSON container itself is empty which is + false in the case of a string. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is constant. + - Has the semantics of `begin() == end()`. + + @sa see @ref size() -- returns the number of elements + + @since version 1.0.0 + */ + bool empty() const noexcept + { + switch (m_type) + { + case value_t::null: + { + // null values are empty + return true; + } + + case value_t::array: + { + // delegate call to array_t::empty() + return m_value.array->empty(); + } + + case value_t::object: + { + // delegate call to object_t::empty() + return m_value.object->empty(); + } + + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + // all other types are nonempty + return false; + } + } + } + + /*! + @brief returns the number of elements + + Returns the number of elements in a JSON value. + + @return The return value depends on the different types and is + defined as follows: + Value type | return value + ----------- | ------------- + null | `0` + boolean | `1` + string | `1` + number | `1` + binary | `1` + object | result of function object_t::size() + array | result of function array_t::size() + + @liveexample{The following code calls `size()` on the different value + types.,size} + + @complexity Constant, as long as @ref array_t and @ref object_t satisfy + the Container concept; that is, their size() functions have constant + complexity. + + @iterators No changes. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @note This function does not return the length of a string stored as JSON + value - it returns the number of elements in the JSON value which is 1 in + the case of a string. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is constant. + - Has the semantics of `std::distance(begin(), end())`. + + @sa see @ref empty() -- checks whether the container is empty + @sa see @ref max_size() -- returns the maximal number of elements + + @since version 1.0.0 + */ + size_type size() const noexcept + { + switch (m_type) + { + case value_t::null: + { + // null values are empty + return 0; + } + + case value_t::array: + { + // delegate call to array_t::size() + return m_value.array->size(); + } + + case value_t::object: + { + // delegate call to object_t::size() + return m_value.object->size(); + } + + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + // all other types have size 1 + return 1; + } + } + } + + /*! + @brief returns the maximum possible number of elements + + Returns the maximum number of elements a JSON value is able to hold due to + system or library implementation limitations, i.e. `std::distance(begin(), + end())` for the JSON value. + + @return The return value depends on the different types and is + defined as follows: + Value type | return value + ----------- | ------------- + null | `0` (same as `size()`) + boolean | `1` (same as `size()`) + string | `1` (same as `size()`) + number | `1` (same as `size()`) + binary | `1` (same as `size()`) + object | result of function `object_t::max_size()` + array | result of function `array_t::max_size()` + + @liveexample{The following code calls `max_size()` on the different value + types. Note the output is implementation specific.,max_size} + + @complexity Constant, as long as @ref array_t and @ref object_t satisfy + the Container concept; that is, their `max_size()` functions have constant + complexity. + + @iterators No changes. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @requirement This function helps `basic_json` satisfying the + [Container](https://en.cppreference.com/w/cpp/named_req/Container) + requirements: + - The complexity is constant. + - Has the semantics of returning `b.size()` where `b` is the largest + possible JSON value. + + @sa see @ref size() -- returns the number of elements + + @since version 1.0.0 + */ + size_type max_size() const noexcept + { + switch (m_type) + { + case value_t::array: + { + // delegate call to array_t::max_size() + return m_value.array->max_size(); + } + + case value_t::object: + { + // delegate call to object_t::max_size() + return m_value.object->max_size(); + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + // all other types have max_size() == size() + return size(); + } + } + } + + /// @} + + + /////////////// + // modifiers // + /////////////// + + /// @name modifiers + /// @{ + + /*! + @brief clears the contents + + Clears the content of a JSON value and resets it to the default value as + if @ref basic_json(value_t) would have been called with the current value + type from @ref type(): + + Value type | initial value + ----------- | ------------- + null | `null` + boolean | `false` + string | `""` + number | `0` + binary | An empty byte vector + object | `{}` + array | `[]` + + @post Has the same effect as calling + @code {.cpp} + *this = basic_json(type()); + @endcode + + @liveexample{The example below shows the effect of `clear()` to different + JSON types.,clear} + + @complexity Linear in the size of the JSON value. + + @iterators All iterators, pointers and references related to this container + are invalidated. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @sa see @ref basic_json(value_t) -- constructor that creates an object with the + same value than calling `clear()` + + @since version 1.0.0 + */ + void clear() noexcept + { + switch (m_type) + { + case value_t::number_integer: + { + m_value.number_integer = 0; + break; + } + + case value_t::number_unsigned: + { + m_value.number_unsigned = 0; + break; + } + + case value_t::number_float: + { + m_value.number_float = 0.0; + break; + } + + case value_t::boolean: + { + m_value.boolean = false; + break; + } + + case value_t::string: + { + m_value.string->clear(); + break; + } + + case value_t::binary: + { + m_value.binary->clear(); + break; + } + + case value_t::array: + { + m_value.array->clear(); + break; + } + + case value_t::object: + { + m_value.object->clear(); + break; + } + + case value_t::null: + case value_t::discarded: + default: + break; + } + } + + /*! + @brief add an object to an array + + Appends the given element @a val to the end of the JSON value. If the + function is called on a JSON null value, an empty array is created before + appending @a val. + + @param[in] val the value to add to the JSON array + + @throw type_error.308 when called on a type other than JSON array or + null; example: `"cannot use push_back() with number"` + + @complexity Amortized constant. + + @liveexample{The example shows how `push_back()` and `+=` can be used to + add elements to a JSON array. Note how the `null` value was silently + converted to a JSON array.,push_back} + + @since version 1.0.0 + */ + void push_back(basic_json&& val) + { + // push_back only works for null objects or arrays + if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) + { + JSON_THROW(type_error::create(308, "cannot use push_back() with " + std::string(type_name()), *this)); + } + + // transform null object into an array + if (is_null()) + { + m_type = value_t::array; + m_value = value_t::array; + assert_invariant(); + } + + // add element to array (move semantics) + const auto old_capacity = m_value.array->capacity(); + m_value.array->push_back(std::move(val)); + set_parent(m_value.array->back(), old_capacity); + // if val is moved from, basic_json move constructor marks it null so we do not call the destructor + } + + /*! + @brief add an object to an array + @copydoc push_back(basic_json&&) + */ + reference operator+=(basic_json&& val) + { + push_back(std::move(val)); + return *this; + } + + /*! + @brief add an object to an array + @copydoc push_back(basic_json&&) + */ + void push_back(const basic_json& val) + { + // push_back only works for null objects or arrays + if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) + { + JSON_THROW(type_error::create(308, "cannot use push_back() with " + std::string(type_name()), *this)); + } + + // transform null object into an array + if (is_null()) + { + m_type = value_t::array; + m_value = value_t::array; + assert_invariant(); + } + + // add element to array + const auto old_capacity = m_value.array->capacity(); + m_value.array->push_back(val); + set_parent(m_value.array->back(), old_capacity); + } + + /*! + @brief add an object to an array + @copydoc push_back(basic_json&&) + */ + reference operator+=(const basic_json& val) + { + push_back(val); + return *this; + } + + /*! + @brief add an object to an object + + Inserts the given element @a val to the JSON object. If the function is + called on a JSON null value, an empty object is created before inserting + @a val. + + @param[in] val the value to add to the JSON object + + @throw type_error.308 when called on a type other than JSON object or + null; example: `"cannot use push_back() with number"` + + @complexity Logarithmic in the size of the container, O(log(`size()`)). + + @liveexample{The example shows how `push_back()` and `+=` can be used to + add elements to a JSON object. Note how the `null` value was silently + converted to a JSON object.,push_back__object_t__value} + + @since version 1.0.0 + */ + void push_back(const typename object_t::value_type& val) + { + // push_back only works for null objects or objects + if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object()))) + { + JSON_THROW(type_error::create(308, "cannot use push_back() with " + std::string(type_name()), *this)); + } + + // transform null object into an object + if (is_null()) + { + m_type = value_t::object; + m_value = value_t::object; + assert_invariant(); + } + + // add element to object + auto res = m_value.object->insert(val); + set_parent(res.first->second); + } + + /*! + @brief add an object to an object + @copydoc push_back(const typename object_t::value_type&) + */ + reference operator+=(const typename object_t::value_type& val) + { + push_back(val); + return *this; + } + + /*! + @brief add an object to an object + + This function allows to use `push_back` with an initializer list. In case + + 1. the current value is an object, + 2. the initializer list @a init contains only two elements, and + 3. the first element of @a init is a string, + + @a init is converted into an object element and added using + @ref push_back(const typename object_t::value_type&). Otherwise, @a init + is converted to a JSON value and added using @ref push_back(basic_json&&). + + @param[in] init an initializer list + + @complexity Linear in the size of the initializer list @a init. + + @note This function is required to resolve an ambiguous overload error, + because pairs like `{"key", "value"}` can be both interpreted as + `object_t::value_type` or `std::initializer_list`, see + https://github.com/nlohmann/json/issues/235 for more information. + + @liveexample{The example shows how initializer lists are treated as + objects when possible.,push_back__initializer_list} + */ + void push_back(initializer_list_t init) + { + if (is_object() && init.size() == 2 && (*init.begin())->is_string()) + { + basic_json&& key = init.begin()->moved_or_copied(); + push_back(typename object_t::value_type( + std::move(key.get_ref()), (init.begin() + 1)->moved_or_copied())); + } + else + { + push_back(basic_json(init)); + } + } + + /*! + @brief add an object to an object + @copydoc push_back(initializer_list_t) + */ + reference operator+=(initializer_list_t init) + { + push_back(init); + return *this; + } + + /*! + @brief add an object to an array + + Creates a JSON value from the passed parameters @a args to the end of the + JSON value. If the function is called on a JSON null value, an empty array + is created before appending the value created from @a args. + + @param[in] args arguments to forward to a constructor of @ref basic_json + @tparam Args compatible types to create a @ref basic_json object + + @return reference to the inserted element + + @throw type_error.311 when called on a type other than JSON array or + null; example: `"cannot use emplace_back() with number"` + + @complexity Amortized constant. + + @liveexample{The example shows how `push_back()` can be used to add + elements to a JSON array. Note how the `null` value was silently converted + to a JSON array.,emplace_back} + + @since version 2.0.8, returns reference since 3.7.0 + */ + template + reference emplace_back(Args&& ... args) + { + // emplace_back only works for null objects or arrays + if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array()))) + { + JSON_THROW(type_error::create(311, "cannot use emplace_back() with " + std::string(type_name()), *this)); + } + + // transform null object into an array + if (is_null()) + { + m_type = value_t::array; + m_value = value_t::array; + assert_invariant(); + } + + // add element to array (perfect forwarding) + const auto old_capacity = m_value.array->capacity(); + m_value.array->emplace_back(std::forward(args)...); + return set_parent(m_value.array->back(), old_capacity); + } + + /*! + @brief add an object to an object if key does not exist + + Inserts a new element into a JSON object constructed in-place with the + given @a args if there is no element with the key in the container. If the + function is called on a JSON null value, an empty object is created before + appending the value created from @a args. + + @param[in] args arguments to forward to a constructor of @ref basic_json + @tparam Args compatible types to create a @ref basic_json object + + @return a pair consisting of an iterator to the inserted element, or the + already-existing element if no insertion happened, and a bool + denoting whether the insertion took place. + + @throw type_error.311 when called on a type other than JSON object or + null; example: `"cannot use emplace() with number"` + + @complexity Logarithmic in the size of the container, O(log(`size()`)). + + @liveexample{The example shows how `emplace()` can be used to add elements + to a JSON object. Note how the `null` value was silently converted to a + JSON object. Further note how no value is added if there was already one + value stored with the same key.,emplace} + + @since version 2.0.8 + */ + template + std::pair emplace(Args&& ... args) + { + // emplace only works for null objects or arrays + if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object()))) + { + JSON_THROW(type_error::create(311, "cannot use emplace() with " + std::string(type_name()), *this)); + } + + // transform null object into an object + if (is_null()) + { + m_type = value_t::object; + m_value = value_t::object; + assert_invariant(); + } + + // add element to array (perfect forwarding) + auto res = m_value.object->emplace(std::forward(args)...); + set_parent(res.first->second); + + // create result iterator and set iterator to the result of emplace + auto it = begin(); + it.m_it.object_iterator = res.first; + + // return pair of iterator and boolean + return {it, res.second}; + } + + /// Helper for insertion of an iterator + /// @note: This uses std::distance to support GCC 4.8, + /// see https://github.com/nlohmann/json/pull/1257 + template + iterator insert_iterator(const_iterator pos, Args&& ... args) + { + iterator result(this); + JSON_ASSERT(m_value.array != nullptr); + + auto insert_pos = std::distance(m_value.array->begin(), pos.m_it.array_iterator); + m_value.array->insert(pos.m_it.array_iterator, std::forward(args)...); + result.m_it.array_iterator = m_value.array->begin() + insert_pos; + + // This could have been written as: + // result.m_it.array_iterator = m_value.array->insert(pos.m_it.array_iterator, cnt, val); + // but the return value of insert is missing in GCC 4.8, so it is written this way instead. + + set_parents(); + return result; + } + + /*! + @brief inserts element + + Inserts element @a val before iterator @a pos. + + @param[in] pos iterator before which the content will be inserted; may be + the end() iterator + @param[in] val element to insert + @return iterator pointing to the inserted @a val. + + @throw type_error.309 if called on JSON values other than arrays; + example: `"cannot use insert() with string"` + @throw invalid_iterator.202 if @a pos is not an iterator of *this; + example: `"iterator does not fit current value"` + + @complexity Constant plus linear in the distance between @a pos and end of + the container. + + @liveexample{The example shows how `insert()` is used.,insert} + + @since version 1.0.0 + */ + iterator insert(const_iterator pos, const basic_json& val) + { + // insert only works for arrays + if (JSON_HEDLEY_LIKELY(is_array())) + { + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", *this)); + } + + // insert to array and return iterator + return insert_iterator(pos, val); + } + + JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name()), *this)); + } + + /*! + @brief inserts element + @copydoc insert(const_iterator, const basic_json&) + */ + iterator insert(const_iterator pos, basic_json&& val) + { + return insert(pos, val); + } + + /*! + @brief inserts elements + + Inserts @a cnt copies of @a val before iterator @a pos. + + @param[in] pos iterator before which the content will be inserted; may be + the end() iterator + @param[in] cnt number of copies of @a val to insert + @param[in] val element to insert + @return iterator pointing to the first element inserted, or @a pos if + `cnt==0` + + @throw type_error.309 if called on JSON values other than arrays; example: + `"cannot use insert() with string"` + @throw invalid_iterator.202 if @a pos is not an iterator of *this; + example: `"iterator does not fit current value"` + + @complexity Linear in @a cnt plus linear in the distance between @a pos + and end of the container. + + @liveexample{The example shows how `insert()` is used.,insert__count} + + @since version 1.0.0 + */ + iterator insert(const_iterator pos, size_type cnt, const basic_json& val) + { + // insert only works for arrays + if (JSON_HEDLEY_LIKELY(is_array())) + { + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", *this)); + } + + // insert to array and return iterator + return insert_iterator(pos, cnt, val); + } + + JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name()), *this)); + } + + /*! + @brief inserts elements + + Inserts elements from range `[first, last)` before iterator @a pos. + + @param[in] pos iterator before which the content will be inserted; may be + the end() iterator + @param[in] first begin of the range of elements to insert + @param[in] last end of the range of elements to insert + + @throw type_error.309 if called on JSON values other than arrays; example: + `"cannot use insert() with string"` + @throw invalid_iterator.202 if @a pos is not an iterator of *this; + example: `"iterator does not fit current value"` + @throw invalid_iterator.210 if @a first and @a last do not belong to the + same JSON value; example: `"iterators do not fit"` + @throw invalid_iterator.211 if @a first or @a last are iterators into + container for which insert is called; example: `"passed iterators may not + belong to container"` + + @return iterator pointing to the first element inserted, or @a pos if + `first==last` + + @complexity Linear in `std::distance(first, last)` plus linear in the + distance between @a pos and end of the container. + + @liveexample{The example shows how `insert()` is used.,insert__range} + + @since version 1.0.0 + */ + iterator insert(const_iterator pos, const_iterator first, const_iterator last) + { + // insert only works for arrays + if (JSON_HEDLEY_UNLIKELY(!is_array())) + { + JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name()), *this)); + } + + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", *this)); + } + + // check if range iterators belong to the same JSON object + if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) + { + JSON_THROW(invalid_iterator::create(210, "iterators do not fit", *this)); + } + + if (JSON_HEDLEY_UNLIKELY(first.m_object == this)) + { + JSON_THROW(invalid_iterator::create(211, "passed iterators may not belong to container", *this)); + } + + // insert to array and return iterator + return insert_iterator(pos, first.m_it.array_iterator, last.m_it.array_iterator); + } + + /*! + @brief inserts elements + + Inserts elements from initializer list @a ilist before iterator @a pos. + + @param[in] pos iterator before which the content will be inserted; may be + the end() iterator + @param[in] ilist initializer list to insert the values from + + @throw type_error.309 if called on JSON values other than arrays; example: + `"cannot use insert() with string"` + @throw invalid_iterator.202 if @a pos is not an iterator of *this; + example: `"iterator does not fit current value"` + + @return iterator pointing to the first element inserted, or @a pos if + `ilist` is empty + + @complexity Linear in `ilist.size()` plus linear in the distance between + @a pos and end of the container. + + @liveexample{The example shows how `insert()` is used.,insert__ilist} + + @since version 1.0.0 + */ + iterator insert(const_iterator pos, initializer_list_t ilist) + { + // insert only works for arrays + if (JSON_HEDLEY_UNLIKELY(!is_array())) + { + JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name()), *this)); + } + + // check if iterator pos fits to this JSON value + if (JSON_HEDLEY_UNLIKELY(pos.m_object != this)) + { + JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", *this)); + } + + // insert to array and return iterator + return insert_iterator(pos, ilist.begin(), ilist.end()); + } + + /*! + @brief inserts elements + + Inserts elements from range `[first, last)`. + + @param[in] first begin of the range of elements to insert + @param[in] last end of the range of elements to insert + + @throw type_error.309 if called on JSON values other than objects; example: + `"cannot use insert() with string"` + @throw invalid_iterator.202 if iterator @a first or @a last does does not + point to an object; example: `"iterators first and last must point to + objects"` + @throw invalid_iterator.210 if @a first and @a last do not belong to the + same JSON value; example: `"iterators do not fit"` + + @complexity Logarithmic: `O(N*log(size() + N))`, where `N` is the number + of elements to insert. + + @liveexample{The example shows how `insert()` is used.,insert__range_object} + + @since version 3.0.0 + */ + void insert(const_iterator first, const_iterator last) + { + // insert only works for objects + if (JSON_HEDLEY_UNLIKELY(!is_object())) + { + JSON_THROW(type_error::create(309, "cannot use insert() with " + std::string(type_name()), *this)); + } + + // check if range iterators belong to the same JSON object + if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) + { + JSON_THROW(invalid_iterator::create(210, "iterators do not fit", *this)); + } + + // passed iterators must belong to objects + if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_object())) + { + JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects", *this)); + } + + m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); + } + + /*! + @brief updates a JSON object from another object, overwriting existing keys + + Inserts all values from JSON object @a j and overwrites existing keys. + + @param[in] j JSON object to read values from + + @throw type_error.312 if called on JSON values other than objects; example: + `"cannot use update() with string"` + + @complexity O(N*log(size() + N)), where N is the number of elements to + insert. + + @liveexample{The example shows how `update()` is used.,update} + + @sa https://docs.python.org/3.6/library/stdtypes.html#dict.update + + @since version 3.0.0 + */ + void update(const_reference j) + { + // implicitly convert null value to an empty object + if (is_null()) + { + m_type = value_t::object; + m_value.object = create(); + assert_invariant(); + } + + if (JSON_HEDLEY_UNLIKELY(!is_object())) + { + JSON_THROW(type_error::create(312, "cannot use update() with " + std::string(type_name()), *this)); + } + if (JSON_HEDLEY_UNLIKELY(!j.is_object())) + { + JSON_THROW(type_error::create(312, "cannot use update() with " + std::string(j.type_name()), *this)); + } + + for (auto it = j.cbegin(); it != j.cend(); ++it) + { + m_value.object->operator[](it.key()) = it.value(); +#if JSON_DIAGNOSTICS + m_value.object->operator[](it.key()).m_parent = this; +#endif + } + } + + /*! + @brief updates a JSON object from another object, overwriting existing keys + + Inserts all values from from range `[first, last)` and overwrites existing + keys. + + @param[in] first begin of the range of elements to insert + @param[in] last end of the range of elements to insert + + @throw type_error.312 if called on JSON values other than objects; example: + `"cannot use update() with string"` + @throw invalid_iterator.202 if iterator @a first or @a last does does not + point to an object; example: `"iterators first and last must point to + objects"` + @throw invalid_iterator.210 if @a first and @a last do not belong to the + same JSON value; example: `"iterators do not fit"` + + @complexity O(N*log(size() + N)), where N is the number of elements to + insert. + + @liveexample{The example shows how `update()` is used__range.,update} + + @sa https://docs.python.org/3.6/library/stdtypes.html#dict.update + + @since version 3.0.0 + */ + void update(const_iterator first, const_iterator last) + { + // implicitly convert null value to an empty object + if (is_null()) + { + m_type = value_t::object; + m_value.object = create(); + assert_invariant(); + } + + if (JSON_HEDLEY_UNLIKELY(!is_object())) + { + JSON_THROW(type_error::create(312, "cannot use update() with " + std::string(type_name()), *this)); + } + + // check if range iterators belong to the same JSON object + if (JSON_HEDLEY_UNLIKELY(first.m_object != last.m_object)) + { + JSON_THROW(invalid_iterator::create(210, "iterators do not fit", *this)); + } + + // passed iterators must belong to objects + if (JSON_HEDLEY_UNLIKELY(!first.m_object->is_object() + || !last.m_object->is_object())) + { + JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects", *this)); + } + + for (auto it = first; it != last; ++it) + { + m_value.object->operator[](it.key()) = it.value(); +#if JSON_DIAGNOSTICS + m_value.object->operator[](it.key()).m_parent = this; +#endif + } + } + + /*! + @brief exchanges the values + + Exchanges the contents of the JSON value with those of @a other. Does not + invoke any move, copy, or swap operations on individual elements. All + iterators and references remain valid. The past-the-end iterator is + invalidated. + + @param[in,out] other JSON value to exchange the contents with + + @complexity Constant. + + @liveexample{The example below shows how JSON values can be swapped with + `swap()`.,swap__reference} + + @since version 1.0.0 + */ + void swap(reference other) noexcept ( + std::is_nothrow_move_constructible::value&& + std::is_nothrow_move_assignable::value&& + std::is_nothrow_move_constructible::value&& + std::is_nothrow_move_assignable::value + ) + { + std::swap(m_type, other.m_type); + std::swap(m_value, other.m_value); + + set_parents(); + other.set_parents(); + assert_invariant(); + } + + /*! + @brief exchanges the values + + Exchanges the contents of the JSON value from @a left with those of @a right. Does not + invoke any move, copy, or swap operations on individual elements. All + iterators and references remain valid. The past-the-end iterator is + invalidated. implemented as a friend function callable via ADL. + + @param[in,out] left JSON value to exchange the contents with + @param[in,out] right JSON value to exchange the contents with + + @complexity Constant. + + @liveexample{The example below shows how JSON values can be swapped with + `swap()`.,swap__reference} + + @since version 1.0.0 + */ + friend void swap(reference left, reference right) noexcept ( + std::is_nothrow_move_constructible::value&& + std::is_nothrow_move_assignable::value&& + std::is_nothrow_move_constructible::value&& + std::is_nothrow_move_assignable::value + ) + { + left.swap(right); + } + + /*! + @brief exchanges the values + + Exchanges the contents of a JSON array with those of @a other. Does not + invoke any move, copy, or swap operations on individual elements. All + iterators and references remain valid. The past-the-end iterator is + invalidated. + + @param[in,out] other array to exchange the contents with + + @throw type_error.310 when JSON value is not an array; example: `"cannot + use swap() with string"` + + @complexity Constant. + + @liveexample{The example below shows how arrays can be swapped with + `swap()`.,swap__array_t} + + @since version 1.0.0 + */ + void swap(array_t& other) // NOLINT(bugprone-exception-escape) + { + // swap only works for arrays + if (JSON_HEDLEY_LIKELY(is_array())) + { + std::swap(*(m_value.array), other); + } + else + { + JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name()), *this)); + } + } + + /*! + @brief exchanges the values + + Exchanges the contents of a JSON object with those of @a other. Does not + invoke any move, copy, or swap operations on individual elements. All + iterators and references remain valid. The past-the-end iterator is + invalidated. + + @param[in,out] other object to exchange the contents with + + @throw type_error.310 when JSON value is not an object; example: + `"cannot use swap() with string"` + + @complexity Constant. + + @liveexample{The example below shows how objects can be swapped with + `swap()`.,swap__object_t} + + @since version 1.0.0 + */ + void swap(object_t& other) // NOLINT(bugprone-exception-escape) + { + // swap only works for objects + if (JSON_HEDLEY_LIKELY(is_object())) + { + std::swap(*(m_value.object), other); + } + else + { + JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name()), *this)); + } + } + + /*! + @brief exchanges the values + + Exchanges the contents of a JSON string with those of @a other. Does not + invoke any move, copy, or swap operations on individual elements. All + iterators and references remain valid. The past-the-end iterator is + invalidated. + + @param[in,out] other string to exchange the contents with + + @throw type_error.310 when JSON value is not a string; example: `"cannot + use swap() with boolean"` + + @complexity Constant. + + @liveexample{The example below shows how strings can be swapped with + `swap()`.,swap__string_t} + + @since version 1.0.0 + */ + void swap(string_t& other) // NOLINT(bugprone-exception-escape) + { + // swap only works for strings + if (JSON_HEDLEY_LIKELY(is_string())) + { + std::swap(*(m_value.string), other); + } + else + { + JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name()), *this)); + } + } + + /*! + @brief exchanges the values + + Exchanges the contents of a JSON string with those of @a other. Does not + invoke any move, copy, or swap operations on individual elements. All + iterators and references remain valid. The past-the-end iterator is + invalidated. + + @param[in,out] other binary to exchange the contents with + + @throw type_error.310 when JSON value is not a string; example: `"cannot + use swap() with boolean"` + + @complexity Constant. + + @liveexample{The example below shows how strings can be swapped with + `swap()`.,swap__binary_t} + + @since version 3.8.0 + */ + void swap(binary_t& other) // NOLINT(bugprone-exception-escape) + { + // swap only works for strings + if (JSON_HEDLEY_LIKELY(is_binary())) + { + std::swap(*(m_value.binary), other); + } + else + { + JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name()), *this)); + } + } + + /// @copydoc swap(binary_t&) + void swap(typename binary_t::container_type& other) // NOLINT(bugprone-exception-escape) + { + // swap only works for strings + if (JSON_HEDLEY_LIKELY(is_binary())) + { + std::swap(*(m_value.binary), other); + } + else + { + JSON_THROW(type_error::create(310, "cannot use swap() with " + std::string(type_name()), *this)); + } + } + + /// @} + + public: + ////////////////////////////////////////// + // lexicographical comparison operators // + ////////////////////////////////////////// + + /// @name lexicographical comparison operators + /// @{ + + /*! + @brief comparison: equal + + Compares two JSON values for equality according to the following rules: + - Two JSON values are equal if (1) they are from the same type and (2) + their stored values are the same according to their respective + `operator==`. + - Integer and floating-point numbers are automatically converted before + comparison. Note that two NaN values are always treated as unequal. + - Two JSON null values are equal. + + @note Floating-point inside JSON values numbers are compared with + `json::number_float_t::operator==` which is `double::operator==` by + default. To compare floating-point while respecting an epsilon, an alternative + [comparison function](https://github.com/mariokonrad/marnav/blob/master/include/marnav/math/floatingpoint.hpp#L34-#L39) + could be used, for instance + @code {.cpp} + template::value, T>::type> + inline bool is_same(T a, T b, T epsilon = std::numeric_limits::epsilon()) noexcept + { + return std::abs(a - b) <= epsilon; + } + @endcode + Or you can self-defined operator equal function like this: + @code {.cpp} + bool my_equal(const_reference lhs, const_reference rhs) { + const auto lhs_type lhs.type(); + const auto rhs_type rhs.type(); + if (lhs_type == rhs_type) { + switch(lhs_type) + // self_defined case + case value_t::number_float: + return std::abs(lhs - rhs) <= std::numeric_limits::epsilon(); + // other cases remain the same with the original + ... + } + ... + } + @endcode + + @note NaN values never compare equal to themselves or to other NaN values. + + @param[in] lhs first JSON value to consider + @param[in] rhs second JSON value to consider + @return whether the values @a lhs and @a rhs are equal + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @complexity Linear. + + @liveexample{The example demonstrates comparing several JSON + types.,operator__equal} + + @since version 1.0.0 + */ + friend bool operator==(const_reference lhs, const_reference rhs) noexcept + { +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wfloat-equal" +#endif + const auto lhs_type = lhs.type(); + const auto rhs_type = rhs.type(); + + if (lhs_type == rhs_type) + { + switch (lhs_type) + { + case value_t::array: + return *lhs.m_value.array == *rhs.m_value.array; + + case value_t::object: + return *lhs.m_value.object == *rhs.m_value.object; + + case value_t::null: + return true; + + case value_t::string: + return *lhs.m_value.string == *rhs.m_value.string; + + case value_t::boolean: + return lhs.m_value.boolean == rhs.m_value.boolean; + + case value_t::number_integer: + return lhs.m_value.number_integer == rhs.m_value.number_integer; + + case value_t::number_unsigned: + return lhs.m_value.number_unsigned == rhs.m_value.number_unsigned; + + case value_t::number_float: + return lhs.m_value.number_float == rhs.m_value.number_float; + + case value_t::binary: + return *lhs.m_value.binary == *rhs.m_value.binary; + + case value_t::discarded: + default: + return false; + } + } + else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_float) + { + return static_cast(lhs.m_value.number_integer) == rhs.m_value.number_float; + } + else if (lhs_type == value_t::number_float && rhs_type == value_t::number_integer) + { + return lhs.m_value.number_float == static_cast(rhs.m_value.number_integer); + } + else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_float) + { + return static_cast(lhs.m_value.number_unsigned) == rhs.m_value.number_float; + } + else if (lhs_type == value_t::number_float && rhs_type == value_t::number_unsigned) + { + return lhs.m_value.number_float == static_cast(rhs.m_value.number_unsigned); + } + else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) + { + return static_cast(lhs.m_value.number_unsigned) == rhs.m_value.number_integer; + } + else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) + { + return lhs.m_value.number_integer == static_cast(rhs.m_value.number_unsigned); + } + + return false; +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + } + + /*! + @brief comparison: equal + @copydoc operator==(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator==(const_reference lhs, ScalarType rhs) noexcept + { + return lhs == basic_json(rhs); + } + + /*! + @brief comparison: equal + @copydoc operator==(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator==(ScalarType lhs, const_reference rhs) noexcept + { + return basic_json(lhs) == rhs; + } + + /*! + @brief comparison: not equal + + Compares two JSON values for inequality by calculating `not (lhs == rhs)`. + + @param[in] lhs first JSON value to consider + @param[in] rhs second JSON value to consider + @return whether the values @a lhs and @a rhs are not equal + + @complexity Linear. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @liveexample{The example demonstrates comparing several JSON + types.,operator__notequal} + + @since version 1.0.0 + */ + friend bool operator!=(const_reference lhs, const_reference rhs) noexcept + { + return !(lhs == rhs); + } + + /*! + @brief comparison: not equal + @copydoc operator!=(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator!=(const_reference lhs, ScalarType rhs) noexcept + { + return lhs != basic_json(rhs); + } + + /*! + @brief comparison: not equal + @copydoc operator!=(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator!=(ScalarType lhs, const_reference rhs) noexcept + { + return basic_json(lhs) != rhs; + } + + /*! + @brief comparison: less than + + Compares whether one JSON value @a lhs is less than another JSON value @a + rhs according to the following rules: + - If @a lhs and @a rhs have the same type, the values are compared using + the default `<` operator. + - Integer and floating-point numbers are automatically converted before + comparison + - In case @a lhs and @a rhs have different types, the values are ignored + and the order of the types is considered, see + @ref operator<(const value_t, const value_t). + + @param[in] lhs first JSON value to consider + @param[in] rhs second JSON value to consider + @return whether @a lhs is less than @a rhs + + @complexity Linear. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @liveexample{The example demonstrates comparing several JSON + types.,operator__less} + + @since version 1.0.0 + */ + friend bool operator<(const_reference lhs, const_reference rhs) noexcept + { + const auto lhs_type = lhs.type(); + const auto rhs_type = rhs.type(); + + if (lhs_type == rhs_type) + { + switch (lhs_type) + { + case value_t::array: + // note parentheses are necessary, see + // https://github.com/nlohmann/json/issues/1530 + return (*lhs.m_value.array) < (*rhs.m_value.array); + + case value_t::object: + return (*lhs.m_value.object) < (*rhs.m_value.object); + + case value_t::null: + return false; + + case value_t::string: + return (*lhs.m_value.string) < (*rhs.m_value.string); + + case value_t::boolean: + return (lhs.m_value.boolean) < (rhs.m_value.boolean); + + case value_t::number_integer: + return (lhs.m_value.number_integer) < (rhs.m_value.number_integer); + + case value_t::number_unsigned: + return (lhs.m_value.number_unsigned) < (rhs.m_value.number_unsigned); + + case value_t::number_float: + return (lhs.m_value.number_float) < (rhs.m_value.number_float); + + case value_t::binary: + return (*lhs.m_value.binary) < (*rhs.m_value.binary); + + case value_t::discarded: + default: + return false; + } + } + else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_float) + { + return static_cast(lhs.m_value.number_integer) < rhs.m_value.number_float; + } + else if (lhs_type == value_t::number_float && rhs_type == value_t::number_integer) + { + return lhs.m_value.number_float < static_cast(rhs.m_value.number_integer); + } + else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_float) + { + return static_cast(lhs.m_value.number_unsigned) < rhs.m_value.number_float; + } + else if (lhs_type == value_t::number_float && rhs_type == value_t::number_unsigned) + { + return lhs.m_value.number_float < static_cast(rhs.m_value.number_unsigned); + } + else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) + { + return lhs.m_value.number_integer < static_cast(rhs.m_value.number_unsigned); + } + else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) + { + return static_cast(lhs.m_value.number_unsigned) < rhs.m_value.number_integer; + } + + // We only reach this line if we cannot compare values. In that case, + // we compare types. Note we have to call the operator explicitly, + // because MSVC has problems otherwise. + return operator<(lhs_type, rhs_type); + } + + /*! + @brief comparison: less than + @copydoc operator<(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator<(const_reference lhs, ScalarType rhs) noexcept + { + return lhs < basic_json(rhs); + } + + /*! + @brief comparison: less than + @copydoc operator<(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator<(ScalarType lhs, const_reference rhs) noexcept + { + return basic_json(lhs) < rhs; + } + + /*! + @brief comparison: less than or equal + + Compares whether one JSON value @a lhs is less than or equal to another + JSON value by calculating `not (rhs < lhs)`. + + @param[in] lhs first JSON value to consider + @param[in] rhs second JSON value to consider + @return whether @a lhs is less than or equal to @a rhs + + @complexity Linear. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @liveexample{The example demonstrates comparing several JSON + types.,operator__greater} + + @since version 1.0.0 + */ + friend bool operator<=(const_reference lhs, const_reference rhs) noexcept + { + return !(rhs < lhs); + } + + /*! + @brief comparison: less than or equal + @copydoc operator<=(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator<=(const_reference lhs, ScalarType rhs) noexcept + { + return lhs <= basic_json(rhs); + } + + /*! + @brief comparison: less than or equal + @copydoc operator<=(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator<=(ScalarType lhs, const_reference rhs) noexcept + { + return basic_json(lhs) <= rhs; + } + + /*! + @brief comparison: greater than + + Compares whether one JSON value @a lhs is greater than another + JSON value by calculating `not (lhs <= rhs)`. + + @param[in] lhs first JSON value to consider + @param[in] rhs second JSON value to consider + @return whether @a lhs is greater than to @a rhs + + @complexity Linear. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @liveexample{The example demonstrates comparing several JSON + types.,operator__lessequal} + + @since version 1.0.0 + */ + friend bool operator>(const_reference lhs, const_reference rhs) noexcept + { + return !(lhs <= rhs); + } + + /*! + @brief comparison: greater than + @copydoc operator>(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator>(const_reference lhs, ScalarType rhs) noexcept + { + return lhs > basic_json(rhs); + } + + /*! + @brief comparison: greater than + @copydoc operator>(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator>(ScalarType lhs, const_reference rhs) noexcept + { + return basic_json(lhs) > rhs; + } + + /*! + @brief comparison: greater than or equal + + Compares whether one JSON value @a lhs is greater than or equal to another + JSON value by calculating `not (lhs < rhs)`. + + @param[in] lhs first JSON value to consider + @param[in] rhs second JSON value to consider + @return whether @a lhs is greater than or equal to @a rhs + + @complexity Linear. + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @liveexample{The example demonstrates comparing several JSON + types.,operator__greaterequal} + + @since version 1.0.0 + */ + friend bool operator>=(const_reference lhs, const_reference rhs) noexcept + { + return !(lhs < rhs); + } + + /*! + @brief comparison: greater than or equal + @copydoc operator>=(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator>=(const_reference lhs, ScalarType rhs) noexcept + { + return lhs >= basic_json(rhs); + } + + /*! + @brief comparison: greater than or equal + @copydoc operator>=(const_reference, const_reference) + */ + template::value, int>::type = 0> + friend bool operator>=(ScalarType lhs, const_reference rhs) noexcept + { + return basic_json(lhs) >= rhs; + } + + /// @} + + /////////////////// + // serialization // + /////////////////// + + /// @name serialization + /// @{ +#ifndef JSON_NO_IO + /*! + @brief serialize to stream + + Serialize the given JSON value @a j to the output stream @a o. The JSON + value will be serialized using the @ref dump member function. + + - The indentation of the output can be controlled with the member variable + `width` of the output stream @a o. For instance, using the manipulator + `std::setw(4)` on @a o sets the indentation level to `4` and the + serialization result is the same as calling `dump(4)`. + + - The indentation character can be controlled with the member variable + `fill` of the output stream @a o. For instance, the manipulator + `std::setfill('\\t')` sets indentation to use a tab character rather than + the default space character. + + @param[in,out] o stream to serialize to + @param[in] j JSON value to serialize + + @return the stream @a o + + @throw type_error.316 if a string stored inside the JSON value is not + UTF-8 encoded + + @complexity Linear. + + @liveexample{The example below shows the serialization with different + parameters to `width` to adjust the indentation level.,operator_serialize} + + @since version 1.0.0; indentation character added in version 3.0.0 + */ + friend std::ostream& operator<<(std::ostream& o, const basic_json& j) + { + // read width member and use it as indentation parameter if nonzero + const bool pretty_print = o.width() > 0; + const auto indentation = pretty_print ? o.width() : 0; + + // reset width to 0 for subsequent calls to this stream + o.width(0); + + // do the actual serialization + serializer s(detail::output_adapter(o), o.fill()); + s.dump(j, pretty_print, false, static_cast(indentation)); + return o; + } + + /*! + @brief serialize to stream + @deprecated This stream operator is deprecated and will be removed in + future 4.0.0 of the library. Please use + @ref operator<<(std::ostream&, const basic_json&) + instead; that is, replace calls like `j >> o;` with `o << j;`. + @since version 1.0.0; deprecated since version 3.0.0 + */ + JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator<<(std::ostream&, const basic_json&)) + friend std::ostream& operator>>(const basic_json& j, std::ostream& o) + { + return o << j; + } +#endif // JSON_NO_IO + /// @} + + + ///////////////////// + // deserialization // + ///////////////////// + + /// @name deserialization + /// @{ + + /*! + @brief deserialize from a compatible input + + @tparam InputType A compatible input, for instance + - an std::istream object + - a FILE pointer + - a C-style array of characters + - a pointer to a null-terminated string of single byte characters + - an object obj for which begin(obj) and end(obj) produces a valid pair of + iterators. + + @param[in] i input to read from + @param[in] cb a parser callback function of type @ref parser_callback_t + which is used to control the deserialization by filtering unwanted values + (optional) + @param[in] allow_exceptions whether to throw exceptions in case of a + parse error (optional, true by default) + @param[in] ignore_comments whether comments should be ignored and treated + like whitespace (true) or yield a parse error (true); (optional, false by + default) + + @return deserialized JSON value; in case of a parse error and + @a allow_exceptions set to `false`, the return value will be + value_t::discarded. + + @throw parse_error.101 if a parse error occurs; example: `""unexpected end + of input; expected string literal""` + @throw parse_error.102 if to_unicode fails or surrogate error + @throw parse_error.103 if to_unicode fails + + @complexity Linear in the length of the input. The parser is a predictive + LL(1) parser. The complexity can be higher if the parser callback function + @a cb or reading from the input @a i has a super-linear complexity. + + @note A UTF-8 byte order mark is silently ignored. + + @liveexample{The example below demonstrates the `parse()` function reading + from an array.,parse__array__parser_callback_t} + + @liveexample{The example below demonstrates the `parse()` function with + and without callback function.,parse__string__parser_callback_t} + + @liveexample{The example below demonstrates the `parse()` function with + and without callback function.,parse__istream__parser_callback_t} + + @liveexample{The example below demonstrates the `parse()` function reading + from a contiguous container.,parse__contiguouscontainer__parser_callback_t} + + @since version 2.0.3 (contiguous containers); version 3.9.0 allowed to + ignore comments. + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json parse(InputType&& i, + const parser_callback_t cb = nullptr, + const bool allow_exceptions = true, + const bool ignore_comments = false) + { + basic_json result; + parser(detail::input_adapter(std::forward(i)), cb, allow_exceptions, ignore_comments).parse(true, result); + return result; + } + + /*! + @brief deserialize from a pair of character iterators + + The value_type of the iterator must be a integral type with size of 1, 2 or + 4 bytes, which will be interpreted respectively as UTF-8, UTF-16 and UTF-32. + + @param[in] first iterator to start of character range + @param[in] last iterator to end of character range + @param[in] cb a parser callback function of type @ref parser_callback_t + which is used to control the deserialization by filtering unwanted values + (optional) + @param[in] allow_exceptions whether to throw exceptions in case of a + parse error (optional, true by default) + @param[in] ignore_comments whether comments should be ignored and treated + like whitespace (true) or yield a parse error (true); (optional, false by + default) + + @return deserialized JSON value; in case of a parse error and + @a allow_exceptions set to `false`, the return value will be + value_t::discarded. + + @throw parse_error.101 if a parse error occurs; example: `""unexpected end + of input; expected string literal""` + @throw parse_error.102 if to_unicode fails or surrogate error + @throw parse_error.103 if to_unicode fails + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json parse(IteratorType first, + IteratorType last, + const parser_callback_t cb = nullptr, + const bool allow_exceptions = true, + const bool ignore_comments = false) + { + basic_json result; + parser(detail::input_adapter(std::move(first), std::move(last)), cb, allow_exceptions, ignore_comments).parse(true, result); + return result; + } + + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, parse(ptr, ptr + len)) + static basic_json parse(detail::span_input_adapter&& i, + const parser_callback_t cb = nullptr, + const bool allow_exceptions = true, + const bool ignore_comments = false) + { + basic_json result; + parser(i.get(), cb, allow_exceptions, ignore_comments).parse(true, result); + return result; + } + + /*! + @brief check if the input is valid JSON + + Unlike the @ref parse(InputType&&, const parser_callback_t,const bool) + function, this function neither throws an exception in case of invalid JSON + input (i.e., a parse error) nor creates diagnostic information. + + @tparam InputType A compatible input, for instance + - an std::istream object + - a FILE pointer + - a C-style array of characters + - a pointer to a null-terminated string of single byte characters + - an object obj for which begin(obj) and end(obj) produces a valid pair of + iterators. + + @param[in] i input to read from + @param[in] ignore_comments whether comments should be ignored and treated + like whitespace (true) or yield a parse error (true); (optional, false by + default) + + @return Whether the input read from @a i is valid JSON. + + @complexity Linear in the length of the input. The parser is a predictive + LL(1) parser. + + @note A UTF-8 byte order mark is silently ignored. + + @liveexample{The example below demonstrates the `accept()` function reading + from a string.,accept__string} + */ + template + static bool accept(InputType&& i, + const bool ignore_comments = false) + { + return parser(detail::input_adapter(std::forward(i)), nullptr, false, ignore_comments).accept(true); + } + + template + static bool accept(IteratorType first, IteratorType last, + const bool ignore_comments = false) + { + return parser(detail::input_adapter(std::move(first), std::move(last)), nullptr, false, ignore_comments).accept(true); + } + + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, accept(ptr, ptr + len)) + static bool accept(detail::span_input_adapter&& i, + const bool ignore_comments = false) + { + return parser(i.get(), nullptr, false, ignore_comments).accept(true); + } + + /*! + @brief generate SAX events + + The SAX event lister must follow the interface of @ref json_sax. + + This function reads from a compatible input. Examples are: + - an std::istream object + - a FILE pointer + - a C-style array of characters + - a pointer to a null-terminated string of single byte characters + - an object obj for which begin(obj) and end(obj) produces a valid pair of + iterators. + + @param[in] i input to read from + @param[in,out] sax SAX event listener + @param[in] format the format to parse (JSON, CBOR, MessagePack, or UBJSON) + @param[in] strict whether the input has to be consumed completely + @param[in] ignore_comments whether comments should be ignored and treated + like whitespace (true) or yield a parse error (true); (optional, false by + default); only applies to the JSON file format. + + @return return value of the last processed SAX event + + @throw parse_error.101 if a parse error occurs; example: `""unexpected end + of input; expected string literal""` + @throw parse_error.102 if to_unicode fails or surrogate error + @throw parse_error.103 if to_unicode fails + + @complexity Linear in the length of the input. The parser is a predictive + LL(1) parser. The complexity can be higher if the SAX consumer @a sax has + a super-linear complexity. + + @note A UTF-8 byte order mark is silently ignored. + + @liveexample{The example below demonstrates the `sax_parse()` function + reading from string and processing the events with a user-defined SAX + event consumer.,sax_parse} + + @since version 3.2.0 + */ + template + JSON_HEDLEY_NON_NULL(2) + static bool sax_parse(InputType&& i, SAX* sax, + input_format_t format = input_format_t::json, + const bool strict = true, + const bool ignore_comments = false) + { + auto ia = detail::input_adapter(std::forward(i)); + return format == input_format_t::json + ? parser(std::move(ia), nullptr, true, ignore_comments).sax_parse(sax, strict) + : detail::binary_reader(std::move(ia)).sax_parse(format, sax, strict); + } + + template + JSON_HEDLEY_NON_NULL(3) + static bool sax_parse(IteratorType first, IteratorType last, SAX* sax, + input_format_t format = input_format_t::json, + const bool strict = true, + const bool ignore_comments = false) + { + auto ia = detail::input_adapter(std::move(first), std::move(last)); + return format == input_format_t::json + ? parser(std::move(ia), nullptr, true, ignore_comments).sax_parse(sax, strict) + : detail::binary_reader(std::move(ia)).sax_parse(format, sax, strict); + } + + template + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, sax_parse(ptr, ptr + len, ...)) + JSON_HEDLEY_NON_NULL(2) + static bool sax_parse(detail::span_input_adapter&& i, SAX* sax, + input_format_t format = input_format_t::json, + const bool strict = true, + const bool ignore_comments = false) + { + auto ia = i.get(); + return format == input_format_t::json + // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) + ? parser(std::move(ia), nullptr, true, ignore_comments).sax_parse(sax, strict) + // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) + : detail::binary_reader(std::move(ia)).sax_parse(format, sax, strict); + } +#ifndef JSON_NO_IO + /*! + @brief deserialize from stream + @deprecated This stream operator is deprecated and will be removed in + version 4.0.0 of the library. Please use + @ref operator>>(std::istream&, basic_json&) + instead; that is, replace calls like `j << i;` with `i >> j;`. + @since version 1.0.0; deprecated since version 3.0.0 + */ + JSON_HEDLEY_DEPRECATED_FOR(3.0.0, operator>>(std::istream&, basic_json&)) + friend std::istream& operator<<(basic_json& j, std::istream& i) + { + return operator>>(i, j); + } + + /*! + @brief deserialize from stream + + Deserializes an input stream to a JSON value. + + @param[in,out] i input stream to read a serialized JSON value from + @param[in,out] j JSON value to write the deserialized input to + + @throw parse_error.101 in case of an unexpected token + @throw parse_error.102 if to_unicode fails or surrogate error + @throw parse_error.103 if to_unicode fails + + @complexity Linear in the length of the input. The parser is a predictive + LL(1) parser. + + @note A UTF-8 byte order mark is silently ignored. + + @liveexample{The example below shows how a JSON value is constructed by + reading a serialization from a stream.,operator_deserialize} + + @sa parse(std::istream&, const parser_callback_t) for a variant with a + parser callback function to filter values while parsing + + @since version 1.0.0 + */ + friend std::istream& operator>>(std::istream& i, basic_json& j) + { + parser(detail::input_adapter(i)).parse(false, j); + return i; + } +#endif // JSON_NO_IO + /// @} + + /////////////////////////// + // convenience functions // + /////////////////////////// + + /*! + @brief return the type as string + + Returns the type name as string to be used in error messages - usually to + indicate that a function was called on a wrong JSON type. + + @return a string representation of a the @a m_type member: + Value type | return value + ----------- | ------------- + null | `"null"` + boolean | `"boolean"` + string | `"string"` + number | `"number"` (for all number types) + object | `"object"` + array | `"array"` + binary | `"binary"` + discarded | `"discarded"` + + @exceptionsafety No-throw guarantee: this function never throws exceptions. + + @complexity Constant. + + @liveexample{The following code exemplifies `type_name()` for all JSON + types.,type_name} + + @sa see @ref type() -- return the type of the JSON value + @sa see @ref operator value_t() -- return the type of the JSON value (implicit) + + @since version 1.0.0, public since 2.1.0, `const char*` and `noexcept` + since 3.0.0 + */ + JSON_HEDLEY_RETURNS_NON_NULL + const char* type_name() const noexcept + { + { + switch (m_type) + { + case value_t::null: + return "null"; + case value_t::object: + return "object"; + case value_t::array: + return "array"; + case value_t::string: + return "string"; + case value_t::boolean: + return "boolean"; + case value_t::binary: + return "binary"; + case value_t::discarded: + return "discarded"; + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + default: + return "number"; + } + } + } + + + JSON_PRIVATE_UNLESS_TESTED: + ////////////////////// + // member variables // + ////////////////////// + + /// the type of the current element + value_t m_type = value_t::null; + + /// the value of the current element + json_value m_value = {}; + +#if JSON_DIAGNOSTICS + /// a pointer to a parent value (for debugging purposes) + basic_json* m_parent = nullptr; +#endif + + ////////////////////////////////////////// + // binary serialization/deserialization // + ////////////////////////////////////////// + + /// @name binary serialization/deserialization support + /// @{ + + public: + /*! + @brief create a CBOR serialization of a given JSON value + + Serializes a given JSON value @a j to a byte vector using the CBOR (Concise + Binary Object Representation) serialization format. CBOR is a binary + serialization format which aims to be more compact than JSON itself, yet + more efficient to parse. + + The library uses the following mapping from JSON values types to + CBOR types according to the CBOR specification (RFC 7049): + + JSON value type | value/range | CBOR type | first byte + --------------- | ------------------------------------------ | ---------------------------------- | --------------- + null | `null` | Null | 0xF6 + boolean | `true` | True | 0xF5 + boolean | `false` | False | 0xF4 + number_integer | -9223372036854775808..-2147483649 | Negative integer (8 bytes follow) | 0x3B + number_integer | -2147483648..-32769 | Negative integer (4 bytes follow) | 0x3A + number_integer | -32768..-129 | Negative integer (2 bytes follow) | 0x39 + number_integer | -128..-25 | Negative integer (1 byte follow) | 0x38 + number_integer | -24..-1 | Negative integer | 0x20..0x37 + number_integer | 0..23 | Integer | 0x00..0x17 + number_integer | 24..255 | Unsigned integer (1 byte follow) | 0x18 + number_integer | 256..65535 | Unsigned integer (2 bytes follow) | 0x19 + number_integer | 65536..4294967295 | Unsigned integer (4 bytes follow) | 0x1A + number_integer | 4294967296..18446744073709551615 | Unsigned integer (8 bytes follow) | 0x1B + number_unsigned | 0..23 | Integer | 0x00..0x17 + number_unsigned | 24..255 | Unsigned integer (1 byte follow) | 0x18 + number_unsigned | 256..65535 | Unsigned integer (2 bytes follow) | 0x19 + number_unsigned | 65536..4294967295 | Unsigned integer (4 bytes follow) | 0x1A + number_unsigned | 4294967296..18446744073709551615 | Unsigned integer (8 bytes follow) | 0x1B + number_float | *any value representable by a float* | Single-Precision Float | 0xFA + number_float | *any value NOT representable by a float* | Double-Precision Float | 0xFB + string | *length*: 0..23 | UTF-8 string | 0x60..0x77 + string | *length*: 23..255 | UTF-8 string (1 byte follow) | 0x78 + string | *length*: 256..65535 | UTF-8 string (2 bytes follow) | 0x79 + string | *length*: 65536..4294967295 | UTF-8 string (4 bytes follow) | 0x7A + string | *length*: 4294967296..18446744073709551615 | UTF-8 string (8 bytes follow) | 0x7B + array | *size*: 0..23 | array | 0x80..0x97 + array | *size*: 23..255 | array (1 byte follow) | 0x98 + array | *size*: 256..65535 | array (2 bytes follow) | 0x99 + array | *size*: 65536..4294967295 | array (4 bytes follow) | 0x9A + array | *size*: 4294967296..18446744073709551615 | array (8 bytes follow) | 0x9B + object | *size*: 0..23 | map | 0xA0..0xB7 + object | *size*: 23..255 | map (1 byte follow) | 0xB8 + object | *size*: 256..65535 | map (2 bytes follow) | 0xB9 + object | *size*: 65536..4294967295 | map (4 bytes follow) | 0xBA + object | *size*: 4294967296..18446744073709551615 | map (8 bytes follow) | 0xBB + binary | *size*: 0..23 | byte string | 0x40..0x57 + binary | *size*: 23..255 | byte string (1 byte follow) | 0x58 + binary | *size*: 256..65535 | byte string (2 bytes follow) | 0x59 + binary | *size*: 65536..4294967295 | byte string (4 bytes follow) | 0x5A + binary | *size*: 4294967296..18446744073709551615 | byte string (8 bytes follow) | 0x5B + + Binary values with subtype are mapped to tagged values (0xD8..0xDB) + depending on the subtype, followed by a byte string, see "binary" cells + in the table above. + + @note The mapping is **complete** in the sense that any JSON value type + can be converted to a CBOR value. + + @note If NaN or Infinity are stored inside a JSON number, they are + serialized properly. This behavior differs from the @ref dump() + function which serializes NaN or Infinity to `null`. + + @note The following CBOR types are not used in the conversion: + - UTF-8 strings terminated by "break" (0x7F) + - arrays terminated by "break" (0x9F) + - maps terminated by "break" (0xBF) + - byte strings terminated by "break" (0x5F) + - date/time (0xC0..0xC1) + - bignum (0xC2..0xC3) + - decimal fraction (0xC4) + - bigfloat (0xC5) + - expected conversions (0xD5..0xD7) + - simple values (0xE0..0xF3, 0xF8) + - undefined (0xF7) + - half-precision floats (0xF9) + - break (0xFF) + + @param[in] j JSON value to serialize + @return CBOR serialization as byte vector + + @complexity Linear in the size of the JSON value @a j. + + @liveexample{The example shows the serialization of a JSON value to a byte + vector in CBOR format.,to_cbor} + + @sa http://cbor.io + @sa see @ref from_cbor(InputType&&, const bool, const bool, const cbor_tag_handler_t) for the + analogous deserialization + @sa see @ref to_msgpack(const basic_json&) for the related MessagePack format + @sa see @ref to_ubjson(const basic_json&, const bool, const bool) for the + related UBJSON format + + @since version 2.0.9; compact representation of floating-point numbers + since version 3.8.0 + */ + static std::vector to_cbor(const basic_json& j) + { + std::vector result; + to_cbor(j, result); + return result; + } + + static void to_cbor(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_cbor(j); + } + + static void to_cbor(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_cbor(j); + } + + /*! + @brief create a MessagePack serialization of a given JSON value + + Serializes a given JSON value @a j to a byte vector using the MessagePack + serialization format. MessagePack is a binary serialization format which + aims to be more compact than JSON itself, yet more efficient to parse. + + The library uses the following mapping from JSON values types to + MessagePack types according to the MessagePack specification: + + JSON value type | value/range | MessagePack type | first byte + --------------- | --------------------------------- | ---------------- | ---------- + null | `null` | nil | 0xC0 + boolean | `true` | true | 0xC3 + boolean | `false` | false | 0xC2 + number_integer | -9223372036854775808..-2147483649 | int64 | 0xD3 + number_integer | -2147483648..-32769 | int32 | 0xD2 + number_integer | -32768..-129 | int16 | 0xD1 + number_integer | -128..-33 | int8 | 0xD0 + number_integer | -32..-1 | negative fixint | 0xE0..0xFF + number_integer | 0..127 | positive fixint | 0x00..0x7F + number_integer | 128..255 | uint 8 | 0xCC + number_integer | 256..65535 | uint 16 | 0xCD + number_integer | 65536..4294967295 | uint 32 | 0xCE + number_integer | 4294967296..18446744073709551615 | uint 64 | 0xCF + number_unsigned | 0..127 | positive fixint | 0x00..0x7F + number_unsigned | 128..255 | uint 8 | 0xCC + number_unsigned | 256..65535 | uint 16 | 0xCD + number_unsigned | 65536..4294967295 | uint 32 | 0xCE + number_unsigned | 4294967296..18446744073709551615 | uint 64 | 0xCF + number_float | *any value representable by a float* | float 32 | 0xCA + number_float | *any value NOT representable by a float* | float 64 | 0xCB + string | *length*: 0..31 | fixstr | 0xA0..0xBF + string | *length*: 32..255 | str 8 | 0xD9 + string | *length*: 256..65535 | str 16 | 0xDA + string | *length*: 65536..4294967295 | str 32 | 0xDB + array | *size*: 0..15 | fixarray | 0x90..0x9F + array | *size*: 16..65535 | array 16 | 0xDC + array | *size*: 65536..4294967295 | array 32 | 0xDD + object | *size*: 0..15 | fix map | 0x80..0x8F + object | *size*: 16..65535 | map 16 | 0xDE + object | *size*: 65536..4294967295 | map 32 | 0xDF + binary | *size*: 0..255 | bin 8 | 0xC4 + binary | *size*: 256..65535 | bin 16 | 0xC5 + binary | *size*: 65536..4294967295 | bin 32 | 0xC6 + + @note The mapping is **complete** in the sense that any JSON value type + can be converted to a MessagePack value. + + @note The following values can **not** be converted to a MessagePack value: + - strings with more than 4294967295 bytes + - byte strings with more than 4294967295 bytes + - arrays with more than 4294967295 elements + - objects with more than 4294967295 elements + + @note Any MessagePack output created @ref to_msgpack can be successfully + parsed by @ref from_msgpack. + + @note If NaN or Infinity are stored inside a JSON number, they are + serialized properly. This behavior differs from the @ref dump() + function which serializes NaN or Infinity to `null`. + + @param[in] j JSON value to serialize + @return MessagePack serialization as byte vector + + @complexity Linear in the size of the JSON value @a j. + + @liveexample{The example shows the serialization of a JSON value to a byte + vector in MessagePack format.,to_msgpack} + + @sa http://msgpack.org + @sa see @ref from_msgpack for the analogous deserialization + @sa see @ref to_cbor(const basic_json& for the related CBOR format + @sa see @ref to_ubjson(const basic_json&, const bool, const bool) for the + related UBJSON format + + @since version 2.0.9 + */ + static std::vector to_msgpack(const basic_json& j) + { + std::vector result; + to_msgpack(j, result); + return result; + } + + static void to_msgpack(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_msgpack(j); + } + + static void to_msgpack(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_msgpack(j); + } + + /*! + @brief create a UBJSON serialization of a given JSON value + + Serializes a given JSON value @a j to a byte vector using the UBJSON + (Universal Binary JSON) serialization format. UBJSON aims to be more compact + than JSON itself, yet more efficient to parse. + + The library uses the following mapping from JSON values types to + UBJSON types according to the UBJSON specification: + + JSON value type | value/range | UBJSON type | marker + --------------- | --------------------------------- | ----------- | ------ + null | `null` | null | `Z` + boolean | `true` | true | `T` + boolean | `false` | false | `F` + number_integer | -9223372036854775808..-2147483649 | int64 | `L` + number_integer | -2147483648..-32769 | int32 | `l` + number_integer | -32768..-129 | int16 | `I` + number_integer | -128..127 | int8 | `i` + number_integer | 128..255 | uint8 | `U` + number_integer | 256..32767 | int16 | `I` + number_integer | 32768..2147483647 | int32 | `l` + number_integer | 2147483648..9223372036854775807 | int64 | `L` + number_unsigned | 0..127 | int8 | `i` + number_unsigned | 128..255 | uint8 | `U` + number_unsigned | 256..32767 | int16 | `I` + number_unsigned | 32768..2147483647 | int32 | `l` + number_unsigned | 2147483648..9223372036854775807 | int64 | `L` + number_unsigned | 2147483649..18446744073709551615 | high-precision | `H` + number_float | *any value* | float64 | `D` + string | *with shortest length indicator* | string | `S` + array | *see notes on optimized format* | array | `[` + object | *see notes on optimized format* | map | `{` + + @note The mapping is **complete** in the sense that any JSON value type + can be converted to a UBJSON value. + + @note The following values can **not** be converted to a UBJSON value: + - strings with more than 9223372036854775807 bytes (theoretical) + + @note The following markers are not used in the conversion: + - `Z`: no-op values are not created. + - `C`: single-byte strings are serialized with `S` markers. + + @note Any UBJSON output created @ref to_ubjson can be successfully parsed + by @ref from_ubjson. + + @note If NaN or Infinity are stored inside a JSON number, they are + serialized properly. This behavior differs from the @ref dump() + function which serializes NaN or Infinity to `null`. + + @note The optimized formats for containers are supported: Parameter + @a use_size adds size information to the beginning of a container and + removes the closing marker. Parameter @a use_type further checks + whether all elements of a container have the same type and adds the + type marker to the beginning of the container. The @a use_type + parameter must only be used together with @a use_size = true. Note + that @a use_size = true alone may result in larger representations - + the benefit of this parameter is that the receiving side is + immediately informed on the number of elements of the container. + + @note If the JSON data contains the binary type, the value stored is a list + of integers, as suggested by the UBJSON documentation. In particular, + this means that serialization and the deserialization of a JSON + containing binary values into UBJSON and back will result in a + different JSON object. + + @param[in] j JSON value to serialize + @param[in] use_size whether to add size annotations to container types + @param[in] use_type whether to add type annotations to container types + (must be combined with @a use_size = true) + @return UBJSON serialization as byte vector + + @complexity Linear in the size of the JSON value @a j. + + @liveexample{The example shows the serialization of a JSON value to a byte + vector in UBJSON format.,to_ubjson} + + @sa http://ubjson.org + @sa see @ref from_ubjson(InputType&&, const bool, const bool) for the + analogous deserialization + @sa see @ref to_cbor(const basic_json& for the related CBOR format + @sa see @ref to_msgpack(const basic_json&) for the related MessagePack format + + @since version 3.1.0 + */ + static std::vector to_ubjson(const basic_json& j, + const bool use_size = false, + const bool use_type = false) + { + std::vector result; + to_ubjson(j, result, use_size, use_type); + return result; + } + + static void to_ubjson(const basic_json& j, detail::output_adapter o, + const bool use_size = false, const bool use_type = false) + { + binary_writer(o).write_ubjson(j, use_size, use_type); + } + + static void to_ubjson(const basic_json& j, detail::output_adapter o, + const bool use_size = false, const bool use_type = false) + { + binary_writer(o).write_ubjson(j, use_size, use_type); + } + + + /*! + @brief Serializes the given JSON object `j` to BSON and returns a vector + containing the corresponding BSON-representation. + + BSON (Binary JSON) is a binary format in which zero or more ordered key/value pairs are + stored as a single entity (a so-called document). + + The library uses the following mapping from JSON values types to BSON types: + + JSON value type | value/range | BSON type | marker + --------------- | --------------------------------- | ----------- | ------ + null | `null` | null | 0x0A + boolean | `true`, `false` | boolean | 0x08 + number_integer | -9223372036854775808..-2147483649 | int64 | 0x12 + number_integer | -2147483648..2147483647 | int32 | 0x10 + number_integer | 2147483648..9223372036854775807 | int64 | 0x12 + number_unsigned | 0..2147483647 | int32 | 0x10 + number_unsigned | 2147483648..9223372036854775807 | int64 | 0x12 + number_unsigned | 9223372036854775808..18446744073709551615| -- | -- + number_float | *any value* | double | 0x01 + string | *any value* | string | 0x02 + array | *any value* | document | 0x04 + object | *any value* | document | 0x03 + binary | *any value* | binary | 0x05 + + @warning The mapping is **incomplete**, since only JSON-objects (and things + contained therein) can be serialized to BSON. + Also, integers larger than 9223372036854775807 cannot be serialized to BSON, + and the keys may not contain U+0000, since they are serialized a + zero-terminated c-strings. + + @throw out_of_range.407 if `j.is_number_unsigned() && j.get() > 9223372036854775807` + @throw out_of_range.409 if a key in `j` contains a NULL (U+0000) + @throw type_error.317 if `!j.is_object()` + + @pre The input `j` is required to be an object: `j.is_object() == true`. + + @note Any BSON output created via @ref to_bson can be successfully parsed + by @ref from_bson. + + @param[in] j JSON value to serialize + @return BSON serialization as byte vector + + @complexity Linear in the size of the JSON value @a j. + + @liveexample{The example shows the serialization of a JSON value to a byte + vector in BSON format.,to_bson} + + @sa http://bsonspec.org/spec.html + @sa see @ref from_bson(detail::input_adapter&&, const bool strict) for the + analogous deserialization + @sa see @ref to_ubjson(const basic_json&, const bool, const bool) for the + related UBJSON format + @sa see @ref to_cbor(const basic_json&) for the related CBOR format + @sa see @ref to_msgpack(const basic_json&) for the related MessagePack format + */ + static std::vector to_bson(const basic_json& j) + { + std::vector result; + to_bson(j, result); + return result; + } + + /*! + @brief Serializes the given JSON object `j` to BSON and forwards the + corresponding BSON-representation to the given output_adapter `o`. + @param j The JSON object to convert to BSON. + @param o The output adapter that receives the binary BSON representation. + @pre The input `j` shall be an object: `j.is_object() == true` + @sa see @ref to_bson(const basic_json&) + */ + static void to_bson(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bson(j); + } + + /*! + @copydoc to_bson(const basic_json&, detail::output_adapter) + */ + static void to_bson(const basic_json& j, detail::output_adapter o) + { + binary_writer(o).write_bson(j); + } + + + /*! + @brief create a JSON value from an input in CBOR format + + Deserializes a given input @a i to a JSON value using the CBOR (Concise + Binary Object Representation) serialization format. + + The library maps CBOR types to JSON value types as follows: + + CBOR type | JSON value type | first byte + ---------------------- | --------------- | ---------- + Integer | number_unsigned | 0x00..0x17 + Unsigned integer | number_unsigned | 0x18 + Unsigned integer | number_unsigned | 0x19 + Unsigned integer | number_unsigned | 0x1A + Unsigned integer | number_unsigned | 0x1B + Negative integer | number_integer | 0x20..0x37 + Negative integer | number_integer | 0x38 + Negative integer | number_integer | 0x39 + Negative integer | number_integer | 0x3A + Negative integer | number_integer | 0x3B + Byte string | binary | 0x40..0x57 + Byte string | binary | 0x58 + Byte string | binary | 0x59 + Byte string | binary | 0x5A + Byte string | binary | 0x5B + UTF-8 string | string | 0x60..0x77 + UTF-8 string | string | 0x78 + UTF-8 string | string | 0x79 + UTF-8 string | string | 0x7A + UTF-8 string | string | 0x7B + UTF-8 string | string | 0x7F + array | array | 0x80..0x97 + array | array | 0x98 + array | array | 0x99 + array | array | 0x9A + array | array | 0x9B + array | array | 0x9F + map | object | 0xA0..0xB7 + map | object | 0xB8 + map | object | 0xB9 + map | object | 0xBA + map | object | 0xBB + map | object | 0xBF + False | `false` | 0xF4 + True | `true` | 0xF5 + Null | `null` | 0xF6 + Half-Precision Float | number_float | 0xF9 + Single-Precision Float | number_float | 0xFA + Double-Precision Float | number_float | 0xFB + + @warning The mapping is **incomplete** in the sense that not all CBOR + types can be converted to a JSON value. The following CBOR types + are not supported and will yield parse errors (parse_error.112): + - date/time (0xC0..0xC1) + - bignum (0xC2..0xC3) + - decimal fraction (0xC4) + - bigfloat (0xC5) + - expected conversions (0xD5..0xD7) + - simple values (0xE0..0xF3, 0xF8) + - undefined (0xF7) + + @warning CBOR allows map keys of any type, whereas JSON only allows + strings as keys in object values. Therefore, CBOR maps with keys + other than UTF-8 strings are rejected (parse_error.113). + + @note Any CBOR output created @ref to_cbor can be successfully parsed by + @ref from_cbor. + + @param[in] i an input in CBOR format convertible to an input adapter + @param[in] strict whether to expect the input to be consumed until EOF + (true by default) + @param[in] allow_exceptions whether to throw exceptions in case of a + parse error (optional, true by default) + @param[in] tag_handler how to treat CBOR tags (optional, error by default) + + @return deserialized JSON value; in case of a parse error and + @a allow_exceptions set to `false`, the return value will be + value_t::discarded. + + @throw parse_error.110 if the given input ends prematurely or the end of + file was not reached when @a strict was set to true + @throw parse_error.112 if unsupported features from CBOR were + used in the given input @a v or if the input is not valid CBOR + @throw parse_error.113 if a string was expected as map key, but not found + + @complexity Linear in the size of the input @a i. + + @liveexample{The example shows the deserialization of a byte vector in CBOR + format to a JSON value.,from_cbor} + + @sa http://cbor.io + @sa see @ref to_cbor(const basic_json&) for the analogous serialization + @sa see @ref from_msgpack(InputType&&, const bool, const bool) for the + related MessagePack format + @sa see @ref from_ubjson(InputType&&, const bool, const bool) for the + related UBJSON format + + @since version 2.0.9; parameter @a start_index since 2.1.1; changed to + consume input adapters, removed start_index parameter, and added + @a strict parameter since 3.0.0; added @a allow_exceptions parameter + since 3.2.0; added @a tag_handler parameter since 3.9.0. + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_cbor(InputType&& i, + const bool strict = true, + const bool allow_exceptions = true, + const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = detail::input_adapter(std::forward(i)); + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::cbor, &sdp, strict, tag_handler); + return res ? result : basic_json(value_t::discarded); + } + + /*! + @copydoc from_cbor(InputType&&, const bool, const bool, const cbor_tag_handler_t) + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_cbor(IteratorType first, IteratorType last, + const bool strict = true, + const bool allow_exceptions = true, + const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = detail::input_adapter(std::move(first), std::move(last)); + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::cbor, &sdp, strict, tag_handler); + return res ? result : basic_json(value_t::discarded); + } + + template + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_cbor(ptr, ptr + len)) + static basic_json from_cbor(const T* ptr, std::size_t len, + const bool strict = true, + const bool allow_exceptions = true, + const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) + { + return from_cbor(ptr, ptr + len, strict, allow_exceptions, tag_handler); + } + + + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_cbor(ptr, ptr + len)) + static basic_json from_cbor(detail::span_input_adapter&& i, + const bool strict = true, + const bool allow_exceptions = true, + const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = i.get(); + // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::cbor, &sdp, strict, tag_handler); + return res ? result : basic_json(value_t::discarded); + } + + /*! + @brief create a JSON value from an input in MessagePack format + + Deserializes a given input @a i to a JSON value using the MessagePack + serialization format. + + The library maps MessagePack types to JSON value types as follows: + + MessagePack type | JSON value type | first byte + ---------------- | --------------- | ---------- + positive fixint | number_unsigned | 0x00..0x7F + fixmap | object | 0x80..0x8F + fixarray | array | 0x90..0x9F + fixstr | string | 0xA0..0xBF + nil | `null` | 0xC0 + false | `false` | 0xC2 + true | `true` | 0xC3 + float 32 | number_float | 0xCA + float 64 | number_float | 0xCB + uint 8 | number_unsigned | 0xCC + uint 16 | number_unsigned | 0xCD + uint 32 | number_unsigned | 0xCE + uint 64 | number_unsigned | 0xCF + int 8 | number_integer | 0xD0 + int 16 | number_integer | 0xD1 + int 32 | number_integer | 0xD2 + int 64 | number_integer | 0xD3 + str 8 | string | 0xD9 + str 16 | string | 0xDA + str 32 | string | 0xDB + array 16 | array | 0xDC + array 32 | array | 0xDD + map 16 | object | 0xDE + map 32 | object | 0xDF + bin 8 | binary | 0xC4 + bin 16 | binary | 0xC5 + bin 32 | binary | 0xC6 + ext 8 | binary | 0xC7 + ext 16 | binary | 0xC8 + ext 32 | binary | 0xC9 + fixext 1 | binary | 0xD4 + fixext 2 | binary | 0xD5 + fixext 4 | binary | 0xD6 + fixext 8 | binary | 0xD7 + fixext 16 | binary | 0xD8 + negative fixint | number_integer | 0xE0-0xFF + + @note Any MessagePack output created @ref to_msgpack can be successfully + parsed by @ref from_msgpack. + + @param[in] i an input in MessagePack format convertible to an input + adapter + @param[in] strict whether to expect the input to be consumed until EOF + (true by default) + @param[in] allow_exceptions whether to throw exceptions in case of a + parse error (optional, true by default) + + @return deserialized JSON value; in case of a parse error and + @a allow_exceptions set to `false`, the return value will be + value_t::discarded. + + @throw parse_error.110 if the given input ends prematurely or the end of + file was not reached when @a strict was set to true + @throw parse_error.112 if unsupported features from MessagePack were + used in the given input @a i or if the input is not valid MessagePack + @throw parse_error.113 if a string was expected as map key, but not found + + @complexity Linear in the size of the input @a i. + + @liveexample{The example shows the deserialization of a byte vector in + MessagePack format to a JSON value.,from_msgpack} + + @sa http://msgpack.org + @sa see @ref to_msgpack(const basic_json&) for the analogous serialization + @sa see @ref from_cbor(InputType&&, const bool, const bool, const cbor_tag_handler_t) for the + related CBOR format + @sa see @ref from_ubjson(InputType&&, const bool, const bool) for + the related UBJSON format + @sa see @ref from_bson(InputType&&, const bool, const bool) for + the related BSON format + + @since version 2.0.9; parameter @a start_index since 2.1.1; changed to + consume input adapters, removed start_index parameter, and added + @a strict parameter since 3.0.0; added @a allow_exceptions parameter + since 3.2.0 + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_msgpack(InputType&& i, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = detail::input_adapter(std::forward(i)); + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::msgpack, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + + /*! + @copydoc from_msgpack(InputType&&, const bool, const bool) + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_msgpack(IteratorType first, IteratorType last, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = detail::input_adapter(std::move(first), std::move(last)); + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::msgpack, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + + + template + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_msgpack(ptr, ptr + len)) + static basic_json from_msgpack(const T* ptr, std::size_t len, + const bool strict = true, + const bool allow_exceptions = true) + { + return from_msgpack(ptr, ptr + len, strict, allow_exceptions); + } + + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_msgpack(ptr, ptr + len)) + static basic_json from_msgpack(detail::span_input_adapter&& i, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = i.get(); + // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::msgpack, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + + + /*! + @brief create a JSON value from an input in UBJSON format + + Deserializes a given input @a i to a JSON value using the UBJSON (Universal + Binary JSON) serialization format. + + The library maps UBJSON types to JSON value types as follows: + + UBJSON type | JSON value type | marker + ----------- | --------------------------------------- | ------ + no-op | *no value, next value is read* | `N` + null | `null` | `Z` + false | `false` | `F` + true | `true` | `T` + float32 | number_float | `d` + float64 | number_float | `D` + uint8 | number_unsigned | `U` + int8 | number_integer | `i` + int16 | number_integer | `I` + int32 | number_integer | `l` + int64 | number_integer | `L` + high-precision number | number_integer, number_unsigned, or number_float - depends on number string | 'H' + string | string | `S` + char | string | `C` + array | array (optimized values are supported) | `[` + object | object (optimized values are supported) | `{` + + @note The mapping is **complete** in the sense that any UBJSON value can + be converted to a JSON value. + + @param[in] i an input in UBJSON format convertible to an input adapter + @param[in] strict whether to expect the input to be consumed until EOF + (true by default) + @param[in] allow_exceptions whether to throw exceptions in case of a + parse error (optional, true by default) + + @return deserialized JSON value; in case of a parse error and + @a allow_exceptions set to `false`, the return value will be + value_t::discarded. + + @throw parse_error.110 if the given input ends prematurely or the end of + file was not reached when @a strict was set to true + @throw parse_error.112 if a parse error occurs + @throw parse_error.113 if a string could not be parsed successfully + + @complexity Linear in the size of the input @a i. + + @liveexample{The example shows the deserialization of a byte vector in + UBJSON format to a JSON value.,from_ubjson} + + @sa http://ubjson.org + @sa see @ref to_ubjson(const basic_json&, const bool, const bool) for the + analogous serialization + @sa see @ref from_cbor(InputType&&, const bool, const bool, const cbor_tag_handler_t) for the + related CBOR format + @sa see @ref from_msgpack(InputType&&, const bool, const bool) for + the related MessagePack format + @sa see @ref from_bson(InputType&&, const bool, const bool) for + the related BSON format + + @since version 3.1.0; added @a allow_exceptions parameter since 3.2.0 + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_ubjson(InputType&& i, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = detail::input_adapter(std::forward(i)); + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::ubjson, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + + /*! + @copydoc from_ubjson(InputType&&, const bool, const bool) + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_ubjson(IteratorType first, IteratorType last, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = detail::input_adapter(std::move(first), std::move(last)); + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::ubjson, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + + template + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_ubjson(ptr, ptr + len)) + static basic_json from_ubjson(const T* ptr, std::size_t len, + const bool strict = true, + const bool allow_exceptions = true) + { + return from_ubjson(ptr, ptr + len, strict, allow_exceptions); + } + + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_ubjson(ptr, ptr + len)) + static basic_json from_ubjson(detail::span_input_adapter&& i, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = i.get(); + // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::ubjson, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + + + /*! + @brief Create a JSON value from an input in BSON format + + Deserializes a given input @a i to a JSON value using the BSON (Binary JSON) + serialization format. + + The library maps BSON record types to JSON value types as follows: + + BSON type | BSON marker byte | JSON value type + --------------- | ---------------- | --------------------------- + double | 0x01 | number_float + string | 0x02 | string + document | 0x03 | object + array | 0x04 | array + binary | 0x05 | binary + undefined | 0x06 | still unsupported + ObjectId | 0x07 | still unsupported + boolean | 0x08 | boolean + UTC Date-Time | 0x09 | still unsupported + null | 0x0A | null + Regular Expr. | 0x0B | still unsupported + DB Pointer | 0x0C | still unsupported + JavaScript Code | 0x0D | still unsupported + Symbol | 0x0E | still unsupported + JavaScript Code | 0x0F | still unsupported + int32 | 0x10 | number_integer + Timestamp | 0x11 | still unsupported + 128-bit decimal float | 0x13 | still unsupported + Max Key | 0x7F | still unsupported + Min Key | 0xFF | still unsupported + + @warning The mapping is **incomplete**. The unsupported mappings + are indicated in the table above. + + @param[in] i an input in BSON format convertible to an input adapter + @param[in] strict whether to expect the input to be consumed until EOF + (true by default) + @param[in] allow_exceptions whether to throw exceptions in case of a + parse error (optional, true by default) + + @return deserialized JSON value; in case of a parse error and + @a allow_exceptions set to `false`, the return value will be + value_t::discarded. + + @throw parse_error.114 if an unsupported BSON record type is encountered + + @complexity Linear in the size of the input @a i. + + @liveexample{The example shows the deserialization of a byte vector in + BSON format to a JSON value.,from_bson} + + @sa http://bsonspec.org/spec.html + @sa see @ref to_bson(const basic_json&) for the analogous serialization + @sa see @ref from_cbor(InputType&&, const bool, const bool, const cbor_tag_handler_t) for the + related CBOR format + @sa see @ref from_msgpack(InputType&&, const bool, const bool) for + the related MessagePack format + @sa see @ref from_ubjson(InputType&&, const bool, const bool) for the + related UBJSON format + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_bson(InputType&& i, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = detail::input_adapter(std::forward(i)); + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::bson, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + + /*! + @copydoc from_bson(InputType&&, const bool, const bool) + */ + template + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json from_bson(IteratorType first, IteratorType last, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = detail::input_adapter(std::move(first), std::move(last)); + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::bson, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + + template + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_bson(ptr, ptr + len)) + static basic_json from_bson(const T* ptr, std::size_t len, + const bool strict = true, + const bool allow_exceptions = true) + { + return from_bson(ptr, ptr + len, strict, allow_exceptions); + } + + JSON_HEDLEY_WARN_UNUSED_RESULT + JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_bson(ptr, ptr + len)) + static basic_json from_bson(detail::span_input_adapter&& i, + const bool strict = true, + const bool allow_exceptions = true) + { + basic_json result; + detail::json_sax_dom_parser sdp(result, allow_exceptions); + auto ia = i.get(); + // NOLINTNEXTLINE(hicpp-move-const-arg,performance-move-const-arg) + const bool res = binary_reader(std::move(ia)).sax_parse(input_format_t::bson, &sdp, strict); + return res ? result : basic_json(value_t::discarded); + } + /// @} + + ////////////////////////// + // JSON Pointer support // + ////////////////////////// + + /// @name JSON Pointer functions + /// @{ + + /*! + @brief access specified element via JSON Pointer + + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. Similar to @ref operator[](const typename + object_t::key_type&), `null` values are created in arrays and objects if + necessary. + + In particular: + - If the JSON pointer points to an object key that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. + - If the JSON pointer points to an array index that does not exist, it + is created an filled with a `null` value before a reference to it + is returned. All indices between the current maximum and the given + index are also filled with `null`. + - The special value `-` is treated as a synonym for the index past the + end. + + @param[in] ptr a JSON pointer + + @return reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.404 if the JSON pointer can not be resolved + + @liveexample{The behavior is shown in the example.,operatorjson_pointer} + + @since version 2.0.0 + */ + reference operator[](const json_pointer& ptr) + { + return ptr.get_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Uses a JSON pointer to retrieve a reference to the respective JSON value. + No bound checking is performed. The function does not change the JSON + value; no `null` values are created. In particular, the special value + `-` yields an exception. + + @param[in] ptr JSON pointer to the desired element + + @return const reference to the element pointed to by @a ptr + + @complexity Constant. + + @throw parse_error.106 if an array index begins with '0' + @throw parse_error.109 if an array index was not a number + @throw out_of_range.402 if the array index '-' is used + @throw out_of_range.404 if the JSON pointer can not be resolved + + @liveexample{The behavior is shown in the example.,operatorjson_pointer_const} + + @since version 2.0.0 + */ + const_reference operator[](const json_pointer& ptr) const + { + return ptr.get_unchecked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a reference to the element at with specified JSON pointer @a ptr, + with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @throw parse_error.106 if an array index in the passed JSON pointer @a ptr + begins with '0'. See example below. + + @throw parse_error.109 if an array index in the passed JSON pointer @a ptr + is not a number. See example below. + + @throw out_of_range.401 if an array index in the passed JSON pointer @a ptr + is out of range. See example below. + + @throw out_of_range.402 if the array index '-' is used in the passed JSON + pointer @a ptr. As `at` provides checked access (and no elements are + implicitly inserted), the index '-' is always invalid. See example below. + + @throw out_of_range.403 if the JSON pointer describes a key of an object + which cannot be found. See example below. + + @throw out_of_range.404 if the JSON pointer @a ptr can not be resolved. + See example below. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @complexity Constant. + + @since version 2.0.0 + + @liveexample{The behavior is shown in the example.,at_json_pointer} + */ + reference at(const json_pointer& ptr) + { + return ptr.get_checked(this); + } + + /*! + @brief access specified element via JSON Pointer + + Returns a const reference to the element at with specified JSON pointer @a + ptr, with bounds checking. + + @param[in] ptr JSON pointer to the desired element + + @return reference to the element pointed to by @a ptr + + @throw parse_error.106 if an array index in the passed JSON pointer @a ptr + begins with '0'. See example below. + + @throw parse_error.109 if an array index in the passed JSON pointer @a ptr + is not a number. See example below. + + @throw out_of_range.401 if an array index in the passed JSON pointer @a ptr + is out of range. See example below. + + @throw out_of_range.402 if the array index '-' is used in the passed JSON + pointer @a ptr. As `at` provides checked access (and no elements are + implicitly inserted), the index '-' is always invalid. See example below. + + @throw out_of_range.403 if the JSON pointer describes a key of an object + which cannot be found. See example below. + + @throw out_of_range.404 if the JSON pointer @a ptr can not be resolved. + See example below. + + @exceptionsafety Strong guarantee: if an exception is thrown, there are no + changes in the JSON value. + + @complexity Constant. + + @since version 2.0.0 + + @liveexample{The behavior is shown in the example.,at_json_pointer_const} + */ + const_reference at(const json_pointer& ptr) const + { + return ptr.get_checked(this); + } + + /*! + @brief return flattened JSON value + + The function creates a JSON object whose keys are JSON pointers (see [RFC + 6901](https://tools.ietf.org/html/rfc6901)) and whose values are all + primitive. The original JSON value can be restored using the @ref + unflatten() function. + + @return an object that maps JSON pointers to primitive values + + @note Empty objects and arrays are flattened to `null` and will not be + reconstructed correctly by the @ref unflatten() function. + + @complexity Linear in the size the JSON value. + + @liveexample{The following code shows how a JSON object is flattened to an + object whose keys consist of JSON pointers.,flatten} + + @sa see @ref unflatten() for the reverse function + + @since version 2.0.0 + */ + basic_json flatten() const + { + basic_json result(value_t::object); + json_pointer::flatten("", *this, result); + return result; + } + + /*! + @brief unflatten a previously flattened JSON value + + The function restores the arbitrary nesting of a JSON value that has been + flattened before using the @ref flatten() function. The JSON value must + meet certain constraints: + 1. The value must be an object. + 2. The keys must be JSON pointers (see + [RFC 6901](https://tools.ietf.org/html/rfc6901)) + 3. The mapped values must be primitive JSON types. + + @return the original JSON from a flattened version + + @note Empty objects and arrays are flattened by @ref flatten() to `null` + values and can not unflattened to their original type. Apart from + this example, for a JSON value `j`, the following is always true: + `j == j.flatten().unflatten()`. + + @complexity Linear in the size the JSON value. + + @throw type_error.314 if value is not an object + @throw type_error.315 if object values are not primitive + + @liveexample{The following code shows how a flattened JSON object is + unflattened into the original nested JSON object.,unflatten} + + @sa see @ref flatten() for the reverse function + + @since version 2.0.0 + */ + basic_json unflatten() const + { + return json_pointer::unflatten(*this); + } + + /// @} + + ////////////////////////// + // JSON Patch functions // + ////////////////////////// + + /// @name JSON Patch functions + /// @{ + + /*! + @brief applies a JSON patch + + [JSON Patch](http://jsonpatch.com) defines a JSON document structure for + expressing a sequence of operations to apply to a JSON) document. With + this function, a JSON Patch is applied to the current JSON value by + executing all operations from the patch. + + @param[in] json_patch JSON patch document + @return patched document + + @note The application of a patch is atomic: Either all operations succeed + and the patched document is returned or an exception is thrown. In + any case, the original value is not changed: the patch is applied + to a copy of the value. + + @throw parse_error.104 if the JSON patch does not consist of an array of + objects + + @throw parse_error.105 if the JSON patch is malformed (e.g., mandatory + attributes are missing); example: `"operation add must have member path"` + + @throw out_of_range.401 if an array index is out of range. + + @throw out_of_range.403 if a JSON pointer inside the patch could not be + resolved successfully in the current JSON value; example: `"key baz not + found"` + + @throw out_of_range.405 if JSON pointer has no parent ("add", "remove", + "move") + + @throw other_error.501 if "test" operation was unsuccessful + + @complexity Linear in the size of the JSON value and the length of the + JSON patch. As usually only a fraction of the JSON value is affected by + the patch, the complexity can usually be neglected. + + @liveexample{The following code shows how a JSON patch is applied to a + value.,patch} + + @sa see @ref diff -- create a JSON patch by comparing two JSON values + + @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) + @sa [RFC 6901 (JSON Pointer)](https://tools.ietf.org/html/rfc6901) + + @since version 2.0.0 + */ + basic_json patch(const basic_json& json_patch) const + { + // make a working copy to apply the patch to + basic_json result = *this; + + // the valid JSON Patch operations + enum class patch_operations {add, remove, replace, move, copy, test, invalid}; + + const auto get_op = [](const std::string & op) + { + if (op == "add") + { + return patch_operations::add; + } + if (op == "remove") + { + return patch_operations::remove; + } + if (op == "replace") + { + return patch_operations::replace; + } + if (op == "move") + { + return patch_operations::move; + } + if (op == "copy") + { + return patch_operations::copy; + } + if (op == "test") + { + return patch_operations::test; + } + + return patch_operations::invalid; + }; + + // wrapper for "add" operation; add value at ptr + const auto operation_add = [&result](json_pointer & ptr, basic_json val) + { + // adding to the root of the target document means replacing it + if (ptr.empty()) + { + result = val; + return; + } + + // make sure the top element of the pointer exists + json_pointer top_pointer = ptr.top(); + if (top_pointer != ptr) + { + result.at(top_pointer); + } + + // get reference to parent of JSON pointer ptr + const auto last_path = ptr.back(); + ptr.pop_back(); + basic_json& parent = result[ptr]; + + switch (parent.m_type) + { + case value_t::null: + case value_t::object: + { + // use operator[] to add value + parent[last_path] = val; + break; + } + + case value_t::array: + { + if (last_path == "-") + { + // special case: append to back + parent.push_back(val); + } + else + { + const auto idx = json_pointer::array_index(last_path); + if (JSON_HEDLEY_UNLIKELY(idx > parent.size())) + { + // avoid undefined behavior + JSON_THROW(out_of_range::create(401, "array index " + std::to_string(idx) + " is out of range", parent)); + } + + // default case: insert add offset + parent.insert(parent.begin() + static_cast(idx), val); + } + break; + } + + // if there exists a parent it cannot be primitive + case value_t::string: // LCOV_EXCL_LINE + case value_t::boolean: // LCOV_EXCL_LINE + case value_t::number_integer: // LCOV_EXCL_LINE + case value_t::number_unsigned: // LCOV_EXCL_LINE + case value_t::number_float: // LCOV_EXCL_LINE + case value_t::binary: // LCOV_EXCL_LINE + case value_t::discarded: // LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE + } + }; + + // wrapper for "remove" operation; remove value at ptr + const auto operation_remove = [this, &result](json_pointer & ptr) + { + // get reference to parent of JSON pointer ptr + const auto last_path = ptr.back(); + ptr.pop_back(); + basic_json& parent = result.at(ptr); + + // remove child + if (parent.is_object()) + { + // perform range check + auto it = parent.find(last_path); + if (JSON_HEDLEY_LIKELY(it != parent.end())) + { + parent.erase(it); + } + else + { + JSON_THROW(out_of_range::create(403, "key '" + last_path + "' not found", *this)); + } + } + else if (parent.is_array()) + { + // note erase performs range check + parent.erase(json_pointer::array_index(last_path)); + } + }; + + // type check: top level value must be an array + if (JSON_HEDLEY_UNLIKELY(!json_patch.is_array())) + { + JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects", json_patch)); + } + + // iterate and apply the operations + for (const auto& val : json_patch) + { + // wrapper to get a value for an operation + const auto get_value = [&val](const std::string & op, + const std::string & member, + bool string_type) -> basic_json & + { + // find value + auto it = val.m_value.object->find(member); + + // context-sensitive error message + const auto error_msg = (op == "op") ? "operation" : "operation '" + op + "'"; + + // check if desired value is present + if (JSON_HEDLEY_UNLIKELY(it == val.m_value.object->end())) + { + // NOLINTNEXTLINE(performance-inefficient-string-concatenation) + JSON_THROW(parse_error::create(105, 0, error_msg + " must have member '" + member + "'", val)); + } + + // check if result is of type string + if (JSON_HEDLEY_UNLIKELY(string_type && !it->second.is_string())) + { + // NOLINTNEXTLINE(performance-inefficient-string-concatenation) + JSON_THROW(parse_error::create(105, 0, error_msg + " must have string member '" + member + "'", val)); + } + + // no error: return value + return it->second; + }; + + // type check: every element of the array must be an object + if (JSON_HEDLEY_UNLIKELY(!val.is_object())) + { + JSON_THROW(parse_error::create(104, 0, "JSON patch must be an array of objects", val)); + } + + // collect mandatory members + const auto op = get_value("op", "op", true).template get(); + const auto path = get_value(op, "path", true).template get(); + json_pointer ptr(path); + + switch (get_op(op)) + { + case patch_operations::add: + { + operation_add(ptr, get_value("add", "value", false)); + break; + } + + case patch_operations::remove: + { + operation_remove(ptr); + break; + } + + case patch_operations::replace: + { + // the "path" location must exist - use at() + result.at(ptr) = get_value("replace", "value", false); + break; + } + + case patch_operations::move: + { + const auto from_path = get_value("move", "from", true).template get(); + json_pointer from_ptr(from_path); + + // the "from" location must exist - use at() + basic_json v = result.at(from_ptr); + + // The move operation is functionally identical to a + // "remove" operation on the "from" location, followed + // immediately by an "add" operation at the target + // location with the value that was just removed. + operation_remove(from_ptr); + operation_add(ptr, v); + break; + } + + case patch_operations::copy: + { + const auto from_path = get_value("copy", "from", true).template get(); + const json_pointer from_ptr(from_path); + + // the "from" location must exist - use at() + basic_json v = result.at(from_ptr); + + // The copy is functionally identical to an "add" + // operation at the target location using the value + // specified in the "from" member. + operation_add(ptr, v); + break; + } + + case patch_operations::test: + { + bool success = false; + JSON_TRY + { + // check if "value" matches the one at "path" + // the "path" location must exist - use at() + success = (result.at(ptr) == get_value("test", "value", false)); + } + JSON_INTERNAL_CATCH (out_of_range&) + { + // ignore out of range errors: success remains false + } + + // throw an exception if test fails + if (JSON_HEDLEY_UNLIKELY(!success)) + { + JSON_THROW(other_error::create(501, "unsuccessful: " + val.dump(), val)); + } + + break; + } + + case patch_operations::invalid: + default: + { + // op must be "add", "remove", "replace", "move", "copy", or + // "test" + JSON_THROW(parse_error::create(105, 0, "operation value '" + op + "' is invalid", val)); + } + } + } + + return result; + } + + /*! + @brief creates a diff as a JSON patch + + Creates a [JSON Patch](http://jsonpatch.com) so that value @a source can + be changed into the value @a target by calling @ref patch function. + + @invariant For two JSON values @a source and @a target, the following code + yields always `true`: + @code {.cpp} + source.patch(diff(source, target)) == target; + @endcode + + @note Currently, only `remove`, `add`, and `replace` operations are + generated. + + @param[in] source JSON value to compare from + @param[in] target JSON value to compare against + @param[in] path helper value to create JSON pointers + + @return a JSON patch to convert the @a source to @a target + + @complexity Linear in the lengths of @a source and @a target. + + @liveexample{The following code shows how a JSON patch is created as a + diff for two JSON values.,diff} + + @sa see @ref patch -- apply a JSON patch + @sa see @ref merge_patch -- apply a JSON Merge Patch + + @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) + + @since version 2.0.0 + */ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json diff(const basic_json& source, const basic_json& target, + const std::string& path = "") + { + // the patch + basic_json result(value_t::array); + + // if the values are the same, return empty patch + if (source == target) + { + return result; + } + + if (source.type() != target.type()) + { + // different types: replace value + result.push_back( + { + {"op", "replace"}, {"path", path}, {"value", target} + }); + return result; + } + + switch (source.type()) + { + case value_t::array: + { + // first pass: traverse common elements + std::size_t i = 0; + while (i < source.size() && i < target.size()) + { + // recursive call to compare array values at index i + auto temp_diff = diff(source[i], target[i], path + "/" + std::to_string(i)); + result.insert(result.end(), temp_diff.begin(), temp_diff.end()); + ++i; + } + + // i now reached the end of at least one array + // in a second pass, traverse the remaining elements + + // remove my remaining elements + const auto end_index = static_cast(result.size()); + while (i < source.size()) + { + // add operations in reverse order to avoid invalid + // indices + result.insert(result.begin() + end_index, object( + { + {"op", "remove"}, + {"path", path + "/" + std::to_string(i)} + })); + ++i; + } + + // add other remaining elements + while (i < target.size()) + { + result.push_back( + { + {"op", "add"}, + {"path", path + "/-"}, + {"value", target[i]} + }); + ++i; + } + + break; + } + + case value_t::object: + { + // first pass: traverse this object's elements + for (auto it = source.cbegin(); it != source.cend(); ++it) + { + // escape the key name to be used in a JSON patch + const auto path_key = path + "/" + detail::escape(it.key()); + + if (target.find(it.key()) != target.end()) + { + // recursive call to compare object values at key it + auto temp_diff = diff(it.value(), target[it.key()], path_key); + result.insert(result.end(), temp_diff.begin(), temp_diff.end()); + } + else + { + // found a key that is not in o -> remove it + result.push_back(object( + { + {"op", "remove"}, {"path", path_key} + })); + } + } + + // second pass: traverse other object's elements + for (auto it = target.cbegin(); it != target.cend(); ++it) + { + if (source.find(it.key()) == source.end()) + { + // found a key that is not in this -> add it + const auto path_key = path + "/" + detail::escape(it.key()); + result.push_back( + { + {"op", "add"}, {"path", path_key}, + {"value", it.value()} + }); + } + } + + break; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + // both primitive type: replace value + result.push_back( + { + {"op", "replace"}, {"path", path}, {"value", target} + }); + break; + } + } + + return result; + } + + /// @} + + //////////////////////////////// + // JSON Merge Patch functions // + //////////////////////////////// + + /// @name JSON Merge Patch functions + /// @{ + + /*! + @brief applies a JSON Merge Patch + + The merge patch format is primarily intended for use with the HTTP PATCH + method as a means of describing a set of modifications to a target + resource's content. This function applies a merge patch to the current + JSON value. + + The function implements the following algorithm from Section 2 of + [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396): + + ``` + define MergePatch(Target, Patch): + if Patch is an Object: + if Target is not an Object: + Target = {} // Ignore the contents and set it to an empty Object + for each Name/Value pair in Patch: + if Value is null: + if Name exists in Target: + remove the Name/Value pair from Target + else: + Target[Name] = MergePatch(Target[Name], Value) + return Target + else: + return Patch + ``` + + Thereby, `Target` is the current object; that is, the patch is applied to + the current value. + + @param[in] apply_patch the patch to apply + + @complexity Linear in the lengths of @a patch. + + @liveexample{The following code shows how a JSON Merge Patch is applied to + a JSON document.,merge_patch} + + @sa see @ref patch -- apply a JSON patch + @sa [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396) + + @since version 3.0.0 + */ + void merge_patch(const basic_json& apply_patch) + { + if (apply_patch.is_object()) + { + if (!is_object()) + { + *this = object(); + } + for (auto it = apply_patch.begin(); it != apply_patch.end(); ++it) + { + if (it.value().is_null()) + { + erase(it.key()); + } + else + { + operator[](it.key()).merge_patch(it.value()); + } + } + } + else + { + *this = apply_patch; + } + } + + /// @} +}; + +/*! +@brief user-defined to_string function for JSON values + +This function implements a user-defined to_string for JSON objects. + +@param[in] j a JSON object +@return a std::string object +*/ + +NLOHMANN_BASIC_JSON_TPL_DECLARATION +std::string to_string(const NLOHMANN_BASIC_JSON_TPL& j) +{ + return j.dump(); +} +} // namespace nlohmann + +/////////////////////// +// nonmember support // +/////////////////////// + +// specialization of std::swap, and std::hash +namespace std +{ + +/// hash value for JSON objects +template<> +struct hash +{ + /*! + @brief return a hash value for a JSON object + + @since version 1.0.0 + */ + std::size_t operator()(const nlohmann::json& j) const + { + return nlohmann::detail::hash(j); + } +}; + +/// specialization for std::less +/// @note: do not remove the space after '<', +/// see https://github.com/nlohmann/json/pull/679 +template<> +struct less<::nlohmann::detail::value_t> +{ + /*! + @brief compare two value_t enum values + @since version 3.0.0 + */ + bool operator()(nlohmann::detail::value_t lhs, + nlohmann::detail::value_t rhs) const noexcept + { + return nlohmann::detail::operator<(lhs, rhs); + } +}; + +// C++20 prohibit function specialization in the std namespace. +#ifndef JSON_HAS_CPP_20 + +/*! +@brief exchanges the values of two JSON objects + +@since version 1.0.0 +*/ +template<> +inline void swap(nlohmann::json& j1, nlohmann::json& j2) noexcept( // NOLINT(readability-inconsistent-declaration-parameter-name) + is_nothrow_move_constructible::value&& // NOLINT(misc-redundant-expression) + is_nothrow_move_assignable::value + ) +{ + j1.swap(j2); +} + +#endif + +} // namespace std + +/*! +@brief user-defined string literal for JSON values + +This operator implements a user-defined string literal for JSON objects. It +can be used by adding `"_json"` to a string literal and returns a JSON object +if no parse error occurred. + +@param[in] s a string representation of a JSON object +@param[in] n the length of string @a s +@return a JSON object + +@since version 1.0.0 +*/ +JSON_HEDLEY_NON_NULL(1) +inline nlohmann::json operator "" _json(const char* s, std::size_t n) +{ + return nlohmann::json::parse(s, s + n); +} + +/*! +@brief user-defined string literal for JSON pointer + +This operator implements a user-defined string literal for JSON Pointers. It +can be used by adding `"_json_pointer"` to a string literal and returns a JSON pointer +object if no parse error occurred. + +@param[in] s a string representation of a JSON Pointer +@param[in] n the length of string @a s +@return a JSON pointer object + +@since version 2.0.0 +*/ +JSON_HEDLEY_NON_NULL(1) +inline nlohmann::json::json_pointer operator "" _json_pointer(const char* s, std::size_t n) +{ + return nlohmann::json::json_pointer(std::string(s, n)); +} + +// #include + + +// restore clang diagnostic settings +#if defined(__clang__) + #pragma clang diagnostic pop +#endif + +// clean up +#undef JSON_ASSERT +#undef JSON_INTERNAL_CATCH +#undef JSON_CATCH +#undef JSON_THROW +#undef JSON_TRY +#undef JSON_PRIVATE_UNLESS_TESTED +#undef JSON_HAS_CPP_11 +#undef JSON_HAS_CPP_14 +#undef JSON_HAS_CPP_17 +#undef JSON_HAS_CPP_20 +#undef NLOHMANN_BASIC_JSON_TPL_DECLARATION +#undef NLOHMANN_BASIC_JSON_TPL +#undef JSON_EXPLICIT +#undef NLOHMANN_CAN_CALL_STD_FUNC_IMPL + +// #include + + +#undef JSON_HEDLEY_ALWAYS_INLINE +#undef JSON_HEDLEY_ARM_VERSION +#undef JSON_HEDLEY_ARM_VERSION_CHECK +#undef JSON_HEDLEY_ARRAY_PARAM +#undef JSON_HEDLEY_ASSUME +#undef JSON_HEDLEY_BEGIN_C_DECLS +#undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE +#undef JSON_HEDLEY_CLANG_HAS_BUILTIN +#undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE +#undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE +#undef JSON_HEDLEY_CLANG_HAS_EXTENSION +#undef JSON_HEDLEY_CLANG_HAS_FEATURE +#undef JSON_HEDLEY_CLANG_HAS_WARNING +#undef JSON_HEDLEY_COMPCERT_VERSION +#undef JSON_HEDLEY_COMPCERT_VERSION_CHECK +#undef JSON_HEDLEY_CONCAT +#undef JSON_HEDLEY_CONCAT3 +#undef JSON_HEDLEY_CONCAT3_EX +#undef JSON_HEDLEY_CONCAT_EX +#undef JSON_HEDLEY_CONST +#undef JSON_HEDLEY_CONSTEXPR +#undef JSON_HEDLEY_CONST_CAST +#undef JSON_HEDLEY_CPP_CAST +#undef JSON_HEDLEY_CRAY_VERSION +#undef JSON_HEDLEY_CRAY_VERSION_CHECK +#undef JSON_HEDLEY_C_DECL +#undef JSON_HEDLEY_DEPRECATED +#undef JSON_HEDLEY_DEPRECATED_FOR +#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ +#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#undef JSON_HEDLEY_DIAGNOSTIC_POP +#undef JSON_HEDLEY_DIAGNOSTIC_PUSH +#undef JSON_HEDLEY_DMC_VERSION +#undef JSON_HEDLEY_DMC_VERSION_CHECK +#undef JSON_HEDLEY_EMPTY_BASES +#undef JSON_HEDLEY_EMSCRIPTEN_VERSION +#undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK +#undef JSON_HEDLEY_END_C_DECLS +#undef JSON_HEDLEY_FLAGS +#undef JSON_HEDLEY_FLAGS_CAST +#undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE +#undef JSON_HEDLEY_GCC_HAS_BUILTIN +#undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE +#undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE +#undef JSON_HEDLEY_GCC_HAS_EXTENSION +#undef JSON_HEDLEY_GCC_HAS_FEATURE +#undef JSON_HEDLEY_GCC_HAS_WARNING +#undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK +#undef JSON_HEDLEY_GCC_VERSION +#undef JSON_HEDLEY_GCC_VERSION_CHECK +#undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE +#undef JSON_HEDLEY_GNUC_HAS_BUILTIN +#undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE +#undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE +#undef JSON_HEDLEY_GNUC_HAS_EXTENSION +#undef JSON_HEDLEY_GNUC_HAS_FEATURE +#undef JSON_HEDLEY_GNUC_HAS_WARNING +#undef JSON_HEDLEY_GNUC_VERSION +#undef JSON_HEDLEY_GNUC_VERSION_CHECK +#undef JSON_HEDLEY_HAS_ATTRIBUTE +#undef JSON_HEDLEY_HAS_BUILTIN +#undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE +#undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS +#undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE +#undef JSON_HEDLEY_HAS_EXTENSION +#undef JSON_HEDLEY_HAS_FEATURE +#undef JSON_HEDLEY_HAS_WARNING +#undef JSON_HEDLEY_IAR_VERSION +#undef JSON_HEDLEY_IAR_VERSION_CHECK +#undef JSON_HEDLEY_IBM_VERSION +#undef JSON_HEDLEY_IBM_VERSION_CHECK +#undef JSON_HEDLEY_IMPORT +#undef JSON_HEDLEY_INLINE +#undef JSON_HEDLEY_INTEL_CL_VERSION +#undef JSON_HEDLEY_INTEL_CL_VERSION_CHECK +#undef JSON_HEDLEY_INTEL_VERSION +#undef JSON_HEDLEY_INTEL_VERSION_CHECK +#undef JSON_HEDLEY_IS_CONSTANT +#undef JSON_HEDLEY_IS_CONSTEXPR_ +#undef JSON_HEDLEY_LIKELY +#undef JSON_HEDLEY_MALLOC +#undef JSON_HEDLEY_MCST_LCC_VERSION +#undef JSON_HEDLEY_MCST_LCC_VERSION_CHECK +#undef JSON_HEDLEY_MESSAGE +#undef JSON_HEDLEY_MSVC_VERSION +#undef JSON_HEDLEY_MSVC_VERSION_CHECK +#undef JSON_HEDLEY_NEVER_INLINE +#undef JSON_HEDLEY_NON_NULL +#undef JSON_HEDLEY_NO_ESCAPE +#undef JSON_HEDLEY_NO_RETURN +#undef JSON_HEDLEY_NO_THROW +#undef JSON_HEDLEY_NULL +#undef JSON_HEDLEY_PELLES_VERSION +#undef JSON_HEDLEY_PELLES_VERSION_CHECK +#undef JSON_HEDLEY_PGI_VERSION +#undef JSON_HEDLEY_PGI_VERSION_CHECK +#undef JSON_HEDLEY_PREDICT +#undef JSON_HEDLEY_PRINTF_FORMAT +#undef JSON_HEDLEY_PRIVATE +#undef JSON_HEDLEY_PUBLIC +#undef JSON_HEDLEY_PURE +#undef JSON_HEDLEY_REINTERPRET_CAST +#undef JSON_HEDLEY_REQUIRE +#undef JSON_HEDLEY_REQUIRE_CONSTEXPR +#undef JSON_HEDLEY_REQUIRE_MSG +#undef JSON_HEDLEY_RESTRICT +#undef JSON_HEDLEY_RETURNS_NON_NULL +#undef JSON_HEDLEY_SENTINEL +#undef JSON_HEDLEY_STATIC_ASSERT +#undef JSON_HEDLEY_STATIC_CAST +#undef JSON_HEDLEY_STRINGIFY +#undef JSON_HEDLEY_STRINGIFY_EX +#undef JSON_HEDLEY_SUNPRO_VERSION +#undef JSON_HEDLEY_SUNPRO_VERSION_CHECK +#undef JSON_HEDLEY_TINYC_VERSION +#undef JSON_HEDLEY_TINYC_VERSION_CHECK +#undef JSON_HEDLEY_TI_ARMCL_VERSION +#undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK +#undef JSON_HEDLEY_TI_CL2000_VERSION +#undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK +#undef JSON_HEDLEY_TI_CL430_VERSION +#undef JSON_HEDLEY_TI_CL430_VERSION_CHECK +#undef JSON_HEDLEY_TI_CL6X_VERSION +#undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK +#undef JSON_HEDLEY_TI_CL7X_VERSION +#undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK +#undef JSON_HEDLEY_TI_CLPRU_VERSION +#undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK +#undef JSON_HEDLEY_TI_VERSION +#undef JSON_HEDLEY_TI_VERSION_CHECK +#undef JSON_HEDLEY_UNAVAILABLE +#undef JSON_HEDLEY_UNLIKELY +#undef JSON_HEDLEY_UNPREDICTABLE +#undef JSON_HEDLEY_UNREACHABLE +#undef JSON_HEDLEY_UNREACHABLE_RETURN +#undef JSON_HEDLEY_VERSION +#undef JSON_HEDLEY_VERSION_DECODE_MAJOR +#undef JSON_HEDLEY_VERSION_DECODE_MINOR +#undef JSON_HEDLEY_VERSION_DECODE_REVISION +#undef JSON_HEDLEY_VERSION_ENCODE +#undef JSON_HEDLEY_WARNING +#undef JSON_HEDLEY_WARN_UNUSED_RESULT +#undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG +#undef JSON_HEDLEY_FALL_THROUGH + + + +#endif // INCLUDE_NLOHMANN_JSON_HPP_ \ No newline at end of file diff --git a/src/viewer3d/gltf/stb_image.h b/src/viewer3d/gltf/stb_image.h new file mode 100644 index 00000000..e9278075 --- /dev/null +++ b/src/viewer3d/gltf/stb_image.h @@ -0,0 +1,8653 @@ +/* stb_image - v2.21 - public domain image loader - http://nothings.org/stb + no warranty implied; use at your own risk + + Do this: + #define STB_IMAGE_IMPLEMENTATION + before you include this file in *one* C or C++ file to create the +implementation. + + // i.e. it should look like this: + #include ... + #include ... + #include ... + #define STB_IMAGE_IMPLEMENTATION + #include "stb_image.h" + + You can #define STBI_ASSERT(x) before the #include to avoid using assert.h. + And #define STBI_MALLOC, STBI_REALLOC, and STBI_FREE to avoid using +malloc,realloc,free + + + QUICK NOTES: + Primarily of interest to game developers and other people who can + avoid problematic images and only need the trivial interface + + JPEG baseline & progressive (12 bpc/arithmetic not supported, same as +stock IJG lib) PNG 1/2/4/8/16-bit-per-channel + + TGA (not sure what subset, if a subset) + BMP non-1bpp, non-RLE + PSD (composited view only, no extra channels, 8/16 bit-per-channel) + + GIF (*comp always reports as 4-channel) + HDR (radiance rgbE format) + PIC (Softimage PIC) + PNM (PPM and PGM binary only) + + Animated GIF still needs a proper API, but here's one way to do it: + http://gist.github.com/urraka/685d9a6340b26b830d49 + + - decode from memory or through FILE (define STBI_NO_STDIO to remove code) + - decode from arbitrary I/O callbacks + - SIMD acceleration on x86/x64 (SSE2) and ARM (NEON) + + Full documentation under "DOCUMENTATION" below. + + +LICENSE + + See end of file for license information. + +RECENT REVISION HISTORY: + + 2.21 (2019-02-25) fix typo in comment + 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and +platform ifdefs 2.19 (2018-02-11) fix warning 2.18 (2018-01-30) fix warnings + 2.17 (2018-01-29) bugfix, 1-bit BMP, 16-bitness query, fix warnings + 2.16 (2017-07-23) all functions have 16-bit variants; optimizations; +bugfixes 2.15 (2017-03-18) fix png-1,2,4; all Imagenet JPGs; no runtime SSE +detection on GCC 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for +Imagenet JPGs 2.13 (2016-12-04) experimental 16-bit API, only for PNG so far; +fixes 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes 2.11 +(2016-04-02) 16-bit PNGS; enable SSE2 in non-gcc x64 RGB-format JPEG; remove +white matting in PSD; allocate large structures on the stack; correct channel +count for PNG & BMP 2.10 (2016-01-22) avoid warning introduced in 2.09 2.09 +(2016-01-16) 16-bit TGA; comments in PNM files; STBI_REALLOC_SIZED + + See end of file for full revision history. + + + ============================ Contributors ========================= + + Image formats Extensions, features + Sean Barrett (jpeg, png, bmp) Jetro Lauha (stbi_info) + Nicolas Schulz (hdr, psd) Martin "SpartanJ" Golini (stbi_info) + Jonathan Dummer (tga) James "moose2000" Brown (iPhone PNG) + Jean-Marc Lienher (gif) Ben "Disch" Wenger (io callbacks) + Tom Seddon (pic) Omar Cornut (1/2/4-bit PNG) + Thatcher Ulrich (psd) Nicolas Guillemot (vertical flip) + Ken Miller (pgm, ppm) Richard Mitton (16-bit PSD) + github:urraka (animated gif) Junggon Kim (PNM comments) + Christopher Forseth (animated gif) Daniel Gibson (16-bit TGA) + socks-the-fox (16-bit PNG) + Jeremy Sawicki (handle all ImageNet +JPGs) Optimizations & bugfixes Mikhail Morozov (1-bit BMP) + Fabian "ryg" Giesen Anael Seghezzi (is-16-bit query) + Arseny Kapoulkine + John-Mark Allen + Carmelo J Fdez-Aguera + + Bug & warning fixes + Marc LeBlanc David Woo Guillaume George Martins +Mozeiko Christpher Lloyd Jerry Jansson Joseph Thomson Phil +Jordan Dave Moore Roy Eltham Hayaki Saito Nathan Reed + Won Chun Luke Graham Johan Duparc Nick Verigakis + the Horde3D community Thomas Ruf Ronny Chevalier github:rlyeh + Janez Zemva John Bartholomew Michal Cichon github:romigrou + Jonathan Blow Ken Hamada Tero Hanninen github:svdijk + Laurent Gomila Cort Stratton Sergio Gonzalez github:snagar + Aruelien Pocheville Thibault Reuille Cass Everitt github:Zelex + Ryamond Barbiero Paul Du Bois Engin Manap github:grim210 + Aldo Culquicondor Philipp Wiesemann Dale Weiler github:sammyhw + Oriol Ferrer Mesia Josh Tobin Matthew Gregan github:phprus + Julian Raschke Gregory Mullen Baldur Karlsson +github:poppolopoppo Christian Floisand Kevin Schmidt JR Smith +github:darealshinji Blazej Dariusz Roszkowski github:Michaelangel007 +*/ + +#ifndef STBI_INCLUDE_STB_IMAGE_H +#define STBI_INCLUDE_STB_IMAGE_H + +// DOCUMENTATION +// +// Limitations: +// - no 12-bit-per-channel JPEG +// - no JPEGs with arithmetic coding +// - GIF always returns *comp=4 +// +// Basic usage (see HDR discussion below for HDR usage): +// int x,y,n; +// unsigned char *data = stbi_load(filename, &x, &y, &n, 0); +// // ... process data if not NULL ... +// // ... x = width, y = height, n = # 8-bit components per pixel ... +// // ... replace '0' with '1'..'4' to force that many components per pixel +// // ... but 'n' will always be the number that it would have been if you +// said 0 stbi_image_free(data) +// +// Standard parameters: +// int *x -- outputs image width in pixels +// int *y -- outputs image height in pixels +// int *channels_in_file -- outputs # of image components in image file +// int desired_channels -- if non-zero, # of image components requested in +// result +// +// The return value from an image loader is an 'unsigned char *' which points +// to the pixel data, or NULL on an allocation failure or if the image is +// corrupt or invalid. The pixel data consists of *y scanlines of *x pixels, +// with each pixel consisting of N interleaved 8-bit components; the first +// pixel pointed to is top-left-most in the image. There is no padding between +// image scanlines or between pixels, regardless of format. The number of +// components N is 'desired_channels' if desired_channels is non-zero, or +// *channels_in_file otherwise. If desired_channels is non-zero, +// *channels_in_file has the number of components that _would_ have been +// output otherwise. E.g. if you set desired_channels to 4, you will always +// get RGBA output, but you can check *channels_in_file to see if it's trivially +// opaque because e.g. there were only 3 channels in the source image. +// +// An output image with N components has the following components interleaved +// in this order in each pixel: +// +// N=#comp components +// 1 grey +// 2 grey, alpha +// 3 red, green, blue +// 4 red, green, blue, alpha +// +// If image loading fails for any reason, the return value will be NULL, +// and *x, *y, *channels_in_file will be unchanged. The function +// stbi_failure_reason() can be queried for an extremely brief, end-user +// unfriendly explanation of why the load failed. Define STBI_NO_FAILURE_STRINGS +// to avoid compiling these strings at all, and STBI_FAILURE_USERMSG to get +// slightly more user-friendly ones. +// +// Paletted PNG, BMP, GIF, and PIC images are automatically depalettized. +// +// =========================================================================== +// +// UNICODE: +// +// If compiling for Windows and you wish to use Unicode filenames, compile +// with +// #define STBI_WINDOWS_UTF8 +// and pass utf8-encoded filenames. Call stbi_convert_wchar_to_utf8 to convert +// Windows wchar_t filenames to utf8. +// +// =========================================================================== +// +// Philosophy +// +// stb libraries are designed with the following priorities: +// +// 1. easy to use +// 2. easy to maintain +// 3. good performance +// +// Sometimes I let "good performance" creep up in priority over "easy to +// maintain", and for best performance I may provide less-easy-to-use APIs that +// give higher performance, in addition to the easy-to-use ones. Nevertheless, +// it's important to keep in mind that from the standpoint of you, a client of +// this library, all you care about is #1 and #3, and stb libraries DO NOT +// emphasize #3 above all. +// +// Some secondary priorities arise directly from the first two, some of which +// provide more explicit reasons why performance can't be emphasized. +// +// - Portable ("ease of use") +// - Small source code footprint ("easy to maintain") +// - No dependencies ("ease of use") +// +// =========================================================================== +// +// I/O callbacks +// +// I/O callbacks allow you to read from arbitrary sources, like packaged +// files or some other source. Data read from callbacks are processed +// through a small internal buffer (currently 128 bytes) to try to reduce +// overhead. +// +// The three functions you must define are "read" (reads some bytes of data), +// "skip" (skips some bytes of data), "eof" (reports if the stream is at the +// end). +// +// =========================================================================== +// +// SIMD support +// +// The JPEG decoder will try to automatically use SIMD kernels on x86 when +// supported by the compiler. For ARM Neon support, you must explicitly +// request it. +// +// (The old do-it-yourself SIMD API is no longer supported in the current +// code.) +// +// On x86, SSE2 will automatically be used when available based on a run-time +// test; if not, the generic C versions are used as a fall-back. On ARM targets, +// the typical path is to have separate builds for NEON and non-NEON devices +// (at least this is true for iOS and Android). Therefore, the NEON support is +// toggled by a build flag: define STBI_NEON to get NEON loops. +// +// If for some reason you do not want to use any of SIMD code, or if +// you have issues compiling it, you can disable it entirely by +// defining STBI_NO_SIMD. +// +// =========================================================================== +// +// HDR image support (disable by defining STBI_NO_HDR) +// +// stb_image supports loading HDR images in general, and currently the Radiance +// .HDR file format specifically. You can still load any file through the +// existing interface; if you attempt to load an HDR file, it will be +// automatically remapped to LDR, assuming gamma 2.2 and an arbitrary scale +// factor defaulting to 1; both of these constants can be reconfigured through +// this interface: +// +// stbi_hdr_to_ldr_gamma(2.2f); +// stbi_hdr_to_ldr_scale(1.0f); +// +// (note, do not use _inverse_ constants; stbi_image will invert them +// appropriately). +// +// Additionally, there is a new, parallel interface for loading files as +// (linear) floats to preserve the full dynamic range: +// +// float *data = stbi_loadf(filename, &x, &y, &n, 0); +// +// If you load LDR images through this interface, those images will +// be promoted to floating point values, run through the inverse of +// constants corresponding to the above: +// +// stbi_ldr_to_hdr_scale(1.0f); +// stbi_ldr_to_hdr_gamma(2.2f); +// +// Finally, given a filename (or an open file or memory block--see header +// file for details) containing image data, you can query for the "most +// appropriate" interface to use (that is, whether the image is HDR or +// not), using: +// +// stbi_is_hdr(char *filename); +// +// =========================================================================== +// +// iPhone PNG support: +// +// By default we convert iphone-formatted PNGs back to RGB, even though +// they are internally encoded differently. You can disable this conversion +// by calling stbi_convert_iphone_png_to_rgb(0), in which case +// you will always just get the native iphone "format" through (which +// is BGR stored in RGB). +// +// Call stbi_set_unpremultiply_on_load(1) as well to force a divide per +// pixel to remove any premultiplied alpha *only* if the image file explicitly +// says there's premultiplied data (currently only happens in iPhone images, +// and only if iPhone convert-to-rgb processing is on). +// +// =========================================================================== +// +// ADDITIONAL CONFIGURATION +// +// - You can suppress implementation of any of the decoders to reduce +// your code footprint by #defining one or more of the following +// symbols before creating the implementation. +// +// STBI_NO_JPEG +// STBI_NO_PNG +// STBI_NO_BMP +// STBI_NO_PSD +// STBI_NO_TGA +// STBI_NO_GIF +// STBI_NO_HDR +// STBI_NO_PIC +// STBI_NO_PNM (.ppm and .pgm) +// +// - You can request *only* certain decoders and suppress all other ones +// (this will be more forward-compatible, as addition of new decoders +// doesn't require you to disable them explicitly): +// +// STBI_ONLY_JPEG +// STBI_ONLY_PNG +// STBI_ONLY_BMP +// STBI_ONLY_PSD +// STBI_ONLY_TGA +// STBI_ONLY_GIF +// STBI_ONLY_HDR +// STBI_ONLY_PIC +// STBI_ONLY_PNM (.ppm and .pgm) +// +// - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still +// want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB +// + +#ifndef STBI_NO_STDIO +#include +#endif // STBI_NO_STDIO + +#define STBI_VERSION 1 + +enum { + STBI_default = 0, // only used for desired_channels + + STBI_grey = 1, + STBI_grey_alpha = 2, + STBI_rgb = 3, + STBI_rgb_alpha = 4 +}; + +#include +typedef unsigned char stbi_uc; +typedef unsigned short stbi_us; + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef STB_IMAGE_STATIC +#define STBIDEF static +#else +#define STBIDEF extern +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// PRIMARY API - works on images of any type +// + +// +// load image by filename, open file, or memory buffer +// + +typedef struct { + int (*read)(void* user, char* data, + int size); // fill 'data' with 'size' bytes. return number of + // bytes actually read + void (*skip)(void* user, int n); // skip the next 'n' bytes, or 'unget' the + // last -n bytes if negative + int (*eof)(void* user); // returns nonzero if we are at end of file/data +} stbi_io_callbacks; + +//////////////////////////////////// +// +// 8-bits-per-channel interface +// + +STBIDEF stbi_uc* stbi_load_from_memory(stbi_uc const* buffer, int len, int* x, + int* y, int* channels_in_file, + int desired_channels); +STBIDEF stbi_uc* stbi_load_from_callbacks(stbi_io_callbacks const* clbk, + void* user, int* x, int* y, + int* channels_in_file, + int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF stbi_uc* stbi_load(char const* filename, int* x, int* y, + int* channels_in_file, int desired_channels); +STBIDEF stbi_uc* stbi_load_from_file(FILE* f, int* x, int* y, + int* channels_in_file, + int desired_channels); +// for stbi_load_from_file, file pointer is left pointing immediately after +// image +#endif + +#ifndef STBI_NO_GIF +STBIDEF stbi_uc* stbi_load_gif_from_memory(stbi_uc const* buffer, int len, + int** delays, int* x, int* y, int* z, + int* comp, int req_comp); +#endif + +#ifdef STBI_WINDOWS_UTF8 +STBIDEF int stbi_convert_wchar_to_utf8(char* buffer, size_t bufferlen, + const wchar_t* input); +#endif + +//////////////////////////////////// +// +// 16-bits-per-channel interface +// + +STBIDEF stbi_us* stbi_load_16_from_memory(stbi_uc const* buffer, int len, + int* x, int* y, int* channels_in_file, + int desired_channels); +STBIDEF stbi_us* stbi_load_16_from_callbacks(stbi_io_callbacks const* clbk, + void* user, int* x, int* y, + int* channels_in_file, + int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF stbi_us* stbi_load_16(char const* filename, int* x, int* y, + int* channels_in_file, int desired_channels); +STBIDEF stbi_us* stbi_load_from_file_16(FILE* f, int* x, int* y, + int* channels_in_file, + int desired_channels); +#endif + +//////////////////////////////////// +// +// float-per-channel interface +// +#ifndef STBI_NO_LINEAR +STBIDEF float* stbi_loadf_from_memory(stbi_uc const* buffer, int len, int* x, + int* y, int* channels_in_file, + int desired_channels); +STBIDEF float* stbi_loadf_from_callbacks(stbi_io_callbacks const* clbk, + void* user, int* x, int* y, + int* channels_in_file, + int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF float* stbi_loadf(char const* filename, int* x, int* y, + int* channels_in_file, int desired_channels); +STBIDEF float* stbi_loadf_from_file(FILE* f, int* x, int* y, + int* channels_in_file, + int desired_channels); +#endif +#endif + +#ifndef STBI_NO_HDR +STBIDEF void stbi_hdr_to_ldr_gamma(float gamma); +STBIDEF void stbi_hdr_to_ldr_scale(float scale); +#endif // STBI_NO_HDR + +#ifndef STBI_NO_LINEAR +STBIDEF void stbi_ldr_to_hdr_gamma(float gamma); +STBIDEF void stbi_ldr_to_hdr_scale(float scale); +#endif // STBI_NO_LINEAR + +// stbi_is_hdr is always defined, but always returns false if STBI_NO_HDR +STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const* clbk, + void* user); +STBIDEF int stbi_is_hdr_from_memory(stbi_uc const* buffer, int len); +#ifndef STBI_NO_STDIO +STBIDEF int stbi_is_hdr(char const* filename); +STBIDEF int stbi_is_hdr_from_file(FILE* f); +#endif // STBI_NO_STDIO + +// get a VERY brief reason for failure +// NOT THREADSAFE +STBIDEF const char* stbi_failure_reason(void); + +// free the loaded image -- this is just free() +STBIDEF void stbi_image_free(void* retval_from_stbi_load); + +// get image dimensions & components without fully decoding +STBIDEF int stbi_info_from_memory(stbi_uc const* buffer, int len, int* x, + int* y, int* comp); +STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const* clbk, void* user, + int* x, int* y, int* comp); +STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const* buffer, int len); +STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const* clbk, + void* user); + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_info(char const* filename, int* x, int* y, int* comp); +STBIDEF int stbi_info_from_file(FILE* f, int* x, int* y, int* comp); +STBIDEF int stbi_is_16_bit(char const* filename); +STBIDEF int stbi_is_16_bit_from_file(FILE* f); +#endif + +// for image formats that explicitly notate that they have premultiplied alpha, +// we just return the colors as stored in the file. set this flag to force +// unpremultiplication. results are undefined if the unpremultiply overflow. +STBIDEF void +stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply); + +// indicate whether we should process iphone images back to canonical format, +// or just pass them through "as-is" +STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert); + +// flip the image vertically, so the first pixel in the output array is the +// bottom left +STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip); + +// ZLIB client - used by PNG, available for other purposes + +STBIDEF char* stbi_zlib_decode_malloc_guesssize(const char* buffer, int len, + int initial_size, int* outlen); +STBIDEF char* stbi_zlib_decode_malloc_guesssize_headerflag(const char* buffer, + int len, + int initial_size, + int* outlen, + int parse_header); +STBIDEF char* stbi_zlib_decode_malloc(const char* buffer, int len, int* outlen); +STBIDEF int stbi_zlib_decode_buffer(char* obuffer, int olen, + const char* ibuffer, int ilen); + +STBIDEF char* stbi_zlib_decode_noheader_malloc(const char* buffer, int len, + int* outlen); +STBIDEF int stbi_zlib_decode_noheader_buffer(char* obuffer, int olen, + const char* ibuffer, int ilen); + +#ifdef __cplusplus +} +#endif + +// +// +//// end header file ///////////////////////////////////////////////////// +#endif // STBI_INCLUDE_STB_IMAGE_H + +#ifdef STB_IMAGE_IMPLEMENTATION + +#if defined(STBI_ONLY_JPEG) || defined(STBI_ONLY_PNG) || \ + defined(STBI_ONLY_BMP) || defined(STBI_ONLY_TGA) || \ + defined(STBI_ONLY_GIF) || defined(STBI_ONLY_PSD) || \ + defined(STBI_ONLY_HDR) || defined(STBI_ONLY_PIC) || \ + defined(STBI_ONLY_PNM) || defined(STBI_ONLY_ZLIB) +#ifndef STBI_ONLY_JPEG +#define STBI_NO_JPEG +#endif +#ifndef STBI_ONLY_PNG +#define STBI_NO_PNG +#endif +#ifndef STBI_ONLY_BMP +#define STBI_NO_BMP +#endif +#ifndef STBI_ONLY_PSD +#define STBI_NO_PSD +#endif +#ifndef STBI_ONLY_TGA +#define STBI_NO_TGA +#endif +#ifndef STBI_ONLY_GIF +#define STBI_NO_GIF +#endif +#ifndef STBI_ONLY_HDR +#define STBI_NO_HDR +#endif +#ifndef STBI_ONLY_PIC +#define STBI_NO_PIC +#endif +#ifndef STBI_ONLY_PNM +#define STBI_NO_PNM +#endif +#endif + +#if defined(STBI_NO_PNG) && !defined(STBI_SUPPORT_ZLIB) && \ + !defined(STBI_NO_ZLIB) +#define STBI_NO_ZLIB +#endif + +#include +#include +#include // ptrdiff_t on osx +#include +#include + +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) +#include // ldexp, pow +#endif + +#ifndef STBI_NO_STDIO +#include +#endif + +#ifndef STBI_ASSERT +#include +#define STBI_ASSERT(x) assert(x) +#endif + +#ifdef __cplusplus +#define STBI_EXTERN extern "C" +#else +#define STBI_EXTERN extern +#endif + +#ifndef _MSC_VER +#ifdef __cplusplus +#define stbi_inline inline +#else +#define stbi_inline +#endif +#else +#define stbi_inline __forceinline +#endif + +#ifdef _MSC_VER +typedef unsigned short stbi__uint16; +typedef signed short stbi__int16; +typedef unsigned int stbi__uint32; +typedef signed int stbi__int32; +#else +#include +typedef uint16_t stbi__uint16; +typedef int16_t stbi__int16; +typedef uint32_t stbi__uint32; +typedef int32_t stbi__int32; +#endif + +// should produce compiler error if size is wrong +typedef unsigned char validate_uint32[sizeof(stbi__uint32) == 4 ? 1 : -1]; + +#ifdef _MSC_VER +#define STBI_NOTUSED(v) (void)(v) +#else +#define STBI_NOTUSED(v) (void)sizeof(v) +#endif + +#ifdef _MSC_VER +#define STBI_HAS_LROTL +#endif + +#ifdef STBI_HAS_LROTL +#define stbi_lrot(x, y) _lrotl(x, y) +#else +#define stbi_lrot(x, y) (((x) << (y)) | ((x) >> (32 - (y)))) +#endif + +#if defined(STBI_MALLOC) && defined(STBI_FREE) && \ + (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED)) +// ok +#elif !defined(STBI_MALLOC) && !defined(STBI_FREE) && \ + !defined(STBI_REALLOC) && !defined(STBI_REALLOC_SIZED) +// ok +#else +#error \ + "Must define all or none of STBI_MALLOC, STBI_FREE, and STBI_REALLOC (or STBI_REALLOC_SIZED)." +#endif + +#ifndef STBI_MALLOC +#define STBI_MALLOC(sz) malloc(sz) +#define STBI_REALLOC(p, newsz) realloc(p, newsz) +#define STBI_FREE(p) free(p) +#endif + +#ifndef STBI_REALLOC_SIZED +#define STBI_REALLOC_SIZED(p, oldsz, newsz) STBI_REALLOC(p, newsz) +#endif + +// x86/x64 detection +#if defined(__x86_64__) || defined(_M_X64) +#define STBI__X64_TARGET +#elif defined(__i386) || defined(_M_IX86) +#define STBI__X86_TARGET +#endif + +#if defined(__GNUC__) && defined(STBI__X86_TARGET) && !defined(__SSE2__) && \ + !defined(STBI_NO_SIMD) +// gcc doesn't support sse2 intrinsics unless you compile with -msse2, +// which in turn means it gets to use SSE2 everywhere. This is unfortunate, +// but previous attempts to provide the SSE2 functions with runtime +// detection caused numerous issues. The way architecture extensions are +// exposed in GCC/Clang is, sadly, not really suited for one-file libs. +// New behavior: if compiled with -msse2, we use SSE2 without any +// detection; if not, we don't use it at all. +#define STBI_NO_SIMD +#endif + +#if defined(__MINGW32__) && defined(STBI__X86_TARGET) && \ + !defined(STBI_MINGW_ENABLE_SSE2) && !defined(STBI_NO_SIMD) +// Note that __MINGW32__ doesn't actually mean 32-bit, so we have to avoid +// STBI__X64_TARGET +// +// 32-bit MinGW wants ESP to be 16-byte aligned, but this is not in the +// Windows ABI and VC++ as well as Windows DLLs don't maintain that invariant. +// As a result, enabling SSE2 on 32-bit MinGW is dangerous when not +// simultaneously enabling "-mstackrealign". +// +// See https://github.com/nothings/stb/issues/81 for more information. +// +// So default to no SSE2 on 32-bit MinGW. If you've read this far and added +// -mstackrealign to your build settings, feel free to #define +// STBI_MINGW_ENABLE_SSE2. +#define STBI_NO_SIMD +#endif + +#if !defined(STBI_NO_SIMD) && \ + (defined(STBI__X86_TARGET) || defined(STBI__X64_TARGET)) +#define STBI_SSE2 +#include + +#ifdef _MSC_VER + +#if _MSC_VER >= 1400 // not VC6 +#include // __cpuid +static int stbi__cpuid3(void) +{ + int info[4]; + __cpuid(info, 1); + return info[3]; +} +#else +static int stbi__cpuid3(void) +{ + int res; + __asm { + mov eax,1 + cpuid + mov res,edx + } + return res; +} +#endif + +#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name + +#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) +static int stbi__sse2_available(void) +{ + int info3 = stbi__cpuid3(); + return ((info3 >> 26) & 1) != 0; +} +#endif + +#else // assume GCC-style if not VC++ +#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) + +#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) +static int stbi__sse2_available(void) +{ + // If we're even attempting to compile this on GCC/Clang, that means + // -msse2 is on, which means the compiler is allowed to use SSE2 + // instructions at will, and so are we. + return 1; +} +#endif + +#endif +#endif + +// ARM NEON +#if defined(STBI_NO_SIMD) && defined(STBI_NEON) +#undef STBI_NEON +#endif + +#ifdef STBI_NEON +#include +// assume GCC or Clang on ARM targets +#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) +#endif + +#ifndef STBI_SIMD_ALIGN +#define STBI_SIMD_ALIGN(type, name) type name +#endif + +/////////////////////////////////////////////// +// +// stbi__context struct and start_xxx functions + +// stbi__context structure is our basic context used by all images, so it +// contains all the IO context, plus some basic image information +typedef struct { + stbi__uint32 img_x, img_y; + int img_n, img_out_n; + + stbi_io_callbacks io; + void* io_user_data; + + int read_from_callbacks; + int buflen; + stbi_uc buffer_start[128]; + + stbi_uc *img_buffer, *img_buffer_end; + stbi_uc *img_buffer_original, *img_buffer_original_end; +} stbi__context; + +static void stbi__refill_buffer(stbi__context* s); + +// initialize a memory-decode context +static void stbi__start_mem(stbi__context* s, stbi_uc const* buffer, int len) +{ + s->io.read = NULL; + s->read_from_callbacks = 0; + s->img_buffer = s->img_buffer_original = (stbi_uc*)buffer; + s->img_buffer_end = s->img_buffer_original_end = (stbi_uc*)buffer + len; +} + +// initialize a callback-based context +static void stbi__start_callbacks(stbi__context* s, stbi_io_callbacks* c, + void* user) +{ + s->io = *c; + s->io_user_data = user; + s->buflen = sizeof(s->buffer_start); + s->read_from_callbacks = 1; + s->img_buffer_original = s->buffer_start; + stbi__refill_buffer(s); + s->img_buffer_original_end = s->img_buffer_end; +} + +#ifndef STBI_NO_STDIO + +static int stbi__stdio_read(void* user, char* data, int size) +{ + return (int)fread(data, 1, size, (FILE*)user); +} + +static void stbi__stdio_skip(void* user, int n) +{ + fseek((FILE*)user, n, SEEK_CUR); +} + +static int stbi__stdio_eof(void* user) { return feof((FILE*)user); } + +static stbi_io_callbacks stbi__stdio_callbacks = { + stbi__stdio_read, + stbi__stdio_skip, + stbi__stdio_eof, +}; + +static void stbi__start_file(stbi__context* s, FILE* f) +{ + stbi__start_callbacks(s, &stbi__stdio_callbacks, (void*)f); +} + +// static void stop_file(stbi__context *s) { } + +#endif // !STBI_NO_STDIO + +static void stbi__rewind(stbi__context* s) +{ + // conceptually rewind SHOULD rewind to the beginning of the stream, + // but we just rewind to the beginning of the initial buffer, because + // we only use it after doing 'test', which only ever looks at at most 92 + // bytes + s->img_buffer = s->img_buffer_original; + s->img_buffer_end = s->img_buffer_original_end; +} + +enum { STBI_ORDER_RGB, STBI_ORDER_BGR }; + +typedef struct { + int bits_per_channel; + int num_channels; + int channel_order; +} stbi__result_info; + +#ifndef STBI_NO_JPEG +static int stbi__jpeg_test(stbi__context* s); +static void* stbi__jpeg_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri); +static int stbi__jpeg_info(stbi__context* s, int* x, int* y, int* comp); +#endif + +#ifndef STBI_NO_PNG +static int stbi__png_test(stbi__context* s); +static void* stbi__png_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri); +static int stbi__png_info(stbi__context* s, int* x, int* y, int* comp); +static int stbi__png_is16(stbi__context* s); +#endif + +#ifndef STBI_NO_BMP +static int stbi__bmp_test(stbi__context* s); +static void* stbi__bmp_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri); +static int stbi__bmp_info(stbi__context* s, int* x, int* y, int* comp); +#endif + +#ifndef STBI_NO_TGA +static int stbi__tga_test(stbi__context* s); +static void* stbi__tga_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri); +static int stbi__tga_info(stbi__context* s, int* x, int* y, int* comp); +#endif + +#ifndef STBI_NO_PSD +static int stbi__psd_test(stbi__context* s); +static void* stbi__psd_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri, int bpc); +static int stbi__psd_info(stbi__context* s, int* x, int* y, int* comp); +static int stbi__psd_is16(stbi__context* s); +#endif + +#ifndef STBI_NO_HDR +static int stbi__hdr_test(stbi__context* s); +static float* stbi__hdr_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri); +static int stbi__hdr_info(stbi__context* s, int* x, int* y, int* comp); +#endif + +#ifndef STBI_NO_PIC +static int stbi__pic_test(stbi__context* s); +static void* stbi__pic_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri); +static int stbi__pic_info(stbi__context* s, int* x, int* y, int* comp); +#endif + +#ifndef STBI_NO_GIF +static int stbi__gif_test(stbi__context* s); +static void* stbi__gif_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri); +static void* stbi__load_gif_main(stbi__context* s, int** delays, int* x, int* y, + int* z, int* comp, int req_comp); +static int stbi__gif_info(stbi__context* s, int* x, int* y, int* comp); +#endif + +#ifndef STBI_NO_PNM +static int stbi__pnm_test(stbi__context* s); +static void* stbi__pnm_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri); +static int stbi__pnm_info(stbi__context* s, int* x, int* y, int* comp); +#endif + +// this is not threadsafe +static const char* stbi__g_failure_reason; + +STBIDEF const char* stbi_failure_reason(void) { return stbi__g_failure_reason; } + +static int stbi__err(const char* str) +{ + stbi__g_failure_reason = str; + return 0; +} + +static void* stbi__malloc(size_t size) { return STBI_MALLOC(size); } + +// stb_image uses ints pervasively, including for offset calculations. +// therefore the largest decoded image size we can support with the +// current code, even on 64-bit targets, is INT_MAX. this is not a +// significant limitation for the intended use case. +// +// we do, however, need to make sure our size calculations don't +// overflow. hence a few helper functions for size calculations that +// multiply integers together, making sure that they're non-negative +// and no overflow occurs. + +// return 1 if the sum is valid, 0 on overflow. +// negative terms are considered invalid. +static int stbi__addsizes_valid(int a, int b) +{ + if (b < 0) + return 0; + // now 0 <= b <= INT_MAX, hence also + // 0 <= INT_MAX - b <= INTMAX. + // And "a + b <= INT_MAX" (which might overflow) is the + // same as a <= INT_MAX - b (no overflow) + return a <= INT_MAX - b; +} + +// returns 1 if the product is valid, 0 on overflow. +// negative factors are considered invalid. +static int stbi__mul2sizes_valid(int a, int b) +{ + if (a < 0 || b < 0) + return 0; + if (b == 0) + return 1; // mul-by-0 is always safe + // portable way to check for no overflows in a*b + return a <= INT_MAX / b; +} + +// returns 1 if "a*b + add" has no negative terms/factors and doesn't overflow +static int stbi__mad2sizes_valid(int a, int b, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a * b, add); +} + +// returns 1 if "a*b*c + add" has no negative terms/factors and doesn't overflow +static int stbi__mad3sizes_valid(int a, int b, int c, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a * b, c) && + stbi__addsizes_valid(a * b * c, add); +} + +// returns 1 if "a*b*c*d + add" has no negative terms/factors and doesn't +// overflow +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) +static int stbi__mad4sizes_valid(int a, int b, int c, int d, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a * b, c) && + stbi__mul2sizes_valid(a * b * c, d) && + stbi__addsizes_valid(a * b * c * d, add); +} +#endif + +// mallocs with size overflow checking +static void* stbi__malloc_mad2(int a, int b, int add) +{ + if (!stbi__mad2sizes_valid(a, b, add)) + return NULL; + return stbi__malloc(a * b + add); +} + +static void* stbi__malloc_mad3(int a, int b, int c, int add) +{ + if (!stbi__mad3sizes_valid(a, b, c, add)) + return NULL; + return stbi__malloc(a * b * c + add); +} + +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) +static void* stbi__malloc_mad4(int a, int b, int c, int d, int add) +{ + if (!stbi__mad4sizes_valid(a, b, c, d, add)) + return NULL; + return stbi__malloc(a * b * c * d + add); +} +#endif + +// stbi__err - error +// stbi__errpf - error returning pointer to float +// stbi__errpuc - error returning pointer to unsigned char + +#ifdef STBI_NO_FAILURE_STRINGS +#define stbi__err(x, y) 0 +#elif defined(STBI_FAILURE_USERMSG) +#define stbi__err(x, y) stbi__err(y) +#else +#define stbi__err(x, y) stbi__err(x) +#endif + +#define stbi__errpf(x, y) ((float*)(size_t)(stbi__err(x, y) ? NULL : NULL)) +#define stbi__errpuc(x, y) \ + ((unsigned char*)(size_t)(stbi__err(x, y) ? NULL : NULL)) + +STBIDEF void stbi_image_free(void* retval_from_stbi_load) +{ + STBI_FREE(retval_from_stbi_load); +} + +#ifndef STBI_NO_LINEAR +static float* stbi__ldr_to_hdr(stbi_uc* data, int x, int y, int comp); +#endif + +#ifndef STBI_NO_HDR +static stbi_uc* stbi__hdr_to_ldr(float* data, int x, int y, int comp); +#endif + +static int stbi__vertically_flip_on_load = 0; + +STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip) +{ + stbi__vertically_flip_on_load = flag_true_if_should_flip; +} + +static void* stbi__load_main(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri, int bpc) +{ + memset(ri, 0, + sizeof(*ri)); // make sure it's initialized if we add new fields + ri->bits_per_channel = + 8; // default is 8 so most paths don't have to be changed + ri->channel_order = + STBI_ORDER_RGB; // all current input & output are this, but this is here + // so we can add BGR order + ri->num_channels = 0; + +#ifndef STBI_NO_JPEG + if (stbi__jpeg_test(s)) + return stbi__jpeg_load(s, x, y, comp, req_comp, ri); +#endif +#ifndef STBI_NO_PNG + if (stbi__png_test(s)) + return stbi__png_load(s, x, y, comp, req_comp, ri); +#endif +#ifndef STBI_NO_BMP + if (stbi__bmp_test(s)) + return stbi__bmp_load(s, x, y, comp, req_comp, ri); +#endif +#ifndef STBI_NO_GIF + if (stbi__gif_test(s)) + return stbi__gif_load(s, x, y, comp, req_comp, ri); +#endif +#ifndef STBI_NO_PSD + if (stbi__psd_test(s)) + return stbi__psd_load(s, x, y, comp, req_comp, ri, bpc); +#endif +#ifndef STBI_NO_PIC + if (stbi__pic_test(s)) + return stbi__pic_load(s, x, y, comp, req_comp, ri); +#endif +#ifndef STBI_NO_PNM + if (stbi__pnm_test(s)) + return stbi__pnm_load(s, x, y, comp, req_comp, ri); +#endif + +#ifndef STBI_NO_HDR + if (stbi__hdr_test(s)) { + float* hdr = stbi__hdr_load(s, x, y, comp, req_comp, ri); + return stbi__hdr_to_ldr(hdr, *x, *y, req_comp ? req_comp : *comp); + } +#endif + +#ifndef STBI_NO_TGA + // test tga last because it's a crappy test! + if (stbi__tga_test(s)) + return stbi__tga_load(s, x, y, comp, req_comp, ri); +#endif + + return stbi__errpuc("unknown image type", + "Image not of any known type, or corrupt"); +} + +static stbi_uc* stbi__convert_16_to_8(stbi__uint16* orig, int w, int h, + int channels) +{ + int i; + int img_len = w * h * channels; + stbi_uc* reduced; + + reduced = (stbi_uc*)stbi__malloc(img_len); + if (reduced == NULL) + return stbi__errpuc("outofmem", "Out of memory"); + + for (i = 0; i < img_len; ++i) + reduced[i] = (stbi_uc)((orig[i] >> 8) & + 0xFF); // top half of each byte is sufficient + // approx of 16->8 bit scaling + + STBI_FREE(orig); + return reduced; +} + +static stbi__uint16* stbi__convert_8_to_16(stbi_uc* orig, int w, int h, + int channels) +{ + int i; + int img_len = w * h * channels; + stbi__uint16* enlarged; + + enlarged = (stbi__uint16*)stbi__malloc(img_len * 2); + if (enlarged == NULL) + return (stbi__uint16*)stbi__errpuc("outofmem", "Out of memory"); + + for (i = 0; i < img_len; ++i) + enlarged[i] = (stbi__uint16)((orig[i] << 8) + + orig[i]); // replicate to high and low + // byte, maps 0->0, 255->0xffff + + STBI_FREE(orig); + return enlarged; +} + +static void stbi__vertical_flip(void* image, int w, int h, int bytes_per_pixel) +{ + int row; + size_t bytes_per_row = (size_t)w * bytes_per_pixel; + stbi_uc temp[2048]; + stbi_uc* bytes = (stbi_uc*)image; + + for (row = 0; row < (h >> 1); row++) { + stbi_uc* row0 = bytes + row * bytes_per_row; + stbi_uc* row1 = bytes + (h - row - 1) * bytes_per_row; + // swap row0 with row1 + size_t bytes_left = bytes_per_row; + while (bytes_left) { + size_t bytes_copy = + (bytes_left < sizeof(temp)) ? bytes_left : sizeof(temp); + memcpy(temp, row0, bytes_copy); + memcpy(row0, row1, bytes_copy); + memcpy(row1, temp, bytes_copy); + row0 += bytes_copy; + row1 += bytes_copy; + bytes_left -= bytes_copy; + } + } +} + +#ifndef STBI_NO_GIF +static void stbi__vertical_flip_slices(void* image, int w, int h, int z, + int bytes_per_pixel) +{ + int slice; + int slice_size = w * h * bytes_per_pixel; + + stbi_uc* bytes = (stbi_uc*)image; + for (slice = 0; slice < z; ++slice) { + stbi__vertical_flip(bytes, w, h, bytes_per_pixel); + bytes += slice_size; + } +} +#endif + +static unsigned char* stbi__load_and_postprocess_8bit(stbi__context* s, int* x, + int* y, int* comp, + int req_comp) +{ + stbi__result_info ri; + void* result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8); + + if (result == NULL) + return NULL; + + if (ri.bits_per_channel != 8) { + STBI_ASSERT(ri.bits_per_channel == 16); + result = stbi__convert_16_to_8((stbi__uint16*)result, *x, *y, + req_comp == 0 ? *comp : req_comp); + ri.bits_per_channel = 8; + } + + // @TODO: move stbi__convert_format to here + + if (stbi__vertically_flip_on_load) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi_uc)); + } + + return (unsigned char*)result; +} + +static stbi__uint16* stbi__load_and_postprocess_16bit(stbi__context* s, int* x, + int* y, int* comp, + int req_comp) +{ + stbi__result_info ri; + void* result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16); + + if (result == NULL) + return NULL; + + if (ri.bits_per_channel != 16) { + STBI_ASSERT(ri.bits_per_channel == 8); + result = stbi__convert_8_to_16((stbi_uc*)result, *x, *y, + req_comp == 0 ? *comp : req_comp); + ri.bits_per_channel = 16; + } + + // @TODO: move stbi__convert_format16 to here + // @TODO: special case RGB-to-Y (and RGBA-to-YA) for 8-bit-to-16-bit case to + // keep more precision + + if (stbi__vertically_flip_on_load) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi__uint16)); + } + + return (stbi__uint16*)result; +} + +#if !defined(STBI_NO_HDR) && !defined(STBI_NO_LINEAR) +static void stbi__float_postprocess(float* result, int* x, int* y, int* comp, + int req_comp) +{ + if (stbi__vertically_flip_on_load && result != NULL) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(float)); + } +} +#endif + +#ifndef STBI_NO_STDIO + +#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8) +STBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar( + unsigned int cp, unsigned long flags, const char* str, int cbmb, + wchar_t* widestr, int cchwide); +STBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte( + unsigned int cp, unsigned long flags, const wchar_t* widestr, int cchwide, + char* str, int cbmb, const char* defchar, int* used_default); +#endif + +#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8) +STBIDEF int stbi_convert_wchar_to_utf8(char* buffer, size_t bufferlen, + const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, + bufferlen, NULL, NULL); +} +#endif + +static FILE* stbi__fopen(char const* filename, char const* mode) +{ + FILE* f; +#if defined(_MSC_VER) && defined(STBI_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, + sizeof(wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, + sizeof(wMode))) + return 0; + +#if _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f = 0; +#else + f = fopen(filename, mode); +#endif + return f; +} + +STBIDEF stbi_uc* stbi_load(char const* filename, int* x, int* y, int* comp, + int req_comp) +{ + FILE* f = stbi__fopen(filename, "rb"); + unsigned char* result; + if (!f) + return stbi__errpuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file(f, x, y, comp, req_comp); + fclose(f); + return result; +} + +STBIDEF stbi_uc* stbi_load_from_file(FILE* f, int* x, int* y, int* comp, + int req_comp) +{ + unsigned char* result; + stbi__context s; + stbi__start_file(&s, f); + result = stbi__load_and_postprocess_8bit(&s, x, y, comp, req_comp); + if (result) { + // need to 'unget' all the characters in the IO buffer + fseek(f, -(int)(s.img_buffer_end - s.img_buffer), SEEK_CUR); + } + return result; +} + +STBIDEF stbi__uint16* stbi_load_from_file_16(FILE* f, int* x, int* y, int* comp, + int req_comp) +{ + stbi__uint16* result; + stbi__context s; + stbi__start_file(&s, f); + result = stbi__load_and_postprocess_16bit(&s, x, y, comp, req_comp); + if (result) { + // need to 'unget' all the characters in the IO buffer + fseek(f, -(int)(s.img_buffer_end - s.img_buffer), SEEK_CUR); + } + return result; +} + +STBIDEF stbi_us* stbi_load_16(char const* filename, int* x, int* y, int* comp, + int req_comp) +{ + FILE* f = stbi__fopen(filename, "rb"); + stbi__uint16* result; + if (!f) + return (stbi_us*)stbi__errpuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file_16(f, x, y, comp, req_comp); + fclose(f); + return result; +} + +#endif //! STBI_NO_STDIO + +STBIDEF stbi_us* stbi_load_16_from_memory(stbi_uc const* buffer, int len, + int* x, int* y, int* channels_in_file, + int desired_channels) +{ + stbi__context s; + stbi__start_mem(&s, buffer, len); + return stbi__load_and_postprocess_16bit(&s, x, y, channels_in_file, + desired_channels); +} + +STBIDEF stbi_us* stbi_load_16_from_callbacks(stbi_io_callbacks const* clbk, + void* user, int* x, int* y, + int* channels_in_file, + int desired_channels) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks*)clbk, user); + return stbi__load_and_postprocess_16bit(&s, x, y, channels_in_file, + desired_channels); +} + +STBIDEF stbi_uc* stbi_load_from_memory(stbi_uc const* buffer, int len, int* x, + int* y, int* comp, int req_comp) +{ + stbi__context s; + stbi__start_mem(&s, buffer, len); + return stbi__load_and_postprocess_8bit(&s, x, y, comp, req_comp); +} + +STBIDEF stbi_uc* stbi_load_from_callbacks(stbi_io_callbacks const* clbk, + void* user, int* x, int* y, int* comp, + int req_comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks*)clbk, user); + return stbi__load_and_postprocess_8bit(&s, x, y, comp, req_comp); +} + +#ifndef STBI_NO_GIF +STBIDEF stbi_uc* stbi_load_gif_from_memory(stbi_uc const* buffer, int len, + int** delays, int* x, int* y, int* z, + int* comp, int req_comp) +{ + unsigned char* result; + stbi__context s; + stbi__start_mem(&s, buffer, len); + + result = (unsigned char*)stbi__load_gif_main(&s, delays, x, y, z, comp, + req_comp); + if (stbi__vertically_flip_on_load) { + stbi__vertical_flip_slices(result, *x, *y, *z, *comp); + } + + return result; +} +#endif + +#ifndef STBI_NO_LINEAR +static float* stbi__loadf_main(stbi__context* s, int* x, int* y, int* comp, + int req_comp) +{ + unsigned char* data; +#ifndef STBI_NO_HDR + if (stbi__hdr_test(s)) { + stbi__result_info ri; + float* hdr_data = stbi__hdr_load(s, x, y, comp, req_comp, &ri); + if (hdr_data) + stbi__float_postprocess(hdr_data, x, y, comp, req_comp); + return hdr_data; + } +#endif + data = stbi__load_and_postprocess_8bit(s, x, y, comp, req_comp); + if (data) + return stbi__ldr_to_hdr(data, *x, *y, req_comp ? req_comp : *comp); + return stbi__errpf("unknown image type", + "Image not of any known type, or corrupt"); +} + +STBIDEF float* stbi_loadf_from_memory(stbi_uc const* buffer, int len, int* x, + int* y, int* comp, int req_comp) +{ + stbi__context s; + stbi__start_mem(&s, buffer, len); + return stbi__loadf_main(&s, x, y, comp, req_comp); +} + +STBIDEF float* stbi_loadf_from_callbacks(stbi_io_callbacks const* clbk, + void* user, int* x, int* y, int* comp, + int req_comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks*)clbk, user); + return stbi__loadf_main(&s, x, y, comp, req_comp); +} + +#ifndef STBI_NO_STDIO +STBIDEF float* stbi_loadf(char const* filename, int* x, int* y, int* comp, + int req_comp) +{ + float* result; + FILE* f = stbi__fopen(filename, "rb"); + if (!f) + return stbi__errpf("can't fopen", "Unable to open file"); + result = stbi_loadf_from_file(f, x, y, comp, req_comp); + fclose(f); + return result; +} + +STBIDEF float* stbi_loadf_from_file(FILE* f, int* x, int* y, int* comp, + int req_comp) +{ + stbi__context s; + stbi__start_file(&s, f); + return stbi__loadf_main(&s, x, y, comp, req_comp); +} +#endif // !STBI_NO_STDIO + +#endif // !STBI_NO_LINEAR + +// these is-hdr-or-not is defined independent of whether STBI_NO_LINEAR is +// defined, for API simplicity; if STBI_NO_LINEAR is defined, it always +// reports false! + +STBIDEF int stbi_is_hdr_from_memory(stbi_uc const* buffer, int len) +{ +#ifndef STBI_NO_HDR + stbi__context s; + stbi__start_mem(&s, buffer, len); + return stbi__hdr_test(&s); +#else + STBI_NOTUSED(buffer); + STBI_NOTUSED(len); + return 0; +#endif +} + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_is_hdr(char const* filename) +{ + FILE* f = stbi__fopen(filename, "rb"); + int result = 0; + if (f) { + result = stbi_is_hdr_from_file(f); + fclose(f); + } + return result; +} + +STBIDEF int stbi_is_hdr_from_file(FILE* f) +{ +#ifndef STBI_NO_HDR + long pos = ftell(f); + int res; + stbi__context s; + stbi__start_file(&s, f); + res = stbi__hdr_test(&s); + fseek(f, pos, SEEK_SET); + return res; +#else + STBI_NOTUSED(f); + return 0; +#endif +} +#endif // !STBI_NO_STDIO + +STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const* clbk, + void* user) +{ +#ifndef STBI_NO_HDR + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks*)clbk, user); + return stbi__hdr_test(&s); +#else + STBI_NOTUSED(clbk); + STBI_NOTUSED(user); + return 0; +#endif +} + +#ifndef STBI_NO_LINEAR +static float stbi__l2h_gamma = 2.2f, stbi__l2h_scale = 1.0f; + +STBIDEF void stbi_ldr_to_hdr_gamma(float gamma) { stbi__l2h_gamma = gamma; } +STBIDEF void stbi_ldr_to_hdr_scale(float scale) { stbi__l2h_scale = scale; } +#endif + +static float stbi__h2l_gamma_i = 1.0f / 2.2f, stbi__h2l_scale_i = 1.0f; + +STBIDEF void stbi_hdr_to_ldr_gamma(float gamma) +{ + stbi__h2l_gamma_i = 1 / gamma; +} +STBIDEF void stbi_hdr_to_ldr_scale(float scale) +{ + stbi__h2l_scale_i = 1 / scale; +} + +////////////////////////////////////////////////////////////////////////////// +// +// Common code used by all image loaders +// + +enum { STBI__SCAN_load = 0, STBI__SCAN_type, STBI__SCAN_header }; + +static void stbi__refill_buffer(stbi__context* s) +{ + int n = (s->io.read)(s->io_user_data, (char*)s->buffer_start, s->buflen); + if (n == 0) { + // at end of file, treat same as if from memory, but need to handle case + // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file + s->read_from_callbacks = 0; + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start + 1; + *s->img_buffer = 0; + } + else { + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start + n; + } +} + +stbi_inline static stbi_uc stbi__get8(stbi__context* s) +{ + if (s->img_buffer < s->img_buffer_end) + return *s->img_buffer++; + if (s->read_from_callbacks) { + stbi__refill_buffer(s); + return *s->img_buffer++; + } + return 0; +} + +stbi_inline static int stbi__at_eof(stbi__context* s) +{ + if (s->io.read) { + if (!(s->io.eof)(s->io_user_data)) + return 0; + // if feof() is true, check if buffer = end + // special case: we've only got the special 0 character at the end + if (s->read_from_callbacks == 0) + return 1; + } + + return s->img_buffer >= s->img_buffer_end; +} + +static void stbi__skip(stbi__context* s, int n) +{ + if (n < 0) { + s->img_buffer = s->img_buffer_end; + return; + } + if (s->io.read) { + int blen = (int)(s->img_buffer_end - s->img_buffer); + if (blen < n) { + s->img_buffer = s->img_buffer_end; + (s->io.skip)(s->io_user_data, n - blen); + return; + } + } + s->img_buffer += n; +} + +static int stbi__getn(stbi__context* s, stbi_uc* buffer, int n) +{ + if (s->io.read) { + int blen = (int)(s->img_buffer_end - s->img_buffer); + if (blen < n) { + int res, count; + + memcpy(buffer, s->img_buffer, blen); + + count = + (s->io.read)(s->io_user_data, (char*)buffer + blen, n - blen); + res = (count == (n - blen)); + s->img_buffer = s->img_buffer_end; + return res; + } + } + + if (s->img_buffer + n <= s->img_buffer_end) { + memcpy(buffer, s->img_buffer, n); + s->img_buffer += n; + return 1; + } + else + return 0; +} + +static int stbi__get16be(stbi__context* s) +{ + int z = stbi__get8(s); + return (z << 8) + stbi__get8(s); +} + +static stbi__uint32 stbi__get32be(stbi__context* s) +{ + stbi__uint32 z = stbi__get16be(s); + return (z << 16) + stbi__get16be(s); +} + +#if defined(STBI_NO_BMP) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) +// nothing +#else +static int stbi__get16le(stbi__context* s) +{ + int z = stbi__get8(s); + return z + (stbi__get8(s) << 8); +} +#endif + +#ifndef STBI_NO_BMP +static stbi__uint32 stbi__get32le(stbi__context* s) +{ + stbi__uint32 z = stbi__get16le(s); + return z + (stbi__get16le(s) << 16); +} +#endif + +#define STBI__BYTECAST(x) \ + ((stbi_uc)((x)&255)) // truncate int to byte without warnings + +////////////////////////////////////////////////////////////////////////////// +// +// generic converter from built-in img_n to req_comp +// individual types do this automatically as much as possible (e.g. jpeg +// does all cases internally since it needs to colorspace convert anyway, +// and it never has alpha, so very few cases ). png can automatically +// interleave an alpha=255 channel, but falls back to this for other cases +// +// assume data buffer is malloced, so malloc a new one and free that one +// only failure mode is malloc failing + +static stbi_uc stbi__compute_y(int r, int g, int b) +{ + return (stbi_uc)(((r * 77) + (g * 150) + (29 * b)) >> 8); +} + +static unsigned char* stbi__convert_format(unsigned char* data, int img_n, + int req_comp, unsigned int x, + unsigned int y) +{ + int i, j; + unsigned char* good; + + if (req_comp == img_n) + return data; + STBI_ASSERT(req_comp >= 1 && req_comp <= 4); + + good = (unsigned char*)stbi__malloc_mad3(req_comp, x, y, 0); + if (good == NULL) { + STBI_FREE(data); + return stbi__errpuc("outofmem", "Out of memory"); + } + + for (j = 0; j < (int)y; ++j) { + unsigned char* src = data + j * x * img_n; + unsigned char* dest = good + j * x * req_comp; + +#define STBI__COMBO(a, b) ((a)*8 + (b)) +#define STBI__CASE(a, b) \ + case STBI__COMBO(a, b): \ + for (i = x - 1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp + // components; avoid switch per pixel, so use switch per scanline and + // massive macros + switch (STBI__COMBO(img_n, req_comp)) { + STBI__CASE(1, 2) + { + dest[0] = src[0]; + dest[1] = 255; + } + break; + STBI__CASE(1, 3) { dest[0] = dest[1] = dest[2] = src[0]; } + break; + STBI__CASE(1, 4) + { + dest[0] = dest[1] = dest[2] = src[0]; + dest[3] = 255; + } + break; + STBI__CASE(2, 1) { dest[0] = src[0]; } + break; + STBI__CASE(2, 3) { dest[0] = dest[1] = dest[2] = src[0]; } + break; + STBI__CASE(2, 4) + { + dest[0] = dest[1] = dest[2] = src[0]; + dest[3] = src[1]; + } + break; + STBI__CASE(3, 4) + { + dest[0] = src[0]; + dest[1] = src[1]; + dest[2] = src[2]; + dest[3] = 255; + } + break; + STBI__CASE(3, 1) + { + dest[0] = stbi__compute_y(src[0], src[1], src[2]); + } + break; + STBI__CASE(3, 2) + { + dest[0] = stbi__compute_y(src[0], src[1], src[2]); + dest[1] = 255; + } + break; + STBI__CASE(4, 1) + { + dest[0] = stbi__compute_y(src[0], src[1], src[2]); + } + break; + STBI__CASE(4, 2) + { + dest[0] = stbi__compute_y(src[0], src[1], src[2]); + dest[1] = src[3]; + } + break; + STBI__CASE(4, 3) + { + dest[0] = src[0]; + dest[1] = src[1]; + dest[2] = src[2]; + } + break; + default: + STBI_ASSERT(0); + } +#undef STBI__CASE + } + + STBI_FREE(data); + return good; +} + +static stbi__uint16 stbi__compute_y_16(int r, int g, int b) +{ + return (stbi__uint16)(((r * 77) + (g * 150) + (29 * b)) >> 8); +} + +static stbi__uint16* stbi__convert_format16(stbi__uint16* data, int img_n, + int req_comp, unsigned int x, + unsigned int y) +{ + int i, j; + stbi__uint16* good; + + if (req_comp == img_n) + return data; + STBI_ASSERT(req_comp >= 1 && req_comp <= 4); + + good = (stbi__uint16*)stbi__malloc(req_comp * x * y * 2); + if (good == NULL) { + STBI_FREE(data); + return (stbi__uint16*)stbi__errpuc("outofmem", "Out of memory"); + } + + for (j = 0; j < (int)y; ++j) { + stbi__uint16* src = data + j * x * img_n; + stbi__uint16* dest = good + j * x * req_comp; + +#define STBI__COMBO(a, b) ((a)*8 + (b)) +#define STBI__CASE(a, b) \ + case STBI__COMBO(a, b): \ + for (i = x - 1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp + // components; avoid switch per pixel, so use switch per scanline and + // massive macros + switch (STBI__COMBO(img_n, req_comp)) { + STBI__CASE(1, 2) + { + dest[0] = src[0]; + dest[1] = 0xffff; + } + break; + STBI__CASE(1, 3) { dest[0] = dest[1] = dest[2] = src[0]; } + break; + STBI__CASE(1, 4) + { + dest[0] = dest[1] = dest[2] = src[0]; + dest[3] = 0xffff; + } + break; + STBI__CASE(2, 1) { dest[0] = src[0]; } + break; + STBI__CASE(2, 3) { dest[0] = dest[1] = dest[2] = src[0]; } + break; + STBI__CASE(2, 4) + { + dest[0] = dest[1] = dest[2] = src[0]; + dest[3] = src[1]; + } + break; + STBI__CASE(3, 4) + { + dest[0] = src[0]; + dest[1] = src[1]; + dest[2] = src[2]; + dest[3] = 0xffff; + } + break; + STBI__CASE(3, 1) + { + dest[0] = stbi__compute_y_16(src[0], src[1], src[2]); + } + break; + STBI__CASE(3, 2) + { + dest[0] = stbi__compute_y_16(src[0], src[1], src[2]); + dest[1] = 0xffff; + } + break; + STBI__CASE(4, 1) + { + dest[0] = stbi__compute_y_16(src[0], src[1], src[2]); + } + break; + STBI__CASE(4, 2) + { + dest[0] = stbi__compute_y_16(src[0], src[1], src[2]); + dest[1] = src[3]; + } + break; + STBI__CASE(4, 3) + { + dest[0] = src[0]; + dest[1] = src[1]; + dest[2] = src[2]; + } + break; + default: + STBI_ASSERT(0); + } +#undef STBI__CASE + } + + STBI_FREE(data); + return good; +} + +#ifndef STBI_NO_LINEAR +static float* stbi__ldr_to_hdr(stbi_uc* data, int x, int y, int comp) +{ + int i, k, n; + float* output; + if (!data) + return NULL; + output = (float*)stbi__malloc_mad4(x, y, comp, sizeof(float), 0); + if (output == NULL) { + STBI_FREE(data); + return stbi__errpf("outofmem", "Out of memory"); + } + // compute number of non-alpha components + if (comp & 1) + n = comp; + else + n = comp - 1; + for (i = 0; i < x * y; ++i) { + for (k = 0; k < n; ++k) { + output[i * comp + k] = + (float)(pow(data[i * comp + k] / 255.0f, stbi__l2h_gamma) * + stbi__l2h_scale); + } + } + if (n < comp) { + for (i = 0; i < x * y; ++i) { + output[i * comp + n] = data[i * comp + n] / 255.0f; + } + } + STBI_FREE(data); + return output; +} +#endif + +#ifndef STBI_NO_HDR +#define stbi__float2int(x) ((int)(x)) +static stbi_uc* stbi__hdr_to_ldr(float* data, int x, int y, int comp) +{ + int i, k, n; + stbi_uc* output; + if (!data) + return NULL; + output = (stbi_uc*)stbi__malloc_mad3(x, y, comp, 0); + if (output == NULL) { + STBI_FREE(data); + return stbi__errpuc("outofmem", "Out of memory"); + } + // compute number of non-alpha components + if (comp & 1) + n = comp; + else + n = comp - 1; + for (i = 0; i < x * y; ++i) { + for (k = 0; k < n; ++k) { + float z = (float)pow(data[i * comp + k] * stbi__h2l_scale_i, + stbi__h2l_gamma_i) * + 255 + + 0.5f; + if (z < 0) + z = 0; + if (z > 255) + z = 255; + output[i * comp + k] = (stbi_uc)stbi__float2int(z); + } + if (k < comp) { + float z = data[i * comp + k] * 255 + 0.5f; + if (z < 0) + z = 0; + if (z > 255) + z = 255; + output[i * comp + k] = (stbi_uc)stbi__float2int(z); + } + } + STBI_FREE(data); + return output; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// "baseline" JPEG/JFIF decoder +// +// simple implementation +// - doesn't support delayed output of y-dimension +// - simple interface (only one output format: 8-bit interleaved RGB) +// - doesn't try to recover corrupt jpegs +// - doesn't allow partial loading, loading multiple at once +// - still fast on x86 (copying globals into locals doesn't help x86) +// - allocates lots of intermediate memory (full size of all components) +// - non-interleaved case requires this anyway +// - allows good upsampling (see next) +// high-quality +// - upsampled channels are bilinearly interpolated, even across blocks +// - quality integer IDCT derived from IJG's 'slow' +// performance +// - fast huffman; reasonable integer IDCT +// - some SIMD kernels for common paths on targets with SSE2/NEON +// - uses a lot of intermediate memory, could cache poorly + +#ifndef STBI_NO_JPEG + +// huffman decoding acceleration +#define FAST_BITS 9 // larger handles more cases; smaller stomps less cache + +typedef struct { + stbi_uc fast[1 << FAST_BITS]; + // weirdly, repacking this into AoS is a 10% speed loss, instead of a win + stbi__uint16 code[256]; + stbi_uc values[256]; + stbi_uc size[257]; + unsigned int maxcode[18]; + int delta[17]; // old 'firstsymbol' - old 'firstcode' +} stbi__huffman; + +typedef struct { + stbi__context* s; + stbi__huffman huff_dc[4]; + stbi__huffman huff_ac[4]; + stbi__uint16 dequant[4][64]; + stbi__int16 fast_ac[4][1 << FAST_BITS]; + + // sizes for components, interleaved MCUs + int img_h_max, img_v_max; + int img_mcu_x, img_mcu_y; + int img_mcu_w, img_mcu_h; + + // definition of jpeg image component + struct { + int id; + int h, v; + int tq; + int hd, ha; + int dc_pred; + + int x, y, w2, h2; + stbi_uc* data; + void *raw_data, *raw_coeff; + stbi_uc* linebuf; + short* coeff; // progressive only + int coeff_w, coeff_h; // number of 8x8 coefficient blocks + } img_comp[4]; + + stbi__uint32 code_buffer; // jpeg entropy-coded buffer + int code_bits; // number of valid bits + unsigned char marker; // marker seen while filling entropy buffer + int nomore; // flag if we saw a marker so must stop + + int progressive; + int spec_start; + int spec_end; + int succ_high; + int succ_low; + int eob_run; + int jfif; + int app14_color_transform; // Adobe APP14 tag + int rgb; + + int scan_n, order[4]; + int restart_interval, todo; + + // kernels + void (*idct_block_kernel)(stbi_uc* out, int out_stride, short data[64]); + void (*YCbCr_to_RGB_kernel)(stbi_uc* out, const stbi_uc* y, + const stbi_uc* pcb, const stbi_uc* pcr, + int count, int step); + stbi_uc* (*resample_row_hv_2_kernel)(stbi_uc* out, stbi_uc* in_near, + stbi_uc* in_far, int w, int hs); +} stbi__jpeg; + +static int stbi__build_huffman(stbi__huffman* h, int* count) +{ + int i, j, k = 0; + unsigned int code; + // build size list for each symbol (from JPEG spec) + for (i = 0; i < 16; ++i) + for (j = 0; j < count[i]; ++j) + h->size[k++] = (stbi_uc)(i + 1); + h->size[k] = 0; + + // compute actual symbols (from jpeg spec) + code = 0; + k = 0; + for (j = 1; j <= 16; ++j) { + // compute delta to add to code to compute symbol id + h->delta[j] = k - code; + if (h->size[k] == j) { + while (h->size[k] == j) + h->code[k++] = (stbi__uint16)(code++); + if (code - 1 >= (1u << j)) + return stbi__err("bad code lengths", "Corrupt JPEG"); + } + // compute largest code + 1 for this size, preshifted as needed later + h->maxcode[j] = code << (16 - j); + code <<= 1; + } + h->maxcode[j] = 0xffffffff; + + // build non-spec acceleration table; 255 is flag for not-accelerated + memset(h->fast, 255, 1 << FAST_BITS); + for (i = 0; i < k; ++i) { + int s = h->size[i]; + if (s <= FAST_BITS) { + int c = h->code[i] << (FAST_BITS - s); + int m = 1 << (FAST_BITS - s); + for (j = 0; j < m; ++j) { + h->fast[c + j] = (stbi_uc)i; + } + } + } + return 1; +} + +// build a table that decodes both magnitude and value of small ACs in +// one go. +static void stbi__build_fast_ac(stbi__int16* fast_ac, stbi__huffman* h) +{ + int i; + for (i = 0; i < (1 << FAST_BITS); ++i) { + stbi_uc fast = h->fast[i]; + fast_ac[i] = 0; + if (fast < 255) { + int rs = h->values[fast]; + int run = (rs >> 4) & 15; + int magbits = rs & 15; + int len = h->size[fast]; + + if (magbits && len + magbits <= FAST_BITS) { + // magnitude code followed by receive_extend code + int k = ((i << len) & ((1 << FAST_BITS) - 1)) >> + (FAST_BITS - magbits); + int m = 1 << (magbits - 1); + if (k < m) + k += (~0U << magbits) + 1; + // if the result is small enough, we can fit it in fast_ac table + if (k >= -128 && k <= 127) + fast_ac[i] = + (stbi__int16)((k * 256) + (run * 16) + (len + magbits)); + } + } + } +} + +static void stbi__grow_buffer_unsafe(stbi__jpeg* j) +{ + do { + unsigned int b = j->nomore ? 0 : stbi__get8(j->s); + if (b == 0xff) { + int c = stbi__get8(j->s); + while (c == 0xff) + c = stbi__get8(j->s); // consume fill bytes + if (c != 0) { + j->marker = (unsigned char)c; + j->nomore = 1; + return; + } + } + j->code_buffer |= b << (24 - j->code_bits); + j->code_bits += 8; + } while (j->code_bits <= 24); +} + +// (1 << n) - 1 +static const stbi__uint32 stbi__bmask[17] = { + 0, 1, 3, 7, 15, 31, 63, 127, 255, + 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535}; + +// decode a jpeg huffman value from the bitstream +stbi_inline static int stbi__jpeg_huff_decode(stbi__jpeg* j, stbi__huffman* h) +{ + unsigned int temp; + int c, k; + + if (j->code_bits < 16) + stbi__grow_buffer_unsafe(j); + + // look at the top FAST_BITS and determine what symbol ID it is, + // if the code is <= FAST_BITS + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS) - 1); + k = h->fast[c]; + if (k < 255) { + int s = h->size[k]; + if (s > j->code_bits) + return -1; + j->code_buffer <<= s; + j->code_bits -= s; + return h->values[k]; + } + + // naive test is to shift the code_buffer down so k bits are + // valid, then test against maxcode. To speed this up, we've + // preshifted maxcode left so that it has (16-k) 0s at the + // end; in other words, regardless of the number of bits, it + // wants to be compared against something shifted to have 16; + // that way we don't need to shift inside the loop. + temp = j->code_buffer >> 16; + for (k = FAST_BITS + 1;; ++k) + if (temp < h->maxcode[k]) + break; + if (k == 17) { + // error! code not found + j->code_bits -= 16; + return -1; + } + + if (k > j->code_bits) + return -1; + + // convert the huffman code to the symbol id + c = ((j->code_buffer >> (32 - k)) & stbi__bmask[k]) + h->delta[k]; + STBI_ASSERT((((j->code_buffer) >> (32 - h->size[c])) & + stbi__bmask[h->size[c]]) == h->code[c]); + + // convert the id to a symbol + j->code_bits -= k; + j->code_buffer <<= k; + return h->values[c]; +} + +// bias[n] = (-1<code_bits < n) + stbi__grow_buffer_unsafe(j); + + sgn = (stbi__int32)j->code_buffer >> 31; // sign bit is always in MSB + k = stbi_lrot(j->code_buffer, n); + STBI_ASSERT(n >= 0 && + n < (int)(sizeof(stbi__bmask) / sizeof(*stbi__bmask))); + j->code_buffer = k & ~stbi__bmask[n]; + k &= stbi__bmask[n]; + j->code_bits -= n; + return k + (stbi__jbias[n] & ~sgn); +} + +// get some unsigned bits +stbi_inline static int stbi__jpeg_get_bits(stbi__jpeg* j, int n) +{ + unsigned int k; + if (j->code_bits < n) + stbi__grow_buffer_unsafe(j); + k = stbi_lrot(j->code_buffer, n); + j->code_buffer = k & ~stbi__bmask[n]; + k &= stbi__bmask[n]; + j->code_bits -= n; + return k; +} + +stbi_inline static int stbi__jpeg_get_bit(stbi__jpeg* j) +{ + unsigned int k; + if (j->code_bits < 1) + stbi__grow_buffer_unsafe(j); + k = j->code_buffer; + j->code_buffer <<= 1; + --j->code_bits; + return k & 0x80000000; +} + +// given a value that's at position X in the zigzag stream, +// where does it appear in the 8x8 matrix coded as row-major? +static const stbi_uc stbi__jpeg_dezigzag[64 + 15] = { + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, + 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, + 54, 47, 55, 62, 63, + // let corrupt input sample past end + 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63}; + +// decode one 64-entry block-- +static int stbi__jpeg_decode_block(stbi__jpeg* j, short data[64], + stbi__huffman* hdc, stbi__huffman* hac, + stbi__int16* fac, int b, + stbi__uint16* dequant) +{ + int diff, dc, k; + int t; + + if (j->code_bits < 16) + stbi__grow_buffer_unsafe(j); + t = stbi__jpeg_huff_decode(j, hdc); + if (t < 0) + return stbi__err("bad huffman code", "Corrupt JPEG"); + + // 0 all the ac values now so we can do it 32-bits at a time + memset(data, 0, 64 * sizeof(data[0])); + + diff = t ? stbi__extend_receive(j, t) : 0; + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + data[0] = (short)(dc * dequant[0]); + + // decode AC components, see JPEG spec + k = 1; + do { + unsigned int zig; + int c, r, s; + if (j->code_bits < 16) + stbi__grow_buffer_unsafe(j); + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS) - 1); + r = fac[c]; + if (r) { // fast-AC path + k += (r >> 4) & 15; // run + s = r & 15; // combined length + j->code_buffer <<= s; + j->code_bits -= s; + // decode into unzigzag'd location + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short)((r >> 8) * dequant[zig]); + } + else { + int rs = stbi__jpeg_huff_decode(j, hac); + if (rs < 0) + return stbi__err("bad huffman code", "Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (rs != 0xf0) + break; // end block + k += 16; + } + else { + k += r; + // decode into unzigzag'd location + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short)(stbi__extend_receive(j, s) * dequant[zig]); + } + } + } while (k < 64); + return 1; +} + +static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg* j, short data[64], + stbi__huffman* hdc, int b) +{ + int diff, dc; + int t; + if (j->spec_end != 0) + return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + + if (j->code_bits < 16) + stbi__grow_buffer_unsafe(j); + + if (j->succ_high == 0) { + // first scan for DC coefficient, must be first + memset(data, 0, 64 * sizeof(data[0])); // 0 all the ac values now + t = stbi__jpeg_huff_decode(j, hdc); + diff = t ? stbi__extend_receive(j, t) : 0; + + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + data[0] = (short)(dc << j->succ_low); + } + else { + // refinement scan for DC coefficient + if (stbi__jpeg_get_bit(j)) + data[0] += (short)(1 << j->succ_low); + } + return 1; +} + +// @OPTIMIZE: store non-zigzagged during the decode passes, +// and only de-zigzag when dequantizing +static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg* j, short data[64], + stbi__huffman* hac, stbi__int16* fac) +{ + int k; + if (j->spec_start == 0) + return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + + if (j->succ_high == 0) { + int shift = j->succ_low; + + if (j->eob_run) { + --j->eob_run; + return 1; + } + + k = j->spec_start; + do { + unsigned int zig; + int c, r, s; + if (j->code_bits < 16) + stbi__grow_buffer_unsafe(j); + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS) - 1); + r = fac[c]; + if (r) { // fast-AC path + k += (r >> 4) & 15; // run + s = r & 15; // combined length + j->code_buffer <<= s; + j->code_bits -= s; + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short)((r >> 8) << shift); + } + else { + int rs = stbi__jpeg_huff_decode(j, hac); + if (rs < 0) + return stbi__err("bad huffman code", "Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + j->eob_run = (1 << r); + if (r) + j->eob_run += stbi__jpeg_get_bits(j, r); + --j->eob_run; + break; + } + k += 16; + } + else { + k += r; + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short)(stbi__extend_receive(j, s) << shift); + } + } + } while (k <= j->spec_end); + } + else { + // refinement scan for these AC coefficients + + short bit = (short)(1 << j->succ_low); + + if (j->eob_run) { + --j->eob_run; + for (k = j->spec_start; k <= j->spec_end; ++k) { + short* p = &data[stbi__jpeg_dezigzag[k]]; + if (*p != 0) + if (stbi__jpeg_get_bit(j)) + if ((*p & bit) == 0) { + if (*p > 0) + *p += bit; + else + *p -= bit; + } + } + } + else { + k = j->spec_start; + do { + int r, s; + int rs = stbi__jpeg_huff_decode( + j, hac); // @OPTIMIZE see if we can use the fast path here, + // advance-by-r is so slow, eh + if (rs < 0) + return stbi__err("bad huffman code", "Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + j->eob_run = (1 << r) - 1; + if (r) + j->eob_run += stbi__jpeg_get_bits(j, r); + r = 64; // force end of block + } + else { + // r=15 s=0 should write 16 0s, so we just do + // a run of 15 0s and then write s (which is 0), + // so we don't have to do anything special here + } + } + else { + if (s != 1) + return stbi__err("bad huffman code", "Corrupt JPEG"); + // sign bit + if (stbi__jpeg_get_bit(j)) + s = bit; + else + s = -bit; + } + + // advance by r + while (k <= j->spec_end) { + short* p = &data[stbi__jpeg_dezigzag[k++]]; + if (*p != 0) { + if (stbi__jpeg_get_bit(j)) + if ((*p & bit) == 0) { + if (*p > 0) + *p += bit; + else + *p -= bit; + } + } + else { + if (r == 0) { + *p = (short)s; + break; + } + --r; + } + } + } while (k <= j->spec_end); + } + } + return 1; +} + +// take a -128..127 value and stbi__clamp it and convert to 0..255 +stbi_inline static stbi_uc stbi__clamp(int x) +{ + // trick to use a single test to catch both cases + if ((unsigned int)x > 255) { + if (x < 0) + return 0; + if (x > 255) + return 255; + } + return (stbi_uc)x; +} + +#define stbi__f2f(x) ((int)(((x)*4096 + 0.5))) +#define stbi__fsh(x) ((x)*4096) + +// derived from jidctint -- DCT_ISLOW +#define STBI__IDCT_1D(s0, s1, s2, s3, s4, s5, s6, s7) \ + int t0, t1, t2, t3, p1, p2, p3, p4, p5, x0, x1, x2, x3; \ + p2 = s2; \ + p3 = s6; \ + p1 = (p2 + p3) * stbi__f2f(0.5411961f); \ + t2 = p1 + p3 * stbi__f2f(-1.847759065f); \ + t3 = p1 + p2 * stbi__f2f(0.765366865f); \ + p2 = s0; \ + p3 = s4; \ + t0 = stbi__fsh(p2 + p3); \ + t1 = stbi__fsh(p2 - p3); \ + x0 = t0 + t3; \ + x3 = t0 - t3; \ + x1 = t1 + t2; \ + x2 = t1 - t2; \ + t0 = s7; \ + t1 = s5; \ + t2 = s3; \ + t3 = s1; \ + p3 = t0 + t2; \ + p4 = t1 + t3; \ + p1 = t0 + t3; \ + p2 = t1 + t2; \ + p5 = (p3 + p4) * stbi__f2f(1.175875602f); \ + t0 = t0 * stbi__f2f(0.298631336f); \ + t1 = t1 * stbi__f2f(2.053119869f); \ + t2 = t2 * stbi__f2f(3.072711026f); \ + t3 = t3 * stbi__f2f(1.501321110f); \ + p1 = p5 + p1 * stbi__f2f(-0.899976223f); \ + p2 = p5 + p2 * stbi__f2f(-2.562915447f); \ + p3 = p3 * stbi__f2f(-1.961570560f); \ + p4 = p4 * stbi__f2f(-0.390180644f); \ + t3 += p1 + p4; \ + t2 += p2 + p3; \ + t1 += p2 + p4; \ + t0 += p1 + p3; + +static void stbi__idct_block(stbi_uc* out, int out_stride, short data[64]) +{ + int i, val[64], *v = val; + stbi_uc* o; + short* d = data; + + // columns + for (i = 0; i < 8; ++i, ++d, ++v) { + // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing + if (d[8] == 0 && d[16] == 0 && d[24] == 0 && d[32] == 0 && d[40] == 0 && + d[48] == 0 && d[56] == 0) { + // no shortcut 0 seconds + // (1|2|3|4|5|6|7)==0 0 seconds + // all separate -0.047 seconds + // 1 && 2|3 && 4|5 && 6|7: -0.047 seconds + int dcterm = d[0] * 4; + v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = + dcterm; + } + else { + STBI__IDCT_1D(d[0], d[8], d[16], d[24], d[32], d[40], d[48], d[56]) + // constants scaled things up by 1<<12; let's bring them back + // down, but keep 2 extra bits of precision + x0 += 512; + x1 += 512; + x2 += 512; + x3 += 512; + v[0] = (x0 + t3) >> 10; + v[56] = (x0 - t3) >> 10; + v[8] = (x1 + t2) >> 10; + v[48] = (x1 - t2) >> 10; + v[16] = (x2 + t1) >> 10; + v[40] = (x2 - t1) >> 10; + v[24] = (x3 + t0) >> 10; + v[32] = (x3 - t0) >> 10; + } + } + + for (i = 0, v = val, o = out; i < 8; ++i, v += 8, o += out_stride) { + // no fast case since the first 1D IDCT spread components out + STBI__IDCT_1D(v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]) + // constants scaled things up by 1<<12, plus we had 1<<2 from first + // loop, plus horizontal and vertical each scale by sqrt(8) so together + // we've got an extra 1<<3, so 1<<17 total we need to remove. + // so we want to round that, which means adding 0.5 * 1<<17, + // aka 65536. Also, we'll end up with -128 to 127 that we want + // to encode as 0..255 by adding 128, so we'll add that before the shift + x0 += 65536 + (128 << 17); + x1 += 65536 + (128 << 17); + x2 += 65536 + (128 << 17); + x3 += 65536 + (128 << 17); + // tried computing the shifts into temps, or'ing the temps to see + // if any were out of range, but that was slower + o[0] = stbi__clamp((x0 + t3) >> 17); + o[7] = stbi__clamp((x0 - t3) >> 17); + o[1] = stbi__clamp((x1 + t2) >> 17); + o[6] = stbi__clamp((x1 - t2) >> 17); + o[2] = stbi__clamp((x2 + t1) >> 17); + o[5] = stbi__clamp((x2 - t1) >> 17); + o[3] = stbi__clamp((x3 + t0) >> 17); + o[4] = stbi__clamp((x3 - t0) >> 17); + } +} + +#ifdef STBI_SSE2 +// sse2 integer IDCT. not the fastest possible implementation but it +// produces bit-identical results to the generic C version so it's +// fully "transparent". +static void stbi__idct_simd(stbi_uc* out, int out_stride, short data[64]) +{ + // This is constructed to match our regular (generic) integer IDCT exactly. + __m128i row0, row1, row2, row3, row4, row5, row6, row7; + __m128i tmp; + +// dot product constant: even elems=x, odd elems=y +#define dct_const(x, y) _mm_setr_epi16((x), (y), (x), (y), (x), (y), (x), (y)) + +// out(0) = c0[even]*x + c0[odd]*y (c0, x, y 16-bit, out 32-bit) +// out(1) = c1[even]*x + c1[odd]*y +#define dct_rot(out0, out1, x, y, c0, c1) \ + __m128i c0##lo = _mm_unpacklo_epi16((x), (y)); \ + __m128i c0##hi = _mm_unpackhi_epi16((x), (y)); \ + __m128i out0##_l = _mm_madd_epi16(c0##lo, c0); \ + __m128i out0##_h = _mm_madd_epi16(c0##hi, c0); \ + __m128i out1##_l = _mm_madd_epi16(c0##lo, c1); \ + __m128i out1##_h = _mm_madd_epi16(c0##hi, c1) + +// out = in << 12 (in 16-bit, out 32-bit) +#define dct_widen(out, in) \ + __m128i out##_l = \ + _mm_srai_epi32(_mm_unpacklo_epi16(_mm_setzero_si128(), (in)), 4); \ + __m128i out##_h = \ + _mm_srai_epi32(_mm_unpackhi_epi16(_mm_setzero_si128(), (in)), 4) + +// wide add +#define dct_wadd(out, a, b) \ + __m128i out##_l = _mm_add_epi32(a##_l, b##_l); \ + __m128i out##_h = _mm_add_epi32(a##_h, b##_h) + +// wide sub +#define dct_wsub(out, a, b) \ + __m128i out##_l = _mm_sub_epi32(a##_l, b##_l); \ + __m128i out##_h = _mm_sub_epi32(a##_h, b##_h) + +// butterfly a/b, add bias, then shift by "s" and pack +#define dct_bfly32o(out0, out1, a, b, bias, s) \ + { \ + __m128i abiased_l = _mm_add_epi32(a##_l, bias); \ + __m128i abiased_h = _mm_add_epi32(a##_h, bias); \ + dct_wadd(sum, abiased, b); \ + dct_wsub(dif, abiased, b); \ + out0 = _mm_packs_epi32(_mm_srai_epi32(sum_l, s), \ + _mm_srai_epi32(sum_h, s)); \ + out1 = _mm_packs_epi32(_mm_srai_epi32(dif_l, s), \ + _mm_srai_epi32(dif_h, s)); \ + } + +// 8-bit interleave step (for transposes) +#define dct_interleave8(a, b) \ + tmp = a; \ + a = _mm_unpacklo_epi8(a, b); \ + b = _mm_unpackhi_epi8(tmp, b) + +// 16-bit interleave step (for transposes) +#define dct_interleave16(a, b) \ + tmp = a; \ + a = _mm_unpacklo_epi16(a, b); \ + b = _mm_unpackhi_epi16(tmp, b) + +#define dct_pass(bias, shift) \ + { \ + /* even part */ \ + dct_rot(t2e, t3e, row2, row6, rot0_0, rot0_1); \ + __m128i sum04 = _mm_add_epi16(row0, row4); \ + __m128i dif04 = _mm_sub_epi16(row0, row4); \ + dct_widen(t0e, sum04); \ + dct_widen(t1e, dif04); \ + dct_wadd(x0, t0e, t3e); \ + dct_wsub(x3, t0e, t3e); \ + dct_wadd(x1, t1e, t2e); \ + dct_wsub(x2, t1e, t2e); \ + /* odd part */ \ + dct_rot(y0o, y2o, row7, row3, rot2_0, rot2_1); \ + dct_rot(y1o, y3o, row5, row1, rot3_0, rot3_1); \ + __m128i sum17 = _mm_add_epi16(row1, row7); \ + __m128i sum35 = _mm_add_epi16(row3, row5); \ + dct_rot(y4o, y5o, sum17, sum35, rot1_0, rot1_1); \ + dct_wadd(x4, y0o, y4o); \ + dct_wadd(x5, y1o, y5o); \ + dct_wadd(x6, y2o, y5o); \ + dct_wadd(x7, y3o, y4o); \ + dct_bfly32o(row0, row7, x0, x7, bias, shift); \ + dct_bfly32o(row1, row6, x1, x6, bias, shift); \ + dct_bfly32o(row2, row5, x2, x5, bias, shift); \ + dct_bfly32o(row3, row4, x3, x4, bias, shift); \ + } + + __m128i rot0_0 = + dct_const(stbi__f2f(0.5411961f), + stbi__f2f(0.5411961f) + stbi__f2f(-1.847759065f)); + __m128i rot0_1 = dct_const(stbi__f2f(0.5411961f) + stbi__f2f(0.765366865f), + stbi__f2f(0.5411961f)); + __m128i rot1_0 = + dct_const(stbi__f2f(1.175875602f) + stbi__f2f(-0.899976223f), + stbi__f2f(1.175875602f)); + __m128i rot1_1 = + dct_const(stbi__f2f(1.175875602f), + stbi__f2f(1.175875602f) + stbi__f2f(-2.562915447f)); + __m128i rot2_0 = + dct_const(stbi__f2f(-1.961570560f) + stbi__f2f(0.298631336f), + stbi__f2f(-1.961570560f)); + __m128i rot2_1 = + dct_const(stbi__f2f(-1.961570560f), + stbi__f2f(-1.961570560f) + stbi__f2f(3.072711026f)); + __m128i rot3_0 = + dct_const(stbi__f2f(-0.390180644f) + stbi__f2f(2.053119869f), + stbi__f2f(-0.390180644f)); + __m128i rot3_1 = + dct_const(stbi__f2f(-0.390180644f), + stbi__f2f(-0.390180644f) + stbi__f2f(1.501321110f)); + + // rounding biases in column/row passes, see stbi__idct_block for + // explanation. + __m128i bias_0 = _mm_set1_epi32(512); + __m128i bias_1 = _mm_set1_epi32(65536 + (128 << 17)); + + // load + row0 = _mm_load_si128((const __m128i*)(data + 0 * 8)); + row1 = _mm_load_si128((const __m128i*)(data + 1 * 8)); + row2 = _mm_load_si128((const __m128i*)(data + 2 * 8)); + row3 = _mm_load_si128((const __m128i*)(data + 3 * 8)); + row4 = _mm_load_si128((const __m128i*)(data + 4 * 8)); + row5 = _mm_load_si128((const __m128i*)(data + 5 * 8)); + row6 = _mm_load_si128((const __m128i*)(data + 6 * 8)); + row7 = _mm_load_si128((const __m128i*)(data + 7 * 8)); + + // column pass + dct_pass(bias_0, 10); + + { + // 16bit 8x8 transpose pass 1 + dct_interleave16(row0, row4); + dct_interleave16(row1, row5); + dct_interleave16(row2, row6); + dct_interleave16(row3, row7); + + // transpose pass 2 + dct_interleave16(row0, row2); + dct_interleave16(row1, row3); + dct_interleave16(row4, row6); + dct_interleave16(row5, row7); + + // transpose pass 3 + dct_interleave16(row0, row1); + dct_interleave16(row2, row3); + dct_interleave16(row4, row5); + dct_interleave16(row6, row7); + } + + // row pass + dct_pass(bias_1, 17); + + { + // pack + __m128i p0 = _mm_packus_epi16(row0, row1); // a0a1a2a3...a7b0b1b2b3...b7 + __m128i p1 = _mm_packus_epi16(row2, row3); + __m128i p2 = _mm_packus_epi16(row4, row5); + __m128i p3 = _mm_packus_epi16(row6, row7); + + // 8bit 8x8 transpose pass 1 + dct_interleave8(p0, p2); // a0e0a1e1... + dct_interleave8(p1, p3); // c0g0c1g1... + + // transpose pass 2 + dct_interleave8(p0, p1); // a0c0e0g0... + dct_interleave8(p2, p3); // b0d0f0h0... + + // transpose pass 3 + dct_interleave8(p0, p2); // a0b0c0d0... + dct_interleave8(p1, p3); // a4b4c4d4... + + // store + _mm_storel_epi64((__m128i*)out, p0); + out += out_stride; + _mm_storel_epi64((__m128i*)out, _mm_shuffle_epi32(p0, 0x4e)); + out += out_stride; + _mm_storel_epi64((__m128i*)out, p2); + out += out_stride; + _mm_storel_epi64((__m128i*)out, _mm_shuffle_epi32(p2, 0x4e)); + out += out_stride; + _mm_storel_epi64((__m128i*)out, p1); + out += out_stride; + _mm_storel_epi64((__m128i*)out, _mm_shuffle_epi32(p1, 0x4e)); + out += out_stride; + _mm_storel_epi64((__m128i*)out, p3); + out += out_stride; + _mm_storel_epi64((__m128i*)out, _mm_shuffle_epi32(p3, 0x4e)); + } + +#undef dct_const +#undef dct_rot +#undef dct_widen +#undef dct_wadd +#undef dct_wsub +#undef dct_bfly32o +#undef dct_interleave8 +#undef dct_interleave16 +#undef dct_pass +} + +#endif // STBI_SSE2 + +#ifdef STBI_NEON + +// NEON integer IDCT. should produce bit-identical +// results to the generic C version. +static void stbi__idct_simd(stbi_uc* out, int out_stride, short data[64]) +{ + int16x8_t row0, row1, row2, row3, row4, row5, row6, row7; + + int16x4_t rot0_0 = vdup_n_s16(stbi__f2f(0.5411961f)); + int16x4_t rot0_1 = vdup_n_s16(stbi__f2f(-1.847759065f)); + int16x4_t rot0_2 = vdup_n_s16(stbi__f2f(0.765366865f)); + int16x4_t rot1_0 = vdup_n_s16(stbi__f2f(1.175875602f)); + int16x4_t rot1_1 = vdup_n_s16(stbi__f2f(-0.899976223f)); + int16x4_t rot1_2 = vdup_n_s16(stbi__f2f(-2.562915447f)); + int16x4_t rot2_0 = vdup_n_s16(stbi__f2f(-1.961570560f)); + int16x4_t rot2_1 = vdup_n_s16(stbi__f2f(-0.390180644f)); + int16x4_t rot3_0 = vdup_n_s16(stbi__f2f(0.298631336f)); + int16x4_t rot3_1 = vdup_n_s16(stbi__f2f(2.053119869f)); + int16x4_t rot3_2 = vdup_n_s16(stbi__f2f(3.072711026f)); + int16x4_t rot3_3 = vdup_n_s16(stbi__f2f(1.501321110f)); + +#define dct_long_mul(out, inq, coeff) \ + int32x4_t out##_l = vmull_s16(vget_low_s16(inq), coeff); \ + int32x4_t out##_h = vmull_s16(vget_high_s16(inq), coeff) + +#define dct_long_mac(out, acc, inq, coeff) \ + int32x4_t out##_l = vmlal_s16(acc##_l, vget_low_s16(inq), coeff); \ + int32x4_t out##_h = vmlal_s16(acc##_h, vget_high_s16(inq), coeff) + +#define dct_widen(out, inq) \ + int32x4_t out##_l = vshll_n_s16(vget_low_s16(inq), 12); \ + int32x4_t out##_h = vshll_n_s16(vget_high_s16(inq), 12) + +// wide add +#define dct_wadd(out, a, b) \ + int32x4_t out##_l = vaddq_s32(a##_l, b##_l); \ + int32x4_t out##_h = vaddq_s32(a##_h, b##_h) + +// wide sub +#define dct_wsub(out, a, b) \ + int32x4_t out##_l = vsubq_s32(a##_l, b##_l); \ + int32x4_t out##_h = vsubq_s32(a##_h, b##_h) + +// butterfly a/b, then shift using "shiftop" by "s" and pack +#define dct_bfly32o(out0, out1, a, b, shiftop, s) \ + { \ + dct_wadd(sum, a, b); \ + dct_wsub(dif, a, b); \ + out0 = vcombine_s16(shiftop(sum_l, s), shiftop(sum_h, s)); \ + out1 = vcombine_s16(shiftop(dif_l, s), shiftop(dif_h, s)); \ + } + +#define dct_pass(shiftop, shift) \ + { \ + /* even part */ \ + int16x8_t sum26 = vaddq_s16(row2, row6); \ + dct_long_mul(p1e, sum26, rot0_0); \ + dct_long_mac(t2e, p1e, row6, rot0_1); \ + dct_long_mac(t3e, p1e, row2, rot0_2); \ + int16x8_t sum04 = vaddq_s16(row0, row4); \ + int16x8_t dif04 = vsubq_s16(row0, row4); \ + dct_widen(t0e, sum04); \ + dct_widen(t1e, dif04); \ + dct_wadd(x0, t0e, t3e); \ + dct_wsub(x3, t0e, t3e); \ + dct_wadd(x1, t1e, t2e); \ + dct_wsub(x2, t1e, t2e); \ + /* odd part */ \ + int16x8_t sum15 = vaddq_s16(row1, row5); \ + int16x8_t sum17 = vaddq_s16(row1, row7); \ + int16x8_t sum35 = vaddq_s16(row3, row5); \ + int16x8_t sum37 = vaddq_s16(row3, row7); \ + int16x8_t sumodd = vaddq_s16(sum17, sum35); \ + dct_long_mul(p5o, sumodd, rot1_0); \ + dct_long_mac(p1o, p5o, sum17, rot1_1); \ + dct_long_mac(p2o, p5o, sum35, rot1_2); \ + dct_long_mul(p3o, sum37, rot2_0); \ + dct_long_mul(p4o, sum15, rot2_1); \ + dct_wadd(sump13o, p1o, p3o); \ + dct_wadd(sump24o, p2o, p4o); \ + dct_wadd(sump23o, p2o, p3o); \ + dct_wadd(sump14o, p1o, p4o); \ + dct_long_mac(x4, sump13o, row7, rot3_0); \ + dct_long_mac(x5, sump24o, row5, rot3_1); \ + dct_long_mac(x6, sump23o, row3, rot3_2); \ + dct_long_mac(x7, sump14o, row1, rot3_3); \ + dct_bfly32o(row0, row7, x0, x7, shiftop, shift); \ + dct_bfly32o(row1, row6, x1, x6, shiftop, shift); \ + dct_bfly32o(row2, row5, x2, x5, shiftop, shift); \ + dct_bfly32o(row3, row4, x3, x4, shiftop, shift); \ + } + + // load + row0 = vld1q_s16(data + 0 * 8); + row1 = vld1q_s16(data + 1 * 8); + row2 = vld1q_s16(data + 2 * 8); + row3 = vld1q_s16(data + 3 * 8); + row4 = vld1q_s16(data + 4 * 8); + row5 = vld1q_s16(data + 5 * 8); + row6 = vld1q_s16(data + 6 * 8); + row7 = vld1q_s16(data + 7 * 8); + + // add DC bias + row0 = vaddq_s16(row0, vsetq_lane_s16(1024, vdupq_n_s16(0), 0)); + + // column pass + dct_pass(vrshrn_n_s32, 10); + + // 16bit 8x8 transpose + { +// these three map to a single VTRN.16, VTRN.32, and VSWP, respectively. +// whether compilers actually get this is another story, sadly. +#define dct_trn16(x, y) \ + { \ + int16x8x2_t t = vtrnq_s16(x, y); \ + x = t.val[0]; \ + y = t.val[1]; \ + } +#define dct_trn32(x, y) \ + { \ + int32x4x2_t t = \ + vtrnq_s32(vreinterpretq_s32_s16(x), vreinterpretq_s32_s16(y)); \ + x = vreinterpretq_s16_s32(t.val[0]); \ + y = vreinterpretq_s16_s32(t.val[1]); \ + } +#define dct_trn64(x, y) \ + { \ + int16x8_t x0 = x; \ + int16x8_t y0 = y; \ + x = vcombine_s16(vget_low_s16(x0), vget_low_s16(y0)); \ + y = vcombine_s16(vget_high_s16(x0), vget_high_s16(y0)); \ + } + + // pass 1 + dct_trn16(row0, row1); // a0b0a2b2a4b4a6b6 + dct_trn16(row2, row3); + dct_trn16(row4, row5); + dct_trn16(row6, row7); + + // pass 2 + dct_trn32(row0, row2); // a0b0c0d0a4b4c4d4 + dct_trn32(row1, row3); + dct_trn32(row4, row6); + dct_trn32(row5, row7); + + // pass 3 + dct_trn64(row0, row4); // a0b0c0d0e0f0g0h0 + dct_trn64(row1, row5); + dct_trn64(row2, row6); + dct_trn64(row3, row7); + +#undef dct_trn16 +#undef dct_trn32 +#undef dct_trn64 + } + + // row pass + // vrshrn_n_s32 only supports shifts up to 16, we need + // 17. so do a non-rounding shift of 16 first then follow + // up with a rounding shift by 1. + dct_pass(vshrn_n_s32, 16); + + { + // pack and round + uint8x8_t p0 = vqrshrun_n_s16(row0, 1); + uint8x8_t p1 = vqrshrun_n_s16(row1, 1); + uint8x8_t p2 = vqrshrun_n_s16(row2, 1); + uint8x8_t p3 = vqrshrun_n_s16(row3, 1); + uint8x8_t p4 = vqrshrun_n_s16(row4, 1); + uint8x8_t p5 = vqrshrun_n_s16(row5, 1); + uint8x8_t p6 = vqrshrun_n_s16(row6, 1); + uint8x8_t p7 = vqrshrun_n_s16(row7, 1); + + // again, these can translate into one instruction, but often don't. +#define dct_trn8_8(x, y) \ + { \ + uint8x8x2_t t = vtrn_u8(x, y); \ + x = t.val[0]; \ + y = t.val[1]; \ + } +#define dct_trn8_16(x, y) \ + { \ + uint16x4x2_t t = \ + vtrn_u16(vreinterpret_u16_u8(x), vreinterpret_u16_u8(y)); \ + x = vreinterpret_u8_u16(t.val[0]); \ + y = vreinterpret_u8_u16(t.val[1]); \ + } +#define dct_trn8_32(x, y) \ + { \ + uint32x2x2_t t = \ + vtrn_u32(vreinterpret_u32_u8(x), vreinterpret_u32_u8(y)); \ + x = vreinterpret_u8_u32(t.val[0]); \ + y = vreinterpret_u8_u32(t.val[1]); \ + } + + // sadly can't use interleaved stores here since we only write + // 8 bytes to each scan line! + + // 8x8 8-bit transpose pass 1 + dct_trn8_8(p0, p1); + dct_trn8_8(p2, p3); + dct_trn8_8(p4, p5); + dct_trn8_8(p6, p7); + + // pass 2 + dct_trn8_16(p0, p2); + dct_trn8_16(p1, p3); + dct_trn8_16(p4, p6); + dct_trn8_16(p5, p7); + + // pass 3 + dct_trn8_32(p0, p4); + dct_trn8_32(p1, p5); + dct_trn8_32(p2, p6); + dct_trn8_32(p3, p7); + + // store + vst1_u8(out, p0); + out += out_stride; + vst1_u8(out, p1); + out += out_stride; + vst1_u8(out, p2); + out += out_stride; + vst1_u8(out, p3); + out += out_stride; + vst1_u8(out, p4); + out += out_stride; + vst1_u8(out, p5); + out += out_stride; + vst1_u8(out, p6); + out += out_stride; + vst1_u8(out, p7); + +#undef dct_trn8_8 +#undef dct_trn8_16 +#undef dct_trn8_32 + } + +#undef dct_long_mul +#undef dct_long_mac +#undef dct_widen +#undef dct_wadd +#undef dct_wsub +#undef dct_bfly32o +#undef dct_pass +} + +#endif // STBI_NEON + +#define STBI__MARKER_none 0xff +// if there's a pending marker from the entropy stream, return that +// otherwise, fetch from the stream and get a marker. if there's no +// marker, return 0xff, which is never a valid marker value +static stbi_uc stbi__get_marker(stbi__jpeg* j) +{ + stbi_uc x; + if (j->marker != STBI__MARKER_none) { + x = j->marker; + j->marker = STBI__MARKER_none; + return x; + } + x = stbi__get8(j->s); + if (x != 0xff) + return STBI__MARKER_none; + while (x == 0xff) + x = stbi__get8(j->s); // consume repeated 0xff fill bytes + return x; +} + +// in each scan, we'll have scan_n components, and the order +// of the components is specified by order[] +#define STBI__RESTART(x) ((x) >= 0xd0 && (x) <= 0xd7) + +// after a restart interval, stbi__jpeg_reset the entropy decoder and +// the dc prediction +static void stbi__jpeg_reset(stbi__jpeg* j) +{ + j->code_bits = 0; + j->code_buffer = 0; + j->nomore = 0; + j->img_comp[0].dc_pred = j->img_comp[1].dc_pred = j->img_comp[2].dc_pred = + j->img_comp[3].dc_pred = 0; + j->marker = STBI__MARKER_none; + j->todo = j->restart_interval ? j->restart_interval : 0x7fffffff; + j->eob_run = 0; + // no more than 1<<31 MCUs if no restart_interal? that's plenty safe, + // since we don't even allow 1<<30 pixels +} + +static int stbi__parse_entropy_coded_data(stbi__jpeg* z) +{ + stbi__jpeg_reset(z); + if (!z->progressive) { + if (z->scan_n == 1) { + int i, j; + STBI_SIMD_ALIGN(short, data[64]); + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a + // time, in trivial scanline order number of blocks to do just + // depends on how many actual "pixels" this component has, + // independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x + 7) >> 3; + int h = (z->img_comp[n].y + 7) >> 3; + for (j = 0; j < h; ++j) { + for (i = 0; i < w; ++i) { + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block( + z, data, z->huff_dc + z->img_comp[n].hd, + z->huff_ac + ha, z->fast_ac[ha], n, + z->dequant[z->img_comp[n].tq])) + return 0; + z->idct_block_kernel(z->img_comp[n].data + + z->img_comp[n].w2 * j * 8 + i * 8, + z->img_comp[n].w2, data); + // every data block is an MCU, so countdown the restart + // interval + if (--z->todo <= 0) { + if (z->code_bits < 24) + stbi__grow_buffer_unsafe(z); + // if it's NOT a restart, then just bail, so we get + // corrupt data rather than no data + if (!STBI__RESTART(z->marker)) + return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + else { // interleaved + int i, j, k, x, y; + STBI_SIMD_ALIGN(short, data[64]); + for (j = 0; j < z->img_mcu_y; ++j) { + for (i = 0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in + // order + for (k = 0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's + // just determined by the basic H and V specified for + // the component + for (y = 0; y < z->img_comp[n].v; ++y) { + for (x = 0; x < z->img_comp[n].h; ++x) { + int x2 = (i * z->img_comp[n].h + x) * 8; + int y2 = (j * z->img_comp[n].v + y) * 8; + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block( + z, data, z->huff_dc + z->img_comp[n].hd, + z->huff_ac + ha, z->fast_ac[ha], n, + z->dequant[z->img_comp[n].tq])) + return 0; + z->idct_block_kernel( + z->img_comp[n].data + + z->img_comp[n].w2 * y2 + x2, + z->img_comp[n].w2, data); + } + } + } + // after all interleaved components, that's an interleaved + // MCU, so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) + stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) + return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + } + else { + if (z->scan_n == 1) { + int i, j; + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a + // time, in trivial scanline order number of blocks to do just + // depends on how many actual "pixels" this component has, + // independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x + 7) >> 3; + int h = (z->img_comp[n].y + 7) >> 3; + for (j = 0; j < h; ++j) { + for (i = 0; i < w; ++i) { + short* data = z->img_comp[n].coeff + + 64 * (i + j * z->img_comp[n].coeff_w); + if (z->spec_start == 0) { + if (!stbi__jpeg_decode_block_prog_dc( + z, data, &z->huff_dc[z->img_comp[n].hd], n)) + return 0; + } + else { + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block_prog_ac( + z, data, &z->huff_ac[ha], z->fast_ac[ha])) + return 0; + } + // every data block is an MCU, so countdown the restart + // interval + if (--z->todo <= 0) { + if (z->code_bits < 24) + stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) + return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + else { // interleaved + int i, j, k, x, y; + for (j = 0; j < z->img_mcu_y; ++j) { + for (i = 0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in + // order + for (k = 0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's + // just determined by the basic H and V specified for + // the component + for (y = 0; y < z->img_comp[n].v; ++y) { + for (x = 0; x < z->img_comp[n].h; ++x) { + int x2 = (i * z->img_comp[n].h + x); + int y2 = (j * z->img_comp[n].v + y); + short* data = + z->img_comp[n].coeff + + 64 * (x2 + y2 * z->img_comp[n].coeff_w); + if (!stbi__jpeg_decode_block_prog_dc( + z, data, &z->huff_dc[z->img_comp[n].hd], + n)) + return 0; + } + } + } + // after all interleaved components, that's an interleaved + // MCU, so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) + stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) + return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + } +} + +static void stbi__jpeg_dequantize(short* data, stbi__uint16* dequant) +{ + int i; + for (i = 0; i < 64; ++i) + data[i] *= dequant[i]; +} + +static void stbi__jpeg_finish(stbi__jpeg* z) +{ + if (z->progressive) { + // dequantize and idct the data + int i, j, n; + for (n = 0; n < z->s->img_n; ++n) { + int w = (z->img_comp[n].x + 7) >> 3; + int h = (z->img_comp[n].y + 7) >> 3; + for (j = 0; j < h; ++j) { + for (i = 0; i < w; ++i) { + short* data = z->img_comp[n].coeff + + 64 * (i + j * z->img_comp[n].coeff_w); + stbi__jpeg_dequantize(data, z->dequant[z->img_comp[n].tq]); + z->idct_block_kernel(z->img_comp[n].data + + z->img_comp[n].w2 * j * 8 + i * 8, + z->img_comp[n].w2, data); + } + } + } + } +} + +static int stbi__process_marker(stbi__jpeg* z, int m) +{ + int L; + switch (m) { + case STBI__MARKER_none: // no marker found + return stbi__err("expected marker", "Corrupt JPEG"); + + case 0xDD: // DRI - specify restart interval + if (stbi__get16be(z->s) != 4) + return stbi__err("bad DRI len", "Corrupt JPEG"); + z->restart_interval = stbi__get16be(z->s); + return 1; + + case 0xDB: // DQT - define quantization table + L = stbi__get16be(z->s) - 2; + while (L > 0) { + int q = stbi__get8(z->s); + int p = q >> 4, sixteen = (p != 0); + int t = q & 15, i; + if (p != 0 && p != 1) + return stbi__err("bad DQT type", "Corrupt JPEG"); + if (t > 3) + return stbi__err("bad DQT table", "Corrupt JPEG"); + + for (i = 0; i < 64; ++i) + z->dequant[t][stbi__jpeg_dezigzag[i]] = + (stbi__uint16)(sixteen ? stbi__get16be(z->s) + : stbi__get8(z->s)); + L -= (sixteen ? 129 : 65); + } + return L == 0; + + case 0xC4: // DHT - define huffman table + L = stbi__get16be(z->s) - 2; + while (L > 0) { + stbi_uc* v; + int sizes[16], i, n = 0; + int q = stbi__get8(z->s); + int tc = q >> 4; + int th = q & 15; + if (tc > 1 || th > 3) + return stbi__err("bad DHT header", "Corrupt JPEG"); + for (i = 0; i < 16; ++i) { + sizes[i] = stbi__get8(z->s); + n += sizes[i]; + } + L -= 17; + if (tc == 0) { + if (!stbi__build_huffman(z->huff_dc + th, sizes)) + return 0; + v = z->huff_dc[th].values; + } + else { + if (!stbi__build_huffman(z->huff_ac + th, sizes)) + return 0; + v = z->huff_ac[th].values; + } + for (i = 0; i < n; ++i) + v[i] = stbi__get8(z->s); + if (tc != 0) + stbi__build_fast_ac(z->fast_ac[th], z->huff_ac + th); + L -= n; + } + return L == 0; + } + + // check for comment block or APP blocks + if ((m >= 0xE0 && m <= 0xEF) || m == 0xFE) { + L = stbi__get16be(z->s); + if (L < 2) { + if (m == 0xFE) + return stbi__err("bad COM len", "Corrupt JPEG"); + else + return stbi__err("bad APP len", "Corrupt JPEG"); + } + L -= 2; + + if (m == 0xE0 && L >= 5) { // JFIF APP0 segment + static const unsigned char tag[5] = {'J', 'F', 'I', 'F', '\0'}; + int ok = 1; + int i; + for (i = 0; i < 5; ++i) + if (stbi__get8(z->s) != tag[i]) + ok = 0; + L -= 5; + if (ok) + z->jfif = 1; + } + else if (m == 0xEE && L >= 12) { // Adobe APP14 segment + static const unsigned char tag[6] = {'A', 'd', 'o', 'b', 'e', '\0'}; + int ok = 1; + int i; + for (i = 0; i < 6; ++i) + if (stbi__get8(z->s) != tag[i]) + ok = 0; + L -= 6; + if (ok) { + stbi__get8(z->s); // version + stbi__get16be(z->s); // flags0 + stbi__get16be(z->s); // flags1 + z->app14_color_transform = stbi__get8(z->s); // color transform + L -= 6; + } + } + + stbi__skip(z->s, L); + return 1; + } + + return stbi__err("unknown marker", "Corrupt JPEG"); +} + +// after we see SOS +static int stbi__process_scan_header(stbi__jpeg* z) +{ + int i; + int Ls = stbi__get16be(z->s); + z->scan_n = stbi__get8(z->s); + if (z->scan_n < 1 || z->scan_n > 4 || z->scan_n > (int)z->s->img_n) + return stbi__err("bad SOS component count", "Corrupt JPEG"); + if (Ls != 6 + 2 * z->scan_n) + return stbi__err("bad SOS len", "Corrupt JPEG"); + for (i = 0; i < z->scan_n; ++i) { + int id = stbi__get8(z->s), which; + int q = stbi__get8(z->s); + for (which = 0; which < z->s->img_n; ++which) + if (z->img_comp[which].id == id) + break; + if (which == z->s->img_n) + return 0; // no match + z->img_comp[which].hd = q >> 4; + if (z->img_comp[which].hd > 3) + return stbi__err("bad DC huff", "Corrupt JPEG"); + z->img_comp[which].ha = q & 15; + if (z->img_comp[which].ha > 3) + return stbi__err("bad AC huff", "Corrupt JPEG"); + z->order[i] = which; + } + + { + int aa; + z->spec_start = stbi__get8(z->s); + z->spec_end = stbi__get8(z->s); // should be 63, but might be 0 + aa = stbi__get8(z->s); + z->succ_high = (aa >> 4); + z->succ_low = (aa & 15); + if (z->progressive) { + if (z->spec_start > 63 || z->spec_end > 63 || + z->spec_start > z->spec_end || z->succ_high > 13 || + z->succ_low > 13) + return stbi__err("bad SOS", "Corrupt JPEG"); + } + else { + if (z->spec_start != 0) + return stbi__err("bad SOS", "Corrupt JPEG"); + if (z->succ_high != 0 || z->succ_low != 0) + return stbi__err("bad SOS", "Corrupt JPEG"); + z->spec_end = 63; + } + } + + return 1; +} + +static int stbi__free_jpeg_components(stbi__jpeg* z, int ncomp, int why) +{ + int i; + for (i = 0; i < ncomp; ++i) { + if (z->img_comp[i].raw_data) { + STBI_FREE(z->img_comp[i].raw_data); + z->img_comp[i].raw_data = NULL; + z->img_comp[i].data = NULL; + } + if (z->img_comp[i].raw_coeff) { + STBI_FREE(z->img_comp[i].raw_coeff); + z->img_comp[i].raw_coeff = 0; + z->img_comp[i].coeff = 0; + } + if (z->img_comp[i].linebuf) { + STBI_FREE(z->img_comp[i].linebuf); + z->img_comp[i].linebuf = NULL; + } + } + return why; +} + +static int stbi__process_frame_header(stbi__jpeg* z, int scan) +{ + stbi__context* s = z->s; + int Lf, p, i, q, h_max = 1, v_max = 1, c; + Lf = stbi__get16be(s); + if (Lf < 11) + return stbi__err("bad SOF len", "Corrupt JPEG"); // JPEG + p = stbi__get8(s); + if (p != 8) + return stbi__err( + "only 8-bit", + "JPEG format not supported: 8-bit only"); // JPEG baseline + s->img_y = stbi__get16be(s); + if (s->img_y == 0) + return stbi__err( + "no header height", + "JPEG format not supported: delayed height"); // Legal, but we don't + // handle it--but + // neither does IJG + s->img_x = stbi__get16be(s); + if (s->img_x == 0) + return stbi__err("0 width", "Corrupt JPEG"); // JPEG requires + c = stbi__get8(s); + if (c != 3 && c != 1 && c != 4) + return stbi__err("bad component count", "Corrupt JPEG"); + s->img_n = c; + for (i = 0; i < c; ++i) { + z->img_comp[i].data = NULL; + z->img_comp[i].linebuf = NULL; + } + + if (Lf != 8 + 3 * s->img_n) + return stbi__err("bad SOF len", "Corrupt JPEG"); + + z->rgb = 0; + for (i = 0; i < s->img_n; ++i) { + static const unsigned char rgb[3] = {'R', 'G', 'B'}; + z->img_comp[i].id = stbi__get8(s); + if (s->img_n == 3 && z->img_comp[i].id == rgb[i]) + ++z->rgb; + q = stbi__get8(s); + z->img_comp[i].h = (q >> 4); + if (!z->img_comp[i].h || z->img_comp[i].h > 4) + return stbi__err("bad H", "Corrupt JPEG"); + z->img_comp[i].v = q & 15; + if (!z->img_comp[i].v || z->img_comp[i].v > 4) + return stbi__err("bad V", "Corrupt JPEG"); + z->img_comp[i].tq = stbi__get8(s); + if (z->img_comp[i].tq > 3) + return stbi__err("bad TQ", "Corrupt JPEG"); + } + + if (scan != STBI__SCAN_load) + return 1; + + if (!stbi__mad3sizes_valid(s->img_x, s->img_y, s->img_n, 0)) + return stbi__err("too large", "Image too large to decode"); + + for (i = 0; i < s->img_n; ++i) { + if (z->img_comp[i].h > h_max) + h_max = z->img_comp[i].h; + if (z->img_comp[i].v > v_max) + v_max = z->img_comp[i].v; + } + + // compute interleaved mcu info + z->img_h_max = h_max; + z->img_v_max = v_max; + z->img_mcu_w = h_max * 8; + z->img_mcu_h = v_max * 8; + // these sizes can't be more than 17 bits + z->img_mcu_x = (s->img_x + z->img_mcu_w - 1) / z->img_mcu_w; + z->img_mcu_y = (s->img_y + z->img_mcu_h - 1) / z->img_mcu_h; + + for (i = 0; i < s->img_n; ++i) { + // number of effective pixels (e.g. for non-interleaved MCU) + z->img_comp[i].x = (s->img_x * z->img_comp[i].h + h_max - 1) / h_max; + z->img_comp[i].y = (s->img_y * z->img_comp[i].v + v_max - 1) / v_max; + // to simplify generation, we'll allocate enough memory to decode + // the bogus oversized data from using interleaved MCUs and their + // big blocks (e.g. a 16x16 iMCU on an image of width 33); we won't + // discard the extra data until colorspace conversion + // + // img_mcu_x, img_mcu_y: <=17 bits; comp[i].h and .v are <=4 (checked + // earlier) so these muls can't overflow with 32-bit ints (which we + // require) + z->img_comp[i].w2 = z->img_mcu_x * z->img_comp[i].h * 8; + z->img_comp[i].h2 = z->img_mcu_y * z->img_comp[i].v * 8; + z->img_comp[i].coeff = 0; + z->img_comp[i].raw_coeff = 0; + z->img_comp[i].linebuf = NULL; + z->img_comp[i].raw_data = + stbi__malloc_mad2(z->img_comp[i].w2, z->img_comp[i].h2, 15); + if (z->img_comp[i].raw_data == NULL) + return stbi__free_jpeg_components( + z, i + 1, stbi__err("outofmem", "Out of memory")); + // align blocks for idct using mmx/sse + z->img_comp[i].data = + (stbi_uc*)(((size_t)z->img_comp[i].raw_data + 15) & ~15); + if (z->progressive) { + // w2, h2 are multiples of 8 (see above) + z->img_comp[i].coeff_w = z->img_comp[i].w2 / 8; + z->img_comp[i].coeff_h = z->img_comp[i].h2 / 8; + z->img_comp[i].raw_coeff = stbi__malloc_mad3( + z->img_comp[i].w2, z->img_comp[i].h2, sizeof(short), 15); + if (z->img_comp[i].raw_coeff == NULL) + return stbi__free_jpeg_components( + z, i + 1, stbi__err("outofmem", "Out of memory")); + z->img_comp[i].coeff = + (short*)(((size_t)z->img_comp[i].raw_coeff + 15) & ~15); + } + } + + return 1; +} + +// use comparisons since in some cases we handle more than one case (e.g. SOF) +#define stbi__DNL(x) ((x) == 0xdc) +#define stbi__SOI(x) ((x) == 0xd8) +#define stbi__EOI(x) ((x) == 0xd9) +#define stbi__SOF(x) ((x) == 0xc0 || (x) == 0xc1 || (x) == 0xc2) +#define stbi__SOS(x) ((x) == 0xda) + +#define stbi__SOF_progressive(x) ((x) == 0xc2) + +static int stbi__decode_jpeg_header(stbi__jpeg* z, int scan) +{ + int m; + z->jfif = 0; + z->app14_color_transform = -1; // valid values are 0,1,2 + z->marker = STBI__MARKER_none; // initialize cached marker to empty + m = stbi__get_marker(z); + if (!stbi__SOI(m)) + return stbi__err("no SOI", "Corrupt JPEG"); + if (scan == STBI__SCAN_type) + return 1; + m = stbi__get_marker(z); + while (!stbi__SOF(m)) { + if (!stbi__process_marker(z, m)) + return 0; + m = stbi__get_marker(z); + while (m == STBI__MARKER_none) { + // some files have extra padding after their blocks, so ok, we'll + // scan + if (stbi__at_eof(z->s)) + return stbi__err("no SOF", "Corrupt JPEG"); + m = stbi__get_marker(z); + } + } + z->progressive = stbi__SOF_progressive(m); + if (!stbi__process_frame_header(z, scan)) + return 0; + return 1; +} + +// decode image to YCbCr format +static int stbi__decode_jpeg_image(stbi__jpeg* j) +{ + int m; + for (m = 0; m < 4; m++) { + j->img_comp[m].raw_data = NULL; + j->img_comp[m].raw_coeff = NULL; + } + j->restart_interval = 0; + if (!stbi__decode_jpeg_header(j, STBI__SCAN_load)) + return 0; + m = stbi__get_marker(j); + while (!stbi__EOI(m)) { + if (stbi__SOS(m)) { + if (!stbi__process_scan_header(j)) + return 0; + if (!stbi__parse_entropy_coded_data(j)) + return 0; + if (j->marker == STBI__MARKER_none) { + // handle 0s at the end of image data from IP Kamera 9060 + while (!stbi__at_eof(j->s)) { + int x = stbi__get8(j->s); + if (x == 255) { + j->marker = stbi__get8(j->s); + break; + } + } + // if we reach eof without hitting a marker, stbi__get_marker() + // below will fail and we'll eventually return 0 + } + } + else if (stbi__DNL(m)) { + int Ld = stbi__get16be(j->s); + stbi__uint32 NL = stbi__get16be(j->s); + if (Ld != 4) + return stbi__err("bad DNL len", "Corrupt JPEG"); + if (NL != j->s->img_y) + return stbi__err("bad DNL height", "Corrupt JPEG"); + } + else { + if (!stbi__process_marker(j, m)) + return 0; + } + m = stbi__get_marker(j); + } + if (j->progressive) + stbi__jpeg_finish(j); + return 1; +} + +// static jfif-centered resampling (across block boundaries) + +typedef stbi_uc* (*resample_row_func)(stbi_uc* out, stbi_uc* in0, stbi_uc* in1, + int w, int hs); + +#define stbi__div4(x) ((stbi_uc)((x) >> 2)) + +static stbi_uc* resample_row_1(stbi_uc* out, stbi_uc* in_near, stbi_uc* in_far, + int w, int hs) +{ + STBI_NOTUSED(out); + STBI_NOTUSED(in_far); + STBI_NOTUSED(w); + STBI_NOTUSED(hs); + return in_near; +} + +static stbi_uc* stbi__resample_row_v_2(stbi_uc* out, stbi_uc* in_near, + stbi_uc* in_far, int w, int hs) +{ + // need to generate two samples vertically for every one in input + int i; + STBI_NOTUSED(hs); + for (i = 0; i < w; ++i) + out[i] = stbi__div4(3 * in_near[i] + in_far[i] + 2); + return out; +} + +static stbi_uc* stbi__resample_row_h_2(stbi_uc* out, stbi_uc* in_near, + stbi_uc* in_far, int w, int hs) +{ + // need to generate two samples horizontally for every one in input + int i; + stbi_uc* input = in_near; + + if (w == 1) { + // if only one sample, can't do any interpolation + out[0] = out[1] = input[0]; + return out; + } + + out[0] = input[0]; + out[1] = stbi__div4(input[0] * 3 + input[1] + 2); + for (i = 1; i < w - 1; ++i) { + int n = 3 * input[i] + 2; + out[i * 2 + 0] = stbi__div4(n + input[i - 1]); + out[i * 2 + 1] = stbi__div4(n + input[i + 1]); + } + out[i * 2 + 0] = stbi__div4(input[w - 2] * 3 + input[w - 1] + 2); + out[i * 2 + 1] = input[w - 1]; + + STBI_NOTUSED(in_far); + STBI_NOTUSED(hs); + + return out; +} + +#define stbi__div16(x) ((stbi_uc)((x) >> 4)) + +static stbi_uc* stbi__resample_row_hv_2(stbi_uc* out, stbi_uc* in_near, + stbi_uc* in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i, t0, t1; + if (w == 1) { + out[0] = out[1] = stbi__div4(3 * in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3 * in_near[0] + in_far[0]; + out[0] = stbi__div4(t1 + 2); + for (i = 1; i < w; ++i) { + t0 = t1; + t1 = 3 * in_near[i] + in_far[i]; + out[i * 2 - 1] = stbi__div16(3 * t0 + t1 + 8); + out[i * 2] = stbi__div16(3 * t1 + t0 + 8); + } + out[w * 2 - 1] = stbi__div4(t1 + 2); + + STBI_NOTUSED(hs); + + return out; +} + +#if defined(STBI_SSE2) || defined(STBI_NEON) +static stbi_uc* stbi__resample_row_hv_2_simd(stbi_uc* out, stbi_uc* in_near, + stbi_uc* in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i = 0, t0, t1; + + if (w == 1) { + out[0] = out[1] = stbi__div4(3 * in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3 * in_near[0] + in_far[0]; + // process groups of 8 pixels for as long as we can. + // note we can't handle the last pixel in a row in this loop + // because we need to handle the filter boundary conditions. + for (; i < ((w - 1) & ~7); i += 8) { +#if defined(STBI_SSE2) + // load and perform the vertical filtering pass + // this uses 3*x + y = 4*x + (y - x) + __m128i zero = _mm_setzero_si128(); + __m128i farb = _mm_loadl_epi64((__m128i*)(in_far + i)); + __m128i nearb = _mm_loadl_epi64((__m128i*)(in_near + i)); + __m128i farw = _mm_unpacklo_epi8(farb, zero); + __m128i nearw = _mm_unpacklo_epi8(nearb, zero); + __m128i diff = _mm_sub_epi16(farw, nearw); + __m128i nears = _mm_slli_epi16(nearw, 2); + __m128i curr = _mm_add_epi16(nears, diff); // current row + + // horizontal filter works the same based on shifted vers of current + // row. "prev" is current row shifted right by 1 pixel; we need to + // insert the previous pixel value (from t1). + // "next" is current row shifted left by 1 pixel, with first pixel + // of next block of 8 pixels added in. + __m128i prv0 = _mm_slli_si128(curr, 2); + __m128i nxt0 = _mm_srli_si128(curr, 2); + __m128i prev = _mm_insert_epi16(prv0, t1, 0); + __m128i next = + _mm_insert_epi16(nxt0, 3 * in_near[i + 8] + in_far[i + 8], 7); + + // horizontal filter, polyphase implementation since it's convenient: + // even pixels = 3*cur + prev = cur*4 + (prev - cur) + // odd pixels = 3*cur + next = cur*4 + (next - cur) + // note the shared term. + __m128i bias = _mm_set1_epi16(8); + __m128i curs = _mm_slli_epi16(curr, 2); + __m128i prvd = _mm_sub_epi16(prev, curr); + __m128i nxtd = _mm_sub_epi16(next, curr); + __m128i curb = _mm_add_epi16(curs, bias); + __m128i even = _mm_add_epi16(prvd, curb); + __m128i odd = _mm_add_epi16(nxtd, curb); + + // interleave even and odd pixels, then undo scaling. + __m128i int0 = _mm_unpacklo_epi16(even, odd); + __m128i int1 = _mm_unpackhi_epi16(even, odd); + __m128i de0 = _mm_srli_epi16(int0, 4); + __m128i de1 = _mm_srli_epi16(int1, 4); + + // pack and write output + __m128i outv = _mm_packus_epi16(de0, de1); + _mm_storeu_si128((__m128i*)(out + i * 2), outv); +#elif defined(STBI_NEON) + // load and perform the vertical filtering pass + // this uses 3*x + y = 4*x + (y - x) + uint8x8_t farb = vld1_u8(in_far + i); + uint8x8_t nearb = vld1_u8(in_near + i); + int16x8_t diff = vreinterpretq_s16_u16(vsubl_u8(farb, nearb)); + int16x8_t nears = vreinterpretq_s16_u16(vshll_n_u8(nearb, 2)); + int16x8_t curr = vaddq_s16(nears, diff); // current row + + // horizontal filter works the same based on shifted vers of current + // row. "prev" is current row shifted right by 1 pixel; we need to + // insert the previous pixel value (from t1). + // "next" is current row shifted left by 1 pixel, with first pixel + // of next block of 8 pixels added in. + int16x8_t prv0 = vextq_s16(curr, curr, 7); + int16x8_t nxt0 = vextq_s16(curr, curr, 1); + int16x8_t prev = vsetq_lane_s16(t1, prv0, 0); + int16x8_t next = + vsetq_lane_s16(3 * in_near[i + 8] + in_far[i + 8], nxt0, 7); + + // horizontal filter, polyphase implementation since it's convenient: + // even pixels = 3*cur + prev = cur*4 + (prev - cur) + // odd pixels = 3*cur + next = cur*4 + (next - cur) + // note the shared term. + int16x8_t curs = vshlq_n_s16(curr, 2); + int16x8_t prvd = vsubq_s16(prev, curr); + int16x8_t nxtd = vsubq_s16(next, curr); + int16x8_t even = vaddq_s16(curs, prvd); + int16x8_t odd = vaddq_s16(curs, nxtd); + + // undo scaling and round, then store with even/odd phases interleaved + uint8x8x2_t o; + o.val[0] = vqrshrun_n_s16(even, 4); + o.val[1] = vqrshrun_n_s16(odd, 4); + vst2_u8(out + i * 2, o); +#endif + + // "previous" value for next iter + t1 = 3 * in_near[i + 7] + in_far[i + 7]; + } + + t0 = t1; + t1 = 3 * in_near[i] + in_far[i]; + out[i * 2] = stbi__div16(3 * t1 + t0 + 8); + + for (++i; i < w; ++i) { + t0 = t1; + t1 = 3 * in_near[i] + in_far[i]; + out[i * 2 - 1] = stbi__div16(3 * t0 + t1 + 8); + out[i * 2] = stbi__div16(3 * t1 + t0 + 8); + } + out[w * 2 - 1] = stbi__div4(t1 + 2); + + STBI_NOTUSED(hs); + + return out; +} +#endif + +static stbi_uc* stbi__resample_row_generic(stbi_uc* out, stbi_uc* in_near, + stbi_uc* in_far, int w, int hs) +{ + // resample with nearest-neighbor + int i, j; + STBI_NOTUSED(in_far); + for (i = 0; i < w; ++i) + for (j = 0; j < hs; ++j) + out[i * hs + j] = in_near[i]; + return out; +} + +// this is a reduced-precision calculation of YCbCr-to-RGB introduced +// to make sure the code produces the same results in both SIMD and scalar +#define stbi__float2fixed(x) (((int)((x)*4096.0f + 0.5f)) << 8) +static void stbi__YCbCr_to_RGB_row(stbi_uc* out, const stbi_uc* y, + const stbi_uc* pcb, const stbi_uc* pcr, + int count, int step) +{ + int i; + for (i = 0; i < count; ++i) { + int y_fixed = (y[i] << 20) + (1 << 19); // rounding + int r, g, b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr * stbi__float2fixed(1.40200f); + g = y_fixed + (cr * -stbi__float2fixed(0.71414f)) + + ((cb * -stbi__float2fixed(0.34414f)) & 0xffff0000); + b = y_fixed + cb * stbi__float2fixed(1.77200f); + r >>= 20; + g >>= 20; + b >>= 20; + if ((unsigned)r > 255) { + if (r < 0) + r = 0; + else + r = 255; + } + if ((unsigned)g > 255) { + if (g < 0) + g = 0; + else + g = 255; + } + if ((unsigned)b > 255) { + if (b < 0) + b = 0; + else + b = 255; + } + out[0] = (stbi_uc)r; + out[1] = (stbi_uc)g; + out[2] = (stbi_uc)b; + out[3] = 255; + out += step; + } +} + +#if defined(STBI_SSE2) || defined(STBI_NEON) +static void stbi__YCbCr_to_RGB_simd(stbi_uc* out, stbi_uc const* y, + stbi_uc const* pcb, stbi_uc const* pcr, + int count, int step) +{ + int i = 0; + +#ifdef STBI_SSE2 + // step == 3 is pretty ugly on the final interleave, and i'm not convinced + // it's useful in practice (you wouldn't use it for textures, for example). + // so just accelerate step == 4 case. + if (step == 4) { + // this is a fairly straightforward implementation and not + // super-optimized. + __m128i signflip = _mm_set1_epi8(-0x80); + __m128i cr_const0 = _mm_set1_epi16((short)(1.40200f * 4096.0f + 0.5f)); + __m128i cr_const1 = _mm_set1_epi16(-(short)(0.71414f * 4096.0f + 0.5f)); + __m128i cb_const0 = _mm_set1_epi16(-(short)(0.34414f * 4096.0f + 0.5f)); + __m128i cb_const1 = _mm_set1_epi16((short)(1.77200f * 4096.0f + 0.5f)); + __m128i y_bias = _mm_set1_epi8((char)(unsigned char)128); + __m128i xw = _mm_set1_epi16(255); // alpha channel + + for (; i + 7 < count; i += 8) { + // load + __m128i y_bytes = _mm_loadl_epi64((__m128i*)(y + i)); + __m128i cr_bytes = _mm_loadl_epi64((__m128i*)(pcr + i)); + __m128i cb_bytes = _mm_loadl_epi64((__m128i*)(pcb + i)); + __m128i cr_biased = _mm_xor_si128(cr_bytes, signflip); // -128 + __m128i cb_biased = _mm_xor_si128(cb_bytes, signflip); // -128 + + // unpack to short (and left-shift cr, cb by 8) + __m128i yw = _mm_unpacklo_epi8(y_bias, y_bytes); + __m128i crw = _mm_unpacklo_epi8(_mm_setzero_si128(), cr_biased); + __m128i cbw = _mm_unpacklo_epi8(_mm_setzero_si128(), cb_biased); + + // color transform + __m128i yws = _mm_srli_epi16(yw, 4); + __m128i cr0 = _mm_mulhi_epi16(cr_const0, crw); + __m128i cb0 = _mm_mulhi_epi16(cb_const0, cbw); + __m128i cb1 = _mm_mulhi_epi16(cbw, cb_const1); + __m128i cr1 = _mm_mulhi_epi16(crw, cr_const1); + __m128i rws = _mm_add_epi16(cr0, yws); + __m128i gwt = _mm_add_epi16(cb0, yws); + __m128i bws = _mm_add_epi16(yws, cb1); + __m128i gws = _mm_add_epi16(gwt, cr1); + + // descale + __m128i rw = _mm_srai_epi16(rws, 4); + __m128i bw = _mm_srai_epi16(bws, 4); + __m128i gw = _mm_srai_epi16(gws, 4); + + // back to byte, set up for transpose + __m128i brb = _mm_packus_epi16(rw, bw); + __m128i gxb = _mm_packus_epi16(gw, xw); + + // transpose to interleave channels + __m128i t0 = _mm_unpacklo_epi8(brb, gxb); + __m128i t1 = _mm_unpackhi_epi8(brb, gxb); + __m128i o0 = _mm_unpacklo_epi16(t0, t1); + __m128i o1 = _mm_unpackhi_epi16(t0, t1); + + // store + _mm_storeu_si128((__m128i*)(out + 0), o0); + _mm_storeu_si128((__m128i*)(out + 16), o1); + out += 32; + } + } +#endif + +#ifdef STBI_NEON + // in this version, step=3 support would be easy to add. but is there + // demand? + if (step == 4) { + // this is a fairly straightforward implementation and not + // super-optimized. + uint8x8_t signflip = vdup_n_u8(0x80); + int16x8_t cr_const0 = vdupq_n_s16((short)(1.40200f * 4096.0f + 0.5f)); + int16x8_t cr_const1 = vdupq_n_s16(-(short)(0.71414f * 4096.0f + 0.5f)); + int16x8_t cb_const0 = vdupq_n_s16(-(short)(0.34414f * 4096.0f + 0.5f)); + int16x8_t cb_const1 = vdupq_n_s16((short)(1.77200f * 4096.0f + 0.5f)); + + for (; i + 7 < count; i += 8) { + // load + uint8x8_t y_bytes = vld1_u8(y + i); + uint8x8_t cr_bytes = vld1_u8(pcr + i); + uint8x8_t cb_bytes = vld1_u8(pcb + i); + int8x8_t cr_biased = + vreinterpret_s8_u8(vsub_u8(cr_bytes, signflip)); + int8x8_t cb_biased = + vreinterpret_s8_u8(vsub_u8(cb_bytes, signflip)); + + // expand to s16 + int16x8_t yws = vreinterpretq_s16_u16(vshll_n_u8(y_bytes, 4)); + int16x8_t crw = vshll_n_s8(cr_biased, 7); + int16x8_t cbw = vshll_n_s8(cb_biased, 7); + + // color transform + int16x8_t cr0 = vqdmulhq_s16(crw, cr_const0); + int16x8_t cb0 = vqdmulhq_s16(cbw, cb_const0); + int16x8_t cr1 = vqdmulhq_s16(crw, cr_const1); + int16x8_t cb1 = vqdmulhq_s16(cbw, cb_const1); + int16x8_t rws = vaddq_s16(yws, cr0); + int16x8_t gws = vaddq_s16(vaddq_s16(yws, cb0), cr1); + int16x8_t bws = vaddq_s16(yws, cb1); + + // undo scaling, round, convert to byte + uint8x8x4_t o; + o.val[0] = vqrshrun_n_s16(rws, 4); + o.val[1] = vqrshrun_n_s16(gws, 4); + o.val[2] = vqrshrun_n_s16(bws, 4); + o.val[3] = vdup_n_u8(255); + + // store, interleaving r/g/b/a + vst4_u8(out, o); + out += 8 * 4; + } + } +#endif + + for (; i < count; ++i) { + int y_fixed = (y[i] << 20) + (1 << 19); // rounding + int r, g, b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr * stbi__float2fixed(1.40200f); + g = y_fixed + cr * -stbi__float2fixed(0.71414f) + + ((cb * -stbi__float2fixed(0.34414f)) & 0xffff0000); + b = y_fixed + cb * stbi__float2fixed(1.77200f); + r >>= 20; + g >>= 20; + b >>= 20; + if ((unsigned)r > 255) { + if (r < 0) + r = 0; + else + r = 255; + } + if ((unsigned)g > 255) { + if (g < 0) + g = 0; + else + g = 255; + } + if ((unsigned)b > 255) { + if (b < 0) + b = 0; + else + b = 255; + } + out[0] = (stbi_uc)r; + out[1] = (stbi_uc)g; + out[2] = (stbi_uc)b; + out[3] = 255; + out += step; + } +} +#endif + +// set up the kernels +static void stbi__setup_jpeg(stbi__jpeg* j) +{ + j->idct_block_kernel = stbi__idct_block; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_row; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2; + +#ifdef STBI_SSE2 + if (stbi__sse2_available()) { + j->idct_block_kernel = stbi__idct_simd; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; + } +#endif + +#ifdef STBI_NEON + j->idct_block_kernel = stbi__idct_simd; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; +#endif +} + +// clean up the temporary component buffers +static void stbi__cleanup_jpeg(stbi__jpeg* j) +{ + stbi__free_jpeg_components(j, j->s->img_n, 0); +} + +typedef struct { + resample_row_func resample; + stbi_uc *line0, *line1; + int hs, vs; // expansion factor in each axis + int w_lores; // horizontal pixels pre-expansion + int ystep; // how far through vertical expansion we are + int ypos; // which pre-expansion row we're on +} stbi__resample; + +// fast 0..255 * 0..255 => 0..255 rounded multiplication +static stbi_uc stbi__blinn_8x8(stbi_uc x, stbi_uc y) +{ + unsigned int t = x * y + 128; + return (stbi_uc)((t + (t >> 8)) >> 8); +} + +static stbi_uc* load_jpeg_image(stbi__jpeg* z, int* out_x, int* out_y, + int* comp, int req_comp) +{ + int n, decode_n, is_rgb; + z->s->img_n = 0; // make stbi__cleanup_jpeg safe + + // validate req_comp + if (req_comp < 0 || req_comp > 4) + return stbi__errpuc("bad req_comp", "Internal error"); + + // load a jpeg image from whichever source, but leave in YCbCr format + if (!stbi__decode_jpeg_image(z)) { + stbi__cleanup_jpeg(z); + return NULL; + } + + // determine actual number of components to generate + n = req_comp ? req_comp : z->s->img_n >= 3 ? 3 : 1; + + is_rgb = z->s->img_n == 3 && + (z->rgb == 3 || (z->app14_color_transform == 0 && !z->jfif)); + + if (z->s->img_n == 3 && n < 3 && !is_rgb) + decode_n = 1; + else + decode_n = z->s->img_n; + + // resample and color-convert + { + int k; + unsigned int i, j; + stbi_uc* output; + stbi_uc* coutput[4]; + + stbi__resample res_comp[4]; + + for (k = 0; k < decode_n; ++k) { + stbi__resample* r = &res_comp[k]; + + // allocate line buffer big enough for upsampling off the edges + // with upsample factor of 4 + z->img_comp[k].linebuf = (stbi_uc*)stbi__malloc(z->s->img_x + 3); + if (!z->img_comp[k].linebuf) { + stbi__cleanup_jpeg(z); + return stbi__errpuc("outofmem", "Out of memory"); + } + + r->hs = z->img_h_max / z->img_comp[k].h; + r->vs = z->img_v_max / z->img_comp[k].v; + r->ystep = r->vs >> 1; + r->w_lores = (z->s->img_x + r->hs - 1) / r->hs; + r->ypos = 0; + r->line0 = r->line1 = z->img_comp[k].data; + + if (r->hs == 1 && r->vs == 1) + r->resample = resample_row_1; + else if (r->hs == 1 && r->vs == 2) + r->resample = stbi__resample_row_v_2; + else if (r->hs == 2 && r->vs == 1) + r->resample = stbi__resample_row_h_2; + else if (r->hs == 2 && r->vs == 2) + r->resample = z->resample_row_hv_2_kernel; + else + r->resample = stbi__resample_row_generic; + } + + // can't error after this so, this is safe + output = (stbi_uc*)stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1); + if (!output) { + stbi__cleanup_jpeg(z); + return stbi__errpuc("outofmem", "Out of memory"); + } + + // now go ahead and resample + for (j = 0; j < z->s->img_y; ++j) { + stbi_uc* out = output + n * z->s->img_x * j; + for (k = 0; k < decode_n; ++k) { + stbi__resample* r = &res_comp[k]; + int y_bot = r->ystep >= (r->vs >> 1); + coutput[k] = r->resample( + z->img_comp[k].linebuf, y_bot ? r->line1 : r->line0, + y_bot ? r->line0 : r->line1, r->w_lores, r->hs); + if (++r->ystep >= r->vs) { + r->ystep = 0; + r->line0 = r->line1; + if (++r->ypos < z->img_comp[k].y) + r->line1 += z->img_comp[k].w2; + } + } + if (n >= 3) { + stbi_uc* y = coutput[0]; + if (z->s->img_n == 3) { + if (is_rgb) { + for (i = 0; i < z->s->img_x; ++i) { + out[0] = y[i]; + out[1] = coutput[1][i]; + out[2] = coutput[2][i]; + out[3] = 255; + out += n; + } + } + else { + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], + z->s->img_x, n); + } + } + else if (z->s->img_n == 4) { + if (z->app14_color_transform == 0) { // CMYK + for (i = 0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + out[0] = stbi__blinn_8x8(coutput[0][i], m); + out[1] = stbi__blinn_8x8(coutput[1][i], m); + out[2] = stbi__blinn_8x8(coutput[2][i], m); + out[3] = 255; + out += n; + } + } + else if (z->app14_color_transform == 2) { // YCCK + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], + z->s->img_x, n); + for (i = 0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + out[0] = stbi__blinn_8x8(255 - out[0], m); + out[1] = stbi__blinn_8x8(255 - out[1], m); + out[2] = stbi__blinn_8x8(255 - out[2], m); + out += n; + } + } + else { // YCbCr + alpha? Ignore the fourth channel for now + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], + z->s->img_x, n); + } + } + else + for (i = 0; i < z->s->img_x; ++i) { + out[0] = out[1] = out[2] = y[i]; + out[3] = 255; // not used if n==3 + out += n; + } + } + else { + if (is_rgb) { + if (n == 1) + for (i = 0; i < z->s->img_x; ++i) + *out++ = stbi__compute_y( + coutput[0][i], coutput[1][i], coutput[2][i]); + else { + for (i = 0; i < z->s->img_x; ++i, out += 2) { + out[0] = stbi__compute_y( + coutput[0][i], coutput[1][i], coutput[2][i]); + out[1] = 255; + } + } + } + else if (z->s->img_n == 4 && z->app14_color_transform == 0) { + for (i = 0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + stbi_uc r = stbi__blinn_8x8(coutput[0][i], m); + stbi_uc g = stbi__blinn_8x8(coutput[1][i], m); + stbi_uc b = stbi__blinn_8x8(coutput[2][i], m); + out[0] = stbi__compute_y(r, g, b); + out[1] = 255; + out += n; + } + } + else if (z->s->img_n == 4 && z->app14_color_transform == 2) { + for (i = 0; i < z->s->img_x; ++i) { + out[0] = + stbi__blinn_8x8(255 - coutput[0][i], coutput[3][i]); + out[1] = 255; + out += n; + } + } + else { + stbi_uc* y = coutput[0]; + if (n == 1) + for (i = 0; i < z->s->img_x; ++i) + out[i] = y[i]; + else + for (i = 0; i < z->s->img_x; ++i) { + *out++ = y[i]; + *out++ = 255; + } + } + } + } + stbi__cleanup_jpeg(z); + *out_x = z->s->img_x; + *out_y = z->s->img_y; + if (comp) + *comp = z->s->img_n >= 3 + ? 3 + : 1; // report original components, not output + return output; + } +} + +static void* stbi__jpeg_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri) +{ + unsigned char* result; + stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg)); + STBI_NOTUSED(ri); + j->s = s; + stbi__setup_jpeg(j); + result = load_jpeg_image(j, x, y, comp, req_comp); + STBI_FREE(j); + return result; +} + +static int stbi__jpeg_test(stbi__context* s) +{ + int r; + stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg)); + j->s = s; + stbi__setup_jpeg(j); + r = stbi__decode_jpeg_header(j, STBI__SCAN_type); + stbi__rewind(s); + STBI_FREE(j); + return r; +} + +static int stbi__jpeg_info_raw(stbi__jpeg* j, int* x, int* y, int* comp) +{ + if (!stbi__decode_jpeg_header(j, STBI__SCAN_header)) { + stbi__rewind(j->s); + return 0; + } + if (x) + *x = j->s->img_x; + if (y) + *y = j->s->img_y; + if (comp) + *comp = j->s->img_n >= 3 ? 3 : 1; + return 1; +} + +static int stbi__jpeg_info(stbi__context* s, int* x, int* y, int* comp) +{ + int result; + stbi__jpeg* j = (stbi__jpeg*)(stbi__malloc(sizeof(stbi__jpeg))); + j->s = s; + result = stbi__jpeg_info_raw(j, x, y, comp); + STBI_FREE(j); + return result; +} +#endif + +// public domain zlib decode v0.2 Sean Barrett 2006-11-18 +// simple implementation +// - all input must be provided in an upfront buffer +// - all output is written to a single output buffer (can malloc/realloc) +// performance +// - fast huffman + +#ifndef STBI_NO_ZLIB + +// fast-way is faster to check than jpeg huffman, but slow way is slower +#define STBI__ZFAST_BITS 9 // accelerate all cases in default tables +#define STBI__ZFAST_MASK ((1 << STBI__ZFAST_BITS) - 1) + +// zlib-style huffman encoding +// (jpegs packs from left, zlib from right, so can't share code) +typedef struct { + stbi__uint16 fast[1 << STBI__ZFAST_BITS]; + stbi__uint16 firstcode[16]; + int maxcode[17]; + stbi__uint16 firstsymbol[16]; + stbi_uc size[288]; + stbi__uint16 value[288]; +} stbi__zhuffman; + +stbi_inline static int stbi__bitreverse16(int n) +{ + n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); + n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); + n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); + n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); + return n; +} + +stbi_inline static int stbi__bit_reverse(int v, int bits) +{ + STBI_ASSERT(bits <= 16); + // to bit reverse n bits, reverse 16 and shift + // e.g. 11 bits, bit reverse and shift away 5 + return stbi__bitreverse16(v) >> (16 - bits); +} + +static int stbi__zbuild_huffman(stbi__zhuffman* z, const stbi_uc* sizelist, + int num) +{ + int i, k = 0; + int code, next_code[16], sizes[17]; + + // DEFLATE spec for generating codes + memset(sizes, 0, sizeof(sizes)); + memset(z->fast, 0, sizeof(z->fast)); + for (i = 0; i < num; ++i) + ++sizes[sizelist[i]]; + sizes[0] = 0; + for (i = 1; i < 16; ++i) + if (sizes[i] > (1 << i)) + return stbi__err("bad sizes", "Corrupt PNG"); + code = 0; + for (i = 1; i < 16; ++i) { + next_code[i] = code; + z->firstcode[i] = (stbi__uint16)code; + z->firstsymbol[i] = (stbi__uint16)k; + code = (code + sizes[i]); + if (sizes[i]) + if (code - 1 >= (1 << i)) + return stbi__err("bad codelengths", "Corrupt PNG"); + z->maxcode[i] = code << (16 - i); // preshift for inner loop + code <<= 1; + k += sizes[i]; + } + z->maxcode[16] = 0x10000; // sentinel + for (i = 0; i < num; ++i) { + int s = sizelist[i]; + if (s) { + int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s]; + stbi__uint16 fastv = (stbi__uint16)((s << 9) | i); + z->size[c] = (stbi_uc)s; + z->value[c] = (stbi__uint16)i; + if (s <= STBI__ZFAST_BITS) { + int j = stbi__bit_reverse(next_code[s], s); + while (j < (1 << STBI__ZFAST_BITS)) { + z->fast[j] = fastv; + j += (1 << s); + } + } + ++next_code[s]; + } + } + return 1; +} + +// zlib-from-memory implementation for PNG reading +// because PNG allows splitting the zlib stream arbitrarily, +// and it's annoying structurally to have PNG call ZLIB call PNG, +// we require PNG read all the IDATs and combine them into a single +// memory buffer + +typedef struct { + stbi_uc *zbuffer, *zbuffer_end; + int num_bits; + stbi__uint32 code_buffer; + + char* zout; + char* zout_start; + char* zout_end; + int z_expandable; + + stbi__zhuffman z_length, z_distance; +} stbi__zbuf; + +stbi_inline static stbi_uc stbi__zget8(stbi__zbuf* z) +{ + if (z->zbuffer >= z->zbuffer_end) + return 0; + return *z->zbuffer++; +} + +static void stbi__fill_bits(stbi__zbuf* z) +{ + do { + STBI_ASSERT(z->code_buffer < (1U << z->num_bits)); + z->code_buffer |= (unsigned int)stbi__zget8(z) << z->num_bits; + z->num_bits += 8; + } while (z->num_bits <= 24); +} + +stbi_inline static unsigned int stbi__zreceive(stbi__zbuf* z, int n) +{ + unsigned int k; + if (z->num_bits < n) + stbi__fill_bits(z); + k = z->code_buffer & ((1 << n) - 1); + z->code_buffer >>= n; + z->num_bits -= n; + return k; +} + +static int stbi__zhuffman_decode_slowpath(stbi__zbuf* a, stbi__zhuffman* z) +{ + int b, s, k; + // not resolved by fast table, so compute it the slow way + // use jpeg approach, which requires MSbits at top + k = stbi__bit_reverse(a->code_buffer, 16); + for (s = STBI__ZFAST_BITS + 1;; ++s) + if (k < z->maxcode[s]) + break; + if (s == 16) + return -1; // invalid code! + // code size is s, so: + b = (k >> (16 - s)) - z->firstcode[s] + z->firstsymbol[s]; + STBI_ASSERT(z->size[b] == s); + a->code_buffer >>= s; + a->num_bits -= s; + return z->value[b]; +} + +stbi_inline static int stbi__zhuffman_decode(stbi__zbuf* a, stbi__zhuffman* z) +{ + int b, s; + if (a->num_bits < 16) + stbi__fill_bits(a); + b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; + if (b) { + s = b >> 9; + a->code_buffer >>= s; + a->num_bits -= s; + return b & 511; + } + return stbi__zhuffman_decode_slowpath(a, z); +} + +static int stbi__zexpand(stbi__zbuf* z, char* zout, + int n) // need to make room for n bytes +{ + char* q; + int cur, limit, old_limit; + z->zout = zout; + if (!z->z_expandable) + return stbi__err("output buffer limit", "Corrupt PNG"); + cur = (int)(z->zout - z->zout_start); + limit = old_limit = (int)(z->zout_end - z->zout_start); + while (cur + n > limit) + limit *= 2; + q = (char*)STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); + STBI_NOTUSED(old_limit); + if (q == NULL) + return stbi__err("outofmem", "Out of memory"); + z->zout_start = q; + z->zout = q + cur; + z->zout_end = q + limit; + return 1; +} + +static const int stbi__zlength_base[31] = { + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0}; + +static const int stbi__zlength_extra[31] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, + 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, + 4, 4, 5, 5, 5, 5, 0, 0, 0}; + +static const int stbi__zdist_base[32] = { + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, + 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, + 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 0, 0}; + +static const int stbi__zdist_extra[32] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, + 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, + 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; + +static int stbi__parse_huffman_block(stbi__zbuf* a) +{ + char* zout = a->zout; + for (;;) { + int z = stbi__zhuffman_decode(a, &a->z_length); + if (z < 256) { + if (z < 0) + return stbi__err("bad huffman code", + "Corrupt PNG"); // error in huffman codes + if (zout >= a->zout_end) { + if (!stbi__zexpand(a, zout, 1)) + return 0; + zout = a->zout; + } + *zout++ = (char)z; + } + else { + stbi_uc* p; + int len, dist; + if (z == 256) { + a->zout = zout; + return 1; + } + z -= 257; + len = stbi__zlength_base[z]; + if (stbi__zlength_extra[z]) + len += stbi__zreceive(a, stbi__zlength_extra[z]); + z = stbi__zhuffman_decode(a, &a->z_distance); + if (z < 0) + return stbi__err("bad huffman code", "Corrupt PNG"); + dist = stbi__zdist_base[z]; + if (stbi__zdist_extra[z]) + dist += stbi__zreceive(a, stbi__zdist_extra[z]); + if (zout - a->zout_start < dist) + return stbi__err("bad dist", "Corrupt PNG"); + if (zout + len > a->zout_end) { + if (!stbi__zexpand(a, zout, len)) + return 0; + zout = a->zout; + } + p = (stbi_uc*)(zout - dist); + if (dist == 1) { // run of one byte; common in images. + stbi_uc v = *p; + if (len) { + do + *zout++ = v; + while (--len); + } + } + else { + if (len) { + do + *zout++ = *p++; + while (--len); + } + } + } + } +} + +static int stbi__compute_huffman_codes(stbi__zbuf* a) +{ + static const stbi_uc length_dezigzag[19] = { + 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; + stbi__zhuffman z_codelength; + stbi_uc lencodes[286 + 32 + 137]; // padding for maximum single op + stbi_uc codelength_sizes[19]; + int i, n; + + int hlit = stbi__zreceive(a, 5) + 257; + int hdist = stbi__zreceive(a, 5) + 1; + int hclen = stbi__zreceive(a, 4) + 4; + int ntot = hlit + hdist; + + memset(codelength_sizes, 0, sizeof(codelength_sizes)); + for (i = 0; i < hclen; ++i) { + int s = stbi__zreceive(a, 3); + codelength_sizes[length_dezigzag[i]] = (stbi_uc)s; + } + if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) + return 0; + + n = 0; + while (n < ntot) { + int c = stbi__zhuffman_decode(a, &z_codelength); + if (c < 0 || c >= 19) + return stbi__err("bad codelengths", "Corrupt PNG"); + if (c < 16) + lencodes[n++] = (stbi_uc)c; + else { + stbi_uc fill = 0; + if (c == 16) { + c = stbi__zreceive(a, 2) + 3; + if (n == 0) + return stbi__err("bad codelengths", "Corrupt PNG"); + fill = lencodes[n - 1]; + } + else if (c == 17) + c = stbi__zreceive(a, 3) + 3; + else { + STBI_ASSERT(c == 18); + c = stbi__zreceive(a, 7) + 11; + } + if (ntot - n < c) + return stbi__err("bad codelengths", "Corrupt PNG"); + memset(lencodes + n, fill, c); + n += c; + } + } + if (n != ntot) + return stbi__err("bad codelengths", "Corrupt PNG"); + if (!stbi__zbuild_huffman(&a->z_length, lencodes, hlit)) + return 0; + if (!stbi__zbuild_huffman(&a->z_distance, lencodes + hlit, hdist)) + return 0; + return 1; +} + +static int stbi__parse_uncompressed_block(stbi__zbuf* a) +{ + stbi_uc header[4]; + int len, nlen, k; + if (a->num_bits & 7) + stbi__zreceive(a, a->num_bits & 7); // discard + // drain the bit-packed data into header + k = 0; + while (a->num_bits > 0) { + header[k++] = + (stbi_uc)(a->code_buffer & 255); // suppress MSVC run-time check + a->code_buffer >>= 8; + a->num_bits -= 8; + } + STBI_ASSERT(a->num_bits == 0); + // now fill header the normal way + while (k < 4) + header[k++] = stbi__zget8(a); + len = header[1] * 256 + header[0]; + nlen = header[3] * 256 + header[2]; + if (nlen != (len ^ 0xffff)) + return stbi__err("zlib corrupt", "Corrupt PNG"); + if (a->zbuffer + len > a->zbuffer_end) + return stbi__err("read past buffer", "Corrupt PNG"); + if (a->zout + len > a->zout_end) + if (!stbi__zexpand(a, a->zout, len)) + return 0; + memcpy(a->zout, a->zbuffer, len); + a->zbuffer += len; + a->zout += len; + return 1; +} + +static int stbi__parse_zlib_header(stbi__zbuf* a) +{ + int cmf = stbi__zget8(a); + int cm = cmf & 15; + /* int cinfo = cmf >> 4; */ + int flg = stbi__zget8(a); + if ((cmf * 256 + flg) % 31 != 0) + return stbi__err("bad zlib header", "Corrupt PNG"); // zlib spec + if (flg & 32) + return stbi__err("no preset dict", + "Corrupt PNG"); // preset dictionary not allowed in png + if (cm != 8) + return stbi__err("bad compression", + "Corrupt PNG"); // DEFLATE required for png + // window = 1 << (8 + cinfo)... but who cares, we fully buffer output + return 1; +} + +static const stbi_uc stbi__zdefault_length[288] = { + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8}; +static const stbi_uc stbi__zdefault_distance[32] = { + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}; +/* +Init algorithm: +{ + int i; // use <= to match clearly with spec + for (i=0; i <= 143; ++i) stbi__zdefault_length[i] = 8; + for ( ; i <= 255; ++i) stbi__zdefault_length[i] = 9; + for ( ; i <= 279; ++i) stbi__zdefault_length[i] = 7; + for ( ; i <= 287; ++i) stbi__zdefault_length[i] = 8; + + for (i=0; i <= 31; ++i) stbi__zdefault_distance[i] = 5; +} +*/ + +static int stbi__parse_zlib(stbi__zbuf* a, int parse_header) +{ + int final, type; + if (parse_header) + if (!stbi__parse_zlib_header(a)) + return 0; + a->num_bits = 0; + a->code_buffer = 0; + do { + final = stbi__zreceive(a, 1); + type = stbi__zreceive(a, 2); + if (type == 0) { + if (!stbi__parse_uncompressed_block(a)) + return 0; + } + else if (type == 3) { + return 0; + } + else { + if (type == 1) { + // use fixed code lengths + if (!stbi__zbuild_huffman(&a->z_length, stbi__zdefault_length, + 288)) + return 0; + if (!stbi__zbuild_huffman(&a->z_distance, + stbi__zdefault_distance, 32)) + return 0; + } + else { + if (!stbi__compute_huffman_codes(a)) + return 0; + } + if (!stbi__parse_huffman_block(a)) + return 0; + } + } while (!final); + return 1; +} + +static int stbi__do_zlib(stbi__zbuf* a, char* obuf, int olen, int exp, + int parse_header) +{ + a->zout_start = obuf; + a->zout = obuf; + a->zout_end = obuf + olen; + a->z_expandable = exp; + + return stbi__parse_zlib(a, parse_header); +} + +STBIDEF char* stbi_zlib_decode_malloc_guesssize(const char* buffer, int len, + int initial_size, int* outlen) +{ + stbi__zbuf a; + char* p = (char*)stbi__malloc(initial_size); + if (p == NULL) + return NULL; + a.zbuffer = (stbi_uc*)buffer; + a.zbuffer_end = (stbi_uc*)buffer + len; + if (stbi__do_zlib(&a, p, initial_size, 1, 1)) { + if (outlen) + *outlen = (int)(a.zout - a.zout_start); + return a.zout_start; + } + else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF char* stbi_zlib_decode_malloc(char const* buffer, int len, int* outlen) +{ + return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen); +} + +STBIDEF char* stbi_zlib_decode_malloc_guesssize_headerflag(const char* buffer, + int len, + int initial_size, + int* outlen, + int parse_header) +{ + stbi__zbuf a; + char* p = (char*)stbi__malloc(initial_size); + if (p == NULL) + return NULL; + a.zbuffer = (stbi_uc*)buffer; + a.zbuffer_end = (stbi_uc*)buffer + len; + if (stbi__do_zlib(&a, p, initial_size, 1, parse_header)) { + if (outlen) + *outlen = (int)(a.zout - a.zout_start); + return a.zout_start; + } + else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF int stbi_zlib_decode_buffer(char* obuffer, int olen, + char const* ibuffer, int ilen) +{ + stbi__zbuf a; + a.zbuffer = (stbi_uc*)ibuffer; + a.zbuffer_end = (stbi_uc*)ibuffer + ilen; + if (stbi__do_zlib(&a, obuffer, olen, 0, 1)) + return (int)(a.zout - a.zout_start); + else + return -1; +} + +STBIDEF char* stbi_zlib_decode_noheader_malloc(char const* buffer, int len, + int* outlen) +{ + stbi__zbuf a; + char* p = (char*)stbi__malloc(16384); + if (p == NULL) + return NULL; + a.zbuffer = (stbi_uc*)buffer; + a.zbuffer_end = (stbi_uc*)buffer + len; + if (stbi__do_zlib(&a, p, 16384, 1, 0)) { + if (outlen) + *outlen = (int)(a.zout - a.zout_start); + return a.zout_start; + } + else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF int stbi_zlib_decode_noheader_buffer(char* obuffer, int olen, + const char* ibuffer, int ilen) +{ + stbi__zbuf a; + a.zbuffer = (stbi_uc*)ibuffer; + a.zbuffer_end = (stbi_uc*)ibuffer + ilen; + if (stbi__do_zlib(&a, obuffer, olen, 0, 0)) + return (int)(a.zout - a.zout_start); + else + return -1; +} +#endif + +// public domain "baseline" PNG decoder v0.10 Sean Barrett 2006-11-18 +// simple implementation +// - only 8-bit samples +// - no CRC checking +// - allocates lots of intermediate memory +// - avoids problem of streaming data between subsystems +// - avoids explicit window management +// performance +// - uses stb_zlib, a PD zlib implementation with fast huffman decoding + +#ifndef STBI_NO_PNG +typedef struct { + stbi__uint32 length; + stbi__uint32 type; +} stbi__pngchunk; + +static stbi__pngchunk stbi__get_chunk_header(stbi__context* s) +{ + stbi__pngchunk c; + c.length = stbi__get32be(s); + c.type = stbi__get32be(s); + return c; +} + +static int stbi__check_png_header(stbi__context* s) +{ + static const stbi_uc png_sig[8] = {137, 80, 78, 71, 13, 10, 26, 10}; + int i; + for (i = 0; i < 8; ++i) + if (stbi__get8(s) != png_sig[i]) + return stbi__err("bad png sig", "Not a PNG"); + return 1; +} + +typedef struct { + stbi__context* s; + stbi_uc *idata, *expanded, *out; + int depth; +} stbi__png; + +enum { + STBI__F_none = 0, + STBI__F_sub = 1, + STBI__F_up = 2, + STBI__F_avg = 3, + STBI__F_paeth = 4, + // synthetic filters used for first scanline to avoid needing a dummy row of + // 0s + STBI__F_avg_first, + STBI__F_paeth_first +}; + +static stbi_uc first_row_filter[5] = {STBI__F_none, STBI__F_sub, STBI__F_none, + STBI__F_avg_first, STBI__F_paeth_first}; + +static int stbi__paeth(int a, int b, int c) +{ + int p = a + b - c; + int pa = abs(p - a); + int pb = abs(p - b); + int pc = abs(p - c); + if (pa <= pb && pa <= pc) + return a; + if (pb <= pc) + return b; + return c; +} + +static const stbi_uc stbi__depth_scale_table[9] = {0, 0xff, 0x55, 0, 0x11, + 0, 0, 0, 0x01}; + +// create the png data from post-deflated data +static int stbi__create_png_image_raw(stbi__png* a, stbi_uc* raw, + stbi__uint32 raw_len, int out_n, + stbi__uint32 x, stbi__uint32 y, int depth, + int color) +{ + int bytes = (depth == 16 ? 2 : 1); + stbi__context* s = a->s; + stbi__uint32 i, j, stride = x * out_n * bytes; + stbi__uint32 img_len, img_width_bytes; + int k; + int img_n = s->img_n; // copy it into a local for later + + int output_bytes = out_n * bytes; + int filter_bytes = img_n * bytes; + int width = x; + + STBI_ASSERT(out_n == s->img_n || out_n == s->img_n + 1); + a->out = (stbi_uc*)stbi__malloc_mad3( + x, y, output_bytes, 0); // extra bytes to write off the end into + if (!a->out) + return stbi__err("outofmem", "Out of memory"); + + if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) + return stbi__err("too large", "Corrupt PNG"); + img_width_bytes = (((img_n * x * depth) + 7) >> 3); + img_len = (img_width_bytes + 1) * y; + + // we used to check for exact match between raw_len and img_len on + // non-interlaced PNGs, but issue #276 reported a PNG in the wild that had + // extra data at the end (all zeros), so just check for raw_len < img_len + // always. + if (raw_len < img_len) + return stbi__err("not enough pixels", "Corrupt PNG"); + + for (j = 0; j < y; ++j) { + stbi_uc* cur = a->out + stride * j; + stbi_uc* prior; + int filter = *raw++; + + if (filter > 4) + return stbi__err("invalid filter", "Corrupt PNG"); + + if (depth < 8) { + STBI_ASSERT(img_width_bytes <= x); + cur += x * out_n - + img_width_bytes; // store output to the rightmost img_len + // bytes, so we can decode in place + filter_bytes = 1; + width = img_width_bytes; + } + prior = cur - stride; // bugfix: need to compute this after 'cur +=' + // computation above + + // if first row, use special filter that doesn't sample previous row + if (j == 0) + filter = first_row_filter[filter]; + + // handle first byte explicitly + for (k = 0; k < filter_bytes; ++k) { + switch (filter) { + case STBI__F_none: + cur[k] = raw[k]; + break; + case STBI__F_sub: + cur[k] = raw[k]; + break; + case STBI__F_up: + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); + break; + case STBI__F_avg: + cur[k] = STBI__BYTECAST(raw[k] + (prior[k] >> 1)); + break; + case STBI__F_paeth: + cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(0, prior[k], 0)); + break; + case STBI__F_avg_first: + cur[k] = raw[k]; + break; + case STBI__F_paeth_first: + cur[k] = raw[k]; + break; + } + } + + if (depth == 8) { + if (img_n != out_n) + cur[img_n] = 255; // first pixel + raw += img_n; + cur += out_n; + prior += out_n; + } + else if (depth == 16) { + if (img_n != out_n) { + cur[filter_bytes] = 255; // first pixel top byte + cur[filter_bytes + 1] = 255; // first pixel bottom byte + } + raw += filter_bytes; + cur += output_bytes; + prior += output_bytes; + } + else { + raw += 1; + cur += 1; + prior += 1; + } + + // this is a little gross, so that we don't switch per-pixel or + // per-component + if (depth < 8 || img_n == out_n) { + int nk = (width - 1) * filter_bytes; +#define STBI__CASE(f) \ + case f: \ + for (k = 0; k < nk; ++k) + switch (filter) { + // "none" filter turns into a memcpy here; make that explicit. + case STBI__F_none: + memcpy(cur, raw, nk); + break; + STBI__CASE(STBI__F_sub) + { + cur[k] = STBI__BYTECAST(raw[k] + cur[k - filter_bytes]); + } + break; + STBI__CASE(STBI__F_up) + { + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); + } + break; + STBI__CASE(STBI__F_avg) + { + cur[k] = STBI__BYTECAST( + raw[k] + ((prior[k] + cur[k - filter_bytes]) >> 1)); + } + break; + STBI__CASE(STBI__F_paeth) + { + cur[k] = STBI__BYTECAST( + raw[k] + stbi__paeth(cur[k - filter_bytes], prior[k], + prior[k - filter_bytes])); + } + break; + STBI__CASE(STBI__F_avg_first) + { + cur[k] = + STBI__BYTECAST(raw[k] + (cur[k - filter_bytes] >> 1)); + } + break; + STBI__CASE(STBI__F_paeth_first) + { + cur[k] = STBI__BYTECAST( + raw[k] + stbi__paeth(cur[k - filter_bytes], 0, 0)); + } + break; + } +#undef STBI__CASE + raw += nk; + } + else { + STBI_ASSERT(img_n + 1 == out_n); +#define STBI__CASE(f) \ + case f: \ + for (i = x - 1; i >= 1; --i, cur[filter_bytes] = 255, \ + raw += filter_bytes, cur += output_bytes, prior += output_bytes) \ + for (k = 0; k < filter_bytes; ++k) + switch (filter) { + STBI__CASE(STBI__F_none) { cur[k] = raw[k]; } + break; + STBI__CASE(STBI__F_sub) + { + cur[k] = STBI__BYTECAST(raw[k] + cur[k - output_bytes]); + } + break; + STBI__CASE(STBI__F_up) + { + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); + } + break; + STBI__CASE(STBI__F_avg) + { + cur[k] = STBI__BYTECAST( + raw[k] + ((prior[k] + cur[k - output_bytes]) >> 1)); + } + break; + STBI__CASE(STBI__F_paeth) + { + cur[k] = STBI__BYTECAST( + raw[k] + stbi__paeth(cur[k - output_bytes], prior[k], + prior[k - output_bytes])); + } + break; + STBI__CASE(STBI__F_avg_first) + { + cur[k] = + STBI__BYTECAST(raw[k] + (cur[k - output_bytes] >> 1)); + } + break; + STBI__CASE(STBI__F_paeth_first) + { + cur[k] = STBI__BYTECAST( + raw[k] + stbi__paeth(cur[k - output_bytes], 0, 0)); + } + break; + } +#undef STBI__CASE + + // the loop above sets the high byte of the pixels' alpha, but for + // 16 bit png files we also need the low byte set. we'll do that + // here. + if (depth == 16) { + cur = a->out + + stride * j; // start at the beginning of the row again + for (i = 0; i < x; ++i, cur += output_bytes) { + cur[filter_bytes + 1] = 255; + } + } + } + } + + // we make a separate pass to expand bits to pixels; for performance, + // this could run two scanlines behind the above code, so it won't + // intefere with filtering but will still be in the cache. + if (depth < 8) { + for (j = 0; j < y; ++j) { + stbi_uc* cur = a->out + stride * j; + stbi_uc* in = a->out + stride * j + x * out_n - img_width_bytes; + // unpack 1/2/4-bit into a 8-bit buffer. allows us to keep the + // common 8-bit path optimal at minimal cost for 1/2/4-bit png + // guarante byte alignment, if width is not multiple of 8/4/2 we'll + // decode dummy trailing data that will be skipped in the later loop + stbi_uc scale = (color == 0) + ? stbi__depth_scale_table[depth] + : 1; // scale grayscale values to 0..255 range + + // note that the final byte might overshoot and write more data than + // desired. we can allocate enough data that this never writes out + // of memory, but it could also overwrite the next scanline. can it + // overwrite non-empty data on the next scanline? yes, consider + // 1-pixel-wide scanlines with 1-bit-per-pixel. so we need to + // explicitly clamp the final ones + + if (depth == 4) { + for (k = x * img_n; k >= 2; k -= 2, ++in) { + *cur++ = scale * ((*in >> 4)); + *cur++ = scale * ((*in) & 0x0f); + } + if (k > 0) + *cur++ = scale * ((*in >> 4)); + } + else if (depth == 2) { + for (k = x * img_n; k >= 4; k -= 4, ++in) { + *cur++ = scale * ((*in >> 6)); + *cur++ = scale * ((*in >> 4) & 0x03); + *cur++ = scale * ((*in >> 2) & 0x03); + *cur++ = scale * ((*in) & 0x03); + } + if (k > 0) + *cur++ = scale * ((*in >> 6)); + if (k > 1) + *cur++ = scale * ((*in >> 4) & 0x03); + if (k > 2) + *cur++ = scale * ((*in >> 2) & 0x03); + } + else if (depth == 1) { + for (k = x * img_n; k >= 8; k -= 8, ++in) { + *cur++ = scale * ((*in >> 7)); + *cur++ = scale * ((*in >> 6) & 0x01); + *cur++ = scale * ((*in >> 5) & 0x01); + *cur++ = scale * ((*in >> 4) & 0x01); + *cur++ = scale * ((*in >> 3) & 0x01); + *cur++ = scale * ((*in >> 2) & 0x01); + *cur++ = scale * ((*in >> 1) & 0x01); + *cur++ = scale * ((*in) & 0x01); + } + if (k > 0) + *cur++ = scale * ((*in >> 7)); + if (k > 1) + *cur++ = scale * ((*in >> 6) & 0x01); + if (k > 2) + *cur++ = scale * ((*in >> 5) & 0x01); + if (k > 3) + *cur++ = scale * ((*in >> 4) & 0x01); + if (k > 4) + *cur++ = scale * ((*in >> 3) & 0x01); + if (k > 5) + *cur++ = scale * ((*in >> 2) & 0x01); + if (k > 6) + *cur++ = scale * ((*in >> 1) & 0x01); + } + if (img_n != out_n) { + int q; + // insert alpha = 255 + cur = a->out + stride * j; + if (img_n == 1) { + for (q = x - 1; q >= 0; --q) { + cur[q * 2 + 1] = 255; + cur[q * 2 + 0] = cur[q]; + } + } + else { + STBI_ASSERT(img_n == 3); + for (q = x - 1; q >= 0; --q) { + cur[q * 4 + 3] = 255; + cur[q * 4 + 2] = cur[q * 3 + 2]; + cur[q * 4 + 1] = cur[q * 3 + 1]; + cur[q * 4 + 0] = cur[q * 3 + 0]; + } + } + } + } + } + else if (depth == 16) { + // force the image data from big-endian to platform-native. + // this is done in a separate pass due to the decoding relying + // on the data being untouched, but could probably be done + // per-line during decode if care is taken. + stbi_uc* cur = a->out; + stbi__uint16* cur16 = (stbi__uint16*)cur; + + for (i = 0; i < x * y * out_n; ++i, cur16++, cur += 2) { + *cur16 = (cur[0] << 8) | cur[1]; + } + } + + return 1; +} + +static int stbi__create_png_image(stbi__png* a, stbi_uc* image_data, + stbi__uint32 image_data_len, int out_n, + int depth, int color, int interlaced) +{ + int bytes = (depth == 16 ? 2 : 1); + int out_bytes = out_n * bytes; + stbi_uc* final; + int p; + if (!interlaced) + return stbi__create_png_image_raw(a, image_data, image_data_len, out_n, + a->s->img_x, a->s->img_y, depth, + color); + + // de-interlacing + final = (stbi_uc*)stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0); + for (p = 0; p < 7; ++p) { + int xorig[] = {0, 4, 0, 2, 0, 1, 0}; + int yorig[] = {0, 0, 4, 0, 2, 0, 1}; + int xspc[] = {8, 8, 4, 4, 2, 2, 1}; + int yspc[] = {8, 8, 8, 4, 4, 2, 2}; + int i, j, x, y; + // pass1_x[4] = 0, pass1_x[5] = 1, pass1_x[12] = 1 + x = (a->s->img_x - xorig[p] + xspc[p] - 1) / xspc[p]; + y = (a->s->img_y - yorig[p] + yspc[p] - 1) / yspc[p]; + if (x && y) { + stbi__uint32 img_len = + ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y; + if (!stbi__create_png_image_raw(a, image_data, image_data_len, + out_n, x, y, depth, color)) { + STBI_FREE(final); + return 0; + } + for (j = 0; j < y; ++j) { + for (i = 0; i < x; ++i) { + int out_y = j * yspc[p] + yorig[p]; + int out_x = i * xspc[p] + xorig[p]; + memcpy(final + out_y * a->s->img_x * out_bytes + + out_x * out_bytes, + a->out + (j * x + i) * out_bytes, out_bytes); + } + } + STBI_FREE(a->out); + image_data += img_len; + image_data_len -= img_len; + } + } + a->out = final; + + return 1; +} + +static int stbi__compute_transparency(stbi__png* z, stbi_uc tc[3], int out_n) +{ + stbi__context* s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi_uc* p = z->out; + + // compute color-based transparency, assuming we've + // already got 255 as the alpha value in the output + STBI_ASSERT(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i = 0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 255); + p += 2; + } + } + else { + for (i = 0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int stbi__compute_transparency16(stbi__png* z, stbi__uint16 tc[3], + int out_n) +{ + stbi__context* s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi__uint16* p = (stbi__uint16*)z->out; + + // compute color-based transparency, assuming we've + // already got 65535 as the alpha value in the output + STBI_ASSERT(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i = 0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 65535); + p += 2; + } + } + else { + for (i = 0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int stbi__expand_png_palette(stbi__png* a, stbi_uc* palette, int len, + int pal_img_n) +{ + stbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y; + stbi_uc *p, *temp_out, *orig = a->out; + + p = (stbi_uc*)stbi__malloc_mad2(pixel_count, pal_img_n, 0); + if (p == NULL) + return stbi__err("outofmem", "Out of memory"); + + // between here and free(out) below, exitting would leak + temp_out = p; + + if (pal_img_n == 3) { + for (i = 0; i < pixel_count; ++i) { + int n = orig[i] * 4; + p[0] = palette[n]; + p[1] = palette[n + 1]; + p[2] = palette[n + 2]; + p += 3; + } + } + else { + for (i = 0; i < pixel_count; ++i) { + int n = orig[i] * 4; + p[0] = palette[n]; + p[1] = palette[n + 1]; + p[2] = palette[n + 2]; + p[3] = palette[n + 3]; + p += 4; + } + } + STBI_FREE(a->out); + a->out = temp_out; + + STBI_NOTUSED(len); + + return 1; +} + +static int stbi__unpremultiply_on_load = 0; +static int stbi__de_iphone_flag = 0; + +STBIDEF void +stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) +{ + stbi__unpremultiply_on_load = flag_true_if_should_unpremultiply; +} + +STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert) +{ + stbi__de_iphone_flag = flag_true_if_should_convert; +} + +static void stbi__de_iphone(stbi__png* z) +{ + stbi__context* s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi_uc* p = z->out; + + if (s->img_out_n == 3) { // convert bgr to rgb + for (i = 0; i < pixel_count; ++i) { + stbi_uc t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 3; + } + } + else { + STBI_ASSERT(s->img_out_n == 4); + if (stbi__unpremultiply_on_load) { + // convert bgr to rgb and unpremultiply + for (i = 0; i < pixel_count; ++i) { + stbi_uc a = p[3]; + stbi_uc t = p[0]; + if (a) { + stbi_uc half = a / 2; + p[0] = (p[2] * 255 + half) / a; + p[1] = (p[1] * 255 + half) / a; + p[2] = (t * 255 + half) / a; + } + else { + p[0] = p[2]; + p[2] = t; + } + p += 4; + } + } + else { + // convert bgr to rgb + for (i = 0; i < pixel_count; ++i) { + stbi_uc t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 4; + } + } + } +} + +#define STBI__PNG_TYPE(a, b, c, d) \ + (((unsigned)(a) << 24) + ((unsigned)(b) << 16) + ((unsigned)(c) << 8) + \ + (unsigned)(d)) + +static int stbi__parse_png_file(stbi__png* z, int scan, int req_comp) +{ + stbi_uc palette[1024], pal_img_n = 0; + stbi_uc has_trans = 0, tc[3] = {0}; + stbi__uint16 tc16[3]; + stbi__uint32 ioff = 0, idata_limit = 0, i, pal_len = 0; + int first = 1, k, interlace = 0, color = 0, is_iphone = 0; + stbi__context* s = z->s; + + z->expanded = NULL; + z->idata = NULL; + z->out = NULL; + + if (!stbi__check_png_header(s)) + return 0; + + if (scan == STBI__SCAN_type) + return 1; + + for (;;) { + stbi__pngchunk c = stbi__get_chunk_header(s); + switch (c.type) { + case STBI__PNG_TYPE('C', 'g', 'B', 'I'): + is_iphone = 1; + stbi__skip(s, c.length); + break; + case STBI__PNG_TYPE('I', 'H', 'D', 'R'): { + int comp, filter; + if (!first) + return stbi__err("multiple IHDR", "Corrupt PNG"); + first = 0; + if (c.length != 13) + return stbi__err("bad IHDR len", "Corrupt PNG"); + s->img_x = stbi__get32be(s); + if (s->img_x > (1 << 24)) + return stbi__err("too large", "Very large image (corrupt?)"); + s->img_y = stbi__get32be(s); + if (s->img_y > (1 << 24)) + return stbi__err("too large", "Very large image (corrupt?)"); + z->depth = stbi__get8(s); + if (z->depth != 1 && z->depth != 2 && z->depth != 4 && + z->depth != 8 && z->depth != 16) + return stbi__err("1/2/4/8/16-bit only", + "PNG not supported: 1/2/4/8/16-bit only"); + color = stbi__get8(s); + if (color > 6) + return stbi__err("bad ctype", "Corrupt PNG"); + if (color == 3 && z->depth == 16) + return stbi__err("bad ctype", "Corrupt PNG"); + if (color == 3) + pal_img_n = 3; + else if (color & 1) + return stbi__err("bad ctype", "Corrupt PNG"); + comp = stbi__get8(s); + if (comp) + return stbi__err("bad comp method", "Corrupt PNG"); + filter = stbi__get8(s); + if (filter) + return stbi__err("bad filter method", "Corrupt PNG"); + interlace = stbi__get8(s); + if (interlace > 1) + return stbi__err("bad interlace method", "Corrupt PNG"); + if (!s->img_x || !s->img_y) + return stbi__err("0-pixel image", "Corrupt PNG"); + if (!pal_img_n) { + s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); + if ((1 << 30) / s->img_x / s->img_n < s->img_y) + return stbi__err("too large", "Image too large to decode"); + if (scan == STBI__SCAN_header) + return 1; + } + else { + // if paletted, then pal_n is our final components, and + // img_n is # components to decompress/filter. + s->img_n = 1; + if ((1 << 30) / s->img_x / 4 < s->img_y) + return stbi__err("too large", "Corrupt PNG"); + // if SCAN_header, have to scan to see if we have a tRNS + } + break; + } + + case STBI__PNG_TYPE('P', 'L', 'T', 'E'): { + if (first) + return stbi__err("first not IHDR", "Corrupt PNG"); + if (c.length > 256 * 3) + return stbi__err("invalid PLTE", "Corrupt PNG"); + pal_len = c.length / 3; + if (pal_len * 3 != c.length) + return stbi__err("invalid PLTE", "Corrupt PNG"); + for (i = 0; i < pal_len; ++i) { + palette[i * 4 + 0] = stbi__get8(s); + palette[i * 4 + 1] = stbi__get8(s); + palette[i * 4 + 2] = stbi__get8(s); + palette[i * 4 + 3] = 255; + } + break; + } + + case STBI__PNG_TYPE('t', 'R', 'N', 'S'): { + if (first) + return stbi__err("first not IHDR", "Corrupt PNG"); + if (z->idata) + return stbi__err("tRNS after IDAT", "Corrupt PNG"); + if (pal_img_n) { + if (scan == STBI__SCAN_header) { + s->img_n = 4; + return 1; + } + if (pal_len == 0) + return stbi__err("tRNS before PLTE", "Corrupt PNG"); + if (c.length > pal_len) + return stbi__err("bad tRNS len", "Corrupt PNG"); + pal_img_n = 4; + for (i = 0; i < c.length; ++i) + palette[i * 4 + 3] = stbi__get8(s); + } + else { + if (!(s->img_n & 1)) + return stbi__err("tRNS with alpha", "Corrupt PNG"); + if (c.length != (stbi__uint32)s->img_n * 2) + return stbi__err("bad tRNS len", "Corrupt PNG"); + has_trans = 1; + if (z->depth == 16) { + for (k = 0; k < s->img_n; ++k) + tc16[k] = (stbi__uint16)stbi__get16be( + s); // copy the values as-is + } + else { + for (k = 0; k < s->img_n; ++k) + tc[k] = + (stbi_uc)(stbi__get16be(s) & 255) * + stbi__depth_scale_table + [z->depth]; // non 8-bit images will be larger + } + } + break; + } + + case STBI__PNG_TYPE('I', 'D', 'A', 'T'): { + if (first) + return stbi__err("first not IHDR", "Corrupt PNG"); + if (pal_img_n && !pal_len) + return stbi__err("no PLTE", "Corrupt PNG"); + if (scan == STBI__SCAN_header) { + s->img_n = pal_img_n; + return 1; + } + if ((int)(ioff + c.length) < (int)ioff) + return 0; + if (ioff + c.length > idata_limit) { + stbi__uint32 idata_limit_old = idata_limit; + stbi_uc* p; + if (idata_limit == 0) + idata_limit = c.length > 4096 ? c.length : 4096; + while (ioff + c.length > idata_limit) + idata_limit *= 2; + STBI_NOTUSED(idata_limit_old); + p = (stbi_uc*)STBI_REALLOC_SIZED(z->idata, idata_limit_old, + idata_limit); + if (p == NULL) + return stbi__err("outofmem", "Out of memory"); + z->idata = p; + } + if (!stbi__getn(s, z->idata + ioff, c.length)) + return stbi__err("outofdata", "Corrupt PNG"); + ioff += c.length; + break; + } + + case STBI__PNG_TYPE('I', 'E', 'N', 'D'): { + stbi__uint32 raw_len, bpl; + if (first) + return stbi__err("first not IHDR", "Corrupt PNG"); + if (scan != STBI__SCAN_load) + return 1; + if (z->idata == NULL) + return stbi__err("no IDAT", "Corrupt PNG"); + // initial guess for decoded data size to avoid unnecessary reallocs + bpl = + (s->img_x * z->depth + 7) / 8; // bytes per line, per component + raw_len = bpl * s->img_y * s->img_n /* pixels */ + + s->img_y /* filter mode per row */; + z->expanded = + (stbi_uc*)stbi_zlib_decode_malloc_guesssize_headerflag( + (char*)z->idata, ioff, raw_len, (int*)&raw_len, !is_iphone); + if (z->expanded == NULL) + return 0; // zlib should set error + STBI_FREE(z->idata); + z->idata = NULL; + if ((req_comp == s->img_n + 1 && req_comp != 3 && !pal_img_n) || + has_trans) + s->img_out_n = s->img_n + 1; + else + s->img_out_n = s->img_n; + if (!stbi__create_png_image(z, z->expanded, raw_len, s->img_out_n, + z->depth, color, interlace)) + return 0; + if (has_trans) { + if (z->depth == 16) { + if (!stbi__compute_transparency16(z, tc16, s->img_out_n)) + return 0; + } + else { + if (!stbi__compute_transparency(z, tc, s->img_out_n)) + return 0; + } + } + if (is_iphone && stbi__de_iphone_flag && s->img_out_n > 2) + stbi__de_iphone(z); + if (pal_img_n) { + // pal_img_n == 3 or 4 + s->img_n = pal_img_n; // record the actual colors we had + s->img_out_n = pal_img_n; + if (req_comp >= 3) + s->img_out_n = req_comp; + if (!stbi__expand_png_palette(z, palette, pal_len, + s->img_out_n)) + return 0; + } + else if (has_trans) { + // non-paletted image with tRNS -> source image has (constant) + // alpha + ++s->img_n; + } + STBI_FREE(z->expanded); + z->expanded = NULL; + return 1; + } + + default: + // if critical, fail + if (first) + return stbi__err("first not IHDR", "Corrupt PNG"); + if ((c.type & (1 << 29)) == 0) { +#ifndef STBI_NO_FAILURE_STRINGS + // not threadsafe + static char invalid_chunk[] = "XXXX PNG chunk not known"; + invalid_chunk[0] = STBI__BYTECAST(c.type >> 24); + invalid_chunk[1] = STBI__BYTECAST(c.type >> 16); + invalid_chunk[2] = STBI__BYTECAST(c.type >> 8); + invalid_chunk[3] = STBI__BYTECAST(c.type >> 0); +#endif + return stbi__err(invalid_chunk, + "PNG not supported: unknown PNG chunk type"); + } + stbi__skip(s, c.length); + break; + } + // end of PNG chunk, read and skip CRC + stbi__get32be(s); + } +} + +static void* stbi__do_png(stbi__png* p, int* x, int* y, int* n, int req_comp, + stbi__result_info* ri) +{ + void* result = NULL; + if (req_comp < 0 || req_comp > 4) + return stbi__errpuc("bad req_comp", "Internal error"); + if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { + if (p->depth < 8) + ri->bits_per_channel = 8; + else + ri->bits_per_channel = p->depth; + result = p->out; + p->out = NULL; + if (req_comp && req_comp != p->s->img_out_n) { + if (ri->bits_per_channel == 8) + result = stbi__convert_format((unsigned char*)result, + p->s->img_out_n, req_comp, + p->s->img_x, p->s->img_y); + else + result = stbi__convert_format16((stbi__uint16*)result, + p->s->img_out_n, req_comp, + p->s->img_x, p->s->img_y); + p->s->img_out_n = req_comp; + if (result == NULL) + return result; + } + *x = p->s->img_x; + *y = p->s->img_y; + if (n) + *n = p->s->img_n; + } + STBI_FREE(p->out); + p->out = NULL; + STBI_FREE(p->expanded); + p->expanded = NULL; + STBI_FREE(p->idata); + p->idata = NULL; + + return result; +} + +static void* stbi__png_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri) +{ + stbi__png p; + p.s = s; + return stbi__do_png(&p, x, y, comp, req_comp, ri); +} + +static int stbi__png_test(stbi__context* s) +{ + int r; + r = stbi__check_png_header(s); + stbi__rewind(s); + return r; +} + +static int stbi__png_info_raw(stbi__png* p, int* x, int* y, int* comp) +{ + if (!stbi__parse_png_file(p, STBI__SCAN_header, 0)) { + stbi__rewind(p->s); + return 0; + } + if (x) + *x = p->s->img_x; + if (y) + *y = p->s->img_y; + if (comp) + *comp = p->s->img_n; + return 1; +} + +static int stbi__png_info(stbi__context* s, int* x, int* y, int* comp) +{ + stbi__png p; + p.s = s; + return stbi__png_info_raw(&p, x, y, comp); +} + +static int stbi__png_is16(stbi__context* s) +{ + stbi__png p; + p.s = s; + if (!stbi__png_info_raw(&p, NULL, NULL, NULL)) + return 0; + if (p.depth != 16) { + stbi__rewind(p.s); + return 0; + } + return 1; +} +#endif + +// Microsoft/Windows BMP image + +#ifndef STBI_NO_BMP +static int stbi__bmp_test_raw(stbi__context* s) +{ + int r; + int sz; + if (stbi__get8(s) != 'B') + return 0; + if (stbi__get8(s) != 'M') + return 0; + stbi__get32le(s); // discard filesize + stbi__get16le(s); // discard reserved + stbi__get16le(s); // discard reserved + stbi__get32le(s); // discard data offset + sz = stbi__get32le(s); + r = (sz == 12 || sz == 40 || sz == 56 || sz == 108 || sz == 124); + return r; +} + +static int stbi__bmp_test(stbi__context* s) +{ + int r = stbi__bmp_test_raw(s); + stbi__rewind(s); + return r; +} + +// returns 0..31 for the highest set bit +static int stbi__high_bit(unsigned int z) +{ + int n = 0; + if (z == 0) + return -1; + if (z >= 0x10000) { + n += 16; + z >>= 16; + } + if (z >= 0x00100) { + n += 8; + z >>= 8; + } + if (z >= 0x00010) { + n += 4; + z >>= 4; + } + if (z >= 0x00004) { + n += 2; + z >>= 2; + } + if (z >= 0x00002) { + n += 1; + z >>= 1; + } + return n; +} + +static int stbi__bitcount(unsigned int a) +{ + a = (a & 0x55555555) + ((a >> 1) & 0x55555555); // max 2 + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); // max 4 + a = (a + (a >> 4)) & 0x0f0f0f0f; // max 8 per 4, now 8 bits + a = (a + (a >> 8)); // max 16 per 8 bits + a = (a + (a >> 16)); // max 32 per 8 bits + return a & 0xff; +} + +// extract an arbitrarily-aligned N-bit value (N=bits) +// from v, and then make it 8-bits long and fractionally +// extend it to full full range. +static int stbi__shiftsigned(unsigned int v, int shift, int bits) +{ + static unsigned int mul_table[9] = { + 0, + 0xff /*0b11111111*/, + 0x55 /*0b01010101*/, + 0x49 /*0b01001001*/, + 0x11 /*0b00010001*/, + 0x21 /*0b00100001*/, + 0x41 /*0b01000001*/, + 0x81 /*0b10000001*/, + 0x01 /*0b00000001*/, + }; + static unsigned int shift_table[9] = { + 0, 0, 0, 1, 0, 2, 4, 6, 0, + }; + if (shift < 0) + v <<= -shift; + else + v >>= shift; + STBI_ASSERT(v >= 0 && v < 256); + v >>= (8 - bits); + STBI_ASSERT(bits >= 0 && bits <= 8); + return (int)((unsigned)v * mul_table[bits]) >> shift_table[bits]; +} + +typedef struct { + int bpp, offset, hsz; + unsigned int mr, mg, mb, ma, all_a; +} stbi__bmp_data; + +static void* stbi__bmp_parse_header(stbi__context* s, stbi__bmp_data* info) +{ + int hsz; + if (stbi__get8(s) != 'B' || stbi__get8(s) != 'M') + return stbi__errpuc("not BMP", "Corrupt BMP"); + stbi__get32le(s); // discard filesize + stbi__get16le(s); // discard reserved + stbi__get16le(s); // discard reserved + info->offset = stbi__get32le(s); + info->hsz = hsz = stbi__get32le(s); + info->mr = info->mg = info->mb = info->ma = 0; + + if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) + return stbi__errpuc("unknown BMP", "BMP type not supported: unknown"); + if (hsz == 12) { + s->img_x = stbi__get16le(s); + s->img_y = stbi__get16le(s); + } + else { + s->img_x = stbi__get32le(s); + s->img_y = stbi__get32le(s); + } + if (stbi__get16le(s) != 1) + return stbi__errpuc("bad BMP", "bad BMP"); + info->bpp = stbi__get16le(s); + if (hsz != 12) { + int compress = stbi__get32le(s); + if (compress == 1 || compress == 2) + return stbi__errpuc("BMP RLE", "BMP type not supported: RLE"); + stbi__get32le(s); // discard sizeof + stbi__get32le(s); // discard hres + stbi__get32le(s); // discard vres + stbi__get32le(s); // discard colorsused + stbi__get32le(s); // discard max important + if (hsz == 40 || hsz == 56) { + if (hsz == 56) { + stbi__get32le(s); + stbi__get32le(s); + stbi__get32le(s); + stbi__get32le(s); + } + if (info->bpp == 16 || info->bpp == 32) { + if (compress == 0) { + if (info->bpp == 32) { + info->mr = 0xffu << 16; + info->mg = 0xffu << 8; + info->mb = 0xffu << 0; + info->ma = 0xffu << 24; + info->all_a = 0; // if all_a is 0 at end, then we loaded + // alpha channel but it was all 0 + } + else { + info->mr = 31u << 10; + info->mg = 31u << 5; + info->mb = 31u << 0; + } + } + else if (compress == 3) { + info->mr = stbi__get32le(s); + info->mg = stbi__get32le(s); + info->mb = stbi__get32le(s); + // not documented, but generated by photoshop and handled by + // mspaint + if (info->mr == info->mg && info->mg == info->mb) { + // ?!?!? + return stbi__errpuc("bad BMP", "bad BMP"); + } + } + else + return stbi__errpuc("bad BMP", "bad BMP"); + } + } + else { + int i; + if (hsz != 108 && hsz != 124) + return stbi__errpuc("bad BMP", "bad BMP"); + info->mr = stbi__get32le(s); + info->mg = stbi__get32le(s); + info->mb = stbi__get32le(s); + info->ma = stbi__get32le(s); + stbi__get32le(s); // discard color space + for (i = 0; i < 12; ++i) + stbi__get32le(s); // discard color space parameters + if (hsz == 124) { + stbi__get32le(s); // discard rendering intent + stbi__get32le(s); // discard offset of profile data + stbi__get32le(s); // discard size of profile data + stbi__get32le(s); // discard reserved + } + } + } + return (void*)1; +} + +static void* stbi__bmp_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri) +{ + stbi_uc* out; + unsigned int mr = 0, mg = 0, mb = 0, ma = 0, all_a; + stbi_uc pal[256][4]; + int psize = 0, i, j, width; + int flip_vertically, pad, target; + stbi__bmp_data info; + STBI_NOTUSED(ri); + + info.all_a = 255; + if (stbi__bmp_parse_header(s, &info) == NULL) + return NULL; // error code already set + + flip_vertically = ((int)s->img_y) > 0; + s->img_y = abs((int)s->img_y); + + mr = info.mr; + mg = info.mg; + mb = info.mb; + ma = info.ma; + all_a = info.all_a; + + if (info.hsz == 12) { + if (info.bpp < 24) + psize = (info.offset - 14 - 24) / 3; + } + else { + if (info.bpp < 16) + psize = (info.offset - 14 - info.hsz) >> 2; + } + + s->img_n = ma ? 4 : 3; + if (req_comp && req_comp >= 3) // we can directly decode 3 or 4 + target = req_comp; + else + target = s->img_n; // if they want monochrome, we'll post-convert + + // sanity-check size + if (!stbi__mad3sizes_valid(target, s->img_x, s->img_y, 0)) + return stbi__errpuc("too large", "Corrupt BMP"); + + out = (stbi_uc*)stbi__malloc_mad3(target, s->img_x, s->img_y, 0); + if (!out) + return stbi__errpuc("outofmem", "Out of memory"); + if (info.bpp < 16) { + int z = 0; + if (psize == 0 || psize > 256) { + STBI_FREE(out); + return stbi__errpuc("invalid", "Corrupt BMP"); + } + for (i = 0; i < psize; ++i) { + pal[i][2] = stbi__get8(s); + pal[i][1] = stbi__get8(s); + pal[i][0] = stbi__get8(s); + if (info.hsz != 12) + stbi__get8(s); + pal[i][3] = 255; + } + stbi__skip(s, info.offset - 14 - info.hsz - + psize * (info.hsz == 12 ? 3 : 4)); + if (info.bpp == 1) + width = (s->img_x + 7) >> 3; + else if (info.bpp == 4) + width = (s->img_x + 1) >> 1; + else if (info.bpp == 8) + width = s->img_x; + else { + STBI_FREE(out); + return stbi__errpuc("bad bpp", "Corrupt BMP"); + } + pad = (-width) & 3; + if (info.bpp == 1) { + for (j = 0; j < (int)s->img_y; ++j) { + int bit_offset = 7, v = stbi__get8(s); + for (i = 0; i < (int)s->img_x; ++i) { + int color = (v >> bit_offset) & 0x1; + out[z++] = pal[color][0]; + out[z++] = pal[color][1]; + out[z++] = pal[color][2]; + if (target == 4) + out[z++] = 255; + if (i + 1 == (int)s->img_x) + break; + if ((--bit_offset) < 0) { + bit_offset = 7; + v = stbi__get8(s); + } + } + stbi__skip(s, pad); + } + } + else { + for (j = 0; j < (int)s->img_y; ++j) { + for (i = 0; i < (int)s->img_x; i += 2) { + int v = stbi__get8(s), v2 = 0; + if (info.bpp == 4) { + v2 = v & 15; + v >>= 4; + } + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) + out[z++] = 255; + if (i + 1 == (int)s->img_x) + break; + v = (info.bpp == 8) ? stbi__get8(s) : v2; + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) + out[z++] = 255; + } + stbi__skip(s, pad); + } + } + } + else { + int rshift = 0, gshift = 0, bshift = 0, ashift = 0, rcount = 0, + gcount = 0, bcount = 0, acount = 0; + int z = 0; + int easy = 0; + stbi__skip(s, info.offset - 14 - info.hsz); + if (info.bpp == 24) + width = 3 * s->img_x; + else if (info.bpp == 16) + width = 2 * s->img_x; + else /* bpp = 32 and pad = 0 */ + width = 0; + pad = (-width) & 3; + if (info.bpp == 24) { + easy = 1; + } + else if (info.bpp == 32) { + if (mb == 0xff && mg == 0xff00 && mr == 0x00ff0000 && + ma == 0xff000000) + easy = 2; + } + if (!easy) { + if (!mr || !mg || !mb) { + STBI_FREE(out); + return stbi__errpuc("bad masks", "Corrupt BMP"); + } + // right shift amt to put high bit in position #7 + rshift = stbi__high_bit(mr) - 7; + rcount = stbi__bitcount(mr); + gshift = stbi__high_bit(mg) - 7; + gcount = stbi__bitcount(mg); + bshift = stbi__high_bit(mb) - 7; + bcount = stbi__bitcount(mb); + ashift = stbi__high_bit(ma) - 7; + acount = stbi__bitcount(ma); + } + for (j = 0; j < (int)s->img_y; ++j) { + if (easy) { + for (i = 0; i < (int)s->img_x; ++i) { + unsigned char a; + out[z + 2] = stbi__get8(s); + out[z + 1] = stbi__get8(s); + out[z + 0] = stbi__get8(s); + z += 3; + a = (easy == 2 ? stbi__get8(s) : 255); + all_a |= a; + if (target == 4) + out[z++] = a; + } + } + else { + int bpp = info.bpp; + for (i = 0; i < (int)s->img_x; ++i) { + stbi__uint32 v = (bpp == 16 ? (stbi__uint32)stbi__get16le(s) + : stbi__get32le(s)); + unsigned int a; + out[z++] = STBI__BYTECAST( + stbi__shiftsigned(v & mr, rshift, rcount)); + out[z++] = STBI__BYTECAST( + stbi__shiftsigned(v & mg, gshift, gcount)); + out[z++] = STBI__BYTECAST( + stbi__shiftsigned(v & mb, bshift, bcount)); + a = (ma ? stbi__shiftsigned(v & ma, ashift, acount) : 255); + all_a |= a; + if (target == 4) + out[z++] = STBI__BYTECAST(a); + } + } + stbi__skip(s, pad); + } + } + + // if alpha channel is all 0s, replace with all 255s + if (target == 4 && all_a == 0) + for (i = 4 * s->img_x * s->img_y - 1; i >= 0; i -= 4) + out[i] = 255; + + if (flip_vertically) { + stbi_uc t; + for (j = 0; j < (int)s->img_y >> 1; ++j) { + stbi_uc* p1 = out + j * s->img_x * target; + stbi_uc* p2 = out + (s->img_y - 1 - j) * s->img_x * target; + for (i = 0; i < (int)s->img_x * target; ++i) { + t = p1[i]; + p1[i] = p2[i]; + p2[i] = t; + } + } + } + + if (req_comp && req_comp != target) { + out = stbi__convert_format(out, target, req_comp, s->img_x, s->img_y); + if (out == NULL) + return out; // stbi__convert_format frees input on failure + } + + *x = s->img_x; + *y = s->img_y; + if (comp) + *comp = s->img_n; + return out; +} +#endif + +// Targa Truevision - TGA +// by Jonathan Dummer +#ifndef STBI_NO_TGA +// returns STBI_rgb or whatever, 0 on error +static int stbi__tga_get_comp(int bits_per_pixel, int is_grey, int* is_rgb16) +{ + // only RGB or RGBA (incl. 16bit) or grey allowed + if (is_rgb16) + *is_rgb16 = 0; + switch (bits_per_pixel) { + case 8: + return STBI_grey; + case 16: + if (is_grey) + return STBI_grey_alpha; + // fallthrough + case 15: + if (is_rgb16) + *is_rgb16 = 1; + return STBI_rgb; + case 24: // fallthrough + case 32: + return bits_per_pixel / 8; + default: + return 0; + } +} + +static int stbi__tga_info(stbi__context* s, int* x, int* y, int* comp) +{ + int tga_w, tga_h, tga_comp, tga_image_type, tga_bits_per_pixel, + tga_colormap_bpp; + int sz, tga_colormap_type; + stbi__get8(s); // discard Offset + tga_colormap_type = stbi__get8(s); // colormap type + if (tga_colormap_type > 1) { + stbi__rewind(s); + return 0; // only RGB or indexed allowed + } + tga_image_type = stbi__get8(s); // image type + if (tga_colormap_type == 1) { // colormapped (paletted) image + if (tga_image_type != 1 && tga_image_type != 9) { + stbi__rewind(s); + return 0; + } + stbi__skip( + s, 4); // skip index of first colormap entry and number of entries + sz = stbi__get8(s); // check bits per palette color entry + if ((sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32)) { + stbi__rewind(s); + return 0; + } + stbi__skip(s, 4); // skip image x and y origin + tga_colormap_bpp = sz; + } + else { // "normal" image w/o colormap - only RGB or grey allowed, +/- RLE + if ((tga_image_type != 2) && (tga_image_type != 3) && + (tga_image_type != 10) && (tga_image_type != 11)) { + stbi__rewind(s); + return 0; // only RGB or grey allowed, +/- RLE + } + stbi__skip(s, 9); // skip colormap specification and image x/y origin + tga_colormap_bpp = 0; + } + tga_w = stbi__get16le(s); + if (tga_w < 1) { + stbi__rewind(s); + return 0; // test width + } + tga_h = stbi__get16le(s); + if (tga_h < 1) { + stbi__rewind(s); + return 0; // test height + } + tga_bits_per_pixel = stbi__get8(s); // bits per pixel + stbi__get8(s); // ignore alpha bits + if (tga_colormap_bpp != 0) { + if ((tga_bits_per_pixel != 8) && (tga_bits_per_pixel != 16)) { + // when using a colormap, tga_bits_per_pixel is the size of the + // indexes I don't think anything but 8 or 16bit indexes makes sense + stbi__rewind(s); + return 0; + } + tga_comp = stbi__tga_get_comp(tga_colormap_bpp, 0, NULL); + } + else { + tga_comp = stbi__tga_get_comp( + tga_bits_per_pixel, (tga_image_type == 3) || (tga_image_type == 11), + NULL); + } + if (!tga_comp) { + stbi__rewind(s); + return 0; + } + if (x) + *x = tga_w; + if (y) + *y = tga_h; + if (comp) + *comp = tga_comp; + return 1; // seems to have passed everything +} + +static int stbi__tga_test(stbi__context* s) +{ + int res = 0; + int sz, tga_color_type; + stbi__get8(s); // discard Offset + tga_color_type = stbi__get8(s); // color type + if (tga_color_type > 1) + goto errorEnd; // only RGB or indexed allowed + sz = stbi__get8(s); // image type + if (tga_color_type == 1) { // colormapped (paletted) image + if (sz != 1 && sz != 9) + goto errorEnd; // colortype 1 demands image type 1 or 9 + stbi__skip( + s, 4); // skip index of first colormap entry and number of entries + sz = stbi__get8(s); // check bits per palette color entry + if ((sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32)) + goto errorEnd; + stbi__skip(s, 4); // skip image x and y origin + } + else { // "normal" image w/o colormap + if ((sz != 2) && (sz != 3) && (sz != 10) && (sz != 11)) + goto errorEnd; // only RGB or grey allowed, +/- RLE + stbi__skip(s, 9); // skip colormap specification and image x/y origin + } + if (stbi__get16le(s) < 1) + goto errorEnd; // test width + if (stbi__get16le(s) < 1) + goto errorEnd; // test height + sz = stbi__get8(s); // bits per pixel + if ((tga_color_type == 1) && (sz != 8) && (sz != 16)) + goto errorEnd; // for colormapped images, bpp is size of an index + if ((sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32)) + goto errorEnd; + + res = 1; // if we got this far, everything's good and we can return 1 + // instead of 0 + +errorEnd: + stbi__rewind(s); + return res; +} + +// read 16bit value and convert to 24bit RGB +static void stbi__tga_read_rgb16(stbi__context* s, stbi_uc* out) +{ + stbi__uint16 px = (stbi__uint16)stbi__get16le(s); + stbi__uint16 fiveBitMask = 31; + // we have 3 channels with 5bits each + int r = (px >> 10) & fiveBitMask; + int g = (px >> 5) & fiveBitMask; + int b = px & fiveBitMask; + // Note that this saves the data in RGB(A) order, so it doesn't need to be + // swapped later + out[0] = (stbi_uc)((r * 255) / 31); + out[1] = (stbi_uc)((g * 255) / 31); + out[2] = (stbi_uc)((b * 255) / 31); + + // some people claim that the most significant bit might be used for alpha + // (possibly if an alpha-bit is set in the "image descriptor byte") + // but that only made 16bit test images completely translucent.. + // so let's treat all 15 and 16bit TGAs as RGB with no alpha. +} + +static void* stbi__tga_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri) +{ + // read in the TGA header stuff + int tga_offset = stbi__get8(s); + int tga_indexed = stbi__get8(s); + int tga_image_type = stbi__get8(s); + int tga_is_RLE = 0; + int tga_palette_start = stbi__get16le(s); + int tga_palette_len = stbi__get16le(s); + int tga_palette_bits = stbi__get8(s); + int tga_x_origin = stbi__get16le(s); + int tga_y_origin = stbi__get16le(s); + int tga_width = stbi__get16le(s); + int tga_height = stbi__get16le(s); + int tga_bits_per_pixel = stbi__get8(s); + int tga_comp, tga_rgb16 = 0; + int tga_inverted = stbi__get8(s); + // int tga_alpha_bits = tga_inverted & 15; // the 4 lowest bits - unused + // (useless?) + // image data + unsigned char* tga_data; + unsigned char* tga_palette = NULL; + int i, j; + unsigned char raw_data[4] = {0}; + int RLE_count = 0; + int RLE_repeating = 0; + int read_next_pixel = 1; + STBI_NOTUSED(ri); + + // do a tiny bit of precessing + if (tga_image_type >= 8) { + tga_image_type -= 8; + tga_is_RLE = 1; + } + tga_inverted = 1 - ((tga_inverted >> 5) & 1); + + // If I'm paletted, then I'll use the number of bits from the palette + if (tga_indexed) + tga_comp = stbi__tga_get_comp(tga_palette_bits, 0, &tga_rgb16); + else + tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3), + &tga_rgb16); + + if (!tga_comp) // shouldn't really happen, stbi__tga_test() should have + // ensured basic consistency + return stbi__errpuc("bad format", "Can't find out TGA pixelformat"); + + // tga info + *x = tga_width; + *y = tga_height; + if (comp) + *comp = tga_comp; + + if (!stbi__mad3sizes_valid(tga_width, tga_height, tga_comp, 0)) + return stbi__errpuc("too large", "Corrupt TGA"); + + tga_data = + (unsigned char*)stbi__malloc_mad3(tga_width, tga_height, tga_comp, 0); + if (!tga_data) + return stbi__errpuc("outofmem", "Out of memory"); + + // skip to the data's starting position (offset usually = 0) + stbi__skip(s, tga_offset); + + if (!tga_indexed && !tga_is_RLE && !tga_rgb16) { + for (i = 0; i < tga_height; ++i) { + int row = tga_inverted ? tga_height - i - 1 : i; + stbi_uc* tga_row = tga_data + row * tga_width * tga_comp; + stbi__getn(s, tga_row, tga_width * tga_comp); + } + } + else { + // do I need to load a palette? + if (tga_indexed) { + // any data to skip? (offset usually = 0) + stbi__skip(s, tga_palette_start); + // load the palette + tga_palette = + (unsigned char*)stbi__malloc_mad2(tga_palette_len, tga_comp, 0); + if (!tga_palette) { + STBI_FREE(tga_data); + return stbi__errpuc("outofmem", "Out of memory"); + } + if (tga_rgb16) { + stbi_uc* pal_entry = tga_palette; + STBI_ASSERT(tga_comp == STBI_rgb); + for (i = 0; i < tga_palette_len; ++i) { + stbi__tga_read_rgb16(s, pal_entry); + pal_entry += tga_comp; + } + } + else if (!stbi__getn(s, tga_palette, tga_palette_len * tga_comp)) { + STBI_FREE(tga_data); + STBI_FREE(tga_palette); + return stbi__errpuc("bad palette", "Corrupt TGA"); + } + } + // load the data + for (i = 0; i < tga_width * tga_height; ++i) { + // if I'm in RLE mode, do I need to get a RLE stbi__pngchunk? + if (tga_is_RLE) { + if (RLE_count == 0) { + // yep, get the next byte as a RLE command + int RLE_cmd = stbi__get8(s); + RLE_count = 1 + (RLE_cmd & 127); + RLE_repeating = RLE_cmd >> 7; + read_next_pixel = 1; + } + else if (!RLE_repeating) { + read_next_pixel = 1; + } + } + else { + read_next_pixel = 1; + } + // OK, if I need to read a pixel, do it now + if (read_next_pixel) { + // load however much data we did have + if (tga_indexed) { + // read in index, then perform the lookup + int pal_idx = (tga_bits_per_pixel == 8) ? stbi__get8(s) + : stbi__get16le(s); + if (pal_idx >= tga_palette_len) { + // invalid index + pal_idx = 0; + } + pal_idx *= tga_comp; + for (j = 0; j < tga_comp; ++j) { + raw_data[j] = tga_palette[pal_idx + j]; + } + } + else if (tga_rgb16) { + STBI_ASSERT(tga_comp == STBI_rgb); + stbi__tga_read_rgb16(s, raw_data); + } + else { + // read in the data raw + for (j = 0; j < tga_comp; ++j) { + raw_data[j] = stbi__get8(s); + } + } + // clear the reading flag for the next pixel + read_next_pixel = 0; + } // end of reading a pixel + + // copy data + for (j = 0; j < tga_comp; ++j) + tga_data[i * tga_comp + j] = raw_data[j]; + + // in case we're in RLE mode, keep counting down + --RLE_count; + } + // do I need to invert the image? + if (tga_inverted) { + for (j = 0; j * 2 < tga_height; ++j) { + int index1 = j * tga_width * tga_comp; + int index2 = (tga_height - 1 - j) * tga_width * tga_comp; + for (i = tga_width * tga_comp; i > 0; --i) { + unsigned char temp = tga_data[index1]; + tga_data[index1] = tga_data[index2]; + tga_data[index2] = temp; + ++index1; + ++index2; + } + } + } + // clear my palette, if I had one + if (tga_palette != NULL) { + STBI_FREE(tga_palette); + } + } + + // swap RGB - if the source data was RGB16, it already is in the right order + if (tga_comp >= 3 && !tga_rgb16) { + unsigned char* tga_pixel = tga_data; + for (i = 0; i < tga_width * tga_height; ++i) { + unsigned char temp = tga_pixel[0]; + tga_pixel[0] = tga_pixel[2]; + tga_pixel[2] = temp; + tga_pixel += tga_comp; + } + } + + // convert to target component count + if (req_comp && req_comp != tga_comp) + tga_data = stbi__convert_format(tga_data, tga_comp, req_comp, tga_width, + tga_height); + + // the things I do to get rid of an error message, and yet keep + // Microsoft's C compilers happy... [8^( + tga_palette_start = tga_palette_len = tga_palette_bits = tga_x_origin = + tga_y_origin = 0; + // OK, done + return tga_data; +} +#endif + +// ************************************************************************************************* +// Photoshop PSD loader -- PD by Thatcher Ulrich, integration by Nicolas Schulz, +// tweaked by STB + +#ifndef STBI_NO_PSD +static int stbi__psd_test(stbi__context* s) +{ + int r = (stbi__get32be(s) == 0x38425053); + stbi__rewind(s); + return r; +} + +static int stbi__psd_decode_rle(stbi__context* s, stbi_uc* p, int pixelCount) +{ + int count, nleft, len; + + count = 0; + while ((nleft = pixelCount - count) > 0) { + len = stbi__get8(s); + if (len == 128) { + // No-op. + } + else if (len < 128) { + // Copy next len+1 bytes literally. + len++; + if (len > nleft) + return 0; // corrupt data + count += len; + while (len) { + *p = stbi__get8(s); + p += 4; + len--; + } + } + else if (len > 128) { + stbi_uc val; + // Next -len+1 bytes in the dest are replicated from next source + // byte. (Interpret len as a negative 8-bit int.) + len = 257 - len; + if (len > nleft) + return 0; // corrupt data + val = stbi__get8(s); + count += len; + while (len) { + *p = val; + p += 4; + len--; + } + } + } + + return 1; +} + +static void* stbi__psd_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri, int bpc) +{ + int pixelCount; + int channelCount, compression; + int channel, i; + int bitdepth; + int w, h; + stbi_uc* out; + STBI_NOTUSED(ri); + + // Check identifier + if (stbi__get32be(s) != 0x38425053) // "8BPS" + return stbi__errpuc("not PSD", "Corrupt PSD image"); + + // Check file type version. + if (stbi__get16be(s) != 1) + return stbi__errpuc("wrong version", + "Unsupported version of PSD image"); + + // Skip 6 reserved bytes. + stbi__skip(s, 6); + + // Read the number of channels (R, G, B, A, etc). + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) + return stbi__errpuc("wrong channel count", + "Unsupported number of channels in PSD image"); + + // Read the rows and columns of the image. + h = stbi__get32be(s); + w = stbi__get32be(s); + + // Make sure the depth is 8 bits. + bitdepth = stbi__get16be(s); + if (bitdepth != 8 && bitdepth != 16) + return stbi__errpuc("unsupported bit depth", + "PSD bit depth is not 8 or 16 bit"); + + // Make sure the color mode is RGB. + // Valid options are: + // 0: Bitmap + // 1: Grayscale + // 2: Indexed color + // 3: RGB color + // 4: CMYK color + // 7: Multichannel + // 8: Duotone + // 9: Lab color + if (stbi__get16be(s) != 3) + return stbi__errpuc("wrong color format", + "PSD is not in RGB color format"); + + // Skip the Mode Data. (It's the palette for indexed color; other info for + // other modes.) + stbi__skip(s, stbi__get32be(s)); + + // Skip the image resources. (resolution, pen tool paths, etc) + stbi__skip(s, stbi__get32be(s)); + + // Skip the reserved data. + stbi__skip(s, stbi__get32be(s)); + + // Find out if the data is compressed. + // Known values: + // 0: no compression + // 1: RLE compressed + compression = stbi__get16be(s); + if (compression > 1) + return stbi__errpuc("bad compression", + "PSD has an unknown compression format"); + + // Check size + if (!stbi__mad3sizes_valid(4, w, h, 0)) + return stbi__errpuc("too large", "Corrupt PSD"); + + // Create the destination image. + + if (!compression && bitdepth == 16 && bpc == 16) { + out = (stbi_uc*)stbi__malloc_mad3(8, w, h, 0); + ri->bits_per_channel = 16; + } + else + out = (stbi_uc*)stbi__malloc(4 * w * h); + + if (!out) + return stbi__errpuc("outofmem", "Out of memory"); + pixelCount = w * h; + + // Initialize the data to zero. + // memset( out, 0, pixelCount * 4 ); + + // Finally, the image data. + if (compression) { + // RLE as used by .PSD and .TIFF + // Loop until you get the number of unpacked bytes you are expecting: + // Read the next source byte into n. + // If n is between 0 and 127 inclusive, copy the next n+1 bytes + // literally. Else if n is between -127 and -1 inclusive, copy the + // next byte -n+1 times. Else if n is 128, noop. + // Endloop + + // The RLE-compressed data is preceded by a 2-byte data count for each + // row in the data, which we're going to just skip. + stbi__skip(s, h * channelCount * 2); + + // Read the RLE data by channel. + for (channel = 0; channel < 4; channel++) { + stbi_uc* p; + + p = out + channel; + if (channel >= channelCount) { + // Fill this channel with default data. + for (i = 0; i < pixelCount; i++, p += 4) + *p = (channel == 3 ? 255 : 0); + } + else { + // Read the RLE data. + if (!stbi__psd_decode_rle(s, p, pixelCount)) { + STBI_FREE(out); + return stbi__errpuc("corrupt", "bad RLE data"); + } + } + } + } + else { + // We're at the raw image data. It's each channel in order (Red, Green, + // Blue, Alpha, ...) where each channel consists of an 8-bit (or 16-bit) + // value for each pixel in the image. + + // Read the data by channel. + for (channel = 0; channel < 4; channel++) { + if (channel >= channelCount) { + // Fill this channel with default data. + if (bitdepth == 16 && bpc == 16) { + stbi__uint16* q = ((stbi__uint16*)out) + channel; + stbi__uint16 val = channel == 3 ? 65535 : 0; + for (i = 0; i < pixelCount; i++, q += 4) + *q = val; + } + else { + stbi_uc* p = out + channel; + stbi_uc val = channel == 3 ? 255 : 0; + for (i = 0; i < pixelCount; i++, p += 4) + *p = val; + } + } + else { + if (ri->bits_per_channel == 16) { // output bpc + stbi__uint16* q = ((stbi__uint16*)out) + channel; + for (i = 0; i < pixelCount; i++, q += 4) + *q = (stbi__uint16)stbi__get16be(s); + } + else { + stbi_uc* p = out + channel; + if (bitdepth == 16) { // input bpc + for (i = 0; i < pixelCount; i++, p += 4) + *p = (stbi_uc)(stbi__get16be(s) >> 8); + } + else { + for (i = 0; i < pixelCount; i++, p += 4) + *p = stbi__get8(s); + } + } + } + } + } + + // remove weird white matte from PSD + if (channelCount >= 4) { + if (ri->bits_per_channel == 16) { + for (i = 0; i < w * h; ++i) { + stbi__uint16* pixel = (stbi__uint16*)out + 4 * i; + if (pixel[3] != 0 && pixel[3] != 65535) { + float a = pixel[3] / 65535.0f; + float ra = 1.0f / a; + float inv_a = 65535.0f * (1 - ra); + pixel[0] = (stbi__uint16)(pixel[0] * ra + inv_a); + pixel[1] = (stbi__uint16)(pixel[1] * ra + inv_a); + pixel[2] = (stbi__uint16)(pixel[2] * ra + inv_a); + } + } + } + else { + for (i = 0; i < w * h; ++i) { + unsigned char* pixel = out + 4 * i; + if (pixel[3] != 0 && pixel[3] != 255) { + float a = pixel[3] / 255.0f; + float ra = 1.0f / a; + float inv_a = 255.0f * (1 - ra); + pixel[0] = (unsigned char)(pixel[0] * ra + inv_a); + pixel[1] = (unsigned char)(pixel[1] * ra + inv_a); + pixel[2] = (unsigned char)(pixel[2] * ra + inv_a); + } + } + } + } + + // convert to desired output format + if (req_comp && req_comp != 4) { + if (ri->bits_per_channel == 16) + out = (stbi_uc*)stbi__convert_format16((stbi__uint16*)out, 4, + req_comp, w, h); + else + out = stbi__convert_format(out, 4, req_comp, w, h); + if (out == NULL) + return out; // stbi__convert_format frees input on failure + } + + if (comp) + *comp = 4; + *y = h; + *x = w; + + return out; +} +#endif + +// ************************************************************************************************* +// Softimage PIC loader +// by Tom Seddon +// +// See http://softimage.wiki.softimage.com/index.php/INFO:_PIC_file_format +// See http://ozviz.wasp.uwa.edu.au/~pbourke/dataformats/softimagepic/ + +#ifndef STBI_NO_PIC +static int stbi__pic_is4(stbi__context* s, const char* str) +{ + int i; + for (i = 0; i < 4; ++i) + if (stbi__get8(s) != (stbi_uc)str[i]) + return 0; + + return 1; +} + +static int stbi__pic_test_core(stbi__context* s) +{ + int i; + + if (!stbi__pic_is4(s, "\x53\x80\xF6\x34")) + return 0; + + for (i = 0; i < 84; ++i) + stbi__get8(s); + + if (!stbi__pic_is4(s, "PICT")) + return 0; + + return 1; +} + +typedef struct { + stbi_uc size, type, channel; +} stbi__pic_packet; + +static stbi_uc* stbi__readval(stbi__context* s, int channel, stbi_uc* dest) +{ + int mask = 0x80, i; + + for (i = 0; i < 4; ++i, mask >>= 1) { + if (channel & mask) { + if (stbi__at_eof(s)) + return stbi__errpuc("bad file", "PIC file too short"); + dest[i] = stbi__get8(s); + } + } + + return dest; +} + +static void stbi__copyval(int channel, stbi_uc* dest, const stbi_uc* src) +{ + int mask = 0x80, i; + + for (i = 0; i < 4; ++i, mask >>= 1) + if (channel & mask) + dest[i] = src[i]; +} + +static stbi_uc* stbi__pic_load_core(stbi__context* s, int width, int height, + int* comp, stbi_uc* result) +{ + int act_comp = 0, num_packets = 0, y, chained; + stbi__pic_packet packets[10]; + + // this will (should...) cater for even some bizarre stuff like having data + // for the same channel in multiple packets. + do { + stbi__pic_packet* packet; + + if (num_packets == sizeof(packets) / sizeof(packets[0])) + return stbi__errpuc("bad format", "too many packets"); + + packet = &packets[num_packets++]; + + chained = stbi__get8(s); + packet->size = stbi__get8(s); + packet->type = stbi__get8(s); + packet->channel = stbi__get8(s); + + act_comp |= packet->channel; + + if (stbi__at_eof(s)) + return stbi__errpuc("bad file", "file too short (reading packets)"); + if (packet->size != 8) + return stbi__errpuc("bad format", "packet isn't 8bpp"); + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); // has alpha channel? + + for (y = 0; y < height; ++y) { + int packet_idx; + + for (packet_idx = 0; packet_idx < num_packets; ++packet_idx) { + stbi__pic_packet* packet = &packets[packet_idx]; + stbi_uc* dest = result + y * width * 4; + + switch (packet->type) { + default: + return stbi__errpuc("bad format", + "packet has bad compression type"); + + case 0: { // uncompressed + int x; + + for (x = 0; x < width; ++x, dest += 4) + if (!stbi__readval(s, packet->channel, dest)) + return 0; + break; + } + + case 1: // Pure RLE + { + int left = width, i; + + while (left > 0) { + stbi_uc count, value[4]; + + count = stbi__get8(s); + if (stbi__at_eof(s)) + return stbi__errpuc("bad file", + "file too short (pure read count)"); + + if (count > left) + count = (stbi_uc)left; + + if (!stbi__readval(s, packet->channel, value)) + return 0; + + for (i = 0; i < count; ++i, dest += 4) + stbi__copyval(packet->channel, dest, value); + left -= count; + } + } break; + + case 2: { // Mixed RLE + int left = width; + while (left > 0) { + int count = stbi__get8(s), i; + if (stbi__at_eof(s)) + return stbi__errpuc( + "bad file", "file too short (mixed read count)"); + + if (count >= 128) { // Repeated + stbi_uc value[4]; + + if (count == 128) + count = stbi__get16be(s); + else + count -= 127; + if (count > left) + return stbi__errpuc("bad file", "scanline overrun"); + + if (!stbi__readval(s, packet->channel, value)) + return 0; + + for (i = 0; i < count; ++i, dest += 4) + stbi__copyval(packet->channel, dest, value); + } + else { // Raw + ++count; + if (count > left) + return stbi__errpuc("bad file", "scanline overrun"); + + for (i = 0; i < count; ++i, dest += 4) + if (!stbi__readval(s, packet->channel, dest)) + return 0; + } + left -= count; + } + break; + } + } + } + } + + return result; +} + +static void* stbi__pic_load(stbi__context* s, int* px, int* py, int* comp, + int req_comp, stbi__result_info* ri) +{ + stbi_uc* result; + int i, x, y, internal_comp; + STBI_NOTUSED(ri); + + if (!comp) + comp = &internal_comp; + + for (i = 0; i < 92; ++i) + stbi__get8(s); + + x = stbi__get16be(s); + y = stbi__get16be(s); + if (stbi__at_eof(s)) + return stbi__errpuc("bad file", "file too short (pic header)"); + if (!stbi__mad3sizes_valid(x, y, 4, 0)) + return stbi__errpuc("too large", "PIC image too large to decode"); + + stbi__get32be(s); // skip `ratio' + stbi__get16be(s); // skip `fields' + stbi__get16be(s); // skip `pad' + + // intermediate buffer is RGBA + result = (stbi_uc*)stbi__malloc_mad3(x, y, 4, 0); + memset(result, 0xff, x * y * 4); + + if (!stbi__pic_load_core(s, x, y, comp, result)) { + STBI_FREE(result); + result = 0; + } + *px = x; + *py = y; + if (req_comp == 0) + req_comp = *comp; + result = stbi__convert_format(result, 4, req_comp, x, y); + + return result; +} + +static int stbi__pic_test(stbi__context* s) +{ + int r = stbi__pic_test_core(s); + stbi__rewind(s); + return r; +} +#endif + +// ************************************************************************************************* +// GIF loader -- public domain by Jean-Marc Lienher -- simplified/shrunk by stb + +#ifndef STBI_NO_GIF +typedef struct { + stbi__int16 prefix; + stbi_uc first; + stbi_uc suffix; +} stbi__gif_lzw; + +typedef struct { + int w, h; + stbi_uc* out; // output buffer (always 4 components) + stbi_uc* + background; // The current "background" as far as a gif is concerned + stbi_uc* history; + int flags, bgindex, ratio, transparent, eflags; + stbi_uc pal[256][4]; + stbi_uc lpal[256][4]; + stbi__gif_lzw codes[8192]; + stbi_uc* color_table; + int parse, step; + int lflags; + int start_x, start_y; + int max_x, max_y; + int cur_x, cur_y; + int line_size; + int delay; +} stbi__gif; + +static int stbi__gif_test_raw(stbi__context* s) +{ + int sz; + if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || + stbi__get8(s) != '8') + return 0; + sz = stbi__get8(s); + if (sz != '9' && sz != '7') + return 0; + if (stbi__get8(s) != 'a') + return 0; + return 1; +} + +static int stbi__gif_test(stbi__context* s) +{ + int r = stbi__gif_test_raw(s); + stbi__rewind(s); + return r; +} + +static void stbi__gif_parse_colortable(stbi__context* s, stbi_uc pal[256][4], + int num_entries, int transp) +{ + int i; + for (i = 0; i < num_entries; ++i) { + pal[i][2] = stbi__get8(s); + pal[i][1] = stbi__get8(s); + pal[i][0] = stbi__get8(s); + pal[i][3] = transp == i ? 0 : 255; + } +} + +static int stbi__gif_header(stbi__context* s, stbi__gif* g, int* comp, + int is_info) +{ + stbi_uc version; + if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || + stbi__get8(s) != '8') + return stbi__err("not GIF", "Corrupt GIF"); + + version = stbi__get8(s); + if (version != '7' && version != '9') + return stbi__err("not GIF", "Corrupt GIF"); + if (stbi__get8(s) != 'a') + return stbi__err("not GIF", "Corrupt GIF"); + + stbi__g_failure_reason = ""; + g->w = stbi__get16le(s); + g->h = stbi__get16le(s); + g->flags = stbi__get8(s); + g->bgindex = stbi__get8(s); + g->ratio = stbi__get8(s); + g->transparent = -1; + + if (comp != 0) + *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the + // comments + + if (is_info) + return 1; + + if (g->flags & 0x80) + stbi__gif_parse_colortable(s, g->pal, 2 << (g->flags & 7), -1); + + return 1; +} + +static int stbi__gif_info_raw(stbi__context* s, int* x, int* y, int* comp) +{ + stbi__gif* g = (stbi__gif*)stbi__malloc(sizeof(stbi__gif)); + if (!stbi__gif_header(s, g, comp, 1)) { + STBI_FREE(g); + stbi__rewind(s); + return 0; + } + if (x) + *x = g->w; + if (y) + *y = g->h; + STBI_FREE(g); + return 1; +} + +static void stbi__out_gif_code(stbi__gif* g, stbi__uint16 code) +{ + stbi_uc *p, *c; + int idx; + + // recurse to decode the prefixes, since the linked-list is backwards, + // and working backwards through an interleaved image would be nasty + if (g->codes[code].prefix >= 0) + stbi__out_gif_code(g, g->codes[code].prefix); + + if (g->cur_y >= g->max_y) + return; + + idx = g->cur_x + g->cur_y; + p = &g->out[idx]; + g->history[idx / 4] = 1; + + c = &g->color_table[g->codes[code].suffix * 4]; + if (c[3] > 128) { // don't render transparent pixels; + p[0] = c[2]; + p[1] = c[1]; + p[2] = c[0]; + p[3] = c[3]; + } + g->cur_x += 4; + + if (g->cur_x >= g->max_x) { + g->cur_x = g->start_x; + g->cur_y += g->step; + + while (g->cur_y >= g->max_y && g->parse > 0) { + g->step = (1 << g->parse) * g->line_size; + g->cur_y = g->start_y + (g->step >> 1); + --g->parse; + } + } +} + +static stbi_uc* stbi__process_gif_raster(stbi__context* s, stbi__gif* g) +{ + stbi_uc lzw_cs; + stbi__int32 len, init_code; + stbi__uint32 first; + stbi__int32 codesize, codemask, avail, oldcode, bits, valid_bits, clear; + stbi__gif_lzw* p; + + lzw_cs = stbi__get8(s); + if (lzw_cs > 12) + return NULL; + clear = 1 << lzw_cs; + first = 1; + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + bits = 0; + valid_bits = 0; + for (init_code = 0; init_code < clear; init_code++) { + g->codes[init_code].prefix = -1; + g->codes[init_code].first = (stbi_uc)init_code; + g->codes[init_code].suffix = (stbi_uc)init_code; + } + + // support no starting clear code + avail = clear + 2; + oldcode = -1; + + len = 0; + for (;;) { + if (valid_bits < codesize) { + if (len == 0) { + len = stbi__get8(s); // start new block + if (len == 0) + return g->out; + } + --len; + bits |= (stbi__int32)stbi__get8(s) << valid_bits; + valid_bits += 8; + } + else { + stbi__int32 code = bits & codemask; + bits >>= codesize; + valid_bits -= codesize; + // @OPTIMIZE: is there some way we can accelerate the non-clear + // path? + if (code == clear) { // clear code + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + avail = clear + 2; + oldcode = -1; + first = 0; + } + else if (code == clear + 1) { // end of stream code + stbi__skip(s, len); + while ((len = stbi__get8(s)) > 0) + stbi__skip(s, len); + return g->out; + } + else if (code <= avail) { + if (first) { + return stbi__errpuc("no clear code", "Corrupt GIF"); + } + + if (oldcode >= 0) { + p = &g->codes[avail++]; + if (avail > 8192) { + return stbi__errpuc("too many codes", "Corrupt GIF"); + } + + p->prefix = (stbi__int16)oldcode; + p->first = g->codes[oldcode].first; + p->suffix = + (code == avail) ? p->first : g->codes[code].first; + } + else if (code == avail) + return stbi__errpuc("illegal code in raster", + "Corrupt GIF"); + + stbi__out_gif_code(g, (stbi__uint16)code); + + if ((avail & codemask) == 0 && avail <= 0x0FFF) { + codesize++; + codemask = (1 << codesize) - 1; + } + + oldcode = code; + } + else { + return stbi__errpuc("illegal code in raster", "Corrupt GIF"); + } + } + } +} + +// this function is designed to support animated gifs, although stb_image +// doesn't support it two back is the image from two frames ago, used for a very +// specific disposal format +static stbi_uc* stbi__gif_load_next(stbi__context* s, stbi__gif* g, int* comp, + int req_comp, stbi_uc* two_back) +{ + int dispose; + int first_frame; + int pi; + int pcount; + STBI_NOTUSED(req_comp); + + // on first frame, any non-written pixels get the background colour + // (non-transparent) + first_frame = 0; + if (g->out == 0) { + if (!stbi__gif_header(s, g, comp, 0)) + return 0; // stbi__g_failure_reason set by stbi__gif_header + g->out = (stbi_uc*)stbi__malloc(4 * g->w * g->h); + g->background = (stbi_uc*)stbi__malloc(4 * g->w * g->h); + g->history = (stbi_uc*)stbi__malloc(g->w * g->h); + if (g->out == 0) + return stbi__errpuc("outofmem", "Out of memory"); + + // image is treated as "transparent" at the start - ie, nothing + // overwrites the current background; background colour is only used for + // pixels that are not rendered first frame, after that "background" + // color refers to the color that was there the previous frame. + memset(g->out, 0x00, 4 * g->w * g->h); + memset(g->background, 0x00, + 4 * g->w * g->h); // state of the background (starts transparent) + memset(g->history, 0x00, + g->w * g->h); // pixels that were affected previous frame + first_frame = 1; + } + else { + // second frame - how do we dispoase of the previous one? + dispose = (g->eflags & 0x1C) >> 2; + pcount = g->w * g->h; + + if ((dispose == 3) && (two_back == 0)) { + dispose = 2; // if I don't have an image to revert back to, default + // to the old background + } + + if (dispose == 3) { // use previous graphic + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi]) { + memcpy(&g->out[pi * 4], &two_back[pi * 4], 4); + } + } + } + else if (dispose == 2) { + // restore what was changed last frame to background before that + // frame; + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi]) { + memcpy(&g->out[pi * 4], &g->background[pi * 4], 4); + } + } + } + else { + // This is a non-disposal case eithe way, so just + // leave the pixels as is, and they will become the new background + // 1: do not dispose + // 0: not specified. + } + + // background is what out is after the undoing of the previou frame; + memcpy(g->background, g->out, 4 * g->w * g->h); + } + + // clear my history; + memset(g->history, 0x00, + g->w * g->h); // pixels that were affected previous frame + + for (;;) { + int tag = stbi__get8(s); + switch (tag) { + case 0x2C: /* Image Descriptor */ + { + stbi__int32 x, y, w, h; + stbi_uc* o; + + x = stbi__get16le(s); + y = stbi__get16le(s); + w = stbi__get16le(s); + h = stbi__get16le(s); + if (((x + w) > (g->w)) || ((y + h) > (g->h))) + return stbi__errpuc("bad Image Descriptor", "Corrupt GIF"); + + g->line_size = g->w * 4; + g->start_x = x * 4; + g->start_y = y * g->line_size; + g->max_x = g->start_x + w * 4; + g->max_y = g->start_y + h * g->line_size; + g->cur_x = g->start_x; + g->cur_y = g->start_y; + + g->lflags = stbi__get8(s); + + if (g->lflags & 0x40) { + g->step = 8 * g->line_size; // first interlaced spacing + g->parse = 3; + } + else { + g->step = g->line_size; + g->parse = 0; + } + + if (g->lflags & 0x80) { + stbi__gif_parse_colortable(s, g->lpal, 2 << (g->lflags & 7), + g->eflags & 0x01 ? g->transparent + : -1); + g->color_table = (stbi_uc*)g->lpal; + } + else if (g->flags & 0x80) { + g->color_table = (stbi_uc*)g->pal; + } + else + return stbi__errpuc("missing color table", "Corrupt GIF"); + + o = stbi__process_gif_raster(s, g); + if (o == NULL) + return NULL; + + // if this was the first frame, + pcount = g->w * g->h; + if (first_frame && (g->bgindex > 0)) { + // if first frame, any pixel not drawn to gets the background + // color + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi] == 0) { + g->pal[g->bgindex][3] = + 255; // just in case it was made transparent, undo + // that; It will be reset next frame if need + // be; + memcpy(&g->out[pi * 4], &g->pal[g->bgindex], 4); + } + } + } + + return o; + } + + case 0x21: // Comment Extension. + { + int len; + int ext = stbi__get8(s); + if (ext == 0xF9) { // Graphic Control Extension. + len = stbi__get8(s); + if (len == 4) { + g->eflags = stbi__get8(s); + g->delay = + 10 * stbi__get16le(s); // delay - 1/100th of a second, + // saving as 1/1000ths. + + // unset old transparent + if (g->transparent >= 0) { + g->pal[g->transparent][3] = 255; + } + if (g->eflags & 0x01) { + g->transparent = stbi__get8(s); + if (g->transparent >= 0) { + g->pal[g->transparent][3] = 0; + } + } + else { + // don't need transparent + stbi__skip(s, 1); + g->transparent = -1; + } + } + else { + stbi__skip(s, len); + break; + } + } + while ((len = stbi__get8(s)) != 0) { + stbi__skip(s, len); + } + break; + } + + case 0x3B: // gif stream termination code + return (stbi_uc*)s; // using '1' causes warning on some compilers + + default: + return stbi__errpuc("unknown code", "Corrupt GIF"); + } + } +} + +static void* stbi__load_gif_main(stbi__context* s, int** delays, int* x, int* y, + int* z, int* comp, int req_comp) +{ + if (stbi__gif_test(s)) { + int layers = 0; + stbi_uc* u = 0; + stbi_uc* out = 0; + stbi_uc* two_back = 0; + stbi__gif g; + int stride; + memset(&g, 0, sizeof(g)); + if (delays) { + *delays = 0; + } + + do { + u = stbi__gif_load_next(s, &g, comp, req_comp, two_back); + if (u == (stbi_uc*)s) + u = 0; // end of animated gif marker + + if (u) { + *x = g.w; + *y = g.h; + ++layers; + stride = g.w * g.h * 4; + + if (out) { + out = (stbi_uc*)STBI_REALLOC(out, layers * stride); + if (delays) { + *delays = + (int*)STBI_REALLOC(*delays, sizeof(int) * layers); + } + } + else { + out = (stbi_uc*)stbi__malloc(layers * stride); + if (delays) { + *delays = (int*)stbi__malloc(layers * sizeof(int)); + } + } + memcpy(out + ((layers - 1) * stride), u, stride); + if (layers >= 2) { + two_back = out - 2 * stride; + } + + if (delays) { + (*delays)[layers - 1U] = g.delay; + } + } + } while (u != 0); + + // free temp buffer; + STBI_FREE(g.out); + STBI_FREE(g.history); + STBI_FREE(g.background); + + // do the final conversion after loading everything; + if (req_comp && req_comp != 4) + out = stbi__convert_format(out, 4, req_comp, layers * g.w, g.h); + + *z = layers; + return out; + } + else { + return stbi__errpuc("not GIF", "Image was not as a gif type."); + } +} + +static void* stbi__gif_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri) +{ + stbi_uc* u = 0; + stbi__gif g; + memset(&g, 0, sizeof(g)); + STBI_NOTUSED(ri); + + u = stbi__gif_load_next(s, &g, comp, req_comp, 0); + if (u == (stbi_uc*)s) + u = 0; // end of animated gif marker + if (u) { + *x = g.w; + *y = g.h; + + // moved conversion to after successful load so that the same + // can be done for multiple frames. + if (req_comp && req_comp != 4) + u = stbi__convert_format(u, 4, req_comp, g.w, g.h); + } + + // free buffers needed for multiple frame loading; + STBI_FREE(g.history); + STBI_FREE(g.background); + + return u; +} + +static int stbi__gif_info(stbi__context* s, int* x, int* y, int* comp) +{ + return stbi__gif_info_raw(s, x, y, comp); +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR loader +// originally by Nicolas Schulz +#ifndef STBI_NO_HDR +static int stbi__hdr_test_core(stbi__context* s, const char* signature) +{ + int i; + for (i = 0; signature[i]; ++i) + if (stbi__get8(s) != signature[i]) + return 0; + stbi__rewind(s); + return 1; +} + +static int stbi__hdr_test(stbi__context* s) +{ + int r = stbi__hdr_test_core(s, "#?RADIANCE\n"); + stbi__rewind(s); + if (!r) { + r = stbi__hdr_test_core(s, "#?RGBE\n"); + stbi__rewind(s); + } + return r; +} + +#define STBI__HDR_BUFLEN 1024 +static char* stbi__hdr_gettoken(stbi__context* z, char* buffer) +{ + int len = 0; + char c = '\0'; + + c = (char)stbi__get8(z); + + while (!stbi__at_eof(z) && c != '\n') { + buffer[len++] = c; + if (len == STBI__HDR_BUFLEN - 1) { + // flush to end of line + while (!stbi__at_eof(z) && stbi__get8(z) != '\n') + ; + break; + } + c = (char)stbi__get8(z); + } + + buffer[len] = 0; + return buffer; +} + +static void stbi__hdr_convert(float* output, stbi_uc* input, int req_comp) +{ + if (input[3] != 0) { + float f1; + // Exponent + f1 = (float)ldexp(1.0f, input[3] - (int)(128 + 8)); + if (req_comp <= 2) + output[0] = (input[0] + input[1] + input[2]) * f1 / 3; + else { + output[0] = input[0] * f1; + output[1] = input[1] * f1; + output[2] = input[2] * f1; + } + if (req_comp == 2) + output[1] = 1; + if (req_comp == 4) + output[3] = 1; + } + else { + switch (req_comp) { + case 4: + output[3] = 1; /* fallthrough */ + case 3: + output[0] = output[1] = output[2] = 0; + break; + case 2: + output[1] = 1; /* fallthrough */ + case 1: + output[0] = 0; + break; + } + } +} + +static float* stbi__hdr_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri) +{ + char buffer[STBI__HDR_BUFLEN]; + char* token; + int valid = 0; + int width, height; + stbi_uc* scanline; + float* hdr_data; + int len; + unsigned char count, value; + int i, j, k, c1, c2, z; + const char* headerToken; + STBI_NOTUSED(ri); + + // Check identifier + headerToken = stbi__hdr_gettoken(s, buffer); + if (strcmp(headerToken, "#?RADIANCE") != 0 && + strcmp(headerToken, "#?RGBE") != 0) + return stbi__errpf("not HDR", "Corrupt HDR image"); + + // Parse header + for (;;) { + token = stbi__hdr_gettoken(s, buffer); + if (token[0] == 0) + break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) + valid = 1; + } + + if (!valid) + return stbi__errpf("unsupported format", "Unsupported HDR format"); + + // Parse width and height + // can't use sscanf() if we're not using stdio! + token = stbi__hdr_gettoken(s, buffer); + if (strncmp(token, "-Y ", 3)) + return stbi__errpf("unsupported data layout", "Unsupported HDR format"); + token += 3; + height = (int)strtol(token, &token, 10); + while (*token == ' ') + ++token; + if (strncmp(token, "+X ", 3)) + return stbi__errpf("unsupported data layout", "Unsupported HDR format"); + token += 3; + width = (int)strtol(token, NULL, 10); + + *x = width; + *y = height; + + if (comp) + *comp = 3; + if (req_comp == 0) + req_comp = 3; + + if (!stbi__mad4sizes_valid(width, height, req_comp, sizeof(float), 0)) + return stbi__errpf("too large", "HDR image is too large"); + + // Read data + hdr_data = + (float*)stbi__malloc_mad4(width, height, req_comp, sizeof(float), 0); + if (!hdr_data) + return stbi__errpf("outofmem", "Out of memory"); + + // Load image data + // image data is stored as some number of sca + if (width < 8 || width >= 32768) { + // Read flat data + for (j = 0; j < height; ++j) { + for (i = 0; i < width; ++i) { + stbi_uc rgbe[4]; + main_decode_loop: + stbi__getn(s, rgbe, 4); + stbi__hdr_convert(hdr_data + j * width * req_comp + + i * req_comp, + rgbe, req_comp); + } + } + } + else { + // Read RLE-encoded data + scanline = NULL; + + for (j = 0; j < height; ++j) { + c1 = stbi__get8(s); + c2 = stbi__get8(s); + len = stbi__get8(s); + if (c1 != 2 || c2 != 2 || (len & 0x80)) { + // not run-length encoded, so we have to actually use THIS data + // as a decoded pixel (note this can't be a valid pixel--one of + // RGB must be >= 128) + stbi_uc rgbe[4]; + rgbe[0] = (stbi_uc)c1; + rgbe[1] = (stbi_uc)c2; + rgbe[2] = (stbi_uc)len; + rgbe[3] = (stbi_uc)stbi__get8(s); + stbi__hdr_convert(hdr_data, rgbe, req_comp); + i = 1; + j = 0; + STBI_FREE(scanline); + goto main_decode_loop; // yes, this makes no sense + } + len <<= 8; + len |= stbi__get8(s); + if (len != width) { + STBI_FREE(hdr_data); + STBI_FREE(scanline); + return stbi__errpf("invalid decoded scanline length", + "corrupt HDR"); + } + if (scanline == NULL) { + scanline = (stbi_uc*)stbi__malloc_mad2(width, 4, 0); + if (!scanline) { + STBI_FREE(hdr_data); + return stbi__errpf("outofmem", "Out of memory"); + } + } + + for (k = 0; k < 4; ++k) { + int nleft; + i = 0; + while ((nleft = width - i) > 0) { + count = stbi__get8(s); + if (count > 128) { + // Run + value = stbi__get8(s); + count -= 128; + if (count > nleft) { + STBI_FREE(hdr_data); + STBI_FREE(scanline); + return stbi__errpf("corrupt", + "bad RLE data in HDR"); + } + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = value; + } + else { + // Dump + if (count > nleft) { + STBI_FREE(hdr_data); + STBI_FREE(scanline); + return stbi__errpf("corrupt", + "bad RLE data in HDR"); + } + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = stbi__get8(s); + } + } + } + for (i = 0; i < width; ++i) + stbi__hdr_convert(hdr_data + (j * width + i) * req_comp, + scanline + i * 4, req_comp); + } + if (scanline) + STBI_FREE(scanline); + } + + return hdr_data; +} + +static int stbi__hdr_info(stbi__context* s, int* x, int* y, int* comp) +{ + char buffer[STBI__HDR_BUFLEN]; + char* token; + int valid = 0; + int dummy; + + if (!x) + x = &dummy; + if (!y) + y = &dummy; + if (!comp) + comp = &dummy; + + if (stbi__hdr_test(s) == 0) { + stbi__rewind(s); + return 0; + } + + for (;;) { + token = stbi__hdr_gettoken(s, buffer); + if (token[0] == 0) + break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) + valid = 1; + } + + if (!valid) { + stbi__rewind(s); + return 0; + } + token = stbi__hdr_gettoken(s, buffer); + if (strncmp(token, "-Y ", 3)) { + stbi__rewind(s); + return 0; + } + token += 3; + *y = (int)strtol(token, &token, 10); + while (*token == ' ') + ++token; + if (strncmp(token, "+X ", 3)) { + stbi__rewind(s); + return 0; + } + token += 3; + *x = (int)strtol(token, NULL, 10); + *comp = 3; + return 1; +} +#endif // STBI_NO_HDR + +#ifndef STBI_NO_BMP +static int stbi__bmp_info(stbi__context* s, int* x, int* y, int* comp) +{ + void* p; + stbi__bmp_data info; + + info.all_a = 255; + p = stbi__bmp_parse_header(s, &info); + stbi__rewind(s); + if (p == NULL) + return 0; + if (x) + *x = s->img_x; + if (y) + *y = s->img_y; + if (comp) + *comp = info.ma ? 4 : 3; + return 1; +} +#endif + +#ifndef STBI_NO_PSD +static int stbi__psd_info(stbi__context* s, int* x, int* y, int* comp) +{ + int channelCount, dummy, depth; + if (!x) + x = &dummy; + if (!y) + y = &dummy; + if (!comp) + comp = &dummy; + if (stbi__get32be(s) != 0x38425053) { + stbi__rewind(s); + return 0; + } + if (stbi__get16be(s) != 1) { + stbi__rewind(s); + return 0; + } + stbi__skip(s, 6); + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) { + stbi__rewind(s); + return 0; + } + *y = stbi__get32be(s); + *x = stbi__get32be(s); + depth = stbi__get16be(s); + if (depth != 8 && depth != 16) { + stbi__rewind(s); + return 0; + } + if (stbi__get16be(s) != 3) { + stbi__rewind(s); + return 0; + } + *comp = 4; + return 1; +} + +static int stbi__psd_is16(stbi__context* s) +{ + int channelCount, depth; + if (stbi__get32be(s) != 0x38425053) { + stbi__rewind(s); + return 0; + } + if (stbi__get16be(s) != 1) { + stbi__rewind(s); + return 0; + } + stbi__skip(s, 6); + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) { + stbi__rewind(s); + return 0; + } + (void)stbi__get32be(s); + (void)stbi__get32be(s); + depth = stbi__get16be(s); + if (depth != 16) { + stbi__rewind(s); + return 0; + } + return 1; +} +#endif + +#ifndef STBI_NO_PIC +static int stbi__pic_info(stbi__context* s, int* x, int* y, int* comp) +{ + int act_comp = 0, num_packets = 0, chained, dummy; + stbi__pic_packet packets[10]; + + if (!x) + x = &dummy; + if (!y) + y = &dummy; + if (!comp) + comp = &dummy; + + if (!stbi__pic_is4(s, "\x53\x80\xF6\x34")) { + stbi__rewind(s); + return 0; + } + + stbi__skip(s, 88); + + *x = stbi__get16be(s); + *y = stbi__get16be(s); + if (stbi__at_eof(s)) { + stbi__rewind(s); + return 0; + } + if ((*x) != 0 && (1 << 28) / (*x) < (*y)) { + stbi__rewind(s); + return 0; + } + + stbi__skip(s, 8); + + do { + stbi__pic_packet* packet; + + if (num_packets == sizeof(packets) / sizeof(packets[0])) + return 0; + + packet = &packets[num_packets++]; + chained = stbi__get8(s); + packet->size = stbi__get8(s); + packet->type = stbi__get8(s); + packet->channel = stbi__get8(s); + act_comp |= packet->channel; + + if (stbi__at_eof(s)) { + stbi__rewind(s); + return 0; + } + if (packet->size != 8) { + stbi__rewind(s); + return 0; + } + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); + + return 1; +} +#endif + +// ************************************************************************************************* +// Portable Gray Map and Portable Pixel Map loader +// by Ken Miller +// +// PGM: http://netpbm.sourceforge.net/doc/pgm.html +// PPM: http://netpbm.sourceforge.net/doc/ppm.html +// +// Known limitations: +// Does not support comments in the header section +// Does not support ASCII image data (formats P2 and P3) +// Does not support 16-bit-per-channel + +#ifndef STBI_NO_PNM + +static int stbi__pnm_test(stbi__context* s) +{ + char p, t; + p = (char)stbi__get8(s); + t = (char)stbi__get8(s); + if (p != 'P' || (t != '5' && t != '6')) { + stbi__rewind(s); + return 0; + } + return 1; +} + +static void* stbi__pnm_load(stbi__context* s, int* x, int* y, int* comp, + int req_comp, stbi__result_info* ri) +{ + stbi_uc* out; + STBI_NOTUSED(ri); + + if (!stbi__pnm_info(s, (int*)&s->img_x, (int*)&s->img_y, (int*)&s->img_n)) + return 0; + + *x = s->img_x; + *y = s->img_y; + if (comp) + *comp = s->img_n; + + if (!stbi__mad3sizes_valid(s->img_n, s->img_x, s->img_y, 0)) + return stbi__errpuc("too large", "PNM too large"); + + out = (stbi_uc*)stbi__malloc_mad3(s->img_n, s->img_x, s->img_y, 0); + if (!out) + return stbi__errpuc("outofmem", "Out of memory"); + stbi__getn(s, out, s->img_n * s->img_x * s->img_y); + + if (req_comp && req_comp != s->img_n) { + out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y); + if (out == NULL) + return out; // stbi__convert_format frees input on failure + } + return out; +} + +static int stbi__pnm_isspace(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || + c == '\r'; +} + +static void stbi__pnm_skip_whitespace(stbi__context* s, char* c) +{ + for (;;) { + while (!stbi__at_eof(s) && stbi__pnm_isspace(*c)) + *c = (char)stbi__get8(s); + + if (stbi__at_eof(s) || *c != '#') + break; + + while (!stbi__at_eof(s) && *c != '\n' && *c != '\r') + *c = (char)stbi__get8(s); + } +} + +static int stbi__pnm_isdigit(char c) { return c >= '0' && c <= '9'; } + +static int stbi__pnm_getinteger(stbi__context* s, char* c) +{ + int value = 0; + + while (!stbi__at_eof(s) && stbi__pnm_isdigit(*c)) { + value = value * 10 + (*c - '0'); + *c = (char)stbi__get8(s); + } + + return value; +} + +static int stbi__pnm_info(stbi__context* s, int* x, int* y, int* comp) +{ + int maxv, dummy; + char c, p, t; + + if (!x) + x = &dummy; + if (!y) + y = &dummy; + if (!comp) + comp = &dummy; + + stbi__rewind(s); + + // Get identifier + p = (char)stbi__get8(s); + t = (char)stbi__get8(s); + if (p != 'P' || (t != '5' && t != '6')) { + stbi__rewind(s); + return 0; + } + + *comp = + (t == '6') ? 3 : 1; // '5' is 1-component .pgm; '6' is 3-component .ppm + + c = (char)stbi__get8(s); + stbi__pnm_skip_whitespace(s, &c); + + *x = stbi__pnm_getinteger(s, &c); // read width + stbi__pnm_skip_whitespace(s, &c); + + *y = stbi__pnm_getinteger(s, &c); // read height + stbi__pnm_skip_whitespace(s, &c); + + maxv = stbi__pnm_getinteger(s, &c); // read max value + + if (maxv > 255) + return stbi__err("max value > 255", "PPM image not 8-bit"); + else + return 1; +} +#endif + +static int stbi__info_main(stbi__context* s, int* x, int* y, int* comp) +{ +#ifndef STBI_NO_JPEG + if (stbi__jpeg_info(s, x, y, comp)) + return 1; +#endif + +#ifndef STBI_NO_PNG + if (stbi__png_info(s, x, y, comp)) + return 1; +#endif + +#ifndef STBI_NO_GIF + if (stbi__gif_info(s, x, y, comp)) + return 1; +#endif + +#ifndef STBI_NO_BMP + if (stbi__bmp_info(s, x, y, comp)) + return 1; +#endif + +#ifndef STBI_NO_PSD + if (stbi__psd_info(s, x, y, comp)) + return 1; +#endif + +#ifndef STBI_NO_PIC + if (stbi__pic_info(s, x, y, comp)) + return 1; +#endif + +#ifndef STBI_NO_PNM + if (stbi__pnm_info(s, x, y, comp)) + return 1; +#endif + +#ifndef STBI_NO_HDR + if (stbi__hdr_info(s, x, y, comp)) + return 1; +#endif + +// test tga last because it's a crappy test! +#ifndef STBI_NO_TGA + if (stbi__tga_info(s, x, y, comp)) + return 1; +#endif + return stbi__err("unknown image type", + "Image not of any known type, or corrupt"); +} + +static int stbi__is_16_main(stbi__context* s) +{ +#ifndef STBI_NO_PNG + if (stbi__png_is16(s)) + return 1; +#endif + +#ifndef STBI_NO_PSD + if (stbi__psd_is16(s)) + return 1; +#endif + + return 0; +} + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_info(char const* filename, int* x, int* y, int* comp) +{ + FILE* f = stbi__fopen(filename, "rb"); + int result; + if (!f) + return stbi__err("can't fopen", "Unable to open file"); + result = stbi_info_from_file(f, x, y, comp); + fclose(f); + return result; +} + +STBIDEF int stbi_info_from_file(FILE* f, int* x, int* y, int* comp) +{ + int r; + stbi__context s; + long pos = ftell(f); + stbi__start_file(&s, f); + r = stbi__info_main(&s, x, y, comp); + fseek(f, pos, SEEK_SET); + return r; +} + +STBIDEF int stbi_is_16_bit(char const* filename) +{ + FILE* f = stbi__fopen(filename, "rb"); + int result; + if (!f) + return stbi__err("can't fopen", "Unable to open file"); + result = stbi_is_16_bit_from_file(f); + fclose(f); + return result; +} + +STBIDEF int stbi_is_16_bit_from_file(FILE* f) +{ + int r; + stbi__context s; + long pos = ftell(f); + stbi__start_file(&s, f); + r = stbi__is_16_main(&s); + fseek(f, pos, SEEK_SET); + return r; +} +#endif // !STBI_NO_STDIO + +STBIDEF int stbi_info_from_memory(stbi_uc const* buffer, int len, int* x, + int* y, int* comp) +{ + stbi__context s; + stbi__start_mem(&s, buffer, len); + return stbi__info_main(&s, x, y, comp); +} + +STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const* c, void* user, + int* x, int* y, int* comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks*)c, user); + return stbi__info_main(&s, x, y, comp); +} + +STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const* buffer, int len) +{ + stbi__context s; + stbi__start_mem(&s, buffer, len); + return stbi__is_16_main(&s); +} + +STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const* c, + void* user) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks*)c, user); + return stbi__is_16_main(&s); +} + +#endif // STB_IMAGE_IMPLEMENTATION + +/* + revision history: + 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and + platform ifdefs 2.19 (2018-02-11) fix warning 2.18 (2018-01-30) fix + warnings 2.17 (2018-01-29) change sbti__shiftsigned to avoid clang -O2 bug + 1-bit BMP + *_is_16_bit api + avoid warnings + 2.16 (2017-07-23) all functions have 16-bit variants; + STBI_NO_STDIO works again; + compilation fixes; + fix rounding in unpremultiply; + optimize vertical flip; + disable raw_len validation; + documentation fixes + 2.15 (2017-03-18) fix png-1,2,4 bug; now all Imagenet JPGs decode; + warning fixes; disable run-time SSE detection on gcc; + uniform handling of optional "return" values; + thread-safe initialization of zlib tables + 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet + JPGs 2.13 (2016-11-29) add 16-bit API, only supported for PNG right now 2.12 + (2016-04-02) fix typo in 2.11 PSD fix that caused crashes 2.11 (2016-04-02) + allocate large structures on the stack remove white matting for transparent + PSD fix reported channel count for PNG & BMP re-enable SSE2 in non-gcc 64-bit + support RGB-formatted JPEG + read 16-bit PNGs (only as 8-bit) + 2.10 (2016-01-22) avoid warning introduced in 2.09 by STBI_REALLOC_SIZED + 2.09 (2016-01-16) allow comments in PNM files + 16-bit-per-pixel TGA (not bit-per-component) + info() for TGA could break due to .hdr handling + info() for BMP to shares code instead of sloppy parse + can use STBI_REALLOC_SIZED if allocator doesn't support + realloc code cleanup 2.08 (2015-09-13) fix to 2.07 cleanup, reading RGB PSD + as RGBA 2.07 (2015-09-13) fix compiler warnings partial animated GIF support + limited 16-bpc PSD support + #ifdef unused functions + bug with < 92 byte PIC,PNM,HDR,TGA + 2.06 (2015-04-19) fix bug where PSD returns wrong '*comp' value + 2.05 (2015-04-19) fix bug in progressive JPEG handling, fix warning + 2.04 (2015-04-15) try to re-enable SIMD on MinGW 64-bit + 2.03 (2015-04-12) extra corruption checking (mmozeiko) + stbi_set_flip_vertically_on_load (nguillemot) + fix NEON support; fix mingw support + 2.02 (2015-01-19) fix incorrect assert, fix warning + 2.01 (2015-01-17) fix various warnings; suppress SIMD on gcc 32-bit + without -msse2 2.00b (2014-12-25) fix STBI_MALLOC in progressive JPEG 2.00 + (2014-12-25) optimize JPG, including x86 SSE2 & NEON SIMD (ryg) progressive + JPEG (stb) PGM/PPM support (Ken Miller) STBI_MALLOC,STBI_REALLOC,STBI_FREE + GIF bugfix -- seemingly never worked + STBI_NO_*, STBI_ONLY_* + 1.48 (2014-12-14) fix incorrectly-named assert() + 1.47 (2014-12-14) 1/2/4-bit PNG support, both direct and paletted (Omar + Cornut & stb) optimize PNG (ryg) fix bug in interlaced PNG with + user-specified channel count (stb) 1.46 (2014-08-26) fix broken tRNS chunk + (colorkey-style transparency) in non-paletted PNG 1.45 (2014-08-16) fix + MSVC-ARM internal compiler error by wrapping malloc 1.44 (2014-08-07) + various warning fixes from Ronny Chevalier + 1.43 (2014-07-15) + fix MSVC-only compiler problem in code changed in 1.42 + 1.42 (2014-07-09) + don't define _CRT_SECURE_NO_WARNINGS (affects user code) + fixes to stbi__cleanup_jpeg path + added STBI_ASSERT to avoid requiring assert.h + 1.41 (2014-06-25) + fix search&replace from 1.36 that messed up comments/error + messages 1.40 (2014-06-22) fix gcc struct-initialization warning 1.39 + (2014-06-15) fix to TGA optimization when req_comp != number of components in + TGA; fix to GIF loading because BMP wasn't rewinding (whoops, no GIFs in my + test suite) add support for BMP version 5 (more ignored fields) 1.38 + (2014-06-06) suppress MSVC warnings on integer casts truncating values fix + accidental rename of 'skip' field of I/O 1.37 (2014-06-04) remove duplicate + typedef 1.36 (2014-06-03) convert to header file single-file library if + de-iphone isn't set, load iphone images color-swapped instead of returning + NULL 1.35 (2014-05-27) various warnings fix broken STBI_SIMD path fix bug + where stbi_load_from_file no longer left file pointer in correct place fix + broken non-easy path for 32-bit BMP (possibly never used) TGA optimization by + Arseny Kapoulkine 1.34 (unknown) use STBI_NOTUSED in + stbi__resample_row_generic(), fix one more leak in tga failure case 1.33 + (2011-07-14) make stbi_is_hdr work in STBI_NO_HDR (as specified), minor + compiler-friendly improvements 1.32 (2011-07-13) support for "info" function + for all supported filetypes (SpartanJ) 1.31 (2011-06-20) a few more leak + fixes, bug in PNG handling (SpartanJ) 1.30 (2011-06-11) added ability to + load files via callbacks to accomidate custom input streams (Ben Wenger) + removed deprecated format-specific test/load functions + removed support for installable file formats (stbi_loader) -- + would have been broken for IO callbacks anyway error cases in bmp and tga + give messages and don't leak (Raymond Barbiero, grisha) fix inefficiency in + decoding 32-bit BMP (David Woo) 1.29 (2010-08-16) various warning fixes from + Aurelien Pocheville 1.28 (2010-08-01) fix bug in GIF palette transparency + (SpartanJ) 1.27 (2010-08-01) cast-to-stbi_uc to fix warnings 1.26 + (2010-07-24) fix bug in file buffering for PNG reported by SpartanJ 1.25 + (2010-07-17) refix trans_data warning (Won Chun) 1.24 (2010-07-12) perf + improvements reading from files on platforms with lock-heavy fgetc() minor + perf improvements for jpeg deprecated type-specific functions so we'll get + feedback if they're needed attempt to fix trans_data warning (Won Chun) 1.23 + fixed bug in iPhone support 1.22 (2010-07-10) removed image *writing* + support stbi_info support from Jetro Lauha GIF support from Jean-Marc Lienher + iPhone PNG-extensions from James Brown + warning-fixes from Nicolas Schulz and Janez Zemva (i.stbi__err. + Janez (U+017D)emva) 1.21 fix use of 'stbi_uc' in header (reported by jon + blow) 1.20 added support for Softimage PIC, by Tom Seddon 1.19 bug in + interlaced PNG corruption check (found by ryg) 1.18 (2008-08-02) fix a + threading bug (local mutable static) 1.17 support interlaced PNG 1.16 + major bugfix - stbi__convert_format converted one too many pixels 1.15 + initialize some fields for thread safety 1.14 fix threadsafe conversion + bug header-file-only version (#define STBI_HEADER_FILE_ONLY before including) + 1.13 threadsafe + 1.12 const qualifiers in the API + 1.11 Support installable IDCT, colorspace conversion routines + 1.10 Fixes for 64-bit (don't use "unsigned long") + optimized upsampling by Fabian "ryg" Giesen + 1.09 Fix format-conversion for PSD code (bad global variables!) + 1.08 Thatcher Ulrich's PSD code integrated by Nicolas Schulz + 1.07 attempt to fix C++ warning/errors again + 1.06 attempt to fix C++ warning/errors again + 1.05 fix TGA loading to return correct *comp and use good luminance + calc 1.04 default float alpha is 1, not 255; use 'void *' for + stbi_image_free 1.03 bugfixes to STBI_NO_STDIO, STBI_NO_HDR 1.02 support + for (subset of) HDR files, float interface for preferred access to them 1.01 + fix bug: possible bug in handling right-side up bmps... not sure fix bug: the + stbi__bmp_load() and stbi__tga_load() functions didn't work at all 1.00 + interface to zlib that skips zlib header 0.99 correct handling of alpha in + palette 0.98 TGA loader by lonesock; dynamically add loaders (untested) + 0.97 jpeg errors on too large a file; also catch another malloc failure + 0.96 fix detection of invalid v value - particleman@mollyrocket forum + 0.95 during header scan, seek to markers in case of padding + 0.94 STBI_NO_STDIO to disable stdio usage; rename all #defines the same + 0.93 handle jpegtran output; verbose errors + 0.92 read 4,8,16,24,32-bit BMP files of several formats + 0.91 output 24-bit Windows 3.0 BMP files + 0.90 fix a few more warnings; bump version number to approach 1.0 + 0.61 bugfixes due to Marc LeBlanc, Christopher Lloyd + 0.60 fix compiling as c++ + 0.59 fix warnings: merge Dave Moore's -Wall fixes + 0.58 fix bug: zlib uncompressed mode len/nlen was wrong endian + 0.57 fix bug: jpg last huffman symbol before marker was >9 bits but + less than 16 available 0.56 fix bug: zlib uncompressed mode len vs. nlen + 0.55 fix bug: restart_interval not initialized to 0 + 0.54 allow NULL for 'int *comp' + 0.53 fix bug in png 3->4; speedup png decoding + 0.52 png handles req_comp=3,4 directly; minor cleanup; jpeg comments + 0.51 obey req_comp requests, 1-component jpegs return as 1-component, + on 'test' only check type, not whether we support this variant + 0.50 (2006-11-19) + first released version +*/ + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ \ No newline at end of file diff --git a/src/viewer3d/gltf/tiny_gltf.h b/src/viewer3d/gltf/tiny_gltf.h new file mode 100644 index 00000000..8730bb49 --- /dev/null +++ b/src/viewer3d/gltf/tiny_gltf.h @@ -0,0 +1,8387 @@ +// +// Header-only tiny glTF 2.0 loader and serializer. +// +// +// The MIT License (MIT) +// +// Copyright (c) 2015 - Present Syoyo Fujita, Aurélien Chatelain and many +// contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Version: +// - v2.6.3 Fix GLB file with empty BIN chunk was not handled. PR#382 and +// PR#383. +// - v2.6.2 Fix out-of-bounds access of accessors. PR#379. +// - v2.6.1 Better GLB validation check when loading. +// - v2.6.0 Support serializing sparse accessor(Thanks to @fynv). +// Disable expanding file path for security(no use of awkward +// `wordexp` anymore). +// - v2.5.0 Add SetPreserveImageChannels() option to load image data as is. +// - v2.4.3 Fix null object output when material has all default +// parameters. +// - v2.4.2 Decode percent-encoded URI. +// - v2.4.1 Fix some glTF object class does not have `extensions` and/or +// `extras` property. +// - v2.4.0 Experimental RapidJSON and C++14 support(Thanks to @jrkoone). +// - v2.3.1 Set default value of minFilter and magFilter in Sampler to -1. +// - v2.3.0 Modified Material representation according to glTF 2.0 schema +// (and introduced TextureInfo class) +// Change the behavior of `Value::IsNumber`. It return true either the +// value is int or real. +// - v2.2.0 Add loading 16bit PNG support. Add Sparse accessor support(Thanks +// to @Ybalrid) +// - v2.1.0 Add draco compression. +// - v2.0.1 Add comparison feature(Thanks to @Selmar). +// - v2.0.0 glTF 2.0!. +// +// Tiny glTF loader is using following third party libraries: +// +// - jsonhpp: C++ JSON library. +// - base64: base64 decode/encode library. +// - stb_image: Image loading library. +// +#ifndef TINY_GLTF_H_ +#define TINY_GLTF_H_ + +#include +#include +#include // std::fabs +#include +#include +#include +#include +#include +#include +#include + +// Auto-detect C++14 standard version +#if !defined(TINYGLTF_USE_CPP14) && defined(__cplusplus) && \ + (__cplusplus >= 201402L) +#define TINYGLTF_USE_CPP14 +#endif + +#ifndef TINYGLTF_USE_CPP14 +#include +#endif + +#ifdef __ANDROID__ +#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS +#include +#endif +#endif + +#ifdef __GNUC__ +#if (__GNUC__ < 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ <= 8)) +#define TINYGLTF_NOEXCEPT +#else +#define TINYGLTF_NOEXCEPT noexcept +#endif +#else +#define TINYGLTF_NOEXCEPT noexcept +#endif + +#define DEFAULT_METHODS(x) \ + ~x() = default; \ + x(const x&) = default; \ + x(x&&) TINYGLTF_NOEXCEPT = default; \ + x& operator=(const x&) = default; \ + x& operator=(x&&) TINYGLTF_NOEXCEPT = default; + +namespace tinygltf { + +#define TINYGLTF_MODE_POINTS (0) +#define TINYGLTF_MODE_LINE (1) +#define TINYGLTF_MODE_LINE_LOOP (2) +#define TINYGLTF_MODE_LINE_STRIP (3) +#define TINYGLTF_MODE_TRIANGLES (4) +#define TINYGLTF_MODE_TRIANGLE_STRIP (5) +#define TINYGLTF_MODE_TRIANGLE_FAN (6) + +#define TINYGLTF_COMPONENT_TYPE_BYTE (5120) +#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE (5121) +#define TINYGLTF_COMPONENT_TYPE_SHORT (5122) +#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT (5123) +#define TINYGLTF_COMPONENT_TYPE_INT (5124) +#define TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT (5125) +#define TINYGLTF_COMPONENT_TYPE_FLOAT (5126) +#define TINYGLTF_COMPONENT_TYPE_DOUBLE \ + (5130) // OpenGL double type. Note that some of glTF 2.0 validator does not + // support double type even the schema seems allow any value of + // integer: + // https://github.com/KhronosGroup/glTF/blob/b9884a2fd45130b4d673dd6c8a706ee21ee5c5f7/specification/2.0/schema/accessor.schema.json#L22 + +#define TINYGLTF_TEXTURE_FILTER_NEAREST (9728) +#define TINYGLTF_TEXTURE_FILTER_LINEAR (9729) +#define TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST (9984) +#define TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST (9985) +#define TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR (9986) +#define TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR (9987) + +#define TINYGLTF_TEXTURE_WRAP_REPEAT (10497) +#define TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE (33071) +#define TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT (33648) + +// Redeclarations of the above for technique.parameters. +#define TINYGLTF_PARAMETER_TYPE_BYTE (5120) +#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_BYTE (5121) +#define TINYGLTF_PARAMETER_TYPE_SHORT (5122) +#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_SHORT (5123) +#define TINYGLTF_PARAMETER_TYPE_INT (5124) +#define TINYGLTF_PARAMETER_TYPE_UNSIGNED_INT (5125) +#define TINYGLTF_PARAMETER_TYPE_FLOAT (5126) + +#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC2 (35664) +#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC3 (35665) +#define TINYGLTF_PARAMETER_TYPE_FLOAT_VEC4 (35666) + +#define TINYGLTF_PARAMETER_TYPE_INT_VEC2 (35667) +#define TINYGLTF_PARAMETER_TYPE_INT_VEC3 (35668) +#define TINYGLTF_PARAMETER_TYPE_INT_VEC4 (35669) + +#define TINYGLTF_PARAMETER_TYPE_BOOL (35670) +#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC2 (35671) +#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC3 (35672) +#define TINYGLTF_PARAMETER_TYPE_BOOL_VEC4 (35673) + +#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT2 (35674) +#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT3 (35675) +#define TINYGLTF_PARAMETER_TYPE_FLOAT_MAT4 (35676) + +#define TINYGLTF_PARAMETER_TYPE_SAMPLER_2D (35678) + +// End parameter types + +#define TINYGLTF_TYPE_VEC2 (2) +#define TINYGLTF_TYPE_VEC3 (3) +#define TINYGLTF_TYPE_VEC4 (4) +#define TINYGLTF_TYPE_MAT2 (32 + 2) +#define TINYGLTF_TYPE_MAT3 (32 + 3) +#define TINYGLTF_TYPE_MAT4 (32 + 4) +#define TINYGLTF_TYPE_SCALAR (64 + 1) +#define TINYGLTF_TYPE_VECTOR (64 + 4) +#define TINYGLTF_TYPE_MATRIX (64 + 16) + +#define TINYGLTF_IMAGE_FORMAT_JPEG (0) +#define TINYGLTF_IMAGE_FORMAT_PNG (1) +#define TINYGLTF_IMAGE_FORMAT_BMP (2) +#define TINYGLTF_IMAGE_FORMAT_GIF (3) + +#define TINYGLTF_TEXTURE_FORMAT_ALPHA (6406) +#define TINYGLTF_TEXTURE_FORMAT_RGB (6407) +#define TINYGLTF_TEXTURE_FORMAT_RGBA (6408) +#define TINYGLTF_TEXTURE_FORMAT_LUMINANCE (6409) +#define TINYGLTF_TEXTURE_FORMAT_LUMINANCE_ALPHA (6410) + +#define TINYGLTF_TEXTURE_TARGET_TEXTURE2D (3553) +#define TINYGLTF_TEXTURE_TYPE_UNSIGNED_BYTE (5121) + +#define TINYGLTF_TARGET_ARRAY_BUFFER (34962) +#define TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER (34963) + +#define TINYGLTF_SHADER_TYPE_VERTEX_SHADER (35633) +#define TINYGLTF_SHADER_TYPE_FRAGMENT_SHADER (35632) + +#define TINYGLTF_DOUBLE_EPS (1.e-12) +#define TINYGLTF_DOUBLE_EQUAL(a, b) (std::fabs((b) - (a)) < TINYGLTF_DOUBLE_EPS) + +#ifdef __ANDROID__ +#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS +#ifdef TINYGLTF_IMPLEMENTATION +AAssetManager* asset_manager = nullptr; +#else +extern AAssetManager* asset_manager; +#endif +#endif +#endif + +typedef enum { + NULL_TYPE, + REAL_TYPE, + INT_TYPE, + BOOL_TYPE, + STRING_TYPE, + ARRAY_TYPE, + BINARY_TYPE, + OBJECT_TYPE +} Type; + +static inline int32_t GetComponentSizeInBytes(uint32_t componentType) +{ + if (componentType == TINYGLTF_COMPONENT_TYPE_BYTE) { + return 1; + } + else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) { + return 1; + } + else if (componentType == TINYGLTF_COMPONENT_TYPE_SHORT) { + return 2; + } + else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) { + return 2; + } + else if (componentType == TINYGLTF_COMPONENT_TYPE_INT) { + return 4; + } + else if (componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) { + return 4; + } + else if (componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) { + return 4; + } + else if (componentType == TINYGLTF_COMPONENT_TYPE_DOUBLE) { + return 8; + } + else { + // Unknown component type + return -1; + } +} + +static inline int32_t GetNumComponentsInType(uint32_t ty) +{ + if (ty == TINYGLTF_TYPE_SCALAR) { + return 1; + } + else if (ty == TINYGLTF_TYPE_VEC2) { + return 2; + } + else if (ty == TINYGLTF_TYPE_VEC3) { + return 3; + } + else if (ty == TINYGLTF_TYPE_VEC4) { + return 4; + } + else if (ty == TINYGLTF_TYPE_MAT2) { + return 4; + } + else if (ty == TINYGLTF_TYPE_MAT3) { + return 9; + } + else if (ty == TINYGLTF_TYPE_MAT4) { + return 16; + } + else { + // Unknown component type + return -1; + } +} + +// TODO(syoyo): Move these functions to TinyGLTF class +bool IsDataURI(const std::string& in); +bool DecodeDataURI(std::vector* out, std::string& mime_type, + const std::string& in, size_t reqBytes, bool checkSize); + +#ifdef __clang__ +#pragma clang diagnostic push +// Suppress warning for : static Value null_value +#pragma clang diagnostic ignored "-Wexit-time-destructors" +#pragma clang diagnostic ignored "-Wpadded" +#endif + +// Simple class to represent JSON object +class Value { +public: + typedef std::vector Array; + typedef std::map Object; + + Value() + : type_(NULL_TYPE), int_value_(0), real_value_(0.0), + boolean_value_(false) + { + } + + explicit Value(bool b) : type_(BOOL_TYPE) { boolean_value_ = b; } + explicit Value(int i) : type_(INT_TYPE) + { + int_value_ = i; + real_value_ = i; + } + explicit Value(double n) : type_(REAL_TYPE) { real_value_ = n; } + explicit Value(const std::string& s) : type_(STRING_TYPE) + { + string_value_ = s; + } + explicit Value(std::string&& s) + : type_(STRING_TYPE), string_value_(std::move(s)) + { + } + explicit Value(const unsigned char* p, size_t n) : type_(BINARY_TYPE) + { + binary_value_.resize(n); + memcpy(binary_value_.data(), p, n); + } + explicit Value(std::vector&& v) noexcept + : type_(BINARY_TYPE), binary_value_(std::move(v)) + { + } + explicit Value(const Array& a) : type_(ARRAY_TYPE) { array_value_ = a; } + explicit Value(Array&& a) noexcept + : type_(ARRAY_TYPE), array_value_(std::move(a)) + { + } + + explicit Value(const Object& o) : type_(OBJECT_TYPE) { object_value_ = o; } + explicit Value(Object&& o) noexcept + : type_(OBJECT_TYPE), object_value_(std::move(o)) + { + } + + DEFAULT_METHODS(Value) + + char Type() const { return static_cast(type_); } + + bool IsBool() const { return (type_ == BOOL_TYPE); } + + bool IsInt() const { return (type_ == INT_TYPE); } + + bool IsNumber() const + { + return (type_ == REAL_TYPE) || (type_ == INT_TYPE); + } + + bool IsReal() const { return (type_ == REAL_TYPE); } + + bool IsString() const { return (type_ == STRING_TYPE); } + + bool IsBinary() const { return (type_ == BINARY_TYPE); } + + bool IsArray() const { return (type_ == ARRAY_TYPE); } + + bool IsObject() const { return (type_ == OBJECT_TYPE); } + + // Use this function if you want to have number value as double. + double GetNumberAsDouble() const + { + if (type_ == INT_TYPE) { + return double(int_value_); + } + else { + return real_value_; + } + } + + // Use this function if you want to have number value as int. + // TODO(syoyo): Support int value larger than 32 bits + int GetNumberAsInt() const + { + if (type_ == REAL_TYPE) { + return int(real_value_); + } + else { + return int_value_; + } + } + + // Accessor + template const T& Get() const; + template T& Get(); + + // Lookup value from an array + const Value& Get(int idx) const + { + static Value null_value; + assert(IsArray()); + assert(idx >= 0); + return (static_cast(idx) < array_value_.size()) + ? array_value_[static_cast(idx)] + : null_value; + } + + // Lookup value from a key-value pair + const Value& Get(const std::string& key) const + { + static Value null_value; + assert(IsObject()); + Object::const_iterator it = object_value_.find(key); + return (it != object_value_.end()) ? it->second : null_value; + } + + size_t ArrayLen() const + { + if (!IsArray()) + return 0; + return array_value_.size(); + } + + // Valid only for object type. + bool Has(const std::string& key) const + { + if (!IsObject()) + return false; + Object::const_iterator it = object_value_.find(key); + return (it != object_value_.end()) ? true : false; + } + + // List keys + std::vector Keys() const + { + std::vector keys; + if (!IsObject()) + return keys; // empty + + for (Object::const_iterator it = object_value_.begin(); + it != object_value_.end(); ++it) { + keys.push_back(it->first); + } + + return keys; + } + + size_t Size() const { return (IsArray() ? ArrayLen() : Keys().size()); } + + bool operator==(const tinygltf::Value& other) const; + +protected: + int type_ = NULL_TYPE; + + int int_value_ = 0; + double real_value_ = 0.0; + std::string string_value_; + std::vector binary_value_; + Array array_value_; + Object object_value_; + bool boolean_value_ = false; +}; + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#define TINYGLTF_VALUE_GET(ctype, var) \ + template <> inline const ctype& Value::Get() const \ + { \ + return var; \ + } \ + template <> inline ctype& Value::Get() \ + { \ + return var; \ + } +TINYGLTF_VALUE_GET(bool, boolean_value_) +TINYGLTF_VALUE_GET(double, real_value_) +TINYGLTF_VALUE_GET(int, int_value_) +TINYGLTF_VALUE_GET(std::string, string_value_) +TINYGLTF_VALUE_GET(std::vector, binary_value_) +TINYGLTF_VALUE_GET(Value::Array, array_value_) +TINYGLTF_VALUE_GET(Value::Object, object_value_) +#undef TINYGLTF_VALUE_GET + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat" +#pragma clang diagnostic ignored "-Wpadded" +#endif + +/// Aggregate object for representing a color +using ColorValue = std::array; + +// === legacy interface ==== +// TODO(syoyo): Deprecate `Parameter` class. +struct Parameter { + bool bool_value = false; + bool has_number_value = false; + std::string string_value; + std::vector number_array; + std::map json_double_value; + double number_value = 0.0; + + // context sensitive methods. depending the type of the Parameter you are + // accessing, these are either valid or not + // If this parameter represent a texture map in a material, will return the + // texture index + + /// Return the index of a texture if this Parameter is a texture map. + /// Returned value is only valid if the parameter represent a texture from a + /// material + int TextureIndex() const + { + const auto it = json_double_value.find("index"); + if (it != std::end(json_double_value)) { + return int(it->second); + } + return -1; + } + + /// Return the index of a texture coordinate set if this Parameter is a + /// texture map. Returned value is only valid if the parameter represent a + /// texture from a material + int TextureTexCoord() const + { + const auto it = json_double_value.find("texCoord"); + if (it != std::end(json_double_value)) { + return int(it->second); + } + // As per the spec, if texCoord is omitted, this parameter is 0 + return 0; + } + + /// Return the scale of a texture if this Parameter is a normal texture map. + /// Returned value is only valid if the parameter represent a normal texture + /// from a material + double TextureScale() const + { + const auto it = json_double_value.find("scale"); + if (it != std::end(json_double_value)) { + return it->second; + } + // As per the spec, if scale is omitted, this parameter is 1 + return 1; + } + + /// Return the strength of a texture if this Parameter is a an occlusion + /// map. Returned value is only valid if the parameter represent an + /// occlusion map from a material + double TextureStrength() const + { + const auto it = json_double_value.find("strength"); + if (it != std::end(json_double_value)) { + return it->second; + } + // As per the spec, if strength is omitted, this parameter is 1 + return 1; + } + + /// Material factor, like the roughness or metalness of a material + /// Returned value is only valid if the parameter represent a texture from a + /// material + double Factor() const { return number_value; } + + /// Return the color of a material + /// Returned value is only valid if the parameter represent a texture from a + /// material + ColorValue ColorFactor() const + { + return {{// this aggregate initialize the std::array object, and uses + // C++11 RVO. + number_array[0], number_array[1], number_array[2], + (number_array.size() > 3 ? number_array[3] : 1.0)}}; + } + + Parameter() = default; + DEFAULT_METHODS(Parameter) + bool operator==(const Parameter&) const; +}; + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" +#endif + +typedef std::map ParameterMap; +typedef std::map ExtensionMap; + +struct AnimationChannel { + int sampler; // required + int target_node; // required (index of the node to target) + std::string target_path; // required in ["translation", "rotation", "scale", + // "weights"] + Value extras; + ExtensionMap extensions; + ExtensionMap target_extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + std::string target_extensions_json_string; + + AnimationChannel() : sampler(-1), target_node(-1) {} + DEFAULT_METHODS(AnimationChannel) + bool operator==(const AnimationChannel&) const; +}; + +struct AnimationSampler { + int input; // required + int output; // required + std::string interpolation; // "LINEAR", "STEP","CUBICSPLINE" or user defined + // string. default "LINEAR" + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + AnimationSampler() : input(-1), output(-1), interpolation("LINEAR") {} + DEFAULT_METHODS(AnimationSampler) + bool operator==(const AnimationSampler&) const; +}; + +struct Animation { + std::string name; + std::vector channels; + std::vector samplers; + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Animation() = default; + DEFAULT_METHODS(Animation) + bool operator==(const Animation&) const; +}; + +struct Skin { + std::string name; + int inverseBindMatrices; // required here but not in the spec + int skeleton; // The index of the node used as a skeleton root + std::vector joints; // Indices of skeleton nodes + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Skin() + { + inverseBindMatrices = -1; + skeleton = -1; + } + DEFAULT_METHODS(Skin) + bool operator==(const Skin&) const; +}; + +struct Sampler { + std::string name; + // glTF 2.0 spec does not define default value for `minFilter` and + // `magFilter`. Set -1 in TinyGLTF(issue #186) + int minFilter = + -1; // optional. -1 = no filter defined. ["NEAREST", "LINEAR", + // "NEAREST_MIPMAP_NEAREST", "LINEAR_MIPMAP_NEAREST", + // "NEAREST_MIPMAP_LINEAR", "LINEAR_MIPMAP_LINEAR"] + int magFilter = + -1; // optional. -1 = no filter defined. ["NEAREST", "LINEAR"] + int wrapS = + TINYGLTF_TEXTURE_WRAP_REPEAT; // ["CLAMP_TO_EDGE", "MIRRORED_REPEAT", + // "REPEAT"], default "REPEAT" + int wrapT = + TINYGLTF_TEXTURE_WRAP_REPEAT; // ["CLAMP_TO_EDGE", "MIRRORED_REPEAT", + // "REPEAT"], default "REPEAT" + // int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT; // TinyGLTF extension. + // currently not used. + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Sampler() + : minFilter(-1), magFilter(-1), wrapS(TINYGLTF_TEXTURE_WRAP_REPEAT), + wrapT(TINYGLTF_TEXTURE_WRAP_REPEAT) + { + } + DEFAULT_METHODS(Sampler) + bool operator==(const Sampler&) const; +}; + +struct Image { + std::string name; + int width; + int height; + int component; + int bits; // bit depth per channel. 8(byte), 16 or 32. + int pixel_type; // pixel type(TINYGLTF_COMPONENT_TYPE_***). usually + // UBYTE(bits = 8) or USHORT(bits = 16) + std::vector image; + int bufferView; // (required if no uri) + std::string mimeType; // (required if no uri) ["image/jpeg", "image/png", + // "image/bmp", "image/gif"] + std::string uri; // (required if no mimeType) uri is not decoded(e.g. + // whitespace may be represented as %20) + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + // When this flag is true, data is stored to `image` in as-is format(e.g. + // jpeg compressed for "image/jpeg" mime) This feature is good if you use + // custom image loader function. (e.g. delayed decoding of images for faster + // glTF parsing) Default parser for Image does not provide as-is loading + // feature at the moment. (You can manipulate this by providing your own + // LoadImageData function) + bool as_is; + + Image() : as_is(false) + { + bufferView = -1; + width = -1; + height = -1; + component = -1; + bits = -1; + pixel_type = -1; + } + DEFAULT_METHODS(Image) + + bool operator==(const Image&) const; +}; + +struct Texture { + std::string name; + + int sampler; + int source; + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Texture() : sampler(-1), source(-1) {} + DEFAULT_METHODS(Texture) + + bool operator==(const Texture&) const; +}; + +struct TextureInfo { + int index = -1; // required. + int texCoord; // The set index of texture's TEXCOORD attribute used for + // texture coordinate mapping. + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + TextureInfo() : index(-1), texCoord(0) {} + DEFAULT_METHODS(TextureInfo) + bool operator==(const TextureInfo&) const; +}; + +struct NormalTextureInfo { + int index = -1; // required + int texCoord; // The set index of texture's TEXCOORD attribute used for + // texture coordinate mapping. + double scale; // scaledNormal = normalize(( + // * 2.0 - 1.0) * vec3(, , 1.0)) + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + NormalTextureInfo() : index(-1), texCoord(0), scale(1.0) {} + DEFAULT_METHODS(NormalTextureInfo) + bool operator==(const NormalTextureInfo&) const; +}; + +struct OcclusionTextureInfo { + int index = -1; // required + int texCoord; // The set index of texture's TEXCOORD attribute used for + // texture coordinate mapping. + double strength; // occludedColor = lerp(color, color * , ) + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + OcclusionTextureInfo() : index(-1), texCoord(0), strength(1.0) {} + DEFAULT_METHODS(OcclusionTextureInfo) + bool operator==(const OcclusionTextureInfo&) const; +}; + +// pbrMetallicRoughness class defined in glTF 2.0 spec. +struct PbrMetallicRoughness { + std::vector baseColorFactor; // len = 4. default [1,1,1,1] + TextureInfo baseColorTexture; + double metallicFactor; // default 1 + double roughnessFactor; // default 1 + TextureInfo metallicRoughnessTexture; + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + PbrMetallicRoughness() + : baseColorFactor(std::vector{1.0, 1.0, 1.0, 1.0}), + metallicFactor(1.0), roughnessFactor(1.0) + { + } + DEFAULT_METHODS(PbrMetallicRoughness) + bool operator==(const PbrMetallicRoughness&) const; +}; + +// Each extension should be stored in a ParameterMap. +// members not in the values could be included in the ParameterMap +// to keep a single material model +struct Material { + std::string name; + + std::vector emissiveFactor; // length 3. default [0, 0, 0] + std::string alphaMode; // default "OPAQUE" + double alphaCutoff; // default 0.5 + bool doubleSided; // default false; + + PbrMetallicRoughness pbrMetallicRoughness; + + NormalTextureInfo normalTexture; + OcclusionTextureInfo occlusionTexture; + TextureInfo emissiveTexture; + + // For backward compatibility + // TODO(syoyo): Remove `values` and `additionalValues` in the next release. + ParameterMap values; + ParameterMap additionalValues; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Material() : alphaMode("OPAQUE"), alphaCutoff(0.5), doubleSided(false) {} + DEFAULT_METHODS(Material) + + bool operator==(const Material&) const; +}; + +struct BufferView { + std::string name; + int buffer{-1}; // Required + size_t byteOffset{0}; // minimum 0, default 0 + size_t byteLength{0}; // required, minimum 1. 0 = invalid + size_t byteStride{0}; // minimum 4, maximum 252 (multiple of 4), default 0 = + // understood to be tightly packed + int target{0}; // ["ARRAY_BUFFER", "ELEMENT_ARRAY_BUFFER"] for vertex + // indices or attribs. Could be 0 for other data + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + bool dracoDecoded{false}; // Flag indicating this has been draco decoded + + BufferView() + : buffer(-1), byteOffset(0), byteLength(0), byteStride(0), target(0), + dracoDecoded(false) + { + } + DEFAULT_METHODS(BufferView) + bool operator==(const BufferView&) const; +}; + +struct Accessor { + int bufferView; // optional in spec but required here since sparse accessor + // are not supported + std::string name; + size_t byteOffset; + bool normalized; // optional. + int componentType; // (required) One of TINYGLTF_COMPONENT_TYPE_*** + size_t count; // required + int type; // (required) One of TINYGLTF_TYPE_*** .. + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + std::vector + minValues; // optional. integer value is promoted to double + std::vector + maxValues; // optional. integer value is promoted to double + + struct { + int count; + bool isSparse; + struct { + int byteOffset; + int bufferView; + int componentType; // a TINYGLTF_COMPONENT_TYPE_ value + } indices; + struct { + int bufferView; + int byteOffset; + } values; + } sparse; + + /// + /// Utility function to compute byteStride for a given bufferView object. + /// Returns -1 upon invalid glTF value or parameter configuration. + /// + int ByteStride(const BufferView& bufferViewObject) const + { + if (bufferViewObject.byteStride == 0) { + // Assume data is tightly packed. + int componentSizeInBytes = + GetComponentSizeInBytes(static_cast(componentType)); + if (componentSizeInBytes <= 0) { + return -1; + } + + int numComponents = + GetNumComponentsInType(static_cast(type)); + if (numComponents <= 0) { + return -1; + } + + return componentSizeInBytes * numComponents; + } + else { + // Check if byteStride is a multiple of the size of the accessor's + // component type. + int componentSizeInBytes = + GetComponentSizeInBytes(static_cast(componentType)); + if (componentSizeInBytes <= 0) { + return -1; + } + + if ((bufferViewObject.byteStride % + uint32_t(componentSizeInBytes)) != 0) { + return -1; + } + return static_cast(bufferViewObject.byteStride); + } + + // unreachable return 0; + } + + Accessor() + : bufferView(-1), byteOffset(0), normalized(false), componentType(-1), + count(0), type(-1) + { + sparse.isSparse = false; + } + DEFAULT_METHODS(Accessor) + bool operator==(const tinygltf::Accessor&) const; +}; + +struct PerspectiveCamera { + double aspectRatio; // min > 0 + double yfov; // required. min > 0 + double zfar; // min > 0 + double znear; // required. min > 0 + + PerspectiveCamera() + : aspectRatio(0.0), yfov(0.0), + zfar(0.0) // 0 = use infinite projection matrix + , + znear(0.0) + { + } + DEFAULT_METHODS(PerspectiveCamera) + bool operator==(const PerspectiveCamera&) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; +}; + +struct OrthographicCamera { + double xmag; // required. must not be zero. + double ymag; // required. must not be zero. + double zfar; // required. `zfar` must be greater than `znear`. + double znear; // required + + OrthographicCamera() : xmag(0.0), ymag(0.0), zfar(0.0), znear(0.0) {} + DEFAULT_METHODS(OrthographicCamera) + bool operator==(const OrthographicCamera&) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; +}; + +struct Camera { + std::string type; // required. "perspective" or "orthographic" + std::string name; + + PerspectiveCamera perspective; + OrthographicCamera orthographic; + + Camera() {} + DEFAULT_METHODS(Camera) + bool operator==(const Camera&) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; +}; + +struct Primitive { + std::map attributes; // (required) A dictionary object of + // integer, where each integer + // is the index of the accessor + // containing an attribute. + int material; // The index of the material to apply to this primitive + // when rendering. + int indices; // The index of the accessor that contains the indices. + int mode; // one of TINYGLTF_MODE_*** + std::vector> targets; // array of morph targets, + // where each target is a dict with attributes in ["POSITION, "NORMAL", + // "TANGENT"] pointing + // to their corresponding accessors + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Primitive() + { + material = -1; + indices = -1; + mode = -1; + } + DEFAULT_METHODS(Primitive) + bool operator==(const Primitive&) const; +}; + +struct Mesh { + std::string name; + std::vector primitives; + std::vector weights; // weights to be applied to the Morph Targets + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Mesh() = default; + DEFAULT_METHODS(Mesh) + bool operator==(const Mesh&) const; +}; + +class Node { +public: + Node() : camera(-1), skin(-1), mesh(-1) {} + + DEFAULT_METHODS(Node) + + bool operator==(const Node&) const; + + int camera; // the index of the camera referenced by this node + + std::string name; + int skin; + int mesh; + std::vector children; + std::vector rotation; // length must be 0 or 4 + std::vector scale; // length must be 0 or 3 + std::vector translation; // length must be 0 or 3 + std::vector matrix; // length must be 0 or 16 + std::vector weights; // The weights of the instantiated Morph Target + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; +}; + +struct Buffer { + std::string name; + std::vector data; + std::string uri; // considered as required here but not in the spec (need to + // clarify) uri is not decoded(e.g. whitespace may be + // represented as %20) + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Buffer() = default; + DEFAULT_METHODS(Buffer) + bool operator==(const Buffer&) const; +}; + +struct Asset { + std::string version = "2.0"; // required + std::string generator; + std::string minVersion; + std::string copyright; + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Asset() = default; + DEFAULT_METHODS(Asset) + bool operator==(const Asset&) const; +}; + +struct Scene { + std::string name; + std::vector nodes; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; + + Scene() = default; + DEFAULT_METHODS(Scene) + bool operator==(const Scene&) const; +}; + +struct SpotLight { + double innerConeAngle; + double outerConeAngle; + + SpotLight() : innerConeAngle(0.0), outerConeAngle(0.7853981634) {} + DEFAULT_METHODS(SpotLight) + bool operator==(const SpotLight&) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; +}; + +struct Light { + std::string name; + std::vector color; + double intensity{1.0}; + std::string type; + double range{0.0}; // 0.0 = infinite + SpotLight spot; + + Light() : intensity(1.0), range(0.0) {} + DEFAULT_METHODS(Light) + + bool operator==(const Light&) const; + + ExtensionMap extensions; + Value extras; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; +}; + +class Model { +public: + Model() = default; + DEFAULT_METHODS(Model) + + bool operator==(const Model&) const; + + std::vector accessors; + std::vector animations; + std::vector buffers; + std::vector bufferViews; + std::vector materials; + std::vector meshes; + std::vector nodes; + std::vector textures; + std::vector images; + std::vector skins; + std::vector samplers; + std::vector cameras; + std::vector scenes; + std::vector lights; + + int defaultScene = -1; + std::vector extensionsUsed; + std::vector extensionsRequired; + + Asset asset; + + Value extras; + ExtensionMap extensions; + + // Filled when SetStoreOriginalJSONForExtrasAndExtensions is enabled. + std::string extras_json_string; + std::string extensions_json_string; +}; + +enum SectionCheck { + NO_REQUIRE = 0x00, + REQUIRE_VERSION = 0x01, + REQUIRE_SCENE = 0x02, + REQUIRE_SCENES = 0x04, + REQUIRE_NODES = 0x08, + REQUIRE_ACCESSORS = 0x10, + REQUIRE_BUFFERS = 0x20, + REQUIRE_BUFFER_VIEWS = 0x40, + REQUIRE_ALL = 0x7f +}; + +/// +/// LoadImageDataFunction type. Signature for custom image loading callbacks. +/// +typedef bool (*LoadImageDataFunction)(Image*, const int, std::string*, + std::string*, int, int, + const unsigned char*, int, + void* user_pointer); + +/// +/// WriteImageDataFunction type. Signature for custom image writing callbacks. +/// +typedef bool (*WriteImageDataFunction)(const std::string*, const std::string*, + Image*, bool, void*); + +#ifndef TINYGLTF_NO_STB_IMAGE +// Declaration of default image loader callback +bool LoadImageData(Image* image, const int image_idx, std::string* err, + std::string* warn, int req_width, int req_height, + const unsigned char* bytes, int size, void*); +#endif + +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE +// Declaration of default image writer callback +bool WriteImageData(const std::string* basepath, const std::string* filename, + Image* image, bool embedImages, void*); +#endif + +/// +/// FilExistsFunction type. Signature for custom filesystem callbacks. +/// +typedef bool (*FileExistsFunction)(const std::string& abs_filename, void*); + +/// +/// ExpandFilePathFunction type. Signature for custom filesystem callbacks. +/// +typedef std::string (*ExpandFilePathFunction)(const std::string&, void*); + +/// +/// ReadWholeFileFunction type. Signature for custom filesystem callbacks. +/// +typedef bool (*ReadWholeFileFunction)(std::vector*, std::string*, + const std::string&, void*); + +/// +/// WriteWholeFileFunction type. Signature for custom filesystem callbacks. +/// +typedef bool (*WriteWholeFileFunction)(std::string*, const std::string&, + const std::vector&, + void*); + +/// +/// A structure containing all required filesystem callbacks and a pointer to +/// their user data. +/// +struct FsCallbacks { + FileExistsFunction FileExists; + ExpandFilePathFunction ExpandFilePath; + ReadWholeFileFunction ReadWholeFile; + WriteWholeFileFunction WriteWholeFile; + + void* user_data; // An argument that is passed to all fs callbacks +}; + +#ifndef TINYGLTF_NO_FS +// Declaration of default filesystem callbacks + +bool FileExists(const std::string& abs_filename, void*); + +/// +/// Expand file path(e.g. `~` to home directory on posix, `%APPDATA%` to +/// `C:\\Users\\tinygltf\\AppData`) +/// +/// @param[in] filepath File path string. Assume UTF-8 +/// @param[in] userdata User data. Set to `nullptr` if you don't need it. +/// +std::string ExpandFilePath(const std::string& filepath, void* userdata); + +bool ReadWholeFile(std::vector* out, std::string* err, + const std::string& filepath, void*); + +bool WriteWholeFile(std::string* err, const std::string& filepath, + const std::vector& contents, void*); +#endif + +/// +/// glTF Parser/Serializer context. +/// +class TinyGLTF { +public: +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat" +#endif + + TinyGLTF() : bin_data_(nullptr), bin_size_(0), is_binary_(false) {} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + + ~TinyGLTF() {} + + /// + /// Loads glTF ASCII asset from a file. + /// Set warning message to `warn` for example it fails to load asserts. + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadASCIIFromFile(Model* model, std::string* err, std::string* warn, + const std::string& filename, + unsigned int check_sections = REQUIRE_VERSION); + + /// + /// Loads glTF ASCII asset from string(memory). + /// `length` = strlen(str); + /// `base_dir` is a search path of glTF asset(e.g. images). Path Must be an + /// expanded path (e.g. no tilde(`~`), no environment variables). Set + /// warning message to `warn` for example it fails to load asserts. Returns + /// false and set error string to `err` if there's an error. + /// + bool LoadASCIIFromString(Model* model, std::string* err, std::string* warn, + const char* str, const unsigned int length, + const std::string& base_dir, + unsigned int check_sections = REQUIRE_VERSION); + + /// + /// Loads glTF binary asset from a file. + /// Set warning message to `warn` for example it fails to load asserts. + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadBinaryFromFile(Model* model, std::string* err, std::string* warn, + const std::string& filename, + unsigned int check_sections = REQUIRE_VERSION); + + /// + /// Loads glTF binary asset from memory. + /// `length` = strlen(str); + /// `base_dir` is a search path of glTF asset(e.g. images). Path Must be an + /// expanded path (e.g. no tilde(`~`), no environment variables). + /// Set warning message to `warn` for example it fails to load asserts. + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadBinaryFromMemory(Model* model, std::string* err, std::string* warn, + const unsigned char* bytes, + const unsigned int length, + const std::string& base_dir = "", + unsigned int check_sections = REQUIRE_VERSION); + + /// + /// Write glTF to stream, buffers and images will be embedded + /// + bool WriteGltfSceneToStream(Model* model, std::ostream& stream, + bool prettyPrint, bool writeBinary); + + /// + /// Write glTF to file. + /// + bool WriteGltfSceneToFile(Model* model, const std::string& filename, + bool embedImages, bool embedBuffers, + bool prettyPrint, bool writeBinary); + + /// + /// Set callback to use for loading image data + /// + void SetImageLoader(LoadImageDataFunction LoadImageData, void* user_data); + + /// + /// Unset(remove) callback of loading image data + /// + void RemoveImageLoader(); + + /// + /// Set callback to use for writing image data + /// + void SetImageWriter(WriteImageDataFunction WriteImageData, void* user_data); + + /// + /// Set callbacks to use for filesystem (fs) access and their user data + /// + void SetFsCallbacks(FsCallbacks callbacks); + + /// + /// Set serializing default values(default = false). + /// When true, default values are force serialized to .glTF. + /// This may be helpful if you want to serialize a full description of glTF + /// data. + /// + /// TODO(LTE): Supply parsing option as function arguments to + /// `LoadASCIIFromFile()` and others, not by a class method + /// + void SetSerializeDefaultValues(const bool enabled) + { + serialize_default_values_ = enabled; + } + + bool GetSerializeDefaultValues() const { return serialize_default_values_; } + + /// + /// Store original JSON string for `extras` and `extensions`. + /// This feature will be useful when the user want to reconstruct custom + /// data structure from JSON string. + /// + void SetStoreOriginalJSONForExtrasAndExtensions(const bool enabled) + { + store_original_json_for_extras_and_extensions_ = enabled; + } + + bool GetStoreOriginalJSONForExtrasAndExtensions() const + { + return store_original_json_for_extras_and_extensions_; + } + + /// + /// Specify whether preserve image channels when loading images or not. + /// (Not effective when the user supplies their own LoadImageData callbacks) + /// + void SetPreserveImageChannels(bool onoff) + { + preserve_image_channels_ = onoff; + } + + bool GetPreserveImageChannels() const { return preserve_image_channels_; } + +private: + /// + /// Loads glTF asset from string(memory). + /// `length` = strlen(str); + /// Set warning message to `warn` for example it fails to load asserts + /// Returns false and set error string to `err` if there's an error. + /// + bool LoadFromString(Model* model, std::string* err, std::string* warn, + const char* str, const unsigned int length, + const std::string& base_dir, + unsigned int check_sections); + + const unsigned char* bin_data_ = nullptr; + size_t bin_size_ = 0; + bool is_binary_ = false; + + bool serialize_default_values_ = false; ///< Serialize default values? + + bool store_original_json_for_extras_and_extensions_ = false; + + bool preserve_image_channels_ = false; /// Default false(expand channels to + /// RGBA) for backward compatibility. + + // Warning & error messages + std::string warn_; + std::string err_; + + FsCallbacks fs = { +#ifndef TINYGLTF_NO_FS + &tinygltf::FileExists, &tinygltf::ExpandFilePath, + &tinygltf::ReadWholeFile, &tinygltf::WriteWholeFile, + + nullptr // Fs callback user data +#else + nullptr, nullptr, nullptr, nullptr, + + nullptr // Fs callback user data +#endif + }; + + LoadImageDataFunction LoadImageData = +#ifndef TINYGLTF_NO_STB_IMAGE + &tinygltf::LoadImageData; +#else + nullptr; +#endif + void* load_image_user_data_{nullptr}; + bool user_image_loader_{false}; + + WriteImageDataFunction WriteImageData = +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE + &tinygltf::WriteImageData; +#else + nullptr; +#endif + void* write_image_user_data_{nullptr}; +}; + +#ifdef __clang__ +#pragma clang diagnostic pop // -Wpadded +#endif + +} // namespace tinygltf + +#endif // TINY_GLTF_H_ + +#if defined(TINYGLTF_IMPLEMENTATION) || defined(__INTELLISENSE__) +#include +// #include +#ifndef TINYGLTF_NO_FS +#include +#include +#endif +#include + +#ifdef __clang__ +// Disable some warnings for external files. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wfloat-equal" +#pragma clang diagnostic ignored "-Wexit-time-destructors" +#pragma clang diagnostic ignored "-Wconversion" +#pragma clang diagnostic ignored "-Wold-style-cast" +#pragma clang diagnostic ignored "-Wglobal-constructors" +#if __has_warning("-Wreserved-id-macro") +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif +#pragma clang diagnostic ignored "-Wdisabled-macro-expansion" +#pragma clang diagnostic ignored "-Wpadded" +#pragma clang diagnostic ignored "-Wc++98-compat" +#pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#pragma clang diagnostic ignored "-Wswitch-enum" +#pragma clang diagnostic ignored "-Wimplicit-fallthrough" +#pragma clang diagnostic ignored "-Wweak-vtables" +#pragma clang diagnostic ignored "-Wcovered-switch-default" +#if __has_warning("-Wdouble-promotion") +#pragma clang diagnostic ignored "-Wdouble-promotion" +#endif +#if __has_warning("-Wcomma") +#pragma clang diagnostic ignored "-Wcomma" +#endif +#if __has_warning("-Wzero-as-null-pointer-constant") +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#endif +#if __has_warning("-Wcast-qual") +#pragma clang diagnostic ignored "-Wcast-qual" +#endif +#if __has_warning("-Wmissing-variable-declarations") +#pragma clang diagnostic ignored "-Wmissing-variable-declarations" +#endif +#if __has_warning("-Wmissing-prototypes") +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#endif +#if __has_warning("-Wcast-align") +#pragma clang diagnostic ignored "-Wcast-align" +#endif +#if __has_warning("-Wnewline-eof") +#pragma clang diagnostic ignored "-Wnewline-eof" +#endif +#if __has_warning("-Wunused-parameter") +#pragma clang diagnostic ignored "-Wunused-parameter" +#endif +#if __has_warning("-Wmismatched-tags") +#pragma clang diagnostic ignored "-Wmismatched-tags" +#endif +#if __has_warning("-Wextra-semi-stmt") +#pragma clang diagnostic ignored "-Wextra-semi-stmt" +#endif +#endif + +// Disable GCC warnings +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wtype-limits" +#endif // __GNUC__ + +#ifndef TINYGLTF_NO_INCLUDE_JSON +#ifndef TINYGLTF_USE_RAPIDJSON +#include "json.hpp" +#else +#ifndef TINYGLTF_NO_INCLUDE_RAPIDJSON +#include "document.h" +#include "prettywriter.h" +#include "rapidjson.h" +#include "stringbuffer.h" +#include "writer.h" +#endif +#endif +#endif + +#ifdef TINYGLTF_ENABLE_DRACO +#include "draco/compression/decode.h" +#include "draco/core/decoder_buffer.h" +#endif + +#ifndef TINYGLTF_NO_STB_IMAGE +#ifndef TINYGLTF_NO_INCLUDE_STB_IMAGE +#include "stb_image.h" +#endif +#endif + +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE +#ifndef TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE +#include "stb_image_write.h" +#endif +#endif + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +#ifdef _WIN32 + +// issue 143. +// Define NOMINMAX to avoid min/max defines, +// but undef it after included Windows.h +#ifndef NOMINMAX +#define TINYGLTF_INTERNAL_NOMINMAX +#define NOMINMAX +#endif + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#define TINYGLTF_INTERNAL_WIN32_LEAN_AND_MEAN +#endif +#ifndef __MINGW32__ +#include // include API for expanding a file path +#else +#include +#endif + +#ifdef TINYGLTF_INTERNAL_WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#endif + +#if defined(TINYGLTF_INTERNAL_NOMINMAX) +#undef NOMINMAX +#endif + +#if defined(__GLIBCXX__) // mingw + +#include // _O_RDONLY + +#include // fstream (all sorts of IO stuff) + stdio_filebuf (=streambuf) + +#endif + +#elif !defined(__ANDROID__) && !defined(__OpenBSD__) +// #include +#endif + +#if defined(__sparcv9) || defined(__powerpc__) +// Big endian +#else +#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || MINIZ_X86_OR_X64_CPU +#define TINYGLTF_LITTLE_ENDIAN 1 +#endif +#endif + +namespace { +#ifdef TINYGLTF_USE_RAPIDJSON + +#ifdef TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR +// This uses the RapidJSON CRTAllocator. It is thread safe and multiple +// documents may be active at once. +using json = + rapidjson::GenericValue, rapidjson::CrtAllocator>; +using json_const_iterator = json::ConstMemberIterator; +using json_const_array_iterator = json const*; +using JsonDocument = + rapidjson::GenericDocument, rapidjson::CrtAllocator>; +rapidjson::CrtAllocator s_CrtAllocator; // stateless and thread safe +rapidjson::CrtAllocator& GetAllocator() { return s_CrtAllocator; } +#else +// This uses the default RapidJSON MemoryPoolAllocator. It is very fast, but +// not thread safe. Only a single JsonDocument may be active at any one time, +// meaning only a single gltf load/save can be active any one time. +using json = rapidjson::Value; +using json_const_iterator = json::ConstMemberIterator; +using json_const_array_iterator = json const*; +rapidjson::Document* s_pActiveDocument = nullptr; +rapidjson::Document::AllocatorType& GetAllocator() +{ + assert(s_pActiveDocument); // Root json node must be JsonDocument type + return s_pActiveDocument->GetAllocator(); +} + +#ifdef __clang__ +#pragma clang diagnostic push +// Suppress JsonDocument(JsonDocument &&rhs) noexcept +#pragma clang diagnostic ignored "-Wunused-member-function" +#endif + +struct JsonDocument : public rapidjson::Document { + JsonDocument() + { + assert(s_pActiveDocument == + nullptr); // When using default allocator, only one document can + // be active at a time, if you need multiple active at + // once, define TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR + s_pActiveDocument = this; + } + JsonDocument(const JsonDocument&) = delete; + JsonDocument(JsonDocument&& rhs) noexcept + : rapidjson::Document(std::move(rhs)) + { + s_pActiveDocument = this; + rhs.isNil = true; + } + ~JsonDocument() + { + if (!isNil) { + s_pActiveDocument = nullptr; + } + } + +private: + bool isNil = false; +}; + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif // TINYGLTF_USE_RAPIDJSON_CRTALLOCATOR + +#else +using nlohmann::json; +using json_const_iterator = json::const_iterator; +using json_const_array_iterator = json_const_iterator; +using JsonDocument = json; +#endif + +void JsonParse(JsonDocument& doc, const char* str, size_t length, + bool throwExc = false) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + (void)throwExc; + doc.Parse(str, length); +#else + doc = json::parse(str, str + length, nullptr, throwExc); +#endif +} +} // namespace + +#ifdef __APPLE__ +#include "TargetConditionals.h" +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wc++98-compat" +#endif + +namespace tinygltf { + +/// +/// Internal LoadImageDataOption struct. +/// This struct is passed through `user_pointer` in LoadImageData. +/// The struct is not passed when the user supply their own LoadImageData +/// callbacks. +/// +struct LoadImageDataOption { + // true: preserve image channels(e.g. load as RGB image if the image has RGB + // channels) default `false`(channels are expanded to RGBA for backward + // compatibility). + bool preserve_channels{false}; +}; + +// Equals function for Value, for recursivity +static bool Equals(const tinygltf::Value& one, const tinygltf::Value& other) +{ + if (one.Type() != other.Type()) + return false; + + switch (one.Type()) { + case NULL_TYPE: + return true; + case BOOL_TYPE: + return one.Get() == other.Get(); + case REAL_TYPE: + return TINYGLTF_DOUBLE_EQUAL(one.Get(), other.Get()); + case INT_TYPE: + return one.Get() == other.Get(); + case OBJECT_TYPE: { + auto oneObj = one.Get(); + auto otherObj = other.Get(); + if (oneObj.size() != otherObj.size()) + return false; + for (auto& it : oneObj) { + auto otherIt = otherObj.find(it.first); + if (otherIt == otherObj.end()) + return false; + + if (!Equals(it.second, otherIt->second)) + return false; + } + return true; + } + case ARRAY_TYPE: { + if (one.Size() != other.Size()) + return false; + for (int i = 0; i < int(one.Size()); ++i) + if (!Equals(one.Get(i), other.Get(i))) + return false; + return true; + } + case STRING_TYPE: + return one.Get() == other.Get(); + case BINARY_TYPE: + return one.Get>() == + other.Get>(); + default: { + // unhandled type + return false; + } + } +} + +// Equals function for std::vector using TINYGLTF_DOUBLE_EPSILON +static bool Equals(const std::vector& one, + const std::vector& other) +{ + if (one.size() != other.size()) + return false; + for (int i = 0; i < int(one.size()); ++i) { + if (!TINYGLTF_DOUBLE_EQUAL(one[size_t(i)], other[size_t(i)])) + return false; + } + return true; +} + +bool Accessor::operator==(const Accessor& other) const +{ + return this->bufferView == other.bufferView && + this->byteOffset == other.byteOffset && + this->componentType == other.componentType && + this->count == other.count && this->extensions == other.extensions && + this->extras == other.extras && + Equals(this->maxValues, other.maxValues) && + Equals(this->minValues, other.minValues) && + this->name == other.name && this->normalized == other.normalized && + this->type == other.type; +} +bool Animation::operator==(const Animation& other) const +{ + return this->channels == other.channels && + this->extensions == other.extensions && + this->extras == other.extras && this->name == other.name && + this->samplers == other.samplers; +} +bool AnimationChannel::operator==(const AnimationChannel& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && + this->target_node == other.target_node && + this->target_path == other.target_path && + this->sampler == other.sampler; +} +bool AnimationSampler::operator==(const AnimationSampler& other) const +{ + return this->extras == other.extras && + this->extensions == other.extensions && this->input == other.input && + this->interpolation == other.interpolation && + this->output == other.output; +} +bool Asset::operator==(const Asset& other) const +{ + return this->copyright == other.copyright && + this->extensions == other.extensions && + this->extras == other.extras && this->generator == other.generator && + this->minVersion == other.minVersion && + this->version == other.version; +} +bool Buffer::operator==(const Buffer& other) const +{ + return this->data == other.data && this->extensions == other.extensions && + this->extras == other.extras && this->name == other.name && + this->uri == other.uri; +} +bool BufferView::operator==(const BufferView& other) const +{ + return this->buffer == other.buffer && + this->byteLength == other.byteLength && + this->byteOffset == other.byteOffset && + this->byteStride == other.byteStride && this->name == other.name && + this->target == other.target && + this->extensions == other.extensions && + this->extras == other.extras && + this->dracoDecoded == other.dracoDecoded; +} +bool Camera::operator==(const Camera& other) const +{ + return this->name == other.name && this->extensions == other.extensions && + this->extras == other.extras && + this->orthographic == other.orthographic && + this->perspective == other.perspective && this->type == other.type; +} +bool Image::operator==(const Image& other) const +{ + return this->bufferView == other.bufferView && + this->component == other.component && + this->extensions == other.extensions && + this->extras == other.extras && this->height == other.height && + this->image == other.image && this->mimeType == other.mimeType && + this->name == other.name && this->uri == other.uri && + this->width == other.width; +} +bool Light::operator==(const Light& other) const +{ + return Equals(this->color, other.color) && this->name == other.name && + this->type == other.type; +} +bool Material::operator==(const Material& other) const +{ + return (this->pbrMetallicRoughness == other.pbrMetallicRoughness) && + (this->normalTexture == other.normalTexture) && + (this->occlusionTexture == other.occlusionTexture) && + (this->emissiveTexture == other.emissiveTexture) && + Equals(this->emissiveFactor, other.emissiveFactor) && + (this->alphaMode == other.alphaMode) && + TINYGLTF_DOUBLE_EQUAL(this->alphaCutoff, other.alphaCutoff) && + (this->doubleSided == other.doubleSided) && + (this->extensions == other.extensions) && + (this->extras == other.extras) && (this->values == other.values) && + (this->additionalValues == other.additionalValues) && + (this->name == other.name); +} +bool Mesh::operator==(const Mesh& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && this->name == other.name && + Equals(this->weights, other.weights) && + this->primitives == other.primitives; +} +bool Model::operator==(const Model& other) const +{ + return this->accessors == other.accessors && + this->animations == other.animations && this->asset == other.asset && + this->buffers == other.buffers && + this->bufferViews == other.bufferViews && + this->cameras == other.cameras && + this->defaultScene == other.defaultScene && + this->extensions == other.extensions && + this->extensionsRequired == other.extensionsRequired && + this->extensionsUsed == other.extensionsUsed && + this->extras == other.extras && this->images == other.images && + this->lights == other.lights && this->materials == other.materials && + this->meshes == other.meshes && this->nodes == other.nodes && + this->samplers == other.samplers && this->scenes == other.scenes && + this->skins == other.skins && this->textures == other.textures; +} +bool Node::operator==(const Node& other) const +{ + return this->camera == other.camera && this->children == other.children && + this->extensions == other.extensions && + this->extras == other.extras && Equals(this->matrix, other.matrix) && + this->mesh == other.mesh && this->name == other.name && + Equals(this->rotation, other.rotation) && + Equals(this->scale, other.scale) && this->skin == other.skin && + Equals(this->translation, other.translation) && + Equals(this->weights, other.weights); +} +bool SpotLight::operator==(const SpotLight& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && + TINYGLTF_DOUBLE_EQUAL(this->innerConeAngle, other.innerConeAngle) && + TINYGLTF_DOUBLE_EQUAL(this->outerConeAngle, other.outerConeAngle); +} +bool OrthographicCamera::operator==(const OrthographicCamera& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && + TINYGLTF_DOUBLE_EQUAL(this->xmag, other.xmag) && + TINYGLTF_DOUBLE_EQUAL(this->ymag, other.ymag) && + TINYGLTF_DOUBLE_EQUAL(this->zfar, other.zfar) && + TINYGLTF_DOUBLE_EQUAL(this->znear, other.znear); +} +bool Parameter::operator==(const Parameter& other) const +{ + if (this->bool_value != other.bool_value || + this->has_number_value != other.has_number_value) + return false; + + if (!TINYGLTF_DOUBLE_EQUAL(this->number_value, other.number_value)) + return false; + + if (this->json_double_value.size() != other.json_double_value.size()) + return false; + for (auto& it : this->json_double_value) { + auto otherIt = other.json_double_value.find(it.first); + if (otherIt == other.json_double_value.end()) + return false; + + if (!TINYGLTF_DOUBLE_EQUAL(it.second, otherIt->second)) + return false; + } + + if (!Equals(this->number_array, other.number_array)) + return false; + + if (this->string_value != other.string_value) + return false; + + return true; +} +bool PerspectiveCamera::operator==(const PerspectiveCamera& other) const +{ + return TINYGLTF_DOUBLE_EQUAL(this->aspectRatio, other.aspectRatio) && + this->extensions == other.extensions && + this->extras == other.extras && + TINYGLTF_DOUBLE_EQUAL(this->yfov, other.yfov) && + TINYGLTF_DOUBLE_EQUAL(this->zfar, other.zfar) && + TINYGLTF_DOUBLE_EQUAL(this->znear, other.znear); +} +bool Primitive::operator==(const Primitive& other) const +{ + return this->attributes == other.attributes && + this->extras == other.extras && this->indices == other.indices && + this->material == other.material && this->mode == other.mode && + this->targets == other.targets; +} +bool Sampler::operator==(const Sampler& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && this->magFilter == other.magFilter && + this->minFilter == other.minFilter && this->name == other.name && + this->wrapS == other.wrapS && this->wrapT == other.wrapT; + + // this->wrapR == other.wrapR +} +bool Scene::operator==(const Scene& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && this->name == other.name && + this->nodes == other.nodes; +} +bool Skin::operator==(const Skin& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && + this->inverseBindMatrices == other.inverseBindMatrices && + this->joints == other.joints && this->name == other.name && + this->skeleton == other.skeleton; +} +bool Texture::operator==(const Texture& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && this->name == other.name && + this->sampler == other.sampler && this->source == other.source; +} +bool TextureInfo::operator==(const TextureInfo& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && this->index == other.index && + this->texCoord == other.texCoord; +} +bool NormalTextureInfo::operator==(const NormalTextureInfo& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && this->index == other.index && + this->texCoord == other.texCoord && + TINYGLTF_DOUBLE_EQUAL(this->scale, other.scale); +} +bool OcclusionTextureInfo::operator==(const OcclusionTextureInfo& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && this->index == other.index && + this->texCoord == other.texCoord && + TINYGLTF_DOUBLE_EQUAL(this->strength, other.strength); +} +bool PbrMetallicRoughness::operator==(const PbrMetallicRoughness& other) const +{ + return this->extensions == other.extensions && + this->extras == other.extras && + (this->baseColorTexture == other.baseColorTexture) && + (this->metallicRoughnessTexture == other.metallicRoughnessTexture) && + Equals(this->baseColorFactor, other.baseColorFactor) && + TINYGLTF_DOUBLE_EQUAL(this->metallicFactor, other.metallicFactor) && + TINYGLTF_DOUBLE_EQUAL(this->roughnessFactor, other.roughnessFactor); +} +bool Value::operator==(const Value& other) const +{ + return Equals(*this, other); +} + +static void swap4(unsigned int* val) +{ +#ifdef TINYGLTF_LITTLE_ENDIAN + (void)val; +#else + unsigned int tmp = *val; + unsigned char* dst = reinterpret_cast(val); + unsigned char* src = reinterpret_cast(&tmp); + + dst[0] = src[3]; + dst[1] = src[2]; + dst[2] = src[1]; + dst[3] = src[0]; +#endif +} + +static std::string JoinPath(const std::string& path0, const std::string& path1) +{ + if (path0.empty()) { + return path1; + } + else { + // check '/' + char lastChar = *path0.rbegin(); + if (lastChar != '/') { + return path0 + std::string("/") + path1; + } + else { + return path0 + path1; + } + } +} + +static std::string FindFile(const std::vector& paths, + const std::string& filepath, FsCallbacks* fs) +{ + if (fs == nullptr || fs->ExpandFilePath == nullptr || + fs->FileExists == nullptr) { + // Error, fs callback[s] missing + return std::string(); + } + + for (size_t i = 0; i < paths.size(); i++) { + std::string absPath = + fs->ExpandFilePath(JoinPath(paths[i], filepath), fs->user_data); + if (fs->FileExists(absPath, fs->user_data)) { + return absPath; + } + } + + return std::string(); +} + +static std::string GetFilePathExtension(const std::string& FileName) +{ + if (FileName.find_last_of(".") != std::string::npos) + return FileName.substr(FileName.find_last_of(".") + 1); + return ""; +} + +static std::string GetBaseDir(const std::string& filepath) +{ + if (filepath.find_last_of("/\\") != std::string::npos) + return filepath.substr(0, filepath.find_last_of("/\\")); + return ""; +} + +static std::string GetBaseFilename(const std::string& filepath) +{ + auto idx = filepath.find_last_of("/\\"); + if (idx != std::string::npos) + return filepath.substr(idx + 1); + return filepath; +} + +std::string base64_encode(unsigned char const*, unsigned int len); +std::string base64_decode(std::string const& s); + +/* + base64.cpp and base64.h + + Copyright (C) 2004-2008 René Nyffenegger + + This source code is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + + 3. This notice may not be removed or altered from any source distribution. + + René Nyffenegger rene.nyffenegger@adp-gmbh.ch + +*/ + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wsign-conversion" +#pragma clang diagnostic ignored "-Wconversion" +#endif + +static inline bool is_base64(unsigned char c) +{ + return (isalnum(c) || (c == '+') || (c == '/')); +} + +std::string base64_encode(unsigned char const* bytes_to_encode, + unsigned int in_len) +{ + std::string ret; + int i = 0; + int j = 0; + unsigned char char_array_3[3]; + unsigned char char_array_4[4]; + + const char* base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + while (in_len--) { + char_array_3[i++] = *(bytes_to_encode++); + if (i == 3) { + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (i = 0; (i < 4); i++) + ret += base64_chars[char_array_4[i]]; + i = 0; + } + } + + if (i) { + for (j = i; j < 3; j++) + char_array_3[j] = '\0'; + + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = + ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = + ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + + for (j = 0; (j < i + 1); j++) + ret += base64_chars[char_array_4[j]]; + + while ((i++ < 3)) + ret += '='; + } + + return ret; +} + +std::string base64_decode(std::string const& encoded_string) +{ + int in_len = static_cast(encoded_string.size()); + int i = 0; + int j = 0; + int in_ = 0; + unsigned char char_array_4[4], char_array_3[3]; + std::string ret; + + const std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + + while (in_len-- && (encoded_string[in_] != '=') && + is_base64(encoded_string[in_])) { + char_array_4[i++] = encoded_string[in_]; + in_++; + if (i == 4) { + for (i = 0; i < 4; i++) + char_array_4[i] = static_cast( + base64_chars.find(char_array_4[i])); + + char_array_3[0] = + (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (i = 0; (i < 3); i++) + ret += char_array_3[i]; + i = 0; + } + } + + if (i) { + for (j = i; j < 4; j++) + char_array_4[j] = 0; + + for (j = 0; j < 4; j++) + char_array_4[j] = + static_cast(base64_chars.find(char_array_4[j])); + + char_array_3[0] = + (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); + char_array_3[1] = + ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); + char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; + + for (j = 0; (j < i - 1); j++) + ret += char_array_3[j]; + } + + return ret; +} +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +// https://github.com/syoyo/tinygltf/issues/228 +// TODO(syoyo): Use uriparser https://uriparser.github.io/ for stricter Uri +// decoding? +// +// Uri Decoding from DLIB +// http://dlib.net/dlib/server/server_http.cpp.html +// --- dlib begin ------------------------------------------------------------ +// Copyright (C) 2003 Davis E. King (davis@dlib.net) +// License: Boost Software License +// Boost Software License - Version 1.0 - August 17th, 2003 + +// Permission is hereby granted, free of charge, to any person or organization +// obtaining a copy of the software and accompanying documentation covered by +// this license (the "Software") to use, reproduce, display, distribute, +// execute, and transmit the Software, and to prepare derivative works of the +// Software, and to permit third-parties to whom the Software is furnished to +// do so, all subject to the following: +// The copyright notices in the Software and this entire statement, including +// the above license grant, this restriction and the following disclaimer, +// must be included in all copies of the Software, in whole or in part, and +// all derivative works of the Software, unless such copies or derivative +// works are solely in the form of machine-executable object code generated by +// a source language processor. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +namespace dlib { + +inline unsigned char from_hex(unsigned char ch) +{ + if (ch <= '9' && ch >= '0') + ch -= '0'; + else if (ch <= 'f' && ch >= 'a') + ch -= 'a' - 10; + else if (ch <= 'F' && ch >= 'A') + ch -= 'A' - 10; + else + ch = 0; + return ch; +} + +static const std::string urldecode(const std::string& str) +{ + using namespace std; + string result; + string::size_type i; + for (i = 0; i < str.size(); ++i) { + if (str[i] == '+') { + result += ' '; + } + else if (str[i] == '%' && str.size() > i + 2) { + const unsigned char ch1 = + from_hex(static_cast(str[i + 1])); + const unsigned char ch2 = + from_hex(static_cast(str[i + 2])); + const unsigned char ch = + static_cast((ch1 << 4) | ch2); + result += static_cast(ch); + i += 2; + } + else { + result += str[i]; + } + } + return result; +} + +} // namespace dlib +// --- dlib end -------------------------------------------------------------- + +static bool LoadExternalFile(std::vector* out, std::string* err, + std::string* warn, const std::string& filename, + const std::string& basedir, bool required, + size_t reqBytes, bool checkSize, FsCallbacks* fs) +{ + if (fs == nullptr || fs->FileExists == nullptr || + fs->ExpandFilePath == nullptr || fs->ReadWholeFile == nullptr) { + // This is a developer error, assert() ? + if (err) { + (*err) += "FS callback[s] not set\n"; + } + return false; + } + + std::string* failMsgOut = required ? err : warn; + + out->clear(); + + std::vector paths; + paths.push_back(basedir); + paths.push_back("."); + + std::string filepath = FindFile(paths, filename, fs); + if (filepath.empty() || filename.empty()) { + if (failMsgOut) { + (*failMsgOut) += "File not found : " + filename + "\n"; + } + return false; + } + + std::vector buf; + std::string fileReadErr; + bool fileRead = + fs->ReadWholeFile(&buf, &fileReadErr, filepath, fs->user_data); + if (!fileRead) { + if (failMsgOut) { + (*failMsgOut) += + "File read error : " + filepath + " : " + fileReadErr + "\n"; + } + return false; + } + + size_t sz = buf.size(); + if (sz == 0) { + if (failMsgOut) { + (*failMsgOut) += "File is empty : " + filepath + "\n"; + } + return false; + } + + if (checkSize) { + if (reqBytes == sz) { + out->swap(buf); + return true; + } + else { + std::stringstream ss; + ss << "File size mismatch : " << filepath << ", requestedBytes " + << reqBytes << ", but got " << sz << std::endl; + if (failMsgOut) { + (*failMsgOut) += ss.str(); + } + return false; + } + } + + out->swap(buf); + return true; +} + +void TinyGLTF::SetImageLoader(LoadImageDataFunction func, void* user_data) +{ + LoadImageData = func; + load_image_user_data_ = user_data; + user_image_loader_ = true; +} + +void TinyGLTF::RemoveImageLoader() +{ + LoadImageData = +#ifndef TINYGLTF_NO_STB_IMAGE + &tinygltf::LoadImageData; +#else + nullptr; +#endif + + load_image_user_data_ = nullptr; + user_image_loader_ = false; +} + +#ifndef TINYGLTF_NO_STB_IMAGE +bool LoadImageData(Image* image, const int image_idx, std::string* err, + std::string* warn, int req_width, int req_height, + const unsigned char* bytes, int size, void* user_data) +{ + (void)warn; + + LoadImageDataOption option; + if (user_data) { + option = *reinterpret_cast(user_data); + } + + int w = 0, h = 0, comp = 0, req_comp = 0; + + unsigned char* data = nullptr; + + // preserve_channels true: Use channels stored in the image file. + // false: force 32-bit textures for common Vulkan compatibility. It appears + // that some GPU drivers do not support 24-bit images for Vulkan + req_comp = option.preserve_channels ? 0 : 4; + int bits = 8; + int pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE; + + // It is possible that the image we want to load is a 16bit per channel + // image We are going to attempt to load it as 16bit per channel, and if it + // worked, set the image data accordingly. We are casting the returned + // pointer into unsigned char, because we are representing "bytes". But we + // are updating the Image metadata to signal that this image uses 2 bytes + // (16bits) per channel: + if (stbi_is_16_bit_from_memory(bytes, size)) { + data = reinterpret_cast( + stbi_load_16_from_memory(bytes, size, &w, &h, &comp, req_comp)); + if (data) { + bits = 16; + pixel_type = TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT; + } + } + + // at this point, if data is still NULL, it means that the image wasn't + // 16bit per channel, we are going to load it as a normal 8bit per channel + // image as we used to do: + // if image cannot be decoded, ignore parsing and keep it by its path + // don't break in this case + // FIXME we should only enter this function if the image is embedded. If + // image->uri references + // an image file, it should be left as it is. Image loading should not be + // mandatory (to support other formats) + if (!data) + data = stbi_load_from_memory(bytes, size, &w, &h, &comp, req_comp); + if (!data) { + // NOTE: you can use `warn` instead of `err` + if (err) { + (*err) += "Unknown image format. STB cannot decode image data for " + "image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\".\n"; + } + return false; + } + + if ((w < 1) || (h < 1)) { + stbi_image_free(data); + if (err) { + (*err) += "Invalid image data for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\"\n"; + } + return false; + } + + if (req_width > 0) { + if (req_width != w) { + stbi_image_free(data); + if (err) { + (*err) += "Image width mismatch for image[" + + std::to_string(image_idx) + "] name = \"" + + image->name + "\"\n"; + } + return false; + } + } + + if (req_height > 0) { + if (req_height != h) { + stbi_image_free(data); + if (err) { + (*err) += "Image height mismatch. for image[" + + std::to_string(image_idx) + "] name = \"" + + image->name + "\"\n"; + } + return false; + } + } + + if (req_comp != 0) { + // loaded data has `req_comp` channels(components) + comp = req_comp; + } + + image->width = w; + image->height = h; + image->component = comp; + image->bits = bits; + image->pixel_type = pixel_type; + image->image.resize(static_cast(w * h * comp) * size_t(bits / 8)); + std::copy(data, data + w * h * comp * (bits / 8), image->image.begin()); + stbi_image_free(data); + + return true; +} +#endif + +void TinyGLTF::SetImageWriter(WriteImageDataFunction func, void* user_data) +{ + WriteImageData = func; + write_image_user_data_ = user_data; +} + +#ifndef TINYGLTF_NO_STB_IMAGE_WRITE +static void WriteToMemory_stbi(void* context, void* data, int size) +{ + std::vector* buffer = + reinterpret_cast*>(context); + + unsigned char* pData = reinterpret_cast(data); + + buffer->insert(buffer->end(), pData, pData + size); +} + +bool WriteImageData(const std::string* basepath, const std::string* filename, + Image* image, bool embedImages, void* fsPtr) +{ + const std::string ext = GetFilePathExtension(*filename); + + // Write image to temporary buffer + std::string header; + std::vector data; + + if (ext == "png") { + if ((image->bits != 8) || + (image->pixel_type != TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE)) { + // Unsupported pixel format + return false; + } + + if (!stbi_write_png_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0], 0)) { + return false; + } + header = "data:image/png;base64,"; + } + else if (ext == "jpg") { + if (!stbi_write_jpg_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0], 100)) { + return false; + } + header = "data:image/jpeg;base64,"; + } + else if (ext == "bmp") { + if (!stbi_write_bmp_to_func(WriteToMemory_stbi, &data, image->width, + image->height, image->component, + &image->image[0])) { + return false; + } + header = "data:image/bmp;base64,"; + } + else if (!embedImages) { + // Error: can't output requested format to file + return false; + } + + if (embedImages) { + // Embed base64-encoded image into URI + if (data.size()) { + image->uri = + header + + base64_encode(&data[0], static_cast(data.size())); + } + else { + // Throw error? + } + } + else { + // Write image to disc + FsCallbacks* fs = reinterpret_cast(fsPtr); + if ((fs != nullptr) && (fs->WriteWholeFile != nullptr)) { + const std::string imagefilepath = JoinPath(*basepath, *filename); + std::string writeError; + if (!fs->WriteWholeFile(&writeError, imagefilepath, data, + fs->user_data)) { + // Could not write image file to disc; Throw error ? + return false; + } + } + else { + // Throw error? + } + image->uri = *filename; + } + + return true; +} +#endif + +void TinyGLTF::SetFsCallbacks(FsCallbacks callbacks) { fs = callbacks; } + +#ifdef _WIN32 +static inline std::wstring UTF8ToWchar(const std::string& str) +{ + int wstr_size = MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), + nullptr, 0); + std::wstring wstr((size_t)wstr_size, 0); + MultiByteToWideChar(CP_UTF8, 0, str.data(), (int)str.size(), &wstr[0], + (int)wstr.size()); + return wstr; +} + +static inline std::string WcharToUTF8(const std::wstring& wstr) +{ + int str_size = + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, + 0, nullptr, nullptr); + std::string str((size_t)str_size, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), &str[0], + (int)str.size(), nullptr, nullptr); + return str; +} +#endif + +#ifndef TINYGLTF_NO_FS +// Default implementations of filesystem functions + +bool FileExists(const std::string& abs_filename, void*) +{ + bool ret; +#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS + if (asset_manager) { + AAsset* asset = AAssetManager_open(asset_manager, abs_filename.c_str(), + AASSET_MODE_STREAMING); + if (!asset) { + return false; + } + AAsset_close(asset); + ret = true; + } + else { + return false; + } +#else +#ifdef _WIN32 +#if defined(_MSC_VER) || defined(__GLIBCXX__) || defined(_LIBCPP_VERSION) + FILE* fp = nullptr; + errno_t err = _wfopen_s(&fp, UTF8ToWchar(abs_filename).c_str(), L"rb"); + if (err != 0) { + return false; + } +#else + FILE* fp = nullptr; + errno_t err = fopen_s(&fp, abs_filename.c_str(), "rb"); + if (err != 0) { + return false; + } +#endif + +#else + FILE* fp = fopen(abs_filename.c_str(), "rb"); +#endif + if (fp) { + ret = true; + fclose(fp); + } + else { + ret = false; + } +#endif + + return ret; +} + +std::string ExpandFilePath(const std::string& filepath, void*) +{ + // https://github.com/syoyo/tinygltf/issues/368 + // + // No file path expansion in built-in FS function anymore, since glTF URI + // should not contain tilde('~') and environment variables, and for security + // reason(`wordexp`). + // + // Users need to supply `base_dir`(in `LoadASCIIFromString`, + // `LoadBinaryFromMemory`) in expanded absolute path. + + return filepath; + +#if 0 +#ifdef _WIN32 + // Assume input `filepath` is encoded in UTF-8 + std::wstring wfilepath = UTF8ToWchar(filepath); + DWORD wlen = ExpandEnvironmentStringsW(wfilepath.c_str(), nullptr, 0); + wchar_t *wstr = new wchar_t[wlen]; + ExpandEnvironmentStringsW(wfilepath.c_str(), wstr, wlen); + + std::wstring ws(wstr); + delete[] wstr; + return WcharToUTF8(ws); + +#else + +#if defined(TARGET_OS_IPHONE) || defined(TARGET_IPHONE_SIMULATOR) || \ + defined(__ANDROID__) || defined(__EMSCRIPTEN__) || defined(__OpenBSD__) + // no expansion + std::string s = filepath; +#else + std::string s; + wordexp_t p; + + if (filepath.empty()) { + return ""; + } + + // Quote the string to keep any spaces in filepath intact. + std::string quoted_path = "\"" + filepath + "\""; + // char** w; + int ret = wordexp(quoted_path.c_str(), &p, 0); + if (ret) { + // err + s = filepath; + return s; + } + + // Use first element only. + if (p.we_wordv) { + s = std::string(p.we_wordv[0]); + wordfree(&p); + } else { + s = filepath; + } + +#endif + + return s; +#endif +#endif +} + +bool ReadWholeFile(std::vector* out, std::string* err, + const std::string& filepath, void*) +{ +#ifdef TINYGLTF_ANDROID_LOAD_FROM_ASSETS + if (asset_manager) { + AAsset* asset = AAssetManager_open(asset_manager, filepath.c_str(), + AASSET_MODE_STREAMING); + if (!asset) { + if (err) { + (*err) += "File open error : " + filepath + "\n"; + } + return false; + } + size_t size = AAsset_getLength(asset); + if (size == 0) { + if (err) { + (*err) += "Invalid file size : " + filepath + + " (does the path point to a directory?)"; + } + return false; + } + out->resize(size); + AAsset_read(asset, reinterpret_cast(&out->at(0)), size); + AAsset_close(asset); + return true; + } + else { + if (err) { + (*err) += "No asset manager specified : " + filepath + "\n"; + } + return false; + } +#else +#ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = + _wopen(UTF8ToWchar(filepath).c_str(), _O_RDONLY | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf(file_descriptor, + std::ios_base::in); + std::istream f(&wfile_buf); +#elif defined(_MSC_VER) || defined(_LIBCPP_VERSION) + // For libcxx, assume _LIBCPP_HAS_OPEN_WITH_WCHAR is defined to accept + // `wchar_t *` + std::ifstream f(UTF8ToWchar(filepath).c_str(), std::ifstream::binary); +#else + // Unknown compiler/runtime + std::ifstream f(filepath.c_str(), std::ifstream::binary); +#endif +#else + std::ifstream f(filepath.c_str(), std::ifstream::binary); +#endif + if (!f) { + if (err) { + (*err) += "File open error : " + filepath + "\n"; + } + return false; + } + + f.seekg(0, f.end); + size_t sz = static_cast(f.tellg()); + f.seekg(0, f.beg); + + if (int64_t(sz) < 0) { + if (err) { + (*err) += "Invalid file size : " + filepath + + " (does the path point to a directory?)"; + } + return false; + } + else if (sz == 0) { + if (err) { + (*err) += "File is empty : " + filepath + "\n"; + } + return false; + } + + out->resize(sz); + f.read(reinterpret_cast(&out->at(0)), + static_cast(sz)); + + return true; +#endif +} + +bool WriteWholeFile(std::string* err, const std::string& filepath, + const std::vector& contents, void*) +{ +#ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = _wopen(UTF8ToWchar(filepath).c_str(), + _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf( + file_descriptor, std::ios_base::out | std::ios_base::binary); + std::ostream f(&wfile_buf); +#elif defined(_MSC_VER) + std::ofstream f(UTF8ToWchar(filepath).c_str(), std::ofstream::binary); +#else // clang? + std::ofstream f(filepath.c_str(), std::ofstream::binary); +#endif +#else + std::ofstream f(filepath.c_str(), std::ofstream::binary); +#endif + if (!f) { + if (err) { + (*err) += "File open error for writing : " + filepath + "\n"; + } + return false; + } + + f.write(reinterpret_cast(&contents.at(0)), + static_cast(contents.size())); + if (!f) { + if (err) { + (*err) += "File write error: " + filepath + "\n"; + } + return false; + } + + return true; +} + +#endif // TINYGLTF_NO_FS + +static std::string MimeToExt(const std::string& mimeType) +{ + if (mimeType == "image/jpeg") { + return "jpg"; + } + else if (mimeType == "image/png") { + return "png"; + } + else if (mimeType == "image/bmp") { + return "bmp"; + } + else if (mimeType == "image/gif") { + return "gif"; + } + + return ""; +} + +static void UpdateImageObject(Image& image, std::string& baseDir, int index, + bool embedImages, + WriteImageDataFunction* WriteImageData = nullptr, + void* user_data = nullptr) +{ + std::string filename; + std::string ext; + // If image has uri, use it as a filename + if (image.uri.size()) { + filename = GetBaseFilename(image.uri); + ext = GetFilePathExtension(filename); + } + else if (image.bufferView != -1) { + // If there's no URI and the data exists in a buffer, + // don't change properties or write images + } + else if (image.name.size()) { + ext = MimeToExt(image.mimeType); + // Otherwise use name as filename + filename = image.name + "." + ext; + } + else { + ext = MimeToExt(image.mimeType); + // Fallback to index of image as filename + filename = std::to_string(index) + "." + ext; + } + + // If callback is set, modify image data object + if (*WriteImageData != nullptr && !filename.empty()) { + std::string uri; + (*WriteImageData)(&baseDir, &filename, &image, embedImages, user_data); + } +} + +bool IsDataURI(const std::string& in) +{ + std::string header = "data:application/octet-stream;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/jpeg;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/png;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/bmp;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:image/gif;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:text/plain;base64,"; + if (in.find(header) == 0) { + return true; + } + + header = "data:application/gltf-buffer;base64,"; + if (in.find(header) == 0) { + return true; + } + + return false; +} + +bool DecodeDataURI(std::vector* out, std::string& mime_type, + const std::string& in, size_t reqBytes, bool checkSize) +{ + std::string header = "data:application/octet-stream;base64,"; + std::string data; + if (in.find(header) == 0) { + data = base64_decode(in.substr(header.size())); // cut mime string. + } + + if (data.empty()) { + header = "data:image/jpeg;base64,"; + if (in.find(header) == 0) { + mime_type = "image/jpeg"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:image/png;base64,"; + if (in.find(header) == 0) { + mime_type = "image/png"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:image/bmp;base64,"; + if (in.find(header) == 0) { + mime_type = "image/bmp"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:image/gif;base64,"; + if (in.find(header) == 0) { + mime_type = "image/gif"; + data = base64_decode(in.substr(header.size())); // cut mime string. + } + } + + if (data.empty()) { + header = "data:text/plain;base64,"; + if (in.find(header) == 0) { + mime_type = "text/plain"; + data = base64_decode(in.substr(header.size())); + } + } + + if (data.empty()) { + header = "data:application/gltf-buffer;base64,"; + if (in.find(header) == 0) { + data = base64_decode(in.substr(header.size())); + } + } + + // TODO(syoyo): Allow empty buffer? #229 + if (data.empty()) { + return false; + } + + if (checkSize) { + if (data.size() != reqBytes) { + return false; + } + out->resize(reqBytes); + } + else { + out->resize(data.size()); + } + std::copy(data.begin(), data.end(), out->begin()); + return true; +} + +namespace { +bool GetInt(const json& o, int& val) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + if (!o.IsDouble()) { + if (o.IsInt()) { + val = o.GetInt(); + return true; + } + else if (o.IsUint()) { + val = static_cast(o.GetUint()); + return true; + } + else if (o.IsInt64()) { + val = static_cast(o.GetInt64()); + return true; + } + else if (o.IsUint64()) { + val = static_cast(o.GetUint64()); + return true; + } + } + + return false; +#else + auto type = o.type(); + + if ((type == json::value_t::number_integer) || + (type == json::value_t::number_unsigned)) { + val = static_cast(o.get()); + return true; + } + + return false; +#endif +} + +#ifdef TINYGLTF_USE_RAPIDJSON +bool GetDouble(const json& o, double& val) +{ + if (o.IsDouble()) { + val = o.GetDouble(); + return true; + } + + return false; +} +#endif + +bool GetNumber(const json& o, double& val) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + if (o.IsNumber()) { + val = o.GetDouble(); + return true; + } + + return false; +#else + if (o.is_number()) { + val = o.get(); + return true; + } + + return false; +#endif +} + +bool GetString(const json& o, std::string& val) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + if (o.IsString()) { + val = o.GetString(); + return true; + } + + return false; +#else + if (o.type() == json::value_t::string) { + val = o.get(); + return true; + } + + return false; +#endif +} + +bool IsArray(const json& o) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return o.IsArray(); +#else + return o.is_array(); +#endif +} + +json_const_array_iterator ArrayBegin(const json& o) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return o.Begin(); +#else + return o.begin(); +#endif +} + +json_const_array_iterator ArrayEnd(const json& o) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return o.End(); +#else + return o.end(); +#endif +} + +bool IsObject(const json& o) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return o.IsObject(); +#else + return o.is_object(); +#endif +} + +json_const_iterator ObjectBegin(const json& o) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return o.MemberBegin(); +#else + return o.begin(); +#endif +} + +json_const_iterator ObjectEnd(const json& o) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return o.MemberEnd(); +#else + return o.end(); +#endif +} + +// Making this a const char* results in a pointer to a temporary when +// TINYGLTF_USE_RAPIDJSON is off. +std::string GetKey(json_const_iterator& it) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return it->name.GetString(); +#else + return it.key().c_str(); +#endif +} + +bool FindMember(const json& o, const char* member, json_const_iterator& it) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + if (!o.IsObject()) { + return false; + } + it = o.FindMember(member); + return it != o.MemberEnd(); +#else + it = o.find(member); + return it != o.end(); +#endif +} + +const json& GetValue(json_const_iterator& it) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return it->value; +#else + return it.value(); +#endif +} + +std::string JsonToString(const json& o, int spacing = -1) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + using namespace rapidjson; + StringBuffer buffer; + if (spacing == -1) { + Writer writer(buffer); + // TODO: Better error handling. + // https://github.com/syoyo/tinygltf/issues/332 + if (!o.Accept(writer)) { + return "tiny_gltf::JsonToString() failed rapidjson conversion"; + } + } + else { + PrettyWriter writer(buffer); + writer.SetIndent(' ', uint32_t(spacing)); + if (!o.Accept(writer)) { + return "tiny_gltf::JsonToString() failed rapidjson conversion"; + } + } + return buffer.GetString(); +#else + return o.dump(spacing); +#endif +} + +} // namespace + +static bool ParseJsonAsValue(Value* ret, const json& o) +{ + Value val{}; +#ifdef TINYGLTF_USE_RAPIDJSON + using rapidjson::Type; + switch (o.GetType()) { + case Type::kObjectType: { + Value::Object value_object; + for (auto it = o.MemberBegin(); it != o.MemberEnd(); ++it) { + Value entry; + ParseJsonAsValue(&entry, it->value); + if (entry.Type() != NULL_TYPE) + value_object.emplace(GetKey(it), std::move(entry)); + } + if (value_object.size() > 0) + val = Value(std::move(value_object)); + } break; + case Type::kArrayType: { + Value::Array value_array; + value_array.reserve(o.Size()); + for (auto it = o.Begin(); it != o.End(); ++it) { + Value entry; + ParseJsonAsValue(&entry, *it); + if (entry.Type() != NULL_TYPE) + value_array.emplace_back(std::move(entry)); + } + if (value_array.size() > 0) + val = Value(std::move(value_array)); + } break; + case Type::kStringType: + val = Value(std::string(o.GetString())); + break; + case Type::kFalseType: + case Type::kTrueType: + val = Value(o.GetBool()); + break; + case Type::kNumberType: + if (!o.IsDouble()) { + int i = 0; + GetInt(o, i); + val = Value(i); + } + else { + double d = 0.0; + GetDouble(o, d); + val = Value(d); + } + break; + case Type::kNullType: + break; + // all types are covered, so no `case default` + } +#else + switch (o.type()) { + case json::value_t::object: { + Value::Object value_object; + for (auto it = o.begin(); it != o.end(); it++) { + Value entry; + ParseJsonAsValue(&entry, it.value()); + if (entry.Type() != NULL_TYPE) + value_object.emplace(it.key(), std::move(entry)); + } + if (value_object.size() > 0) + val = Value(std::move(value_object)); + } break; + case json::value_t::array: { + Value::Array value_array; + value_array.reserve(o.size()); + for (auto it = o.begin(); it != o.end(); it++) { + Value entry; + ParseJsonAsValue(&entry, it.value()); + if (entry.Type() != NULL_TYPE) + value_array.emplace_back(std::move(entry)); + } + if (value_array.size() > 0) + val = Value(std::move(value_array)); + } break; + case json::value_t::string: + val = Value(o.get()); + break; + case json::value_t::boolean: + val = Value(o.get()); + break; + case json::value_t::number_integer: + case json::value_t::number_unsigned: + val = Value(static_cast(o.get())); + break; + case json::value_t::number_float: + val = Value(o.get()); + break; + case json::value_t::null: + case json::value_t::discarded: + case json::value_t::binary: + // default: + break; + } +#endif + const bool isNotNull = val.Type() != NULL_TYPE; + + if (ret) + *ret = std::move(val); + + return isNotNull; +} + +static bool ParseExtrasProperty(Value* ret, const json& o) +{ + json_const_iterator it; + if (!FindMember(o, "extras", it)) { + return false; + } + + return ParseJsonAsValue(ret, GetValue(it)); +} + +static bool ParseBooleanProperty(bool* ret, std::string* err, const json& o, + const std::string& property, + const bool required, + const std::string& parent_node = "") +{ + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + auto& value = GetValue(it); + + bool isBoolean; + bool boolValue = false; +#ifdef TINYGLTF_USE_RAPIDJSON + isBoolean = value.IsBool(); + if (isBoolean) { + boolValue = value.GetBool(); + } +#else + isBoolean = value.is_boolean(); + if (isBoolean) { + boolValue = value.get(); + } +#endif + if (!isBoolean) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a bool type.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = boolValue; + } + + return true; +} + +static bool ParseIntegerProperty(int* ret, std::string* err, const json& o, + const std::string& property, + const bool required, + const std::string& parent_node = "") +{ + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + int intValue; + bool isInt = GetInt(GetValue(it), intValue); + if (!isInt) { + if (required) { + if (err) { + (*err) += + "'" + property + "' property is not an integer type.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = intValue; + } + + return true; +} + +static bool ParseUnsignedProperty(size_t* ret, std::string* err, const json& o, + const std::string& property, + const bool required, + const std::string& parent_node = "") +{ + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + auto& value = GetValue(it); + + size_t uValue = 0; + bool isUValue; +#ifdef TINYGLTF_USE_RAPIDJSON + isUValue = false; + if (value.IsUint()) { + uValue = value.GetUint(); + isUValue = true; + } + else if (value.IsUint64()) { + uValue = value.GetUint64(); + isUValue = true; + } +#else + isUValue = value.is_number_unsigned(); + if (isUValue) { + uValue = value.get(); + } +#endif + if (!isUValue) { + if (required) { + if (err) { + (*err) += + "'" + property + "' property is not a positive integer.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = uValue; + } + + return true; +} + +static bool ParseNumberProperty(double* ret, std::string* err, const json& o, + const std::string& property, + const bool required, + const std::string& parent_node = "") +{ + json_const_iterator it; + + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + double numberValue; + bool isNumber = GetNumber(GetValue(it), numberValue); + + if (!isNumber) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a number type.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = numberValue; + } + + return true; +} + +static bool ParseNumberArrayProperty(std::vector* ret, std::string* err, + const json& o, const std::string& property, + bool required, + const std::string& parent_node = "") +{ + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + if (!IsArray(GetValue(it))) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not an array"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + ret->clear(); + auto end = ArrayEnd(GetValue(it)); + for (auto i = ArrayBegin(GetValue(it)); i != end; ++i) { + double numberValue; + const bool isNumber = GetNumber(*i, numberValue); + if (!isNumber) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a number.\n"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + ret->push_back(numberValue); + } + + return true; +} + +static bool ParseIntegerArrayProperty(std::vector* ret, std::string* err, + const json& o, + const std::string& property, + bool required, + const std::string& parent_node = "") +{ + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + if (!IsArray(GetValue(it))) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not an array"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + + ret->clear(); + auto end = ArrayEnd(GetValue(it)); + for (auto i = ArrayBegin(GetValue(it)); i != end; ++i) { + int numberValue; + bool isNumber = GetInt(*i, numberValue); + if (!isNumber) { + if (required) { + if (err) { + (*err) += + "'" + property + "' property is not an integer type.\n"; + if (!parent_node.empty()) { + (*err) += " in " + parent_node; + } + (*err) += ".\n"; + } + } + return false; + } + ret->push_back(numberValue); + } + + return true; +} + +static bool ParseStringProperty(std::string* ret, std::string* err, + const json& o, const std::string& property, + bool required, + const std::string& parent_node = std::string()) +{ + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing"; + if (parent_node.empty()) { + (*err) += ".\n"; + } + else { + (*err) += " in `" + parent_node + "'.\n"; + } + } + } + return false; + } + + std::string strValue; + if (!GetString(GetValue(it), strValue)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a string type.\n"; + } + } + return false; + } + + if (ret) { + (*ret) = std::move(strValue); + } + + return true; +} + +static bool ParseStringIntegerProperty(std::map* ret, + std::string* err, const json& o, + const std::string& property, + bool required, + const std::string& parent = "") +{ + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + if (!parent.empty()) { + (*err) += "'" + property + "' property is missing in " + + parent + ".\n"; + } + else { + (*err) += "'" + property + "' property is missing.\n"; + } + } + } + return false; + } + + const json& dict = GetValue(it); + + // Make sure we are dealing with an object / dictionary. + if (!IsObject(dict)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not an object.\n"; + } + } + return false; + } + + ret->clear(); + + json_const_iterator dictIt(ObjectBegin(dict)); + json_const_iterator dictItEnd(ObjectEnd(dict)); + + for (; dictIt != dictItEnd; ++dictIt) { + int intVal; + if (!GetInt(GetValue(dictIt), intVal)) { + if (required) { + if (err) { + (*err) += + "'" + property + "' value is not an integer type.\n"; + } + } + return false; + } + + // Insert into the list. + (*ret)[GetKey(dictIt)] = intVal; + } + return true; +} + +static bool ParseJSONProperty(std::map* ret, + std::string* err, const json& o, + const std::string& property, bool required) +{ + json_const_iterator it; + if (!FindMember(o, property.c_str(), it)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is missing. \n'"; + } + } + return false; + } + + const json& obj = GetValue(it); + + if (!IsObject(obj)) { + if (required) { + if (err) { + (*err) += "'" + property + "' property is not a JSON object.\n"; + } + } + return false; + } + + ret->clear(); + + json_const_iterator it2(ObjectBegin(obj)); + json_const_iterator itEnd(ObjectEnd(obj)); + for (; it2 != itEnd; ++it2) { + double numVal; + if (GetNumber(GetValue(it2), numVal)) + ret->emplace(std::string(GetKey(it2)), numVal); + } + + return true; +} + +static bool ParseParameterProperty(Parameter* param, std::string* err, + const json& o, const std::string& prop, + bool required) +{ + // A parameter value can either be a string or an array of either a boolean + // or a number. Booleans of any kind aren't supported here. Granted, it + // complicates the Parameter structure and breaks it semantically in the + // sense that the client probably works off the assumption that if the + // string is empty the vector is used, etc. Would a tagged union work? + if (ParseStringProperty(¶m->string_value, err, o, prop, false)) { + // Found string property. + return true; + } + else if (ParseNumberArrayProperty(¶m->number_array, err, o, prop, + false)) { + // Found a number array. + return true; + } + else if (ParseNumberProperty(¶m->number_value, err, o, prop, false)) { + param->has_number_value = true; + return true; + } + else if (ParseJSONProperty(¶m->json_double_value, err, o, prop, + false)) { + return true; + } + else if (ParseBooleanProperty(¶m->bool_value, err, o, prop, false)) { + return true; + } + else { + if (required) { + if (err) { + (*err) += + "parameter must be a string or number / number array.\n"; + } + } + return false; + } +} + +static bool ParseExtensionsProperty(ExtensionMap* ret, std::string* err, + const json& o) +{ + (void)err; + + json_const_iterator it; + if (!FindMember(o, "extensions", it)) { + return false; + } + + auto& obj = GetValue(it); + if (!IsObject(obj)) { + return false; + } + ExtensionMap extensions; + json_const_iterator extIt = ObjectBegin(obj); // it.value().begin(); + json_const_iterator extEnd = ObjectEnd(obj); + for (; extIt != extEnd; ++extIt) { + auto& itObj = GetValue(extIt); + if (!IsObject(itObj)) + continue; + std::string key(GetKey(extIt)); + if (!ParseJsonAsValue(&extensions[key], itObj)) { + if (!key.empty()) { + // create empty object so that an extension object is still of + // type object + extensions[key] = Value{Value::Object{}}; + } + } + } + if (ret) { + (*ret) = std::move(extensions); + } + return true; +} + +static bool ParseAsset(Asset* asset, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + ParseStringProperty(&asset->version, err, o, "version", true, "Asset"); + ParseStringProperty(&asset->generator, err, o, "generator", false, "Asset"); + ParseStringProperty(&asset->minVersion, err, o, "minVersion", false, + "Asset"); + ParseStringProperty(&asset->copyright, err, o, "copyright", false, "Asset"); + + ParseExtensionsProperty(&asset->extensions, err, o); + + // Unity exporter version is added as extra here + ParseExtrasProperty(&(asset->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + asset->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + asset->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseImage(Image* image, const int image_idx, std::string* err, + std::string* warn, const json& o, + bool store_original_json_for_extras_and_extensions, + const std::string& basedir, FsCallbacks* fs, + LoadImageDataFunction* LoadImageData = nullptr, + void* load_image_user_data = nullptr) +{ + // A glTF image must either reference a bufferView or an image uri + + // schema says oneOf [`bufferView`, `uri`] + // TODO(syoyo): Check the type of each parameters. + json_const_iterator it; + bool hasBufferView = FindMember(o, "bufferView", it); + bool hasURI = FindMember(o, "uri", it); + + ParseStringProperty(&image->name, err, o, "name", false); + + if (hasBufferView && hasURI) { + // Should not both defined. + if (err) { + (*err) += "Only one of `bufferView` or `uri` should be defined, " + "but both are " + "defined for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\"\n"; + } + return false; + } + + if (!hasBufferView && !hasURI) { + if (err) { + (*err) += + "Neither required `bufferView` nor `uri` defined for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\"\n"; + } + return false; + } + + ParseExtensionsProperty(&image->extensions, err, o); + ParseExtrasProperty(&image->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator eit; + if (FindMember(o, "extensions", eit)) { + image->extensions_json_string = JsonToString(GetValue(eit)); + } + } + { + json_const_iterator eit; + if (FindMember(o, "extras", eit)) { + image->extras_json_string = JsonToString(GetValue(eit)); + } + } + } + + if (hasBufferView) { + int bufferView = -1; + if (!ParseIntegerProperty(&bufferView, err, o, "bufferView", true)) { + if (err) { + (*err) += "Failed to parse `bufferView` for image[" + + std::to_string(image_idx) + "] name = \"" + + image->name + "\"\n"; + } + return false; + } + + std::string mime_type; + ParseStringProperty(&mime_type, err, o, "mimeType", false); + + int width = 0; + ParseIntegerProperty(&width, err, o, "width", false); + + int height = 0; + ParseIntegerProperty(&height, err, o, "height", false); + + // Just only save some information here. Loading actual image data from + // bufferView is done after this `ParseImage` function. + image->bufferView = bufferView; + image->mimeType = mime_type; + image->width = width; + image->height = height; + + return true; + } + + // Parse URI & Load image data. + + std::string uri; + std::string tmp_err; + if (!ParseStringProperty(&uri, &tmp_err, o, "uri", true)) { + if (err) { + (*err) += "Failed to parse `uri` for image[" + + std::to_string(image_idx) + "] name = \"" + image->name + + "\".\n"; + } + return false; + } + + std::vector img; + + if (IsDataURI(uri)) { + if (!DecodeDataURI(&img, image->mimeType, uri, 0, false)) { + if (err) { + (*err) += "Failed to decode 'uri' for image[" + + std::to_string(image_idx) + "] name = [" + + image->name + "]\n"; + } + return false; + } + } + else { + // Assume external file + // Keep texture path (for textures that cannot be decoded) + image->uri = uri; +#ifdef TINYGLTF_NO_EXTERNAL_IMAGE + return true; +#else + std::string decoded_uri = dlib::urldecode(uri); + if (!LoadExternalFile(&img, err, warn, decoded_uri, basedir, + /* required */ false, /* required bytes */ 0, + /* checksize */ false, fs)) { + if (warn) { + (*warn) += "Failed to load external 'uri' for image[" + + std::to_string(image_idx) + "] name = [" + + image->name + "]\n"; + } + // If the image cannot be loaded, keep uri as image->uri. + return true; + } + + if (img.empty()) { + if (warn) { + (*warn) += "Image data is empty for image[" + + std::to_string(image_idx) + "] name = [" + + image->name + "] \n"; + } + return false; + } +#endif + } + + if (*LoadImageData == nullptr) { + if (err) { + (*err) += "No LoadImageData callback specified.\n"; + } + return false; + } + return (*LoadImageData)(image, image_idx, err, warn, 0, 0, &img.at(0), + static_cast(img.size()), load_image_user_data); +} + +static bool ParseTexture(Texture* texture, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions, + const std::string& basedir) +{ + (void)basedir; + int sampler = -1; + int source = -1; + ParseIntegerProperty(&sampler, err, o, "sampler", false); + + ParseIntegerProperty(&source, err, o, "source", false); + + texture->sampler = sampler; + texture->source = source; + + ParseExtensionsProperty(&texture->extensions, err, o); + ParseExtrasProperty(&texture->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + texture->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + texture->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + ParseStringProperty(&texture->name, err, o, "name", false); + + return true; +} + +static bool ParseTextureInfo(TextureInfo* texinfo, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + if (texinfo == nullptr) { + return false; + } + + if (!ParseIntegerProperty(&texinfo->index, err, o, "index", + /* required */ true, "TextureInfo")) { + return false; + } + + ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); + + ParseExtensionsProperty(&texinfo->extensions, err, o); + ParseExtrasProperty(&texinfo->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + texinfo->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + texinfo->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool +ParseNormalTextureInfo(NormalTextureInfo* texinfo, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + if (texinfo == nullptr) { + return false; + } + + if (!ParseIntegerProperty(&texinfo->index, err, o, "index", + /* required */ true, "NormalTextureInfo")) { + return false; + } + + ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); + ParseNumberProperty(&texinfo->scale, err, o, "scale", false); + + ParseExtensionsProperty(&texinfo->extensions, err, o); + ParseExtrasProperty(&texinfo->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + texinfo->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + texinfo->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool +ParseOcclusionTextureInfo(OcclusionTextureInfo* texinfo, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + if (texinfo == nullptr) { + return false; + } + + if (!ParseIntegerProperty(&texinfo->index, err, o, "index", + /* required */ true, "NormalTextureInfo")) { + return false; + } + + ParseIntegerProperty(&texinfo->texCoord, err, o, "texCoord", false); + ParseNumberProperty(&texinfo->strength, err, o, "strength", false); + + ParseExtensionsProperty(&texinfo->extensions, err, o); + ParseExtrasProperty(&texinfo->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + texinfo->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + texinfo->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseBuffer(Buffer* buffer, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions, + FsCallbacks* fs, const std::string& basedir, + bool is_binary = false, + const unsigned char* bin_data = nullptr, + size_t bin_size = 0) +{ + size_t byteLength; + if (!ParseUnsignedProperty(&byteLength, err, o, "byteLength", true, + "Buffer")) { + return false; + } + + // In glTF 2.0, uri is not mandatory anymore + buffer->uri.clear(); + ParseStringProperty(&buffer->uri, err, o, "uri", false, "Buffer"); + + // having an empty uri for a non embedded image should not be valid + if (!is_binary && buffer->uri.empty()) { + if (err) { + (*err) += "'uri' is missing from non binary glTF file buffer.\n"; + } + } + + json_const_iterator type; + if (FindMember(o, "type", type)) { + std::string typeStr; + if (GetString(GetValue(type), typeStr)) { + if (typeStr.compare("arraybuffer") == 0) { + // buffer.type = "arraybuffer"; + } + } + } + + if (is_binary) { + // Still binary glTF accepts external dataURI. + if (!buffer->uri.empty()) { + // First try embedded data URI. + if (IsDataURI(buffer->uri)) { + std::string mime_type; + if (!DecodeDataURI(&buffer->data, mime_type, buffer->uri, + byteLength, true)) { + if (err) { + (*err) += "Failed to decode 'uri' : " + buffer->uri + + " in Buffer\n"; + } + return false; + } + } + else { + // External .bin file. + std::string decoded_uri = dlib::urldecode(buffer->uri); + if (!LoadExternalFile(&buffer->data, err, /* warn */ nullptr, + decoded_uri, basedir, /* required */ true, + byteLength, /* checkSize */ true, fs)) { + return false; + } + } + } + else { + // load data from (embedded) binary data + + if ((bin_size == 0) || (bin_data == nullptr)) { + if (err) { + (*err) += "Invalid binary data in `Buffer', or GLB with " + "empty BIN chunk.\n"; + } + return false; + } + + if (byteLength > bin_size) { + if (err) { + std::stringstream ss; + ss << "Invalid `byteLength'. Must be equal or less than " + "binary size: " + "`byteLength' = " + << byteLength << ", binary size = " << bin_size + << std::endl; + (*err) += ss.str(); + } + return false; + } + + // Read buffer data + buffer->data.resize(static_cast(byteLength)); + memcpy(&(buffer->data.at(0)), bin_data, + static_cast(byteLength)); + } + } + else { + if (IsDataURI(buffer->uri)) { + std::string mime_type; + if (!DecodeDataURI(&buffer->data, mime_type, buffer->uri, + byteLength, true)) { + if (err) { + (*err) += "Failed to decode 'uri' : " + buffer->uri + + " in Buffer\n"; + } + return false; + } + } + else { + // Assume external .bin file. + std::string decoded_uri = dlib::urldecode(buffer->uri); + if (!LoadExternalFile(&buffer->data, err, /* warn */ nullptr, + decoded_uri, basedir, /* required */ true, + byteLength, + /* checkSize */ true, fs)) { + return false; + } + } + } + + ParseStringProperty(&buffer->name, err, o, "name", false); + + ParseExtensionsProperty(&buffer->extensions, err, o); + ParseExtrasProperty(&buffer->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + buffer->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + buffer->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseBufferView(BufferView* bufferView, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + int buffer = -1; + if (!ParseIntegerProperty(&buffer, err, o, "buffer", true, "BufferView")) { + return false; + } + + size_t byteOffset = 0; + ParseUnsignedProperty(&byteOffset, err, o, "byteOffset", false); + + size_t byteLength = 1; + if (!ParseUnsignedProperty(&byteLength, err, o, "byteLength", true, + "BufferView")) { + return false; + } + + size_t byteStride = 0; + if (!ParseUnsignedProperty(&byteStride, err, o, "byteStride", false)) { + // Spec says: When byteStride of referenced bufferView is not defined, + // it means that accessor elements are tightly packed, i.e., effective + // stride equals the size of the element. We cannot determine the actual + // byteStride until Accessor are parsed, thus set 0(= tightly packed) + // here(as done in OpenGL's VertexAttribPoiner) + byteStride = 0; + } + + if ((byteStride > 252) || ((byteStride % 4) != 0)) { + if (err) { + std::stringstream ss; + ss << "Invalid `byteStride' value. `byteStride' must be the " + "multiple of " + "4 : " + << byteStride << std::endl; + + (*err) += ss.str(); + } + return false; + } + + int target = 0; + ParseIntegerProperty(&target, err, o, "target", false); + if ((target == TINYGLTF_TARGET_ARRAY_BUFFER) || + (target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER)) { + // OK + } + else { + target = 0; + } + bufferView->target = target; + + ParseStringProperty(&bufferView->name, err, o, "name", false); + + ParseExtensionsProperty(&bufferView->extensions, err, o); + ParseExtrasProperty(&bufferView->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + bufferView->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + bufferView->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + bufferView->buffer = buffer; + bufferView->byteOffset = byteOffset; + bufferView->byteLength = byteLength; + bufferView->byteStride = byteStride; + return true; +} + +static bool ParseSparseAccessor(Accessor* accessor, std::string* err, + const json& o) +{ + accessor->sparse.isSparse = true; + + int count = 0; + if (!ParseIntegerProperty(&count, err, o, "count", true, + "SparseAccessor")) { + return false; + } + + json_const_iterator indices_iterator; + json_const_iterator values_iterator; + if (!FindMember(o, "indices", indices_iterator)) { + (*err) = "the sparse object of this accessor doesn't have indices"; + return false; + } + + if (!FindMember(o, "values", values_iterator)) { + (*err) = "the sparse object of this accessor doesn't have values"; + return false; + } + + const json& indices_obj = GetValue(indices_iterator); + const json& values_obj = GetValue(values_iterator); + + int indices_buffer_view = 0, indices_byte_offset = 0, component_type = 0; + if (!ParseIntegerProperty(&indices_buffer_view, err, indices_obj, + "bufferView", true, "SparseAccessor")) { + return false; + } + ParseIntegerProperty(&indices_byte_offset, err, indices_obj, "byteOffset", + false); + if (!ParseIntegerProperty(&component_type, err, indices_obj, + "componentType", true, "SparseAccessor")) { + return false; + } + + int values_buffer_view = 0, values_byte_offset = 0; + if (!ParseIntegerProperty(&values_buffer_view, err, values_obj, + "bufferView", true, "SparseAccessor")) { + return false; + } + ParseIntegerProperty(&values_byte_offset, err, values_obj, "byteOffset", + false); + + accessor->sparse.count = count; + accessor->sparse.indices.bufferView = indices_buffer_view; + accessor->sparse.indices.byteOffset = indices_byte_offset; + accessor->sparse.indices.componentType = component_type; + accessor->sparse.values.bufferView = values_buffer_view; + accessor->sparse.values.byteOffset = values_byte_offset; + + return true; +} + +static bool ParseAccessor(Accessor* accessor, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + int bufferView = -1; + ParseIntegerProperty(&bufferView, err, o, "bufferView", false, "Accessor"); + + size_t byteOffset = 0; + ParseUnsignedProperty(&byteOffset, err, o, "byteOffset", false, "Accessor"); + + bool normalized = false; + ParseBooleanProperty(&normalized, err, o, "normalized", false, "Accessor"); + + size_t componentType = 0; + if (!ParseUnsignedProperty(&componentType, err, o, "componentType", true, + "Accessor")) { + return false; + } + + size_t count = 0; + if (!ParseUnsignedProperty(&count, err, o, "count", true, "Accessor")) { + return false; + } + + std::string type; + if (!ParseStringProperty(&type, err, o, "type", true, "Accessor")) { + return false; + } + + if (type.compare("SCALAR") == 0) { + accessor->type = TINYGLTF_TYPE_SCALAR; + } + else if (type.compare("VEC2") == 0) { + accessor->type = TINYGLTF_TYPE_VEC2; + } + else if (type.compare("VEC3") == 0) { + accessor->type = TINYGLTF_TYPE_VEC3; + } + else if (type.compare("VEC4") == 0) { + accessor->type = TINYGLTF_TYPE_VEC4; + } + else if (type.compare("MAT2") == 0) { + accessor->type = TINYGLTF_TYPE_MAT2; + } + else if (type.compare("MAT3") == 0) { + accessor->type = TINYGLTF_TYPE_MAT3; + } + else if (type.compare("MAT4") == 0) { + accessor->type = TINYGLTF_TYPE_MAT4; + } + else { + std::stringstream ss; + ss << "Unsupported `type` for accessor object. Got \"" << type + << "\"\n"; + if (err) { + (*err) += ss.str(); + } + return false; + } + + ParseStringProperty(&accessor->name, err, o, "name", false); + + accessor->minValues.clear(); + accessor->maxValues.clear(); + ParseNumberArrayProperty(&accessor->minValues, err, o, "min", false, + "Accessor"); + + ParseNumberArrayProperty(&accessor->maxValues, err, o, "max", false, + "Accessor"); + + accessor->count = count; + accessor->bufferView = bufferView; + accessor->byteOffset = byteOffset; + accessor->normalized = normalized; + { + if (componentType >= TINYGLTF_COMPONENT_TYPE_BYTE && + componentType <= TINYGLTF_COMPONENT_TYPE_DOUBLE) { + // OK + accessor->componentType = int(componentType); + } + else { + std::stringstream ss; + ss << "Invalid `componentType` in accessor. Got " << componentType + << "\n"; + if (err) { + (*err) += ss.str(); + } + return false; + } + } + + ParseExtensionsProperty(&(accessor->extensions), err, o); + ParseExtrasProperty(&(accessor->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + accessor->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + accessor->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + // check if accessor has a "sparse" object: + json_const_iterator iterator; + if (FindMember(o, "sparse", iterator)) { + // here this accessor has a "sparse" subobject + return ParseSparseAccessor(accessor, err, GetValue(iterator)); + } + + return true; +} + +#ifdef TINYGLTF_ENABLE_DRACO + +static void DecodeIndexBuffer(draco::Mesh* mesh, size_t componentSize, + std::vector& outBuffer) +{ + if (componentSize == 4) { + assert(sizeof(mesh->face(draco::FaceIndex(0))[0]) == componentSize); + memcpy(outBuffer.data(), &mesh->face(draco::FaceIndex(0))[0], + outBuffer.size()); + } + else { + size_t faceStride = componentSize * 3; + for (draco::FaceIndex f(0); f < mesh->num_faces(); ++f) { + const draco::Mesh::Face& face = mesh->face(f); + if (componentSize == 2) { + uint16_t indices[3] = {(uint16_t)face[0].value(), + (uint16_t)face[1].value(), + (uint16_t)face[2].value()}; + memcpy(outBuffer.data() + f.value() * faceStride, &indices[0], + faceStride); + } + else { + uint8_t indices[3] = {(uint8_t)face[0].value(), + (uint8_t)face[1].value(), + (uint8_t)face[2].value()}; + memcpy(outBuffer.data() + f.value() * faceStride, &indices[0], + faceStride); + } + } + } +} + +template +static bool GetAttributeForAllPoints(draco::Mesh* mesh, + const draco::PointAttribute* pAttribute, + std::vector& outBuffer) +{ + size_t byteOffset = 0; + T values[4] = {0, 0, 0, 0}; + for (draco::PointIndex i(0); i < mesh->num_points(); ++i) { + const draco::AttributeValueIndex val_index = + pAttribute->mapped_index(i); + if (!pAttribute->ConvertValue(val_index, + pAttribute->num_components(), values)) + return false; + + memcpy(outBuffer.data() + byteOffset, &values[0], + sizeof(T) * pAttribute->num_components()); + byteOffset += sizeof(T) * pAttribute->num_components(); + } + + return true; +} + +static bool GetAttributeForAllPoints(uint32_t componentType, draco::Mesh* mesh, + const draco::PointAttribute* pAttribute, + std::vector& outBuffer) +{ + bool decodeResult = false; + switch (componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_BYTE: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_SHORT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_INT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + case TINYGLTF_COMPONENT_TYPE_DOUBLE: + decodeResult = + GetAttributeForAllPoints(mesh, pAttribute, outBuffer); + break; + default: + return false; + } + + return decodeResult; +} + +static bool ParseDracoExtension(Primitive* primitive, Model* model, + std::string* err, + const Value& dracoExtensionValue) +{ + (void)err; + auto bufferViewValue = dracoExtensionValue.Get("bufferView"); + if (!bufferViewValue.IsInt()) + return false; + auto attributesValue = dracoExtensionValue.Get("attributes"); + if (!attributesValue.IsObject()) + return false; + + auto attributesObject = attributesValue.Get(); + int bufferView = bufferViewValue.Get(); + + BufferView& view = model->bufferViews[bufferView]; + Buffer& buffer = model->buffers[view.buffer]; + // BufferView has already been decoded + if (view.dracoDecoded) + return true; + view.dracoDecoded = true; + + const char* bufferViewData = + reinterpret_cast(buffer.data.data() + view.byteOffset); + size_t bufferViewSize = view.byteLength; + + // decode draco + draco::DecoderBuffer decoderBuffer; + decoderBuffer.Init(bufferViewData, bufferViewSize); + draco::Decoder decoder; + auto decodeResult = decoder.DecodeMeshFromBuffer(&decoderBuffer); + if (!decodeResult.ok()) { + return false; + } + const std::unique_ptr& mesh = decodeResult.value(); + + // create new bufferView for indices + if (primitive->indices >= 0) { + int32_t componentSize = GetComponentSizeInBytes( + model->accessors[primitive->indices].componentType); + Buffer decodedIndexBuffer; + decodedIndexBuffer.data.resize(mesh->num_faces() * 3 * componentSize); + + DecodeIndexBuffer(mesh.get(), componentSize, decodedIndexBuffer.data); + + model->buffers.emplace_back(std::move(decodedIndexBuffer)); + + BufferView decodedIndexBufferView; + decodedIndexBufferView.buffer = int(model->buffers.size() - 1); + decodedIndexBufferView.byteLength = + int(mesh->num_faces() * 3 * componentSize); + decodedIndexBufferView.byteOffset = 0; + decodedIndexBufferView.byteStride = 0; + decodedIndexBufferView.target = TINYGLTF_TARGET_ARRAY_BUFFER; + model->bufferViews.emplace_back(std::move(decodedIndexBufferView)); + + model->accessors[primitive->indices].bufferView = + int(model->bufferViews.size() - 1); + model->accessors[primitive->indices].count = int(mesh->num_faces() * 3); + } + + for (const auto& attribute : attributesObject) { + if (!attribute.second.IsInt()) + return false; + auto primitiveAttribute = primitive->attributes.find(attribute.first); + if (primitiveAttribute == primitive->attributes.end()) + return false; + + int dracoAttributeIndex = attribute.second.Get(); + const auto pAttribute = + mesh->GetAttributeByUniqueId(dracoAttributeIndex); + const auto componentType = + model->accessors[primitiveAttribute->second].componentType; + + // Create a new buffer for this decoded buffer + Buffer decodedBuffer; + size_t bufferSize = mesh->num_points() * pAttribute->num_components() * + GetComponentSizeInBytes(componentType); + decodedBuffer.data.resize(bufferSize); + + if (!GetAttributeForAllPoints(componentType, mesh.get(), pAttribute, + decodedBuffer.data)) + return false; + + model->buffers.emplace_back(std::move(decodedBuffer)); + + BufferView decodedBufferView; + decodedBufferView.buffer = int(model->buffers.size() - 1); + decodedBufferView.byteLength = bufferSize; + decodedBufferView.byteOffset = pAttribute->byte_offset(); + decodedBufferView.byteStride = pAttribute->byte_stride(); + decodedBufferView.target = primitive->indices >= 0 + ? TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER + : TINYGLTF_TARGET_ARRAY_BUFFER; + model->bufferViews.emplace_back(std::move(decodedBufferView)); + + model->accessors[primitiveAttribute->second].bufferView = + int(model->bufferViews.size() - 1); + model->accessors[primitiveAttribute->second].count = + int(mesh->num_points()); + } + + return true; +} +#endif + +static bool ParsePrimitive(Primitive* primitive, Model* model, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + int material = -1; + ParseIntegerProperty(&material, err, o, "material", false); + primitive->material = material; + + int mode = TINYGLTF_MODE_TRIANGLES; + ParseIntegerProperty(&mode, err, o, "mode", false); + primitive->mode = mode; // Why only triangles were supported ? + + int indices = -1; + ParseIntegerProperty(&indices, err, o, "indices", false); + primitive->indices = indices; + if (!ParseStringIntegerProperty(&primitive->attributes, err, o, + "attributes", true, "Primitive")) { + return false; + } + + // Look for morph targets + json_const_iterator targetsObject; + if (FindMember(o, "targets", targetsObject) && + IsArray(GetValue(targetsObject))) { + auto targetsObjectEnd = ArrayEnd(GetValue(targetsObject)); + for (json_const_array_iterator i = ArrayBegin(GetValue(targetsObject)); + i != targetsObjectEnd; ++i) { + std::map targetAttribues; + + const json& dict = *i; + if (IsObject(dict)) { + json_const_iterator dictIt(ObjectBegin(dict)); + json_const_iterator dictItEnd(ObjectEnd(dict)); + + for (; dictIt != dictItEnd; ++dictIt) { + int iVal; + if (GetInt(GetValue(dictIt), iVal)) + targetAttribues[GetKey(dictIt)] = iVal; + } + primitive->targets.emplace_back(std::move(targetAttribues)); + } + } + } + + ParseExtrasProperty(&(primitive->extras), o); + ParseExtensionsProperty(&primitive->extensions, err, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + primitive->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + primitive->extras_json_string = JsonToString(GetValue(it)); + } + } + } + +#ifdef TINYGLTF_ENABLE_DRACO + auto dracoExtension = + primitive->extensions.find("KHR_draco_mesh_compression"); + if (dracoExtension != primitive->extensions.end()) { + ParseDracoExtension(primitive, model, err, dracoExtension->second); + } +#else + (void)model; +#endif + + return true; +} + +static bool ParseMesh(Mesh* mesh, Model* model, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + ParseStringProperty(&mesh->name, err, o, "name", false); + + mesh->primitives.clear(); + json_const_iterator primObject; + if (FindMember(o, "primitives", primObject) && + IsArray(GetValue(primObject))) { + json_const_array_iterator primEnd = ArrayEnd(GetValue(primObject)); + for (json_const_array_iterator i = ArrayBegin(GetValue(primObject)); + i != primEnd; ++i) { + Primitive primitive; + if (ParsePrimitive(&primitive, model, err, *i, + store_original_json_for_extras_and_extensions)) { + // Only add the primitive if the parsing succeeds. + mesh->primitives.emplace_back(std::move(primitive)); + } + } + } + + // Should probably check if has targets and if dimensions fit + ParseNumberArrayProperty(&mesh->weights, err, o, "weights", false); + + ParseExtensionsProperty(&mesh->extensions, err, o); + ParseExtrasProperty(&(mesh->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + mesh->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + mesh->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseNode(Node* node, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + ParseStringProperty(&node->name, err, o, "name", false); + + int skin = -1; + ParseIntegerProperty(&skin, err, o, "skin", false); + node->skin = skin; + + // Matrix and T/R/S are exclusive + if (!ParseNumberArrayProperty(&node->matrix, err, o, "matrix", false)) { + ParseNumberArrayProperty(&node->rotation, err, o, "rotation", false); + ParseNumberArrayProperty(&node->scale, err, o, "scale", false); + ParseNumberArrayProperty(&node->translation, err, o, "translation", + false); + } + + int camera = -1; + ParseIntegerProperty(&camera, err, o, "camera", false); + node->camera = camera; + + int mesh = -1; + ParseIntegerProperty(&mesh, err, o, "mesh", false); + node->mesh = mesh; + + node->children.clear(); + ParseIntegerArrayProperty(&node->children, err, o, "children", false); + + ParseNumberArrayProperty(&node->weights, err, o, "weights", false); + + ParseExtensionsProperty(&node->extensions, err, o); + ParseExtrasProperty(&(node->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + node->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + node->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool +ParsePbrMetallicRoughness(PbrMetallicRoughness* pbr, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + if (pbr == nullptr) { + return false; + } + + std::vector baseColorFactor; + if (ParseNumberArrayProperty(&baseColorFactor, err, o, "baseColorFactor", + /* required */ false)) { + if (baseColorFactor.size() != 4) { + if (err) { + (*err) += "Array length of `baseColorFactor` parameter in " + "pbrMetallicRoughness must be 4, but got " + + std::to_string(baseColorFactor.size()) + "\n"; + } + return false; + } + pbr->baseColorFactor = baseColorFactor; + } + + { + json_const_iterator it; + if (FindMember(o, "baseColorTexture", it)) { + ParseTextureInfo(&pbr->baseColorTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + { + json_const_iterator it; + if (FindMember(o, "metallicRoughnessTexture", it)) { + ParseTextureInfo(&pbr->metallicRoughnessTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + ParseNumberProperty(&pbr->metallicFactor, err, o, "metallicFactor", false); + ParseNumberProperty(&pbr->roughnessFactor, err, o, "roughnessFactor", + false); + + ParseExtensionsProperty(&pbr->extensions, err, o); + ParseExtrasProperty(&pbr->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + pbr->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + pbr->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseMaterial(Material* material, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + ParseStringProperty(&material->name, err, o, "name", /* required */ false); + + if (ParseNumberArrayProperty(&material->emissiveFactor, err, o, + "emissiveFactor", + /* required */ false)) { + if (material->emissiveFactor.size() != 3) { + if (err) { + (*err) += "Array length of `emissiveFactor` parameter in " + "material must be 3, but got " + + std::to_string(material->emissiveFactor.size()) + + "\n"; + } + return false; + } + } + else { + // fill with default values + material->emissiveFactor = {0.0, 0.0, 0.0}; + } + + ParseStringProperty(&material->alphaMode, err, o, "alphaMode", + /* required */ false); + ParseNumberProperty(&material->alphaCutoff, err, o, "alphaCutoff", + /* required */ false); + ParseBooleanProperty(&material->doubleSided, err, o, "doubleSided", + /* required */ false); + + { + json_const_iterator it; + if (FindMember(o, "pbrMetallicRoughness", it)) { + ParsePbrMetallicRoughness( + &material->pbrMetallicRoughness, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + { + json_const_iterator it; + if (FindMember(o, "normalTexture", it)) { + ParseNormalTextureInfo( + &material->normalTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + { + json_const_iterator it; + if (FindMember(o, "occlusionTexture", it)) { + ParseOcclusionTextureInfo( + &material->occlusionTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + { + json_const_iterator it; + if (FindMember(o, "emissiveTexture", it)) { + ParseTextureInfo(&material->emissiveTexture, err, GetValue(it), + store_original_json_for_extras_and_extensions); + } + } + + // Old code path. For backward compatibility, we still store material values + // as Parameter. This will create duplicated information for + // example(pbrMetallicRoughness), but should be negligible in terms of + // memory consumption. + // TODO(syoyo): Remove in the next major release. + material->values.clear(); + material->additionalValues.clear(); + + json_const_iterator it(ObjectBegin(o)); + json_const_iterator itEnd(ObjectEnd(o)); + + for (; it != itEnd; ++it) { + std::string key(GetKey(it)); + if (key == "pbrMetallicRoughness") { + if (IsObject(GetValue(it))) { + const json& values_object = GetValue(it); + + json_const_iterator itVal(ObjectBegin(values_object)); + json_const_iterator itValEnd(ObjectEnd(values_object)); + + for (; itVal != itValEnd; ++itVal) { + Parameter param; + if (ParseParameterProperty(¶m, err, values_object, + GetKey(itVal), false)) { + material->values.emplace(GetKey(itVal), + std::move(param)); + } + } + } + } + else if (key == "extensions" || key == "extras") { + // done later, skip, otherwise poorly parsed contents will be saved + // in the parametermap and serialized again later + } + else { + Parameter param; + if (ParseParameterProperty(¶m, err, o, key, false)) { + // names of materials have already been parsed. Putting it in + // this map doesn't correctly reflect the glTF specification + if (key != "name") + material->additionalValues.emplace(std::move(key), + std::move(param)); + } + } + } + + material->extensions.clear(); + ParseExtensionsProperty(&material->extensions, err, o); + ParseExtrasProperty(&(material->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator eit; + if (FindMember(o, "extensions", eit)) { + material->extensions_json_string = JsonToString(GetValue(eit)); + } + } + { + json_const_iterator eit; + if (FindMember(o, "extras", eit)) { + material->extras_json_string = JsonToString(GetValue(eit)); + } + } + } + + return true; +} + +static bool +ParseAnimationChannel(AnimationChannel* channel, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + int samplerIndex = -1; + int targetIndex = -1; + if (!ParseIntegerProperty(&samplerIndex, err, o, "sampler", true, + "AnimationChannel")) { + if (err) { + (*err) += "`sampler` field is missing in animation channels\n"; + } + return false; + } + + json_const_iterator targetIt; + if (FindMember(o, "target", targetIt) && IsObject(GetValue(targetIt))) { + const json& target_object = GetValue(targetIt); + + if (!ParseIntegerProperty(&targetIndex, err, target_object, "node", + true)) { + if (err) { + (*err) += + "`node` field is missing in animation.channels.target\n"; + } + return false; + } + + if (!ParseStringProperty(&channel->target_path, err, target_object, + "path", true)) { + if (err) { + (*err) += + "`path` field is missing in animation.channels.target\n"; + } + return false; + } + ParseExtensionsProperty(&channel->target_extensions, err, + target_object); + if (store_original_json_for_extras_and_extensions) { + json_const_iterator it; + if (FindMember(target_object, "extensions", it)) { + channel->target_extensions_json_string = + JsonToString(GetValue(it)); + } + } + } + + channel->sampler = samplerIndex; + channel->target_node = targetIndex; + + ParseExtensionsProperty(&channel->extensions, err, o); + ParseExtrasProperty(&(channel->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + channel->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + channel->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseAnimation(Animation* animation, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + { + json_const_iterator channelsIt; + if (FindMember(o, "channels", channelsIt) && + IsArray(GetValue(channelsIt))) { + json_const_array_iterator channelEnd = + ArrayEnd(GetValue(channelsIt)); + for (json_const_array_iterator i = ArrayBegin(GetValue(channelsIt)); + i != channelEnd; ++i) { + AnimationChannel channel; + if (ParseAnimationChannel( + &channel, err, *i, + store_original_json_for_extras_and_extensions)) { + // Only add the channel if the parsing succeeds. + animation->channels.emplace_back(std::move(channel)); + } + } + } + } + + { + json_const_iterator samplerIt; + if (FindMember(o, "samplers", samplerIt) && + IsArray(GetValue(samplerIt))) { + const json& sampler_array = GetValue(samplerIt); + + json_const_array_iterator it = ArrayBegin(sampler_array); + json_const_array_iterator itEnd = ArrayEnd(sampler_array); + + for (; it != itEnd; ++it) { + const json& s = *it; + + AnimationSampler sampler; + int inputIndex = -1; + int outputIndex = -1; + if (!ParseIntegerProperty(&inputIndex, err, s, "input", true)) { + if (err) { + (*err) += + "`input` field is missing in animation.sampler\n"; + } + return false; + } + ParseStringProperty(&sampler.interpolation, err, s, + "interpolation", false); + if (!ParseIntegerProperty(&outputIndex, err, s, "output", + true)) { + if (err) { + (*err) += + "`output` field is missing in animation.sampler\n"; + } + return false; + } + sampler.input = inputIndex; + sampler.output = outputIndex; + ParseExtensionsProperty(&(sampler.extensions), err, o); + ParseExtrasProperty(&(sampler.extras), s); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator eit; + if (FindMember(o, "extensions", eit)) { + sampler.extensions_json_string = + JsonToString(GetValue(eit)); + } + } + { + json_const_iterator eit; + if (FindMember(o, "extras", eit)) { + sampler.extras_json_string = + JsonToString(GetValue(eit)); + } + } + } + + animation->samplers.emplace_back(std::move(sampler)); + } + } + } + + ParseStringProperty(&animation->name, err, o, "name", false); + + ParseExtensionsProperty(&animation->extensions, err, o); + ParseExtrasProperty(&(animation->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + animation->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + animation->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseSampler(Sampler* sampler, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + ParseStringProperty(&sampler->name, err, o, "name", false); + + int minFilter = -1; + int magFilter = -1; + int wrapS = TINYGLTF_TEXTURE_WRAP_REPEAT; + int wrapT = TINYGLTF_TEXTURE_WRAP_REPEAT; + // int wrapR = TINYGLTF_TEXTURE_WRAP_REPEAT; + ParseIntegerProperty(&minFilter, err, o, "minFilter", false); + ParseIntegerProperty(&magFilter, err, o, "magFilter", false); + ParseIntegerProperty(&wrapS, err, o, "wrapS", false); + ParseIntegerProperty(&wrapT, err, o, "wrapT", false); + // ParseIntegerProperty(&wrapR, err, o, "wrapR", false); // tinygltf + // extension + + // TODO(syoyo): Check the value is allowed one. + // (e.g. we allow 9728(NEAREST), but don't allow 9727) + + sampler->minFilter = minFilter; + sampler->magFilter = magFilter; + sampler->wrapS = wrapS; + sampler->wrapT = wrapT; + // sampler->wrapR = wrapR; + + ParseExtensionsProperty(&(sampler->extensions), err, o); + ParseExtrasProperty(&(sampler->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + sampler->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + sampler->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseSkin(Skin* skin, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + ParseStringProperty(&skin->name, err, o, "name", false, "Skin"); + + std::vector joints; + if (!ParseIntegerArrayProperty(&joints, err, o, "joints", false, "Skin")) { + return false; + } + skin->joints = std::move(joints); + + int skeleton = -1; + ParseIntegerProperty(&skeleton, err, o, "skeleton", false, "Skin"); + skin->skeleton = skeleton; + + int invBind = -1; + ParseIntegerProperty(&invBind, err, o, "inverseBindMatrices", true, "Skin"); + skin->inverseBindMatrices = invBind; + + ParseExtensionsProperty(&(skin->extensions), err, o); + ParseExtrasProperty(&(skin->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + skin->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + skin->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool +ParsePerspectiveCamera(PerspectiveCamera* camera, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + double yfov = 0.0; + if (!ParseNumberProperty(&yfov, err, o, "yfov", true, + "OrthographicCamera")) { + return false; + } + + double znear = 0.0; + if (!ParseNumberProperty(&znear, err, o, "znear", true, + "PerspectiveCamera")) { + return false; + } + + double aspectRatio = 0.0; // = invalid + ParseNumberProperty(&aspectRatio, err, o, "aspectRatio", false, + "PerspectiveCamera"); + + double zfar = 0.0; // = invalid + ParseNumberProperty(&zfar, err, o, "zfar", false, "PerspectiveCamera"); + + camera->aspectRatio = aspectRatio; + camera->zfar = zfar; + camera->yfov = yfov; + camera->znear = znear; + + ParseExtensionsProperty(&camera->extensions, err, o); + ParseExtrasProperty(&(camera->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + camera->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + camera->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + // TODO(syoyo): Validate parameter values. + + return true; +} + +static bool ParseSpotLight(SpotLight* light, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + ParseNumberProperty(&light->innerConeAngle, err, o, "innerConeAngle", + false); + ParseNumberProperty(&light->outerConeAngle, err, o, "outerConeAngle", + false); + + ParseExtensionsProperty(&light->extensions, err, o); + ParseExtrasProperty(&light->extras, o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + light->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + light->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + // TODO(syoyo): Validate parameter values. + + return true; +} + +static bool +ParseOrthographicCamera(OrthographicCamera* camera, std::string* err, + const json& o, + bool store_original_json_for_extras_and_extensions) +{ + double xmag = 0.0; + if (!ParseNumberProperty(&xmag, err, o, "xmag", true, + "OrthographicCamera")) { + return false; + } + + double ymag = 0.0; + if (!ParseNumberProperty(&ymag, err, o, "ymag", true, + "OrthographicCamera")) { + return false; + } + + double zfar = 0.0; + if (!ParseNumberProperty(&zfar, err, o, "zfar", true, + "OrthographicCamera")) { + return false; + } + + double znear = 0.0; + if (!ParseNumberProperty(&znear, err, o, "znear", true, + "OrthographicCamera")) { + return false; + } + + ParseExtensionsProperty(&camera->extensions, err, o); + ParseExtrasProperty(&(camera->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + camera->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + camera->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + camera->xmag = xmag; + camera->ymag = ymag; + camera->zfar = zfar; + camera->znear = znear; + + // TODO(syoyo): Validate parameter values. + + return true; +} + +static bool ParseCamera(Camera* camera, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + if (!ParseStringProperty(&camera->type, err, o, "type", true, "Camera")) { + return false; + } + + if (camera->type.compare("orthographic") == 0) { + json_const_iterator orthoIt; + if (!FindMember(o, "orthographic", orthoIt)) { + if (err) { + std::stringstream ss; + ss << "Orthographic camera description not found." << std::endl; + (*err) += ss.str(); + } + return false; + } + + const json& v = GetValue(orthoIt); + if (!IsObject(v)) { + if (err) { + std::stringstream ss; + ss << "\"orthographic\" is not a JSON object." << std::endl; + (*err) += ss.str(); + } + return false; + } + + if (!ParseOrthographicCamera( + &camera->orthographic, err, v, + store_original_json_for_extras_and_extensions)) { + return false; + } + } + else if (camera->type.compare("perspective") == 0) { + json_const_iterator perspIt; + if (!FindMember(o, "perspective", perspIt)) { + if (err) { + std::stringstream ss; + ss << "Perspective camera description not found." << std::endl; + (*err) += ss.str(); + } + return false; + } + + const json& v = GetValue(perspIt); + if (!IsObject(v)) { + if (err) { + std::stringstream ss; + ss << "\"perspective\" is not a JSON object." << std::endl; + (*err) += ss.str(); + } + return false; + } + + if (!ParsePerspectiveCamera( + &camera->perspective, err, v, + store_original_json_for_extras_and_extensions)) { + return false; + } + } + else { + if (err) { + std::stringstream ss; + ss << "Invalid camera type: \"" << camera->type + << "\". Must be \"perspective\" or \"orthographic\"" + << std::endl; + (*err) += ss.str(); + } + return false; + } + + ParseStringProperty(&camera->name, err, o, "name", false); + + ParseExtensionsProperty(&camera->extensions, err, o); + ParseExtrasProperty(&(camera->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + camera->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + camera->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +static bool ParseLight(Light* light, std::string* err, const json& o, + bool store_original_json_for_extras_and_extensions) +{ + if (!ParseStringProperty(&light->type, err, o, "type", true)) { + return false; + } + + if (light->type == "spot") { + json_const_iterator spotIt; + if (!FindMember(o, "spot", spotIt)) { + if (err) { + std::stringstream ss; + ss << "Spot light description not found." << std::endl; + (*err) += ss.str(); + } + return false; + } + + const json& v = GetValue(spotIt); + if (!IsObject(v)) { + if (err) { + std::stringstream ss; + ss << "\"spot\" is not a JSON object." << std::endl; + (*err) += ss.str(); + } + return false; + } + + if (!ParseSpotLight(&light->spot, err, v, + store_original_json_for_extras_and_extensions)) { + return false; + } + } + + ParseStringProperty(&light->name, err, o, "name", false); + ParseNumberArrayProperty(&light->color, err, o, "color", false); + ParseNumberProperty(&light->range, err, o, "range", false); + ParseNumberProperty(&light->intensity, err, o, "intensity", false); + ParseExtensionsProperty(&light->extensions, err, o); + ParseExtrasProperty(&(light->extras), o); + + if (store_original_json_for_extras_and_extensions) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + light->extensions_json_string = JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + light->extras_json_string = JsonToString(GetValue(it)); + } + } + } + + return true; +} + +bool TinyGLTF::LoadFromString(Model* model, std::string* err, std::string* warn, + const char* json_str, + unsigned int json_str_length, + const std::string& base_dir, + unsigned int check_sections) +{ + if (json_str_length < 4) { + if (err) { + (*err) = "JSON string too short.\n"; + } + return false; + } + + JsonDocument v; + +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || \ + defined(_CPPUNWIND)) && \ + !defined(TINYGLTF_NOEXCEPTION) + try { + JsonParse(v, json_str, json_str_length, true); + } + catch (const std::exception& e) { + if (err) { + (*err) = e.what(); + } + return false; + } +#else + { + JsonParse(v, json_str, json_str_length); + + if (!IsObject(v)) { + // Assume parsing was failed. + if (err) { + (*err) = "Failed to parse JSON object\n"; + } + return false; + } + } +#endif + + if (!IsObject(v)) { + // root is not an object. + if (err) { + (*err) = "Root element is not a JSON object\n"; + } + return false; + } + + { + bool version_found = false; + json_const_iterator it; + if (FindMember(v, "asset", it) && IsObject(GetValue(it))) { + auto& itObj = GetValue(it); + json_const_iterator version_it; + std::string versionStr; + if (FindMember(itObj, "version", version_it) && + GetString(GetValue(version_it), versionStr)) { + version_found = true; + } + } + if (version_found) { + // OK + } + else if (check_sections & REQUIRE_VERSION) { + if (err) { + (*err) += "\"asset\" object not found in .gltf or not an " + "object type\n"; + } + return false; + } + } + + // scene is not mandatory. + // FIXME Maybe a better way to handle it than removing the code + + auto IsArrayMemberPresent = [](const json& _v, const char* name) -> bool { + json_const_iterator it; + return FindMember(_v, name, it) && IsArray(GetValue(it)); + }; + + { + if ((check_sections & REQUIRE_SCENES) && + !IsArrayMemberPresent(v, "scenes")) { + if (err) { + (*err) += "\"scenes\" object not found in .gltf or not an " + "array type\n"; + } + return false; + } + } + + { + if ((check_sections & REQUIRE_NODES) && + !IsArrayMemberPresent(v, "nodes")) { + if (err) { + (*err) += "\"nodes\" object not found in .gltf\n"; + } + return false; + } + } + + { + if ((check_sections & REQUIRE_ACCESSORS) && + !IsArrayMemberPresent(v, "accessors")) { + if (err) { + (*err) += "\"accessors\" object not found in .gltf\n"; + } + return false; + } + } + + { + if ((check_sections & REQUIRE_BUFFERS) && + !IsArrayMemberPresent(v, "buffers")) { + if (err) { + (*err) += "\"buffers\" object not found in .gltf\n"; + } + return false; + } + } + + { + if ((check_sections & REQUIRE_BUFFER_VIEWS) && + !IsArrayMemberPresent(v, "bufferViews")) { + if (err) { + (*err) += "\"bufferViews\" object not found in .gltf\n"; + } + return false; + } + } + + model->buffers.clear(); + model->bufferViews.clear(); + model->accessors.clear(); + model->meshes.clear(); + model->cameras.clear(); + model->nodes.clear(); + model->extensionsUsed.clear(); + model->extensionsRequired.clear(); + model->extensions.clear(); + model->defaultScene = -1; + + // 1. Parse Asset + { + json_const_iterator it; + if (FindMember(v, "asset", it) && IsObject(GetValue(it))) { + const json& root = GetValue(it); + + ParseAsset(&model->asset, err, root, + store_original_json_for_extras_and_extensions_); + } + } + +#ifdef TINYGLTF_USE_CPP14 + auto ForEachInArray = [](const json& _v, const char* member, + const auto& cb) -> bool +#else + // The std::function<> implementation can be less efficient because it will + // allocate heap when the size of the captured lambda is above 16 bytes with + // clang and gcc, but it does not require C++14. + auto ForEachInArray = [](const json& _v, const char* member, + const std::function& cb) -> bool +#endif + { + json_const_iterator itm; + if (FindMember(_v, member, itm) && IsArray(GetValue(itm))) { + const json& root = GetValue(itm); + auto it = ArrayBegin(root); + auto end = ArrayEnd(root); + for (; it != end; ++it) { + if (!cb(*it)) + return false; + } + } + return true; + }; + + // 2. Parse extensionUsed + { + ForEachInArray(v, "extensionsUsed", [&](const json& o) { + std::string str; + GetString(o, str); + model->extensionsUsed.emplace_back(std::move(str)); + return true; + }); + } + + { + ForEachInArray(v, "extensionsRequired", [&](const json& o) { + std::string str; + GetString(o, str); + model->extensionsRequired.emplace_back(std::move(str)); + return true; + }); + } + + // 3. Parse Buffer + { + bool success = ForEachInArray(v, "buffers", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`buffers' does not contain an JSON object."; + } + return false; + } + Buffer buffer; + if (!ParseBuffer(&buffer, err, o, + store_original_json_for_extras_and_extensions_, + &fs, base_dir, is_binary_, bin_data_, bin_size_)) { + return false; + } + + model->buffers.emplace_back(std::move(buffer)); + return true; + }); + + if (!success) { + return false; + } + } + // 4. Parse BufferView + { + bool success = ForEachInArray(v, "bufferViews", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`bufferViews' does not contain an JSON object."; + } + return false; + } + BufferView bufferView; + if (!ParseBufferView( + &bufferView, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->bufferViews.emplace_back(std::move(bufferView)); + return true; + }); + + if (!success) { + return false; + } + } + + // 5. Parse Accessor + { + bool success = ForEachInArray(v, "accessors", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`accessors' does not contain an JSON object."; + } + return false; + } + Accessor accessor; + if (!ParseAccessor( + &accessor, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->accessors.emplace_back(std::move(accessor)); + return true; + }); + + if (!success) { + return false; + } + } + + // 6. Parse Mesh + { + bool success = ForEachInArray(v, "meshes", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`meshes' does not contain an JSON object."; + } + return false; + } + Mesh mesh; + if (!ParseMesh(&mesh, model, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->meshes.emplace_back(std::move(mesh)); + return true; + }); + + if (!success) { + return false; + } + } + + // Assign missing bufferView target types + // - Look for missing Mesh indices + // - Look for missing Mesh attributes + for (auto& mesh : model->meshes) { + for (auto& primitive : mesh.primitives) { + if (primitive.indices > -1) // has indices from parsing step, must + // be Element Array Buffer + { + if (size_t(primitive.indices) >= model->accessors.size()) { + if (err) { + (*err) += "primitive indices accessor out of bounds"; + } + return false; + } + + auto bufferView = + model->accessors[size_t(primitive.indices)].bufferView; + if (bufferView < 0 || + size_t(bufferView) >= model->bufferViews.size()) { + if (err) { + (*err) += "accessor[" + + std::to_string(primitive.indices) + + "] invalid bufferView"; + } + return false; + } + + model->bufferViews[size_t(bufferView)].target = + TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER; + // we could optionally check if accessors' bufferView type is + // Scalar, as it should be + } + + for (auto& attribute : primitive.attributes) { + const auto accessorsIndex = size_t(attribute.second); + if (accessorsIndex < model->accessors.size()) { + const auto bufferView = + model->accessors[accessorsIndex].bufferView; + // bufferView could be null(-1) for sparse morph target + if (bufferView >= 0 && + bufferView < (int)model->bufferViews.size()) { + model->bufferViews[size_t(bufferView)].target = + TINYGLTF_TARGET_ARRAY_BUFFER; + } + } + } + + for (auto& target : primitive.targets) { + for (auto& attribute : target) { + const auto accessorsIndex = size_t(attribute.second); + if (accessorsIndex < model->accessors.size()) { + const auto bufferView = + model->accessors[accessorsIndex].bufferView; + // bufferView could be null(-1) for sparse morph target + if (bufferView >= 0 && + bufferView < (int)model->bufferViews.size()) { + model->bufferViews[size_t(bufferView)].target = + TINYGLTF_TARGET_ARRAY_BUFFER; + } + } + } + } + } + } + + // 7. Parse Node + { + bool success = ForEachInArray(v, "nodes", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`nodes' does not contain an JSON object."; + } + return false; + } + Node node; + if (!ParseNode(&node, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->nodes.emplace_back(std::move(node)); + return true; + }); + + if (!success) { + return false; + } + } + + // 8. Parse scenes. + { + bool success = ForEachInArray(v, "scenes", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`scenes' does not contain an JSON object."; + } + return false; + } + std::vector nodes; + ParseIntegerArrayProperty(&nodes, err, o, "nodes", false); + + Scene scene; + scene.nodes = std::move(nodes); + + ParseStringProperty(&scene.name, err, o, "name", false); + + ParseExtensionsProperty(&scene.extensions, err, o); + ParseExtrasProperty(&scene.extras, o); + + if (store_original_json_for_extras_and_extensions_) { + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + scene.extensions_json_string = + JsonToString(GetValue(it)); + } + } + { + json_const_iterator it; + if (FindMember(o, "extras", it)) { + scene.extras_json_string = JsonToString(GetValue(it)); + } + } + } + + model->scenes.emplace_back(std::move(scene)); + return true; + }); + + if (!success) { + return false; + } + } + + // 9. Parse default scenes. + { + json_const_iterator rootIt; + int iVal; + if (FindMember(v, "scene", rootIt) && GetInt(GetValue(rootIt), iVal)) { + model->defaultScene = iVal; + } + } + + // 10. Parse Material + { + bool success = ForEachInArray(v, "materials", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`materials' does not contain an JSON object."; + } + return false; + } + Material material; + ParseStringProperty(&material.name, err, o, "name", false); + + if (!ParseMaterial( + &material, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->materials.emplace_back(std::move(material)); + return true; + }); + + if (!success) { + return false; + } + } + + // 11. Parse Image + void* load_image_user_data{nullptr}; + + LoadImageDataOption load_image_option; + + if (user_image_loader_) { + // Use user supplied pointer + load_image_user_data = load_image_user_data_; + } + else { + load_image_option.preserve_channels = preserve_image_channels_; + load_image_user_data = reinterpret_cast(&load_image_option); + } + + { + int idx = 0; + bool success = ForEachInArray(v, "images", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "image[" + std::to_string(idx) + + "] is not a JSON object."; + } + return false; + } + Image image; + if (!ParseImage(&image, idx, err, warn, o, + store_original_json_for_extras_and_extensions_, + base_dir, &fs, &this->LoadImageData, + load_image_user_data)) { + return false; + } + + if (image.bufferView != -1) { + // Load image from the buffer view. + if (size_t(image.bufferView) >= model->bufferViews.size()) { + if (err) { + std::stringstream ss; + ss << "image[" << idx << "] bufferView \"" + << image.bufferView << "\" not found in the scene." + << std::endl; + (*err) += ss.str(); + } + return false; + } + + const BufferView& bufferView = + model->bufferViews[size_t(image.bufferView)]; + if (size_t(bufferView.buffer) >= model->buffers.size()) { + if (err) { + std::stringstream ss; + ss << "image[" << idx << "] buffer \"" + << bufferView.buffer << "\" not found in the scene." + << std::endl; + (*err) += ss.str(); + } + return false; + } + const Buffer& buffer = + model->buffers[size_t(bufferView.buffer)]; + + if (*LoadImageData == nullptr) { + if (err) { + (*err) += "No LoadImageData callback specified.\n"; + } + return false; + } + bool ret = LoadImageData( + &image, idx, err, warn, image.width, image.height, + &buffer.data[bufferView.byteOffset], + static_cast(bufferView.byteLength), + load_image_user_data); + if (!ret) { + return false; + } + } + + model->images.emplace_back(std::move(image)); + ++idx; + return true; + }); + + if (!success) { + return false; + } + } + + // 12. Parse Texture + { + bool success = ForEachInArray(v, "textures", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`textures' does not contain an JSON object."; + } + return false; + } + Texture texture; + if (!ParseTexture(&texture, err, o, + store_original_json_for_extras_and_extensions_, + base_dir)) { + return false; + } + + model->textures.emplace_back(std::move(texture)); + return true; + }); + + if (!success) { + return false; + } + } + + // 13. Parse Animation + { + bool success = ForEachInArray(v, "animations", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`animations' does not contain an JSON object."; + } + return false; + } + Animation animation; + if (!ParseAnimation( + &animation, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->animations.emplace_back(std::move(animation)); + return true; + }); + + if (!success) { + return false; + } + } + + // 14. Parse Skin + { + bool success = ForEachInArray(v, "skins", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`skins' does not contain an JSON object."; + } + return false; + } + Skin skin; + if (!ParseSkin(&skin, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->skins.emplace_back(std::move(skin)); + return true; + }); + + if (!success) { + return false; + } + } + + // 15. Parse Sampler + { + bool success = ForEachInArray(v, "samplers", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`samplers' does not contain an JSON object."; + } + return false; + } + Sampler sampler; + if (!ParseSampler(&sampler, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->samplers.emplace_back(std::move(sampler)); + return true; + }); + + if (!success) { + return false; + } + } + + // 16. Parse Camera + { + bool success = ForEachInArray(v, "cameras", [&](const json& o) { + if (!IsObject(o)) { + if (err) { + (*err) += "`cameras' does not contain an JSON object."; + } + return false; + } + Camera camera; + if (!ParseCamera(&camera, err, o, + store_original_json_for_extras_and_extensions_)) { + return false; + } + + model->cameras.emplace_back(std::move(camera)); + return true; + }); + + if (!success) { + return false; + } + } + + // 17. Parse Extensions + ParseExtensionsProperty(&model->extensions, err, v); + + // 18. Specific extension implementations + { + json_const_iterator rootIt; + if (FindMember(v, "extensions", rootIt) && IsObject(GetValue(rootIt))) { + const json& root = GetValue(rootIt); + + json_const_iterator it(ObjectBegin(root)); + json_const_iterator itEnd(ObjectEnd(root)); + for (; it != itEnd; ++it) { + // parse KHR_lights_punctual extension + std::string key(GetKey(it)); + if ((key == "KHR_lights_punctual") && IsObject(GetValue(it))) { + const json& object = GetValue(it); + json_const_iterator itLight; + if (FindMember(object, "lights", itLight)) { + const json& lights = GetValue(itLight); + if (!IsArray(lights)) { + continue; + } + + auto arrayIt(ArrayBegin(lights)); + auto arrayItEnd(ArrayEnd(lights)); + for (; arrayIt != arrayItEnd; ++arrayIt) { + Light light; + if (!ParseLight( + &light, err, *arrayIt, + store_original_json_for_extras_and_extensions_)) { + return false; + } + model->lights.emplace_back(std::move(light)); + } + } + } + } + } + } + + // 19. Parse Extras + ParseExtrasProperty(&model->extras, v); + + if (store_original_json_for_extras_and_extensions_) { + model->extras_json_string = JsonToString(v["extras"]); + model->extensions_json_string = JsonToString(v["extensions"]); + } + + return true; +} + +bool TinyGLTF::LoadASCIIFromString(Model* model, std::string* err, + std::string* warn, const char* str, + unsigned int length, + const std::string& base_dir, + unsigned int check_sections) +{ + is_binary_ = false; + bin_data_ = nullptr; + bin_size_ = 0; + + return LoadFromString(model, err, warn, str, length, base_dir, + check_sections); +} + +bool TinyGLTF::LoadASCIIFromFile(Model* model, std::string* err, + std::string* warn, const std::string& filename, + unsigned int check_sections) +{ + std::stringstream ss; + + if (fs.ReadWholeFile == nullptr) { + // Programmer error, assert() ? + ss << "Failed to read file: " << filename + << ": one or more FS callback not set" << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + + std::vector data; + std::string fileerr; + bool fileread = fs.ReadWholeFile(&data, &fileerr, filename, fs.user_data); + if (!fileread) { + ss << "Failed to read file: " << filename << ": " << fileerr + << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + + size_t sz = data.size(); + if (sz == 0) { + if (err) { + (*err) = "Empty file."; + } + return false; + } + + std::string basedir = GetBaseDir(filename); + + bool ret = LoadASCIIFromString( + model, err, warn, reinterpret_cast(&data.at(0)), + static_cast(data.size()), basedir, check_sections); + + return ret; +} + +bool TinyGLTF::LoadBinaryFromMemory(Model* model, std::string* err, + std::string* warn, + const unsigned char* bytes, + unsigned int size, + const std::string& base_dir, + unsigned int check_sections) +{ + if (size < 20) { + if (err) { + (*err) = "Too short data size for glTF Binary."; + } + return false; + } + + if (bytes[0] == 'g' && bytes[1] == 'l' && bytes[2] == 'T' && + bytes[3] == 'F') { + // ok + } + else { + if (err) { + (*err) = "Invalid magic."; + } + return false; + } + + unsigned int version; // 4 bytes + unsigned int length; // 4 bytes + unsigned int chunk0_length; // 4 bytes + unsigned int chunk0_format; // 4 bytes; + + memcpy(&version, bytes + 4, 4); + swap4(&version); + memcpy(&length, bytes + 8, 4); + swap4(&length); + memcpy(&chunk0_length, bytes + 12, 4); // JSON data length + swap4(&chunk0_length); + memcpy(&chunk0_format, bytes + 16, 4); + swap4(&chunk0_format); + + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#binary-gltf-layout + // + // In case the Bin buffer is not present, the size is exactly 20 + size of + // JSON contents, + // so use "greater than" operator. + // + // https://github.com/syoyo/tinygltf/issues/372 + // Use 64bit uint to avoid integer overflow. + uint64_t header_and_json_size = 20ull + uint64_t(chunk0_length); + + if (header_and_json_size > std::numeric_limits::max()) { + // Do not allow 4GB or more GLB data. + (*err) = "Invalid glTF binary. GLB data exceeds 4GB."; + } + + if ((header_and_json_size > uint64_t(size)) || (chunk0_length < 1) || + (length > size) || (header_and_json_size > uint64_t(length)) || + (chunk0_format != 0x4E4F534A)) { // 0x4E4F534A = JSON format. + if (err) { + (*err) = "Invalid glTF binary."; + } + return false; + } + + // Padding check + // The start and the end of each chunk must be aligned to a 4-byte boundary. + // No padding check for chunk0 start since its 4byte-boundary is ensured. + if ((header_and_json_size % 4) != 0) { + if (err) { + (*err) = "JSON Chunk end does not aligned to a 4-byte boundary."; + } + } + + // std::cout << "header_and_json_size = " << header_and_json_size << "\n"; + // std::cout << "length = " << length << "\n"; + + // Chunk1(BIN) data + // The spec says: When the binary buffer is empty or when it is stored by + // other means, this chunk SHOULD be omitted. So when header + JSON data == + // binary size, Chunk1 is omitted. + if (header_and_json_size == uint64_t(length)) { + + bin_data_ = nullptr; + bin_size_ = 0; + } + else { + // Read Chunk1 info(BIN data) + // At least Chunk1 should have 12 bytes(8 bytes(header) + 4 bytes(bin + // payload could be 1~3 bytes, but need to be aligned to 4 bytes) + if ((header_and_json_size + 12ull) > uint64_t(length)) { + if (err) { + (*err) = + "Insufficient storage space for Chunk1(BIN data). At least " + "Chunk1 Must have 4 bytes or more bytes, but got " + + std::to_string((header_and_json_size + 12ull) - + uint64_t(length)) + + ".\n"; + } + return false; + } + + unsigned int chunk1_length; // 4 bytes + unsigned int chunk1_format; // 4 bytes; + memcpy(&chunk1_length, bytes + header_and_json_size, + 4); // JSON data length + swap4(&chunk1_length); + memcpy(&chunk1_format, bytes + header_and_json_size + 4, 4); + swap4(&chunk1_format); + + // std::cout << "chunk1_length = " << chunk1_length << "\n"; + + if (chunk1_length < 4) { + if (err) { + (*err) = "Insufficient Chunk1(BIN) data size."; + } + return false; + } + + if ((chunk1_length % 4) != 0) { + if (err) { + (*err) = "BIN Chunk end does not aligned to a 4-byte boundary."; + } + return false; + } + + if (uint64_t(chunk1_length) + header_and_json_size > uint64_t(length)) { + if (err) { + (*err) = "BIN Chunk data length exceeds the GLB size."; + } + return false; + } + + if (chunk1_format != 0x004e4942) { + if (err) { + (*err) = "Invalid type for chunk1 data."; + } + return false; + } + + // std::cout << "chunk1_length = " << chunk1_length << "\n"; + + bin_data_ = + bytes + header_and_json_size + + 8; // 4 bytes (bin_buffer_length) + 4 bytes(bin_buffer_format) + + bin_size_ = size_t(chunk1_length); + } + + // Extract JSON string. + std::string jsonString(reinterpret_cast(&bytes[20]), + chunk0_length); + + is_binary_ = true; + + bool ret = LoadFromString(model, err, warn, + reinterpret_cast(&bytes[20]), + chunk0_length, base_dir, check_sections); + if (!ret) { + return ret; + } + + return true; +} + +bool TinyGLTF::LoadBinaryFromFile(Model* model, std::string* err, + std::string* warn, + const std::string& filename, + unsigned int check_sections) +{ + std::stringstream ss; + + if (fs.ReadWholeFile == nullptr) { + // Programmer error, assert() ? + ss << "Failed to read file: " << filename + << ": one or more FS callback not set" << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + + std::vector data; + std::string fileerr; + bool fileread = fs.ReadWholeFile(&data, &fileerr, filename, fs.user_data); + if (!fileread) { + ss << "Failed to read file: " << filename << ": " << fileerr + << std::endl; + if (err) { + (*err) = ss.str(); + } + return false; + } + + std::string basedir = GetBaseDir(filename); + + bool ret = LoadBinaryFromMemory(model, err, warn, &data.at(0), + static_cast(data.size()), + basedir, check_sections); + + return ret; +} + +/////////////////////// +// GLTF Serialization +/////////////////////// +namespace { +json JsonFromString(const char* s) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return json(s, GetAllocator()); +#else + return json(s); +#endif +} + +void JsonAssign(json& dest, const json& src) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + dest.CopyFrom(src, GetAllocator()); +#else + dest = src; +#endif +} + +void JsonAddMember(json& o, const char* key, json&& value) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + if (!o.IsObject()) { + o.SetObject(); + } + o.AddMember(json(key, GetAllocator()), std::move(value), GetAllocator()); +#else + o[key] = std::move(value); +#endif +} + +void JsonPushBack(json& o, json&& value) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + o.PushBack(std::move(value), GetAllocator()); +#else + o.push_back(std::move(value)); +#endif +} + +bool JsonIsNull(const json& o) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + return o.IsNull(); +#else + return o.is_null(); +#endif +} + +void JsonSetObject(json& o) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + o.SetObject(); +#else + o = o.object({}); +#endif +} + +void JsonReserveArray(json& o, size_t s) +{ +#ifdef TINYGLTF_USE_RAPIDJSON + o.SetArray(); + o.Reserve(static_cast(s), GetAllocator()); +#endif + (void)(o); + (void)(s); +} +} // namespace + +// typedef std::pair json_object_pair; + +template +static void SerializeNumberProperty(const std::string& key, T number, json& obj) +{ + // obj.insert( + // json_object_pair(key, json(static_cast(number)))); + // obj[key] = static_cast(number); + JsonAddMember(obj, key.c_str(), json(number)); +} + +#ifdef TINYGLTF_USE_RAPIDJSON +template <> +void SerializeNumberProperty(const std::string& key, size_t number, json& obj) +{ + JsonAddMember(obj, key.c_str(), json(static_cast(number))); +} +#endif + +template +static void SerializeNumberArrayProperty(const std::string& key, + const std::vector& value, json& obj) +{ + if (value.empty()) + return; + + json ary; + JsonReserveArray(ary, value.size()); + for (const auto& s : value) { + JsonPushBack(ary, json(s)); + } + JsonAddMember(obj, key.c_str(), std::move(ary)); +} + +static void SerializeStringProperty(const std::string& key, + const std::string& value, json& obj) +{ + JsonAddMember(obj, key.c_str(), JsonFromString(value.c_str())); +} + +static void SerializeStringArrayProperty(const std::string& key, + const std::vector& value, + json& obj) +{ + json ary; + JsonReserveArray(ary, value.size()); + for (auto& s : value) { + JsonPushBack(ary, JsonFromString(s.c_str())); + } + JsonAddMember(obj, key.c_str(), std::move(ary)); +} + +static bool ValueToJson(const Value& value, json* ret) +{ + json obj; +#ifdef TINYGLTF_USE_RAPIDJSON + switch (value.Type()) { + case REAL_TYPE: + obj.SetDouble(value.Get()); + break; + case INT_TYPE: + obj.SetInt(value.Get()); + break; + case BOOL_TYPE: + obj.SetBool(value.Get()); + break; + case STRING_TYPE: + obj.SetString(value.Get().c_str(), GetAllocator()); + break; + case ARRAY_TYPE: { + obj.SetArray(); + obj.Reserve(static_cast(value.ArrayLen()), + GetAllocator()); + for (unsigned int i = 0; i < value.ArrayLen(); ++i) { + Value elementValue = value.Get(int(i)); + json elementJson; + if (ValueToJson(value.Get(int(i)), &elementJson)) + obj.PushBack(std::move(elementJson), GetAllocator()); + } + break; + } + case BINARY_TYPE: + // TODO + // obj = json(value.Get>()); + return false; + break; + case OBJECT_TYPE: { + obj.SetObject(); + Value::Object objMap = value.Get(); + for (auto& it : objMap) { + json elementJson; + if (ValueToJson(it.second, &elementJson)) { + obj.AddMember(json(it.first.c_str(), GetAllocator()), + std::move(elementJson), GetAllocator()); + } + } + break; + } + case NULL_TYPE: + default: + return false; + } +#else + switch (value.Type()) { + case REAL_TYPE: + obj = json(value.Get()); + break; + case INT_TYPE: + obj = json(value.Get()); + break; + case BOOL_TYPE: + obj = json(value.Get()); + break; + case STRING_TYPE: + obj = json(value.Get()); + break; + case ARRAY_TYPE: { + for (unsigned int i = 0; i < value.ArrayLen(); ++i) { + Value elementValue = value.Get(int(i)); + json elementJson; + if (ValueToJson(value.Get(int(i)), &elementJson)) + obj.push_back(elementJson); + } + break; + } + case BINARY_TYPE: + // TODO + // obj = json(value.Get>()); + return false; + break; + case OBJECT_TYPE: { + Value::Object objMap = value.Get(); + for (auto& it : objMap) { + json elementJson; + if (ValueToJson(it.second, &elementJson)) + obj[it.first] = elementJson; + } + break; + } + case NULL_TYPE: + default: + return false; + } +#endif + if (ret) + *ret = std::move(obj); + return true; +} + +static void SerializeValue(const std::string& key, const Value& value, + json& obj) +{ + json ret; + if (ValueToJson(value, &ret)) { + JsonAddMember(obj, key.c_str(), std::move(ret)); + } +} + +static void SerializeGltfBufferData(const std::vector& data, + json& o) +{ + std::string header = "data:application/octet-stream;base64,"; + if (data.size() > 0) { + std::string encodedData = + base64_encode(&data[0], static_cast(data.size())); + SerializeStringProperty("uri", header + encodedData, o); + } + else { + // Issue #229 + // size 0 is allowed. Just emit mime header. + SerializeStringProperty("uri", header, o); + } +} + +static bool SerializeGltfBufferData(const std::vector& data, + const std::string& binFilename) +{ +#ifdef _WIN32 +#if defined(__GLIBCXX__) // mingw + int file_descriptor = _wopen(UTF8ToWchar(binFilename).c_str(), + _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf( + file_descriptor, std::ios_base::out | std::ios_base::binary); + std::ostream output(&wfile_buf); + if (!wfile_buf.is_open()) + return false; +#elif defined(_MSC_VER) + std::ofstream output(UTF8ToWchar(binFilename).c_str(), + std::ofstream::binary); + if (!output.is_open()) + return false; +#else + std::ofstream output(binFilename.c_str(), std::ofstream::binary); + if (!output.is_open()) + return false; +#endif +#else + std::ofstream output(binFilename.c_str(), std::ofstream::binary); + if (!output.is_open()) + return false; +#endif + if (data.size() > 0) { + output.write(reinterpret_cast(&data[0]), + std::streamsize(data.size())); + } + else { + // Issue #229 + // size 0 will be still valid buffer data. + // write empty file. + } + return true; +} + +#if 0 // FIXME(syoyo): not used. will be removed in the future release. +static void SerializeParameterMap(ParameterMap ¶m, json &o) { + for (ParameterMap::iterator paramIt = param.begin(); paramIt != param.end(); + ++paramIt) { + if (paramIt->second.number_array.size()) { + SerializeNumberArrayProperty(paramIt->first, + paramIt->second.number_array, o); + } else if (paramIt->second.json_double_value.size()) { + json json_double_value; + for (std::map::iterator it = + paramIt->second.json_double_value.begin(); + it != paramIt->second.json_double_value.end(); ++it) { + if (it->first == "index") { + json_double_value[it->first] = paramIt->second.TextureIndex(); + } else { + json_double_value[it->first] = it->second; + } + } + + o[paramIt->first] = json_double_value; + } else if (!paramIt->second.string_value.empty()) { + SerializeStringProperty(paramIt->first, paramIt->second.string_value, o); + } else if (paramIt->second.has_number_value) { + o[paramIt->first] = paramIt->second.number_value; + } else { + o[paramIt->first] = paramIt->second.bool_value; + } + } +} +#endif + +static void SerializeExtensionMap(const ExtensionMap& extensions, json& o) +{ + if (!extensions.size()) + return; + + json extMap; + for (ExtensionMap::const_iterator extIt = extensions.begin(); + extIt != extensions.end(); ++extIt) { + // Allow an empty object for extension(#97) + json ret; + bool isNull = true; + if (ValueToJson(extIt->second, &ret)) { + isNull = JsonIsNull(ret); + JsonAddMember(extMap, extIt->first.c_str(), std::move(ret)); + } + if (isNull) { + if (!(extIt->first + .empty())) { // name should not be empty, but for sure + // create empty object so that an extension name is still + // included in json. + json empty; + JsonSetObject(empty); + JsonAddMember(extMap, extIt->first.c_str(), std::move(empty)); + } + } + } + JsonAddMember(o, "extensions", std::move(extMap)); +} + +static void SerializeGltfAccessor(Accessor& accessor, json& o) +{ + if (accessor.bufferView >= 0) + SerializeNumberProperty("bufferView", accessor.bufferView, o); + + if (accessor.byteOffset != 0) + SerializeNumberProperty("byteOffset", int(accessor.byteOffset), o); + + SerializeNumberProperty("componentType", accessor.componentType, o); + SerializeNumberProperty("count", accessor.count, o); + + if ((accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) || + (accessor.componentType == TINYGLTF_COMPONENT_TYPE_DOUBLE)) { + SerializeNumberArrayProperty("min", accessor.minValues, o); + SerializeNumberArrayProperty("max", accessor.maxValues, o); + } + else { + // Issue #301. Serialize as integer. + // Assume int value is within [-2**31-1, 2**31-1] + { + std::vector values; + std::transform(accessor.minValues.begin(), accessor.minValues.end(), + std::back_inserter(values), + [](double v) { return static_cast(v); }); + + SerializeNumberArrayProperty("min", values, o); + } + + { + std::vector values; + std::transform(accessor.maxValues.begin(), accessor.maxValues.end(), + std::back_inserter(values), + [](double v) { return static_cast(v); }); + + SerializeNumberArrayProperty("max", values, o); + } + } + + if (accessor.normalized) + SerializeValue("normalized", Value(accessor.normalized), o); + std::string type; + switch (accessor.type) { + case TINYGLTF_TYPE_SCALAR: + type = "SCALAR"; + break; + case TINYGLTF_TYPE_VEC2: + type = "VEC2"; + break; + case TINYGLTF_TYPE_VEC3: + type = "VEC3"; + break; + case TINYGLTF_TYPE_VEC4: + type = "VEC4"; + break; + case TINYGLTF_TYPE_MAT2: + type = "MAT2"; + break; + case TINYGLTF_TYPE_MAT3: + type = "MAT3"; + break; + case TINYGLTF_TYPE_MAT4: + type = "MAT4"; + break; + } + + SerializeStringProperty("type", type, o); + if (!accessor.name.empty()) + SerializeStringProperty("name", accessor.name, o); + + if (accessor.extras.Type() != NULL_TYPE) { + SerializeValue("extras", accessor.extras, o); + } + + // sparse + if (accessor.sparse.isSparse) { + json sparse; + SerializeNumberProperty("count", accessor.sparse.count, sparse); + { + json indices; + SerializeNumberProperty( + "bufferView", accessor.sparse.indices.bufferView, indices); + SerializeNumberProperty( + "byteOffset", accessor.sparse.indices.byteOffset, indices); + SerializeNumberProperty("componentType", + accessor.sparse.indices.componentType, + indices); + JsonAddMember(sparse, "indices", std::move(indices)); + } + { + json values; + SerializeNumberProperty( + "bufferView", accessor.sparse.values.bufferView, values); + SerializeNumberProperty( + "byteOffset", accessor.sparse.values.byteOffset, values); + JsonAddMember(sparse, "values", std::move(values)); + } + JsonAddMember(o, "sparse", std::move(sparse)); + } +} + +static void SerializeGltfAnimationChannel(AnimationChannel& channel, json& o) +{ + SerializeNumberProperty("sampler", channel.sampler, o); + { + json target; + SerializeNumberProperty("node", channel.target_node, target); + SerializeStringProperty("path", channel.target_path, target); + + SerializeExtensionMap(channel.target_extensions, target); + + JsonAddMember(o, "target", std::move(target)); + } + + if (channel.extras.Type() != NULL_TYPE) { + SerializeValue("extras", channel.extras, o); + } + + SerializeExtensionMap(channel.extensions, o); +} + +static void SerializeGltfAnimationSampler(AnimationSampler& sampler, json& o) +{ + SerializeNumberProperty("input", sampler.input, o); + SerializeNumberProperty("output", sampler.output, o); + SerializeStringProperty("interpolation", sampler.interpolation, o); + + if (sampler.extras.Type() != NULL_TYPE) { + SerializeValue("extras", sampler.extras, o); + } +} + +static void SerializeGltfAnimation(Animation& animation, json& o) +{ + if (!animation.name.empty()) + SerializeStringProperty("name", animation.name, o); + + { + json channels; + JsonReserveArray(channels, animation.channels.size()); + for (unsigned int i = 0; i < animation.channels.size(); ++i) { + json channel; + AnimationChannel gltfChannel = animation.channels[i]; + SerializeGltfAnimationChannel(gltfChannel, channel); + JsonPushBack(channels, std::move(channel)); + } + + JsonAddMember(o, "channels", std::move(channels)); + } + + { + json samplers; + JsonReserveArray(samplers, animation.samplers.size()); + for (unsigned int i = 0; i < animation.samplers.size(); ++i) { + json sampler; + AnimationSampler gltfSampler = animation.samplers[i]; + SerializeGltfAnimationSampler(gltfSampler, sampler); + JsonPushBack(samplers, std::move(sampler)); + } + JsonAddMember(o, "samplers", std::move(samplers)); + } + + if (animation.extras.Type() != NULL_TYPE) { + SerializeValue("extras", animation.extras, o); + } + + SerializeExtensionMap(animation.extensions, o); +} + +static void SerializeGltfAsset(Asset& asset, json& o) +{ + if (!asset.generator.empty()) { + SerializeStringProperty("generator", asset.generator, o); + } + + if (!asset.copyright.empty()) { + SerializeStringProperty("copyright", asset.copyright, o); + } + + if (asset.version.empty()) { + // Just in case + // `version` must be defined + asset.version = "2.0"; + } + + // TODO(syoyo): Do we need to check if `version` is greater or equal to 2.0? + SerializeStringProperty("version", asset.version, o); + + if (asset.extras.Keys().size()) { + SerializeValue("extras", asset.extras, o); + } + + SerializeExtensionMap(asset.extensions, o); +} + +static void SerializeGltfBufferBin(Buffer& buffer, json& o, + std::vector& binBuffer) +{ + SerializeNumberProperty("byteLength", buffer.data.size(), o); + binBuffer = buffer.data; + + if (buffer.name.size()) + SerializeStringProperty("name", buffer.name, o); + + if (buffer.extras.Type() != NULL_TYPE) { + SerializeValue("extras", buffer.extras, o); + } +} + +static void SerializeGltfBuffer(Buffer& buffer, json& o) +{ + SerializeNumberProperty("byteLength", buffer.data.size(), o); + SerializeGltfBufferData(buffer.data, o); + + if (buffer.name.size()) + SerializeStringProperty("name", buffer.name, o); + + if (buffer.extras.Type() != NULL_TYPE) { + SerializeValue("extras", buffer.extras, o); + } +} + +static bool SerializeGltfBuffer(Buffer& buffer, json& o, + const std::string& binFilename, + const std::string& binBaseFilename) +{ + if (!SerializeGltfBufferData(buffer.data, binFilename)) + return false; + SerializeNumberProperty("byteLength", buffer.data.size(), o); + SerializeStringProperty("uri", binBaseFilename, o); + + if (buffer.name.size()) + SerializeStringProperty("name", buffer.name, o); + + if (buffer.extras.Type() != NULL_TYPE) { + SerializeValue("extras", buffer.extras, o); + } + return true; +} + +static void SerializeGltfBufferView(BufferView& bufferView, json& o) +{ + SerializeNumberProperty("buffer", bufferView.buffer, o); + SerializeNumberProperty("byteLength", bufferView.byteLength, o); + + // byteStride is optional, minimum allowed is 4 + if (bufferView.byteStride >= 4) { + SerializeNumberProperty("byteStride", bufferView.byteStride, o); + } + // byteOffset is optional, default is 0 + if (bufferView.byteOffset > 0) { + SerializeNumberProperty("byteOffset", bufferView.byteOffset, o); + } + // Target is optional, check if it contains a valid value + if (bufferView.target == TINYGLTF_TARGET_ARRAY_BUFFER || + bufferView.target == TINYGLTF_TARGET_ELEMENT_ARRAY_BUFFER) { + SerializeNumberProperty("target", bufferView.target, o); + } + if (bufferView.name.size()) { + SerializeStringProperty("name", bufferView.name, o); + } + + if (bufferView.extras.Type() != NULL_TYPE) { + SerializeValue("extras", bufferView.extras, o); + } +} + +static void SerializeGltfImage(Image& image, json& o) +{ + // if uri empty, the mimeType and bufferview should be set + if (image.uri.empty()) { + SerializeStringProperty("mimeType", image.mimeType, o); + SerializeNumberProperty("bufferView", image.bufferView, o); + } + else { + // TODO(syoyo): dlib::urilencode? + SerializeStringProperty("uri", image.uri, o); + } + + if (image.name.size()) { + SerializeStringProperty("name", image.name, o); + } + + if (image.extras.Type() != NULL_TYPE) { + SerializeValue("extras", image.extras, o); + } + + SerializeExtensionMap(image.extensions, o); +} + +static void SerializeGltfTextureInfo(TextureInfo& texinfo, json& o) +{ + SerializeNumberProperty("index", texinfo.index, o); + + if (texinfo.texCoord != 0) { + SerializeNumberProperty("texCoord", texinfo.texCoord, o); + } + + if (texinfo.extras.Type() != NULL_TYPE) { + SerializeValue("extras", texinfo.extras, o); + } + + SerializeExtensionMap(texinfo.extensions, o); +} + +static void SerializeGltfNormalTextureInfo(NormalTextureInfo& texinfo, json& o) +{ + SerializeNumberProperty("index", texinfo.index, o); + + if (texinfo.texCoord != 0) { + SerializeNumberProperty("texCoord", texinfo.texCoord, o); + } + + if (!TINYGLTF_DOUBLE_EQUAL(texinfo.scale, 1.0)) { + SerializeNumberProperty("scale", texinfo.scale, o); + } + + if (texinfo.extras.Type() != NULL_TYPE) { + SerializeValue("extras", texinfo.extras, o); + } + + SerializeExtensionMap(texinfo.extensions, o); +} + +static void SerializeGltfOcclusionTextureInfo(OcclusionTextureInfo& texinfo, + json& o) +{ + SerializeNumberProperty("index", texinfo.index, o); + + if (texinfo.texCoord != 0) { + SerializeNumberProperty("texCoord", texinfo.texCoord, o); + } + + if (!TINYGLTF_DOUBLE_EQUAL(texinfo.strength, 1.0)) { + SerializeNumberProperty("strength", texinfo.strength, o); + } + + if (texinfo.extras.Type() != NULL_TYPE) { + SerializeValue("extras", texinfo.extras, o); + } + + SerializeExtensionMap(texinfo.extensions, o); +} + +static void SerializeGltfPbrMetallicRoughness(PbrMetallicRoughness& pbr, + json& o) +{ + std::vector default_baseColorFactor = {1.0, 1.0, 1.0, 1.0}; + if (!Equals(pbr.baseColorFactor, default_baseColorFactor)) { + SerializeNumberArrayProperty("baseColorFactor", + pbr.baseColorFactor, o); + } + + if (!TINYGLTF_DOUBLE_EQUAL(pbr.metallicFactor, 1.0)) { + SerializeNumberProperty("metallicFactor", pbr.metallicFactor, o); + } + + if (!TINYGLTF_DOUBLE_EQUAL(pbr.roughnessFactor, 1.0)) { + SerializeNumberProperty("roughnessFactor", pbr.roughnessFactor, o); + } + + if (pbr.baseColorTexture.index > -1) { + json texinfo; + SerializeGltfTextureInfo(pbr.baseColorTexture, texinfo); + JsonAddMember(o, "baseColorTexture", std::move(texinfo)); + } + + if (pbr.metallicRoughnessTexture.index > -1) { + json texinfo; + SerializeGltfTextureInfo(pbr.metallicRoughnessTexture, texinfo); + JsonAddMember(o, "metallicRoughnessTexture", std::move(texinfo)); + } + + SerializeExtensionMap(pbr.extensions, o); + + if (pbr.extras.Type() != NULL_TYPE) { + SerializeValue("extras", pbr.extras, o); + } +} + +static void SerializeGltfMaterial(Material& material, json& o) +{ + if (material.name.size()) { + SerializeStringProperty("name", material.name, o); + } + + // QUESTION(syoyo): Write material parameters regardless of its default + // value? + + if (!TINYGLTF_DOUBLE_EQUAL(material.alphaCutoff, 0.5)) { + SerializeNumberProperty("alphaCutoff", material.alphaCutoff, o); + } + + if (material.alphaMode.compare("OPAQUE") != 0) { + SerializeStringProperty("alphaMode", material.alphaMode, o); + } + + if (material.doubleSided != false) + JsonAddMember(o, "doubleSided", json(material.doubleSided)); + + if (material.normalTexture.index > -1) { + json texinfo; + SerializeGltfNormalTextureInfo(material.normalTexture, texinfo); + JsonAddMember(o, "normalTexture", std::move(texinfo)); + } + + if (material.occlusionTexture.index > -1) { + json texinfo; + SerializeGltfOcclusionTextureInfo(material.occlusionTexture, texinfo); + JsonAddMember(o, "occlusionTexture", std::move(texinfo)); + } + + if (material.emissiveTexture.index > -1) { + json texinfo; + SerializeGltfTextureInfo(material.emissiveTexture, texinfo); + JsonAddMember(o, "emissiveTexture", std::move(texinfo)); + } + + std::vector default_emissiveFactor = {0.0, 0.0, 0.0}; + if (!Equals(material.emissiveFactor, default_emissiveFactor)) { + SerializeNumberArrayProperty("emissiveFactor", + material.emissiveFactor, o); + } + + { + json pbrMetallicRoughness; + SerializeGltfPbrMetallicRoughness(material.pbrMetallicRoughness, + pbrMetallicRoughness); + // Issue 204 + // Do not serialize `pbrMetallicRoughness` if pbrMetallicRoughness has + // all default values(json is null). Otherwise it will serialize to + // `pbrMetallicRoughness : null`, which cannot be read by other glTF + // importers (and validators). + // + if (!JsonIsNull(pbrMetallicRoughness)) { + JsonAddMember(o, "pbrMetallicRoughness", + std::move(pbrMetallicRoughness)); + } + } + +#if 0 // legacy way. just for the record. + if (material.values.size()) { + json pbrMetallicRoughness; + SerializeParameterMap(material.values, pbrMetallicRoughness); + JsonAddMember(o, "pbrMetallicRoughness", std::move(pbrMetallicRoughness)); + } + + SerializeParameterMap(material.additionalValues, o); +#else + +#endif + + SerializeExtensionMap(material.extensions, o); + + if (material.extras.Type() != NULL_TYPE) { + SerializeValue("extras", material.extras, o); + } +} + +static void SerializeGltfMesh(Mesh& mesh, json& o) +{ + json primitives; + JsonReserveArray(primitives, mesh.primitives.size()); + for (unsigned int i = 0; i < mesh.primitives.size(); ++i) { + json primitive; + const Primitive& gltfPrimitive = + mesh.primitives[i]; // don't make a copy + { + json attributes; + for (auto attrIt = gltfPrimitive.attributes.begin(); + attrIt != gltfPrimitive.attributes.end(); ++attrIt) { + SerializeNumberProperty(attrIt->first, attrIt->second, + attributes); + } + + JsonAddMember(primitive, "attributes", std::move(attributes)); + } + + // Indices is optional + if (gltfPrimitive.indices > -1) { + SerializeNumberProperty("indices", gltfPrimitive.indices, + primitive); + } + // Material is optional + if (gltfPrimitive.material > -1) { + SerializeNumberProperty("material", gltfPrimitive.material, + primitive); + } + SerializeNumberProperty("mode", gltfPrimitive.mode, primitive); + + // Morph targets + if (gltfPrimitive.targets.size()) { + json targets; + JsonReserveArray(targets, gltfPrimitive.targets.size()); + for (unsigned int k = 0; k < gltfPrimitive.targets.size(); ++k) { + json targetAttributes; + std::map targetData = + gltfPrimitive.targets[k]; + for (std::map::iterator attrIt = + targetData.begin(); + attrIt != targetData.end(); ++attrIt) { + SerializeNumberProperty(attrIt->first, attrIt->second, + targetAttributes); + } + JsonPushBack(targets, std::move(targetAttributes)); + } + JsonAddMember(primitive, "targets", std::move(targets)); + } + + SerializeExtensionMap(gltfPrimitive.extensions, primitive); + + if (gltfPrimitive.extras.Type() != NULL_TYPE) { + SerializeValue("extras", gltfPrimitive.extras, primitive); + } + + JsonPushBack(primitives, std::move(primitive)); + } + + JsonAddMember(o, "primitives", std::move(primitives)); + + if (mesh.weights.size()) { + SerializeNumberArrayProperty("weights", mesh.weights, o); + } + + if (mesh.name.size()) { + SerializeStringProperty("name", mesh.name, o); + } + + SerializeExtensionMap(mesh.extensions, o); + if (mesh.extras.Type() != NULL_TYPE) { + SerializeValue("extras", mesh.extras, o); + } +} + +static void SerializeSpotLight(SpotLight& spot, json& o) +{ + SerializeNumberProperty("innerConeAngle", spot.innerConeAngle, o); + SerializeNumberProperty("outerConeAngle", spot.outerConeAngle, o); + SerializeExtensionMap(spot.extensions, o); + if (spot.extras.Type() != NULL_TYPE) { + SerializeValue("extras", spot.extras, o); + } +} + +static void SerializeGltfLight(Light& light, json& o) +{ + if (!light.name.empty()) + SerializeStringProperty("name", light.name, o); + SerializeNumberProperty("intensity", light.intensity, o); + if (light.range > 0.0) { + SerializeNumberProperty("range", light.range, o); + } + SerializeNumberArrayProperty("color", light.color, o); + SerializeStringProperty("type", light.type, o); + if (light.type == "spot") { + json spot; + SerializeSpotLight(light.spot, spot); + JsonAddMember(o, "spot", std::move(spot)); + } + SerializeExtensionMap(light.extensions, o); + if (light.extras.Type() != NULL_TYPE) { + SerializeValue("extras", light.extras, o); + } +} + +static void SerializeGltfNode(Node& node, json& o) +{ + if (node.translation.size() > 0) { + SerializeNumberArrayProperty("translation", node.translation, + o); + } + if (node.rotation.size() > 0) { + SerializeNumberArrayProperty("rotation", node.rotation, o); + } + if (node.scale.size() > 0) { + SerializeNumberArrayProperty("scale", node.scale, o); + } + if (node.matrix.size() > 0) { + SerializeNumberArrayProperty("matrix", node.matrix, o); + } + if (node.mesh != -1) { + SerializeNumberProperty("mesh", node.mesh, o); + } + + if (node.skin != -1) { + SerializeNumberProperty("skin", node.skin, o); + } + + if (node.camera != -1) { + SerializeNumberProperty("camera", node.camera, o); + } + + if (node.weights.size() > 0) { + SerializeNumberArrayProperty("weights", node.weights, o); + } + + if (node.extras.Type() != NULL_TYPE) { + SerializeValue("extras", node.extras, o); + } + + SerializeExtensionMap(node.extensions, o); + if (!node.name.empty()) + SerializeStringProperty("name", node.name, o); + SerializeNumberArrayProperty("children", node.children, o); +} + +static void SerializeGltfSampler(Sampler& sampler, json& o) +{ + if (sampler.magFilter != -1) { + SerializeNumberProperty("magFilter", sampler.magFilter, o); + } + if (sampler.minFilter != -1) { + SerializeNumberProperty("minFilter", sampler.minFilter, o); + } + // SerializeNumberProperty("wrapR", sampler.wrapR, o); + SerializeNumberProperty("wrapS", sampler.wrapS, o); + SerializeNumberProperty("wrapT", sampler.wrapT, o); + + if (sampler.extras.Type() != NULL_TYPE) { + SerializeValue("extras", sampler.extras, o); + } +} + +static void SerializeGltfOrthographicCamera(const OrthographicCamera& camera, + json& o) +{ + SerializeNumberProperty("zfar", camera.zfar, o); + SerializeNumberProperty("znear", camera.znear, o); + SerializeNumberProperty("xmag", camera.xmag, o); + SerializeNumberProperty("ymag", camera.ymag, o); + + if (camera.extras.Type() != NULL_TYPE) { + SerializeValue("extras", camera.extras, o); + } +} + +static void SerializeGltfPerspectiveCamera(const PerspectiveCamera& camera, + json& o) +{ + SerializeNumberProperty("zfar", camera.zfar, o); + SerializeNumberProperty("znear", camera.znear, o); + if (camera.aspectRatio > 0) { + SerializeNumberProperty("aspectRatio", camera.aspectRatio, o); + } + + if (camera.yfov > 0) { + SerializeNumberProperty("yfov", camera.yfov, o); + } + + if (camera.extras.Type() != NULL_TYPE) { + SerializeValue("extras", camera.extras, o); + } +} + +static void SerializeGltfCamera(const Camera& camera, json& o) +{ + SerializeStringProperty("type", camera.type, o); + if (!camera.name.empty()) { + SerializeStringProperty("name", camera.name, o); + } + + if (camera.type.compare("orthographic") == 0) { + json orthographic; + SerializeGltfOrthographicCamera(camera.orthographic, orthographic); + JsonAddMember(o, "orthographic", std::move(orthographic)); + } + else if (camera.type.compare("perspective") == 0) { + json perspective; + SerializeGltfPerspectiveCamera(camera.perspective, perspective); + JsonAddMember(o, "perspective", std::move(perspective)); + } + else { + // ??? + } + + if (camera.extras.Type() != NULL_TYPE) { + SerializeValue("extras", camera.extras, o); + } + SerializeExtensionMap(camera.extensions, o); +} + +static void SerializeGltfScene(Scene& scene, json& o) +{ + SerializeNumberArrayProperty("nodes", scene.nodes, o); + + if (scene.name.size()) { + SerializeStringProperty("name", scene.name, o); + } + if (scene.extras.Type() != NULL_TYPE) { + SerializeValue("extras", scene.extras, o); + } + SerializeExtensionMap(scene.extensions, o); +} + +static void SerializeGltfSkin(Skin& skin, json& o) +{ + // required + SerializeNumberArrayProperty("joints", skin.joints, o); + + if (skin.inverseBindMatrices >= 0) { + SerializeNumberProperty("inverseBindMatrices", skin.inverseBindMatrices, + o); + } + + if (skin.skeleton >= 0) { + SerializeNumberProperty("skeleton", skin.skeleton, o); + } + + if (skin.name.size()) { + SerializeStringProperty("name", skin.name, o); + } +} + +static void SerializeGltfTexture(Texture& texture, json& o) +{ + if (texture.sampler > -1) { + SerializeNumberProperty("sampler", texture.sampler, o); + } + if (texture.source > -1) { + SerializeNumberProperty("source", texture.source, o); + } + if (texture.name.size()) { + SerializeStringProperty("name", texture.name, o); + } + if (texture.extras.Type() != NULL_TYPE) { + SerializeValue("extras", texture.extras, o); + } + SerializeExtensionMap(texture.extensions, o); +} + +/// +/// Serialize all properties except buffers and images. +/// +static void SerializeGltfModel(Model* model, json& o) +{ + // ACCESSORS + if (model->accessors.size()) { + json accessors; + JsonReserveArray(accessors, model->accessors.size()); + for (unsigned int i = 0; i < model->accessors.size(); ++i) { + json accessor; + SerializeGltfAccessor(model->accessors[i], accessor); + JsonPushBack(accessors, std::move(accessor)); + } + JsonAddMember(o, "accessors", std::move(accessors)); + } + + // ANIMATIONS + if (model->animations.size()) { + json animations; + JsonReserveArray(animations, model->animations.size()); + for (unsigned int i = 0; i < model->animations.size(); ++i) { + if (model->animations[i].channels.size()) { + json animation; + SerializeGltfAnimation(model->animations[i], animation); + JsonPushBack(animations, std::move(animation)); + } + } + + JsonAddMember(o, "animations", std::move(animations)); + } + + // ASSET + json asset; + SerializeGltfAsset(model->asset, asset); + JsonAddMember(o, "asset", std::move(asset)); + + // BUFFERVIEWS + if (model->bufferViews.size()) { + json bufferViews; + JsonReserveArray(bufferViews, model->bufferViews.size()); + for (unsigned int i = 0; i < model->bufferViews.size(); ++i) { + json bufferView; + SerializeGltfBufferView(model->bufferViews[i], bufferView); + JsonPushBack(bufferViews, std::move(bufferView)); + } + JsonAddMember(o, "bufferViews", std::move(bufferViews)); + } + + // Extensions required + if (model->extensionsRequired.size()) { + SerializeStringArrayProperty("extensionsRequired", + model->extensionsRequired, o); + } + + // MATERIALS + if (model->materials.size()) { + json materials; + JsonReserveArray(materials, model->materials.size()); + for (unsigned int i = 0; i < model->materials.size(); ++i) { + json material; + SerializeGltfMaterial(model->materials[i], material); + + if (JsonIsNull(material)) { + // Issue 294. + // `material` does not have any required parameters + // so the result may be null(unmodified) when all material + // parameters have default value. + // + // null is not allowed thus we create an empty JSON object. + JsonSetObject(material); + } + JsonPushBack(materials, std::move(material)); + } + JsonAddMember(o, "materials", std::move(materials)); + } + + // MESHES + if (model->meshes.size()) { + json meshes; + JsonReserveArray(meshes, model->meshes.size()); + for (unsigned int i = 0; i < model->meshes.size(); ++i) { + json mesh; + SerializeGltfMesh(model->meshes[i], mesh); + JsonPushBack(meshes, std::move(mesh)); + } + JsonAddMember(o, "meshes", std::move(meshes)); + } + + // NODES + if (model->nodes.size()) { + json nodes; + JsonReserveArray(nodes, model->nodes.size()); + for (unsigned int i = 0; i < model->nodes.size(); ++i) { + json node; + SerializeGltfNode(model->nodes[i], node); + JsonPushBack(nodes, std::move(node)); + } + JsonAddMember(o, "nodes", std::move(nodes)); + } + + // SCENE + if (model->defaultScene > -1) { + SerializeNumberProperty("scene", model->defaultScene, o); + } + + // SCENES + if (model->scenes.size()) { + json scenes; + JsonReserveArray(scenes, model->scenes.size()); + for (unsigned int i = 0; i < model->scenes.size(); ++i) { + json currentScene; + SerializeGltfScene(model->scenes[i], currentScene); + JsonPushBack(scenes, std::move(currentScene)); + } + JsonAddMember(o, "scenes", std::move(scenes)); + } + + // SKINS + if (model->skins.size()) { + json skins; + JsonReserveArray(skins, model->skins.size()); + for (unsigned int i = 0; i < model->skins.size(); ++i) { + json skin; + SerializeGltfSkin(model->skins[i], skin); + JsonPushBack(skins, std::move(skin)); + } + JsonAddMember(o, "skins", std::move(skins)); + } + + // TEXTURES + if (model->textures.size()) { + json textures; + JsonReserveArray(textures, model->textures.size()); + for (unsigned int i = 0; i < model->textures.size(); ++i) { + json texture; + SerializeGltfTexture(model->textures[i], texture); + JsonPushBack(textures, std::move(texture)); + } + JsonAddMember(o, "textures", std::move(textures)); + } + + // SAMPLERS + if (model->samplers.size()) { + json samplers; + JsonReserveArray(samplers, model->samplers.size()); + for (unsigned int i = 0; i < model->samplers.size(); ++i) { + json sampler; + SerializeGltfSampler(model->samplers[i], sampler); + JsonPushBack(samplers, std::move(sampler)); + } + JsonAddMember(o, "samplers", std::move(samplers)); + } + + // CAMERAS + if (model->cameras.size()) { + json cameras; + JsonReserveArray(cameras, model->cameras.size()); + for (unsigned int i = 0; i < model->cameras.size(); ++i) { + json camera; + SerializeGltfCamera(model->cameras[i], camera); + JsonPushBack(cameras, std::move(camera)); + } + JsonAddMember(o, "cameras", std::move(cameras)); + } + + // EXTENSIONS + SerializeExtensionMap(model->extensions, o); + + auto extensionsUsed = model->extensionsUsed; + + // LIGHTS as KHR_lights_punctual + if (model->lights.size()) { + json lights; + JsonReserveArray(lights, model->lights.size()); + for (unsigned int i = 0; i < model->lights.size(); ++i) { + json light; + SerializeGltfLight(model->lights[i], light); + JsonPushBack(lights, std::move(light)); + } + json khr_lights_cmn; + JsonAddMember(khr_lights_cmn, "lights", std::move(lights)); + json ext_j; + + { + json_const_iterator it; + if (FindMember(o, "extensions", it)) { + JsonAssign(ext_j, GetValue(it)); + } + } + + JsonAddMember(ext_j, "KHR_lights_punctual", std::move(khr_lights_cmn)); + + JsonAddMember(o, "extensions", std::move(ext_j)); + + // Also add "KHR_lights_punctual" to `extensionsUsed` + { + auto has_khr_lights_punctual = + std::find_if(extensionsUsed.begin(), extensionsUsed.end(), + [](const std::string& s) { + return (s.compare("KHR_lights_punctual") == 0); + }); + + if (has_khr_lights_punctual == extensionsUsed.end()) { + extensionsUsed.push_back("KHR_lights_punctual"); + } + } + } + + // Extensions used + if (extensionsUsed.size()) { + SerializeStringArrayProperty("extensionsUsed", extensionsUsed, o); + } + + // EXTRAS + if (model->extras.Type() != NULL_TYPE) { + SerializeValue("extras", model->extras, o); + } +} + +static bool WriteGltfStream(std::ostream& stream, const std::string& content) +{ + stream << content << std::endl; + return true; +} + +static bool WriteGltfFile(const std::string& output, const std::string& content) +{ +#ifdef _WIN32 +#if defined(_MSC_VER) + std::ofstream gltfFile(UTF8ToWchar(output).c_str()); +#elif defined(__GLIBCXX__) + int file_descriptor = _wopen(UTF8ToWchar(output).c_str(), + _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf( + file_descriptor, std::ios_base::out | std::ios_base::binary); + std::ostream gltfFile(&wfile_buf); + if (!wfile_buf.is_open()) + return false; +#else + std::ofstream gltfFile(output.c_str()); + if (!gltfFile.is_open()) + return false; +#endif +#else + std::ofstream gltfFile(output.c_str()); + if (!gltfFile.is_open()) + return false; +#endif + return WriteGltfStream(gltfFile, content); +} + +static bool WriteBinaryGltfStream(std::ostream& stream, + const std::string& content, + const std::vector& binBuffer) +{ + const std::string header = "glTF"; + const int version = 2; + + const uint32_t content_size = uint32_t(content.size()); + const uint32_t binBuffer_size = uint32_t(binBuffer.size()); + // determine number of padding bytes required to ensure 4 byte alignment + const uint32_t content_padding_size = + content_size % 4 == 0 ? 0 : 4 - content_size % 4; + const uint32_t bin_padding_size = + binBuffer_size % 4 == 0 ? 0 : 4 - binBuffer_size % 4; + + // 12 bytes for header, JSON content length, 8 bytes for JSON chunk info. + // Chunk data must be located at 4-byte boundary, which may require padding + const uint32_t length = + 12 + 8 + content_size + content_padding_size + + (binBuffer_size ? (8 + binBuffer_size + bin_padding_size) : 0); + + stream.write(header.c_str(), std::streamsize(header.size())); + stream.write(reinterpret_cast(&version), sizeof(version)); + stream.write(reinterpret_cast(&length), sizeof(length)); + + // JSON chunk info, then JSON data + const uint32_t model_length = + uint32_t(content.size()) + content_padding_size; + const uint32_t model_format = 0x4E4F534A; + stream.write(reinterpret_cast(&model_length), + sizeof(model_length)); + stream.write(reinterpret_cast(&model_format), + sizeof(model_format)); + stream.write(content.c_str(), std::streamsize(content.size())); + + // Chunk must be multiplies of 4, so pad with spaces + if (content_padding_size > 0) { + const std::string padding = + std::string(size_t(content_padding_size), ' '); + stream.write(padding.c_str(), std::streamsize(padding.size())); + } + if (binBuffer.size() > 0) { + // BIN chunk info, then BIN data + const uint32_t bin_length = + uint32_t(binBuffer.size()) + bin_padding_size; + const uint32_t bin_format = 0x004e4942; + stream.write(reinterpret_cast(&bin_length), + sizeof(bin_length)); + stream.write(reinterpret_cast(&bin_format), + sizeof(bin_format)); + stream.write(reinterpret_cast(binBuffer.data()), + std::streamsize(binBuffer.size())); + // Chunksize must be multiplies of 4, so pad with zeroes + if (bin_padding_size > 0) { + const std::vector padding = + std::vector(size_t(bin_padding_size), 0); + stream.write(reinterpret_cast(padding.data()), + std::streamsize(padding.size())); + } + } + + // TODO: Check error on stream.write + return true; +} + +static bool WriteBinaryGltfFile(const std::string& output, + const std::string& content, + const std::vector& binBuffer) +{ +#ifdef _WIN32 +#if defined(_MSC_VER) + std::ofstream gltfFile(UTF8ToWchar(output).c_str(), std::ios::binary); +#elif defined(__GLIBCXX__) + int file_descriptor = _wopen(UTF8ToWchar(output).c_str(), + _O_CREAT | _O_WRONLY | _O_TRUNC | _O_BINARY); + __gnu_cxx::stdio_filebuf wfile_buf( + file_descriptor, std::ios_base::out | std::ios_base::binary); + std::ostream gltfFile(&wfile_buf); +#else + std::ofstream gltfFile(output.c_str(), std::ios::binary); +#endif +#else + std::ofstream gltfFile(output.c_str(), std::ios::binary); +#endif + return WriteBinaryGltfStream(gltfFile, content, binBuffer); +} + +bool TinyGLTF::WriteGltfSceneToStream(Model* model, std::ostream& stream, + bool prettyPrint = true, + bool writeBinary = false) +{ + JsonDocument output; + + /// Serialize all properties except buffers and images. + SerializeGltfModel(model, output); + + // BUFFERS + std::vector binBuffer; + if (model->buffers.size()) { + json buffers; + JsonReserveArray(buffers, model->buffers.size()); + for (unsigned int i = 0; i < model->buffers.size(); ++i) { + json buffer; + if (writeBinary && i == 0 && model->buffers[i].uri.empty()) { + SerializeGltfBufferBin(model->buffers[i], buffer, binBuffer); + } + else { + SerializeGltfBuffer(model->buffers[i], buffer); + } + JsonPushBack(buffers, std::move(buffer)); + } + JsonAddMember(output, "buffers", std::move(buffers)); + } + + // IMAGES + if (model->images.size()) { + json images; + JsonReserveArray(images, model->images.size()); + for (unsigned int i = 0; i < model->images.size(); ++i) { + json image; + + std::string dummystring = ""; + // UpdateImageObject need baseDir but only uses it if embeddedImages + // is enabled, since we won't write separate images when writing to + // a stream we + UpdateImageObject(model->images[i], dummystring, int(i), true, + &this->WriteImageData, + this->write_image_user_data_); + SerializeGltfImage(model->images[i], image); + JsonPushBack(images, std::move(image)); + } + JsonAddMember(output, "images", std::move(images)); + } + + if (writeBinary) { + return WriteBinaryGltfStream(stream, JsonToString(output), binBuffer); + } + else { + return WriteGltfStream(stream, + JsonToString(output, prettyPrint ? 2 : -1)); + } +} + +bool TinyGLTF::WriteGltfSceneToFile(Model* model, const std::string& filename, + bool embedImages = false, + bool embedBuffers = false, + bool prettyPrint = true, + bool writeBinary = false) +{ + JsonDocument output; + std::string defaultBinFilename = GetBaseFilename(filename); + std::string defaultBinFileExt = ".bin"; + std::string::size_type pos = + defaultBinFilename.rfind('.', defaultBinFilename.length()); + + if (pos != std::string::npos) { + defaultBinFilename = defaultBinFilename.substr(0, pos); + } + std::string baseDir = GetBaseDir(filename); + if (baseDir.empty()) { + baseDir = "./"; + } + /// Serialize all properties except buffers and images. + SerializeGltfModel(model, output); + + // BUFFERS + std::vector usedUris; + std::vector binBuffer; + if (model->buffers.size()) { + json buffers; + JsonReserveArray(buffers, model->buffers.size()); + for (unsigned int i = 0; i < model->buffers.size(); ++i) { + json buffer; + if (writeBinary && i == 0 && model->buffers[i].uri.empty()) { + SerializeGltfBufferBin(model->buffers[i], buffer, binBuffer); + } + else if (embedBuffers) { + SerializeGltfBuffer(model->buffers[i], buffer); + } + else { + std::string binSavePath; + std::string binUri; + if (!model->buffers[i].uri.empty() && + !IsDataURI(model->buffers[i].uri)) { + binUri = model->buffers[i].uri; + } + else { + binUri = defaultBinFilename + defaultBinFileExt; + bool inUse = true; + int numUsed = 0; + while (inUse) { + inUse = false; + for (const std::string& usedName : usedUris) { + if (binUri.compare(usedName) != 0) + continue; + inUse = true; + binUri = defaultBinFilename + + std::to_string(numUsed++) + + defaultBinFileExt; + break; + } + } + } + usedUris.push_back(binUri); + binSavePath = JoinPath(baseDir, binUri); + if (!SerializeGltfBuffer(model->buffers[i], buffer, binSavePath, + binUri)) { + return false; + } + } + JsonPushBack(buffers, std::move(buffer)); + } + JsonAddMember(output, "buffers", std::move(buffers)); + } + + // IMAGES + if (model->images.size()) { + json images; + JsonReserveArray(images, model->images.size()); + for (unsigned int i = 0; i < model->images.size(); ++i) { + json image; + + UpdateImageObject(model->images[i], baseDir, int(i), embedImages, + &this->WriteImageData, + this->write_image_user_data_); + SerializeGltfImage(model->images[i], image); + JsonPushBack(images, std::move(image)); + } + JsonAddMember(output, "images", std::move(images)); + } + + if (writeBinary) { + return WriteBinaryGltfFile(filename, JsonToString(output), binBuffer); + } + else { + return WriteGltfFile(filename, + JsonToString(output, (prettyPrint ? 2 : -1))); + } +} + +} // namespace tinygltf + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif // TINYGLTF_IMPLEMENTATION \ No newline at end of file diff --git a/src/viewer3d/iblsampler.cpp b/src/viewer3d/iblsampler.cpp new file mode 100644 index 00000000..1fae6091 --- /dev/null +++ b/src/viewer3d/iblsampler.cpp @@ -0,0 +1,546 @@ +#include "iblsampler.h" +#include "shadercache.h" +#include + +#define STB_IMAGE_IMPLEMENTATION +#include "gltf/stb_image.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +enum class VertexUsage : int { + Position = 0, + Color = 1, + TexCoord0 = 2, + TexCoord1 = 3, + TexCoord2 = 4, + TexCoord3 = 5, + Normal = 6, + Tangent = 7, + Count = 8 +}; + +void createSampleCube(QOpenGLFunctions* gl) +{ + GLuint tex; + gl->glGenTextures(1, &tex); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, tex); + + for (unsigned int i = 0; i < 6; ++i) { + gl->glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 256, + 256, 0, GL_RGB, GL_FLOAT, nullptr); + } + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, + GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, + GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, + GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + gl->glGenerateMipmap(GL_TEXTURE_CUBE_MAP); +} + +IblSampler::IblSampler() +{ + shaderCache = new ShaderCache(); + shaderCache->addShaderFile("fullscreen.vert", ":assets/fullscreen.vert"); + shaderCache->addShaderFile("panorama_to_cubemap.frag", + ":assets/panorama_to_cubemap.frag"); + shaderCache->addShaderFile("ibl_filtering.frag", + ":assets/ibl_filtering.frag"); +} + +void IblSampler::init(const QString& panoramaPath) +{ + // THIS SOLVES THE ISSUE CAUSING BANDING TO SHOW + // AROUND BRIGHT AREAS NEAR SEEMS IN LAMBERTIAN CUBEMAP!!! + gl->glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); + + mipmapLevels = + std::floor(std::log2(this->textureSize)) + 1 - this->lowestMipLevel; + + framebuffer = new QOpenGLFramebufferObject(textureSize, textureSize); + if (!framebuffer->isValid()) { + qFatal("FBO could not be created"); + } + // setup vbo + vao = new QOpenGLVertexArrayObject; + if (vao->create()) + vao->bind(); + + // setup vertex array + QVector data; + // TRIANGLE 1 + data.append(-1); + data.append(-1); + data.append(0); + + data.append(0); + data.append(0); + + data.append(1); + data.append(-1); + data.append(0); + + data.append(1); + data.append(0); + + data.append(-1); + data.append(1); + data.append(0); + + data.append(0); + data.append(1); + + // TRIANGLE 2 + data.append(-1); + data.append(1); + data.append(0); + + data.append(0); + data.append(1); + + data.append(1); + data.append(-1); + data.append(0); + + data.append(1); + data.append(0); + + data.append(1); + data.append(1); + data.append(0); + + data.append(1); + data.append(1); + + vbo = new QOpenGLBuffer; + vbo->create(); + vbo->bind(); + vbo->allocate(data.constData(), data.count() * sizeof(float)); + vbo->release(); + + this->loadPanorama(panoramaPath); + + // createSampleCube(gl); + + // cubemapTexture = this->createCubemap(true); + // lambertianTexture = this->createCubemap(false); + // ggxTexture = this->createCubemap(true); + // sheenTexture = this->createCubemap(true); + + // ggxLutTexture = this->createLut(); + // charlieLutTexture = this->createLut(); + + cubemapTextureID = this->createCubemap(true); + lambertianTextureID = this->createCubemap(false); + ggxTextureID = this->createCubemap(true); + sheenTextureID = this->createCubemap(true); + + ggxLutTextureID = this->createLut(); + charlieLutTextureID = this->createLut(); + + // ggxTexture->bind(); + // ggxTexture->generateMipMaps(0); + // sheenTexture->bind(); + // sheenTexture->generateMipMaps(0); + // sheenTexture->release(); + // gl->glBindTexture(GL_TEXTURE_CUBE_MAP, ggxTexture->textureId()); + // gl->glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + // gl->glBindTexture(GL_TEXTURE_CUBE_MAP, sheenTexture->textureId()); + // gl->glGenerateMipmap(GL_TEXTURE_CUBE_MAP); +} + +void IblSampler::filterAll() +{ + this->panoramaToCubemap(); + this->cubeMapToLambertian(); + this->cubeMapToGGX(); + this->cubeMapToSheen(); + + this->sampleGGXLut(); + this->sampleCharlieLut(); +} + +// stbi_loadf_from_memory +// https://stackoverflow.com/questions/32666824/qopengltexture-qt-from-raw-data-freeimage +void IblSampler::loadPanorama(const QString& path) +{ + int width, height, numComponents; + QFile file(path); + file.open(QFile::ReadOnly); + auto byteArray = file.readAll(); + + auto textureData = stbi_loadf_from_memory( + (unsigned char*)byteArray.constData(), byteArray.length(), &width, + &height, &numComponents, 3); + + if (!textureData) { + qFatal("Failed to load HDR panorama: %s", stbi_failure_reason()); + return; + } + + // qDebug() << "Loaded HDR:" << width << "x" << height + // << "components:" << numComponents; + + QOpenGLTexture* text = new QOpenGLTexture(QOpenGLTexture::Target2D); + text->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, + QOpenGLTexture::Linear); + text->setWrapMode(QOpenGLTexture::MirroredRepeat); + text->create(); + + // given some `width`, `height` and `data_ptr` + text->setSize(width, height, 1); + text->setFormat(QOpenGLTexture::RGB32F); + text->allocateStorage(); + text->setData(QOpenGLTexture::RGB, QOpenGLTexture::Float32, textureData); + + // Free stb_image buffer after uploading to GPU + stbi_image_free(textureData); + + inputTexture = text; +} + +// https://stackoverflow.com/questions/50666781/create-cubemap-from-qopenglframebuffer + +// QOpenGLTexture* IblSampler::createCubemap(bool withMipmaps) +// { +// auto cubemap = new QOpenGLTexture(QOpenGLTexture::TargetCubeMap); +// cubemap->create(); + +// if (withMipmaps) +// cubemap->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, +// QOpenGLTexture::Linear); +// else +// cubemap->setMinMagFilters(QOpenGLTexture::Linear, +// QOpenGLTexture::Linear); + +// cubemap->setWrapMode(QOpenGLTexture::ClampToEdge); +// cubemap->setSize(textureSize, textureSize, 1); + +// // cubemap->setMipLevels() +// cubemap->setFormat(QOpenGLTexture::RGB16F); +// cubemap->allocateStorage(); + +// cubemap->generateMipMaps(0); +// // cubemap->bind(); +// // gl->glGenerateMipmap(GL_TEXTURE_CUBE_MAP); +// // cubemap->release(); +// // cubemap->setMipBaseLevel(0); +// // cubemap->setMipMaxLevel(11); + +// return cubemap; +// } + +// QOpenGLTexture* IblSampler::createLut() +// { +// auto texture = new QOpenGLTexture(QOpenGLTexture::Target2D); +// texture->setMinMagFilters(QOpenGLTexture::Linear, +// QOpenGLTexture::Linear); +// texture->setWrapMode(QOpenGLTexture::ClampToEdge); +// texture->create(); + +// texture->setSize(textureSize, textureSize, 3); +// // cubemap->setMipLevels() +// texture->setFormat(QOpenGLTexture::RGBA32F); +// texture->allocateStorage(); + +// return texture; +// } + +GLuint IblSampler::createCubemap(bool withMipmaps) +{ + GLuint tex; + gl->glGenTextures(1, &tex); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, tex); + + for (unsigned int i = 0; i < 6; ++i) { + gl->glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB32F, + textureSize, textureSize, 0, GL_RGB, GL_FLOAT, + nullptr); + } + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, + GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, + GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, + GL_CLAMP_TO_EDGE); + + if (withMipmaps) + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, + GL_LINEAR_MIPMAP_LINEAR); + else + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, + GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + if (withMipmaps) + gl->glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + + return tex; +} + +GLuint IblSampler::createLut() +{ + GLuint tex; + gl->glGenTextures(1, &tex); + gl->glBindTexture(GL_TEXTURE_2D, tex); + + gl->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, lutResolution, lutResolution, + 0, GL_RGBA, GL_FLOAT, nullptr); + + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + return tex; +} + +void IblSampler::panoramaToCubemap() +{ + // todo: cleanup old cubemap + + auto vertSource = shaderCache->generateShaderSource("fullscreen.vert"); + auto fragSource = + shaderCache->generateShaderSource("panorama_to_cubemap.frag"); + + auto shader = createShader(vertSource, fragSource); + + // render each face + for (int i = 0; i < 6; ++i) { + gl->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->handle()); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, + cubemapTextureID, 0); + + gl->glViewport(0, 0, textureSize, textureSize); + gl->glClearColor(1.0, 0, 0, 0); + gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + shader->bind(); + inputTexture->bind(0); + + shader->setUniformValue("u_panorama", 0); + shader->setUniformValue("u_currentFace", i); + + vbo->bind(); + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), nullptr); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), + reinterpret_cast(3 * sizeof(float))); + + gl->glDrawArrays(GL_TRIANGLES, 0, 6); + + vbo->release(); + } + + // gl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + auto ctx = QOpenGLContext::currentContext(); + gl->glBindFramebuffer(GL_FRAMEBUFFER, ctx->defaultFramebufferObject()); + + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTextureID); + gl->glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, 0); +} + +QOpenGLShaderProgram* IblSampler::createShader(const QString& vertSource, + const QString& fragSource) +{ + QOpenGLShader* vshader = new QOpenGLShader(QOpenGLShader::Vertex); + QOpenGLShader* fshader = new QOpenGLShader(QOpenGLShader::Fragment); + auto program = new QOpenGLShaderProgram; + + if (!vshader->compileSourceCode(vertSource)) { + qDebug() << "VERTEX SHADER ERROR"; + qDebug() << vshader->log(); + } + + if (!fshader->compileSourceCode(fragSource)) { + qDebug() << "FRAGMENT SHADER ERROR"; + qDebug() << fshader->log(); + } + + program->removeAllShaders(); + + program->addShader(vshader); + program->addShader(fshader); + + program->bindAttributeLocation("a_pos", (int)VertexUsage::Position); + program->bindAttributeLocation("a_color", (int)VertexUsage::Color); + program->bindAttributeLocation("a_texCoord", (int)VertexUsage::TexCoord0); + + if (!program->link()) { + qDebug() << "SHADER LINK ERROR"; + qDebug() << program->log(); + } + + return program; +} + +void IblSampler::applyFilter(int distribution, float roughness, + int targetMipLevel, int targetTexture, + int sampleCount, float lodBias) +{ + auto currentTextureSize = this->textureSize >> targetMipLevel; + + auto vertSource = shaderCache->generateShaderSource("fullscreen.vert"); + auto fragSource = shaderCache->generateShaderSource("ibl_filtering.frag"); + + auto shader = createShader(vertSource, fragSource); + + // render each face + for (int i = 0; i < 6; ++i) { + gl->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->handle()); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, + targetTexture, targetMipLevel); + + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, targetTexture); + + gl->glViewport(0, 0, currentTextureSize, currentTextureSize); + gl->glClearColor(1, 0, 0, 0); + gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + shader->bind(); + gl->glActiveTexture(GL_TEXTURE0); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTextureID); + // cubemapTexture->bind(0); + shader->setUniformValue("uCubeMap", 0); + + shader->setUniformValue("u_roughness", roughness); + shader->setUniformValue("u_sampleCount", sampleCount); + shader->setUniformValue("u_width", textureSize); + shader->setUniformValue("u_lodBias", lodBias); + shader->setUniformValue("u_distribution", distribution); + shader->setUniformValue("u_currentFace", i); + shader->setUniformValue("u_isGeneratingLUT", 0); + shader->setUniformValue("u_floatTexture", 1); + shader->setUniformValue("u_intensityScale", (GLfloat)intensityScale); + + vbo->bind(); + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), nullptr); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), + reinterpret_cast(3 * sizeof(float))); + + gl->glDrawArrays(GL_TRIANGLES, 0, 3); + + vbo->release(); + } + + // gl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + auto ctx = QOpenGLContext::currentContext(); + gl->glBindFramebuffer(GL_FRAMEBUFFER, ctx->defaultFramebufferObject()); +} + +void IblSampler::cubeMapToLambertian() +{ + this->applyFilter(0, 0.0, 0, this->lambertianTextureID, + this->lambertianSampleCount); + + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, lambertianTextureID); + gl->glGenerateMipmap(GL_TEXTURE_CUBE_MAP); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, 0); +} + +void IblSampler::cubeMapToGGX() +{ + // qDebug() << "MIPMAP LEVELS: " << this->mipmapLevels; + for (int currentMipLevel = 0; currentMipLevel < this->mipmapLevels; + ++currentMipLevel) { + auto roughness = ((float)currentMipLevel) / (this->mipmapLevels - 1.0); + + // qDebug() << "ROUGHNESS: " << roughness << " MIP" << currentMipLevel; + this->applyFilter(1, roughness, currentMipLevel, this->ggxTextureID, + this->ggxSampleCount); + } +} +void IblSampler::cubeMapToSheen() +{ + for (auto currentMipLevel = 0; currentMipLevel < this->mipmapLevels; + ++currentMipLevel) { + auto roughness = ((float)currentMipLevel) / (this->mipmapLevels - 1.0); + this->applyFilter(2, roughness, currentMipLevel, this->sheenTextureID, + this->sheenSamplCount); + } +} + +void IblSampler::sampleLut(int distribution, int targetTextureId, + int currentTextureSize) +{ + auto vertSource = shaderCache->generateShaderSource("fullscreen.vert"); + auto fragSource = shaderCache->generateShaderSource("ibl_filtering.frag"); + + auto shader = createShader(vertSource, fragSource); + + // render each face + gl->glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->handle()); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, targetTextureId, 0); + + gl->glViewport(0, 0, currentTextureSize, currentTextureSize); + gl->glClearColor(0, 0, 0, 1); + gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + shader->bind(); + // cubemapTexture->bind(0); + gl->glActiveTexture(GL_TEXTURE0); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTextureID); + shader->setUniformValue("uCubeMap", 0); + + shader->setUniformValue("u_roughness", 0.0f); + shader->setUniformValue("u_sampleCount", 512); + shader->setUniformValue("u_width", 0.0f); + shader->setUniformValue("u_lodBias", 0.0f); + shader->setUniformValue("u_distribution", distribution); + shader->setUniformValue("u_currentFace", 0); + shader->setUniformValue("u_isGeneratingLUT", 1); + shader->setUniformValue("u_floatTexture", 1); + shader->setUniformValue("u_intensityScale", (GLfloat)intensityScale); + + vbo->bind(); + gl->glEnableVertexAttribArray((int)VertexUsage::Position); + gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + gl->glVertexAttribPointer((int)VertexUsage::Position, 3, GL_FLOAT, GL_FALSE, + 5 * sizeof(float), nullptr); + gl->glVertexAttribPointer((int)VertexUsage::TexCoord0, 2, GL_FLOAT, + GL_FALSE, 5 * sizeof(float), + reinterpret_cast(3 * sizeof(float))); + + gl->glDrawArrays(GL_TRIANGLES, 0, 6); + + vbo->release(); + + // gl->glBindFramebuffer(GL_FRAMEBUFFER, 0); + auto ctx = QOpenGLContext::currentContext(); + gl->glBindFramebuffer(GL_FRAMEBUFFER, ctx->defaultFramebufferObject()); +} + +void IblSampler::sampleGGXLut() +{ + this->ggxLutTextureID = this->createLut(); + this->sampleLut(1, this->ggxLutTextureID, this->lutResolution); +} + +void IblSampler::sampleCharlieLut() +{ + this->charlieLutTextureID = this->createLut(); + this->sampleLut(2, this->charlieLutTextureID, this->lutResolution); +} diff --git a/src/viewer3d/iblsampler.h b/src/viewer3d/iblsampler.h new file mode 100644 index 00000000..ae88e63e --- /dev/null +++ b/src/viewer3d/iblsampler.h @@ -0,0 +1,83 @@ +#pragma once + +#include + +class QOpenGLFramebufferObject; +class QOpenGLFunctions; +class QOpenGLTexture; +class QOpenGLBuffer; +class QOpenGLVertexArrayObject; +class ShaderCache; +class QOpenGLShaderProgram; + +// reference: +// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/ibl_sampler.js + +class IblSampler { +public: + QOpenGLFunctions* gl; + + int textureSize = 256; + int ggxSampleCount = 1024; + int lambertianSampleCount = 2048; + int sheenSamplCount = 64; + float lodBias = 0.0; + int lowestMipLevel = 4; + int lutResolution = 1024; + float intensityScale = 1.0; + + int mipmapLevels = -1; + + // QOpenGLTexture* lambertianTexture; + // QOpenGLTexture* ggxTexture; + // QOpenGLTexture* sheenTexture; + GLuint lambertianTextureID; + GLuint ggxTextureID; + GLuint sheenTextureID; + + // QOpenGLTexture* ggxLutTexture; + // QOpenGLTexture* charlieLutTexture; + GLuint ggxLutTextureID; + GLuint charlieLutTextureID; + + QOpenGLTexture* inputTexture; + // QOpenGLTexture* cubemapTexture; + // GLuint inputTextureID; + GLuint cubemapTextureID; + + QOpenGLFramebufferObject* framebuffer; + QOpenGLVertexArrayObject* vao; + QOpenGLBuffer* vbo; + + ShaderCache* shaderCache; + + IblSampler(); + + void loadPanorama(const QString& path); + + // QOpenGLTexture* createCubemap(bool withMipmaps); + // QOpenGLTexture* createLut(); + GLuint createCubemap(bool withMipmaps); + GLuint createLut(); + + // generation functions + void panoramaToCubemap(); + void cubeMapToLambertian(); + void cubeMapToGGX(); + void cubeMapToSheen(); + + void sampleGGXLut(); + void sampleCharlieLut(); + + void applyFilter(int distribution, float roughness, int targetMipLevel, + int targetTexture, int sampleCount, float lodBias = 0.0); + void sampleLut(int distribution, int targetTextureId, + int currentTextureSize); + + void init(const QString& panoramaPath); + + QOpenGLShaderProgram* createShader(const QString& vertSource, + const QString& fragSource); + + void filterAll(); +}; \ No newline at end of file diff --git a/src/viewer3d/main.cpp b/src/viewer3d/main.cpp new file mode 100644 index 00000000..3a0eec34 --- /dev/null +++ b/src/viewer3d/main.cpp @@ -0,0 +1,47 @@ +#include "viewer3d.h" +#include +#include +#include + +#include +#include +#include + +int main(int argc, char* argv[]) +{ + QApplication a(argc, argv); + QMainWindow w; + + // init gl + QSurfaceFormat fmt; + fmt.setDepthBufferSize(24); + + // Request OpenGL 3.3 core or OpenGL ES 3.0. + // if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { + qDebug("Requesting 3.3 core context"); + fmt.setVersion(3, 3); + fmt.setProfile(QSurfaceFormat::CoreProfile); + // https://doc.qt.io/qt-6/qopengldebuglogger.html + fmt.setOption(QSurfaceFormat::DebugContext); + // } + // else { + // qDebug("Requesting 3.0 context"); + // fmt.setVersion(3, 0); + // } + + QSurfaceFormat::setDefaultFormat(fmt); + + auto viewer = new Viewer3D(); + + // use viewer as you would in texturlab project + // auto albedoId = loadTexture(); + // viewer->setAlbedoTexture(albedoId); + + w.setCentralWidget(viewer); + w.resize(800, 600); + + w.show(); + w.showMaximized(); + + return a.exec(); +} diff --git a/src/viewer3d/renderer/renderer.cpp b/src/viewer3d/renderer/renderer.cpp new file mode 100644 index 00000000..ff9b282b --- /dev/null +++ b/src/viewer3d/renderer/renderer.cpp @@ -0,0 +1,571 @@ +#include "renderer.h" +#include "../gltf/gltf.cpp" +#include "../iblsampler.h" +#include "../shadercache.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TINYGLTF_IMPLEMENTATION +#define TINYGLTF_NO_STB_IMAGE +#define TINYGLTF_NO_INCLUDE_STB_IMAGE +#define TINYGLTF_NO_STB_IMAGE_WRITE +#include "../gltf/tiny_gltf.h" + +class MeshPrivate { +public: + tinygltf::Primitive primitive; + tinygltf::Accessor indexAccessor; +}; + +Mesh::~Mesh() +{ + // For glTF meshes, indexBuffer aliases one of the vbos entries (see + // loadMeshFromRc), so delete it only if it isn't already owned by vbos. + bool indexAliased = false; + for (auto& kv : vbos) { + if (kv.second == indexBuffer) + indexAliased = true; + delete kv.second; + } + if (indexBuffer && !indexAliased) + delete indexBuffer; + delete vao; + // material is not owned by the mesh (shared, owned by Viewer3D) — not freed. +} + +void Renderer::init(QOpenGLFunctions* gl) +{ + this->gl = gl; + + // init shader cache + shaderCache = new ShaderCache(); + shaderCache->addShaderFile("animation.glsl", ":assets/animation.glsl"); + shaderCache->addShaderFile("brdf.glsl", ":assets/brdf.glsl"); + shaderCache->addShaderFile("functions.glsl", ":assets/functions.glsl"); + shaderCache->addShaderFile("iridiscence.glsl", ":assets/iridiscence.glsl"); + shaderCache->addShaderFile("ibl.glsl", ":assets/ibl.glsl"); + shaderCache->addShaderFile("material_info.glsl", + ":assets/material_info.glsl"); + shaderCache->addShaderFile("pbr.frag", ":assets/pbr.frag"); + shaderCache->addShaderFile("primitive.vert", ":assets/primitive.vert"); + shaderCache->addShaderFile("punctual.glsl", ":assets/punctual.glsl"); + shaderCache->addShaderFile("textures.glsl", ":assets/textures.glsl"); + shaderCache->addShaderFile("tonemapping.glsl", ":assets/tonemapping.glsl"); + + // init ibls + iblSampler = new IblSampler(); + iblSampler->gl = gl; +} + +void Renderer::loadEnvironment(const QString& path, float rotationDegrees) +{ + this->envRotation = rotationDegrees; + iblSampler->init(path); + iblSampler->filterAll(); +} + +void Renderer::setEnvironmentRotation(float degrees) +{ + this->envRotation = degrees; +} + +QMatrix3x3 Renderer::envRotationMatrix() const +{ + const float rad = qDegreesToRadians(envRotation); + const float c = std::cos(rad); + const float s = std::sin(rad); + + // Row-major yaw about the up (Y) axis. The shaders apply it to the lookup + // direction, which turns the environment itself by the same angle: content + // sitting at azimuth a ends up seen at azimuth a + envRotation. + const float values[9] = {c, 0.0f, s, 0.0f, 1.0f, 0.0f, -s, 0.0f, c}; + return QMatrix3x3(values); +} + +void Renderer::renderMesh(Mesh* mesh, Material* material) {} + +void Renderer::updateMaterial(Material* material) +{ + if (material->shader) { + material->shader->deleteLater(); + } + + // flags + QStringList flags; + + // the default + // todo: make this dynamic instead of using flags + flags << "USE_IBL 1"; + + // determine attribs from mesh + flags << "HAS_NORMAL_VEC3 1"; + flags << "HAS_TEXCOORD_0_VEC2 1"; + flags << "HAS_TANGENT_VEC4 1"; + + flags << "DEBUG_NONE 1"; // IMPORTANT!! caused many headaches.. + flags << "DEBUG DEBUG_NONE"; + + if (material->albedoMapId != 0) + flags << "HAS_BASE_COLOR_MAP 1"; + if (material->normalMapId != 0) + flags << "HAS_NORMAL_MAP 1"; + if (material->metalnessMapId != 0) + flags << "HAS_METALNESS_MAP 1"; + if (material->roughnessMapId != 0) + flags << "HAS_ROUGHNESS_MAP 1"; + if (material->heightMapId != 0) + flags << "HAS_HEIGHT_MAP 1"; + if (material->aoMapId != 0) + flags << "HAS_OCCLUSION_MAP 1"; + if (material->alphaMapId != 0) + flags << "HAS_ALPHA_MAP 1"; + // flags << "HAS_NORMAL_MAP 1"; + // flags << "HAS_ROUGHNESS_MAP 1"; + // flags << "HAS_METALNESS_MAP 1"; + // flags << "HAS_METALLIC_ROUGHNESS_MAP 1"; + // flags << "HAS_EMISSIVE_MAP 1"; + flags << "MATERIAL_METALLICROUGHNESS 1"; // MR mode + + // Enable punctual lighting (matches Three.js) - toggleable + if (this->usePunctualLights) { + flags << "USE_PUNCTUAL 1"; + flags << "LIGHT_COUNT 3"; + } + + // blending + flags << "ALPHAMODE_OPAQUE 0"; + flags << "ALPHAMODE_MASK 1"; + flags << "ALPHAMODE_BLEND 2"; + flags << "ALPHAMODE ALPHAMODE_BLEND"; + + // tone mapping (match 3js as much as we can) + flags << "TONEMAP_ACES_HILL 1"; + flags << "TONEMAP_ACES_HILL_EXPOSURE_BOOST 1"; + + // build shader + auto vertShader = + shaderCache->generateShaderSource("primitive.vert", flags); + auto fragShader = shaderCache->generateShaderSource("pbr.frag", flags); + + // qDebug() << "Generated shader with flags:" << flags; + // qDebug() << fragShader; + + auto shader = new QOpenGLShaderProgram; + shader->bind(); + shader->addShaderFromSourceCode(QOpenGLShader::Vertex, vertShader); + shader->addShaderFromSourceCode(QOpenGLShader::Fragment, fragShader); + + shader->bindAttributeLocation("a_position", (int)VertexUsage::Position); + shader->bindAttributeLocation("a_normal", (int)VertexUsage::Normal); + shader->bindAttributeLocation("a_tangent", (int)VertexUsage::Tangent); + shader->bindAttributeLocation("a_color_0", (int)VertexUsage::Color); + shader->bindAttributeLocation("a_texcoord_0", (int)VertexUsage::TexCoord0); + shader->bindAttributeLocation("a_texcoord_1", (int)VertexUsage::TexCoord1); + + shader->link(); + + material->shader = shader; + material->needsUpdate = false; +} + +#define BUFFER_OFFSET(i) ((char*)NULL + (i)) +bool loadGltfModel(tinygltf::Model& model, const QString& filename); + +// https://github.com/syoyo/tinygltf/blob/release/examples/basic/main.cpp +Mesh* loadMeshFromRc(const QString& path) +{ + auto gl = QOpenGLContext::currentContext()->functions(); + + tinygltf::Model model; + if (!loadGltfModel(model, path)) { + return nullptr; + } + + // just convert the first mesh + if (model.meshes.size() == 0) + return nullptr; + + auto mesh = model.meshes[0]; + + auto vao = new QOpenGLVertexArrayObject(nullptr); + vao->create(); + vao->bind(); + + std::map vbos; + + // upload all model buffer views into GPU memory + for (size_t i = 0; i < model.bufferViews.size(); ++i) { + const tinygltf::BufferView& bufferView = model.bufferViews[i]; + if (bufferView.target == 0) { // TODO impl drawarrays + std::cout << "WARN: bufferView.target is zero" << std::endl; + continue; // Unsupported bufferView. + } + + const tinygltf::Buffer& buffer = model.buffers[bufferView.buffer]; + std::cout << "bufferview.target " << bufferView.target << std::endl; + + // vertex or index buffer + auto vbo = new QOpenGLBuffer((QOpenGLBuffer::Type)bufferView.target); + vbo->create(); + vbo->bind(); + vbo->setUsagePattern(QOpenGLBuffer::StaticDraw); + vbo->allocate(&buffer.data.at(0) + bufferView.byteOffset, + bufferView.byteLength); + vbo->release(); + + vbos[i] = vbo; + } + + tinygltf::Primitive primitive = mesh.primitives[0]; + tinygltf::Accessor indexAccessor = model.accessors[primitive.indices]; + QList attribs; + + // assign vertex channels to buffers + for (auto& attrib : primitive.attributes) { + tinygltf::Accessor accessor = model.accessors[attrib.second]; + int byteStride = + accessor.ByteStride(model.bufferViews[accessor.bufferView]); + // glBindBuffer(GL_ARRAY_BUFFER, vbos[accessor.bufferView]); + vbos[accessor.bufferView]->bind(); + + int size = 1; + if (accessor.type != TINYGLTF_TYPE_SCALAR) { + size = accessor.type; + } + + int vaa = -1; + if (attrib.first.compare("POSITION") == 0) + vaa = (int)VertexUsage::Position; + if (attrib.first.compare("NORMAL") == 0) + vaa = (int)VertexUsage::Normal; + if (attrib.first.compare("TANGENT") == 0) + vaa = (int)VertexUsage::Tangent; + if (attrib.first.compare("TEXCOORD_0") == 0) + vaa = (int)VertexUsage::TexCoord0; + if (attrib.first.compare("TEXCOORD_1") == 0) + vaa = (int)VertexUsage::TexCoord1; + if (attrib.first.compare("TEXCOORD_2") == 0) + vaa = (int)VertexUsage::TexCoord2; + if (vaa > -1) { + gl->glEnableVertexAttribArray(vaa); + gl->glVertexAttribPointer(vaa, size, accessor.componentType, + accessor.normalized ? GL_TRUE : GL_FALSE, + byteStride, + BUFFER_OFFSET(accessor.byteOffset)); + attribs.append((VertexUsage)vaa); + } + else + std::cout << "vaa missing: " << attrib.first << std::endl; + } + + vao->release(); + + Mesh* finalMesh = new Mesh; + finalMesh->vao = vao; + finalMesh->vbos = vbos; + finalMesh->attribs = attribs; + + finalMesh->indexBuffer = vbos.at(indexAccessor.bufferView); + // finalMesh->primitive = primitive; + // finalMesh->indexAccessor = indexAccessor; + finalMesh->primitiveMode = primitive.mode; + finalMesh->numElements = indexAccessor.count; + finalMesh->indexType = indexAccessor.componentType; + finalMesh->indexByteOffset = indexAccessor.byteOffset; + + return finalMesh; +} + +// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/glTF-WebGL-PBR/mesh.js#L113 +// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/Renderer/renderer.js +void Renderer::renderGltfMesh(Mesh* mesh, Material* material, + const QVector3D& camPos, + const QMatrix4x4& worldMatrix, + const QMatrix4x4& viewMatrix, + const QMatrix4x4& projMatrix) +{ + if (!mesh || !material) + return; + + // setup material + auto mat = material; + if (mat->needsUpdate) { + this->updateMaterial(mat); + } + + auto shader = mat->shader; + shader->bind(); + + QMatrix4x4 mvp; + mvp.setToIdentity(); + mvp = projMatrix * viewMatrix * worldMatrix; + + auto modelInverse = worldMatrix.inverted(); + auto normalMatrix = modelInverse.transposed(); + + shader->setUniformValue("u_ViewProjectionMatrix", projMatrix * viewMatrix); + shader->setUniformValue("u_ModelMatrix", worldMatrix); + shader->setUniformValue("u_NormalMatrix", normalMatrix); + shader->setUniformValue("u_Exposure", 1.0f); + shader->setUniformValue("u_Camera", camPos); + + // default mat props + shader->setUniformValue("u_BaseColorFactor", material->albedo); + // Match Three.js: factor 1.0 when map present, otherwise use material value + shader->setUniformValue("u_MetallicFactor", material->metalnessMapId != 0 + ? 1.0f + : material->metalness); + shader->setUniformValue("u_RoughnessFactor", material->roughnessMapId != 0 + ? 1.0f + : material->roughness); + shader->setUniformValue("u_EmissiveStrength", 0.f); + shader->setUniformValue("u_NormalScale", material->normalIntensity); + // uv sets + shader->setUniformValue("u_BaseColorUVSet", 0); + shader->setUniformValue("u_NormalUVSet", 0); + shader->setUniformValue("u_MetalnessUVSet", 0); + shader->setUniformValue("u_RoughnessUVSet", 0); + shader->setUniformValue("u_EmissiveUVSet", 0); + shader->setUniformValue("u_MetallicRoughnessUVSet", 0); + + auto bindLinear = [&](GLuint texId) { + gl->glBindTexture(GL_TEXTURE_2D, texId); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + gl->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + }; + + shader->setUniformValue("u_BaseColorSampler", 0); + gl->glActiveTexture(GL_TEXTURE0); + bindLinear(mat->albedoMapId); + + shader->setUniformValue("u_NormalSampler", 1); + gl->glActiveTexture(GL_TEXTURE1); + bindLinear(mat->normalMapId); + + shader->setUniformValue("u_MetalnessSampler", 2); + gl->glActiveTexture(GL_TEXTURE2); + bindLinear(mat->metalnessMapId); + + shader->setUniformValue("u_RoughnessSampler", 3); + gl->glActiveTexture(GL_TEXTURE3); + bindLinear(mat->roughnessMapId); + + shader->setUniformValue("u_HeightSampler", 4); + gl->glActiveTexture(GL_TEXTURE4); + bindLinear(mat->heightMapId); + + shader->setUniformValue("u_HeightScale", material->heightScale); + + shader->setUniformValue("u_OcclusionSampler", 5); + gl->glActiveTexture(GL_TEXTURE5); + bindLinear(mat->aoMapId); + shader->setUniformValue("u_OcclusionUVSet", 0); + shader->setUniformValue("u_OcclusionStrength", 1.0f); + + shader->setUniformValue("u_AlphaSampler", 6); + gl->glActiveTexture(GL_TEXTURE6); + bindLinear(mat->alphaMapId); + shader->setUniformValue("u_AlphaUVSet", 0); + + // albedo + // mainProgram->setUniformValue("u_BaseColorFactor", mat->albedo); + // shader->setUniformValue("u_BaseColorSampler", 0); + // mat->albedoMap->bind(0); + // shader->setUniformValue("u_NormalSampler", 1); + // mat->normalMap->bind(1); + // shader->setUniformValue("u_MetalnessSampler", 2); + // mat->metalnessMap->bind(2); + // shader->setUniformValue("u_RoughnessSampler", 3); + // mat->roughnessMap->bind(3); + + // pbr maps - they start at 8 + // https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/Renderer/renderer.js#L732 + shader->setUniformValue("u_LambertianEnvSampler", 8); + // iblSampler->lambertianTexture->bind(8); + gl->glActiveTexture(GL_TEXTURE0 + 8); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, iblSampler->lambertianTextureID); + shader->setUniformValue("u_GGXEnvSampler", 9); + // iblSampler->ggxTexture->bind(9); + gl->glActiveTexture(GL_TEXTURE0 + 9); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, iblSampler->ggxTextureID); + shader->setUniformValue("u_GGXLUT", 10); + // iblSampler->ggxLutTexture->bind(10); + gl->glActiveTexture(GL_TEXTURE0 + 10); + gl->glBindTexture(GL_TEXTURE_2D, iblSampler->ggxLutTextureID); + shader->setUniformValue("u_CharlieEnvSampler", 11); + // iblSampler->sheenTexture->bind(11); + gl->glActiveTexture(GL_TEXTURE0 + 11); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, iblSampler->sheenTextureID); + shader->setUniformValue("u_CharlieLUT", 12); + // iblSampler->charlieLutTexture->bind(12); + gl->glActiveTexture(GL_TEXTURE0 + 12); + gl->glBindTexture(GL_TEXTURE_2D, iblSampler->charlieLutTextureID); + + shader->setUniformValue("u_MipCount", iblSampler->mipmapLevels); + + shader->setUniformValue("u_EnvRotation", envRotationMatrix()); + shader->setUniformValue("u_EnvIntensity", 1.0f); + + // Setup punctual lights (matches Three.js setupLighting) - conditional + if (this->usePunctualLights) { + // Three.js uses brightness = 2 multiplier + // Back light: 0.225 * 2 = 0.45 at (2.6, 1, 3) + shader->setUniformValue("u_Lights[0].direction", QVector3D(0, 0, 0)); + shader->setUniformValue("u_Lights[0].range", -1.0f); // unlimited + shader->setUniformValue("u_Lights[0].color", + QVector3D(1, 1, 1)); // white + shader->setUniformValue("u_Lights[0].intensity", 0.45f); + shader->setUniformValue("u_Lights[0].position", + QVector3D(2.6f, 1.0f, 3.0f)); + shader->setUniformValue("u_Lights[0].innerConeCos", 0.0f); + shader->setUniformValue("u_Lights[0].outerConeCos", 0.0f); + shader->setUniformValue("u_Lights[0].type", 0); // Directional + + // Key light: 0.375 * 2 = 0.75 at (-2, -1, 0) + shader->setUniformValue("u_Lights[1].direction", QVector3D(0, 0, 0)); + shader->setUniformValue("u_Lights[1].range", -1.0f); + shader->setUniformValue("u_Lights[1].color", QVector3D(1, 1, 1)); + shader->setUniformValue("u_Lights[1].intensity", 0.75f); + shader->setUniformValue("u_Lights[1].position", + QVector3D(-2.0f, -1.0f, 0.0f)); + shader->setUniformValue("u_Lights[1].innerConeCos", 0.0f); + shader->setUniformValue("u_Lights[1].outerConeCos", 0.0f); + shader->setUniformValue("u_Lights[1].type", 0); + + // Fill light: 0.75 * 2 = 1.5 at (3, 3, 2) + shader->setUniformValue("u_Lights[2].direction", QVector3D(0, 0, 0)); + shader->setUniformValue("u_Lights[2].range", -1.0f); + shader->setUniformValue("u_Lights[2].color", QVector3D(1, 1, 1)); + shader->setUniformValue("u_Lights[2].intensity", 1.5f); + shader->setUniformValue("u_Lights[2].position", + QVector3D(3.0f, 3.0f, 2.0f)); + shader->setUniformValue("u_Lights[2].innerConeCos", 0.0f); + shader->setUniformValue("u_Lights[2].outerConeCos", 0.0f); + shader->setUniformValue("u_Lights[2].type", 0); + } + + // render mesh + mesh->vao->bind(); + // tinygltf::Primitive primitive = mesh->primitive; + // tinygltf::Accessor indexAccessor = mesh->indexAccessor; + + // glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbos.at(indexAccessor.bufferView)); + mesh->indexBuffer->bind(); + + // gl->glEnableVertexAttribArray((int)VertexUsage::Position); + // gl->glEnableVertexAttribArray((int)VertexUsage::Normal); + // gl->glEnableVertexAttribArray((int)VertexUsage::Tangent); + // gl->glEnableVertexAttribArray((int)VertexUsage::TexCoord0); + + // glDrawElements(primitive.mode, indexAccessor.count, + // indexAccessor.componentType, + // BUFFER_OFFSET(indexAccessor.byteOffset)); + + glDrawElements(mesh->primitiveMode, mesh->numElements, mesh->indexType, + BUFFER_OFFSET(mesh->indexByteOffset)); + + mesh->vao->release(); +} + +bool loadGltfModel(tinygltf::Model& model, const QString& filename) +{ + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "filenot opened \n"; + return false; + } + + auto text = file.readAll().toStdString(); + + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + + bool res = loader.LoadASCIIFromString(&model, &err, &warn, text.c_str(), + text.length(), ""); + // bool res = loader.LoadASCIIFromFile(&model, &err, &warn, text.c_str(), + // text.length()); + if (!warn.empty()) { + std::cout << "WARN: " << warn << std::endl; + } + + if (!err.empty()) { + std::cout << "ERR: " << err << std::endl; + } + + if (!res) + std::cout << "Failed to load glTF: " << filename.toStdString() + << std::endl; + else + std::cout << "Loaded glTF: " << filename.toStdString() << std::endl; + + return res; +} + +void Renderer::renderSkybox(Mesh* mesh, const QMatrix4x4& viewMatrix, + const QMatrix4x4& projMatrix) +{ + // Create skybox shader if it doesn't exist + if (!skyboxShader) { + skyboxShader = new QOpenGLShaderProgram(); + + QFile vertFile(":assets/skybox.vert"); + QFile fragFile(":assets/skybox.frag"); + + if (vertFile.open(QIODevice::ReadOnly) && + fragFile.open(QIODevice::ReadOnly)) { + QString vertSource = vertFile.readAll(); + QString fragSource = fragFile.readAll(); + + skyboxShader->addShaderFromSourceCode(QOpenGLShader::Vertex, + vertSource); + skyboxShader->addShaderFromSourceCode(QOpenGLShader::Fragment, + fragSource); + skyboxShader->bindAttributeLocation("a_position", + (int)VertexUsage::Position); + skyboxShader->link(); + } + } + + if (!skyboxShader || !iblSampler || !mesh) + return; + + skyboxShader->bind(); + + // Set uniforms + QMatrix4x4 modelMatrix; + modelMatrix.setToIdentity(); + + skyboxShader->setUniformValue("u_modelMatrix", modelMatrix); + skyboxShader->setUniformValue("u_viewMatrix", viewMatrix); + skyboxShader->setUniformValue("u_projectionMatrix", projMatrix); + skyboxShader->setUniformValue("u_envRotation", envRotationMatrix()); + + // Bind environment cubemap + gl->glActiveTexture(GL_TEXTURE0); + gl->glBindTexture(GL_TEXTURE_CUBE_MAP, iblSampler->cubemapTextureID); + skyboxShader->setUniformValue("u_environmentMap", 0); + + // Render the skybox mesh + mesh->vao->bind(); + mesh->indexBuffer->bind(); + + gl->glDrawElements(mesh->primitiveMode, mesh->numElements, mesh->indexType, + BUFFER_OFFSET(mesh->indexByteOffset)); + + mesh->vao->release(); + skyboxShader->release(); +} diff --git a/src/viewer3d/renderer/renderer.h b/src/viewer3d/renderer/renderer.h new file mode 100644 index 00000000..6ea6430b --- /dev/null +++ b/src/viewer3d/renderer/renderer.h @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class QOpenGLVertexArrayObject; +class QOpenGLBuffer; +class Material; +class QOpenGLFunctions; +class QOpenGLTexture; +class QOpenGLShaderProgram; +class IblSampler; +class ShaderCache; + +typedef unsigned int GLuint; + +enum class VertexUsage : int { + Position = 0, + Color = 1, + TexCoord0 = 2, + TexCoord1 = 3, + TexCoord2 = 4, + TexCoord3 = 5, + Normal = 6, + Tangent = 7, + Count = 8 +}; + +enum class MeshType { Generated, Gltf }; + +class MeshPrivate; +class Mesh { +public: + Mesh() = default; + // Frees the owned GL objects (vao, vbos, indexBuffer). Defined in + // renderer.cpp where those types are complete. + ~Mesh(); + // Owns raw GL pointers; non-copyable to avoid double-free. + Mesh(const Mesh&) = delete; + Mesh& operator=(const Mesh&) = delete; + + QOpenGLVertexArrayObject* vao = nullptr; + std::map vbos; + QList attribs; + + QOpenGLBuffer* indexBuffer = nullptr; + // int elementCount; + int numElements = 0; + int indexByteOffset; + int primitiveMode; // GL_TRIANGLES + int indexType; // most likely GL_INT or GL_UNSIGNED_INT + + // transformation props + QMatrix4x4 transform; + QMatrix3x3 normalMatrix; + Material* material = nullptr; + + // determines how to render (for now) + // in the future, all meshes should be rendered the same + MeshType meshType; +}; + +struct Material { + QOpenGLShaderProgram* shader = nullptr; + + GLuint albedoMapId = 0; + GLuint normalMapId = 0; + GLuint metalnessMapId = 0; + GLuint roughnessMapId = 0; + GLuint heightMapId = 0; + GLuint aoMapId = 0; + GLuint alphaMapId = 0; + + // QOpenGLTexture* albedoMap = nullptr; + // QOpenGLTexture* normalMap = nullptr; + // QOpenGLTexture* metalnessMap = nullptr; + // QOpenGLTexture* roughnessMap = nullptr; + // QOpenGLTexture* heightMap = nullptr; + // QOpenGLTexture* aoMap = nullptr; + // QOpenGLTexture* emissiveMap = nullptr; + // QOpenGLTexture* alphaMap = nullptr; + + QVector4D albedo = QVector4D(1, 1, 1, 1); + float metalness = 0.0; // Three.js default + float normalIntensity = 1.0; + float roughness = 0.5; // Three.js default + float heightScale = 0.1; // Three.js displacementScale + float alpha = 1.0; + QVector3D emission = QVector3D(1, 1, 1); + + QStringList flags; + bool needsUpdate = true; +}; + +class Renderer { +public: + IblSampler* iblSampler; + ShaderCache* shaderCache; + QOpenGLFunctions* gl; + QOpenGLShaderProgram* skyboxShader = nullptr; + bool usePunctualLights = true; // Toggle punctual lighting (Three.js style) + + // Yaw applied to the environment about the up axis, in degrees. Several + // HDRIs face their darkest quarter at the default camera, so each sky + // carries a rotation that turns its bright side towards the viewer. + float envRotation = 0.0f; + + void init(QOpenGLFunctions* gl); + void loadEnvironment(const QString& path, float rotationDegrees = 0.0f); + void setEnvironmentRotation(float degrees); + // Yaw matrix handed to both the skybox and the IBL lookups so the + // background and the lighting always agree. + QMatrix3x3 envRotationMatrix() const; + + void renderMesh(Mesh* mesh, Material* material); + void updateMaterial(Material* material); + + void renderGltfMesh(Mesh* mesh, Material* material, const QVector3D& camPos, + const QMatrix4x4& worldMatrix, + const QMatrix4x4& viewMatrix, + const QMatrix4x4& projMatrix); + + void renderSkybox(Mesh* mesh, const QMatrix4x4& viewMatrix, + const QMatrix4x4& projMatrix); +}; + +Mesh* loadMeshFromRc(const QString& path); diff --git a/src/viewer3d/shadercache.cpp b/src/viewer3d/shadercache.cpp new file mode 100644 index 00000000..8b6e498d --- /dev/null +++ b/src/viewer3d/shadercache.cpp @@ -0,0 +1,52 @@ +#include "shadercache.h" +#include + +void ShaderCache::addShaderSource(const QString& shaderName, + const QString& shaderSource) +{ + shaders[shaderName] = shaderSource; +} + +void ShaderCache::addShaderFile(const QString& shaderName, + const QString& filePath) +{ + QFile file(filePath); + file.open(QFile::ReadOnly); + + auto source = file.readAll(); + this->addShaderSource(shaderName, source); + + file.close(); +} + +QString ShaderCache::generateShaderSource(const QString& shaderName, + const QVector defines) +{ + QString flags = ""; + for (auto define : defines) { + flags += "#define " + define + "\n"; + } + + auto source = getShaderAndResolveIncludes(shaderName); + auto finalSource = shaderVersion + flags + source; + + return finalSource; +} + +QString ShaderCache::getShaderAndResolveIncludes(const QString& shader) +{ + auto source = this->shaders[shader]; + + // https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/Renderer/shader_cache.js#L15 + // todo: re-add changes to shaders? + for (auto shaderFile : this->shaders.keys()) { + auto pattern = "#include <" + shaderFile + ">"; + + if (source.contains(pattern)) { + auto shaderSource = this->shaders[shaderFile]; + source.replace(pattern, shaderSource); + } + } + + return source; +} \ No newline at end of file diff --git a/src/viewer3d/shadercache.h b/src/viewer3d/shadercache.h new file mode 100644 index 00000000..1acfe6da --- /dev/null +++ b/src/viewer3d/shadercache.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +class QOpenGLShaderProgram; + +class ShaderCache { + QMap shaders; + QString shaderVersion = "#version 150 core \n"; + +public: + void addShaderSource(const QString& shaderName, + const QString& shaderSource); + void addShaderFile(const QString& shaderName, const QString& filePath); + + QString generateShaderSource(const QString& shaderName, + const QVector defines = {}); + +private: + QString getShaderAndResolveIncludes(const QString& shader); +}; \ No newline at end of file diff --git a/src/viewer3d/viewer3d.cpp b/src/viewer3d/viewer3d.cpp new file mode 100644 index 00000000..b089273b --- /dev/null +++ b/src/viewer3d/viewer3d.cpp @@ -0,0 +1,674 @@ +#include "viewer3d.h" +#include "thememanager.h" +#include "tokens.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "iblsampler.h" +#include "shadercache.h" + +#include "geometry/geometry.h" +#include "renderer/renderer.h" + +// Environment used when the host app doesn't pick one. The rotation turns the +// HDRI's bright side towards the default camera; see View3DWidget's env list. +static const char* kFallbackEnvPath = ":env/studio_kontrast_03_1k.hdr"; +static const float kFallbackEnvRotation = 160.0f; + +QOpenGLShaderProgram* createMainShader(); +QOpenGLBuffer* loadMesh(); +Mesh* loadMeshFromRc(const QString& path); +void renderGltfMesh(Mesh* mesh); + +Viewer3D::Viewer3D() : QOpenGLWidget() +{ + // QOpenGLContext* context = new QOpenGLContext; + // context->setFormat(QSurfaceFormat::defaultFormat()); + // context->create(); + // this->setContext(context); + + Q_INIT_RESOURCE(viewerassets); +} + +void Viewer3D::reRender() { this->update(); } + +void Viewer3D::initializeGL() +{ + gl = QOpenGLContext::currentContext()->functions(); + gl->initializeOpenGLFunctions(); + + QOpenGLContext* ctx = QOpenGLContext::currentContext(); + QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); + logger->initialize(); + QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, + [=](const QOpenGLDebugMessage& debugMessage) { + qDebug() << debugMessage; + }); + + logger->startLogging(); + + vao = new QOpenGLVertexArrayObject; + if (vao->create()) + vao->bind(); + + shaderCache = new ShaderCache(); + shaderCache->addShaderFile("animation.glsl", ":assets/animation.glsl"); + shaderCache->addShaderFile("brdf.glsl", ":assets/brdf.glsl"); + shaderCache->addShaderFile("functions.glsl", ":assets/functions.glsl"); + shaderCache->addShaderFile("iridiscence.glsl", ":assets/iridiscence.glsl"); + shaderCache->addShaderFile("ibl.glsl", ":assets/ibl.glsl"); + shaderCache->addShaderFile("material_info.glsl", + ":assets/material_info.glsl"); + shaderCache->addShaderFile("pbr.frag", ":assets/pbr.frag"); + shaderCache->addShaderFile("primitive.vert", ":assets/primitive.vert"); + shaderCache->addShaderFile("punctual.glsl", ":assets/punctual.glsl"); + shaderCache->addShaderFile("textures.glsl", ":assets/textures.glsl"); + shaderCache->addShaderFile("tonemapping.glsl", ":assets/tonemapping.glsl"); + + // mainProgram = createMainShader(); + + // mesh = loadMesh(); + auto mat = this->loadMaterial(); + // mat->albedo = QVector4D(0.7, 0.7, 0.7, 1.0); + mat->roughness = 0.5; // Three.js default + mat->metalness = 0.0; // Three.js default + // gltfMesh = loadMeshFromRc(":assets/cube.gltf"); + gltfMesh = createSphere(this->gl, 2, 1000, 1000); + this->material = mat; + + // Create skydome for rendering environment + skydomeMesh = createSkydome(this->gl, 100, 32, 16); + + // setup matrices + worldMatrix.setToIdentity(); + + viewMatrix.setToIdentity(); + viewMatrix.lookAt(QVector3D(0, 5, -5), QVector3D(), QVector3D(0, 1, 0)); + + projMatrix.setToIdentity(); + projMatrix.perspective(45, this->width() / (float)this->height(), 1.0, + 1000); + + // iblSampler = new IblSampler(); + // iblSampler->gl = gl; + // iblSampler->init(":assets/panorama.hdr"); + // iblSampler->filterAll(); + + renderer = new Renderer(); + renderer->init(this->gl); + if (!defaultEnvPath.isEmpty()) + renderer->loadEnvironment(defaultEnvPath, defaultEnvRotation); + else + renderer->loadEnvironment(kFallbackEnvPath, kFallbackEnvRotation); +} + +void Viewer3D::setDefaultEnvironment(const QString path, float rotation) +{ + this->defaultEnvPath = path; + this->defaultEnvRotation = rotation; +} + +void Viewer3D::paintGL() +{ + buildView(); + + // this is already done by QOpenGLWidget + // also, the supplied width and height are incorrect + // gl->glViewport(0, 0, this->width(), this->height()); + gl->glClearDepthf(1.0); + // Themed clear color; read each frame so it follows --dev-theme hot-reload. + const QColor clear = ThemeManager::instance().theme().color(Tokens::View3dClear); + gl->glClearColor(clear.redF(), clear.greenF(), clear.blueF(), 1); + gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + gl->glEnable(GL_DEPTH_TEST); + gl->glDisable(GL_CULL_FACE); + + vao->bind(); + + // Render skydome first (as background) + if (skydomeMesh) { + gl->glDepthFunc(GL_LEQUAL); + renderer->renderSkybox(skydomeMesh, viewMatrix, projMatrix); + gl->glDepthFunc(GL_LESS); + } + + // render gltf mesh, double-sided: draw the back faces first and the + // front faces second so alpha-blended fragments composite back-to-front + gl->glEnable(GL_BLEND); + gl->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + gl->glEnable(GL_CULL_FACE); + + if (gltfMesh) { + gl->glCullFace(GL_FRONT); + renderer->renderGltfMesh(gltfMesh, material, camPos, worldMatrix, + viewMatrix, projMatrix); + + gl->glCullFace(GL_BACK); + renderer->renderGltfMesh(gltfMesh, material, camPos, worldMatrix, + viewMatrix, projMatrix); + } + + gl->glDisable(GL_CULL_FACE); + gl->glDisable(GL_BLEND); + + // test several in a row + // int totalSpheres = 6; + // float roughness = 0; + // float rinc = 1.0 / (totalSpheres - 1); + // float x = -12; + // material->metalness = 1; + // material->roughness = 0; + // for (int i = 0; i < totalSpheres; i++) { + + // worldMatrix.setColumn(3, QVector4D(x, 0, 0, 1)); + // x += 4; + + // material->roughness = roughness * roughness; + // renderer->renderGltfMesh(gltfMesh, material, camPos, worldMatrix, + // viewMatrix, projMatrix); + + // roughness += rinc; + // } + + vao->bind(); +} + +void Viewer3D::resizeGL(int w, int h) +{ + // Calculate aspect ratio + qreal aspect = qreal(w) / qreal(h ? h : 1); + + // Set near plane to 3.0, far plane to 7.0, field of view 45 degrees + const qreal zNear = 1.0, zFar = 1000.0, fov = 45.0; + + // Reset projection + projMatrix.setToIdentity(); + + // Set perspective projection + projMatrix.perspective(fov, aspect, zNear, zFar); +} + +void Viewer3D::mousePressEvent(QMouseEvent* e) +{ + prevPos = e->pos(); + + if (e->button() == Qt::LeftButton) { + leftMouseDown = true; + } + if (e->button() == Qt::MiddleButton) { + middleMouseDown = true; + } +} +void Viewer3D::mouseReleaseEvent(QMouseEvent* e) +{ + if (e->button() == Qt::LeftButton) { + leftMouseDown = true; + } + if (e->button() == Qt::MiddleButton) { + middleMouseDown = true; + } +} +void Viewer3D::mouseMoveEvent(QMouseEvent* e) +{ + if (leftMouseDown) { + auto diff = e->pos() - prevPos; + + yaw += -diff.x() * dragSpeed; + pitch += diff.y() * dragSpeed; + + prevPos = e->pos(); + + this->repaint(); + } +} + +void Viewer3D::wheelEvent(QWheelEvent* e) +{ + auto dir = e->angleDelta().y() > 0 ? -1 : 1; + zoom += dir * zoomSpeed; + + this->repaint(); +} + +void Viewer3D::buildView() +{ + QVector3D view(0, 0, -zoom); + // auto rot = QMatrix4x4::rotate(); + auto rot = QQuaternion::fromEulerAngles(pitch, yaw, 0); + auto eyePos = rot.rotatedVector(view); + + // offset by center + eyePos += center; + camPos = eyePos; + + viewMatrix.setToIdentity(); + viewMatrix.lookAt(eyePos, center, QVector3D(0, 1, 0)); +} + +QOpenGLShaderProgram* createMainShader() +{ + QOpenGLShader* vshader = new QOpenGLShader(QOpenGLShader::Vertex); + QOpenGLShader* fshader = new QOpenGLShader(QOpenGLShader::Fragment); + auto program = new QOpenGLShaderProgram; + + QString vSource = R""""( + #version 150 core + + in vec3 a_pos; + in vec3 a_normal; + in vec3 a_tangent; + in vec2 a_texCoord; + + uniform mat4 worldMatrix; + uniform mat4 viewMatrix; + uniform mat4 projMatrix; + + out vec2 v_texCoord; + + void main() + { + v_texCoord = a_texCoord*vec2(1,1); + vec4 v_pos = projMatrix * viewMatrix * worldMatrix * vec4(a_pos, 1.0); + gl_Position = v_pos; + } + )""""; + + QString fSource = R""""( + #version 150 core + + in vec2 v_texCoord; + out vec4 fragColor; + + uniform sampler2D diffuse; + + void main() + { + fragColor = texture(diffuse, v_texCoord); + } + )""""; + + if (!vshader->compileSourceCode(vSource)) { + qDebug() << "VERTEX SHADER ERROR"; + qDebug() << vshader->log(); + } + + if (!fshader->compileSourceCode(fSource)) { + qDebug() << "FRAGMENT SHADER ERROR"; + qDebug() << fshader->log(); + } + + program->removeAllShaders(); + + program->addShader(vshader); + program->addShader(fshader); + + program->bindAttributeLocation("a_pos", (int)VertexUsage::Position); + program->bindAttributeLocation("a_normal", (int)VertexUsage::Normal); + program->bindAttributeLocation("a_tangent", (int)VertexUsage::Tangent); + program->bindAttributeLocation("a_color", (int)VertexUsage::Color); + program->bindAttributeLocation("a_texCoord", (int)VertexUsage::TexCoord0); + program->bindAttributeLocation("a_texCoord1", (int)VertexUsage::TexCoord1); + + if (!program->link()) { + qDebug() << "SHADER LINK ERROR"; + qDebug() << program->log(); + + return nullptr; + } + + return program; +} + +QOpenGLBuffer* loadMesh() +{ + // setup vertex array + QVector data; + // TRIANGLE 1 + data.append(-1); + data.append(-1); + data.append(0); + + data.append(0); + data.append(0); + + data.append(1); + data.append(-1); + data.append(0); + + data.append(1); + data.append(0); + + data.append(-1); + data.append(1); + data.append(0); + + data.append(0); + data.append(1); + + // TRIANGLE 2 + data.append(-1); + data.append(1); + data.append(0); + + data.append(0); + data.append(1); + + data.append(1); + data.append(-1); + data.append(0); + + data.append(1); + data.append(0); + + data.append(1); + data.append(1); + data.append(0); + + data.append(1); + data.append(1); + + auto vbo = new QOpenGLBuffer; + vbo->create(); + vbo->bind(); + vbo->allocate(data.constData(), data.count() * sizeof(float)); + vbo->release(); + + return vbo; +} + +Material* Viewer3D::loadMaterial() +{ + auto mat = new Material(); + + QStringList flags; + flags << "USE_IBL 1"; + flags << "HAS_NORMAL_VEC3 1"; + // flags << "LINEAR_OUTPUT 1"; + // flags << "TEST 1"; + // flags << "TEST2 1"; + flags << "HAS_TANGENT_VEC4 1"; + // flags << "DEBUG_NORMAL_GEOMETRY 1"; + flags << "DEBUG_NONE 1"; // IMPORTANT!! caused many headaches.. + flags << "DEBUG DEBUG_NONE"; + // flags << "DEBUG DEBUG_METALLIC_ROUGHNESS"; + + flags << "HAS_TEXCOORD_0_VEC2 1"; + flags << "HAS_BASE_COLOR_MAP 1"; + // flags << "HAS_NORMAL_MAP 1"; + // flags << "HAS_ROUGHNESS_MAP 1"; + // flags << "HAS_METALNESS_MAP 1"; + // flags << "HAS_METALLIC_ROUGHNESS_MAP 1"; + // flags << "HAS_EMISSIVE_MAP 1"; + flags << "MATERIAL_METALLICROUGHNESS 1"; // MR mode + + flags << "ALPHAMODE_OPAQUE 0"; + flags << "ALPHAMODE_MASK 1"; + flags << "ALPHAMODE_BLEND 2"; + flags << "ALPHAMODE ALPHAMODE_OPAQUE"; + // flags << "TONEMAP_ACES_NARKOWICZ 1"; + // flags << "TONEMAP_ACES_HILL_EXPOSURE_BOOST 1"; + + auto vertShader = + shaderCache->generateShaderSource("primitive.vert", flags); + auto fragShader = shaderCache->generateShaderSource("pbr.frag", flags); + + auto shader = new QOpenGLShaderProgram; + shader->bind(); + shader->addShaderFromSourceCode(QOpenGLShader::Vertex, vertShader); + shader->addShaderFromSourceCode(QOpenGLShader::Fragment, fragShader); + + shader->bindAttributeLocation("a_position", (int)VertexUsage::Position); + shader->bindAttributeLocation("a_normal", (int)VertexUsage::Normal); + shader->bindAttributeLocation("a_tangent", (int)VertexUsage::Tangent); + shader->bindAttributeLocation("a_color_0", (int)VertexUsage::Color); + shader->bindAttributeLocation("a_texcoord_0", (int)VertexUsage::TexCoord0); + shader->bindAttributeLocation("a_texcoord_1", (int)VertexUsage::TexCoord1); + + shader->link(); + // shader->release(); + + mat->shader = shader; + + // textures + // mat->albedoMapId = loadTextureId(":assets/brick.jpg"); + // mat->albedoMap = loadTexture(":assets/brick.jpg"); + // mat->albedoMap = loadTexture(":assets/albedo.jpg"); + // mat->normalMap = loadTexture(":assets/normal.jpg"); + // mat->metalnessMap = loadTexture(":assets/metalness.jpg"); + // mat->roughnessMap = loadTexture(":assets/roughness.jpg"); + + return mat; +} +QOpenGLTexture* Viewer3D::loadTexture(const QString& path) +{ + QImage image(path); + auto tex = new QOpenGLTexture(image); + tex->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, + QOpenGLTexture::Linear); + tex->generateMipMaps(); + + return tex; +} + +GLuint Viewer3D::loadTextureId(const QString& path) +{ + QImage image(path); + auto tex = new QOpenGLTexture(image); + tex->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear, + QOpenGLTexture::Linear); + tex->generateMipMaps(); + + return tex->textureId(); +} + +void Viewer3D::setAlbedoTexture(GLuint texId) +{ + this->material->albedoMapId = texId; + this->material->needsUpdate = true; +} + +void Viewer3D::clearAlbedoTexture() +{ + if (!this->material) + return; + this->material->albedoMapId = 0; + this->material->needsUpdate = true; +} + +void Viewer3D::setNormalTexture(GLuint texId) +{ + this->material->normalMapId = texId; + this->material->needsUpdate = true; +} + +void Viewer3D::clearNormalTexture() +{ + if (!this->material) + return; + this->material->normalMapId = 0; + this->material->needsUpdate = true; +} + +void Viewer3D::setMetalnessTexture(GLuint texId) +{ + this->material->metalnessMapId = texId; + this->material->needsUpdate = true; +} + +void Viewer3D::clearMetalnessTexture() +{ + if (!this->material) + return; + this->material->metalnessMapId = 0; + this->material->needsUpdate = true; +} + +void Viewer3D::setRoughnessTexture(GLuint texId) +{ + this->material->roughnessMapId = texId; + this->material->needsUpdate = true; +} + +void Viewer3D::clearRoughnessTexture() +{ + if (!this->material) + return; + this->material->roughnessMapId = 0; + this->material->needsUpdate = true; +} + +void Viewer3D::setHeightTexture(GLuint texId) +{ + this->material->heightMapId = texId; + this->material->needsUpdate = true; +} + +void Viewer3D::clearHeightTexture() +{ + if (!this->material) + return; + this->material->heightMapId = 0; + this->material->needsUpdate = true; +} + +void Viewer3D::setHeightScale(float scale) +{ + this->material->heightScale = scale; + this->material->needsUpdate = true; +} + +void Viewer3D::setAoTexture(GLuint texId) +{ + this->material->aoMapId = texId; + this->material->needsUpdate = true; +} + +void Viewer3D::clearAoTexture() +{ + if (!this->material) + return; + this->material->aoMapId = 0; + this->material->needsUpdate = true; +} + +void Viewer3D::setAlphaTexture(GLuint texId) +{ + this->material->alphaMapId = texId; + this->material->needsUpdate = true; +} + +void Viewer3D::clearAlphaTexture() +{ + if (!this->material) + return; + this->material->alphaMapId = 0; + this->material->needsUpdate = true; +} + +void Viewer3D::clearTextures() +{ + this->clearAlbedoTexture(); + this->clearNormalTexture(); + this->clearMetalnessTexture(); + this->clearRoughnessTexture(); + this->clearHeightTexture(); + this->clearAoTexture(); + this->clearAlphaTexture(); +} + +void Viewer3D::resetCamera() +{ + zoom = 7; + yaw = 0; + pitch = 0; + center = QVector3D(0, 0, 0); + this->repaint(); +} + +void Viewer3D::loadEnvironment(const QString path, float rotation) +{ + if (renderer) { + renderer->loadEnvironment(path, rotation); + this->repaint(); + } +} + +void Viewer3D::setEnvironmentRotation(float rotation) +{ + if (renderer) { + renderer->setEnvironmentRotation(rotation); + this->repaint(); + } +} + +void Viewer3D::setModel(const QString& modelType) +{ + // Bind OpenGL context + makeCurrent(); + + // Build the new mesh into a local first; only swap in (and free the old + // one) once creation succeeds, so a failed allocation can't leave gltfMesh + // null or delete the current mesh prematurely. + Mesh* newMesh = nullptr; + if (modelType == "sphere") { + newMesh = createSphere(this->gl, 2, 1000, 1000); + } + else if (modelType == "plane_xy") { + // Create a subdivided plane in XY orientation + newMesh = createPlane(this->gl, 4, 4, 1000, 1000, PlaneOrientation::XY); + } + else if (modelType == "plane_yz") { + // Create a subdivided plane in YZ orientation + newMesh = createPlane(this->gl, 4, 4, 1000, 1000, PlaneOrientation::YZ); + } + else if (modelType == "plane_xz") { + // Create a subdivided plane in XZ orientation + newMesh = createPlane(this->gl, 4, 4, 1000, 1000, PlaneOrientation::XZ); + } + else if (modelType == "cylinder") { + // Create a cylinder with height subdivisions for displacement mapping + newMesh = createCylinder(this->gl, 1, 1, 2, 1000, 1000, 0.1f, 16); + } + else if (modelType == "cube") { + // Create a subdivided cube + newMesh = createCube(this->gl, 2, 2, 2, 1000, 1000, 1000); + } + else if (modelType == "cubesphere") { + // CubeSphere - a sphere with low segments for a more cubic look + newMesh = createSphere(this->gl, 2, 8, 8); + } + else { + // Default to sphere + newMesh = createSphere(this->gl, 2, 1000, 1000); + } + + if (newMesh) { + delete gltfMesh; + gltfMesh = newMesh; + } + + // Release OpenGL context + doneCurrent(); + + this->repaint(); +} \ No newline at end of file diff --git a/src/viewer3d/viewer3d.h b/src/viewer3d/viewer3d.h new file mode 100644 index 00000000..474e2ccb --- /dev/null +++ b/src/viewer3d/viewer3d.h @@ -0,0 +1,130 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QOpenGLTexture; +class QOpenGLShaderProgram; +class QOpenGLBuffer; +class QOpenGLVertexArrayObject; +class Mesh; +class ShaderCache; + +class IblSampler; +class Material; +class Renderer; + +class Viewer3D : public QOpenGLWidget { + QOpenGLTexture* texture = nullptr; + QOpenGLShaderProgram* mainProgram = nullptr; + QOpenGLBuffer* mesh = nullptr; + QOpenGLVertexArrayObject* vao = nullptr; + + // Initialized to nullptr: these are only assigned in initializeGL() (first + // paint), but setProject()/clearTextures() can run earlier (from the + // MainWindow constructor). Without this, the null-guards in clear*Texture() + // dereference uninitialized garbage and crash. See viewer3d.cpp clear*. + Renderer* renderer = nullptr; + Material* material = nullptr; + Mesh* gltfMesh = nullptr; + Mesh* skydomeMesh = nullptr; + QString defaultEnvPath; + float defaultEnvRotation = 0.0f; + + QOpenGLFunctions* gl = nullptr; + + int m_projMatrixLoc = 0; + int m_camMatrixLoc = 0; + int m_worldMatrixLoc = 0; + int m_myMatrixLoc = 0; + int m_lightPosLoc = 0; + + QMatrix4x4 projMatrix; + QMatrix4x4 viewMatrix; + QMatrix4x4 worldMatrix; + + // QVector3D m_eye; + // QVector3D m_target = {0, 0, -1}; + + // trackball params + float zoom = 7; + float zoomSpeed = 0.3f; + QPoint prevPos; + QVector3D center = {0, 0, 0}; + QVector3D camPos = {0, 0, 0}; + float yaw = 0; + float pitch = 0; + float dragSpeed = 0.25f; + + bool leftMouseDown = false; + bool middleMouseDown = false; + + IblSampler* iblSampler; + ShaderCache* shaderCache; + +protected: + void initializeGL(); + void paintGL(); + void resizeGL(int w, int h); + + // trackball events + void mousePressEvent(QMouseEvent* e) override; + void mouseReleaseEvent(QMouseEvent* e) override; + void mouseMoveEvent(QMouseEvent* e) override; + void wheelEvent(QWheelEvent* e) override; + + void buildView(); + + Material* loadMaterial(); + QOpenGLTexture* loadTexture(const QString& path); + GLuint loadTextureId(const QString& path); + // void renderMesh(Mesh* mesh); + void renderGltfMesh(Mesh* mesh); + +public: + Viewer3D(); + + void reRender(); + + // public functions exposed to main app + void setAlbedoTexture(GLuint texId); + void clearAlbedoTexture(); + void setNormalTexture(GLuint texId); + void clearNormalTexture(); + void setMetalnessTexture(GLuint texId); + void clearMetalnessTexture(); + void setRoughnessTexture(GLuint texId); + void clearRoughnessTexture(); + void setHeightTexture(GLuint texId); + void clearHeightTexture(); + void setHeightScale(float scale); + void setAoTexture(GLuint texId); + void clearAoTexture(); + void setAlphaTexture(GLuint texId); + void clearAlphaTexture(); + void resetMaterial(); + + void clearTextures(); + void resetCamera(); + // rotation is a yaw in degrees about the up axis, used to turn the lit + // side of the HDRI towards the camera. + void loadEnvironment(const QString path, float rotation = 0.0f); + void setEnvironmentRotation(float rotation); + void setModel(const QString& modelType); + + // sets env to use on load + void setDefaultEnvironment(const QString path, float rotation = 0.0f); +}; \ No newline at end of file diff --git a/src/viewer3d/viewerassets.qrc b/src/viewer3d/viewerassets.qrc new file mode 100644 index 00000000..a6a179b0 --- /dev/null +++ b/src/viewer3d/viewerassets.qrc @@ -0,0 +1,23 @@ + + + assets/brick.jpg + assets/cube.gltf + assets/panorama.hdr + assets/fullscreen.vert + assets/panorama_to_cubemap.frag + assets/ibl_filtering.frag + assets/animation.glsl + assets/brdf.glsl + assets/functions.glsl + assets/ibl.glsl + assets/iridiscence.glsl + assets/material_info.glsl + assets/pbr.frag + assets/primitive.vert + assets/punctual.glsl + assets/textures.glsl + assets/tonemapping.glsl + assets/skybox.vert + assets/skybox.frag + + \ No newline at end of file diff --git a/src/views/About.vue b/src/views/About.vue deleted file mode 100644 index 7054f596..00000000 --- a/src/views/About.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/src/views/Editor.vue b/src/views/Editor.vue deleted file mode 100644 index e94a69c7..00000000 --- a/src/views/Editor.vue +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/src/views/Home.vue b/src/views/Home.vue deleted file mode 100644 index f3b51a27..00000000 --- a/src/views/Home.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/src/views/Library.vue b/src/views/Library.vue deleted file mode 100644 index e7c66d37..00000000 --- a/src/views/Library.vue +++ /dev/null @@ -1,296 +0,0 @@ - - - - - diff --git a/src/views/NodeProperties.vue b/src/views/NodeProperties.vue deleted file mode 100644 index fc3919cd..00000000 --- a/src/views/NodeProperties.vue +++ /dev/null @@ -1,275 +0,0 @@ - - - - - diff --git a/src/views/Preview2D.vue b/src/views/Preview2D.vue deleted file mode 100644 index e2ceaf7f..00000000 --- a/src/views/Preview2D.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - diff --git a/src/views/Preview3D.vue b/src/views/Preview3D.vue deleted file mode 100644 index 94368b3c..00000000 --- a/src/views/Preview3D.vue +++ /dev/null @@ -1,403 +0,0 @@ - - - - - diff --git a/src/views/TextureVariables.vue b/src/views/TextureVariables.vue deleted file mode 100644 index 60cadb41..00000000 --- a/src/views/TextureVariables.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/src/views/dialogs/ExportDialog.vue b/src/views/dialogs/ExportDialog.vue deleted file mode 100644 index 8ffd19cb..00000000 --- a/src/views/dialogs/ExportDialog.vue +++ /dev/null @@ -1,212 +0,0 @@ - - - - - diff --git a/src/views/preview2d/previewcanvas2d.ts b/src/views/preview2d/previewcanvas2d.ts deleted file mode 100644 index 71295e80..00000000 --- a/src/views/preview2d/previewcanvas2d.ts +++ /dev/null @@ -1,338 +0,0 @@ -const IMAGE_RENDER_SIZE = 1000; - -function _getMousePos(canvas, evt) { - const rect = canvas.getBoundingClientRect(); - return new Vector2(evt.clientX - rect.left, evt.clientY - rect.top); -} - -class Vector2 { - x: number; - y: number; - - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } -} - -export enum DrawMode { - Single, - Nine -} - -class Rect { - protected visible = true; - - protected x = 0; - protected y = 0; - protected width: number; - protected height: number; - - color: string; - - public constructor() { - //this.scene = scene; - //scene.addItem(this); - this.width = 1; - this.height = 1; - this.color = "rgb(255, 50, 50)"; - } - - public setSize(w: number, h: number) { - this.width = w; - this.height = h; - } - - public isPointInside(px: number, py: number): boolean { - if ( - px >= this.x && - px <= this.x + this.width && - py >= this.y && - py <= this.y + this.height - ) - return true; - return false; - } - - public setCenter(x: number, y: number) { - this.x = x - this.width / 2; - this.y = y - this.height / 2; - } - - public centerX(): number { - return this.x + this.width / 2; - } - - public centerY(): number { - return this.y + this.height / 2; - } - - public move(dx: number, dy: number) { - this.x += dx; - this.y += dy; - } - - // to be overriden - draw(ctx: CanvasRenderingContext2D) { - // background - ctx.beginPath(); - ctx.fillStyle = this.color; - ctx.rect(this.x, this.y, this.width, this.height); - ctx.fill(); - - // border - ctx.beginPath(); - ctx.lineWidth = 4; - ctx.strokeStyle = "rgb(0, 0, 0)"; - ctx.rect(this.x, this.y, this.width, this.height); - ctx.stroke(); - } -} - -export class DragZoom { - canvas: HTMLCanvasElement; - context: CanvasRenderingContext2D; - - mousePos: Vector2; - prevMousePos: Vector2; - mouseDownPos: Vector2; // pos of last mouse down - mouseDragDiff: Vector2; // mouse drag diff - - zoomFactor: number; - offset: Vector2; - - panning: boolean; - panStart: SVGPoint; - - rect: Rect; - image: HTMLCanvasElement; - - public drawMode: DrawMode; - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - this.context = this.canvas.getContext("2d"); - this.image = null; - - const self = this; - canvas.addEventListener("mousemove", function(evt: MouseEvent) { - self.onMouseMove(evt); - }); - canvas.addEventListener("mousedown", function(evt: MouseEvent) { - self.onMouseDown(evt); - }); - canvas.addEventListener("mouseup", function(evt: MouseEvent) { - self.onMouseUp(evt); - }); - canvas.addEventListener("mouseout", function(evt: MouseEvent) { - self.onMouseOut(evt); - }); - canvas.addEventListener("mousewheel", function(evt: WheelEvent) { - self.onMouseScroll(evt); - }); - canvas.addEventListener("contextmenu", function(evt: MouseEvent) { - evt.preventDefault(); - }); - canvas.addEventListener("resize", function(evt: MouseEvent) {}); - - this.drawMode = DrawMode.Single; - - // this.zoomFactor = 1; - this.zoomFactor = 0.4; - this.offset = new Vector2(0, 0); - - this.mousePos = new Vector2(0, 0); - - this.rect = new Rect(); - this.rect.setSize(50, 50); - - // offset to put center(0,0) in middle of view - this.offset = new Vector2(canvas.width * 0.5, canvas.height * 0.5); - } - - onResize(width: number, height: number) { - this.offset = new Vector2(width * 0.5, height * 0.5); - } - - // puts image in center and set appropriate zoom level - centerImage() { - this.offset = new Vector2( - this.canvas.width * 0.5, - this.canvas.height * 0.5 - ); - this.zoomFactor = 0.4; - } - - setImage(image: HTMLCanvasElement) { - this.image = image; - - // center image in view - } - - getAbsPos() { - return new Vector2(this.canvas.offsetLeft, this.canvas.offsetTop); - } - - onMouseDown(evt: MouseEvent) { - if (evt.button == 1 || evt.button == 2) { - this.panning = true; - - this.mouseDownPos = _getMousePos(this.canvas, evt); - } - - this.mousePos = _getMousePos(this.canvas, evt); - } - - onMouseUp(evt: MouseEvent) { - // this.mouseX = pos.x; - // this.mouseY = pos.y; - if (evt.button == 1 || evt.button == 2) { - this.panning = false; - } - } - - onMouseMove(evt: MouseEvent) { - this.prevMousePos = this.mousePos; - this.mousePos = _getMousePos(this.canvas, evt); - - if (this.panning) { - const prev = this.canvasToScene(this.prevMousePos); - const cur = this.canvasToScene(this.mousePos); - const diff = new Vector2(prev.x - cur.x, prev.y - cur.y); - // const diff = new Vector2(this.prevMousePos.x - this.mousePos.x, this.prevMousePos.y - this.mousePos.y); - - const factor = this.zoomFactor; - this.offset.x -= diff.x * factor; - this.offset.y -= diff.y * factor; - } - - // var lastX = this.mouseX; - // var lastY = this.mouseY; - // var pos = this.getScenePos(evt); - // this.mouseX = pos.x; - // this.mouseY = pos.y; - - // if (this.panning) { - // // convert to scene space first - // //var lastPt = this.contextExtra.transformedPoint(lastX, lastY); - // //var pt = this.contextExtra.transformedPoint(this.mouseX, this.mouseY); - // //this.context.translate(pt.x - lastPt.x, pt.y - lastPt.y); - // //console.log(pt.x - this.panStart.x, pt.y - this.panStart.y); - // console.log(this.mouseX - this.panStart.x, this.mouseY - this.panStart.y); - // this.context.translate(this.mouseX - this.panStart.x, this.mouseY - this.panStart.y); - // //this.panStart = pos; - // } - } - - onMouseScroll(evt: WheelEvent) { - // no panning while zooming - if (this.panning) return; - - const pos = _getMousePos(this.canvas, evt); - const delta = (evt).wheelDelta > 0 ? 1.1 : 1.0 / 1.1; - - // offset from mouse pos - // find offset from previous zoom then move offset by that value - - this.zoomFactor *= delta; - this.offset.x = pos.x - (pos.x - this.offset.x) * delta; // * (factor); - this.offset.y = pos.y - (pos.y - this.offset.y) * delta; // * (factor); - - //this.zoom(pos.x, pos.y, delta); - - evt.preventDefault(); - return false; - } - - onMouseOut(evt: MouseEvent) { - // cancel panning - this.panning = false; - } - - zoom(x: number, y: number, level: number) { - // var scaleFactor = 1.01; - // var pt = this.contextExtra.transformedPoint(x,y); - // var factor = Math.pow(scaleFactor, level); - } - - draw() { - const ctx = this.context; - - ctx.setTransform(1, 0, 0, 1, 0, 0); - ctx.fillStyle = "rgb(50,50,50)"; - ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - - //ctx.scale(this.zoomFactor, this.zoomFactor); - //ctx.translate(this.offset.x, this.offset.y); - - ctx.setTransform( - this.zoomFactor, - 0, - 0, - this.zoomFactor, - this.offset.x, - this.offset.y - ); - - // ctx.translate(this.offset.x, this.offset.y); - // ctx.scale(this.zoomFactor, this.zoomFactor); - - // highlight rect if mouse over - const scenePos = this.canvasToScene(this.mousePos); - //console.log(scenePos); - if (this.rect.isPointInside(scenePos.x, scenePos.y)) { - this.rect.color = "rgb(0, 255, 255)"; - } else { - this.rect.color = "rgb(255, 50, 50)"; - } - - if (this.image) { - if (this.drawMode == DrawMode.Single) { - this.drawImage(0, 0); - } else { - // top - this.drawImage(-1, -1); - this.drawImage(0, -1); - this.drawImage(1, -1); - - // middle - this.drawImage(-1, 0); - this.drawImage(0, 0); - this.drawImage(1, 0); - - // bottom - this.drawImage(-1, 1); - this.drawImage(0, 1); - this.drawImage(1, 1); - } - - // this.context.lineWidth = 5; - // this.context.strokeRect( - // -IMAGE_RENDER_SIZE * 0.5, - // -IMAGE_RENDER_SIZE * 0.5, - // IMAGE_RENDER_SIZE, - // IMAGE_RENDER_SIZE - // ); - } - } - - drawImage(offsetX: number, offsetY: number) { - this.context.drawImage( - this.image, - -IMAGE_RENDER_SIZE * 0.5 + offsetX * IMAGE_RENDER_SIZE, - -IMAGE_RENDER_SIZE * 0.5 + offsetY * IMAGE_RENDER_SIZE, - IMAGE_RENDER_SIZE, - IMAGE_RENDER_SIZE - ); - } - - // converts from canvas(screen) coords to the scene(world) coords - canvasToScene(pos: Vector2): Vector2 { - //return new Vector2(pos.x * (1.0 / this.zoomFactor) - this.offset.x, pos.y * (1.0 / this.zoomFactor) - this.offset.y); - return new Vector2( - (pos.x - this.offset.x) * (1.0 / this.zoomFactor), - (pos.y - this.offset.y) * (1.0 / this.zoomFactor) - ); - //return new Vector2(pos.x * this.zoomFactor - this.offset.x, pos.y * this.zoomFactor - this.offset.y); - } -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..83a1ff3f --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.10) + +# QtTest rather than gtest or catch2: Qt is already a hard dependency, so this +# adds no third-party code to vendor, update, or explain. + +set(CMAKE_AUTOMOC ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Test Gui Network) + +# Each test file becomes its own binary, so one crashing suite can't take the +# others' results with it. +function(texturelab_add_test name) + add_executable(${name} ${name}.cpp) + target_link_libraries(${name} PRIVATE + catalog + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Test + ) + add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +endfunction() + +texturelab_add_test(tst_database) +texturelab_add_test(tst_catalogindex) +texturelab_add_test(tst_thumbnailcache) +# QImage/QPixmap round-trip needs the GUI module. +target_link_libraries(tst_thumbnailcache PRIVATE Qt${QT_VERSION_MAJOR}::Gui) + +# Update version ordering: pure QtCore, so it compiles straight into a test. +texturelab_add_test(tst_versioncompare) +target_sources(tst_versioncompare PRIVATE + ${CMAKE_SOURCE_DIR}/src/texturelab/update/versioncompare.cpp) +target_include_directories(tst_versioncompare PRIVATE + ${CMAKE_SOURCE_DIR}/src/texturelab/update) + +# Which server gets asked, and on which channel. Needs Network (the checker +# owns a QNetworkAccessManager) but makes no request. +texturelab_add_test(tst_updateendpoint) +target_sources(tst_updateendpoint PRIVATE + ${CMAKE_SOURCE_DIR}/src/texturelab/update/updatechecker.cpp + ${CMAKE_SOURCE_DIR}/src/texturelab/update/versioncompare.cpp) +target_include_directories(tst_updateendpoint PRIVATE + ${CMAKE_SOURCE_DIR}/src/texturelab/update + ${CMAKE_SOURCE_DIR}/src/texturelab) +target_link_libraries(tst_updateendpoint PRIVATE Qt${QT_VERSION_MAJOR}::Network) +# The launcher's model is deliberately free of app dependencies — Qt plus the +# catalog and nothing else — so its .cpp can be compiled straight into a test +# without dragging in the node graph or an OpenGL context. +texturelab_add_test(tst_texturelistmodel) +target_sources(tst_texturelistmodel PRIVATE + ${CMAKE_SOURCE_DIR}/src/texturelab/launcher/texturelistmodel.cpp) +target_include_directories(tst_texturelistmodel PRIVATE + ${CMAKE_SOURCE_DIR}/src/texturelab/launcher) +target_link_libraries(tst_texturelistmodel PRIVATE Qt${QT_VERSION_MAJOR}::Gui) + +# Loose types in old project files: numbers as strings, ints as floats. +# Header-only, so the test just includes it. +texturelab_add_test(tst_jsonutils) +target_include_directories(tst_jsonutils PRIVATE + ${CMAKE_SOURCE_DIR}/src/texturelab) diff --git a/tests/tst_catalogindex.cpp b/tests/tst_catalogindex.cpp new file mode 100644 index 00000000..d033bdce --- /dev/null +++ b/tests/tst_catalogindex.cpp @@ -0,0 +1,501 @@ +#include "catalogindex.h" + +#include +#include +#include + +using namespace catalog; + +namespace { + +constexpr qint64 kT0 = 1'700'000'000'000LL; // arbitrary fixed "now", in ms + +TextureRecord makeRecord(const QString& path) +{ + TextureRecord rec; + rec.path = path; + rec.name = QFileInfo(path).completeBaseName(); + rec.fileSize = 4096; + rec.fileMtime = kT0; + rec.width = 2048; + rec.height = 2048; + rec.nodeCount = 12; + rec.libVersion = QStringLiteral("v3"); + rec.channels = ChannelAlbedo | ChannelNormal | ChannelRoughness; + return rec; +} + +} // namespace + +class TestCatalogIndex : public QObject { + Q_OBJECT + +private slots: + void init(); + void cleanup(); + + void createsSchemaOnFirstOpen(); + void reopenPreservesRows(); + void refusesToWriteWhenSchemaIsNewer(); + + void recordOpenedInsertsAndStamps(); + void recordOpenedTwiceDoesNotDuplicate(); + void createdAtSurvivesLaterWrites(); + void recordSavedStampsSavedNotOpened(); + void recordingClearsMissingFlag(); + void refusesRecordWithoutPath(); + + void movedFileBecomesASeparateRow(); + + void relocateRepointsAMissingRow(); + void relocateMergesWhenTargetIsAlreadyIndexed(); + void relocateRejectsUnknownRows(); + + void markMissingStampsOnlyOnce(); + void markPresentUpdatesFileStateAndClearsMissing(); + + void listFiltersRecentsExcludingMissing(); + void listFiltersStarredIncludingMissing(); + void listSortsAndPages(); + void searchMatchesNameAndTagsCaseInsensitively(); + void searchTreatsWildcardsLiterally(); + + void tagsRoundTripAndCascadeOnDelete(); + void removeAllMissingOnlyRemovesMissing(); + +private: + QString indexPath() const { return dir->filePath(QStringLiteral("index.db")); } + + QScopedPointer dir; + QScopedPointer index; +}; + +void TestCatalogIndex::init() +{ + dir.reset(new QTemporaryDir); + QVERIFY(dir->isValid()); + index.reset(new CatalogIndex); + QVERIFY(index->open(indexPath())); +} + +void TestCatalogIndex::cleanup() +{ + index.reset(); + dir.reset(); +} + +void TestCatalogIndex::createsSchemaOnFirstOpen() +{ + QVERIFY(index->isOpen()); + QVERIFY(!index->isReadOnly()); + QCOMPARE(index->schemaVersion(), CatalogIndex::SchemaVersion); + QCOMPARE(index->count(Query()), 0); +} + +void TestCatalogIndex::reopenPreservesRows() +{ + TextureRecord rec = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + + index->close(); + QVERIFY(index->open(indexPath())); + + QCOMPARE(index->count(Query()), 1); + const TextureRecord loaded = index->byPath(QStringLiteral("/tex/brick.texture")); + QVERIFY(loaded.isValid()); + QCOMPARE(loaded.name, QStringLiteral("brick")); + QCOMPARE(loaded.channels, int(ChannelAlbedo | ChannelNormal | ChannelRoughness)); + QCOMPARE(loaded.libVersion, QStringLiteral("v3")); +} + +void TestCatalogIndex::refusesToWriteWhenSchemaIsNewer() +{ + // index.db cannot be regenerated by rescanning, so a file from a newer + // build must be left strictly alone rather than migrated on a guess. + TextureRecord rec = makeRecord(QStringLiteral("/tex/a.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + index->close(); + + { + Database raw; + QVERIFY(raw.open(indexPath())); + QVERIFY(raw.exec(QStringLiteral("UPDATE meta SET v = '999' WHERE k = 'schema_version'"))); + } + + QTest::ignoreMessage(QtWarningMsg, + QRegularExpression(QStringLiteral("newer than this build"))); + QVERIFY(index->open(indexPath())); + QVERIFY(index->isReadOnly()); + + // Reads still work — the launcher should show what it can. + QCOMPARE(index->count(Query()), 1); + + TextureRecord blocked = makeRecord(QStringLiteral("/tex/b.texture")); + QVERIFY(!index->recordOpened(blocked, kT0)); + QVERIFY(!index->setStarred(1, true)); + QVERIFY(!index->remove(1)); + QCOMPARE(index->count(Query()), 1); +} + +void TestCatalogIndex::recordOpenedInsertsAndStamps() +{ + TextureRecord rec = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + + QVERIFY(rec.isValid()); + QCOMPARE(rec.lastOpened, kT0); + QCOMPARE(rec.lastSaved, 0LL); + QCOMPARE(rec.createdAt, kT0); + QVERIFY(!rec.isMissing()); + QCOMPARE(index->count(Query()), 1); +} + +void TestCatalogIndex::recordOpenedTwiceDoesNotDuplicate() +{ + TextureRecord first = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordOpened(first, kT0)); + + TextureRecord second = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordOpened(second, kT0 + 5000)); + + QCOMPARE(index->count(Query()), 1); + QCOMPARE(second.id, first.id); + QCOMPARE(second.lastOpened, kT0 + 5000); +} + +void TestCatalogIndex::createdAtSurvivesLaterWrites() +{ + TextureRecord rec = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + + TextureRecord again = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordSaved(again, kT0 + 60'000)); + + QCOMPARE(again.createdAt, kT0); + QCOMPARE(again.lastOpened, kT0); // preserved, not clobbered + QCOMPARE(again.lastSaved, kT0 + 60'000); +} + +void TestCatalogIndex::recordSavedStampsSavedNotOpened() +{ + TextureRecord rec = makeRecord(QStringLiteral("/tex/new.texture")); + QVERIFY(index->recordSaved(rec, kT0)); + + QCOMPARE(rec.lastSaved, kT0); + QCOMPARE(rec.lastOpened, 0LL); + + // A never-opened row must not appear under Recents. + Query recents; + recents.filter = Filter::Recents; + QCOMPARE(index->count(recents), 0); +} + +void TestCatalogIndex::recordingClearsMissingFlag() +{ + TextureRecord rec = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + QVERIFY(index->markMissing(rec.id, kT0 + 1000)); + QVERIFY(index->byId(rec.id).isMissing()); + + TextureRecord again = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordOpened(again, kT0 + 2000)); + QVERIFY(!index->byId(rec.id).isMissing()); +} + +void TestCatalogIndex::refusesRecordWithoutPath() +{ + TextureRecord rec; + QVERIFY(!index->recordOpened(rec, kT0)); + QCOMPARE(index->count(Query()), 0); +} + +void TestCatalogIndex::movedFileBecomesASeparateRow() +{ + // Paths are identity. Recognizing that a file moved would need a content + // hash or an mtime heuristic, and neither was worth its keep — so the old + // row simply stays behind, flagged missing, and the user removes it. The + // cost is losing stars and recency on a moved file; nothing breaks. + TextureRecord original = makeRecord(QStringLiteral("/old/brick.texture")); + QVERIFY(index->recordOpened(original, kT0)); + QVERIFY(index->setStarred(original.id, true)); + QVERIFY(index->addTag(original.id, QStringLiteral("stone"))); + QVERIFY(index->markMissing(original.id, kT0 + 1000)); + + TextureRecord moved = makeRecord(QStringLiteral("/new/brick.texture")); + QVERIFY(index->recordOpened(moved, kT0 + 2000)); + + QCOMPARE(index->count(Query()), 2); + QVERIFY(moved.id != original.id); + + // The old row is untouched and still dimmed, holding its own stars. + const TextureRecord stale = index->byId(original.id); + QVERIFY(stale.isValid()); + QVERIFY(stale.isMissing()); + QVERIFY(stale.starred); + + // The new one starts clean. + QVERIFY(!moved.starred); + QVERIFY(index->tags(moved.id).isEmpty()); + + // And "Remove from Launcher" is the way out. + QVERIFY(index->remove(original.id)); + QCOMPARE(index->count(Query()), 1); +} + +void TestCatalogIndex::relocateRepointsAMissingRow() +{ + // The user-driven counterpart to not detecting moves: the launcher can't + // guess where a file went, but Locate… lets the user say, and the row keeps + // everything it had. + TextureRecord rec = makeRecord(QStringLiteral("/old/brick.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + QVERIFY(index->setStarred(rec.id, true)); + QVERIFY(index->addTag(rec.id, QStringLiteral("stone"))); + QVERIFY(index->markMissing(rec.id, kT0 + 1000)); + + const qint64 survivor = + index->relocate(rec.id, QStringLiteral("/new/brick.texture"), 8192, kT0 + 5000); + QCOMPARE(survivor, rec.id); + + const TextureRecord moved = index->byId(rec.id); + QCOMPARE(moved.path, QStringLiteral("/new/brick.texture")); + QCOMPARE(moved.name, QStringLiteral("brick")); + QCOMPARE(moved.fileSize, 8192LL); + QCOMPARE(moved.fileMtime, kT0 + 5000); + QVERIFY(!moved.isMissing()); + QVERIFY(moved.starred); + QCOMPARE(index->tags(moved.id), QStringList({QStringLiteral("stone")})); + QCOMPARE(index->count(Query()), 1); +} + +void TestCatalogIndex::relocateMergesWhenTargetIsAlreadyIndexed() +{ + // path is UNIQUE, so relocating onto a path that's already indexed has to + // merge rather than fail — otherwise Locate… dead-ends exactly when the + // user already opened the file at its new home. + TextureRecord stale = makeRecord(QStringLiteral("/old/brick.texture")); + QVERIFY(index->recordOpened(stale, kT0)); + QVERIFY(index->setStarred(stale.id, true)); + QVERIFY(index->addTag(stale.id, QStringLiteral("stone"))); + QVERIFY(index->markMissing(stale.id, kT0 + 1000)); + + TextureRecord current = makeRecord(QStringLiteral("/new/brick.texture")); + QVERIFY(index->recordOpened(current, kT0 + 2000)); + QVERIFY(!current.starred); + + const qint64 survivor = + index->relocate(stale.id, QStringLiteral("/new/brick.texture"), 4096, kT0 + 2000); + + QCOMPARE(survivor, current.id); + QVERIFY(!index->byId(stale.id).isValid()); + QCOMPARE(index->count(Query()), 1); + + const TextureRecord merged = index->byId(current.id); + QVERIFY(merged.starred); // carried across + QCOMPARE(merged.createdAt, kT0); // earliest of the two + QCOMPARE(index->tags(merged.id), QStringList({QStringLiteral("stone")})); +} + +void TestCatalogIndex::relocateRejectsUnknownRows() +{ + QCOMPARE(index->relocate(999, QStringLiteral("/x.texture"), 1, 1), -1LL); + QCOMPARE(index->relocate(1, QString(), 1, 1), -1LL); +} + +void TestCatalogIndex::markMissingStampsOnlyOnce() +{ + TextureRecord rec = makeRecord(QStringLiteral("/tex/gone.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + + QVERIFY(index->markMissing(rec.id, kT0 + 1000)); + QVERIFY(index->markMissing(rec.id, kT0 + 9999)); + + // The card wants to say how long it's been gone, so the first sighting + // wins rather than being reset on every launcher open. + QCOMPARE(index->byId(rec.id).missingSince, kT0 + 1000); +} + +void TestCatalogIndex::markPresentUpdatesFileStateAndClearsMissing() +{ + // What reconciliation does when a file turns out to still be there: record + // the current size/mtime and un-dim the card. Comparing those two values + // against the stored row is the system's only change detection — a caller + // that sees them differ drops the cached thumbnail before calling this. + TextureRecord rec = makeRecord(QStringLiteral("/tex/edited.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + QVERIFY(index->markMissing(rec.id, kT0 + 1000)); + + QVERIFY(index->markPresent(rec.id, 8192, kT0 + 5000)); + + const TextureRecord updated = index->byId(rec.id); + QVERIFY(!updated.isMissing()); + QCOMPARE(updated.fileSize, 8192LL); + QCOMPARE(updated.fileMtime, kT0 + 5000); +} + +void TestCatalogIndex::listFiltersRecentsExcludingMissing() +{ + TextureRecord opened = makeRecord(QStringLiteral("/tex/opened.texture")); + QVERIFY(index->recordOpened(opened, kT0)); + + TextureRecord savedOnly = makeRecord(QStringLiteral("/tex/saved.texture")); + QVERIFY(index->recordSaved(savedOnly, kT0)); + + TextureRecord missing = makeRecord(QStringLiteral("/tex/missing.texture")); + QVERIFY(index->recordOpened(missing, kT0)); + QVERIFY(index->markMissing(missing.id, kT0 + 1000)); + + Query recents; + recents.filter = Filter::Recents; + const QVector rows = index->list(recents); + + QCOMPARE(rows.size(), 1); + QCOMPARE(rows.first().path, QStringLiteral("/tex/opened.texture")); + + // All still shows everything, missing included — those cards render dimmed. + QCOMPARE(index->count(Query()), 3); +} + +void TestCatalogIndex::listFiltersStarredIncludingMissing() +{ + TextureRecord starred = makeRecord(QStringLiteral("/tex/star.texture")); + QVERIFY(index->recordOpened(starred, kT0)); + QVERIFY(index->setStarred(starred.id, true)); + QVERIFY(index->markMissing(starred.id, kT0 + 1000)); + + TextureRecord plain = makeRecord(QStringLiteral("/tex/plain.texture")); + QVERIFY(index->recordOpened(plain, kT0)); + + Query query; + query.filter = Filter::Starred; + const QVector rows = index->list(query); + + // An unmounted drive must not hide the things the user explicitly marked. + QCOMPARE(rows.size(), 1); + QCOMPARE(rows.first().path, QStringLiteral("/tex/star.texture")); + QVERIFY(rows.first().isMissing()); +} + +void TestCatalogIndex::listSortsAndPages() +{ + const QStringList names = {QStringLiteral("charlie"), QStringLiteral("alpha"), + QStringLiteral("bravo"), QStringLiteral("delta")}; + for (int i = 0; i < names.size(); ++i) { + TextureRecord rec = makeRecord(QStringLiteral("/tex/%1.texture").arg(names[i])); + rec.fileMtime = kT0 + i * 1000; + rec.fileSize = 1000 * (i + 1); + QVERIFY(index->recordOpened(rec, kT0 + i * 1000)); + } + + Query byName; + byName.sort = SortKey::Name; + byName.ascending = true; + QVector rows = index->list(byName); + QCOMPARE(rows.size(), 4); + QCOMPARE(rows[0].name, QStringLiteral("alpha")); + QCOMPARE(rows[3].name, QStringLiteral("delta")); + + Query newestFirst; + newestFirst.sort = SortKey::Modified; + newestFirst.ascending = false; + rows = index->list(newestFirst); + QCOMPARE(rows.first().name, QStringLiteral("delta")); + + Query biggestFirst; + biggestFirst.sort = SortKey::Size; + biggestFirst.ascending = false; + rows = index->list(biggestFirst); + QCOMPARE(rows.first().fileSize, 4000LL); + + // Paging: the grid asks for a window, not the whole table. + Query page; + page.sort = SortKey::Name; + page.ascending = true; + page.limit = 2; + page.offset = 1; + rows = index->list(page); + QCOMPARE(rows.size(), 2); + QCOMPARE(rows[0].name, QStringLiteral("bravo")); + QCOMPARE(rows[1].name, QStringLiteral("charlie")); + + // count() ignores paging — it's the total the scrollbar needs. + QCOMPARE(index->count(page), 4); +} + +void TestCatalogIndex::searchMatchesNameAndTagsCaseInsensitively() +{ + TextureRecord brick = makeRecord(QStringLiteral("/tex/BrickWall.texture")); + QVERIFY(index->recordOpened(brick, kT0)); + + TextureRecord metal = makeRecord(QStringLiteral("/tex/rust.texture")); + QVERIFY(index->recordOpened(metal, kT0)); + QVERIFY(index->addTag(metal.id, QStringLiteral("Brickish"))); + + TextureRecord other = makeRecord(QStringLiteral("/tex/cloth.texture")); + QVERIFY(index->recordOpened(other, kT0)); + + Query query; + query.search = QStringLiteral("brick"); + const QVector rows = index->list(query); + + QCOMPARE(rows.size(), 2); + QCOMPARE(index->count(query), 2); +} + +void TestCatalogIndex::searchTreatsWildcardsLiterally() +{ + TextureRecord percent = makeRecord(QStringLiteral("/tex/50%25 grey.texture")); + percent.name = QStringLiteral("50% grey"); + QVERIFY(index->recordOpened(percent, kT0)); + + TextureRecord other = makeRecord(QStringLiteral("/tex/brick.texture")); + QVERIFY(index->recordOpened(other, kT0)); + + // Unescaped, "%" would match every row. + Query query; + query.search = QStringLiteral("%"); + QCOMPARE(index->count(query), 1); + + // Same for the single-character wildcard. + query.search = QStringLiteral("_"); + QCOMPARE(index->count(query), 0); +} + +void TestCatalogIndex::tagsRoundTripAndCascadeOnDelete() +{ + TextureRecord rec = makeRecord(QStringLiteral("/tex/tagged.texture")); + QVERIFY(index->recordOpened(rec, kT0)); + + QVERIFY(index->addTag(rec.id, QStringLiteral("stone"))); + QVERIFY(index->addTag(rec.id, QStringLiteral("outdoor"))); + QVERIFY(index->addTag(rec.id, QStringLiteral("stone"))); // idempotent + + QCOMPARE(index->tags(rec.id), + QStringList({QStringLiteral("outdoor"), QStringLiteral("stone")})); + + QVERIFY(index->removeTag(rec.id, QStringLiteral("outdoor"))); + QCOMPARE(index->tags(rec.id), QStringList({QStringLiteral("stone")})); + + // The tag table is WITHOUT ROWID with an ON DELETE CASCADE foreign key; + // this proves both are actually in force. + QVERIFY(index->remove(rec.id)); + QCOMPARE(index->tags(rec.id), QStringList()); + QCOMPARE(index->database().scalar(QStringLiteral("SELECT count(*) FROM tag")), 0LL); +} + +void TestCatalogIndex::removeAllMissingOnlyRemovesMissing() +{ + TextureRecord present = makeRecord(QStringLiteral("/tex/here.texture")); + QVERIFY(index->recordOpened(present, kT0)); + + TextureRecord gone = makeRecord(QStringLiteral("/tex/gone.texture")); + QVERIFY(index->recordOpened(gone, kT0)); + QVERIFY(index->markMissing(gone.id, kT0 + 1000)); + + QCOMPARE(index->removeAllMissing(), 1); + QCOMPARE(index->count(Query()), 1); + QVERIFY(index->byPath(QStringLiteral("/tex/here.texture")).isValid()); +} + +QTEST_GUILESS_MAIN(TestCatalogIndex) +#include "tst_catalogindex.moc" diff --git a/tests/tst_database.cpp b/tests/tst_database.cpp new file mode 100644 index 00000000..30ab3584 --- /dev/null +++ b/tests/tst_database.cpp @@ -0,0 +1,293 @@ +#include "database.h" + +#include +#include +#include + +using namespace catalog; + +class TestDatabase : public QObject { + Q_OBJECT + +private slots: + void init(); + + void opensFileAndReportsOpen(); + void appliesPageSizeBeforeAnyDdl(); + void appliesIncrementalAutoVacuum(); + void enablesWalMode(); + void enablesForeignKeys(); + void memoryDatabaseSkipsFilePragmas(); + + void transactionCommitPersists(); + void transactionRollsBackWhenScopeExits(); + void transactionRollsBackOnExplicitCall(); + void nestedTransactionIsInertAndDoesNotCommitOuter(); + + void execBatchIsAtomic(); + void scalarReturnsFallbackOnFailure(); + void readOnlyConnectionRejectsWrites(); + + void separateConnectionsCanReadConcurrently(); + +private: + QString dbFile(const QString& name) const { return dir.filePath(name); } + + QTemporaryDir dir; +}; + +void TestDatabase::init() +{ + QVERIFY(dir.isValid()); +} + +void TestDatabase::opensFileAndReportsOpen() +{ + Database db; + QVERIFY(db.open(dbFile(QStringLiteral("open.db")))); + QVERIFY(db.isOpen()); + + db.close(); + QVERIFY(!db.isOpen()); +} + +void TestDatabase::appliesPageSizeBeforeAnyDdl() +{ + // The ordering this asserts is the whole reason Database applies PRAGMAs + // itself: page_size only takes on an empty file. If a future refactor + // creates a table first, this drops back to the 4096 default and the + // setting is silently lost. + const QString path = dbFile(QStringLiteral("pagesize.db")); + + Database::Options options; + options.pageSize = 8192; + + Database db; + QVERIFY(db.open(path, options)); + QVERIFY(db.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); + + QCOMPARE(db.scalar(QStringLiteral("PRAGMA page_size")), 8192LL); + + // And it survives a reopen, i.e. it was written into the file header + // rather than just held in the connection. + db.close(); + Database reopened; + QVERIFY(reopened.open(path)); + QCOMPARE(reopened.scalar(QStringLiteral("PRAGMA page_size")), 8192LL); +} + +void TestDatabase::appliesIncrementalAutoVacuum() +{ + const QString path = dbFile(QStringLiteral("autovacuum.db")); + + Database::Options options; + options.incrementalAutoVacuum = true; + + Database db; + QVERIFY(db.open(path, options)); + QVERIFY(db.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); + + // 0 = NONE, 1 = FULL, 2 = INCREMENTAL. + QCOMPARE(db.scalar(QStringLiteral("PRAGMA auto_vacuum")), 2LL); + + db.close(); + Database reopened; + QVERIFY(reopened.open(path)); + QCOMPARE(reopened.scalar(QStringLiteral("PRAGMA auto_vacuum")), 2LL); +} + +void TestDatabase::enablesWalMode() +{ + Database db; + QVERIFY(db.open(dbFile(QStringLiteral("wal.db")))); + + QSqlQuery query = db.prepare(QStringLiteral("PRAGMA journal_mode")); + QVERIFY(query.exec()); + QVERIFY(query.next()); + QCOMPARE(query.value(0).toString().toLower(), QStringLiteral("wal")); +} + +void TestDatabase::enablesForeignKeys() +{ + Database db; + QVERIFY(db.open(dbFile(QStringLiteral("fk.db")))); + QCOMPARE(db.scalar(QStringLiteral("PRAGMA foreign_keys")), 1LL); + + QVERIFY(db.exec(QStringLiteral("CREATE TABLE parent (id INTEGER PRIMARY KEY)"))); + QVERIFY(db.exec(QStringLiteral( + "CREATE TABLE child (id INTEGER PRIMARY KEY, " + "parent_id INTEGER REFERENCES parent(id) ON DELETE CASCADE)"))); + QVERIFY(db.exec(QStringLiteral("INSERT INTO parent (id) VALUES (1)"))); + QVERIFY(db.exec(QStringLiteral("INSERT INTO child (id, parent_id) VALUES (1, 1)"))); + + // The cascade is what the tag table depends on for cleanup. + QVERIFY(db.exec(QStringLiteral("DELETE FROM parent WHERE id = 1"))); + QCOMPARE(db.scalar(QStringLiteral("SELECT count(*) FROM child")), 0LL); +} + +void TestDatabase::memoryDatabaseSkipsFilePragmas() +{ + // WAL is meaningless in memory and setting it fails; open() must not treat + // that as an error, because every test below uses :memory:. + Database db; + QVERIFY(db.open(QStringLiteral(":memory:"))); + QVERIFY(db.isOpen()); + QVERIFY(db.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); +} + +void TestDatabase::transactionCommitPersists() +{ + Database db; + QVERIFY(db.open(QStringLiteral(":memory:"))); + QVERIFY(db.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); + + { + Transaction tx(db); + QVERIFY(tx.isActive()); + QVERIFY(db.exec(QStringLiteral("INSERT INTO t (a) VALUES (1)"))); + QVERIFY(tx.commit()); + } + + QCOMPARE(db.scalar(QStringLiteral("SELECT count(*) FROM t")), 1LL); +} + +void TestDatabase::transactionRollsBackWhenScopeExits() +{ + // The property that matters: an early return anywhere inside a write batch + // leaves nothing behind. Nobody has to remember to roll back. + Database db; + QVERIFY(db.open(QStringLiteral(":memory:"))); + QVERIFY(db.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); + + { + Transaction tx(db); + QVERIFY(tx.isActive()); + QVERIFY(db.exec(QStringLiteral("INSERT INTO t (a) VALUES (1)"))); + // No commit — destructor rolls back. + } + + QCOMPARE(db.scalar(QStringLiteral("SELECT count(*) FROM t")), 0LL); + QVERIFY(!db.inTransaction()); +} + +void TestDatabase::transactionRollsBackOnExplicitCall() +{ + Database db; + QVERIFY(db.open(QStringLiteral(":memory:"))); + QVERIFY(db.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); + + Transaction tx(db); + QVERIFY(db.exec(QStringLiteral("INSERT INTO t (a) VALUES (1)"))); + tx.rollback(); + + QVERIFY(!tx.isActive()); + QCOMPARE(db.scalar(QStringLiteral("SELECT count(*) FROM t")), 0LL); +} + +void TestDatabase::nestedTransactionIsInertAndDoesNotCommitOuter() +{ + Database db; + QVERIFY(db.open(QStringLiteral(":memory:"))); + QVERIFY(db.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); + + { + Transaction outer(db); + QVERIFY(outer.isActive()); + QVERIFY(db.exec(QStringLiteral("INSERT INTO t (a) VALUES (1)"))); + + { + QTest::ignoreMessage(QtWarningMsg, + "catalog: nested transaction requested; inner scope is inert"); + Transaction inner(db); + QVERIFY(!inner.isActive()); + QVERIFY(!inner.commit()); + } + + // The inner scope ending must not have committed or rolled back the + // outer one — the write is still pending and still reversible. + QVERIFY(db.inTransaction()); + QCOMPARE(db.scalar(QStringLiteral("SELECT count(*) FROM t")), 1LL); + } + + QCOMPARE(db.scalar(QStringLiteral("SELECT count(*) FROM t")), 0LL); +} + +void TestDatabase::execBatchIsAtomic() +{ + Database db; + QVERIFY(db.open(QStringLiteral(":memory:"))); + + QStringList statements; + statements << QStringLiteral("CREATE TABLE a (x INTEGER)") + << QStringLiteral("CREATE TABLE b (x INTEGER)") + << QStringLiteral("THIS IS NOT SQL"); + + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("catalog: query failed"))); + QVERIFY(!db.execBatch(statements)); + + // Neither table should exist: the batch rolled back as a unit. + QCOMPARE(db.scalar(QStringLiteral("SELECT count(*) FROM sqlite_master WHERE type='table'")), + 0LL); +} + +void TestDatabase::scalarReturnsFallbackOnFailure() +{ + Database db; + QVERIFY(db.open(QStringLiteral(":memory:"))); + + QCOMPARE(db.scalar(QStringLiteral("SELECT x FROM nonexistent"), -7), -7LL); + QCOMPARE(db.scalar(QStringLiteral("SELECT NULL"), -7), -7LL); + QCOMPARE(db.scalar(QStringLiteral("SELECT 42"), -7), 42LL); +} + +void TestDatabase::readOnlyConnectionRejectsWrites() +{ + const QString path = dbFile(QStringLiteral("readonly.db")); + + { + Database writable; + QVERIFY(writable.open(path)); + QVERIFY(writable.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); + QVERIFY(writable.exec(QStringLiteral("INSERT INTO t (a) VALUES (1)"))); + } + + Database::Options options; + options.readOnly = true; + + Database readonly; + QVERIFY(readonly.open(path, options)); + QCOMPARE(readonly.scalar(QStringLiteral("SELECT count(*) FROM t")), 1LL); + + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("catalog: query failed"))); + QVERIFY(!readonly.exec(QStringLiteral("INSERT INTO t (a) VALUES (2)"))); +} + +void TestDatabase::separateConnectionsCanReadConcurrently() +{ + // WAL's actual promise: a reader on one connection is not blocked by an + // open write transaction on another. The reconciliation pass depends on + // this, since it runs off-thread while the GUI reads. + const QString path = dbFile(QStringLiteral("concurrent.db")); + + Database writer; + QVERIFY(writer.open(path)); + QVERIFY(writer.exec(QStringLiteral("CREATE TABLE t (a INTEGER)"))); + QVERIFY(writer.exec(QStringLiteral("INSERT INTO t (a) VALUES (1)"))); + + Database reader; + QVERIFY(reader.open(path)); + + Transaction tx(writer); + QVERIFY(tx.isActive()); + QVERIFY(writer.exec(QStringLiteral("INSERT INTO t (a) VALUES (2)"))); + + // Uncommitted write is invisible to the reader, and the read succeeds + // rather than blocking until the busy timeout expires. + QCOMPARE(reader.scalar(QStringLiteral("SELECT count(*) FROM t")), 1LL); + + QVERIFY(tx.commit()); + QCOMPARE(reader.scalar(QStringLiteral("SELECT count(*) FROM t")), 2LL); +} + +QTEST_GUILESS_MAIN(TestDatabase) +#include "tst_database.moc" diff --git a/tests/tst_jsonutils.cpp b/tests/tst_jsonutils.cpp new file mode 100644 index 00000000..630f921d --- /dev/null +++ b/tests/tst_jsonutils.cpp @@ -0,0 +1,80 @@ +#include "jsonutils.h" + +#include + +using namespace jsonutils; + +class TestJsonUtils : public QObject { + Q_OBJECT + +private slots: + void readsPlainNumbers(); + void readsNumbersStoredAsStrings(); + void roundsFloatsStoredForInts(); + void readsBoolsInEveryFormThatWasWritten(); + void readsNumbersStoredAsStringsForBools(); + void stringifiesNumbers(); + void fallsBackWhenMissingOrJunk(); +}; + +void TestJsonUtils::readsPlainNumbers() +{ + QCOMPARE(getDouble(QJsonValue(1.5)), 1.5); + QCOMPARE(getFloat(QJsonValue(1.5)), 1.5f); + QCOMPARE(getInt(QJsonValue(3)), 3); + QCOMPARE(getLong(QJsonValue(2147483648.0)), 2147483648L); +} + +void TestJsonUtils::readsNumbersStoredAsStrings() +{ + // Old files wrote prop values through JS string conversion. + QCOMPARE(getDouble(QJsonValue(QStringLiteral("1.5"))), 1.5); + QCOMPARE(getDouble(QJsonValue(QStringLiteral(" -0.25 "))), -0.25); + QCOMPARE(getInt(QJsonValue(QStringLiteral("7"))), 7); + QCOMPARE(getFloat(QJsonValue(QStringLiteral("1e2"))), 100.0f); +} + +void TestJsonUtils::roundsFloatsStoredForInts() +{ + // An int prop saved as 3.0 (or 2.7 after a slider drag) must not truncate + // to something a step below what the user set. + QCOMPARE(getInt(QJsonValue(3.0)), 3); + QCOMPARE(getInt(QJsonValue(2.7)), 3); + QCOMPARE(getInt(QJsonValue(QStringLiteral("2.7"))), 3); + QCOMPARE(getInt(QJsonValue(-2.7)), -3); +} + +void TestJsonUtils::readsBoolsInEveryFormThatWasWritten() +{ + QCOMPARE(getBool(QJsonValue(true)), true); + // BoolProp::toJson still writes the string form. + QCOMPARE(getBool(QJsonValue(QStringLiteral("true"))), true); + QCOMPARE(getBool(QJsonValue(QStringLiteral("TRUE"))), true); + QCOMPARE(getBool(QJsonValue(QStringLiteral("false"))), false); + QCOMPARE(getBool(QJsonValue(1.0)), true); + QCOMPARE(getBool(QJsonValue(0.0)), false); +} + +void TestJsonUtils::readsNumbersStoredAsStringsForBools() +{ + QCOMPARE(getBool(QJsonValue(QStringLiteral("1"))), true); + QCOMPARE(getBool(QJsonValue(QStringLiteral("0"))), false); +} + +void TestJsonUtils::stringifiesNumbers() +{ + QCOMPARE(getString(QJsonValue(3.0)), QStringLiteral("3")); + QCOMPARE(getString(QJsonValue(QStringLiteral("abc"))), QStringLiteral("abc")); +} + +void TestJsonUtils::fallsBackWhenMissingOrJunk() +{ + QJsonObject obj; + QCOMPARE(getDouble(obj["missing"], 300.0), 300.0); + QCOMPARE(getFloat(QJsonValue(QStringLiteral("not a number")), 2.5f), 2.5f); + QCOMPARE(getBool(QJsonValue(QJsonValue::Null), true), true); + QCOMPARE(getInt(QJsonValue(QJsonArray()), 9), 9); +} + +QTEST_APPLESS_MAIN(TestJsonUtils) +#include "tst_jsonutils.moc" diff --git a/tests/tst_texturelistmodel.cpp b/tests/tst_texturelistmodel.cpp new file mode 100644 index 00000000..9d2a7ddb --- /dev/null +++ b/tests/tst_texturelistmodel.cpp @@ -0,0 +1,290 @@ +#include "catalogindex.h" +#include "texturelistmodel.h" + +#include +#include +#include + +using namespace catalog; + +namespace { + +constexpr qint64 kT0 = 1'700'000'000'000LL; + +TextureRecord makeRecord(const QString& name, int ordinal) +{ + TextureRecord rec; + rec.path = QStringLiteral("/tex/%1.texture").arg(name); + rec.name = name; + rec.fileSize = 1000 * (ordinal + 1); + rec.fileMtime = kT0 + qint64(ordinal) * 1000; + rec.width = 2048; + rec.height = 2048; + rec.nodeCount = 12; + rec.libVersion = QStringLiteral("v3"); + rec.channels = ChannelAlbedo | ChannelNormal; + return rec; +} + +} // namespace + +class TestTextureListModel : public QObject { + Q_OBJECT + +private slots: + void init(); + void cleanup(); + + void emptyWithoutIndex(); + void passesModelTester(); + + void exposesRolesWithoutFormatting(); + void reportsTotalSeparatelyFromRowCount(); + void pagesInWithFetchMore(); + + void filterSwitchesRowSet(); + void sortChangesOrder(); + void searchNarrowsRows(); + + void openPathDrivesIsOpenRole(); + void migrationBadgeOnlyForKnownOlderVersions(); + + void refreshPicksUpExternalChanges(); + +private: + void seed(int count); + + QScopedPointer dir; + QScopedPointer index; + QScopedPointer model; +}; + +void TestTextureListModel::init() +{ + dir.reset(new QTemporaryDir); + QVERIFY(dir->isValid()); + + index.reset(new CatalogIndex); + QVERIFY(index->open(dir->filePath(QStringLiteral("index.db")))); + + model.reset(new TextureListModel); +} + +void TestTextureListModel::cleanup() +{ + model.reset(); + index.reset(); + dir.reset(); +} + +void TestTextureListModel::seed(int count) +{ + for (int i = 0; i < count; ++i) { + TextureRecord rec = makeRecord(QStringLiteral("tex%1").arg(i, 4, 10, QLatin1Char('0')), i); + QVERIFY(index->recordOpened(rec, kT0 + qint64(i) * 1000)); + } +} + +void TestTextureListModel::emptyWithoutIndex() +{ + // The launcher builds its model before the catalog is necessarily open; + // that must be an empty grid, not a crash. + QCOMPARE(model->rowCount(), 0); + QCOMPARE(model->totalCount(), 0); + QVERIFY(!model->data(model->index(0), TextureListModel::NameRole).isValid()); + QVERIFY(!model->canFetchMore(QModelIndex())); +} + +void TestTextureListModel::passesModelTester() +{ + // Catches the whole class of index/rowCount/parent contract violations that + // otherwise surface as a view crash on someone's machine. + seed(10); + QAbstractItemModelTester tester(model.data(), QAbstractItemModelTester::FailureReportingMode::Warning); + model->setIndex(index.data()); + model->setFilter(Filter::Recents); + model->setSort(SortKey::Name, true); + model->setSearchTerm(QStringLiteral("tex000")); + model->setSearchTerm(QString()); + model->refresh(); + QVERIFY(model->rowCount() > 0); +} + +void TestTextureListModel::exposesRolesWithoutFormatting() +{ + seed(1); + model->setIndex(index.data()); + QCOMPARE(model->rowCount(), 1); + + const QModelIndex idx = model->index(0); + + // Raw values, not display strings: no "2h ago", no "2K". Formatting belongs + // to the delegate, so the grid and a future list view can differ. + QCOMPARE(idx.data(TextureListModel::NameRole).toString(), QStringLiteral("tex0000")); + QCOMPARE(idx.data(TextureListModel::ModifiedRole).toLongLong(), kT0); + QCOMPARE(idx.data(TextureListModel::WidthRole).toInt(), 2048); + QCOMPARE(idx.data(TextureListModel::HeightRole).toInt(), 2048); + QCOMPARE(idx.data(TextureListModel::NodeCountRole).toInt(), 12); + QCOMPARE(idx.data(TextureListModel::ChannelsRole).toInt(), int(ChannelAlbedo | ChannelNormal)); + QCOMPARE(idx.data(TextureListModel::StarredRole).toBool(), false); + QCOMPARE(idx.data(TextureListModel::MissingRole).toBool(), false); + QCOMPARE(idx.data(Qt::ToolTipRole).toString(), QStringLiteral("/tex/tex0000.texture")); + + const TextureRecord rec = model->recordAt(idx); + QVERIFY(rec.isValid()); + QCOMPARE(model->indexForId(rec.id).row(), 0); +} + +void TestTextureListModel::reportsTotalSeparatelyFromRowCount() +{ + // The empty-state copy keys off totalCount(), which must describe the whole + // result set — not just the page that happens to be resident. + seed(TextureListModel::PageSize + 25); + model->setIndex(index.data()); + + QCOMPARE(model->rowCount(), TextureListModel::PageSize); + QCOMPARE(model->totalCount(), TextureListModel::PageSize + 25); +} + +void TestTextureListModel::pagesInWithFetchMore() +{ + const int extra = 25; + seed(TextureListModel::PageSize + extra); + model->setIndex(index.data()); + + QVERIFY(model->canFetchMore(QModelIndex())); + + QSignalSpy inserted(model.data(), &QAbstractItemModel::rowsInserted); + model->fetchMore(QModelIndex()); + + QCOMPARE(inserted.count(), 1); + QCOMPARE(model->rowCount(), TextureListModel::PageSize + extra); + QVERIFY(!model->canFetchMore(QModelIndex())); + + // Every row is distinct — an off-by-one in the offset would duplicate the + // page boundary, which looks like a rendering glitch rather than a bug. + QSet paths; + for (int row = 0; row < model->rowCount(); ++row) + paths.insert(model->index(row).data(TextureListModel::PathRole).toString()); + QCOMPARE(paths.size(), model->rowCount()); +} + +void TestTextureListModel::filterSwitchesRowSet() +{ + seed(3); + + // One saved but never opened, one starred. + TextureRecord savedOnly = makeRecord(QStringLiteral("savedonly"), 99); + QVERIFY(index->recordSaved(savedOnly, kT0)); + QVERIFY(index->setStarred(index->byPath(QStringLiteral("/tex/tex0000.texture")).id, true)); + + model->setIndex(index.data()); + QCOMPARE(model->totalCount(), 4); + + model->setFilter(Filter::Recents); + QCOMPARE(model->totalCount(), 3); // the save-only row is not a "recent" + + model->setFilter(Filter::Starred); + QCOMPARE(model->totalCount(), 1); + + model->setFilter(Filter::All); + QCOMPARE(model->totalCount(), 4); +} + +void TestTextureListModel::sortChangesOrder() +{ + seed(4); + model->setIndex(index.data()); + + model->setSort(SortKey::Name, true); + QCOMPARE(model->index(0).data(TextureListModel::NameRole).toString(), + QStringLiteral("tex0000")); + + model->setSort(SortKey::Name, false); + QCOMPARE(model->index(0).data(TextureListModel::NameRole).toString(), + QStringLiteral("tex0003")); + + model->setSort(SortKey::Modified, false); + QCOMPARE(model->index(0).data(TextureListModel::ModifiedRole).toLongLong(), kT0 + 3000); + + model->setSort(SortKey::Size, false); + QCOMPARE(model->index(0).data(TextureListModel::FileSizeRole).toLongLong(), 4000LL); +} + +void TestTextureListModel::searchNarrowsRows() +{ + seed(3); + model->setIndex(index.data()); + QCOMPARE(model->totalCount(), 3); + + model->setSearchTerm(QStringLiteral("tex0001")); + QCOMPARE(model->totalCount(), 1); + QCOMPARE(model->rowCount(), 1); + + model->setSearchTerm(QString()); + QCOMPARE(model->totalCount(), 3); +} + +void TestTextureListModel::openPathDrivesIsOpenRole() +{ + seed(2); + model->setIndex(index.data()); + model->setSort(SortKey::Name, true); + + QVERIFY(!model->index(0).data(TextureListModel::IsOpenRole).toBool()); + + QSignalSpy changed(model.data(), &QAbstractItemModel::dataChanged); + model->setOpenPath(QStringLiteral("/tex/tex0000.texture")); + + QCOMPARE(changed.count(), 1); + QVERIFY(model->index(0).data(TextureListModel::IsOpenRole).toBool()); + QVERIFY(!model->index(1).data(TextureListModel::IsOpenRole).toBool()); +} + +void TestTextureListModel::migrationBadgeOnlyForKnownOlderVersions() +{ + TextureRecord current = makeRecord(QStringLiteral("current"), 0); + current.libVersion = QStringLiteral("v3"); + QVERIFY(index->recordOpened(current, kT0)); + + TextureRecord old = makeRecord(QStringLiteral("old"), 1); + old.libVersion = QStringLiteral("v1"); + QVERIFY(index->recordOpened(old, kT0)); + + // Seeded from the recents list: we've never looked inside it. + TextureRecord unknown = makeRecord(QStringLiteral("unknown"), 2); + unknown.libVersion.clear(); + QVERIFY(index->recordOpened(unknown, kT0)); + + model->setIndex(index.data()); + model->setCurrentLibVersion(QStringLiteral("v3")); + model->setSort(SortKey::Name, true); + + auto badge = [this](int row) { + return model->index(row).data(TextureListModel::NeedsMigrationRole).toBool(); + }; + + QVERIFY(!badge(0)); // "current" + QVERIFY(badge(1)); // "old" + QVERIFY(!badge(2)); // "unknown" — absence of data is not evidence of age +} + +void TestTextureListModel::refreshPicksUpExternalChanges() +{ + seed(2); + model->setIndex(index.data()); + QCOMPARE(model->totalCount(), 2); + + TextureRecord added = makeRecord(QStringLiteral("added"), 5); + QVERIFY(index->recordOpened(added, kT0 + 99'000)); + + // The model doesn't watch the database; CatalogService::catalogChanged is + // what drives this in the app. + QCOMPARE(model->totalCount(), 2); + + model->refresh(); + QCOMPARE(model->totalCount(), 3); +} + +QTEST_MAIN(TestTextureListModel) +#include "tst_texturelistmodel.moc" diff --git a/tests/tst_thumbnailcache.cpp b/tests/tst_thumbnailcache.cpp new file mode 100644 index 00000000..4968edf4 --- /dev/null +++ b/tests/tst_thumbnailcache.cpp @@ -0,0 +1,339 @@ +#include "thumbnailcache.h" + +#include +#include +#include +#include +#include +#include + +using namespace catalog; + +namespace { + +constexpr qint64 kT0 = 1'700'000'000'000LL; + +// Stand-in for an encoded JPEG. Content doesn't matter, only that it round +// trips byte for byte — a BLOB column that mangles data would be silent. +QByteArray fakeImage(int sizeBytes, char fill = 'x') +{ + return QByteArray(sizeBytes, fill); +} + +ThumbKey keyFor(qint64 textureId, int size = 256) +{ + ThumbKey key; + key.textureId = textureId; + key.size = size; + return key; +} + +} // namespace + +class TestThumbnailCache : public QObject { + Q_OBJECT + +private slots: + void init(); + void cleanup(); + + void createsSchemaWithTunedPragmas(); + void putGetRoundTripsExactBytes(); + void realJpegSurvivesStoreAndDecode(); + void missReturnsEmpty(); + void sizeAndMeshArePartOfTheKey(); + void putOverwritesExistingVariant(); + void putRejectsEmptyOrUnkeyedImages(); + void putBatchWritesAll(); + + void removeTextureDropsEveryVariant(); + + void getDoesNotWriteLastUsed(); + void touchUpdatesLastUsed(); + + void evictionRemovesLeastRecentlyUsedFirst(); + void evictionIsNoOpUnderBudget(); + + void rebuildsWhenSchemaVersionDiffers(); + void rebuildsWhenFileIsCorrupt(); + void deletingFileWhileClosedIsRecoverable(); + +private: + QString cachePath() const { return dir->filePath(QStringLiteral("thumbs.db")); } + + QScopedPointer dir; + QScopedPointer cache; +}; + +void TestThumbnailCache::init() +{ + dir.reset(new QTemporaryDir); + QVERIFY(dir->isValid()); + cache.reset(new ThumbnailCache); + QVERIFY(cache->open(cachePath())); +} + +void TestThumbnailCache::cleanup() +{ + cache.reset(); + dir.reset(); +} + +void TestThumbnailCache::createsSchemaWithTunedPragmas() +{ + QVERIFY(cache->isOpen()); + QCOMPARE(cache->rowCount(), 0); + QCOMPARE(cache->totalBytes(), 0LL); + + // Both of these can only be set on an empty file, so if the schema were + // ever created before the PRAGMAs they'd silently revert to the defaults + // and incremental_vacuum would become a no-op. + Database raw; + QVERIFY(raw.open(cachePath())); + QCOMPARE(raw.scalar(QStringLiteral("PRAGMA page_size")), 8192LL); + QCOMPARE(raw.scalar(QStringLiteral("PRAGMA auto_vacuum")), 2LL); +} + +void TestThumbnailCache::putGetRoundTripsExactBytes() +{ + const QByteArray image = fakeImage(4096, '\x1'); + const ThumbKey key = keyFor(1); + + QVERIFY(cache->put(key, image, ThumbSource::Save, kT0)); + QVERIFY(cache->contains(key)); + QCOMPARE(cache->get(key), image); + QCOMPARE(cache->rowCount(), 1); + QCOMPARE(cache->totalBytes(), 4096LL); +} + +void TestThumbnailCache::realJpegSurvivesStoreAndDecode() +{ + // The capture path end to end minus the GL grab: encode a real image the + // way CatalogService::captureThumbnail does, store it, then decode it back + // the way the model does. A BLOB column that truncated or re-encoded would + // show up as a garbled card, which is hard to attribute after the fact. + QImage source(256, 256, QImage::Format_RGB32); + for (int y = 0; y < source.height(); ++y) + for (int x = 0; x < source.width(); ++x) + source.setPixel(x, y, qRgb(x, y, (x ^ y) & 0xFF)); + + QByteArray encoded; + QBuffer buffer(&encoded); + QVERIFY(buffer.open(QIODevice::WriteOnly)); + QVERIFY(source.save(&buffer, "JPG", 85)); + QVERIFY(!encoded.isEmpty()); + + const ThumbKey key = keyFor(7); + QVERIFY(cache->put(key, encoded, ThumbSource::Save, kT0)); + + const QByteArray fetched = cache->get(key); + QCOMPARE(fetched, encoded); + + // QImage, not QPixmap: a pixmap needs a QGuiApplication and this suite runs + // guiless. The decode path is what's under test either way. + QImage back; + QVERIFY(back.loadFromData(fetched, "JPG")); + QCOMPARE(back.size(), QSize(256, 256)); + + // Lossy, so compare structure rather than exact pixels: a black or + // transposed image would fail this while surviving a byte comparison. + QVERIFY(qAbs(qRed(back.pixel(200, 10)) - 200) < 24); + QVERIFY(qAbs(qGreen(back.pixel(10, 200)) - 200) < 24); +} + +void TestThumbnailCache::missReturnsEmpty() +{ + QVERIFY(cache->get(keyFor(2)).isEmpty()); + QVERIFY(!cache->contains(keyFor(2))); + + // An unkeyed request is a miss, not a crash. + QVERIFY(cache->get(ThumbKey()).isEmpty()); + QVERIFY(!cache->contains(ThumbKey())); +} + +void TestThumbnailCache::sizeAndMeshArePartOfTheKey() +{ + QVERIFY(cache->put(keyFor(1, 256), fakeImage(100, 'a'), ThumbSource::Save, kT0)); + QVERIFY(cache->put(keyFor(1, 512), fakeImage(200, 'b'), ThumbSource::Save, kT0)); + + ThumbKey otherMesh = keyFor(1, 256); + otherMesh.mesh = QStringLiteral("cube"); + QVERIFY(cache->put(otherMesh, fakeImage(300, 'c'), ThumbSource::Save, kT0)); + + QCOMPARE(cache->rowCount(), 3); + QCOMPARE(cache->get(keyFor(1, 256)).at(0), 'a'); + QCOMPARE(cache->get(keyFor(1, 512)).at(0), 'b'); + QCOMPARE(cache->get(otherMesh).at(0), 'c'); +} + +void TestThumbnailCache::putOverwritesExistingVariant() +{ + const ThumbKey key = keyFor(1); + QVERIFY(cache->put(key, fakeImage(100, 'a'), ThumbSource::Open, kT0)); + QVERIFY(cache->put(key, fakeImage(200, 'b'), ThumbSource::Save, kT0 + 1000)); + + // A better capture supersedes a cheaper one rather than adding a row. + QCOMPARE(cache->rowCount(), 1); + QCOMPARE(cache->get(key).size(), 200); + QCOMPARE(cache->get(key).at(0), 'b'); +} + +void TestThumbnailCache::putRejectsEmptyOrUnkeyedImages() +{ + QVERIFY(!cache->put(keyFor(1), QByteArray(), ThumbSource::Save, kT0)); + QVERIFY(!cache->put(ThumbKey(), fakeImage(100), ThumbSource::Save, kT0)); + QCOMPARE(cache->rowCount(), 0); +} + +void TestThumbnailCache::putBatchWritesAll() +{ + // One transaction per thumbnail means one fsync per thumbnail; the batch + // path exists so a bulk write doesn't crawl. + QVector entries; + for (int i = 0; i < 25; ++i) { + ThumbnailCache::Entry entry; + entry.key = keyFor(i); + entry.bytes = fakeImage(512); + entries << entry; + } + + QVERIFY(cache->putBatch(entries, kT0)); + QCOMPARE(cache->rowCount(), 25); + QCOMPARE(cache->totalBytes(), 25LL * 512); +} + +void TestThumbnailCache::removeTextureDropsEveryVariant() +{ + // How a stale thumbnail is invalidated now that there's no content hash: + // reconciliation sees size or mtime differ and drops the texture's images + // outright. Every variant goes, not just the size that happened to be on + // screen. + QVERIFY(cache->put(keyFor(1, 256), fakeImage(100), ThumbSource::Save, kT0)); + QVERIFY(cache->put(keyFor(1, 512), fakeImage(100), ThumbSource::Save, kT0)); + QVERIFY(cache->put(keyFor(2, 256), fakeImage(100), ThumbSource::Save, kT0)); + + QCOMPARE(cache->removeTexture(1), 2); + QCOMPARE(cache->rowCount(), 1); + QVERIFY(cache->contains(keyFor(2, 256))); + + QCOMPARE(cache->removeTexture(-1), 0); + QCOMPARE(cache->removeTexture(999), 0); +} + +void TestThumbnailCache::getDoesNotWriteLastUsed() +{ + // get() runs during scroll. A write per painted card is exactly what the + // "no disk writes on the GUI thread" rule forbids, so reads must be pure. + const ThumbKey key = keyFor(1); + QVERIFY(cache->put(key, fakeImage(100), ThumbSource::Save, kT0)); + + Database raw; + QVERIFY(raw.open(cachePath())); + const qint64 before = raw.scalar(QStringLiteral("SELECT last_used FROM thumb")); + + for (int i = 0; i < 10; ++i) + QVERIFY(!cache->get(key).isEmpty()); + + QCOMPARE(raw.scalar(QStringLiteral("SELECT last_used FROM thumb")), before); +} + +void TestThumbnailCache::touchUpdatesLastUsed() +{ + const ThumbKey key = keyFor(1); + QVERIFY(cache->put(key, fakeImage(100), ThumbSource::Save, kT0)); + + QVERIFY(cache->touch({key}, kT0 + 86'400'000)); + + Database raw; + QVERIFY(raw.open(cachePath())); + QCOMPARE(raw.scalar(QStringLiteral("SELECT last_used FROM thumb")), kT0 + 86'400'000); + + QVERIFY(cache->touch({}, kT0)); // empty batch is fine +} + +void TestThumbnailCache::evictionRemovesLeastRecentlyUsedFirst() +{ + // Ten 10 KiB images, each used a day apart. + for (int i = 0; i < 10; ++i) { + const ThumbKey key = keyFor(i); + QVERIFY(cache->put(key, fakeImage(10 * 1024), ThumbSource::Save, + kT0 + qint64(i) * 86'400'000)); + } + QCOMPARE(cache->totalBytes(), 10LL * 10 * 1024); + + // Trim to roughly half. + const int deleted = cache->evictTo(50 * 1024); + QCOMPARE(deleted, 5); + QCOMPARE(cache->rowCount(), 5); + QVERIFY(cache->totalBytes() <= 50 * 1024); + + // The five that survived are the five most recently used. + for (int i = 0; i < 5; ++i) + QVERIFY(!cache->contains(keyFor(i))); + for (int i = 5; i < 10; ++i) + QVERIFY(cache->contains(keyFor(i))); +} + +void TestThumbnailCache::evictionIsNoOpUnderBudget() +{ + QVERIFY(cache->put(keyFor(1), fakeImage(1024), ThumbSource::Save, kT0)); + + QCOMPARE(cache->evictTo(ThumbnailCache::DefaultBudgetBytes), 0); + QCOMPARE(cache->rowCount(), 1); +} + +void TestThumbnailCache::rebuildsWhenSchemaVersionDiffers() +{ + // A cache has no history worth migrating, so a schema from another build + // is thrown away rather than upgraded. This is the difference that makes + // thumbs.db disposable and index.db not. + QVERIFY(cache->put(keyFor(1), fakeImage(1024), ThumbSource::Save, kT0)); + cache->close(); + + { + Database raw; + QVERIFY(raw.open(cachePath())); + QVERIFY(raw.exec(QStringLiteral("UPDATE meta SET v = '99' WHERE k = 'schema_version'"))); + } + + QVERIFY(cache->open(cachePath())); + QVERIFY(cache->isOpen()); + QCOMPARE(cache->rowCount(), 0); + + // And it's usable immediately afterwards. + QVERIFY(cache->put(keyFor(1), fakeImage(1024), ThumbSource::Save, kT0)); + QCOMPARE(cache->rowCount(), 1); +} + +void TestThumbnailCache::rebuildsWhenFileIsCorrupt() +{ + cache->close(); + + { + QFile file(cachePath()); + QVERIFY(file.open(QIODevice::WriteOnly | QIODevice::Truncate)); + file.write("this is not a database, it is a picture of a database"); + } + + QVERIFY(cache->open(cachePath())); + QVERIFY(cache->isOpen()); + QVERIFY(cache->put(keyFor(1), fakeImage(1024), ThumbSource::Save, kT0)); + QCOMPARE(cache->rowCount(), 1); +} + +void TestThumbnailCache::deletingFileWhileClosedIsRecoverable() +{ + // "Deleting thumbs.db degrades gracefully" from the acceptance criteria. + QVERIFY(cache->put(keyFor(1), fakeImage(1024), ThumbSource::Save, kT0)); + cache->close(); + + QVERIFY(QFile::remove(cachePath())); + + QVERIFY(cache->open(cachePath())); + QCOMPARE(cache->rowCount(), 0); + QVERIFY(cache->get(keyFor(1)).isEmpty()); + QVERIFY(cache->put(keyFor(1), fakeImage(1024), ThumbSource::Save, kT0)); +} + +QTEST_GUILESS_MAIN(TestThumbnailCache) +#include "tst_thumbnailcache.moc" diff --git a/tests/tst_updateendpoint.cpp b/tests/tst_updateendpoint.cpp new file mode 100644 index 00000000..a5e7e14e --- /dev/null +++ b/tests/tst_updateendpoint.cpp @@ -0,0 +1,141 @@ +#include "updatechecker.h" + +#include "telemetry.h" + +#include +#include +#include +#include + +// UpdateChecker leaves breadcrumbs; the real Telemetry pulls in Sentry and a +// generated version header, neither of which this test has an opinion about. +// Stubbing the one entry point it uses keeps the test to the code under test. +namespace Telemetry { +void breadcrumb(const char*, const std::string&) {} +} // namespace Telemetry + +// Covers what decides *which server gets asked* and on what channel — the +// question you have when a dev server sees no traffic. The endpoint itself is +// hardcoded, so the interesting assertions are that it stays well-formed and +// that nothing outside the source can redirect it. +class TestUpdateEndpoint : public QObject { + Q_OBJECT + +private slots: + void initTestCase(); + void cleanup(); + + void endpointIsAWellFormedAbsoluteUrl(); + void endpointIgnoresTheEnvironment(); + + void channelFollowsOwnVersion(); + void channelSettingWinsOverVersion(); + void nonsenseChannelSettingIsIgnored(); + + void remembersAKnownUpdateAcrossRestarts(); + void forgetsAKnownUpdateOnceTheBuildCatchesUp(); +}; + +void TestUpdateEndpoint::initTestCase() +{ + // Keep every settings write inside the test's own scope rather than the + // developer's real config. + QStandardPaths::setTestModeEnabled(true); +} + +void TestUpdateEndpoint::cleanup() +{ + qunsetenv("TEXTURELAB_API_BASE"); + + QSettings settings(QSettings::UserScope, "texturelab", "texturelab"); + settings.remove("updateChannel"); + settings.remove("updateKnownVersion"); +} + +void TestUpdateEndpoint::endpointIsAWellFormedAbsoluteUrl() +{ + // The request path is appended directly, so the base has to be absolute and + // free of a trailing slash — "…:3333/" + "/api/…" is a double-slashed path + // that some routers answer with a 404. + const QString base = UpdateChecker::apiBase(); + + QVERIFY(!base.isEmpty()); + QVERIFY(base.startsWith(QStringLiteral("http"))); + QVERIFY(!base.endsWith(QLatin1Char('/'))); + + const QUrl url(base + QStringLiteral("/api/releases/latest")); + QVERIFY(url.isValid()); + QCOMPARE(url.path(), QStringLiteral("/api/releases/latest")); +} + +void TestUpdateEndpoint::endpointIgnoresTheEnvironment() +{ + // The endpoint is hardcoded. It used to be overridable, and this asserts + // the override is really gone rather than quietly still honoured — a stale + // variable in someone's shell would otherwise redirect update checks. + const QString before = UpdateChecker::apiBase(); + + qputenv("TEXTURELAB_API_BASE", "http://example.invalid:1234"); + QCOMPARE(UpdateChecker::apiBase(), before); +} + +void TestUpdateEndpoint::channelFollowsOwnVersion() +{ + QCoreApplication::setApplicationVersion(QStringLiteral("0.4.0-beta+abc1234")); + QCOMPARE(UpdateChecker::channel(), QStringLiteral("beta")); + + QCoreApplication::setApplicationVersion(QStringLiteral("1.0.0+abc1234")); + QCOMPARE(UpdateChecker::channel(), QStringLiteral("stable")); +} + +void TestUpdateEndpoint::channelSettingWinsOverVersion() +{ + QCoreApplication::setApplicationVersion(QStringLiteral("0.4.0-beta")); + QSettings(QSettings::UserScope, "texturelab", "texturelab") + .setValue(QStringLiteral("updateChannel"), QStringLiteral("stable")); + + QCOMPARE(UpdateChecker::channel(), QStringLiteral("stable")); +} + +void TestUpdateEndpoint::nonsenseChannelSettingIsIgnored() +{ + QCoreApplication::setApplicationVersion(QStringLiteral("0.4.0-beta")); + QSettings(QSettings::UserScope, "texturelab", "texturelab") + .setValue(QStringLiteral("updateChannel"), QStringLiteral("nightly")); + + // The API only accepts stable|beta; anything else would come back 422. + QCOMPARE(UpdateChecker::channel(), QStringLiteral("beta")); +} + +void TestUpdateEndpoint::remembersAKnownUpdateAcrossRestarts() +{ + // The launcher shows the notice from this, not from a fresh request — the + // network check is throttled to once every few hours, and without a + // remembered answer the notice would vanish in between. + QCoreApplication::setApplicationVersion(QStringLiteral("0.4.0-beta+abc1234")); + + QSettings settings(QSettings::UserScope, "texturelab", "texturelab"); + settings.setValue(QStringLiteral("updateKnownVersion"), QStringLiteral("1.3.0-beta")); + + QCOMPARE(UpdateChecker::knownUpdateVersion(), QStringLiteral("1.3.0-beta")); +} + +void TestUpdateEndpoint::forgetsAKnownUpdateOnceTheBuildCatchesUp() +{ + QSettings settings(QSettings::UserScope, "texturelab", "texturelab"); + settings.setValue(QStringLiteral("updateKnownVersion"), QStringLiteral("1.3.0-beta")); + + // Running exactly the remembered version: nothing left to announce. + QCoreApplication::setApplicationVersion(QStringLiteral("1.3.0-beta+deadbee")); + QVERIFY(UpdateChecker::knownUpdateVersion().isEmpty()); + + // And past it, which is what a beta tester hits after installing. + QCoreApplication::setApplicationVersion(QStringLiteral("1.4.0")); + QVERIFY(UpdateChecker::knownUpdateVersion().isEmpty()); + + settings.remove(QStringLiteral("updateKnownVersion")); + QVERIFY(UpdateChecker::knownUpdateVersion().isEmpty()); +} + +QTEST_GUILESS_MAIN(TestUpdateEndpoint) +#include "tst_updateendpoint.moc" diff --git a/tests/tst_versioncompare.cpp b/tests/tst_versioncompare.cpp new file mode 100644 index 00000000..03b87e11 --- /dev/null +++ b/tests/tst_versioncompare.cpp @@ -0,0 +1,104 @@ +#include "versioncompare.h" + +#include + +using namespace appversion; + +class TestVersionCompare : public QObject { + Q_OBJECT + +private slots: + void normalizeStripsBuildMetadataAndPrefix(); + + void comparesNumericParts(); + void treatsMissingPartsAsZero(); + void preReleaseSortsBelowRelease(); + void comparesPreReleaseIdentifiers(); + + void thisBuildIsNotNewerThanItself(); + void betaBuildSeesMatchingStableRelease(); + void malformedInputNeverClaimsAnUpdate(); +}; + +void TestVersionCompare::normalizeStripsBuildMetadataAndPrefix() +{ + // The app's own version is "0.4.0-beta+"; the API publishes + // "0.4.0-beta". Without stripping the metadata every check would either + // compare unequal strings or mis-parse the hash as a version part. + QCOMPARE(normalize(QStringLiteral("0.4.0-beta+a1b2c3d")), QStringLiteral("0.4.0-beta")); + QCOMPARE(normalize(QStringLiteral("v1.2.3")), QStringLiteral("1.2.3")); + QCOMPARE(normalize(QStringLiteral(" 1.2.3 ")), QStringLiteral("1.2.3")); +} + +void TestVersionCompare::comparesNumericParts() +{ + QCOMPARE(compare(QStringLiteral("1.0.0"), QStringLiteral("1.0.0")), 0); + QVERIFY(isNewer(QStringLiteral("1.0.1"), QStringLiteral("1.0.0"))); + QVERIFY(isNewer(QStringLiteral("1.1.0"), QStringLiteral("1.0.9"))); + QVERIFY(isNewer(QStringLiteral("2.0.0"), QStringLiteral("1.9.9"))); + QVERIFY(!isNewer(QStringLiteral("1.0.0"), QStringLiteral("1.0.1"))); + + // Not a string comparison: "0.10.0" is newer than "0.9.0" even though it + // sorts lower lexically. This is the classic way this goes wrong. + QVERIFY(isNewer(QStringLiteral("0.10.0"), QStringLiteral("0.9.0"))); + QVERIFY(isNewer(QStringLiteral("0.4.10"), QStringLiteral("0.4.9"))); +} + +void TestVersionCompare::treatsMissingPartsAsZero() +{ + QCOMPARE(compare(QStringLiteral("1.2"), QStringLiteral("1.2.0")), 0); + QCOMPARE(compare(QStringLiteral("1"), QStringLiteral("1.0.0")), 0); + QVERIFY(isNewer(QStringLiteral("1.2.1"), QStringLiteral("1.2"))); +} + +void TestVersionCompare::preReleaseSortsBelowRelease() +{ + // Semver's rule, and the one that matters most here: shipping 0.4.0 final + // must register as an update for someone on 0.4.0-beta. + QVERIFY(isNewer(QStringLiteral("0.4.0"), QStringLiteral("0.4.0-beta"))); + QVERIFY(!isNewer(QStringLiteral("0.4.0-beta"), QStringLiteral("0.4.0"))); + QCOMPARE(compare(QStringLiteral("0.4.0-beta"), QStringLiteral("0.4.0-beta")), 0); +} + +void TestVersionCompare::comparesPreReleaseIdentifiers() +{ + QVERIFY(isNewer(QStringLiteral("1.0.0-beta.2"), QStringLiteral("1.0.0-beta.1"))); + QVERIFY(isNewer(QStringLiteral("1.0.0-beta.10"), QStringLiteral("1.0.0-beta.9"))); + QVERIFY(isNewer(QStringLiteral("1.0.0-rc"), QStringLiteral("1.0.0-beta"))); + + // A longer run of otherwise-equal identifiers is the higher version. + QVERIFY(isNewer(QStringLiteral("1.0.0-beta.1"), QStringLiteral("1.0.0-beta"))); + + // Numeric identifiers rank below alphanumeric ones. + QVERIFY(isNewer(QStringLiteral("1.0.0-alpha"), QStringLiteral("1.0.0-1"))); +} + +void TestVersionCompare::thisBuildIsNotNewerThanItself() +{ + // The exact shape the app compares at runtime: its own version string, with + // the build hash attached, against what the API would publish for it. + const QString running = QStringLiteral("0.4.0-beta+e759268"); + QVERIFY(!isNewer(QStringLiteral("0.4.0-beta"), running)); + QCOMPARE(compare(QStringLiteral("0.4.0-beta"), running), 0); +} + +void TestVersionCompare::betaBuildSeesMatchingStableRelease() +{ + const QString running = QStringLiteral("0.4.0-beta+e759268"); + QVERIFY(isNewer(QStringLiteral("0.4.0"), running)); + QVERIFY(isNewer(QStringLiteral("0.5.0-beta"), running)); +} + +void TestVersionCompare::malformedInputNeverClaimsAnUpdate() +{ + // A broken response should mean "no update", never a false prompt and never + // a crash. + const QString running = QStringLiteral("0.4.0-beta+e759268"); + QVERIFY(!isNewer(QString(), running)); + QVERIFY(!isNewer(QStringLiteral("not-a-version"), running)); + QVERIFY(!isNewer(QStringLiteral("...."), running)); + QVERIFY(!isNewer(QStringLiteral("0.0.0"), running)); +} + +QTEST_GUILESS_MAIN(TestVersionCompare) +#include "tst_versioncompare.moc" diff --git a/tests/unit/.eslintrc.js b/tests/unit/.eslintrc.js deleted file mode 100644 index 84d2cc9d..00000000 --- a/tests/unit/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - env: { - jest: true - } -}; diff --git a/tests/unit/example.spec.ts b/tests/unit/example.spec.ts deleted file mode 100644 index 33601366..00000000 --- a/tests/unit/example.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { shallowMount } from "@vue/test-utils"; -import HelloWorld from "@/components/HelloWorld.vue"; - -describe("HelloWorld.vue", () => { - it("renders props.msg when passed", () => { - const msg = "new message"; - const wrapper = shallowMount(HelloWorld, { - propsData: { msg } - }); - expect(wrapper.text()).toMatch(msg); - }); -}); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index b42c6cd6..00000000 --- a/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "esnext", - "strict": false, - "jsx": "preserve", - "importHelpers": true, - "moduleResolution": "node", - "experimentalDecorators": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "sourceMap": true, - "baseUrl": ".", - "types": ["webpack-env", "jest"], - "paths": { - "@/*": ["src/*"] - }, - "lib": ["esnext", "dom", "dom.iterable", "scripthost", "es5", "es6"] - }, - "include": [ - "src/**/*.ts", - "src/**/*.tsx", - "src/**/*.vue", - "tests/**/*.ts", - "tests/**/*.tsx" - ], - "exclude": ["node_modules"] -} diff --git a/vue.config.js b/vue.config.js deleted file mode 100644 index 08b10752..00000000 --- a/vue.config.js +++ /dev/null @@ -1,25 +0,0 @@ -// vue.config.js - -module.exports = { - pluginOptions: { - electronBuilder: { - chainWebpackMainProcess: config => { - config.plugins.delete("uglify"); - }, - chainWebpackRendererProcess: config => { - config.plugins.delete("uglify"); - }, - builderOptions: { - win: { - icon: "build/icons/win/icon.ico" - }, - linux: { - icon: "build/icons/png/512x512.png" - }, - mac: { - icon: "build/icons/mac/icon.icns" - } - } - } - } -}; diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 1d67598e..00000000 --- a/yarn.lock +++ /dev/null @@ -1,13477 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"7zip-bin@~5.0.3": - version "5.0.3" - resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f" - integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA== - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/core@^7.0.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.5.tgz#1f15e2cca8ad9a1d78a38ddba612f5e7cdbbd330" - integrity sha512-O34LQooYVDXPl7QWCdW9p4NR+QlzOr7xShPPJz8GsuCU3/8ua/wqTr7gmnxXv+WBESiGU/G5s16i6tUvHkNb+w== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.5" - "@babel/helper-module-transforms" "^7.10.5" - "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.10.5" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.5" - "@babel/types" "^7.10.5" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.19" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/generator@^7.10.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.5.tgz#1b903554bc8c583ee8d25f1e8969732e6b829a69" - integrity sha512-3vXxr3FEW7E7lJZiWQ3bM4+v/Vyr9C+hpolQ8BGFr9Y8Ri2tFLWTixmwKBafDujO1WVah4fhZBeU1bieKdghig== - dependencies: - "@babel/types" "^7.10.5" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-annotate-as-pure@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" - integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" - integrity sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" - integrity sha512-0nkdeijB7VlZoLT3r/mY3bUkw3T8WG/hNw+FATs/6+pG2039IJWjTYL0VTISqsNHMUTEnwbVnc89WIJX9Qed0A== - dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-member-expression-to-functions" "^7.10.5" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" - -"@babel/helper-create-regexp-features-plugin@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" - integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-regex" "^7.10.4" - regexpu-core "^4.7.0" - -"@babel/helper-define-map@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" - integrity sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ== - dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/types" "^7.10.5" - lodash "^4.17.19" - -"@babel/helper-explode-assignable-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c" - integrity sha512-4K71RyRQNPRrR85sr5QY4X3VwG4wtVoXZB9+L3r1Gp38DhELyHCtovqydRi7c1Ovb17eRGiQ/FD5s8JdU0Uy5A== - dependencies: - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-hoist-variables@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" - integrity sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.5.tgz#172f56e7a63e78112f3a04055f24365af702e7ee" - integrity sha512-HiqJpYD5+WopCXIAbQDG0zye5XYVvcO9w/DHp5GsaGkRUaamLj2bEtu6i8rnGGprAhHM3qidCMgp71HF4endhA== - dependencies: - "@babel/types" "^7.10.5" - -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" - integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-module-transforms@^7.10.4", "@babel/helper-module-transforms@^7.10.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.5.tgz#120c271c0b3353673fcdfd8c053db3c544a260d6" - integrity sha512-4P+CWMJ6/j1W915ITJaUkadLObmCRRSC234uctJfn/vHrsLNxsR8dwlcXv9ZhJWzl77awf+mWXSZEKt5t0OnlA== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.5" - lodash "^4.17.19" - -"@babel/helper-optimise-call-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" - integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== - -"@babel/helper-regex@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" - integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== - dependencies: - lodash "^4.17.19" - -"@babel/helper-remap-async-to-generator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" - integrity sha512-86Lsr6NNw3qTNl+TBcF1oRZMaVzJtbWTyTko+CQL/tvNvcGYEFKbLXDPxtW0HKk3McNOk4KzY55itGWCAGK5tg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-wrap-function" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-replace-supers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" - integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.4" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-simple-access@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" - integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== - dependencies: - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-split-export-declaration@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz#2c70576eaa3b5609b24cb99db2888cc3fc4251d1" - integrity sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/helper-wrap-function@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" - integrity sha512-6py45WvEF0MhiLrdxtRjKjufwLL1/ob2qDJgg5JgNdojBAZSAKnAjkyOCNug6n+OBl4VW76XjvgSFTdaMcW0Ug== - dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helpers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" - integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== - dependencies: - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.10.4", "@babel/parser@^7.10.5", "@babel/parser@^7.7.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.5.tgz#e7c6bf5a7deff957cec9f04b551e2762909d826b" - integrity sha512-wfryxy4bE1UivvQKSQDU4/X6dr+i8bctjUjj8Zyt3DQy7NtPizJXT8M52nqpNKL+nq2PW8lxk4ZqLj0fD4B4hQ== - -"@babel/plugin-proposal-async-generator-functions@^7.2.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.5.tgz#3491cabf2f7c179ab820606cec27fed15e0e8558" - integrity sha512-cNMCVezQbrRGvXJwm9fu/1sJj9bHdGAgKodZdLqOQIpfoH3raqmRPBM17+lh7CzhiKRRBrGtZL9WcjxSoGYUSg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.10.4" - "@babel/plugin-syntax-async-generators" "^7.8.0" - -"@babel/plugin-proposal-class-properties@^7.0.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" - integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-proposal-decorators@^7.1.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.5.tgz#42898bba478bc4b1ae242a703a953a7ad350ffb4" - integrity sha512-Sc5TAQSZuLzgY0664mMDn24Vw2P8g/VhyLyGPaWiHahhgLqeZvcGeyBZOrJW0oSKIK2mvQ22a1ENXBIQLhrEiQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.10.5" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-decorators" "^7.10.4" - -"@babel/plugin-proposal-json-strings@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.4.tgz#593e59c63528160233bd321b1aebe0820c2341db" - integrity sha512-fCL7QF0Jo83uy1K0P2YXrfX11tj3lkpN7l4dMv9Y9VkowkhkQDwFHFd8IiwyK5MZjE8UpbgokkgtcReH88Abaw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.0" - -"@babel/plugin-proposal-object-rest-spread@^7.3.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.4.tgz#50129ac216b9a6a55b3853fdd923e74bf553a4c0" - integrity sha512-6vh4SqRuLLarjgeOf4EaROJAHjvu9Gl+/346PbDH9yWbJyfnJ/ah3jmYKYtswEyCoWZiidvVHjHshd4WgjB9BA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-transform-parameters" "^7.10.4" - -"@babel/plugin-proposal-optional-catch-binding@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" - integrity sha512-LflT6nPh+GK2MnFiKDyLiqSqVHkQnVf7hdoAvyTnnKj9xB3docGRsdPuxp6qqqW19ifK3xgc9U5/FwrSaCNX5g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - -"@babel/plugin-proposal-unicode-property-regex@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" - integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-async-generators@^7.2.0", "@babel/plugin-syntax-async-generators@^7.8.0": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-decorators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.10.4.tgz#6853085b2c429f9d322d02f5a635018cdeb2360c" - integrity sha512-2NaoC6fAk2VMdhY1eerkfHV+lVYC1u8b+jmRJISqANCJlTxYy19HGdIkkQtix2UtkcPuPu+IlDgrVseZnU03bw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-dynamic-import@^7.0.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-json-strings@^7.2.0", "@babel/plugin-syntax-json-strings@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.10.4.tgz#39abaae3cbf710c4373d8429484e6ba21340166c" - integrity sha512-KCg9mio9jwiARCB7WAcQ7Y1q+qicILjoK8LP/VkPkEKaf5dkaZZK1EcTe91a3JJlZ3qy6L5s9X52boEYi8DM9g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.2.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.2.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-transform-arrow-functions@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.4.tgz#e22960d77e697c74f41c501d44d73dbf8a6a64cd" - integrity sha512-9J/oD1jV0ZCBcgnoFWFq1vJd4msoKb/TCpGNFyyLt0zABdcvgK3aYikZ8HjzB14c26bc7E3Q1yugpwGy2aTPNA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-async-to-generator@^7.3.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" - integrity sha512-F6nREOan7J5UXTLsDsZG3DXmZSVofr2tGNwfdrVwkDWHfQckbQXnXSPfD7iO+c/2HGqycwyLST3DnZ16n+cBJQ== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.10.4" - -"@babel/plugin-transform-block-scoped-functions@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" - integrity sha512-WzXDarQXYYfjaV1szJvN3AD7rZgZzC1JtjJZ8dMHUyiK8mxPRahynp14zzNjU3VkPqPsO38CzxiWO1c9ARZ8JA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-block-scoping@^7.3.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.5.tgz#b81b8aafefbfe68f0f65f7ef397b9ece68a6037d" - integrity sha512-6Ycw3hjpQti0qssQcA6AMSFDHeNJ++R6dIMnpRqUjFeBBTmTDPa8zgF90OVfTvAo11mXZTlVUViY1g8ffrURLg== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-classes@^7.3.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" - integrity sha512-2oZ9qLjt161dn1ZE0Ms66xBncQH4In8Sqw1YWgBUZuGVJJS5c0OFZXL6dP2MRHrkU/eKhWg8CzFJhRQl50rQxA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-define-map" "^7.10.4" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" - integrity sha512-JFwVDXcP/hM/TbyzGq3l/XWGut7p46Z3QvqFMXTfk6/09m7xZHJUN9xHfsv7vqqD4YnfI5ueYdSJtXqqBLyjBw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-destructuring@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" - integrity sha512-+WmfvyfsyF603iPa6825mq6Qrb7uLjTOsa3XOFzlYcYDHSS4QmpOWOL0NNBY5qMbvrcf3tq0Cw+v4lxswOBpgA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-dotall-regex@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" - integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-duplicate-keys@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" - integrity sha512-GL0/fJnmgMclHiBTTWXNlYjYsA7rDrtsazHG6mglaGSTh0KsrW04qml+Bbz9FL0LcJIRwBWL5ZqlNHKTkU3xAA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-exponentiation-operator@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" - integrity sha512-S5HgLVgkBcRdyQAHbKj+7KyuWx8C6t5oETmUuwz1pt3WTWJhsUV0WIIXuVvfXMxl/QQyHKlSCNNtaIamG8fysw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-for-of@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.4.tgz#c08892e8819d3a5db29031b115af511dbbfebae9" - integrity sha512-ItdQfAzu9AlEqmusA/65TqJ79eRcgGmpPPFvBnGILXZH975G0LNjP1yjHvGgfuCxqrPPueXOPe+FsvxmxKiHHQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-function-name@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" - integrity sha512-OcDCq2y5+E0dVD5MagT5X+yTRbcvFjDI2ZVAottGH6tzqjx/LKpgkUepu3hp/u4tZBzxxpNGwLsAvGBvQ2mJzg== - dependencies: - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-literals@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" - integrity sha512-Xd/dFSTEVuUWnyZiMu76/InZxLTYilOSr1UlHV+p115Z/Le2Fi1KXkJUYz0b42DfndostYlPub3m8ZTQlMaiqQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-modules-amd@^7.2.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" - integrity sha512-elm5uruNio7CTLFItVC/rIzKLfQ17+fX7EVz5W0TMgIHFo1zY0Ozzx+lgwhL4plzl8OzVn6Qasx5DeEFyoNiRw== - dependencies: - "@babel/helper-module-transforms" "^7.10.5" - "@babel/helper-plugin-utils" "^7.10.4" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" - integrity sha512-Xj7Uq5o80HDLlW64rVfDBhao6OX89HKUmb+9vWYaLXBZOma4gA6tw4Ni1O5qVDoZWUV0fxMYA0aYzOawz0l+1w== - dependencies: - "@babel/helper-module-transforms" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.3.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" - integrity sha512-f4RLO/OL14/FP1AEbcsWMzpbUz6tssRaeQg11RH1BP/XnPpRoVwgeYViMFacnkaw4k4wjRSjn3ip1Uw9TaXuMw== - dependencies: - "@babel/helper-hoist-variables" "^7.10.4" - "@babel/helper-module-transforms" "^7.10.5" - "@babel/helper-plugin-utils" "^7.10.4" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" - integrity sha512-mohW5q3uAEt8T45YT7Qc5ws6mWgJAaL/8BfWD9Dodo1A3RKWli8wTS+WiQ/knF+tXlPirW/1/MqzzGfCExKECA== - dependencies: - "@babel/helper-module-transforms" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" - integrity sha512-V6LuOnD31kTkxQPhKiVYzYC/Jgdq53irJC/xBSmqcNcqFGV+PER4l6rU5SH2Vl7bH9mLDHcc0+l9HUOe4RNGKA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" - -"@babel/plugin-transform-new-target@^7.0.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" - integrity sha512-YXwWUDAH/J6dlfwqlWsztI2Puz1NtUAubXhOPLQ5gjR/qmQ5U96DY4FQO8At33JN4XPBhrjB8I4eMmLROjjLjw== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-object-super@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" - integrity sha512-5iTw0JkdRdJvr7sY0vHqTpnruUpTea32JHmq/atIWqsnNussbRzjEDyWep8UNztt1B5IusBYg8Irb0bLbiEBCQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - -"@babel/plugin-transform-parameters@^7.10.4", "@babel/plugin-transform-parameters@^7.2.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" - integrity sha512-xPHwUj5RdFV8l1wuYiu5S9fqWGM2DrYc24TMvUiRrPVm+SM3XeqU9BcokQX/kEUe+p2RBwy+yoiR1w/Blq6ubw== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-regenerator@^7.3.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.4.tgz#2015e59d839074e76838de2159db421966fd8b63" - integrity sha512-3thAHwtor39A7C04XucbMg17RcZ3Qppfxr22wYzZNcVIkPHfpM9J0SO8zuCV6SZa265kxBJSrfKTvDCYqBFXGw== - dependencies: - regenerator-transform "^0.14.2" - -"@babel/plugin-transform-runtime@^7.4.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.5.tgz#3b39b7b24830e0c2d8ff7a4489fe5cf99fbace86" - integrity sha512-tV4V/FjElJ9lQtyjr5xD2IFFbgY46r7EeVu5a8CpEKT5laheHKSlFeHjpkPppW3PqzGLAuv5k2qZX5LgVZIX5w== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - resolve "^1.8.1" - semver "^5.5.1" - -"@babel/plugin-transform-shorthand-properties@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.4.tgz#9fd25ec5cdd555bb7f473e5e6ee1c971eede4dd6" - integrity sha512-AC2K/t7o07KeTIxMoHneyX90v3zkm5cjHJEokrPEAGEy3UCp8sLKfnfOIGdZ194fyN4wfX/zZUWT9trJZ0qc+Q== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-spread@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.4.tgz#4e2c85ea0d6abaee1b24dcfbbae426fe8d674cff" - integrity sha512-1e/51G/Ni+7uH5gktbWv+eCED9pP8ZpRhZB3jOaI3mmzfvJTWHkuyYTv0Z5PYtyM+Tr2Ccr9kUdQxn60fI5WuQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-sticky-regex@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" - integrity sha512-Ddy3QZfIbEV0VYcVtFDCjeE4xwVTJWTmUtorAJkn6u/92Z/nWJNV+mILyqHKrUxXYKA2EoCilgoPePymKL4DvQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-regex" "^7.10.4" - -"@babel/plugin-transform-template-literals@^7.2.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" - integrity sha512-V/lnPGIb+KT12OQikDvgSuesRX14ck5FfJXt6+tXhdkJ+Vsd0lDCVtF6jcB4rNClYFzaB2jusZ+lNISDk2mMMw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-typeof-symbol@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" - integrity sha512-QqNgYwuuW0y0H+kUE/GWSR45t/ccRhe14Fs/4ZRouNNQsyd4o3PG4OtHiIrepbM2WKUBDAXKCAK/Lk4VhzTaGA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-unicode-regex@^7.2.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.4.tgz#e56d71f9282fac6db09c82742055576d5e6d80a8" - integrity sha512-wNfsc4s8N2qnIwpO/WP2ZiSyjfpTamT2C9V9FDH/Ljub9zw6P3SjkXcFmc0RQUt96k2fmIvtla2MMjgTwIAC+A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/preset-env@^7.0.0 < 7.4.0": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1" - integrity sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-async-generator-functions" "^7.2.0" - "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.3.4" - "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" - "@babel/plugin-syntax-async-generators" "^7.2.0" - "@babel/plugin-syntax-json-strings" "^7.2.0" - "@babel/plugin-syntax-object-rest-spread" "^7.2.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" - "@babel/plugin-transform-arrow-functions" "^7.2.0" - "@babel/plugin-transform-async-to-generator" "^7.3.4" - "@babel/plugin-transform-block-scoped-functions" "^7.2.0" - "@babel/plugin-transform-block-scoping" "^7.3.4" - "@babel/plugin-transform-classes" "^7.3.4" - "@babel/plugin-transform-computed-properties" "^7.2.0" - "@babel/plugin-transform-destructuring" "^7.2.0" - "@babel/plugin-transform-dotall-regex" "^7.2.0" - "@babel/plugin-transform-duplicate-keys" "^7.2.0" - "@babel/plugin-transform-exponentiation-operator" "^7.2.0" - "@babel/plugin-transform-for-of" "^7.2.0" - "@babel/plugin-transform-function-name" "^7.2.0" - "@babel/plugin-transform-literals" "^7.2.0" - "@babel/plugin-transform-modules-amd" "^7.2.0" - "@babel/plugin-transform-modules-commonjs" "^7.2.0" - "@babel/plugin-transform-modules-systemjs" "^7.3.4" - "@babel/plugin-transform-modules-umd" "^7.2.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" - "@babel/plugin-transform-new-target" "^7.0.0" - "@babel/plugin-transform-object-super" "^7.2.0" - "@babel/plugin-transform-parameters" "^7.2.0" - "@babel/plugin-transform-regenerator" "^7.3.4" - "@babel/plugin-transform-shorthand-properties" "^7.2.0" - "@babel/plugin-transform-spread" "^7.2.0" - "@babel/plugin-transform-sticky-regex" "^7.2.0" - "@babel/plugin-transform-template-literals" "^7.2.0" - "@babel/plugin-transform-typeof-symbol" "^7.2.0" - "@babel/plugin-transform-unicode-regex" "^7.2.0" - browserslist "^4.3.4" - invariant "^2.2.2" - js-levenshtein "^1.1.3" - semver "^5.3.0" - -"@babel/runtime-corejs2@^7.2.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.10.5.tgz#8daa1ceccc0468e5c2e15f124e3f51c2b3033b49" - integrity sha512-LJwyb1ac//Jr2zrGTTaNJhrP1wYCgVw9rzHbQPogKXCTLQ60EEWgeNtuqs6cLsq64O557SYzziCrOxNp0rRi8w== - dependencies: - core-js "^2.6.5" - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.8.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" - integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.7.2": - version "7.12.18" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.18.tgz#af137bd7e7d9705a412b3caaf991fe6aaa97831b" - integrity sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/template@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/traverse@^7.10.4", "@babel/traverse@^7.10.5", "@babel/traverse@^7.7.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.5.tgz#77ce464f5b258be265af618d8fddf0536f20b564" - integrity sha512-yc/fyv2gUjPqzTz0WHeRJH2pv7jA9kA7mBX2tXl/x5iOE81uaVPuGPtaYk7wmkx4b67mQ7NqI8rmT2pF47KYKQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.10.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.10.4" - "@babel/parser" "^7.10.5" - "@babel/types" "^7.10.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.7.0": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.5.tgz#d88ae7e2fde86bfbfe851d4d81afa70a997b5d15" - integrity sha512-ixV66KWfCI6GKoA/2H9v6bQdbfXEwwpOdQ8cRvb4F+eyvhlaHxWFMQB4+3d9QFJXZsiiiqVrewNV0DFEQpyT4Q== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - -"@develar/schema-utils@~2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.1.0.tgz#eceb1695bfbed6f6bb84666d5d3abe5e1fd54e17" - integrity sha512-qjCqB4ctMig9Gz5bd6lkdFr3bO6arOdQqptdBSpF1ZpCnjofieCciEzkoS9ujY9cMGyllYSCSmBJ3x9OKHXzoA== - dependencies: - ajv "^6.1.0" - ajv-keywords "^3.1.0" - -"@electron/get@^1.0.1": - version "1.12.4" - resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.4.tgz#a5971113fc1bf8fa12a8789dc20152a7359f06ab" - integrity sha512-6nr9DbJPUR9Xujw6zD3y+rS95TyItEVM0NVjt1EehY2vUWfIgPiIPVHxCvaTS0xr2B+DRxovYVKbuOWqC35kjg== - dependencies: - debug "^4.1.1" - env-paths "^2.2.0" - fs-extra "^8.1.0" - got "^9.6.0" - progress "^2.0.3" - semver "^6.2.0" - sumchecker "^3.0.1" - optionalDependencies: - global-agent "^2.0.2" - global-tunnel-ng "^2.7.1" - -"@electron/remote@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@electron/remote/-/remote-1.2.0.tgz#772eb4c3ac17aaba5a9cf05a09092f6277f5671f" - integrity sha512-C774t2DFVJsa+dxU9Gc2nYzylRZoJ79I0Sxrh8T9cN69fBkntfGbyBEQiD9UfZopqL0CYLzk1anY2Ywhql6h1w== - -"@hapi/address@2.x.x": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" - integrity sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ== - -"@hapi/bourne@1.x.x": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-1.3.2.tgz#0a7095adea067243ce3283e1b56b8a8f453b242a" - integrity sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA== - -"@hapi/hoek@8.x.x", "@hapi/hoek@^8.3.0": - version "8.5.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" - integrity sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow== - -"@hapi/joi@^15.0.1": - version "15.1.1" - resolved "https://registry.yarnpkg.com/@hapi/joi/-/joi-15.1.1.tgz#c675b8a71296f02833f8d6d243b34c57b8ce19d7" - integrity sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ== - dependencies: - "@hapi/address" "2.x.x" - "@hapi/bourne" "1.x.x" - "@hapi/hoek" "8.x.x" - "@hapi/topo" "3.x.x" - -"@hapi/topo@3.x.x": - version "3.1.6" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-3.1.6.tgz#68d935fa3eae7fdd5ab0d7f953f3205d8b2bfc29" - integrity sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ== - dependencies: - "@hapi/hoek" "^8.3.0" - -"@intervolga/optimize-cssnano-plugin@^1.0.5": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@intervolga/optimize-cssnano-plugin/-/optimize-cssnano-plugin-1.0.6.tgz#be7c7846128b88f6a9b1d1261a0ad06eb5c0fdf8" - integrity sha512-zN69TnSr0viRSU6cEDIcuPcP67QcpQ6uHACg58FiN9PDrU6SLyGW3MR4tiISbYxy1kDWAVPwD+XwQTWE5cigAA== - dependencies: - cssnano "^4.0.0" - cssnano-preset-default "^4.0.0" - postcss "^7.0.0" - -"@jimp/bmp@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.16.1.tgz#6e2da655b2ba22e721df0795423f34e92ef13768" - integrity sha512-iwyNYQeBawrdg/f24x3pQ5rEx+/GwjZcCXd3Kgc+ZUd+Ivia7sIqBsOnDaMZdKCBPlfW364ekexnlOqyVa0NWg== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - bmp-js "^0.1.0" - -"@jimp/core@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/core/-/core-0.16.1.tgz#68c4288f6ef7f31a0f6b859ba3fb28dae930d39d" - integrity sha512-la7kQia31V6kQ4q1kI/uLimu8FXx7imWVajDGtwUG8fzePLWDFJyZl0fdIXVCL1JW2nBcRHidUot6jvlRDi2+g== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - any-base "^1.1.0" - buffer "^5.2.0" - exif-parser "^0.1.12" - file-type "^9.0.0" - load-bmfont "^1.3.1" - mkdirp "^0.5.1" - phin "^2.9.1" - pixelmatch "^4.0.2" - tinycolor2 "^1.4.1" - -"@jimp/custom@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/custom/-/custom-0.16.1.tgz#28b659c59e20a1d75a0c46067bd3f4bd302cf9c5" - integrity sha512-DNUAHNSiUI/j9hmbatD6WN/EBIyeq4AO0frl5ETtt51VN1SvE4t4v83ZA/V6ikxEf3hxLju4tQ5Pc3zmZkN/3A== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/core" "^0.16.1" - -"@jimp/gif@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/gif/-/gif-0.16.1.tgz#d1f7c3a58f4666482750933af8b8f4666414f3ca" - integrity sha512-r/1+GzIW1D5zrP4tNrfW+3y4vqD935WBXSc8X/wm23QTY9aJO9Lw6PEdzpYCEY+SOklIFKaJYUAq/Nvgm/9ryw== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - gifwrap "^0.9.2" - omggif "^1.0.9" - -"@jimp/jpeg@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/jpeg/-/jpeg-0.16.1.tgz#3b7bb08a4173f2f6d81f3049b251df3ee2ac8175" - integrity sha512-8352zrdlCCLFdZ/J+JjBslDvml+fS3Z8gttdml0We759PnnZGqrnPRhkOEOJbNUlE+dD4ckLeIe6NPxlS/7U+w== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - jpeg-js "0.4.2" - -"@jimp/plugin-blit@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-blit/-/plugin-blit-0.16.1.tgz#09ea919f9d326de3b9c2826fe4155da37dde8edb" - integrity sha512-fKFNARm32RoLSokJ8WZXHHH2CGzz6ire2n1Jh6u+XQLhk9TweT1DcLHIXwQMh8oR12KgjbgsMGvrMVlVknmOAg== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-blur@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-blur/-/plugin-blur-0.16.1.tgz#e614fa002797dcd662e705d4cea376e7db968bf5" - integrity sha512-1WhuLGGj9MypFKRcPvmW45ht7nXkOKu+lg3n2VBzIB7r4kKNVchuI59bXaCYQumOLEqVK7JdB4glaDAbCQCLyw== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-circle@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-circle/-/plugin-circle-0.16.1.tgz#20e3194a67ca29740aba2630fd4d0a89afa27491" - integrity sha512-JK7yi1CIU7/XL8hdahjcbGA3V7c+F+Iw+mhMQhLEi7Q0tCnZ69YJBTamMiNg3fWPVfMuvWJJKOBRVpwNTuaZRg== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-color@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-color/-/plugin-color-0.16.1.tgz#0f298ba74dee818b663834cd80d53e56f3755233" - integrity sha512-9yQttBAO5SEFj7S6nJK54f+1BnuBG4c28q+iyzm1JjtnehjqMg6Ljw4gCSDCvoCQ3jBSYHN66pmwTV74SU1B7A== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - tinycolor2 "^1.4.1" - -"@jimp/plugin-contain@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-contain/-/plugin-contain-0.16.1.tgz#3c5f5c495fd9bb08a970739d83694934f58123f2" - integrity sha512-44F3dUIjBDHN+Ym/vEfg+jtjMjAqd2uw9nssN67/n4FdpuZUVs7E7wadKY1RRNuJO+WgcD5aDQcsvurXMETQTg== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-cover@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-cover/-/plugin-cover-0.16.1.tgz#0e8caec16a40abe15b1b32e5383a603a3306dc41" - integrity sha512-YztWCIldBAVo0zxcQXR+a/uk3/TtYnpKU2CanOPJ7baIuDlWPsG+YE4xTsswZZc12H9Kl7CiziEbDtvF9kwA/Q== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-crop@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-crop/-/plugin-crop-0.16.1.tgz#b362497c873043fe47ba881ab08604bf7226f50f" - integrity sha512-UQdva9oQzCVadkyo3T5Tv2CUZbf0klm2cD4cWMlASuTOYgaGaFHhT9st+kmfvXjKL8q3STkBu/zUPV6PbuV3ew== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-displace@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-displace/-/plugin-displace-0.16.1.tgz#4dd9db518c3e78de9d723f86a234bf98922afe8d" - integrity sha512-iVAWuz2+G6Heu8gVZksUz+4hQYpR4R0R/RtBzpWEl8ItBe7O6QjORAkhxzg+WdYLL2A/Yd4ekTpvK0/qW8hTVw== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-dither@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-dither/-/plugin-dither-0.16.1.tgz#b47de2c0bb09608bed228b41c3cd01a85ec2d45b" - integrity sha512-tADKVd+HDC9EhJRUDwMvzBXPz4GLoU6s5P7xkVq46tskExYSptgj5713J5Thj3NMgH9Rsqu22jNg1H/7tr3V9Q== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-fisheye@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-fisheye/-/plugin-fisheye-0.16.1.tgz#f625047b6cdbe1b83b89e9030fd025ab19cdb1a4" - integrity sha512-BWHnc5hVobviTyIRHhIy9VxI1ACf4CeSuCfURB6JZm87YuyvgQh5aX5UDKtOz/3haMHXBLP61ZBxlNpMD8CG4A== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-flip@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-flip/-/plugin-flip-0.16.1.tgz#7a99ea22bde802641017ed0f2615870c144329bb" - integrity sha512-KdxTf0zErfZ8DyHkImDTnQBuHby+a5YFdoKI/G3GpBl3qxLBvC+PWkS2F/iN3H7wszP7/TKxTEvWL927pypT0w== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-gaussian@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-gaussian/-/plugin-gaussian-0.16.1.tgz#0845e314085ccd52e34fad9a83949bc0d81a68e8" - integrity sha512-u9n4wjskh3N1mSqketbL6tVcLU2S5TEaFPR40K6TDv4phPLZALi1Of7reUmYpVm8mBDHt1I6kGhuCJiWvzfGyg== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-invert@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-invert/-/plugin-invert-0.16.1.tgz#7e6f5a15707256f3778d06921675bbcf18545c97" - integrity sha512-2DKuyVXANH8WDpW9NG+PYFbehzJfweZszFYyxcaewaPLN0GxvxVLOGOPP1NuUTcHkOdMFbE0nHDuB7f+sYF/2w== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-mask@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-mask/-/plugin-mask-0.16.1.tgz#e7f2460e05c3cda7af5e76f33ccb0579f66f90df" - integrity sha512-snfiqHlVuj4bSFS0v96vo2PpqCDMe4JB+O++sMo5jF5mvGcGL6AIeLo8cYqPNpdO6BZpBJ8MY5El0Veckhr39Q== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-normalize@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-normalize/-/plugin-normalize-0.16.1.tgz#032dfd88eefbc4dedc8b1b2d243832e4f3af30c8" - integrity sha512-dOQfIOvGLKDKXPU8xXWzaUeB0nvkosHw6Xg1WhS1Z5Q0PazByhaxOQkSKgUryNN/H+X7UdbDvlyh/yHf3ITRaw== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-print@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-print/-/plugin-print-0.16.1.tgz#66b803563f9d109825970714466e6ab9ae639ff6" - integrity sha512-ceWgYN40jbN4cWRxixym+csyVymvrryuKBQ+zoIvN5iE6OyS+2d7Mn4zlNgumSczb9GGyZZESIgVcBDA1ezq0Q== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - load-bmfont "^1.4.0" - -"@jimp/plugin-resize@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-resize/-/plugin-resize-0.16.1.tgz#65e39d848ed13ba2d6c6faf81d5d590396571d10" - integrity sha512-u4JBLdRI7dargC04p2Ha24kofQBk3vhaf0q8FwSYgnCRwxfvh2RxvhJZk9H7Q91JZp6wgjz/SjvEAYjGCEgAwQ== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-rotate@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-rotate/-/plugin-rotate-0.16.1.tgz#53fb5d51a4b3d05af9c91c2a8fffe5d7a1a47c8c" - integrity sha512-ZUU415gDQ0VjYutmVgAYYxC9Og9ixu2jAGMCU54mSMfuIlmohYfwARQmI7h4QB84M76c9hVLdONWjuo+rip/zg== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-scale@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-scale/-/plugin-scale-0.16.1.tgz#89f6ba59feed3429847ed226aebda33a240cc647" - integrity sha512-jM2QlgThIDIc4rcyughD5O7sOYezxdafg/2Xtd1csfK3z6fba3asxDwthqPZAgitrLgiKBDp6XfzC07Y/CefUw== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-shadow@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-shadow/-/plugin-shadow-0.16.1.tgz#a7af892a740febf41211e10a5467c3c5c521a04c" - integrity sha512-MeD2Is17oKzXLnsphAa1sDstTu6nxscugxAEk3ji0GV1FohCvpHBcec0nAq6/czg4WzqfDts+fcPfC79qWmqrA== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugin-threshold@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugin-threshold/-/plugin-threshold-0.16.1.tgz#34f3078f9965145b7ae26c53a32ad74b1195bbf5" - integrity sha512-iGW8U/wiCSR0+6syrPioVGoSzQFt4Z91SsCRbgNKTAk7D+XQv6OI78jvvYg4o0c2FOlwGhqz147HZV5utoSLxA== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - -"@jimp/plugins@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/plugins/-/plugins-0.16.1.tgz#9f08544c97226d6460a16ced79f57e85bec3257b" - integrity sha512-c+lCqa25b+4q6mJZSetlxhMoYuiltyS+ValLzdwK/47+aYsq+kcJNl+TuxIEKf59yr9+5rkbpsPkZHLF/V7FFA== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/plugin-blit" "^0.16.1" - "@jimp/plugin-blur" "^0.16.1" - "@jimp/plugin-circle" "^0.16.1" - "@jimp/plugin-color" "^0.16.1" - "@jimp/plugin-contain" "^0.16.1" - "@jimp/plugin-cover" "^0.16.1" - "@jimp/plugin-crop" "^0.16.1" - "@jimp/plugin-displace" "^0.16.1" - "@jimp/plugin-dither" "^0.16.1" - "@jimp/plugin-fisheye" "^0.16.1" - "@jimp/plugin-flip" "^0.16.1" - "@jimp/plugin-gaussian" "^0.16.1" - "@jimp/plugin-invert" "^0.16.1" - "@jimp/plugin-mask" "^0.16.1" - "@jimp/plugin-normalize" "^0.16.1" - "@jimp/plugin-print" "^0.16.1" - "@jimp/plugin-resize" "^0.16.1" - "@jimp/plugin-rotate" "^0.16.1" - "@jimp/plugin-scale" "^0.16.1" - "@jimp/plugin-shadow" "^0.16.1" - "@jimp/plugin-threshold" "^0.16.1" - timm "^1.6.1" - -"@jimp/png@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/png/-/png-0.16.1.tgz#f24cfc31529900b13a2dd9d4fdb4460c1e4d814e" - integrity sha512-iyWoCxEBTW0OUWWn6SveD4LePW89kO7ZOy5sCfYeDM/oTPLpR8iMIGvZpZUz1b8kvzFr27vPst4E5rJhGjwsdw== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/utils" "^0.16.1" - pngjs "^3.3.3" - -"@jimp/tiff@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/tiff/-/tiff-0.16.1.tgz#0e8756695687d7574b6bc73efab0acd4260b7a12" - integrity sha512-3K3+xpJS79RmSkAvFMgqY5dhSB+/sxhwTFA9f4AVHUK0oKW+u6r52Z1L0tMXHnpbAdR9EJ+xaAl2D4x19XShkQ== - dependencies: - "@babel/runtime" "^7.7.2" - utif "^2.0.1" - -"@jimp/types@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/types/-/types-0.16.1.tgz#0dbab37b3202315c91010f16c31766d35a2322cc" - integrity sha512-g1w/+NfWqiVW4CaXSJyD28JQqZtm2eyKMWPhBBDCJN9nLCN12/Az0WFF3JUAktzdsEC2KRN2AqB1a2oMZBNgSQ== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/bmp" "^0.16.1" - "@jimp/gif" "^0.16.1" - "@jimp/jpeg" "^0.16.1" - "@jimp/png" "^0.16.1" - "@jimp/tiff" "^0.16.1" - timm "^1.6.1" - -"@jimp/utils@^0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@jimp/utils/-/utils-0.16.1.tgz#2f51e6f14ff8307c4aa83d5e1a277da14a9fe3f7" - integrity sha512-8fULQjB0x4LzUSiSYG6ZtQl355sZjxbv8r9PPAuYHzS9sGiSHJQavNqK/nKnpDsVkU88/vRGcE7t3nMU0dEnVw== - dependencies: - "@babel/runtime" "^7.7.2" - regenerator-runtime "^0.13.3" - -"@mrmlnc/readdir-enhanced@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" - integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== - dependencies: - call-me-maybe "^1.0.1" - glob-to-regexp "^0.3.0" - -"@nodelib/fs.scandir@2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" - integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== - dependencies: - "@nodelib/fs.stat" "2.0.4" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" - integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== - -"@nodelib/fs.stat@^1.1.2": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" - integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" - integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== - dependencies: - "@nodelib/fs.scandir" "2.1.4" - fastq "^1.6.0" - -"@petamoriken/float16@^3.4.7": - version "3.4.7" - resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.4.7.tgz#63ce6cb698881bca0fc272807196d497615c95ea" - integrity sha512-Mir0MAKxg5v6BUIg9SI5VAyrIa/3uptf7aPyvPhHNh0RMYMevrWbaLrsVSZ1f92C39hWd8v7GrjvvOT+WG5VUQ== - -"@sentry/browser@6.17.2": - version "6.17.2" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.17.2.tgz#8e794b846f43a341068c83420918d896683d903e" - integrity sha512-4Ow5z9GxK5dG9+stBNKb7s6NoxE4wgEcHRmO66QTK4gH2NNmzV4R/aaZ7iDoS/lD86sH0M86jm76dpg9uiJPmw== - dependencies: - "@sentry/core" "6.17.2" - "@sentry/types" "6.17.2" - "@sentry/utils" "6.17.2" - tslib "^1.9.3" - -"@sentry/cli@^1.67.1": - version "1.67.2" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.67.2.tgz#dbb5631cb3637e10298f67915013592cb22f04ff" - integrity sha512-lPn0Sffbjg2UmCkHl2iw8pKlqpPhy85mW0za5kz3LEqC9JGUXHo9eSyyCkiRktlemMXKk+DeS/nyFy/LTRUG2Q== - dependencies: - https-proxy-agent "^5.0.0" - mkdirp "^0.5.5" - node-fetch "^2.6.0" - npmlog "^4.1.2" - progress "^2.0.3" - proxy-from-env "^1.1.0" - -"@sentry/core@6.17.2": - version "6.17.2" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.17.2.tgz#f218920f269ccdbaee20a092bbc90a71a007cc88" - integrity sha512-Uew0CNMr+QvowrF4EJYjOUgHep/sZJ3l5zevPEELugIgqWBodd+ZDCV3fQFR7cr6KOqx1rMgVrgcKIkLl0l+RA== - dependencies: - "@sentry/hub" "6.17.2" - "@sentry/minimal" "6.17.2" - "@sentry/types" "6.17.2" - "@sentry/utils" "6.17.2" - tslib "^1.9.3" - -"@sentry/electron@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@sentry/electron/-/electron-3.0.2.tgz#3edac1130e30b7090a1b5dbf89b446d0f3ca8914" - integrity sha512-Mg2Tl/AK+ukrAMIrEhij+pMs24HQtRz7f446kTAOYUANIkpkdoEsetXenk2NIP7mE+okbjhGD6kdr0Jm/h2b5g== - dependencies: - "@sentry/browser" "6.17.2" - "@sentry/core" "6.17.2" - "@sentry/node" "6.17.2" - "@sentry/types" "6.17.2" - "@sentry/utils" "6.17.2" - deepmerge "^4.2.2" - tslib "^2.3.1" - -"@sentry/hub@6.17.2": - version "6.17.2" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.17.2.tgz#d92accada845fa21fff1b2b491d3c6964851693b" - integrity sha512-CMi6jU920bTwRTmGHjP4u8toOx4gm1dsx+rsxvp+FKzqRwpwoyi9mOw8oEYERVzaqaYceGdFylyRUrjdf0f77g== - dependencies: - "@sentry/types" "6.17.2" - "@sentry/utils" "6.17.2" - tslib "^1.9.3" - -"@sentry/minimal@6.17.2": - version "6.17.2" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.17.2.tgz#3b482a0d76aa33b6c9441dd21acbcc3a113e5120" - integrity sha512-Cdh+iM6QhLKfxwUWWP4mk2K7+EsQj4tuF2dGQke4Zcbp7zQ7wbcMruUcZHiZfvg5kiSYxwNVkH7cXMzcO7AJsg== - dependencies: - "@sentry/hub" "6.17.2" - "@sentry/types" "6.17.2" - tslib "^1.9.3" - -"@sentry/node@6.17.2": - version "6.17.2" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.17.2.tgz#32a5fa00b64a331073daf1e44f500c8c57184eb1" - integrity sha512-358z45WaejnsE8RZVpuLJJiFVCSEi0TRY7P60CljZuz8rnvniD3G0tuXChvu4djVty8NScWZHT/QoxvuJdTHgQ== - dependencies: - "@sentry/core" "6.17.2" - "@sentry/hub" "6.17.2" - "@sentry/tracing" "6.17.2" - "@sentry/types" "6.17.2" - "@sentry/utils" "6.17.2" - cookie "^0.4.1" - https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" - -"@sentry/tracing@6.17.2": - version "6.17.2" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.17.2.tgz#437337071fdeffa319746905b3706518b099ec6b" - integrity sha512-oWY2Ga+5D5f90utvfF2Y0eQvme+eS768ZWjR+klRYgZWoY8r1v8uWwWsvroYU1g+h6X0G/xh3giFjsdOWtRENw== - dependencies: - "@sentry/hub" "6.17.2" - "@sentry/minimal" "6.17.2" - "@sentry/types" "6.17.2" - "@sentry/utils" "6.17.2" - tslib "^1.9.3" - -"@sentry/types@6.17.2": - version "6.17.2" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.17.2.tgz#4dde3423db5953e798b19ed29618c28fc7bf2e30" - integrity sha512-UrFLRDz5mn253O8k/XftLxoldF+NyZdkqKLGIQmST5HEVr7ub9nQJ4Y5ZFA3zJYWpraaW8faIbuw+pgetC8hmQ== - -"@sentry/utils@6.17.2": - version "6.17.2" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.17.2.tgz#e8044e753b47f86068053c8d79e4ae61a39b6732" - integrity sha512-ePWtO44KJQwUULOiU86fa1WU3Ird2TH0i39gqB2d3zNS3QyVp9qPlzSdPKSPJ9LdgadzBHw7ikEuE+GY8JTrhA== - dependencies: - "@sentry/types" "6.17.2" - tslib "^1.9.3" - -"@sentry/webpack-plugin@^1.16.0": - version "1.16.0" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.16.0.tgz#f9b15b5bb485995e776729b71bcf2d9fad7dca7a" - integrity sha512-Ax0QZ3a+LFYU876Si2HElPYSj+mX3vinvzH+o9F1g/5T2Z3HqITnX6gg+zVfLFsE819PN9KeLpmoHtO352dlmQ== - dependencies: - "@sentry/cli" "^1.67.1" - -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - -"@soda/friendly-errors-webpack-plugin@^1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.1.tgz#706f64bcb4a8b9642b48ae3ace444c70334d615d" - integrity sha512-cWKrGaFX+rfbMrAxVv56DzhPNqOJPZuNIS2HGMELtgGzb+vsMzyig9mml5gZ/hr2BGtSLV+dP2LUEuAL8aG2mQ== - dependencies: - chalk "^1.1.3" - error-stack-parser "^2.0.0" - string-width "^2.0.0" - -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - -"@types/adm-zip@^0.4.34": - version "0.4.34" - resolved "https://registry.yarnpkg.com/@types/adm-zip/-/adm-zip-0.4.34.tgz#62ac859eb2af6024362a1b3e43527ab79e0c624e" - integrity sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ== - dependencies: - "@types/node" "*" - -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - -"@types/debug@^4.1.4": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" - integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== - -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== - -"@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - -"@types/jest@^23.1.4": - version "23.3.14" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.14.tgz#37daaf78069e7948520474c87b80092ea912520a" - integrity sha512-Q5hTcfdudEL2yOmluA1zaSyPbzWPmJ3XfSWeP3RyoYvS9hnje1ZyagrZOuQ6+1nQC1Gw+7gap3pLNL3xL6UBug== - -"@types/jquery@^3.3.30": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.0.tgz#ccb7dfd317d02d4227dd3803c75297d0c10dad68" - integrity sha512-C7qQUjpMWDUNYQRTXsP5nbYYwCwwgy84yPgoTT7fPN69NH92wLeCtFaMsWeolJD1AF/6uQw3pYt62rzv83sMmw== - dependencies: - "@types/sizzle" "*" - -"@types/json-schema@^7.0.3", "@types/json-schema@^7.0.4": - version "7.0.5" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" - integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== - -"@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - -"@types/node@*": - version "14.0.24" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.24.tgz#b0f86f58564fa02a28b68f8b55d4cdec42e3b9d6" - integrity sha512-btt/oNOiDWcSuI721MdL8VQGnjsKjlTMdrKyTcLCKeQp/n4AAMFJ961wMbp+09y8WuGPClDEv07RIItdXKIXAA== - -"@types/node@^14.6.2": - version "14.17.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.4.tgz#218712242446fc868d0e007af29a4408c7765bc0" - integrity sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A== - -"@types/node@^16.9.2": - version "16.9.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.2.tgz#81f5a039d6ed1941f8cc57506c74e7c2b8fc64b9" - integrity sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w== - -"@types/normalize-package-data@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" - integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== - -"@types/pako@^1.0.1": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.2.tgz#17c9b136877f33d9ecc8e73cd26944f1f6dd39a1" - integrity sha512-8UJl2MjkqqS6ncpLZqRZ5LmGiFBkbYxocD4e4jmBqGvfRG1RS23gKsBQbdtV9O9GvRyjFTiRHRByjSlKCLlmZw== - -"@types/pngjs@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-6.0.1.tgz#c711ec3fbbf077fed274ecccaf85dd4673130072" - integrity sha512-J39njbdW1U/6YyVXvC9+1iflZghP8jgRf2ndYghdJb5xL49LYDB+1EuAxfbuJ2IBbWIL3AjHPQhgaTxT3YaYeg== - dependencies: - "@types/node" "*" - -"@types/q@^1.5.1": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" - integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== - -"@types/sharp@^0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.2.tgz#b4e932e982e258d1013236c8b4bcc14f9883c9a3" - integrity sha512-tIbMvtPa8kMyFMKNhpsPT1HO3CgXLuiCAA8bxHAGAZLyALpYvYc4hUu3pu0+3oExQA5LwvHrWp+OilgXCYVQgg== - dependencies: - "@types/node" "*" - -"@types/sizzle@*": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" - integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== - -"@types/strip-bom@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" - integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= - -"@types/strip-json-comments@0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" - integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== - -"@types/three@^0.103.2": - version "0.103.2" - resolved "https://registry.yarnpkg.com/@types/three/-/three-0.103.2.tgz#f7d49130001c551941a0ded757def810579aafc4" - integrity sha512-zhtf0Qs5wLJpIn1+VWCpzSgpKayj/GSWZ6woiuz09FW59KEDeLpnBkYz6lbblVpRmGdlnG8nd0unaASshOvcXw== - dependencies: - three "*" - -"@types/webpack-env@^1.13.9": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" - integrity sha512-67ZgZpAlhIICIdfQrB5fnDvaKFcDxpKibxznfYRVAT4mQE41Dido/3Ty+E3xGBmTogc5+0Qb8tWhna+5B8z1iQ== - -"@typescript-eslint/eslint-plugin@^1.1.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz#22fed9b16ddfeb402fd7bcde56307820f6ebc49f" - integrity sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g== - dependencies: - "@typescript-eslint/experimental-utils" "1.13.0" - eslint-utils "^1.3.1" - functional-red-black-tree "^1.0.1" - regexpp "^2.0.1" - tsutils "^3.7.0" - -"@typescript-eslint/eslint-plugin@^2.33.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" - integrity sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ== - dependencies: - "@typescript-eslint/experimental-utils" "2.34.0" - functional-red-black-tree "^1.0.1" - regexpp "^3.0.0" - tsutils "^3.17.1" - -"@typescript-eslint/experimental-utils@1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz#b08c60d780c0067de2fb44b04b432f540138301e" - integrity sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "1.13.0" - eslint-scope "^4.0.0" - -"@typescript-eslint/experimental-utils@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" - integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.34.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - -"@typescript-eslint/parser@^1.1.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.13.0.tgz#61ac7811ea52791c47dc9fd4dd4a184fae9ac355" - integrity sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ== - dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "1.13.0" - "@typescript-eslint/typescript-estree" "1.13.0" - eslint-visitor-keys "^1.0.0" - -"@typescript-eslint/parser@^2.33.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8" - integrity sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA== - dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.34.0" - "@typescript-eslint/typescript-estree" "2.34.0" - eslint-visitor-keys "^1.1.0" - -"@typescript-eslint/typescript-estree@1.13.0": - version "1.13.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz#8140f17d0f60c03619798f1d628b8434913dc32e" - integrity sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw== - dependencies: - lodash.unescape "4.0.1" - semver "5.5.0" - -"@typescript-eslint/typescript-estree@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" - integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== - dependencies: - debug "^4.1.1" - eslint-visitor-keys "^1.1.0" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" - -"@vue/babel-helper-vue-jsx-merge-props@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040" - integrity sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw== - -"@vue/babel-plugin-transform-vue-jsx@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz#c0a3e6efc022e75e4247b448a8fc6b86f03e91c0" - integrity sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0" - html-tags "^2.0.0" - lodash.kebabcase "^4.1.1" - svg-tags "^1.0.0" - -"@vue/babel-preset-app@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-3.12.1.tgz#24c477052f078f30fdb7735103b14dd1fa2cbfe1" - integrity sha512-Zjy5jQaikV1Pz+ri0YgXFS7q4/5wCxB5tRkDOEIt5+4105u0Feb/pvH20nVL6nx9GyXrECFfcm7Yxr/z++OaPQ== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/plugin-proposal-class-properties" "^7.0.0" - "@babel/plugin-proposal-decorators" "^7.1.0" - "@babel/plugin-syntax-dynamic-import" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.0.0" - "@babel/plugin-transform-runtime" "^7.4.0" - "@babel/preset-env" "^7.0.0 < 7.4.0" - "@babel/runtime" "^7.0.0" - "@babel/runtime-corejs2" "^7.2.0" - "@vue/babel-preset-jsx" "^1.0.0" - babel-plugin-dynamic-import-node "^2.2.0" - babel-plugin-module-resolver "3.2.0" - core-js "^2.6.5" - -"@vue/babel-preset-jsx@^1.0.0": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.1.2.tgz#2e169eb4c204ea37ca66c2ea85a880bfc99d4f20" - integrity sha512-zDpVnFpeC9YXmvGIDSsKNdL7qCG2rA3gjywLYHPCKDT10erjxF4U+6ay9X6TW5fl4GsDlJp9bVfAVQAAVzxxvQ== - dependencies: - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0" - "@vue/babel-plugin-transform-vue-jsx" "^1.1.2" - "@vue/babel-sugar-functional-vue" "^1.1.2" - "@vue/babel-sugar-inject-h" "^1.1.2" - "@vue/babel-sugar-v-model" "^1.1.2" - "@vue/babel-sugar-v-on" "^1.1.2" - -"@vue/babel-sugar-functional-vue@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.1.2.tgz#f7e24fba09e6f1ee70104560a8808057555f1a9a" - integrity sha512-YhmdJQSVEFF5ETJXzrMpj0nkCXEa39TvVxJTuVjzvP2rgKhdMmQzlJuMv/HpadhZaRVMCCF3AEjjJcK5q/cYzQ== - dependencies: - "@babel/plugin-syntax-jsx" "^7.2.0" - -"@vue/babel-sugar-inject-h@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.1.2.tgz#8a5276b6d8e2ed16ffc8078aad94236274e6edf0" - integrity sha512-VRSENdTvD5htpnVp7i7DNuChR5rVMcORdXjvv5HVvpdKHzDZAYiLSD+GhnhxLm3/dMuk8pSzV+k28ECkiN5m8w== - dependencies: - "@babel/plugin-syntax-jsx" "^7.2.0" - -"@vue/babel-sugar-v-model@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.1.2.tgz#1ff6fd1b800223fc9cb1e84dceb5e52d737a8192" - integrity sha512-vLXPvNq8vDtt0u9LqFdpGM9W9IWDmCmCyJXuozlq4F4UYVleXJ2Fa+3JsnTZNJcG+pLjjfnEGHci2339Kj5sGg== - dependencies: - "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0" - "@vue/babel-plugin-transform-vue-jsx" "^1.1.2" - camelcase "^5.0.0" - html-tags "^2.0.0" - svg-tags "^1.0.0" - -"@vue/babel-sugar-v-on@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.1.2.tgz#b2ef99b8f2fab09fbead25aad70ef42e1cf5b13b" - integrity sha512-T8ZCwC8Jp2uRtcZ88YwZtZXe7eQrJcfRq0uTFy6ShbwYJyz5qWskRFoVsdTi9o0WEhmQXxhQUewodOSCUPVmsQ== - dependencies: - "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-plugin-transform-vue-jsx" "^1.1.2" - camelcase "^5.0.0" - -"@vue/cli-overlay@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@vue/cli-overlay/-/cli-overlay-3.12.1.tgz#bdfde8f7123561ab06e4e4c60b854cc5092f5ab1" - integrity sha512-Bym92EN+lj+cNRN2ozbYyH+V8DMXWGbCDUk+hiJ4EYDBZfBkZKvalk1/mOBFwyxiopnnbOEBAAhL/UuMQ1xARg== - -"@vue/cli-plugin-babel@^3.8.0": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-babel/-/cli-plugin-babel-3.12.1.tgz#9a79159de8cd086b013fa6d78a39830b2e2ec706" - integrity sha512-Zetvz8PikLCGomeKOKu8pC9YQ7cfxs7pGpvEOzaxGdhMnebhjAYR6i6dOB57A6N5lhxQksXCtYTv26QgfiIpdg== - dependencies: - "@babel/core" "^7.0.0" - "@vue/babel-preset-app" "^3.12.1" - "@vue/cli-shared-utils" "^3.12.1" - babel-loader "^8.0.5" - webpack "^4.0.0" - -"@vue/cli-plugin-eslint@^3.8.0": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-eslint/-/cli-plugin-eslint-3.12.1.tgz#302c463867f38e790bb996eafdf7159c782dc8cf" - integrity sha512-tVTZlEZsy3sQbO4LLWFK11yzlWwqVAqaM+IY+BeWHITBzEJKh2KmouG+x6x/reXiU3qROsMJ4Ej3Hs8buSMWyQ== - dependencies: - "@vue/cli-shared-utils" "^3.12.1" - babel-eslint "^10.0.1" - eslint-loader "^2.1.2" - globby "^9.2.0" - webpack "^4.0.0" - yorkie "^2.0.0" - optionalDependencies: - eslint "^4.19.1" - eslint-plugin-vue "^4.7.1" - -"@vue/cli-plugin-typescript@^3.8.0": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-typescript/-/cli-plugin-typescript-3.12.1.tgz#71b306983de37a03c43860ac035bd0a15eb29d27" - integrity sha512-sh+WKbpsDw6wOrpM4FSD1xKXpyp8mVcl+yyEk+WvJuuSdfwueRubAM7uYbrOGtNSOegpZqBwbNxEO4FIUBeLKQ== - dependencies: - "@types/webpack-env" "^1.13.9" - "@vue/cli-shared-utils" "^3.12.1" - fork-ts-checker-webpack-plugin "^0.5.2" - globby "^9.2.0" - ts-loader "^5.3.3" - tslint "^5.15.0" - webpack "^4.0.0" - yorkie "^2.0.0" - -"@vue/cli-plugin-unit-jest@^3.8.0": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@vue/cli-plugin-unit-jest/-/cli-plugin-unit-jest-3.12.1.tgz#b7edd30701191deaa9d53cea752b8d72b825d640" - integrity sha512-Cc9Kq4+RaUN1yfNVb7c9hVDNXo2tFTWHgwooCL3XWMu2iL+pDawQt8ZeSqauDY95JoMeEAVy2xBimjL+7jo/jw== - dependencies: - "@vue/cli-shared-utils" "^3.12.1" - babel-jest "^23.6.0" - babel-plugin-transform-es2015-modules-commonjs "^6.26.2" - jest "^23.6.0" - jest-serializer-vue "^2.0.2" - jest-transform-stub "^2.0.0" - jest-watch-typeahead "0.2.1" - vue-jest "^3.0.4" - -"@vue/cli-service@^3.8.0": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@vue/cli-service/-/cli-service-3.12.1.tgz#13220b1c189254e7c003390df329086f9b6e77e6" - integrity sha512-PDxNrTGnSKzeV1ruFlsRIAO8JcPizwT0EJXq9GeyooU+p+sOkv7aKkCBJQVYNjZapD1NOGWx6CvAAC/wAW+gew== - dependencies: - "@intervolga/optimize-cssnano-plugin" "^1.0.5" - "@soda/friendly-errors-webpack-plugin" "^1.7.1" - "@vue/cli-overlay" "^3.12.1" - "@vue/cli-shared-utils" "^3.12.1" - "@vue/component-compiler-utils" "^3.0.0" - "@vue/preload-webpack-plugin" "^1.1.0" - "@vue/web-component-wrapper" "^1.2.0" - acorn "^6.1.1" - acorn-walk "^6.1.1" - address "^1.0.3" - autoprefixer "^9.5.1" - browserslist "^4.5.4" - cache-loader "^2.0.1" - case-sensitive-paths-webpack-plugin "^2.2.0" - chalk "^2.4.2" - cli-highlight "^2.1.0" - clipboardy "^2.0.0" - cliui "^5.0.0" - copy-webpack-plugin "^4.6.0" - css-loader "^1.0.1" - cssnano "^4.1.10" - current-script-polyfill "^1.0.0" - debug "^4.1.1" - default-gateway "^5.0.2" - dotenv "^7.0.0" - dotenv-expand "^5.1.0" - escape-string-regexp "^1.0.5" - file-loader "^3.0.1" - fs-extra "^7.0.1" - globby "^9.2.0" - hash-sum "^1.0.2" - html-webpack-plugin "^3.2.0" - launch-editor-middleware "^2.2.1" - lodash.defaultsdeep "^4.6.1" - lodash.mapvalues "^4.6.0" - lodash.transform "^4.6.0" - mini-css-extract-plugin "^0.8.0" - minimist "^1.2.0" - ora "^3.4.0" - portfinder "^1.0.20" - postcss-loader "^3.0.0" - read-pkg "^5.0.0" - semver "^6.0.0" - slash "^2.0.0" - source-map-url "^0.4.0" - ssri "^6.0.1" - string.prototype.padend "^3.0.0" - terser-webpack-plugin "^1.2.3" - thread-loader "^2.1.2" - url-loader "^1.1.2" - vue-loader "^15.7.0" - webpack "^4.0.0" - webpack-bundle-analyzer "^3.3.0" - webpack-chain "^4.11.0" - webpack-dev-server "^3.4.1" - webpack-merge "^4.2.1" - -"@vue/cli-shared-utils@^3.12.1": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@vue/cli-shared-utils/-/cli-shared-utils-3.12.1.tgz#bcf076287ddadeebbb97c6a748dfe9ff50ec8df0" - integrity sha512-jFblzRFjutGwu5utOKdVlPlsbA1lBUNNQlAThzNqej+JtTKJjnvjlhjKX0Gq0oOny5FjKWhoyfQ74p9h1qE6JQ== - dependencies: - "@hapi/joi" "^15.0.1" - chalk "^2.4.1" - execa "^1.0.0" - launch-editor "^2.2.1" - lru-cache "^5.1.1" - node-ipc "^9.1.1" - open "^6.3.0" - ora "^3.4.0" - request "^2.87.0" - request-promise-native "^1.0.7" - semver "^6.0.0" - string.prototype.padstart "^3.0.0" - -"@vue/component-compiler-utils@^3.0.0", "@vue/component-compiler-utils@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.1.2.tgz#8213a5ff3202f9f2137fe55370f9e8b9656081c3" - integrity sha512-QLq9z8m79mCinpaEeSURhnNCN6djxpHw0lpP/bodMlt5kALfONpryMthvnrQOlTcIKoF+VoPi+lPHUYeDFPXug== - dependencies: - consolidate "^0.15.1" - hash-sum "^1.0.2" - lru-cache "^4.1.2" - merge-source-map "^1.1.0" - postcss "^7.0.14" - postcss-selector-parser "^6.0.2" - source-map "~0.6.1" - vue-template-es2015-compiler "^1.9.0" - optionalDependencies: - prettier "^1.18.2" - -"@vue/eslint-config-prettier@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@vue/eslint-config-prettier/-/eslint-config-prettier-4.0.1.tgz#a036d0d2193c5c836542b35a3a7c35c4e1c68c97" - integrity sha512-rJEDXPb61Hfgg8GllO3XXFP98bcIxdNNHSrNcxP/vBSukOolgOwQyZJ5f5z/c7ViPyh5/IDlC4qBnhx/0n+I4g== - dependencies: - eslint-config-prettier "^3.3.0" - eslint-plugin-prettier "^3.0.0" - prettier "^1.15.2" - -"@vue/eslint-config-typescript@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-4.0.0.tgz#a202983598a4a826460cbb8ee43826875b0f6673" - integrity sha512-uSMAMgw4xDgVdZQhpbtJRo8nMV4oOy3Ht8olfOo7xvYFYLMF2JZ1tDRKd9/NSusxA72O2Vma+HzmyzDHg9evcQ== - dependencies: - "@typescript-eslint/eslint-plugin" "^1.1.0" - "@typescript-eslint/parser" "^1.1.0" - -"@vue/preload-webpack-plugin@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.1.tgz#18723530d304f443021da2292d6ec9502826104a" - integrity sha512-8VCoJeeH8tCkzhkpfOkt+abALQkS11OIHhte5MBzYaKMTqK0A3ZAKEUVAffsOklhEv7t0yrQt696Opnu9oAx+w== - -"@vue/test-utils@1.0.0-beta.29": - version "1.0.0-beta.29" - resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.29.tgz#c942cf25e891cf081b6a03332b4ae1ef430726f0" - integrity sha512-yX4sxEIHh4M9yAbLA/ikpEnGKMNBCnoX98xE1RwxfhQVcn0MaXNSj1Qmac+ZydTj6VBSEVukchBogXBTwc+9iA== - dependencies: - dom-event-types "^1.0.0" - lodash "^4.17.4" - -"@vue/web-component-wrapper@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@vue/web-component-wrapper/-/web-component-wrapper-1.2.0.tgz#bb0e46f1585a7e289b4ee6067dcc5a6ae62f1dd1" - integrity sha512-Xn/+vdm9CjuC9p3Ae+lTClNutrVhsXpzxvoTXXtoys6kVRX9FkueSUAqSWAyZntmVLlR4DosBV4pH8y5Z/HbUw== - -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== - dependencies: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== - -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== - -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== - -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== - dependencies: - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== - -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== - dependencies: - "@webassemblyjs/ast" "1.9.0" - -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== - -"@webassemblyjs/helper-wasm-section@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== - -"@webassemblyjs/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/wasm-gen@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - -"@webassemblyjs/wasm-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/wast-printer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - "@xtuc/long" "4.2.2" - -"@webcomponents/webcomponentsjs@^2.0.2": - version "2.4.4" - resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.4.tgz#14b7e78da47f8f0071ff96c35335b871534179bc" - integrity sha512-UWXZYbaDLLfhm+xONXTiDciyhOSwKRrZieGQHFMSMGSxY4mbjZ5uYzOKgnuX0luYFvjJw32G3r0sCwQZPJIR4Q== - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -abab@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" - integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== - dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" - -acorn-globals@^4.1.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" - integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== - dependencies: - acorn "^6.0.1" - acorn-walk "^6.0.1" - -acorn-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" - integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s= - dependencies: - acorn "^3.0.4" - -acorn-jsx@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" - integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== - -acorn-walk@^6.0.1, acorn-walk@^6.1.1: - version "6.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" - integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== - -acorn-walk@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== - -acorn@^3.0.4: - version "3.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" - integrity sha1-ReN/s56No/JbruP/U2niu18iAXo= - -acorn@^5.5.0, acorn@^5.5.3: - version "5.7.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" - integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== - -acorn@^6.0.1, acorn@^6.0.2, acorn@^6.0.7, acorn@^6.1.1, acorn@^6.4.1: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== - -acorn@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" - integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== - -address@^1.0.3: - version "1.1.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" - integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== - -adm-zip@^0.4.16: - version "0.4.16" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" - integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" - integrity sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I= - -ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.1.tgz#b83ca89c5d42d69031f424cad49aada0236c6957" - integrity sha512-KWcq3xN8fDjSB+IMoh2VaXVhRI0BBGxoYp3rx7Pkb6z0cFjYR9Q9l4yZqqals0/zsioCmocC5H6UvsGD4MoIBA== - -ajv@^5.2.3, ajv@^5.3.0: - version "5.5.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" - integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.5.5, ajv@^6.9.1: - version "6.12.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" - integrity sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -alphanum-sort@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" - integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= - -ansi-align@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" - integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== - dependencies: - string-width "^3.0.0" - -ansi-colors@^3.0.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" - integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== - -ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-html@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" - integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== - dependencies: - "@types/color-name" "^1.1.1" - color-convert "^2.0.1" - -any-base@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/any-base/-/any-base-1.1.0.tgz#ae101a62bc08a597b4c9ab5b7089d456630549fe" - integrity sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg== - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -app-builder-bin@3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-3.4.3.tgz#58a74193eb882f029be6b7f0cd3f0c6805927a6b" - integrity sha512-qMhayIwi3juerQEVJMQ76trObEbfQT0nhUdxZz9a26/3NLT3pE6awmQ8S1cEnrGugaaM5gYqR8OElcDezfmEsg== - -app-builder-lib@21.2.0, app-builder-lib@~21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-21.2.0.tgz#fa1d1604601431e2c3476857e9b9b61d33ad26cc" - integrity sha512-aOX/nv77/Bti6NymJDg7p9T067xD8m1ipIEJR7B4Mm1GsJWpMm9PZdXtCRiMNRjHtQS5KIljT0g17781y6qn5A== - dependencies: - "7zip-bin" "~5.0.3" - "@develar/schema-utils" "~2.1.0" - async-exit-hook "^2.0.1" - bluebird-lst "^1.0.9" - builder-util "21.2.0" - builder-util-runtime "8.3.0" - chromium-pickle-js "^0.2.0" - debug "^4.1.1" - ejs "^2.6.2" - electron-publish "21.2.0" - fs-extra "^8.1.0" - hosted-git-info "^2.7.1" - is-ci "^2.0.0" - isbinaryfile "^4.0.2" - js-yaml "^3.13.1" - lazy-val "^1.0.4" - minimatch "^3.0.4" - normalize-package-data "^2.5.0" - read-config-file "5.0.0" - sanitize-filename "^1.6.2" - semver "^6.3.0" - temp-file "^3.3.4" - -append-transform@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" - integrity sha1-126/jKlNJ24keja61EpLdKthGZE= - dependencies: - default-require-extensions "^1.0.0" - -aproba@^1.0.3, aproba@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -arch@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" - integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== - -archiver-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-1.3.0.tgz#e50b4c09c70bf3d680e32ff1b7994e9f9d895174" - integrity sha1-5QtMCccL89aA4y/xt5lOn52JUXQ= - dependencies: - glob "^7.0.0" - graceful-fs "^4.1.0" - lazystream "^1.0.0" - lodash "^4.8.0" - normalize-path "^2.0.0" - readable-stream "^2.0.0" - -archiver@~2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-2.1.1.tgz#ff662b4a78201494a3ee544d3a33fe7496509ebc" - integrity sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw= - dependencies: - archiver-utils "^1.3.0" - async "^2.0.0" - buffer-crc32 "^0.2.1" - glob "^7.0.0" - lodash "^4.8.0" - readable-stream "^2.0.0" - tar-stream "^1.5.0" - zip-stream "^1.2.0" - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -args@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761" - integrity sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ== - dependencies: - camelcase "5.0.0" - chalk "2.4.2" - leven "2.1.0" - mri "1.1.4" - -arr-diff@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" - integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= - dependencies: - arr-flatten "^1.0.1" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= - -arr-flatten@^1.0.1, arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= - -array-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= - -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -array-flatten@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-union@^1.0.1, array-union@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= - -arrify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - -asn1.js@^4.0.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -async-each@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" - integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== - -async-exit-hook@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" - integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== - -async-limiter@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" - integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== - -async@^2.0.0, async@^2.1.4, async@^2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -autoprefixer@^9.5.1: - version "9.8.5" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.5.tgz#2c225de229ddafe1d1424c02791d0c3e10ccccaa" - integrity sha512-C2p5KkumJlsTHoNv9w31NrBRgXhf6eCMteJuHZi2xhkgC+5Vm40MEtCKPhc0qdgAOhox0YPy1SQHTAky05UoKg== - dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001097" - colorette "^1.2.0" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" - integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== - -babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - -babel-core@7.0.0-bridge.0: - version "7.0.0-bridge.0" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" - integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== - -babel-core@^6.0.0, babel-core@^6.26.0: - version "6.26.3" - resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" - integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== - dependencies: - babel-code-frame "^6.26.0" - babel-generator "^6.26.0" - babel-helpers "^6.24.1" - babel-messages "^6.23.0" - babel-register "^6.26.0" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - convert-source-map "^1.5.1" - debug "^2.6.9" - json5 "^0.5.1" - lodash "^4.17.4" - minimatch "^3.0.4" - path-is-absolute "^1.0.1" - private "^0.1.8" - slash "^1.0.0" - source-map "^0.5.7" - -babel-eslint@^10.0.1: - version "10.1.0" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" - integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.7.0" - "@babel/traverse" "^7.7.0" - "@babel/types" "^7.7.0" - eslint-visitor-keys "^1.0.0" - resolve "^1.12.0" - -babel-generator@^6.18.0, babel-generator@^6.26.0: - version "6.26.1" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" - integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== - dependencies: - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - detect-indent "^4.0.0" - jsesc "^1.3.0" - lodash "^4.17.4" - source-map "^0.5.7" - trim-right "^1.0.1" - -babel-helpers@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" - integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= - dependencies: - babel-runtime "^6.22.0" - babel-template "^6.24.1" - -babel-jest@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-23.6.0.tgz#a644232366557a2240a0c083da6b25786185a2f1" - integrity sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew== - dependencies: - babel-plugin-istanbul "^4.1.6" - babel-preset-jest "^23.2.0" - -babel-loader@^8.0.5: - version "8.1.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3" - integrity sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw== - dependencies: - find-cache-dir "^2.1.0" - loader-utils "^1.4.0" - mkdirp "^0.5.3" - pify "^4.0.1" - schema-utils "^2.6.5" - -babel-messages@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" - integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= - dependencies: - babel-runtime "^6.22.0" - -babel-plugin-dynamic-import-node@^2.2.0, babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-istanbul@^4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" - integrity sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ== - dependencies: - babel-plugin-syntax-object-rest-spread "^6.13.0" - find-up "^2.1.0" - istanbul-lib-instrument "^1.10.1" - test-exclude "^4.2.1" - -babel-plugin-jest-hoist@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" - integrity sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc= - -babel-plugin-module-resolver@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-3.2.0.tgz#ddfa5e301e3b9aa12d852a9979f18b37881ff5a7" - integrity sha512-tjR0GvSndzPew/Iayf4uICWZqjBwnlMWjSx6brryfQ81F9rxBVqwDJtFCV8oOs0+vJeefK9TmdZtkIFdFe1UnA== - dependencies: - find-babel-config "^1.1.0" - glob "^7.1.2" - pkg-up "^2.0.0" - reselect "^3.0.1" - resolve "^1.4.0" - -babel-plugin-syntax-object-rest-spread@^6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" - integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= - -babel-plugin-transform-es2015-modules-commonjs@^6.26.0, babel-plugin-transform-es2015-modules-commonjs@^6.26.2: - version "6.26.2" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" - integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== - dependencies: - babel-plugin-transform-strict-mode "^6.24.1" - babel-runtime "^6.26.0" - babel-template "^6.26.0" - babel-types "^6.26.0" - -babel-plugin-transform-strict-mode@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" - integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= - dependencies: - babel-runtime "^6.22.0" - babel-types "^6.24.1" - -babel-preset-jest@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46" - integrity sha1-jsegOhOPABoaj7HoETZSvxpV2kY= - dependencies: - babel-plugin-jest-hoist "^23.2.0" - babel-plugin-syntax-object-rest-spread "^6.13.0" - -babel-register@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" - integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= - dependencies: - babel-core "^6.26.0" - babel-runtime "^6.26.0" - core-js "^2.5.0" - home-or-tmp "^2.0.0" - lodash "^4.17.4" - mkdirp "^0.5.1" - source-map-support "^0.4.15" - -babel-runtime@^6.22.0, babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - -babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" - integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= - dependencies: - babel-runtime "^6.26.0" - babel-traverse "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - lodash "^4.17.4" - -babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" - integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= - dependencies: - babel-code-frame "^6.26.0" - babel-messages "^6.23.0" - babel-runtime "^6.26.0" - babel-types "^6.26.0" - babylon "^6.18.0" - debug "^2.6.8" - globals "^9.18.0" - invariant "^2.2.2" - lodash "^4.17.4" - -babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.24.1, babel-types@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" - integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= - dependencies: - babel-runtime "^6.26.0" - esutils "^2.0.2" - lodash "^4.17.4" - to-fast-properties "^1.0.3" - -babylon@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" - integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -base64-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -batch-processor@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" - integrity sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg= - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -bfj@^6.1.1: - version "6.1.2" - resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.2.tgz#325c861a822bcb358a41c78a33b8e6e2086dde7f" - integrity sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw== - dependencies: - bluebird "^3.5.5" - check-types "^8.0.3" - hoopy "^0.1.4" - tryer "^1.0.1" - -big.js@^3.1.3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" - integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^1.0.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" - integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bluebird-lst@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz#a64a0e4365658b9ab5fe875eb9dfb694189bb41c" - integrity sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw== - dependencies: - bluebird "^3.5.5" - -bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bmp-js@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233" - integrity sha1-4Fpj95amwf8l9Hcex62twUjAcjM= - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0" - integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA== - -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - -bonjour@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" - integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= - dependencies: - array-flatten "^2.1.0" - deep-equal "^1.0.1" - dns-equal "^1.0.0" - dns-txt "^2.0.2" - multicast-dns "^6.0.1" - multicast-dns-service-types "^1.1.0" - -boolbase@^1.0.0, boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= - -boolean@^3.0.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.1.2.tgz#e30f210a26b02458482a8cc353ab06f262a780c2" - integrity sha512-YN6UmV0FfLlBVvRvNPx3pz5W/mUoYB24J4WSXOKP/OOJpi+Oq6WYqPaNTHzjI0QzwWtnvEd5CGYyQPgp1jFxnw== - -boxen@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-3.2.0.tgz#fbdff0de93636ab4450886b6ff45b92d098f45eb" - integrity sha512-cU4J/+NodM3IHdSL2yN8bqYqnmlBTidDR4RC7nJs61ZmtGz8VZzM3HLQX0zY5mrSmPtR3xWwsq2jOUQqFZN8+A== - dependencies: - ansi-align "^3.0.0" - camelcase "^5.3.1" - chalk "^2.4.2" - cli-boxes "^2.2.0" - string-width "^3.0.0" - term-size "^1.2.0" - type-fest "^0.3.0" - widest-line "^2.0.0" - -boxicons@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/boxicons/-/boxicons-2.0.5.tgz#c967dd5fd82276a3463929286be2e830394eaf56" - integrity sha512-qMRNvjaxjR1aQWssbwjWqV2cBKmqq4slWIeo1ovXPsuksQrGb7CWVi9PeC7tngUjnrQE4Vk79DE4+W702vdKWg== - dependencies: - "@webcomponents/webcomponentsjs" "^2.0.2" - prop-types "^15.6.0" - react "^16.0.0" - react-dom "^16.0.0" - react-interactive "^0.8.1" - react-router-dom "^4.2.2" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^1.8.2: - version "1.8.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" - integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc= - dependencies: - expand-range "^1.8.1" - preserve "^0.2.0" - repeat-element "^1.1.2" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== - -browser-resolve@^1.11.3: - version "1.11.3" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" - integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== - dependencies: - resolve "1.1.7" - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.0.tgz#545d0b1b07e6b2c99211082bf1b12cce7a0b0e11" - integrity sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.2" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.3.4, browserslist@^4.5.4: - version "4.13.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.13.0.tgz#42556cba011e1b0a2775b611cba6a8eca18e940d" - integrity sha512-MINatJ5ZNrLnQ6blGvePd/QOz9Xtu+Ne+x29iQSCHfkU5BugKVJwZKn/iiL8UbpIpa3JhviKjz+XxMo0m2caFQ== - dependencies: - caniuse-lite "^1.0.30001093" - electron-to-chromium "^1.3.488" - escalade "^3.0.1" - node-releases "^1.1.58" - -bs-logger@0.x: - version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" - integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== - dependencies: - fast-json-stable-stringify "2.x" - -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - -buffer-crc32@^0.2.1, buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - -buffer-equal@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" - integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= - -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - -buffer-from@1.x, buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -buffer-indexof@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" - integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -buffer@^5.1.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" - integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - -buffer@^5.2.0, buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -builder-util-runtime@8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.3.0.tgz#f5fac9139af6facf42a21fbe4d3aebed88fda33e" - integrity sha512-CSOdsYqf4RXIHh1HANPbrZHlZ9JQJXSuDDloblZPcWQVN62inyYoTQuSmY3KrgefME2Sv3Kn2MxHvbGQHRf8Iw== - dependencies: - debug "^4.1.1" - sax "^1.2.4" - -builder-util@21.2.0, builder-util@~21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-21.2.0.tgz#aba721190e4e841009d9fb4b88f1130ed616522f" - integrity sha512-Nd6CUb6YgDY8EXAXEIegx+1kzKqyFQ5ZM5BoYkeunAlwz/zDJoH1UCyULjoS5wQe5czNClFQy07zz2bzYD0Z4A== - dependencies: - "7zip-bin" "~5.0.3" - "@types/debug" "^4.1.4" - app-builder-bin "3.4.3" - bluebird-lst "^1.0.9" - builder-util-runtime "8.3.0" - chalk "^2.4.2" - debug "^4.1.1" - fs-extra "^8.1.0" - is-ci "^2.0.0" - js-yaml "^3.13.1" - source-map-support "^0.5.13" - stat-mode "^0.3.0" - temp-file "^3.3.4" - -builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - -cacache@^10.0.4: - version "10.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" - integrity sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA== - dependencies: - bluebird "^3.5.1" - chownr "^1.0.1" - glob "^7.1.2" - graceful-fs "^4.1.11" - lru-cache "^4.1.1" - mississippi "^2.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.2" - ssri "^5.2.4" - unique-filename "^1.1.0" - y18n "^4.0.0" - -cacache@^12.0.2: - version "12.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" - integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== - dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.4" - graceful-fs "^4.1.15" - infer-owner "^1.0.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -cache-loader@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-2.0.1.tgz#5758f41a62d7c23941e3c3c7016e6faeb03acb07" - integrity sha512-V99T3FOynmGx26Zom+JrVBytLBsmUCzVG2/4NnUKgvXN4bEV42R1ERl1IyiH/cvFIDA1Ytq2lPZ9tXDSahcQpQ== - dependencies: - loader-utils "^1.1.0" - mkdirp "^0.5.1" - neo-async "^2.6.0" - normalize-path "^3.0.0" - schema-utils "^1.0.0" - -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - -call-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= - -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" - integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8= - dependencies: - callsites "^0.2.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" - integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@3.0.x: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" - integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= - dependencies: - no-case "^2.2.0" - upper-case "^1.1.1" - -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - -camelcase@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" - integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== - -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001097: - version "1.0.30001104" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001104.tgz#4e3d5b3b1dd3c3529f10cb7f519c62ba3e579f5d" - integrity sha512-pkpCg7dmI/a7WcqM2yfdOiT4Xx5tzyoHAXWsX5/HxZ3TemwDZs0QXdqbE0UPLPVy/7BeK7693YfzfRYfu1YVpg== - -capture-exit@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" - integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28= - dependencies: - rsvp "^3.3.3" - -case-sensitive-paths-webpack-plugin@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" - integrity sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chardet@^0.4.0: - version "0.4.2" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" - integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -check-types@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" - integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ== - -chokidar@^2.0.4, chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chokidar@^3.0.2, chokidar@^3.4.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.1.tgz#e905bdecf10eaa0a0b1db0c664481cc4cbc22ba1" - integrity sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.4.0" - optionalDependencies: - fsevents "~2.1.2" - -chownr@^1.0.1, chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chrome-trace-event@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" - -chromium-pickle-js@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" - integrity sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= - -ci-info@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -circular-json@^0.3.1: - version "0.3.3" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" - integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== - -clamp@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/clamp/-/clamp-1.0.1.tgz#66a0e64011816e37196828fdc8c8c147312c8634" - integrity sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ= - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -clean-css@4.2.x: - version "4.2.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" - integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== - dependencies: - source-map "~0.6.0" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-boxes@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" - integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== - -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= - dependencies: - restore-cursor "^2.0.0" - -cli-highlight@^2.1.0: - version "2.1.4" - resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.4.tgz#098cb642cf17f42adc1c1145e07f960ec4d7522b" - integrity sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ== - dependencies: - chalk "^3.0.0" - highlight.js "^9.6.0" - mz "^2.4.0" - parse5 "^5.1.1" - parse5-htmlparser2-tree-adapter "^5.1.1" - yargs "^15.0.0" - -cli-spinners@^2.0.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" - integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== - -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== - -clipboardy@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290" - integrity sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ== - dependencies: - arch "^2.1.1" - execa "^1.0.0" - is-wsl "^2.1.1" - -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -clone@2.x: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= - -coa@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" - integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== - dependencies: - "@types/q" "^1.5.1" - chalk "^2.4.1" - q "^1.1.2" - -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0, color-convert@^1.9.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@^1.0.0, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color-string@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312" - integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" - integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== - dependencies: - color-convert "^1.9.1" - color-string "^1.5.2" - -color@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67" - integrity sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA== - dependencies: - color-convert "^2.0.1" - color-string "^1.6.0" - -colorette@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@2.17.x: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== - -commander@^2.12.1, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^6.2.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -commander@~2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" - integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== - -compress-commons@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.2.tgz#524a9f10903f3a813389b0225d27c48bb751890f" - integrity sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8= - dependencies: - buffer-crc32 "^0.2.1" - crc32-stream "^2.0.0" - normalize-path "^2.0.0" - readable-stream "^2.0.0" - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -condense-newlines@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f" - integrity sha1-PemFVTE5R10yUCyDsC9gaE0kxV8= - dependencies: - extend-shallow "^2.0.1" - is-whitespace "^0.3.0" - kind-of "^3.0.2" - -config-chain@^1.1.11: - version "1.1.13" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -config-chain@^1.1.12: - version "1.1.12" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" - integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -configstore@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-4.0.0.tgz#5933311e95d3687efb592c528b922d9262d227e7" - integrity sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ== - dependencies: - dot-prop "^4.1.0" - graceful-fs "^4.1.2" - make-dir "^1.0.0" - unique-string "^1.0.0" - write-file-atomic "^2.0.0" - xdg-basedir "^3.0.0" - -connect-history-api-fallback@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" - integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= - -consolidate@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7" - integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw== - dependencies: - bluebird "^3.1.1" - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -convert-source-map@^1.4.0, convert-source-map@^1.5.1, convert-source-map@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== - dependencies: - safe-buffer "~5.1.1" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - -cookie@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== - -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= - -copy-webpack-plugin@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae" - integrity sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA== - dependencies: - cacache "^10.0.4" - find-cache-dir "^1.0.0" - globby "^7.1.1" - is-glob "^4.0.0" - loader-utils "^1.1.0" - minimatch "^3.0.4" - p-limit "^1.0.0" - serialize-javascript "^1.4.0" - -core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.5: - version "2.6.11" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" - integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== - -core-js@^3.6.5: - version "3.15.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.1.tgz#6c08ab88abdf56545045ccf5fd81f47f407e7f1a" - integrity sha512-h8VbZYnc9pDzueiS2610IULDkpFFPunHwIpl8yRwFahAEEdSpHlTy3h3z3rKq5h11CaUdBEeRViu9AYvbxiMeg== - -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -crc32-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-2.0.0.tgz#e3cdd3b4df3168dd74e3de3fbbcb7b297fe908f4" - integrity sha1-483TtN8xaN10494/u8t7KX/pCPQ= - dependencies: - crc "^3.4.4" - readable-stream "^2.0.0" - -crc@^3.4.4: - version "3.8.0" - resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" - integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== - dependencies: - buffer "^5.1.0" - -create-ecdh@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" - integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== - dependencies: - bn.js "^4.1.0" - elliptic "^6.0.0" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@^5.0.1, cross-spawn@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^6.0.0, cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -cross-spawn@^7.0.0: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -crypto-random-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" - integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= - -css-color-names@0.0.4, css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-declaration-sorter@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" - integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== - dependencies: - postcss "^7.0.1" - timsort "^0.3.0" - -css-element-queries@^1.0.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/css-element-queries/-/css-element-queries-1.2.3.tgz#e14940b1fcd4bf0da60ea4145d05742d7172e516" - integrity sha512-QK9uovYmKTsV2GXWQiMOByVNrLn2qz6m3P7vWpOR4IdD6I3iXoDw5qtgJEN3Xq7gIbdHVKvzHjdAtcl+4Arc4Q== - -css-loader@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe" - integrity sha512-+ZHAZm/yqvJ2kDtPne3uX0C+Vr3Zn5jFn2N4HywtS5ujwvsVkyg0VArEXpl3BgczDA8anieki1FIzhchX4yrDw== - dependencies: - babel-code-frame "^6.26.0" - css-selector-tokenizer "^0.7.0" - icss-utils "^2.1.0" - loader-utils "^1.0.2" - lodash "^4.17.11" - postcss "^6.0.23" - postcss-modules-extract-imports "^1.2.0" - postcss-modules-local-by-default "^1.2.0" - postcss-modules-scope "^1.1.0" - postcss-modules-values "^1.3.0" - postcss-value-parser "^3.3.0" - source-list-map "^2.0.0" - -css-parse@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" - integrity sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q= - dependencies: - css "^2.0.0" - -css-select-base-adapter@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" - integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== - -css-select@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" - integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= - dependencies: - boolbase "~1.0.0" - css-what "2.1" - domutils "1.5.1" - nth-check "~1.0.1" - -css-select@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" - integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== - dependencies: - boolbase "^1.0.0" - css-what "^3.2.1" - domutils "^1.7.0" - nth-check "^1.0.2" - -css-selector-tokenizer@^0.7.0: - version "0.7.2" - resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.2.tgz#11e5e27c9a48d90284f22d45061c303d7a25ad87" - integrity sha512-yj856NGuAymN6r8bn8/Jl46pR+OC3eEvAhfGYDUe7YPtTPAYrSSw4oAniZ9Y8T5B92hjhwTBLUen0/vKPxf6pw== - dependencies: - cssesc "^3.0.0" - fastparse "^1.1.2" - regexpu-core "^4.6.0" - -css-tree@1.0.0-alpha.37: - version "1.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" - integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== - dependencies: - mdn-data "2.0.4" - source-map "^0.6.1" - -css-tree@1.0.0-alpha.39: - version "1.0.0-alpha.39" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" - integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== - dependencies: - mdn-data "2.0.6" - source-map "^0.6.1" - -css-value@~0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/css-value/-/css-value-0.0.1.tgz#5efd6c2eea5ea1fd6b6ac57ec0427b18452424ea" - integrity sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo= - -css-what@2.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" - integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== - -css-what@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" - integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== - -css@^2.0.0, css@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" - integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== - dependencies: - inherits "^2.0.3" - source-map "^0.6.1" - source-map-resolve "^0.5.2" - urix "^0.1.0" - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-default@^4.0.0, cssnano-preset-default@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" - integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== - dependencies: - css-declaration-sorter "^4.0.1" - cssnano-util-raw-cache "^4.0.1" - postcss "^7.0.0" - postcss-calc "^7.0.1" - postcss-colormin "^4.0.3" - postcss-convert-values "^4.0.1" - postcss-discard-comments "^4.0.2" - postcss-discard-duplicates "^4.0.2" - postcss-discard-empty "^4.0.1" - postcss-discard-overridden "^4.0.1" - postcss-merge-longhand "^4.0.11" - postcss-merge-rules "^4.0.3" - postcss-minify-font-values "^4.0.2" - postcss-minify-gradients "^4.0.2" - postcss-minify-params "^4.0.2" - postcss-minify-selectors "^4.0.2" - postcss-normalize-charset "^4.0.1" - postcss-normalize-display-values "^4.0.2" - postcss-normalize-positions "^4.0.2" - postcss-normalize-repeat-style "^4.0.2" - postcss-normalize-string "^4.0.2" - postcss-normalize-timing-functions "^4.0.2" - postcss-normalize-unicode "^4.0.1" - postcss-normalize-url "^4.0.1" - postcss-normalize-whitespace "^4.0.2" - postcss-ordered-values "^4.1.2" - postcss-reduce-initial "^4.0.3" - postcss-reduce-transforms "^4.0.2" - postcss-svgo "^4.0.2" - postcss-unique-selectors "^4.0.1" - -cssnano-util-get-arguments@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" - integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= - -cssnano-util-get-match@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" - integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= - -cssnano-util-raw-cache@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" - integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== - dependencies: - postcss "^7.0.0" - -cssnano-util-same-parent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" - integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== - -cssnano@^4.0.0, cssnano@^4.1.10: - version "4.1.10" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" - integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== - dependencies: - cosmiconfig "^5.0.0" - cssnano-preset-default "^4.0.7" - is-resolvable "^1.0.0" - postcss "^7.0.0" - -csso@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903" - integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ== - dependencies: - css-tree "1.0.0-alpha.39" - -cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": - version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - -cssstyle@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" - integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== - dependencies: - cssom "0.3.x" - -current-script-polyfill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/current-script-polyfill/-/current-script-polyfill-1.0.0.tgz#f31cf7e4f3e218b0726e738ca92a02d3488ef615" - integrity sha1-8xz35PPiGLBybnOMqSoC00iO9hU= - -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - -custom-electron-titlebar@^3.1.0: - version "3.2.3" - resolved "https://registry.yarnpkg.com/custom-electron-titlebar/-/custom-electron-titlebar-3.2.3.tgz#41da10b97958639e96a04275051bb5f78a666fc3" - integrity sha512-7DIZELlDoiAKT+i73XF9EG778M3/SYMmGa867cn3d0mLEEXDtEJq9sqM/u57vrtjolXKJ5ZJQ9hCCL2uxx9nxQ== - -cyclist@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" - integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -data-urls@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" - integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== - dependencies: - abab "^2.0.0" - whatwg-mimetype "^2.2.0" - whatwg-url "^7.0.0" - -de-indent@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" - integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= - -deasync@^0.1.15: - version "0.1.20" - resolved "https://registry.yarnpkg.com/deasync/-/deasync-0.1.20.tgz#546fd2660688a1eeed55edce2308c5cf7104f9da" - integrity sha512-E1GI7jMI57hL30OX6Ht/hfQU8DO4AuB9m72WFm4c38GNbUD4Q03//XZaOIHZiY+H1xUaomcot5yk2q/qIZQkGQ== - dependencies: - bindings "^1.5.0" - node-addon-api "^1.7.1" - -debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - -debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= - dependencies: - mimic-response "^1.0.0" - -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== - dependencies: - mimic-response "^2.0.0" - -dedent-js@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dedent-js/-/dedent-js-1.0.1.tgz#bee5fb7c9e727d85dffa24590d10ec1ab1255305" - integrity sha1-vuX7fJ5yfYXf+iRZDRDsGrElUwU= - -deep-equal@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -deepmerge@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" - integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== - -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - -deepmerge@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.0.1.tgz#25c1c24f110fb914f80001b925264dd77f3f4312" - integrity sha512-VIPwiMJqJ13ZQfaCsIFnp5Me9tnjURiaIFxfz7EH0Ci0dTSQpZtSLrqOicXqEd/z2r+z+Klk9GzmnRsgpgbOsQ== - -default-gateway@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" - integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== - dependencies: - execa "^1.0.0" - ip-regex "^2.1.0" - -default-gateway@^5.0.2: - version "5.0.5" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-5.0.5.tgz#4fd6bd5d2855d39b34cc5a59505486e9aafc9b10" - integrity sha512-z2RnruVmj8hVMmAnEJMTIJNijhKCDiGjbLP+BHJFOT7ld3Bo5qcIBpVYDniqhbMIIf+jZDlkP2MkPXiQy/DBLA== - dependencies: - execa "^3.3.0" - -default-require-extensions@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" - integrity sha1-836hXT4T/9m0N9M+GnW1+5eHTLg= - dependencies: - strip-bom "^2.0.0" - -defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= - dependencies: - clone "^1.0.2" - -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - -define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -del@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" - integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== - dependencies: - "@types/glob" "^7.1.1" - globby "^6.1.0" - is-path-cwd "^2.0.0" - is-path-in-cwd "^2.0.0" - p-map "^2.0.0" - pify "^4.0.1" - rimraf "^2.6.3" - -del@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" - integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= - -detect-hover@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/detect-hover/-/detect-hover-1.0.2.tgz#589fb0b469220897a9eee3fa36a917e1eda37a21" - integrity sha1-WJ+wtGkiCJep7uP6NqkX4e2jeiE= - -detect-indent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" - integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= - dependencies: - repeating "^2.0.0" - -detect-it@^3.0.3: - version "3.0.5" - resolved "https://registry.yarnpkg.com/detect-it/-/detect-it-3.0.5.tgz#816396c0fe12b11af08299a7c2b8381e58d15175" - integrity sha1-gWOWwP4SsRrwgpmnwrg4HljRUXU= - dependencies: - detect-hover "^1.0.2" - detect-passive-events "^1.0.4" - detect-pointer "^1.0.2" - detect-touch-events "^2.0.1" - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -detect-newline@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" - integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= - -detect-node@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" - integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== - -detect-passive-events@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a" - integrity sha1-btR35uW863kHlzXc01d4nTf5qRo= - -detect-pointer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/detect-pointer/-/detect-pointer-1.0.2.tgz#1e0e4e261dab45055c50c74fb5a4ff09ceb18fbd" - integrity sha1-Hg5OJh2rRQVcUMdPtaT/Cc6xj70= - -detect-touch-events@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/detect-touch-events/-/detect-touch-events-2.0.1.tgz#365833cf0c5c40c4090a08096b8a688db00fa337" - integrity sha1-NlgzzwxcQMQJCggJa4pojbAPozc= - -dev-null@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/dev-null/-/dev-null-0.1.1.tgz#5a205ce3c2b2ef77b6238d6ba179eb74c6a0e818" - integrity sha1-WiBc48Ky73e2I41roXnrdMag6Bg= - -diff@^3.2.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dir-glob@^2.0.0, dir-glob@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" - integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== - dependencies: - path-type "^3.0.0" - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dmg-builder@21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-21.2.0.tgz#a9c883557cacb9abdb66c7133b30fe921c1a3ba7" - integrity sha512-9cJEclnGy7EyKFCoHDYDf54pub/t92CQapyiUxU0w9Bj2vUvfoDagP1PMiX4XD5rPp96141h9A+QN0OB4VgvQg== - dependencies: - app-builder-lib "~21.2.0" - bluebird-lst "^1.0.9" - builder-util "~21.2.0" - fs-extra "^8.1.0" - iconv-lite "^0.5.0" - js-yaml "^3.13.1" - sanitize-filename "^1.6.2" - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" - integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= - -dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== - dependencies: - ip "^1.1.0" - safe-buffer "^5.0.1" - -dns-txt@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" - integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= - dependencies: - buffer-indexof "^1.0.0" - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-converter@^0.2: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-event-types@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dom-event-types/-/dom-event-types-1.0.0.tgz#5830a0a29e1bf837fe50a70cd80a597232813cae" - integrity sha512-2G2Vwi2zXTHBGqXHsJ4+ak/iP0N8Ar+G8a7LiD2oup5o4sQWytwqqrZu/O6hIMV0KMID2PL69OhpshLO0n7UJQ== - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -dom-walk@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" - integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@1, domelementtype@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" - integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== - -domexception@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" - integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== - dependencies: - webidl-conversions "^4.0.2" - -domhandler@^2.3.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" - integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== - dependencies: - domelementtype "1" - -domutils@1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= - dependencies: - dom-serializer "0" - domelementtype "1" - -domutils@^1.5.1, domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -dot-prop@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" - integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== - dependencies: - is-obj "^1.0.0" - -dot-prop@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" - integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== - dependencies: - is-obj "^2.0.0" - -dotenv-expand@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" - integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== - -dotenv@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c" - integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g== - -dotenv@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - -duplexer3@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" - integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= - -duplexer@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= - -duplexify@^3.4.2, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -easy-stack@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.0.tgz#12c91b3085a37f0baa336e9486eac4bf94e3e788" - integrity sha1-EskbMIWjfwuqM26UhurEv5Tj54g= - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -editorconfig@^0.15.3: - version "0.15.3" - resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" - integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g== - dependencies: - commander "^2.19.0" - lru-cache "^4.1.5" - semver "^5.6.0" - sigmund "^1.0.1" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -ejs@^2.6.1, ejs@^2.6.2: - version "2.7.4" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" - integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== - -ejs@~2.5.6: - version "2.5.9" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5" - integrity sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ== - -electron-builder@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-21.2.0.tgz#b68ec4def713fc0b8602654ce842f972432f50c5" - integrity sha512-x8EXrqFbAb2L3N22YlGar3dGh8vwptbB3ovo3OF6K7NTpcsmM2zEoJv7GhFyX73rNzSG2HaWpXwGAtOp2JWiEw== - dependencies: - app-builder-lib "21.2.0" - bluebird-lst "^1.0.9" - builder-util "21.2.0" - builder-util-runtime "8.3.0" - chalk "^2.4.2" - dmg-builder "21.2.0" - fs-extra "^8.1.0" - is-ci "^2.0.0" - lazy-val "^1.0.4" - read-config-file "5.0.0" - sanitize-filename "^1.6.2" - update-notifier "^3.0.1" - yargs "^13.3.0" - -electron-chromedriver@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/electron-chromedriver/-/electron-chromedriver-5.0.1.tgz#0c102a482f16bd47f54fcdd238cf64210395ff4a" - integrity sha512-w82q6KkIsKjzhcucllpxeulIxYn5rccNw43rpbMuZcgMQ0EPsckoYwUt7Gadmdi14xniZ+debN9SM8V1EUyaBQ== - dependencies: - electron-download "^4.1.1" - extract-zip "^1.6.7" - -electron-devtools-installer@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-3.2.0.tgz#acc48d24eb7033fe5af284a19667e73b78d406d0" - integrity sha512-t3UczsYugm4OAbqvdImMCImIMVdFzJAHgbwHpkl5jmfu1izVgUcP/mnrPqJIpEeCK1uZGpt+yHgWEN+9EwoYhQ== - dependencies: - rimraf "^3.0.2" - semver "^7.2.1" - tslib "^2.1.0" - unzip-crx-3 "^0.2.0" - -electron-download@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/electron-download/-/electron-download-4.1.1.tgz#02e69556705cc456e520f9e035556ed5a015ebe8" - integrity sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg== - dependencies: - debug "^3.0.0" - env-paths "^1.0.0" - fs-extra "^4.0.1" - minimist "^1.2.0" - nugget "^2.0.1" - path-exists "^3.0.0" - rc "^1.2.1" - semver "^5.4.1" - sumchecker "^2.0.2" - -electron-icon-builder@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/electron-icon-builder/-/electron-icon-builder-2.0.1.tgz#f43171bde3074870158433aa6103497bc5249793" - integrity sha512-rg9BxW2kJi3TXsMFFNXWXrwQEd5dzXmeD+w7Pj3k3z7aYRePLxE89qU4lvL/rK1X/NTY5KDn3+Dbgm1TU2dGXQ== - dependencies: - args "^5.0.1" - icon-gen "^2.0.0" - jimp "^0.16.1" - -electron-publish@21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-21.2.0.tgz#cc225cb46aa62e74b899f2f7299b396c9802387d" - integrity sha512-mWavuoWJe87iaeKd0I24dNWIaR+0yRzshjNVqGyK019H766fsPWl3caQJnVKFaEyrZRP397v4JZVG0e7s16AxA== - dependencies: - bluebird-lst "^1.0.9" - builder-util "~21.2.0" - builder-util-runtime "8.3.0" - chalk "^2.4.2" - fs-extra "^8.1.0" - lazy-val "^1.0.4" - mime "^2.4.4" - -electron-settings@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/electron-settings/-/electron-settings-4.0.2.tgz#26ef242397393e0e69119f6fb879fc2287d0f508" - integrity sha512-WnUlrnBsO784oXcag0ym+A3ySoIwonz5GhYFsWroMHVzslzmsP+81f/Fof41T9UrRUxuPPKiZPZMwGO+yvWChg== - dependencies: - lodash.get "^4.4.2" - lodash.has "^4.5.2" - lodash.set "^4.3.2" - lodash.unset "^4.5.2" - mkdirp "^1.0.4" - write-file-atomic "^3.0.3" - -electron-to-chromium@^1.3.488: - version "1.3.501" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.501.tgz#faa17a2cb0105ee30d5e1ca87eae7d8e85dd3175" - integrity sha512-tyzuKaV2POw2mtqBBzQGNBojMZzH0MRu8bT8T/50x+hWeucyG/9pkgAATy+PcM2ySNM9+8eG2VllY9c6j4i+bg== - -electron@^13.0.0: - version "13.1.4" - resolved "https://registry.yarnpkg.com/electron/-/electron-13.1.4.tgz#6d20d932a0651c3cba9f09a3d08cbaf5b69aa84b" - integrity sha512-4qhRZbRvGqHmMWsCG/kRVF4X8VIq9Nujgm+gXZLBSpiR6uUtMHy7ViBTQZl1PGf6O9Ppxhpr9Yz+k6Um9WoP3Q== - dependencies: - "@electron/get" "^1.0.1" - "@types/node" "^14.6.2" - extract-zip "^1.0.3" - -element-resize-detector@^1.1.15: - version "1.2.1" - resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.1.tgz#b0305194447a4863155e58f13323a0aef30851d1" - integrity sha512-BdFsPepnQr9fznNPF9nF4vQ457U/ZJXQDSNF1zBe7yaga8v9AdZf3/NElYxFdUh7SitSGt040QygiTo6dtatIw== - dependencies: - batch-processor "1.0.0" - -elliptic@^6.0.0, elliptic@^6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emojis-list@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" - integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -encodeurl@^1.0.2, encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" - integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -entities@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" - integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== - -entities@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" - integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== - -env-paths@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" - integrity sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA= - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -errno@^0.1.3, errno@~0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== - dependencies: - prr "~1.0.1" - -error-ex@^1.2.0, error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -error-stack-parser@^2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.6.tgz#5a99a707bd7a4c58a797902d48d82803ede6aad8" - integrity sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ== - dependencies: - stackframe "^1.1.1" - -es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: - version "1.17.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" - integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.0" - is-regex "^1.1.0" - object-inspect "^1.7.0" - object-keys "^1.1.1" - object.assign "^4.1.0" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es6-error@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" - integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -escalade@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" - integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -escodegen@^1.9.1: - version "1.14.3" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" - integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== - dependencies: - esprima "^4.0.1" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -eslint-config-prettier@^3.3.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-3.6.0.tgz#8ca3ffac4bd6eeef623a0651f9d754900e3ec217" - integrity sha512-ixJ4U3uTLXwJts4rmSVW/lMXjlGwCijhBJHk8iVqKKSifeI0qgFEfWl8L63isfc8Od7EiBALF6BX3jKLluf/jQ== - dependencies: - get-stdin "^6.0.0" - -eslint-loader@^2.1.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.2.1.tgz#28b9c12da54057af0845e2a6112701a2f6bf8337" - integrity sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg== - dependencies: - loader-fs-cache "^1.0.0" - loader-utils "^1.0.2" - object-assign "^4.0.1" - object-hash "^1.1.4" - rimraf "^2.6.1" - -eslint-plugin-prettier@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz#168ab43154e2ea57db992a2cd097c828171f75c2" - integrity sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg== - dependencies: - prettier-linter-helpers "^1.0.0" - -eslint-plugin-vue@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.7.1.tgz#c829b9fc62582c1897b5a0b94afd44ecca511e63" - integrity sha512-esETKhVMI7Vdli70Wt4bvAwnZBJeM0pxVX9Yb0wWKxdCJc2EADalVYK/q2FzMw8oKN0wPMdqVCKS8kmR89recA== - dependencies: - vue-eslint-parser "^2.0.3" - -eslint-plugin-vue@^5.0.0: - version "5.2.3" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-5.2.3.tgz#3ee7597d823b5478804b2feba9863b1b74273961" - integrity sha512-mGwMqbbJf0+VvpGR5Lllq0PMxvTdrZ/ZPjmhkacrCHbubJeJOt+T6E3HUzAifa2Mxi7RSdJfC9HFpOeSYVMMIw== - dependencies: - vue-eslint-parser "^5.0.0" - -eslint-scope@^3.7.1: - version "3.7.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535" - integrity sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-scope@^4.0.0, eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-scope@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.0.tgz#d0f971dfe59c69e0cada684b23d49dbf82600ce5" - integrity sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-utils@^1.3.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-utils@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint@^4.19.1: - version "4.19.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" - integrity sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ== - dependencies: - ajv "^5.3.0" - babel-code-frame "^6.22.0" - chalk "^2.1.0" - concat-stream "^1.6.0" - cross-spawn "^5.1.0" - debug "^3.1.0" - doctrine "^2.1.0" - eslint-scope "^3.7.1" - eslint-visitor-keys "^1.0.0" - espree "^3.5.4" - esquery "^1.0.0" - esutils "^2.0.2" - file-entry-cache "^2.0.0" - functional-red-black-tree "^1.0.1" - glob "^7.1.2" - globals "^11.0.1" - ignore "^3.3.3" - imurmurhash "^0.1.4" - inquirer "^3.0.6" - is-resolvable "^1.0.0" - js-yaml "^3.9.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.4" - minimatch "^3.0.2" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.2" - pluralize "^7.0.0" - progress "^2.0.0" - regexpp "^1.0.1" - require-uncached "^1.0.3" - semver "^5.3.0" - strip-ansi "^4.0.0" - strip-json-comments "~2.0.1" - table "4.0.2" - text-table "~0.2.0" - -eslint@^5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea" - integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.9.1" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^4.0.3" - eslint-utils "^1.3.1" - eslint-visitor-keys "^1.0.0" - espree "^5.0.1" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob "^7.1.2" - globals "^11.7.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^6.2.2" - js-yaml "^3.13.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.11" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.2" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^5.5.1" - strip-ansi "^4.0.0" - strip-json-comments "^2.0.1" - table "^5.2.3" - text-table "^0.2.0" - -espree@^3.5.2, espree@^3.5.4: - version "3.5.4" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" - integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A== - dependencies: - acorn "^5.5.0" - acorn-jsx "^3.0.0" - -espree@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-4.1.0.tgz#728d5451e0fd156c04384a7ad89ed51ff54eb25f" - integrity sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w== - dependencies: - acorn "^6.0.2" - acorn-jsx "^5.0.0" - eslint-visitor-keys "^1.0.0" - -espree@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a" - integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A== - dependencies: - acorn "^6.0.7" - acorn-jsx "^5.0.0" - eslint-visitor-keys "^1.0.0" - -esprima@^4.0.0, esprima@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.0.0, esquery@^1.0.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== - dependencies: - estraverse "^4.1.0" - -estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" - integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -event-pubsub@4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/event-pubsub/-/event-pubsub-4.3.0.tgz#f68d816bc29f1ec02c539dc58c8dd40ce72cb36e" - integrity sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ== - -eventemitter3@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" - integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== - -events@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" - integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== - -eventsource@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== - dependencies: - original "^1.0.0" - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -exec-sh@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" - integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== - dependencies: - merge "^1.2.0" - -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" - integrity sha1-2NdrvBtVIX7RkP1t1J08d07PyNo= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - -execa@^3.3.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" - integrity sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - -exif-parser@^0.1.12: - version "0.1.12" - resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" - integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI= - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= - -expand-brackets@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" - integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s= - dependencies: - is-posix-bracket "^0.1.0" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-range@^1.8.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" - integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= - dependencies: - fill-range "^2.1.0" - -expand-template@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" - integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== - -expect@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-23.6.0.tgz#1e0c8d3ba9a581c87bd71fb9bc8862d443425f98" - integrity sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w== - dependencies: - ansi-styles "^3.2.0" - jest-diff "^23.6.0" - jest-get-type "^22.1.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - -express@^4.16.3, express@^4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== - dependencies: - accepts "~1.3.7" - array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" - content-type "~1.0.4" - cookie "0.4.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" - range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^2.0.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5" - integrity sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A== - dependencies: - chardet "^0.4.0" - iconv-lite "^0.4.17" - tmp "^0.0.33" - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extglob@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" - integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE= - dependencies: - is-extglob "^1.0.0" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -extract-from-css@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/extract-from-css/-/extract-from-css-0.4.4.tgz#1ea7df2e7c7c6eb9922fa08e8adaea486f6f8f92" - integrity sha1-HqffLnx8brmSL6COitrqSG9vj5I= - dependencies: - css "^2.1.0" - -extract-zip@^1.0.3, extract-zip@^1.6.5, extract-zip@^1.6.7: - version "1.7.0" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" - integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-diff@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" - integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== - -fast-glob@^2.2.6: - version "2.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" - integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== - dependencies: - "@mrmlnc/readdir-enhanced" "^2.2.1" - "@nodelib/fs.stat" "^1.1.2" - glob-parent "^3.1.0" - is-glob "^4.0.0" - merge2 "^1.2.3" - micromatch "^3.1.10" - -fast-glob@^3.1.1: - version "3.2.5" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" - integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" - merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" - -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fast-png@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/fast-png/-/fast-png-5.0.4.tgz#e621bf76f85fa4a836c09c13514dce7684c5ea23" - integrity sha512-vTNj6yixRnclW6sTlCeH6sNRLBOhM5ITmlo1LSU5ojKEc2e9kZkqXPo2xzBxKb61MBCXRXBcr8qJztOHr2O6WQ== - dependencies: - "@types/pako" "^1.0.1" - iobuffer "^5.0.2" - pako "^2.0.2" - -fastparse@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" - integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== - -fastq@^1.6.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.1.tgz#8b8f2ac8bf3632d67afcd65dac248d5fdc45385e" - integrity sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA== - dependencies: - reusify "^1.0.4" - -faye-websocket@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" - integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= - dependencies: - websocket-driver ">=0.5.1" - -faye-websocket@~0.11.1: - version "0.11.3" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== - dependencies: - websocket-driver ">=0.5.1" - -fb-watchman@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" - integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== - dependencies: - bser "2.1.1" - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= - dependencies: - pend "~1.2.0" - -figgy-pudding@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" - integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= - dependencies: - flat-cache "^1.2.1" - object-assign "^4.0.1" - -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== - dependencies: - flat-cache "^2.0.1" - -file-loader@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" - integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== - dependencies: - loader-utils "^1.0.2" - schema-utils "^1.0.0" - -file-type@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-9.0.0.tgz#a68d5ad07f486414dfb2c8866f73161946714a18" - integrity sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw== - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -file-url@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/file-url/-/file-url-2.0.2.tgz#e951784d79095127d3713029ab063f40818ca2ae" - integrity sha1-6VF4TXkJUSfTcTApqwY/QIGMoq4= - -filename-regex@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY= - -fileset@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" - integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA= - dependencies: - glob "^7.0.3" - minimatch "^3.0.3" - -filesize@^3.6.1: - version "3.6.1" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" - integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg== - -fill-range@^2.1.0: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" - integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== - dependencies: - is-number "^2.1.0" - isobject "^2.0.0" - randomatic "^3.0.0" - repeat-element "^1.1.2" - repeat-string "^1.5.2" - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -find-babel-config@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/find-babel-config/-/find-babel-config-1.2.0.tgz#a9b7b317eb5b9860cda9d54740a8c8337a2283a2" - integrity sha512-jB2CHJeqy6a820ssiqwrKMeyC6nNdmrcgkKWJWmpoxpE8RKciYJXCcXRq1h2AzCo5I5BJeN2tkGEO3hLTuePRA== - dependencies: - json5 "^0.5.1" - path-exists "^3.0.0" - -find-cache-dir@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" - integrity sha1-yN765XyKUqinhPnjHFfHQumToLk= - dependencies: - commondir "^1.0.1" - mkdirp "^0.5.1" - pkg-dir "^1.0.0" - -find-cache-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" - integrity sha1-kojj6ePMN0hxfTnq3hfPcfww7m8= - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^2.0.0" - -find-cache-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flat-cache@^1.2.1: - version "1.3.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" - integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg== - dependencies: - circular-json "^0.3.1" - graceful-fs "^4.1.2" - rimraf "~2.6.2" - write "^0.2.1" - -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== - dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== - -flush-write-stream@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - -follow-redirects@^1.0.0: - version "1.12.1" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" - integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== - -for-in@^1.0.1, for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= - -for-own@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= - dependencies: - for-in "^1.0.1" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -fork-ts-checker-webpack-plugin@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-0.5.2.tgz#a73b3630bd0a69409a6e4824e54c03a62fe82d8f" - integrity sha512-a5IG+xXyKnpruI0CP/anyRLAoxWtp3lzdG6flxicANnoSzz64b12dJ7ASAVRrI2OaWwZR2JyBaMHFQqInhWhIw== - dependencies: - babel-code-frame "^6.22.0" - chalk "^2.4.1" - chokidar "^2.0.4" - micromatch "^3.1.10" - minimatch "^3.0.4" - tapable "^1.0.0" - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= - dependencies: - map-cache "^0.2.2" - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -friendly-errors-webpack-plugin@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.0.tgz#efc86cbb816224565861a1be7a9d84d0aafea136" - integrity sha512-K27M3VK30wVoOarP651zDmb93R9zF28usW4ocaK3mfQeIEI5BPht/EzZs5E8QLLwbLRJQMwscAjDxYPb1FuNiw== - dependencies: - chalk "^1.1.3" - error-stack-parser "^2.0.0" - string-width "^2.0.0" - -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-extra@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" - integrity sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA= - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - -fs-extra@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" - integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-extra@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" - integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@^1.2.3, fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -gaze@~1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" - integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== - dependencies: - globule "^1.0.0" - -gensync@^1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" - integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== - -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - -get-stream@^4.0.0, get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.0.0, get-stream@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" - integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== - dependencies: - pump "^3.0.0" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -gifwrap@^0.9.2: - version "0.9.2" - resolved "https://registry.yarnpkg.com/gifwrap/-/gifwrap-0.9.2.tgz#348e286e67d7cf57942172e1e6f05a71cee78489" - integrity sha512-fcIswrPaiCDAyO8xnWvHSZdWChjKXUanKKpAiWWJ/UTkEi/aYKn5+90e7DE820zbEaVR9CE2y4z9bzhQijZ0BA== - dependencies: - image-q "^1.1.1" - omggif "^1.0.10" - -github-from-package@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" - integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= - -glob-base@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" - integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q= - dependencies: - glob-parent "^2.0.0" - is-glob "^2.0.0" - -glob-parent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" - integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg= - dependencies: - is-glob "^2.0.0" - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== - dependencies: - is-glob "^4.0.1" - -glob-to-regexp@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" - integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= - -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-agent@^2.0.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-2.2.0.tgz#566331b0646e6bf79429a16877685c4a1fbf76dc" - integrity sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg== - dependencies: - boolean "^3.0.1" - core-js "^3.6.5" - es6-error "^4.1.1" - matcher "^3.0.0" - roarr "^2.15.3" - semver "^7.3.2" - serialize-error "^7.0.1" - -global-dirs@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" - integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= - dependencies: - ini "^1.3.4" - -global-tunnel-ng@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz#d03b5102dfde3a69914f5ee7d86761ca35d57d8f" - integrity sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg== - dependencies: - encodeurl "^1.0.2" - lodash "^4.17.10" - npm-conf "^1.1.3" - tunnel "^0.0.6" - -global@~4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" - integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== - dependencies: - min-document "^2.19.0" - process "^0.11.10" - -globals@^11.0.1, globals@^11.1.0, globals@^11.7.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^9.18.0: - version "9.18.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" - integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== - -globalthis@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.2.tgz#2a235d34f4d8036219f7e34929b5de9e18166b8b" - integrity sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ== - dependencies: - define-properties "^1.1.3" - -globby@^11.0.1: - version "11.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" - integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -globby@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" - integrity sha1-+yzP+UAfhgCUXfral0QMypcrhoA= - dependencies: - array-union "^1.0.1" - dir-glob "^2.0.0" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" - -globby@^9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" - integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== - dependencies: - "@types/glob" "^7.1.1" - array-union "^1.0.2" - dir-glob "^2.2.2" - fast-glob "^2.2.6" - glob "^7.1.3" - ignore "^4.0.3" - pify "^4.0.1" - slash "^2.0.0" - -globule@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.2.tgz#d8bdd9e9e4eef8f96e245999a5dee7eb5d8529c4" - integrity sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA== - dependencies: - glob "~7.1.1" - lodash "~4.17.10" - minimatch "~3.0.2" - -golden-layout@^1.5.9: - version "1.5.9" - resolved "https://registry.yarnpkg.com/golden-layout/-/golden-layout-1.5.9.tgz#a39bc1f6a67e6f886b797c016dd924e9426ba77f" - integrity sha1-o5vB9qZ+b4hreXwBbdkk6UJrp38= - dependencies: - jquery "*" - -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - -graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -graceful-fs@^4.1.9, graceful-fs@^4.2.4: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -grapheme-splitter@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= - -gzip-size@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" - integrity sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA== - dependencies: - duplexer "^0.1.1" - pify "^4.0.1" - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -handlebars@^4.0.3: - version "4.7.6" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" - integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== - dependencies: - minimist "^1.2.5" - neo-async "^2.6.0" - source-map "^0.6.1" - wordwrap "^1.0.0" - optionalDependencies: - uglify-js "^3.1.4" - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" - integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= - -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" - integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.0, has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== - -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== - -has@^1.0.0, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash-sum@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" - integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ= - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasha@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" - integrity sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE= - dependencies: - is-stream "^1.0.1" - pinkie-promise "^2.0.0" - -he@1.2.x, he@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - -highlight.js@^9.6.0: - version "9.18.1" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.1.tgz#ed21aa001fe6252bb10a3d76d47573c6539fe13c" - integrity sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg== - -history@^4.7.2: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -hoist-non-react-statics@^2.5.0: - version "2.5.5" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" - integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== - -home-or-tmp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" - integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.1" - -hoopy@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" - integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== - -hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: - version "2.8.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" - integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= - -html-comment-regex@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" - integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== - -html-encoding-sniffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" - integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== - dependencies: - whatwg-encoding "^1.0.1" - -html-entities@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" - integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== - -html-minifier@^3.2.3: - version "3.5.21" - resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" - integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== - dependencies: - camel-case "3.0.x" - clean-css "4.2.x" - commander "2.17.x" - he "1.2.x" - param-case "2.1.x" - relateurl "0.2.x" - uglify-js "3.4.x" - -html-tags@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" - integrity sha1-ELMKOGCF9Dzt41PMj6fLDe7qZos= - -html-webpack-plugin@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b" - integrity sha1-sBq71yOsqqeze2r0SS69oD2d03s= - dependencies: - html-minifier "^3.2.3" - loader-utils "^0.2.16" - lodash "^4.17.3" - pretty-error "^2.0.2" - tapable "^1.0.0" - toposort "^1.0.0" - util.promisify "1.0.0" - -htmlparser2@^3.3.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" - integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== - dependencies: - domelementtype "^1.3.1" - domhandler "^2.3.0" - domutils "^1.5.1" - entities "^1.1.1" - inherits "^2.0.1" - readable-stream "^3.1.1" - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= - -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-parser-js@>=0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" - integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== - -http-proxy-middleware@0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" - integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== - dependencies: - http-proxy "^1.17.0" - is-glob "^4.0.0" - lodash "^4.17.11" - micromatch "^3.1.10" - -http-proxy@^1.17.0: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - -icon-gen@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/icon-gen/-/icon-gen-2.1.0.tgz#401642b68d7c79316a8e6729bcf3fdfcdb71bdfa" - integrity sha512-rqIVvq9MJ8X7wnJW0NO8Eau/+5RWV7AH6L5vEt/U5Ajv5WefdDNDxGwJhGokyHuyBWeX7JqRMQ03tG0gAco4Eg== - dependencies: - commander "^6.2.0" - del "^6.0.0" - mkdirp "^1.0.4" - pngjs "^6.0.0" - svg2png "4.1.1" - uuid "^8.3.1" - -iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@^0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.2.tgz#af6d628dccfb463b7364d97f715e4b74b8c8c2b8" - integrity sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= - -icss-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" - integrity sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI= - dependencies: - postcss "^6.0.1" - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ieee754@^1.1.4: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= - -ignore@^3.3.3, ignore@^3.3.5: - version "3.3.10" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" - integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== - -ignore@^4.0.3, ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== - -image-q@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/image-q/-/image-q-1.1.1.tgz#fc84099664460b90ca862d9300b6bfbbbfbf8056" - integrity sha1-/IQJlmRGC5DKhi2TALa/u7+/gFY= - -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= - -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= - dependencies: - import-from "^2.1.0" - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - -import-fresh@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" - integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha1-M1238qev/VOqpHHUuAId7ja387E= - dependencies: - resolve-from "^3.0.0" - -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= - -import-local@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" - integrity sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ== - dependencies: - pkg-dir "^2.0.0" - resolve-cwd "^2.0.0" - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= - -infer-owner@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - -ini@^1.3.4, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== - -inquirer@^3.0.6, inquirer@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" - integrity sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ== - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.0" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^2.0.4" - figures "^2.0.0" - lodash "^4.3.0" - mute-stream "0.0.7" - run-async "^2.2.0" - rx-lite "^4.0.8" - rx-lite-aggregates "^4.0.8" - string-width "^2.1.0" - strip-ansi "^4.0.0" - through "^2.3.6" - -inquirer@^6.2.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - -internal-ip@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" - integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== - dependencies: - default-gateway "^4.2.0" - ipaddr.js "^1.9.0" - -intersection-observer@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.5.1.tgz#e340fc56ce74290fe2b2394d1ce88c4353ac6dfa" - integrity sha512-Zd7Plneq82kiXFixs7bX62YnuZ0BMRci9br7io88LwDyF3V43cQMI+G5IiTlTNTt+LsDUppl19J/M2Fp9UkH6g== - -invariant@^2.2.2, invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - -iobuffer@^5.0.2: - version "5.0.3" - resolved "https://registry.yarnpkg.com/iobuffer/-/iobuffer-5.0.3.tgz#31a640b4e33659955bbf57ce796324334265dacf" - integrity sha512-0SNk4hbHVXx9oE27vTJY+oiI0txkhBdQV12RvILd/7XuIhBZ0TkImq5EnhFYCcRcDff8jpFhZ9C2Sg+NIo3ZMQ== - -ip-regex@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" - integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= - -ip@^1.1.0, ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ipaddr.js@1.9.1, ipaddr.js@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-absolute-url@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" - integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= - -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== - dependencies: - kind-of "^6.0.0" - -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.4, is-callable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" - integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== - -is-ci@^1.0.10: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" - integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== - dependencies: - ci-info "^1.5.0" - -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - -is-color-stop@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= - dependencies: - kind-of "^3.0.2" - -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== - dependencies: - kind-of "^6.0.0" - -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - -is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== - dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== - dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - -is-docker@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" - integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== - -is-dotfile@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" - integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE= - -is-equal-shallow@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" - integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ= - dependencies: - is-primitive "^2.0.0" - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" - integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-function@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" - integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ== - -is-generator-fn@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a" - integrity sha1-lp1J4bszKfa7fwkIm+JleLLd1Go= - -is-glob@^2.0.0, is-glob@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" - integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM= - dependencies: - is-extglob "^1.0.0" - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-installed-globally@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" - integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= - dependencies: - global-dirs "^0.1.0" - is-path-inside "^1.0.0" - -is-npm@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-3.0.0.tgz#ec9147bfb629c43f494cf67936a961edec7e8053" - integrity sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA== - -is-number@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" - integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= - dependencies: - kind-of "^3.0.2" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= - dependencies: - kind-of "^3.0.2" - -is-number@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" - integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-cwd@^2.0.0, is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-in-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" - integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== - dependencies: - is-path-inside "^2.1.0" - -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" - -is-path-inside@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" - integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== - dependencies: - path-is-inside "^1.0.2" - -is-path-inside@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" - integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== - -is-plain-obj@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-posix-bracket@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" - integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q= - -is-primitive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" - integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= - -is-regex@^1.0.4, is-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" - integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== - dependencies: - has-symbols "^1.0.1" - -is-resolvable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== - -is-stream@^1.0.1, is-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - -is-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" - integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== - -is-svg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" - integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== - dependencies: - html-comment-regex "^1.1.0" - -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - -is-typedarray@^1.0.0, is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-whitespace@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-whitespace/-/is-whitespace-0.3.0.tgz#1639ecb1be036aec69a54cbb401cfbed7114ab7f" - integrity sha1-Fjnssb4DauxppUy7QBz77XEUq38= - -is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= - -is-wsl@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -is-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isbinaryfile@^4.0.2: - version "4.0.6" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" - integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -istanbul-api@^1.3.1: - version "1.3.7" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.7.tgz#a86c770d2b03e11e3f778cd7aedd82d2722092aa" - integrity sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA== - dependencies: - async "^2.1.4" - fileset "^2.0.2" - istanbul-lib-coverage "^1.2.1" - istanbul-lib-hook "^1.2.2" - istanbul-lib-instrument "^1.10.2" - istanbul-lib-report "^1.1.5" - istanbul-lib-source-maps "^1.2.6" - istanbul-reports "^1.5.1" - js-yaml "^3.7.0" - mkdirp "^0.5.1" - once "^1.4.0" - -istanbul-lib-coverage@^1.2.0, istanbul-lib-coverage@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" - integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== - -istanbul-lib-hook@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz#bc6bf07f12a641fbf1c85391d0daa8f0aea6bf86" - integrity sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw== - dependencies: - append-transform "^0.4.0" - -istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" - integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== - dependencies: - babel-generator "^6.18.0" - babel-template "^6.16.0" - babel-traverse "^6.18.0" - babel-types "^6.18.0" - babylon "^6.18.0" - istanbul-lib-coverage "^1.2.1" - semver "^5.3.0" - -istanbul-lib-report@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz#f2a657fc6282f96170aaf281eb30a458f7f4170c" - integrity sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw== - dependencies: - istanbul-lib-coverage "^1.2.1" - mkdirp "^0.5.1" - path-parse "^1.0.5" - supports-color "^3.1.2" - -istanbul-lib-source-maps@^1.2.4, istanbul-lib-source-maps@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f" - integrity sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg== - dependencies: - debug "^3.1.0" - istanbul-lib-coverage "^1.2.1" - mkdirp "^0.5.1" - rimraf "^2.6.1" - source-map "^0.5.3" - -istanbul-reports@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.5.1.tgz#97e4dbf3b515e8c484caea15d6524eebd3ff4e1a" - integrity sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw== - dependencies: - handlebars "^4.0.3" - -javascript-stringify@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3" - integrity sha1-FC0RHzpuPa6PSpr9d9RYVbWpzOM= - -javascript-stringify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.0.1.tgz#6ef358035310e35d667c675ed63d3eb7c1aa19e5" - integrity sha512-yV+gqbd5vaOYjqlbk16EG89xB5udgjqQF3C5FAORDg4f/IS1Yc5ERCv5e/57yBcfJYw05V5JyIXabhwb75Xxow== - -jest-changed-files@^23.4.2: - version "23.4.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" - integrity sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA== - dependencies: - throat "^4.0.0" - -jest-cli@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-23.6.0.tgz#61ab917744338f443ef2baa282ddffdd658a5da4" - integrity sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ== - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.1" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.1.11" - import-local "^1.0.0" - is-ci "^1.0.10" - istanbul-api "^1.3.1" - istanbul-lib-coverage "^1.2.0" - istanbul-lib-instrument "^1.10.1" - istanbul-lib-source-maps "^1.2.4" - jest-changed-files "^23.4.2" - jest-config "^23.6.0" - jest-environment-jsdom "^23.4.0" - jest-get-type "^22.1.0" - jest-haste-map "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - jest-resolve-dependencies "^23.6.0" - jest-runner "^23.6.0" - jest-runtime "^23.6.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - jest-watcher "^23.4.0" - jest-worker "^23.2.0" - micromatch "^2.3.11" - node-notifier "^5.2.1" - prompts "^0.1.9" - realpath-native "^1.0.0" - rimraf "^2.5.4" - slash "^1.0.0" - string-length "^2.0.0" - strip-ansi "^4.0.0" - which "^1.2.12" - yargs "^11.0.0" - -jest-config@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-23.6.0.tgz#f82546a90ade2d8c7026fbf6ac5207fc22f8eb1d" - integrity sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ== - dependencies: - babel-core "^6.0.0" - babel-jest "^23.6.0" - chalk "^2.0.1" - glob "^7.1.1" - jest-environment-jsdom "^23.4.0" - jest-environment-node "^23.4.0" - jest-get-type "^22.1.0" - jest-jasmine2 "^23.6.0" - jest-regex-util "^23.3.0" - jest-resolve "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - micromatch "^2.3.11" - pretty-format "^23.6.0" - -jest-diff@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.6.0.tgz#1500f3f16e850bb3d71233408089be099f610c7d" - integrity sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g== - dependencies: - chalk "^2.0.1" - diff "^3.2.0" - jest-get-type "^22.1.0" - pretty-format "^23.6.0" - -jest-docblock@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-23.2.0.tgz#f085e1f18548d99fdd69b20207e6fd55d91383a7" - integrity sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c= - dependencies: - detect-newline "^2.1.0" - -jest-each@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-23.6.0.tgz#ba0c3a82a8054387016139c733a05242d3d71575" - integrity sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg== - dependencies: - chalk "^2.0.1" - pretty-format "^23.6.0" - -jest-environment-jsdom@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz#056a7952b3fea513ac62a140a2c368c79d9e6023" - integrity sha1-BWp5UrP+pROsYqFAosNox52eYCM= - dependencies: - jest-mock "^23.2.0" - jest-util "^23.4.0" - jsdom "^11.5.1" - -jest-environment-node@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-23.4.0.tgz#57e80ed0841dea303167cce8cd79521debafde10" - integrity sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA= - dependencies: - jest-mock "^23.2.0" - jest-util "^23.4.0" - -jest-get-type@^22.1.0: - version "22.4.3" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" - integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w== - -jest-haste-map@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-23.6.0.tgz#2e3eb997814ca696d62afdb3f2529f5bbc935e16" - integrity sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg== - dependencies: - fb-watchman "^2.0.0" - graceful-fs "^4.1.11" - invariant "^2.2.4" - jest-docblock "^23.2.0" - jest-serializer "^23.0.1" - jest-worker "^23.2.0" - micromatch "^2.3.11" - sane "^2.0.0" - -jest-jasmine2@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz#840e937f848a6c8638df24360ab869cc718592e0" - integrity sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ== - dependencies: - babel-traverse "^6.0.0" - chalk "^2.0.1" - co "^4.6.0" - expect "^23.6.0" - is-generator-fn "^1.0.0" - jest-diff "^23.6.0" - jest-each "^23.6.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - pretty-format "^23.6.0" - -jest-leak-detector@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz#e4230fd42cf381a1a1971237ad56897de7e171de" - integrity sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg== - dependencies: - pretty-format "^23.6.0" - -jest-matcher-utils@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz#726bcea0c5294261a7417afb6da3186b4b8cac80" - integrity sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog== - dependencies: - chalk "^2.0.1" - jest-get-type "^22.1.0" - pretty-format "^23.6.0" - -jest-message-util@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-23.4.0.tgz#17610c50942349508d01a3d1e0bda2c079086a9f" - integrity sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8= - dependencies: - "@babel/code-frame" "^7.0.0-beta.35" - chalk "^2.0.1" - micromatch "^2.3.11" - slash "^1.0.0" - stack-utils "^1.0.1" - -jest-mock@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-23.2.0.tgz#ad1c60f29e8719d47c26e1138098b6d18b261134" - integrity sha1-rRxg8p6HGdR8JuETgJi20YsmETQ= - -jest-regex-util@^23.3.0: - version "23.3.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-23.3.0.tgz#5f86729547c2785c4002ceaa8f849fe8ca471bc5" - integrity sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U= - -jest-resolve-dependencies@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz#b4526af24c8540d9a3fab102c15081cf509b723d" - integrity sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA== - dependencies: - jest-regex-util "^23.3.0" - jest-snapshot "^23.6.0" - -jest-resolve@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-23.6.0.tgz#cf1d1a24ce7ee7b23d661c33ba2150f3aebfa0ae" - integrity sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA== - dependencies: - browser-resolve "^1.11.3" - chalk "^2.0.1" - realpath-native "^1.0.0" - -jest-runner@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-23.6.0.tgz#3894bd219ffc3f3cb94dc48a4170a2e6f23a5a38" - integrity sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA== - dependencies: - exit "^0.1.2" - graceful-fs "^4.1.11" - jest-config "^23.6.0" - jest-docblock "^23.2.0" - jest-haste-map "^23.6.0" - jest-jasmine2 "^23.6.0" - jest-leak-detector "^23.6.0" - jest-message-util "^23.4.0" - jest-runtime "^23.6.0" - jest-util "^23.4.0" - jest-worker "^23.2.0" - source-map-support "^0.5.6" - throat "^4.0.0" - -jest-runtime@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-23.6.0.tgz#059e58c8ab445917cd0e0d84ac2ba68de8f23082" - integrity sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw== - dependencies: - babel-core "^6.0.0" - babel-plugin-istanbul "^4.1.6" - chalk "^2.0.1" - convert-source-map "^1.4.0" - exit "^0.1.2" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.1.11" - jest-config "^23.6.0" - jest-haste-map "^23.6.0" - jest-message-util "^23.4.0" - jest-regex-util "^23.3.0" - jest-resolve "^23.6.0" - jest-snapshot "^23.6.0" - jest-util "^23.4.0" - jest-validate "^23.6.0" - micromatch "^2.3.11" - realpath-native "^1.0.0" - slash "^1.0.0" - strip-bom "3.0.0" - write-file-atomic "^2.1.0" - yargs "^11.0.0" - -jest-serializer-vue@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jest-serializer-vue/-/jest-serializer-vue-2.0.2.tgz#b238ef286357ec6b480421bd47145050987d59b3" - integrity sha1-sjjvKGNX7GtIBCG9RxRQUJh9WbM= - dependencies: - pretty "2.0.0" - -jest-serializer@^23.0.1: - version "23.0.1" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-23.0.1.tgz#a3776aeb311e90fe83fab9e533e85102bd164165" - integrity sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU= - -jest-snapshot@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-23.6.0.tgz#f9c2625d1b18acda01ec2d2b826c0ce58a5aa17a" - integrity sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg== - dependencies: - babel-types "^6.0.0" - chalk "^2.0.1" - jest-diff "^23.6.0" - jest-matcher-utils "^23.6.0" - jest-message-util "^23.4.0" - jest-resolve "^23.6.0" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^23.6.0" - semver "^5.5.0" - -jest-transform-stub@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d" - integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg== - -jest-util@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-23.4.0.tgz#4d063cb927baf0a23831ff61bec2cbbf49793561" - integrity sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE= - dependencies: - callsites "^2.0.0" - chalk "^2.0.1" - graceful-fs "^4.1.11" - is-ci "^1.0.10" - jest-message-util "^23.4.0" - mkdirp "^0.5.1" - slash "^1.0.0" - source-map "^0.6.0" - -jest-validate@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.6.0.tgz#36761f99d1ed33fcd425b4e4c5595d62b6597474" - integrity sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A== - dependencies: - chalk "^2.0.1" - jest-get-type "^22.1.0" - leven "^2.1.0" - pretty-format "^23.6.0" - -jest-watch-typeahead@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/jest-watch-typeahead/-/jest-watch-typeahead-0.2.1.tgz#6c40f232996ca6c39977e929e9f79b189e7d87e4" - integrity sha512-xdhEtKSj0gmnkDQbPTIHvcMmXNUDzYpHLEJ5TFqlaI+schi2NI96xhWiZk9QoesAS7oBmKwWWsHazTrYl2ORgg== - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.4.1" - jest-watcher "^23.1.0" - slash "^2.0.0" - string-length "^2.0.0" - strip-ansi "^5.0.0" - -jest-watcher@^23.1.0, jest-watcher@^23.4.0: - version "23.4.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-23.4.0.tgz#d2e28ce74f8dad6c6afc922b92cabef6ed05c91c" - integrity sha1-0uKM50+NrWxq/JIrksq+9u0FyRw= - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.1" - string-length "^2.0.0" - -jest-worker@^23.2.0: - version "23.2.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-23.2.0.tgz#faf706a8da36fae60eb26957257fa7b5d8ea02b9" - integrity sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk= - dependencies: - merge-stream "^1.0.1" - -jest@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-23.6.0.tgz#ad5835e923ebf6e19e7a1d7529a432edfee7813d" - integrity sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw== - dependencies: - import-local "^1.0.0" - jest-cli "^23.6.0" - -jimp@^0.16.1: - version "0.16.1" - resolved "https://registry.yarnpkg.com/jimp/-/jimp-0.16.1.tgz#192f851a30e5ca11112a3d0aa53137659a78ca7a" - integrity sha512-+EKVxbR36Td7Hfd23wKGIeEyHbxShZDX6L8uJkgVW3ESA9GiTEPK08tG1XI2r/0w5Ch0HyJF5kPqF9K7EmGjaw== - dependencies: - "@babel/runtime" "^7.7.2" - "@jimp/custom" "^0.16.1" - "@jimp/plugins" "^0.16.1" - "@jimp/types" "^0.16.1" - regenerator-runtime "^0.13.3" - -jpeg-js@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.2.tgz#8b345b1ae4abde64c2da2fe67ea216a114ac279d" - integrity sha512-+az2gi/hvex7eLTMTlbRLOhH6P6WFdk2ITI8HJsaH2VqYO0I594zXSYEP+tf4FW+8Cy68ScDXoAsQdyQanv3sw== - -jquery@*, jquery@^3.5.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" - integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== - -js-beautify@^1.6.12, js-beautify@^1.6.14: - version "1.11.0" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.11.0.tgz#afb873dc47d58986360093dcb69951e8bcd5ded2" - integrity sha512-a26B+Cx7USQGSWnz9YxgJNMmML/QG2nqIaL7VVYPCXbqiKz8PN0waSNvroMtvAK6tY7g/wPdNWGEP+JTNIBr6A== - dependencies: - config-chain "^1.1.12" - editorconfig "^0.15.3" - glob "^7.1.3" - mkdirp "~1.0.3" - nopt "^4.0.3" - -js-levenshtein@^1.1.3: - version "1.1.6" - resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" - integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== - -js-message@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/js-message/-/js-message-1.0.5.tgz#2300d24b1af08e89dd095bc1a4c9c9cfcb892d15" - integrity sha1-IwDSSxrwjondCVvBpMnJz8uJLRU= - -js-queue@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/js-queue/-/js-queue-2.0.0.tgz#362213cf860f468f0125fc6c96abc1742531f948" - integrity sha1-NiITz4YPRo8BJfxslqvBdCUx+Ug= - dependencies: - easy-stack "^1.0.0" - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - -js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.7.0, js-yaml@^3.9.1: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -jsdom@^11.5.1: - version "11.12.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" - integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== - dependencies: - abab "^2.0.0" - acorn "^5.5.3" - acorn-globals "^4.1.0" - array-equal "^1.0.0" - cssom ">= 0.3.2 < 0.4.0" - cssstyle "^1.0.0" - data-urls "^1.0.0" - domexception "^1.0.1" - escodegen "^1.9.1" - html-encoding-sniffer "^1.0.2" - left-pad "^1.3.0" - nwsapi "^2.0.7" - parse5 "4.0.0" - pn "^1.1.0" - request "^2.87.0" - request-promise-native "^1.0.5" - sax "^1.2.4" - symbol-tree "^3.2.2" - tough-cookie "^2.3.4" - w3c-hr-time "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-encoding "^1.0.3" - whatwg-mimetype "^2.1.0" - whatwg-url "^6.4.1" - ws "^5.2.0" - xml-name-validator "^3.0.0" - -jsesc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" - integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= - -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-schema-traverse@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" - integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -json3@^3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" - integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== - -json5@2.x, json5@^2.1.0, json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== - dependencies: - minimist "^1.2.5" - -json5@^0.5.0, json5@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" - integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= - optionalDependencies: - graceful-fs "^4.1.6" - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -jszip@^3.1.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" - integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== - dependencies: - lie "~3.3.0" - pako "~1.0.2" - readable-stream "~2.3.6" - set-immediate-shim "~1.0.1" - -kew@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" - integrity sha1-edk9LTM2PW/dKXCzNdkUGtWR15s= - -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - -killable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" - integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= - dependencies: - is-buffer "^1.1.5" - -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= - optionalDependencies: - graceful-fs "^4.1.9" - -kleur@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" - integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== - -latest-version@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - -launch-editor-middleware@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/launch-editor-middleware/-/launch-editor-middleware-2.2.1.tgz#e14b07e6c7154b0a4b86a0fd345784e45804c157" - integrity sha512-s0UO2/gEGiCgei3/2UN3SMuUj1phjQN8lcpnvgLSz26fAzNWPQ6Nf/kF5IFClnfU2ehp6LrmKdMU/beveO+2jg== - dependencies: - launch-editor "^2.2.1" - -launch-editor@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.2.1.tgz#871b5a3ee39d6680fcc26d37930b6eeda89db0ca" - integrity sha512-On+V7K2uZK6wK7x691ycSUbLD/FyKKelArkbaAMSSJU8JmqmhwN2+mnJDNINuJWSrh2L0kDk+ZQtbC/gOWUwLw== - dependencies: - chalk "^2.3.0" - shell-quote "^1.6.1" - -lazy-val@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.4.tgz#882636a7245c2cfe6e0a4e3ba6c5d68a137e5c65" - integrity sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q== - -lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= - dependencies: - readable-stream "^2.0.5" - -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - -left-pad@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" - integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== - -leven@2.1.0, leven@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" - integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -lie@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" - integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== - dependencies: - immediate "~3.0.5" - -lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= - -load-bmfont@^1.3.1, load-bmfont@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/load-bmfont/-/load-bmfont-1.4.1.tgz#c0f5f4711a1e2ccff725a7b6078087ccfcddd3e9" - integrity sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA== - dependencies: - buffer-equal "0.0.1" - mime "^1.3.4" - parse-bmfont-ascii "^1.0.3" - parse-bmfont-binary "^1.0.5" - parse-bmfont-xml "^1.1.4" - phin "^2.9.1" - xhr "^2.0.1" - xtend "^4.0.0" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -loader-fs-cache@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz#f08657646d607078be2f0a032f8bd69dd6f277d9" - integrity sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA== - dependencies: - find-cache-dir "^0.1.1" - mkdirp "^0.5.1" - -loader-runner@^2.3.1, loader-runner@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== - -loader-utils@^0.2.16: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - -loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - -lodash.defaultsdeep@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6" - integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA== - -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - -lodash.has@^4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" - integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= - -lodash.kebabcase@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" - integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY= - -lodash.mapvalues@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c" - integrity sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw= - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= - -lodash.merge@^4.6.1: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - -lodash.sortby@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" - integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= - -lodash.throttle@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= - -lodash.transform@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0" - integrity sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A= - -lodash.unescape@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" - integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= - -lodash.unset@^4.5.2: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed" - integrity sha1-Nw0dPoW3Kn4bDN8tJyEhMG8j5O0= - -lodash@^4.17.10: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10: - version "4.17.19" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" - integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== - -log-symbols@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" - -loglevel@^1.6.8: - version "1.6.8" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" - integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - -lower-case@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" - integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= - -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= - -make-dir@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" - integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== - dependencies: - pify "^3.0.0" - -make-dir@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-error@1.x: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -makeerror@1.0.x: - version "1.0.11" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" - integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= - dependencies: - tmpl "1.0.x" - -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= - -map-obj@^1.0.0, map-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= - dependencies: - object-visit "^1.0.0" - -matcher@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" - integrity sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng== - dependencies: - escape-string-regexp "^4.0.0" - -material-colors@^1.0.0: - version "1.2.6" - resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" - integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== - -math-random@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" - integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -mdn-data@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" - integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== - -mdn-data@2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" - integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - -memory-fs@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -meow@^3.1.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -merge-source-map@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" - integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== - dependencies: - source-map "^0.6.1" - -merge-stream@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" - integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= - dependencies: - readable-stream "^2.0.1" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.2.3, merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -merge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -micromatch@^2.3.11: - version "2.3.11" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" - integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= - dependencies: - arr-diff "^2.0.0" - array-unique "^0.2.1" - braces "^1.8.2" - expand-brackets "^0.1.4" - extglob "^0.3.1" - filename-regex "^2.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.1" - kind-of "^3.0.2" - normalize-path "^2.0.1" - object.omit "^2.0.0" - parse-glob "^3.0.4" - regex-cache "^0.4.2" - -micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== - -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== - dependencies: - mime-db "1.44.0" - -mime@1.6.0, mime@^1.3.4: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.0.3, mime@^2.4.4: - version "2.4.6" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - -mimic-fn@^2.0.0, mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== - -min-document@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" - integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU= - dependencies: - dom-walk "^0.1.0" - -mini-css-extract-plugin@^0.8.0: - version "0.8.2" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.2.tgz#a875e169beb27c88af77dd962771c9eedc3da161" - integrity sha512-a3Y4of27Wz+mqK3qrcd3VhYz6cU0iW5x3Sgvqzbj+XmlrSizmvu8QQMl5oMYJjgHOC4iyt+w7l4umP+dQeW3bw== - dependencies: - loader-utils "^1.1.0" - normalize-url "1.9.1" - schema-utils "^1.0.0" - webpack-sources "^1.1.0" - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - -minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - -mississippi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" - integrity sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^2.0.1" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -mkdirp@^1.0.4, mkdirp@~1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= - dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" - -mri@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" - integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - -ms@2.1.2, ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -multicast-dns-service-types@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" - integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= - -multicast-dns@^6.0.1: - version "6.2.3" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" - integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== - dependencies: - dns-packet "^1.3.1" - thunky "^1.0.2" - -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= - -mz@^2.4.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -nan@^2.12.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -no-case@^2.2.0: - version "2.3.2" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" - integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== - dependencies: - lower-case "^1.1.1" - -node-abi@^2.21.0: - version "2.30.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" - integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== - dependencies: - semver "^5.4.1" - -node-addon-api@^1.7.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" - integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== - -node-addon-api@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.1.0.tgz#f1722f1f60793584632ffffb79e12ca042c48bd0" - integrity sha512-Zz1o1BDX2VtduiAt6kgiUl8jX1Vm3NMboljFYKQJ6ee8AGfiTvM2mlZFI3xPbqjs80rCQgiVJI/DjQ/1QJ0HwA== - -node-cache@^4.1.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-4.2.1.tgz#efd8474dee4edec4138cdded580f5516500f7334" - integrity sha512-BOb67bWg2dTyax5kdef5WfU3X8xu4wPg+zHzkvls0Q/QpYycIFRLEEIdAx9Wma43DxG6Qzn4illdZoYseKWa4A== - dependencies: - clone "2.x" - lodash "^4.17.15" - -node-fetch@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" - integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== - -node-forge@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" - integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== - -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= - -node-ipc@^9.1.1: - version "9.1.1" - resolved "https://registry.yarnpkg.com/node-ipc/-/node-ipc-9.1.1.tgz#4e245ed6938e65100e595ebc5dc34b16e8dd5d69" - integrity sha512-FAyICv0sIRJxVp3GW5fzgaf9jwwRQxAKDJlmNFUL5hOy+W4X/I5AypyHoq0DXXbo9o/gt79gj++4cMr4jVWE/w== - dependencies: - event-pubsub "4.3.0" - js-message "1.0.5" - js-queue "2.0.0" - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-notifier@^5.2.1: - version "5.4.3" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" - integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== - dependencies: - growly "^1.3.0" - is-wsl "^1.1.0" - semver "^5.5.0" - shellwords "^0.1.1" - which "^1.3.0" - -node-releases@^1.1.58: - version "1.1.59" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.59.tgz#4d648330641cec704bff10f8e4fe28e453ab8e8e" - integrity sha512-H3JrdUczbdiwxN5FuJPyCHnGHIFqQ0wWxo+9j1kAXAzqNMAHlo+4I/sYYxpyK0irQ73HgdiyzD32oqQDcU2Osw== - -nopt@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" - integrity sha1-MtDkcvkf80VwHBWoMRAY07CpA3k= - -normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= - -normalize-url@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" - integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= - dependencies: - object-assign "^4.0.1" - prepend-http "^1.0.0" - query-string "^4.1.0" - sort-keys "^1.0.0" - -normalize-url@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" - integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== - -normalize-url@^4.1.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" - integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== - -npm-conf@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/npm-conf/-/npm-conf-1.1.3.tgz#256cc47bd0e218c259c4e9550bf413bc2192aff9" - integrity sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw== - dependencies: - config-chain "^1.1.11" - pify "^3.0.0" - -npm-install-package@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/npm-install-package/-/npm-install-package-2.1.0.tgz#d7efe3cfcd7ab00614b896ea53119dc9ab259125" - integrity sha1-1+/jz816sAYUuJbqUxGdyaslkSU= - -npm-run-path@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" - integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= - dependencies: - path-key "^2.0.0" - -npm-run-path@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -npmlog@^4.0.1, npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -nth-check@^1.0.2, nth-check@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -nugget@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0" - integrity sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA= - dependencies: - debug "^2.1.3" - minimist "^1.1.0" - pretty-bytes "^1.0.2" - progress-stream "^1.1.0" - request "^2.45.0" - single-line-log "^1.1.2" - throttleit "0.0.2" - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= - -nwsapi@^2.0.7: - version "2.2.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" - integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-hash@^1.1.4: - version "1.3.1" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" - integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== - -object-inspect@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - -object-is@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" - integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-keys@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336" - integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= - dependencies: - isobject "^3.0.0" - -object.assign@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - -object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" - integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -object.omit@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" - integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo= - dependencies: - for-own "^0.1.4" - is-extendable "^0.1.1" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= - dependencies: - isobject "^3.0.1" - -object.values@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" - integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -omggif@^1.0.10, omggif@^1.0.9: - version "1.0.10" - resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" - integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw== - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - -onetime@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" - integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== - dependencies: - mimic-fn "^2.1.0" - -open@^6.3.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" - integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg== - dependencies: - is-wsl "^1.1.0" - -opener@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed" - integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA== - -opn@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" - integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== - dependencies: - is-wsl "^1.1.0" - -optimist@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - -optionator@^0.8.1, optionator@^0.8.2: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -ora@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" - integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg== - dependencies: - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-spinners "^2.0.0" - log-symbols "^2.2.0" - strip-ansi "^5.2.0" - wcwidth "^1.0.1" - -original@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" - integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== - dependencies: - url-parse "^1.4.3" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - -os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - -p-finally@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" - integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= - -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== - -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - -p-limit@^1.0.0, p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-map@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-retry@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" - integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== - dependencies: - retry "^0.12.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - -pako@^1.0.5, pako@~1.0.2, pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -pako@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d" - integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg== - -parallel-transform@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" - integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== - dependencies: - cyclist "^1.0.1" - inherits "^2.0.3" - readable-stream "^2.1.5" - -param-case@2.1.x: - version "2.1.1" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" - integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= - dependencies: - no-case "^2.2.0" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" - integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== - dependencies: - asn1.js "^4.0.0" - browserify-aes "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - -parse-bmfont-ascii@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz#11ac3c3ff58f7c2020ab22769079108d4dfa0285" - integrity sha1-Eaw8P/WPfCAgqyJ2kHkQjU36AoU= - -parse-bmfont-binary@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz#d038b476d3e9dd9db1e11a0b0e53a22792b69006" - integrity sha1-0Di0dtPp3Z2x4RoLDlOiJ5K2kAY= - -parse-bmfont-xml@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/parse-bmfont-xml/-/parse-bmfont-xml-1.1.4.tgz#015319797e3e12f9e739c4d513872cd2fa35f389" - integrity sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ== - dependencies: - xml-parse-from-string "^1.0.0" - xml2js "^0.4.5" - -parse-glob@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" - integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw= - dependencies: - glob-base "^0.3.0" - is-dotfile "^1.0.0" - is-extglob "^1.0.0" - is-glob "^2.0.0" - -parse-headers@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.3.tgz#5e8e7512383d140ba02f0c7aa9f49b4399c92515" - integrity sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA== - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-json@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" - integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - lines-and-columns "^1.1.6" - -parse5-htmlparser2-tree-adapter@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz#e8c743d4e92194d5293ecde2b08be31e67461cbc" - integrity sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw== - dependencies: - parse5 "^5.1.1" - -parse5@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" - integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== - -parse5@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" - integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.1, path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= - -path-key@^2.0.0, path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.5, path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -phantomjs-prebuilt@^2.1.14: - version "2.1.16" - resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" - integrity sha1-79ISpKOWbTZHaE6ouniFSb4q7+8= - dependencies: - es6-promise "^4.0.3" - extract-zip "^1.6.5" - fs-extra "^1.0.0" - hasha "^2.2.0" - kew "^0.7.0" - progress "^1.1.8" - request "^2.81.0" - request-progress "^2.0.1" - which "^1.2.10" - -phin@^2.9.1: - version "2.9.3" - resolved "https://registry.yarnpkg.com/phin/-/phin-2.9.3.tgz#f9b6ac10a035636fb65dfc576aaaa17b8743125c" - integrity sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA== - -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pixelmatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854" - integrity sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ= - dependencies: - pngjs "^3.0.0" - -pkg-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" - integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q= - dependencies: - find-up "^1.0.0" - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" - integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= - dependencies: - find-up "^2.1.0" - -pluralize@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" - integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== - -pn@^1.0.0, pn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== - -pngjs@^3.0.0, pngjs@^3.3.3: - version "3.4.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" - integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== - -pngjs@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821" - integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg== - -portfinder@^1.0.16, portfinder@^1.0.20, portfinder@^1.0.26: - version "1.0.27" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.27.tgz#a41333c116b5e5f3d380f9745ac2f35084c4c758" - integrity sha512-bJ3U3MThKnyJ9Dx1Idtm5pQmxXqw08+XOHhi/Lie8OF1OlhVaBFhsntAIhkZYjfDcCzszSr0w1yCbccThhzgxQ== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.1" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= - -postcss-calc@^7.0.1: - version "7.0.2" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.2.tgz#504efcd008ca0273120568b0792b16cdcde8aac1" - integrity sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ== - dependencies: - postcss "^7.0.27" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.2" - -postcss-colormin@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" - integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== - dependencies: - browserslist "^4.0.0" - color "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-convert-values@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" - integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-discard-comments@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" - integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== - dependencies: - postcss "^7.0.0" - -postcss-discard-duplicates@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" - integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== - dependencies: - postcss "^7.0.0" - -postcss-discard-empty@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" - integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== - dependencies: - postcss "^7.0.0" - -postcss-discard-overridden@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" - integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== - dependencies: - postcss "^7.0.0" - -postcss-load-config@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003" - integrity sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q== - dependencies: - cosmiconfig "^5.0.0" - import-cwd "^2.0.0" - -postcss-loader@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" - integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== - dependencies: - loader-utils "^1.1.0" - postcss "^7.0.0" - postcss-load-config "^2.0.0" - schema-utils "^1.0.0" - -postcss-merge-longhand@^4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" - integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== - dependencies: - css-color-names "0.0.4" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - stylehacks "^4.0.0" - -postcss-merge-rules@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" - integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== - dependencies: - browserslist "^4.0.0" - caniuse-api "^3.0.0" - cssnano-util-same-parent "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - vendors "^1.0.0" - -postcss-minify-font-values@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" - integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-minify-gradients@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" - integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== - dependencies: - cssnano-util-get-arguments "^4.0.0" - is-color-stop "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-minify-params@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" - integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== - dependencies: - alphanum-sort "^1.0.0" - browserslist "^4.0.0" - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - uniqs "^2.0.0" - -postcss-minify-selectors@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" - integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== - dependencies: - alphanum-sort "^1.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - -postcss-modules-extract-imports@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz#dc87e34148ec7eab5f791f7cd5849833375b741a" - integrity sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw== - dependencies: - postcss "^6.0.1" - -postcss-modules-local-by-default@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" - integrity sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk= - dependencies: - css-selector-tokenizer "^0.7.0" - postcss "^6.0.1" - -postcss-modules-scope@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" - integrity sha1-1upkmUx5+XtipytCb75gVqGUu5A= - dependencies: - css-selector-tokenizer "^0.7.0" - postcss "^6.0.1" - -postcss-modules-values@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" - integrity sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA= - dependencies: - icss-replace-symbols "^1.1.0" - postcss "^6.0.1" - -postcss-normalize-charset@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" - integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== - dependencies: - postcss "^7.0.0" - -postcss-normalize-display-values@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" - integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== - dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-positions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" - integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== - dependencies: - cssnano-util-get-arguments "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-repeat-style@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" - integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== - dependencies: - cssnano-util-get-arguments "^4.0.0" - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-string@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" - integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== - dependencies: - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-timing-functions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" - integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== - dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-unicode@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" - integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== - dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-url@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" - integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== - dependencies: - is-absolute-url "^2.0.0" - normalize-url "^3.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-whitespace@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" - integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-ordered-values@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" - integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== - dependencies: - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-reduce-initial@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" - integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== - dependencies: - browserslist "^4.0.0" - caniuse-api "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" - -postcss-reduce-transforms@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" - integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== - dependencies: - cssnano-util-get-match "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-selector-parser@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" - integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== - dependencies: - dot-prop "^5.2.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c" - integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg== - dependencies: - cssesc "^3.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-svgo@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258" - integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw== - dependencies: - is-svg "^3.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - svgo "^1.0.0" - -postcss-unique-selectors@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" - integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== - dependencies: - alphanum-sort "^1.0.0" - postcss "^7.0.0" - uniqs "^2.0.0" - -postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - -postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== - -postcss@^6.0.1, postcss@^6.0.23: - version "6.0.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.32: - version "7.0.32" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" - integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -prebuild-install@^6.1.4: - version "6.1.4" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" - integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== - dependencies: - detect-libc "^1.0.3" - expand-template "^2.0.3" - github-from-package "0.0.0" - minimist "^1.2.3" - mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" - node-abi "^2.21.0" - npmlog "^4.0.1" - pump "^3.0.0" - rc "^1.2.7" - simple-get "^3.0.3" - tar-fs "^2.0.0" - tunnel-agent "^0.6.0" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prepend-http@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= - -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= - -preserve@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" - integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= - -prettier-linter-helpers@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" - integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== - dependencies: - fast-diff "^1.1.2" - -prettier@^1.15.2, prettier@^1.18.2: - version "1.19.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== - -pretty-bytes@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" - integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ= - dependencies: - get-stdin "^4.0.1" - meow "^3.1.0" - -pretty-error@^2.0.2: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" - integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= - dependencies: - renderkid "^2.0.1" - utila "~0.4" - -pretty-format@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" - integrity sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw== - dependencies: - ansi-regex "^3.0.0" - ansi-styles "^3.2.0" - -pretty@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pretty/-/pretty-2.0.0.tgz#adbc7960b7bbfe289a557dc5f737619a220d06a5" - integrity sha1-rbx5YLe7/iiaVX3F9zdhmiINBqU= - dependencies: - condense-newlines "^0.2.1" - extend-shallow "^2.0.1" - js-beautify "^1.6.12" - -private@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress-stream@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" - integrity sha1-LNPP6jO6OonJwSHsM0er6asSX3c= - dependencies: - speedometer "~0.1.2" - through2 "~0.2.3" - -progress@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" - integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= - -progress@^2.0.0, progress@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= - -prompts@^0.1.9: - version "0.1.14" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2" - integrity sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w== - dependencies: - kleur "^2.0.1" - sisteransi "^0.1.1" - -prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: - version "15.7.2" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" - integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.8.1" - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= - -proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== - dependencies: - forwarded "~0.1.2" - ipaddr.js "1.9.1" - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -psl@^1.1.28: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^2.0.0, pump@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.3, pumpify@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -q@^1.1.2, q@~1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= - -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -query-string@^4.1.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" - integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= - dependencies: - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - -querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== - -queue-microtask@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" - integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg== - -randomatic@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" - integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== - dependencies: - is-number "^4.0.0" - kind-of "^6.0.0" - math-random "^1.0.1" - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - -rc@^1.2.1, rc@^1.2.7, rc@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-dom@^16.0.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" - integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" - -react-interactive@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/react-interactive/-/react-interactive-0.8.3.tgz#c3c6c3f99b2e5efc20a9ccc54504cf2c03984d5b" - integrity sha512-mmRvA9aKP7zu9kVfP1AX1egX8tFlnE3DDXq92z0JTZezfOpmeQBzr77O1+mTV54OOmn+M2t6c5kFD5VnuFoM7A== - dependencies: - detect-it "^3.0.3" - object-assign "^4.1.1" - prop-types "^15.6.0" - -react-is@^16.8.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-router-dom@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" - integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== - dependencies: - history "^4.7.2" - invariant "^2.2.4" - loose-envify "^1.3.1" - prop-types "^15.6.1" - react-router "^4.3.1" - warning "^4.0.1" - -react-router@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" - integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== - dependencies: - history "^4.7.2" - hoist-non-react-statics "^2.5.0" - invariant "^2.2.4" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.1" - warning "^4.0.1" - -react@^16.0.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" - integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - -read-config-file@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/read-config-file/-/read-config-file-5.0.0.tgz#1487c983fae9c1b672d3acda5cac899a2d451f02" - integrity sha512-jIKUu+C84bfnKxyJ5j30CxCqgXWYjZLXuVE/NYlMEpeni+dhESgAeZOZd0JZbg1xTkMmnCdxksDoarkOyfEsOg== - dependencies: - dotenv "^8.0.0" - dotenv-expand "^5.1.0" - fs-extra "^8.1.0" - js-yaml "^3.13.1" - json5 "^2.1.0" - lazy-val "^1.0.4" - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -read-pkg@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@~1.1.9: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" - integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== - dependencies: - picomatch "^2.2.1" - -realpath-native@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" - integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== - dependencies: - util.promisify "^1.0.0" - -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== - dependencies: - regenerate "^1.4.0" - -regenerate@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f" - integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A== - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - -regenerator-runtime@^0.13.3: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== - -regenerator-runtime@^0.13.4: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== - -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== - dependencies: - "@babel/runtime" "^7.8.4" - -regex-cache@^0.4.2: - version "0.4.4" - resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" - integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== - dependencies: - is-equal-shallow "^0.1.3" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexp.prototype.flags@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -regexpp@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" - integrity sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw== - -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -regexpp@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== - -regexpu-core@^4.6.0, regexpu-core@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938" - integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ== - dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" - -registry-auth-token@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.0.tgz#1d37dffda72bbecd0f581e4715540213a65eb7da" - integrity sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w== - dependencies: - rc "^1.2.8" - -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - -regjsgen@^0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" - integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== - -regjsparser@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" - integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== - dependencies: - jsesc "~0.5.0" - -relateurl@0.2.x: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= - -renderkid@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.3.tgz#380179c2ff5ae1365c522bf2fcfcff01c5b74149" - integrity sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA== - dependencies: - css-select "^1.1.0" - dom-converter "^0.2" - htmlparser2 "^3.3.0" - strip-ansi "^3.0.0" - utila "^0.4.0" - -repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== - -repeat-string@^1.5.2, repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= - -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - -request-progress@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" - integrity sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg= - dependencies: - throttleit "^1.0.0" - -request-promise-core@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" - integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== - dependencies: - lodash "^4.17.15" - -request-promise-native@^1.0.5, request-promise-native@^1.0.7: - version "1.0.8" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" - integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== - dependencies: - request-promise-core "1.1.3" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.45.0, request@^2.81.0, request@^2.83.0, request@^2.87.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -require-uncached@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" - integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= - dependencies: - caller-path "^0.1.0" - resolve-from "^1.0.0" - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -reselect@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" - integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= - dependencies: - resolve-from "^3.0.0" - -resolve-from@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" - integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= - -resolve@1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= - -resolve@1.x, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= - dependencies: - lowercase-keys "^1.0.0" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgb2hex@^0.1.9: - version "0.1.10" - resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.1.10.tgz#4fdd432665273e2d5900434940ceba0a04c8a8a8" - integrity sha512-vKz+kzolWbL3rke/xeTE2+6vHmZnNxGyDnaVW4OckntAIcc7DcZzWkQSfxMDwqHS8vhgySnIFyBUH7lIk6PxvQ== - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= - -rimraf@2.6.3, rimraf@~2.6.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -roarr@^2.15.3: - version "2.15.4" - resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" - integrity sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A== - dependencies: - boolean "^3.0.1" - detect-node "^2.0.4" - globalthis "^1.0.1" - json-stringify-safe "^5.0.1" - semver-compare "^1.0.0" - sprintf-js "^1.1.2" - -rsvp@^3.3.3: - version "3.6.2" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" - integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== - -run-async@^2.2.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= - dependencies: - aproba "^1.1.1" - -rx-lite-aggregates@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be" - integrity sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74= - dependencies: - rx-lite "*" - -rx-lite@*, rx-lite@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" - integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= - -rxjs@^6.4.0: - version "6.6.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84" - integrity sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg== - dependencies: - tslib "^1.9.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= - dependencies: - ret "~0.1.10" - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sane@^2.0.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" - integrity sha1-tNwYYcIbQn6SlQej51HiosuKs/o= - dependencies: - anymatch "^2.0.0" - capture-exit "^1.2.0" - exec-sh "^0.2.0" - fb-watchman "^2.0.0" - micromatch "^3.1.4" - minimist "^1.1.1" - walker "~1.0.5" - watch "~0.18.0" - optionalDependencies: - fsevents "^1.2.3" - -sanitize-filename@^1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" - integrity sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg== - dependencies: - truncate-utf8-bytes "^1.0.0" - -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -schema-utils@^2.6.5: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= - -selfsigned@^1.10.7: - version "1.10.7" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b" - integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA== - dependencies: - node-forge "0.9.0" - -semver-compare@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" - integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= - -semver-diff@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" - integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= - dependencies: - semver "^5.0.3" - -"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.0.3, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" - integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== - -semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.2.1, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.7.2" - mime "1.6.0" - ms "2.1.1" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - -serialize-error@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" - integrity sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw== - dependencies: - type-fest "^0.13.1" - -serialize-javascript@^1.4.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" - integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== - -serialize-javascript@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" - integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== - dependencies: - randombytes "^2.1.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - -set-blocking@^2.0.0, set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -set-immediate-shim@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" - integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -sharp@^0.29.1: - version "0.29.1" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.1.tgz#f60b50f24f399464a24187c86bd2da41aae50b85" - integrity sha512-DpgdAny9TuS+oWCQ7MRS8XyY9x6q1+yW3a5wNx0J3HrGuB/Jot/8WcT+lElHY9iJu2pwtegSGxqMaqFiMhs4rQ== - dependencies: - color "^4.0.1" - detect-libc "^1.0.3" - node-addon-api "^4.1.0" - prebuild-install "^6.1.4" - semver "^7.3.5" - simple-get "^3.1.0" - tar-fs "^2.1.1" - tunnel-agent "^0.6.0" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-loader@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/shebang-loader/-/shebang-loader-0.0.1.tgz#a4000495d44cceefbec63435e7b1698569fa52ec" - integrity sha1-pAAEldRMzu++xjQ157FphWn6Uuw= - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== - -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== - -sigmund@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= - -signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - -simple-get@^3.0.3, simple-get@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" - integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== - dependencies: - decompress-response "^4.2.0" - once "^1.3.1" - simple-concat "^1.0.0" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= - dependencies: - is-arrayish "^0.3.1" - -single-line-log@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364" - integrity sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q= - dependencies: - string-width "^1.0.1" - -sisteransi@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" - integrity sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g== - -slash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" - integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= - -slash@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" - integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slice-ansi@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" - integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg== - dependencies: - is-fullwidth-code-point "^2.0.0" - -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -sockjs-client@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" - integrity sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g== - dependencies: - debug "^3.2.5" - eventsource "^1.0.7" - faye-websocket "~0.11.1" - inherits "^2.0.3" - json3 "^3.3.2" - url-parse "^1.4.3" - -sockjs@0.3.20: - version "0.3.20" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855" - integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA== - dependencies: - faye-websocket "^0.10.0" - uuid "^3.4.0" - websocket-driver "0.6.5" - -sort-keys@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" - integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= - dependencies: - is-plain-obj "^1.0.0" - -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@^0.4.15: - version "0.4.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" - integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== - dependencies: - source-map "^0.5.6" - -source-map-support@^0.5.13, source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.19" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= - -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -spectron@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/spectron/-/spectron-7.0.0.tgz#2a16ea2b2fbe6149f5fa9fa1a2be601f5878e00c" - integrity sha512-l6EqXNJLLjbHFr4s2tky0hQU7Ql8UzNsAJm6CiDvX1eGOPiRVJBf2lqZWHGPayZQ7auxdhqAhnHceJJkokDiPQ== - dependencies: - dev-null "^0.1.1" - electron-chromedriver "^5.0.1" - request "^2.87.0" - split "^1.0.0" - webdriverio "^4.13.0" - -speedometer@~0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d" - integrity sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0= - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -split2@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/split2/-/split2-3.1.1.tgz#c51f18f3e06a8c4469aaab487687d8d956160bb6" - integrity sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q== - dependencies: - readable-stream "^3.0.0" - -split@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - -sprintf-js@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" - integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -ssri@^5.2.4: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" - integrity sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ== - dependencies: - safe-buffer "^5.1.1" - -ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== - dependencies: - figgy-pudding "^3.5.1" - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -stack-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" - integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== - -stackframe@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303" - integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA== - -stat-mode@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.3.0.tgz#69283b081f851582b328d2a4ace5f591ce52f54b" - integrity sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng== - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= - -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-each@^1.1.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" - integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== - dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" - integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= - -string-length@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" - integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= - dependencies: - astral-regex "^1.0.0" - strip-ansi "^4.0.0" - -string-width@^1.0.1, string-width@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string.prototype.padend@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz#dc08f57a8010dc5c153550318f67e13adbb72ac3" - integrity sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -string.prototype.padstart@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.padstart/-/string.prototype.padstart-3.1.0.tgz#b47c087540d0710be5a49375751a0a627bd4ff90" - integrity sha512-envqZvUp2JItI+OeQ5UAh1ihbAV5G/2bixTojvlIa090GGqF+NQRxbWb2nv9fTGrZABv6+pE6jXoAZhhS2k4Hw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-bom@3.0.0, strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-eof@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" - integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= - -strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -stylehacks@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" - integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== - dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - -sumchecker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-2.0.2.tgz#0f42c10e5d05da5d42eea3e56c3399a37d6c5b3e" - integrity sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4= - dependencies: - debug "^2.2.0" - -sumchecker@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-3.0.1.tgz#6377e996795abb0b6d348e9b3e1dfb24345a8e42" - integrity sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg== - dependencies: - debug "^4.1.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^3.1.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= - dependencies: - has-flag "^1.0.0" - -supports-color@^5.3.0, supports-color@^5.4.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== - dependencies: - has-flag "^4.0.0" - -supports-color@~5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.0.1.tgz#1c5331f22250c84202805b2f17adf16699f3a39a" - integrity sha512-7FQGOlSQ+AQxBNXJpVDj8efTA/FtyB5wcNE1omXXJ0cq6jm1jjDwuROlYDbnzHqdNPqliWFhcioCWSyav+xBnA== - dependencies: - has-flag "^2.0.0" - -svg-tags@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" - integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= - -svg2png@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/svg2png/-/svg2png-4.1.1.tgz#6b9e0398aa418778b6436e127a2fb7f00d499c28" - integrity sha1-a54DmKpBh3i2Q24Sei+38A1JnCg= - dependencies: - file-url "^2.0.0" - phantomjs-prebuilt "^2.1.14" - pn "^1.0.0" - yargs "^6.5.0" - -svgo@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" - integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== - dependencies: - chalk "^2.4.1" - coa "^2.0.2" - css-select "^2.0.0" - css-select-base-adapter "^0.1.1" - css-tree "1.0.0-alpha.37" - csso "^4.0.2" - js-yaml "^3.13.1" - mkdirp "~0.5.1" - object.values "^1.1.0" - sax "~1.2.4" - stable "^0.1.8" - unquote "~1.1.1" - util.promisify "~1.0.0" - -symbol-tree@^3.2.2: - version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" - integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== - -table@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" - integrity sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA== - dependencies: - ajv "^5.2.3" - ajv-keywords "^2.1.0" - chalk "^2.1.0" - lodash "^4.17.4" - slice-ansi "1.0.0" - string-width "^2.1.1" - -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - -tapable@^1.0.0, tapable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tar-fs@^2.0.0, tar-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" - integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== - dependencies: - bl "^1.0.0" - buffer-alloc "^1.2.0" - end-of-stream "^1.0.0" - fs-constants "^1.0.0" - readable-stream "^2.3.0" - to-buffer "^1.1.1" - xtend "^4.0.0" - -tar-stream@^2.1.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -temp-file@^3.3.4: - version "3.3.7" - resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.3.7.tgz#686885d635f872748e384e871855958470aeb18a" - integrity sha512-9tBJKt7GZAQt/Rg0QzVWA8Am8c1EFl+CAv04/aBVqlx5oyfQ508sFIABshQ0xbZu6mBrFLWIUXO/bbLYghW70g== - dependencies: - async-exit-hook "^2.0.1" - fs-extra "^8.1.0" - -term-size@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" - integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= - dependencies: - execa "^0.7.0" - -terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.2.3, terser-webpack-plugin@^1.4.3: - version "1.4.4" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz#2c63544347324baafa9a56baaddf1634c8abfc2f" - integrity sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA== - dependencies: - cacache "^12.0.2" - find-cache-dir "^2.1.0" - is-wsl "^1.1.0" - schema-utils "^1.0.0" - serialize-javascript "^3.1.0" - source-map "^0.6.1" - terser "^4.1.2" - webpack-sources "^1.4.0" - worker-farm "^1.7.0" - -terser@^4.1.2: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -test-exclude@^4.2.1: - version "4.2.3" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" - integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA== - dependencies: - arrify "^1.0.1" - micromatch "^2.3.11" - object-assign "^4.1.0" - read-pkg-up "^1.0.1" - require-main-filename "^1.0.1" - -text-table@^0.2.0, text-table@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -thread-loader@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/thread-loader/-/thread-loader-2.1.3.tgz#cbd2c139fc2b2de6e9d28f62286ab770c1acbdda" - integrity sha512-wNrVKH2Lcf8ZrWxDF/khdlLlsTMczdcwPA9VEK4c2exlEPynYWxi9op3nPTo5lAnDIkE0rQEB3VBP+4Zncc9Hg== - dependencies: - loader-runner "^2.3.1" - loader-utils "^1.1.0" - neo-async "^2.6.0" - -"three-orbitcontrols-ts@git+https://git@github.com/nicolaspanel/three-orbitcontrols-ts.git": - version "0.1.2" - resolved "git+https://git@github.com/nicolaspanel/three-orbitcontrols-ts.git#b5b2685a88b880822c62275f2a76bdaa3954f76c" - dependencies: - three "^0.94.0" - -three@*, three@^0.118.3: - version "0.118.3" - resolved "https://registry.yarnpkg.com/three/-/three-0.118.3.tgz#c0bf8c10a68155478f12f4ccac2ff979526a4a0a" - integrity sha512-ijECXrNzDkHieoeh2H69kgawTGH8DiamhR4uBN8jEM7VHSKvfTdEvOoHsA8Aq7dh7PHAxhlqBsN5arBI3KixSw== - -three@^0.94.0: - version "0.94.0" - resolved "https://registry.yarnpkg.com/three/-/three-0.94.0.tgz#4ce6db7f2bfbf79c2d73444aa6e3cfc08a32d762" - integrity sha1-TObbfyv795wtc0RKpuPPwIoy12I= - -throat@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" - integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= - -throttleit@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf" - integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8= - -throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" - integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= - -through2-filter@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" - integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== - dependencies: - through2 "~2.0.0" - xtend "~4.0.0" - -through2-map@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/through2-map/-/through2-map-3.0.0.tgz#a6c3026ce63b4898a997d540506b66ffd970f271" - integrity sha1-psMCbOY7SJipl9VAUGtm/9lw8nE= - dependencies: - through2 "~2.0.0" - xtend "^4.0.0" - -through2@^2.0.0, through2@~2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -through2@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f" - integrity sha1-6zKE2k6jEbbMis42U3SKUqvyWj8= - dependencies: - readable-stream "~1.1.9" - xtend "~2.1.1" - -through@2, through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -timers-browserify@^2.0.4: - version "2.0.11" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" - integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== - dependencies: - setimmediate "^1.0.4" - -timm@^1.6.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/timm/-/timm-1.7.1.tgz#96bab60c7d45b5a10a8a4d0f0117c6b7e5aff76f" - integrity sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw== - -timsort@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" - integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= - -tiny-invariant@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" - integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== - -tiny-warning@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -tinycolor2@^1.1.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" - integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= - -tinycolor2@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" - integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" - integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= - -to-buffer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" - integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== - -to-fast-properties@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" - integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= - dependencies: - kind-of "^3.0.2" - -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - -toposort@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" - integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= - -tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tr46@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" - integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= - dependencies: - punycode "^2.1.0" - -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= - -truncate-utf8-bytes@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz#405923909592d56f78a5818434b0b78489ca5f2b" - integrity sha1-QFkjkJWS1W94pYGENLC3hInKXys= - dependencies: - utf8-byte-length "^1.0.1" - -tryer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" - integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== - -ts-jest@^23.0.0: - version "23.10.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-23.10.5.tgz#cdb550df4466a30489bf70ba867615799f388dd5" - integrity sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A== - dependencies: - bs-logger "0.x" - buffer-from "1.x" - fast-json-stable-stringify "2.x" - json5 "2.x" - make-error "1.x" - mkdirp "0.x" - resolve "1.x" - semver "^5.5" - yargs-parser "10.x" - -ts-loader@^5.3.3: - version "5.4.5" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-5.4.5.tgz#a0c1f034b017a9344cef0961bfd97cc192492b8b" - integrity sha512-XYsjfnRQCBum9AMRZpk2rTYSVpdZBpZK+kDh0TeT3kxmQNBDVIeUjdPjY5RZry4eIAb8XHc4gYSUiUWPYvzSRw== - dependencies: - chalk "^2.3.0" - enhanced-resolve "^4.0.0" - loader-utils "^1.0.2" - micromatch "^3.1.4" - semver "^5.0.1" - -tsconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" - integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== - dependencies: - "@types/strip-bom" "^3.0.0" - "@types/strip-json-comments" "0.0.30" - strip-bom "^3.0.0" - strip-json-comments "^2.0.0" - -tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== - -tslib@^1.9.3: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" - integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== - -tslib@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -tslint@^5.15.0: - version "5.20.1" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" - integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== - dependencies: - "@babel/code-frame" "^7.0.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^4.0.1" - glob "^7.1.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - mkdirp "^0.5.1" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.8.0" - tsutils "^2.29.0" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== - dependencies: - tslib "^1.8.1" - -tsutils@^3.17.1, tsutils@^3.7.0: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== - dependencies: - tslib "^1.8.1" - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tunnel@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" - integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -type-fest@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" - integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== - -type-fest@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" - integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== - -type-fest@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-is@~1.6.17, type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - -typeface-open-sans@^0.0.75: - version "0.0.75" - resolved "https://registry.yarnpkg.com/typeface-open-sans/-/typeface-open-sans-0.0.75.tgz#20d0c330f14c0c40463c334adbedd6005389abe4" - integrity sha512-0lLmB7pfj113OP4T78SbpSmC4OCdFQ0vUxdSXQccsSb6qF76F92iEuC/DghFgmPswTyidk8+Hwf+PS/htiJoRQ== - -typescript@^3.9.5: - version "3.9.7" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" - integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== - -uglify-js@3.4.x: - version "3.4.10" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" - integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== - dependencies: - commander "~2.19.0" - source-map "~0.6.1" - -uglify-js@^3.1.4: - version "3.10.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.0.tgz#397a7e6e31ce820bfd1cb55b804ee140c587a9e7" - integrity sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA== - -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== - -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== - dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" - -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== - -unicode-property-aliases-ecmascript@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" - integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= - -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - -unique-filename@^1.1.0, unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -unique-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" - integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= - dependencies: - crypto-random-string "^1.0.0" - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -unquote@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" - integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -unzip-crx-3@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/unzip-crx-3/-/unzip-crx-3-0.2.0.tgz#d5324147b104a8aed9ae8639c95521f6f7cda292" - integrity sha512-0+JiUq/z7faJ6oifVB5nSwt589v1KCduqIJupNVDoWSXZtWDmjDGO3RAEOvwJ07w90aoXoP4enKsR7ecMrJtWQ== - dependencies: - jszip "^3.1.0" - mkdirp "^0.5.1" - yaku "^0.16.6" - -unzip-crx@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/unzip-crx/-/unzip-crx-0.2.0.tgz#4c0baa8bdac756256754beca7843c13d7b858c18" - integrity sha1-TAuqi9rHViVnVL7KeEPBPXuFjBg= - dependencies: - jszip "^3.1.0" - mkdirp "^0.5.1" - yaku "^0.16.6" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -update-notifier@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-3.0.1.tgz#78ecb68b915e2fd1be9f767f6e298ce87b736250" - integrity sha512-grrmrB6Zb8DUiyDIaeRTBCkgISYUgETNe7NglEbVsrLWXeESnlCSP50WfRSj/GmzMPl6Uchj24S/p80nP/ZQrQ== - dependencies: - boxen "^3.0.0" - chalk "^2.0.1" - configstore "^4.0.0" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.1.0" - is-npm "^3.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.0.0" - semver-diff "^2.0.0" - xdg-basedir "^3.0.0" - -upng-js@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/upng-js/-/upng-js-2.1.0.tgz#7176e73973db361ca95d0fa14f958385db6b9dd2" - integrity sha512-d3xzZzpMP64YkjP5pr8gNyvBt7dLk/uGI67EctzDuVp4lCZyVMo0aJO6l/VDlgbInJYDY6cnClLoBp29eKWI6g== - dependencies: - pako "^1.0.5" - -upper-case@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" - integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= - -url-loader@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" - integrity sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg== - dependencies: - loader-utils "^1.1.0" - mime "^2.0.3" - schema-utils "^1.0.0" - -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= - dependencies: - prepend-http "^2.0.0" - -url-parse@^1.4.3: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url@^0.11.0, url@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -utf8-byte-length@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" - integrity sha1-9F8VDExm7uloGGUFq5P8u4rWv2E= - -utif@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/utif/-/utif-2.0.1.tgz#9e1582d9bbd20011a6588548ed3266298e711759" - integrity sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg== - dependencies: - pako "^1.0.5" - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -util.promisify@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== - dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" - -util.promisify@^1.0.0, util.promisify@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" - integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.2" - has-symbols "^1.0.1" - object.getownpropertydescriptors "^2.1.0" - -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -utila@^0.4.0, utila@~0.4: - version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -uuid@^3.3.2, uuid@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -uuid@^8.3.1: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -vendors@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -vue-class-component@^6.3.2: - version "6.3.2" - resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-6.3.2.tgz#e6037e84d1df2af3bde4f455e50ca1b9eec02be6" - integrity sha512-cH208IoM+jgZyEf/g7mnFyofwPDJTM/QvBNhYMjqGB8fCsRyTf68rH2ISw/G20tJv+5mIThQ3upKwoL4jLTr1A== - -vue-class-component@^7.0.2, vue-class-component@^7.1.0: - version "7.2.4" - resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.4.tgz#5c15cef356d7ce793aefef3b23cb5ac0df7ff62d" - integrity sha512-3LMd3yITe6wcHJ5Lyz85MSsNJrUUWeIBKnGO/YUV1ZwH2i4YmrE7t8T8eRLG45aqMlgi/BRFRyfNh3Us7k/v5Q== - -vue-cli-plugin-electron-builder@^1.4.0: - version "1.4.6" - resolved "https://registry.yarnpkg.com/vue-cli-plugin-electron-builder/-/vue-cli-plugin-electron-builder-1.4.6.tgz#5de519edc158c80ae72a67e8ae8ae983e38ecb7a" - integrity sha512-nHTkvhBdyyFQaJZaoAfGHf7JxxCGe8TTEthA+wXhA68cuuHLow5vX0EieApgaOXoYa0a3vkD/oWvEJXqToXrZA== - dependencies: - chokidar "^3.0.2" - electron-builder "^21.2.0" - execa "^1.0.0" - friendly-errors-webpack-plugin "^1.7.0" - fs-extra "^8.1.0" - lodash.merge "^4.6.1" - portfinder "^1.0.16" - pumpify "^1.5.1" - semver "^6.0.0" - shebang-loader "^0.0.1" - spectron "^7.0.0" - split2 "^3.0.0" - terser-webpack-plugin "^1.1.0" - through2-filter "^3.0.0" - through2-map "^3.0.0" - unzip-crx "^0.2.0" - webpack "^4.18.0" - webpack-chain "^5.0.0" - yargs "^14.0.0" - -vue-color@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/vue-color/-/vue-color-2.7.1.tgz#ca035109ea0010f0d60b889b97d63d37ac712f2d" - integrity sha512-u3yl46B2eEej9zfAOIRRSphX1QfeNQzMwO82EIA+aoi0AKX3o1KcfsmMzm4BFkkj2ukCxLVfQ41k7g1gSI7SlA== - dependencies: - clamp "^1.0.1" - lodash.throttle "^4.0.0" - material-colors "^1.0.0" - tinycolor2 "^1.1.2" - -vue-eslint-parser@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1" - integrity sha512-ZezcU71Owm84xVF6gfurBQUGg8WQ+WZGxgDEQu1IHFBZNx7BFZg3L1yHxrCBNNwbwFtE1GuvfJKMtb6Xuwc/Bw== - dependencies: - debug "^3.1.0" - eslint-scope "^3.7.1" - eslint-visitor-keys "^1.0.0" - espree "^3.5.2" - esquery "^1.0.0" - lodash "^4.17.4" - -vue-eslint-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1" - integrity sha512-JlHVZwBBTNVvzmifwjpZYn0oPWH2SgWv5dojlZBsrhablDu95VFD+hriB1rQGwbD+bms6g+rAFhQHk6+NyiS6g== - dependencies: - debug "^4.1.0" - eslint-scope "^4.0.0" - eslint-visitor-keys "^1.0.0" - espree "^4.1.0" - esquery "^1.0.1" - lodash "^4.17.11" - -vue-final-modal@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/vue-final-modal/-/vue-final-modal-2.4.1.tgz#7c3776295fecbd456e4f83de921379f1d980590a" - integrity sha512-tvkPaf94wzoANjNFNP8b40bXuCAk28jg0OkhRKL3oQ9VTTwhlX25gnWG5d2qyxLfnE2237E8ngwF3KNPf/bhrw== - -vue-golden-layout@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vue-golden-layout/-/vue-golden-layout-1.6.0.tgz#6a489cb2ff3e4fa1ac302036283f5af870defe6a" - integrity sha512-jBYMpynStb8srqivUepx3VYZUxvTvxeHbnQnprKwLGSkF5hWrfT3IhyQhXoKuFTTVdB/ghdDv8cdw2M6+qf+FA== - dependencies: - golden-layout "^1.5.9" - vue-class-component "^6.3.2" - vue-resize-directive "^1.1.4" - vue-storage-decorator "^1.0.6" - -vue-hot-reload-api@^2.3.0: - version "2.3.4" - resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" - integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog== - -vue-jest@^3.0.4: - version "3.0.6" - resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.6.tgz#27f79d75dcddbe6b3d8327ca1450a107b9cd6f38" - integrity sha512-VyuM8wR0vAlYCbPRY+PhIqRU5yUyBnUmwYTo4IFScs2+tiuis5VBItU0PGC8Wcx6qJwKB5jq5p7WFhabzMFMgQ== - dependencies: - babel-plugin-transform-es2015-modules-commonjs "^6.26.0" - chalk "^2.1.0" - deasync "^0.1.15" - extract-from-css "^0.4.4" - find-babel-config "^1.1.0" - js-beautify "^1.6.14" - node-cache "^4.1.1" - object-assign "^4.1.1" - source-map "^0.5.6" - tsconfig "^7.0.0" - vue-template-es2015-compiler "^1.6.0" - -vue-loader@^15.7.0: - version "15.9.3" - resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.3.tgz#0de35d9e555d3ed53969516cac5ce25531299dda" - integrity sha512-Y67VnGGgVLH5Voostx8JBZgPQTlDQeOVBLOEsjc2cXbCYBKexSKEpOA56x0YZofoDOTszrLnIShyOX1p9uCEHA== - dependencies: - "@vue/component-compiler-utils" "^3.1.0" - hash-sum "^1.0.2" - loader-utils "^1.1.0" - vue-hot-reload-api "^2.3.0" - vue-style-loader "^4.1.0" - -vue-property-decorator@^8.1.0: - version "8.5.1" - resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-8.5.1.tgz#571a91cf8d2b507f537d79bf8275af3184572fff" - integrity sha512-O6OUN2OMsYTGPvgFtXeBU3jPnX5ffQ9V4I1WfxFQ6dqz6cOUbR3Usou7kgFpfiXDvV7dJQSFcJ5yUPgOtPPm1Q== - dependencies: - vue-class-component "^7.1.0" - -vue-resize-directive@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vue-resize-directive/-/vue-resize-directive-1.2.0.tgz#4214ca8a80a425529b036ea868cad54a38f613e4" - integrity sha512-LmpFexQcl1XYyz3DQrTrq3Efgj50MKEON60nR9MMAq1D2ZtXFg1WDcy1wy1T0SeduNtIu9hos2aLTsJUplvjlQ== - dependencies: - css-element-queries "^1.0.2" - intersection-observer "^0.5.0" - lodash.debounce "^4.0.8" - -vue-router@^3.0.3: - version "3.3.4" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.3.4.tgz#4e38abc34a11c41b6c3d8244449a2e363ba6250b" - integrity sha512-SdKRBeoXUjaZ9R/8AyxsdTqkOfMcI5tWxPZOUX5Ie1BTL5rPSZ0O++pbiZCeYeythiZIdLEfkDiQPKIaWk5hDg== - -vue-storage-decorator@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/vue-storage-decorator/-/vue-storage-decorator-1.0.7.tgz#857e909cf7c593d6b9a8ae3fde20cd1f6eb2775d" - integrity sha512-ZXW84gGb3lLcW6U7KJhdua966IVZeXswOI/8yJW3/JAJD0v2irxwMDdFR1V5+tZMbsGkvdIZIvQ7wLKpXxAuUw== - -vue-style-loader@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8" - integrity sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ== - dependencies: - hash-sum "^1.0.2" - loader-utils "^1.0.2" - -vue-template-compiler@^2.6.10: - version "2.6.11" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080" - integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA== - dependencies: - de-indent "^1.0.2" - he "^1.1.0" - -vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" - integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== - -vue-toast-notification@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/vue-toast-notification/-/vue-toast-notification-0.6.2.tgz#44bb164505c310f6a721ff96921761b006ebb5cc" - integrity sha512-fIsgALA3yQAtTnZjyUg5Y1tbVrYkLNBCET/ZvvDTS7tnmaWz8L1IzkESrRs1KIiqIGRVbBbiZAzUImrVBEziDA== - -vue@^2.6.10: - version "2.6.11" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" - integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== - -vuex@^3.0.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.5.1.tgz#f1b8dcea649bc25254cf4f4358081dbf5da18b3d" - integrity sha512-w7oJzmHQs0FM9LXodfskhw9wgKBiaB+totOdb8sNzbTB2KDCEEwEs29NzBZFh/lmEK1t5tDmM1vtsO7ubG1DFw== - -w3c-hr-time@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" - integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== - dependencies: - browser-process-hrtime "^1.0.0" - -walker@~1.0.5: - version "1.0.7" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" - integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= - dependencies: - makeerror "1.0.x" - -warning@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - -watch@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" - integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY= - dependencies: - exec-sh "^0.2.0" - minimist "^1.2.0" - -watchpack-chokidar2@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" - integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== - dependencies: - chokidar "^2.1.8" - -watchpack@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa" - integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g== - dependencies: - graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.0" - watchpack-chokidar2 "^2.0.0" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= - dependencies: - defaults "^1.0.3" - -wdio-dot-reporter@~0.0.8: - version "0.0.10" - resolved "https://registry.yarnpkg.com/wdio-dot-reporter/-/wdio-dot-reporter-0.0.10.tgz#facfb7c9c5984149951f59cbc3cd0752101cf0e0" - integrity sha512-A0TCk2JdZEn3M1DSG9YYbNRcGdx/YRw19lTiRpgwzH4qqWkO/oRDZRmi3Snn4L2j54KKTfPalBhlOtc8fojVgg== - -webdriverio@^4.13.0: - version "4.14.4" - resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-4.14.4.tgz#f7a94e9a6530819796088f42b009833d83de0386" - integrity sha512-Knp2vzuzP5c5ybgLu+zTwy/l1Gh0bRP4zAr8NWcrStbuomm9Krn9oRF0rZucT6AyORpXinETzmeowFwIoo7mNA== - dependencies: - archiver "~2.1.0" - babel-runtime "^6.26.0" - css-parse "^2.0.0" - css-value "~0.0.1" - deepmerge "~2.0.1" - ejs "~2.5.6" - gaze "~1.1.2" - glob "~7.1.1" - grapheme-splitter "^1.0.2" - inquirer "~3.3.0" - json-stringify-safe "~5.0.1" - mkdirp "~0.5.1" - npm-install-package "~2.1.0" - optimist "~0.6.1" - q "~1.5.0" - request "^2.83.0" - rgb2hex "^0.1.9" - safe-buffer "~5.1.1" - supports-color "~5.0.0" - url "~0.11.0" - wdio-dot-reporter "~0.0.8" - wgxpath "~1.0.0" - -webidl-conversions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" - integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== - -webpack-bundle-analyzer@^3.3.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.8.0.tgz#ce6b3f908daf069fd1f7266f692cbb3bded9ba16" - integrity sha512-PODQhAYVEourCcOuU+NiYI7WdR8QyELZGgPvB1y2tjbUpbmcQOt5Q7jEK+ttd5se0KSBKD9SXHCEozS++Wllmw== - dependencies: - acorn "^7.1.1" - acorn-walk "^7.1.1" - bfj "^6.1.1" - chalk "^2.4.1" - commander "^2.18.0" - ejs "^2.6.1" - express "^4.16.3" - filesize "^3.6.1" - gzip-size "^5.0.0" - lodash "^4.17.15" - mkdirp "^0.5.1" - opener "^1.5.1" - ws "^6.0.0" - -webpack-chain@^4.11.0: - version "4.12.1" - resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-4.12.1.tgz#6c8439bbb2ab550952d60e1ea9319141906c02a6" - integrity sha512-BCfKo2YkDe2ByqkEWe1Rw+zko4LsyS75LVr29C6xIrxAg9JHJ4pl8kaIZ396SUSNp6b4815dRZPSTAS8LlURRQ== - dependencies: - deepmerge "^1.5.2" - javascript-stringify "^1.6.0" - -webpack-chain@^5.0.0: - version "5.2.4" - resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-5.2.4.tgz#cc3665a296a6edcda738324599103ac6e215dda3" - integrity sha512-3g0uIbzM/EWnmf52bYhB5IZeBZiw5g9vNqF4iTBEqabpxGxcv+Aj9oL4Cvr19ujOsv/HPvpRFRPLZ/aylv10Wg== - dependencies: - deepmerge "^1.5.2" - javascript-stringify "^2.0.0" - -webpack-dev-middleware@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" - integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== - dependencies: - memory-fs "^0.4.1" - mime "^2.4.4" - mkdirp "^0.5.1" - range-parser "^1.2.1" - webpack-log "^2.0.0" - -webpack-dev-server@^3.4.1: - version "3.11.0" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c" - integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg== - dependencies: - ansi-html "0.0.7" - bonjour "^3.5.0" - chokidar "^2.1.8" - compression "^1.7.4" - connect-history-api-fallback "^1.6.0" - debug "^4.1.1" - del "^4.1.1" - express "^4.17.1" - html-entities "^1.3.1" - http-proxy-middleware "0.19.1" - import-local "^2.0.0" - internal-ip "^4.3.0" - ip "^1.1.5" - is-absolute-url "^3.0.3" - killable "^1.0.1" - loglevel "^1.6.8" - opn "^5.5.0" - p-retry "^3.0.1" - portfinder "^1.0.26" - schema-utils "^1.0.0" - selfsigned "^1.10.7" - semver "^6.3.0" - serve-index "^1.9.1" - sockjs "0.3.20" - sockjs-client "1.4.0" - spdy "^4.0.2" - strip-ansi "^3.0.1" - supports-color "^6.1.0" - url "^0.11.0" - webpack-dev-middleware "^3.7.2" - webpack-log "^2.0.0" - ws "^6.2.1" - yargs "^13.3.2" - -webpack-log@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" - integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== - dependencies: - ansi-colors "^3.0.0" - uuid "^3.3.2" - -webpack-merge@^4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" - integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== - dependencies: - lodash "^4.17.15" - -webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@^4.0.0, webpack@^4.18.0: - version "4.43.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" - integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.4.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.3" - json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.6.1" - webpack-sources "^1.4.1" - -websocket-driver@0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" - integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY= - dependencies: - websocket-extensions ">=0.1.1" - -websocket-driver@>=0.5.1: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -wgxpath@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wgxpath/-/wgxpath-1.0.0.tgz#eef8a4b9d558cc495ad3a9a2b751597ecd9af690" - integrity sha1-7vikudVYzEla06mit1FZfs2a9pA= - -whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: - version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== - dependencies: - iconv-lite "0.4.24" - -whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== - -whatwg-url@^6.4.1: - version "6.5.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" - integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -whatwg-url@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" - integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.10, which@^1.2.12, which@^1.2.9, which@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - -widest-line@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" - integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== - dependencies: - string-width "^2.1.1" - -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wordwrap@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= - -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - -worker-farm@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" - integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== - dependencies: - errno "~0.1.7" - -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write-file-atomic@^2.0.0, write-file-atomic@^2.1.0: - version "2.4.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" - integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - signal-exit "^3.0.2" - -write-file-atomic@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - -write@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" - integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= - dependencies: - mkdirp "^0.5.1" - -ws@^5.2.0: - version "5.2.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" - integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== - dependencies: - async-limiter "~1.0.0" - -ws@^6.0.0, ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== - dependencies: - async-limiter "~1.0.0" - -xdg-basedir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" - integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= - -xhr@^2.0.1: - version "2.6.0" - resolved "https://registry.yarnpkg.com/xhr/-/xhr-2.6.0.tgz#b69d4395e792b4173d6b7df077f0fc5e4e2b249d" - integrity sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA== - dependencies: - global "~4.4.0" - is-function "^1.0.1" - parse-headers "^2.0.0" - xtend "^4.0.0" - -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== - -xml-parse-from-string@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz#a9029e929d3dbcded169f3c6e28238d95a5d5a28" - integrity sha1-qQKekp09vN7RafPG4oI42VpdWig= - -xml2js@^0.4.5: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - -xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -xtend@~2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" - integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os= - dependencies: - object-keys "~0.4.0" - -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - -y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yaku@^0.16.6: - version "0.16.7" - resolved "https://registry.yarnpkg.com/yaku/-/yaku-0.16.7.tgz#1d195c78aa9b5bf8479c895b9504fd4f0847984e" - integrity sha1-HRlceKqbW/hHnIlblQT9TwhHmE4= - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yargs-parser@10.x: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3" - integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" - integrity sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw= - dependencies: - camelcase "^3.0.0" - -yargs-parser@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" - integrity sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc= - dependencies: - camelcase "^4.1.0" - -yargs@^11.0.0: - version "11.1.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.1.tgz#5052efe3446a4df5ed669c995886cc0f13702766" - integrity sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw== - dependencies: - cliui "^4.0.0" - decamelize "^1.1.1" - find-up "^2.1.0" - get-caller-file "^1.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^9.0.2" - -yargs@^13.3.0, yargs@^13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yargs@^14.0.0: - version "14.2.3" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" - integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== - dependencies: - cliui "^5.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^15.0.1" - -yargs@^15.0.0: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - -yargs@^6.5.0: - version "6.6.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" - integrity sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg= - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^4.2.0" - -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - -yorkie@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yorkie/-/yorkie-2.0.0.tgz#92411912d435214e12c51c2ae1093e54b6bb83d9" - integrity sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw== - dependencies: - execa "^0.8.0" - is-ci "^1.0.10" - normalize-path "^1.0.0" - strip-indent "^2.0.0" - -zip-stream@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04" - integrity sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ= - dependencies: - archiver-utils "^1.3.0" - compress-commons "^1.2.0" - lodash "^4.8.0" - readable-stream "^2.0.0"