diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..0a67737a --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,41 @@ +name: Deploy website to GitHub Pages + +on: + push: + branches: [main] + paths: + - "website/**" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# 同一时间只跑一次部署,新的推送取消排队中的旧任务 +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload website/ as artifact + uses: actions/upload-pages-artifact@v3 + with: + path: website + + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c42f0d7..d7440004 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release (Windows) +name: Release (Windows and macOS ARM64) on: push: @@ -9,91 +9,91 @@ permissions: contents: write jobs: - build-windows-installer: + build-windows: runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Derive version from tag shell: pwsh run: | - $tag = "${{ github.ref_name }}" - $version = $tag -replace '^v', '' + $version = "${{ github.ref_name }}" -replace '^v', '' if ([string]::IsNullOrWhiteSpace($version)) { - throw "Invalid tag: '$tag' (expected like v1.0.0)" + throw "Invalid tag: '${{ github.ref_name }}' (expected like v1.0.0)" } - "TAG_NAME=$tag" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - - name: Generate release notes from commits - shell: pwsh - run: | - git fetch --force --tags + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: | + frontend/package-lock.json + desktop/package-lock.json - $tag = $env:TAG_NAME - if ([string]::IsNullOrWhiteSpace($tag)) { - throw "TAG_NAME is empty" - } + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version-file: .python-version - $repo = "${{ github.repository }}" + - name: Install uv + shell: pwsh + run: python -m pip install uv - $prev = "" - try { - $commit = (git rev-list -n 1 $tag).Trim() - if (-not [string]::IsNullOrWhiteSpace($commit)) { - $prev = (git describe --tags --abbrev=0 "$commit^" 2>$null).Trim() - } - } catch {} + - name: Install frontend dependencies + working-directory: frontend + run: npm ci - if ([string]::IsNullOrWhiteSpace($prev)) { - # Fallback: best-effort previous version tag by semver-ish sorting. - $prev = (git tag --list "v*" --sort=-v:refname | Where-Object { $_ -ne $tag } | Select-Object -First 1) - } + - name: Install desktop dependencies + working-directory: desktop + run: npm ci - $range = "" - if (-not [string]::IsNullOrWhiteSpace($prev)) { - $range = "$prev..$tag" - } + - name: Run Python tests on Windows + env: + PYTHONPATH: src + run: uv run pytest -q - $lines = @() - if (-not [string]::IsNullOrWhiteSpace($range)) { - $lines = @(git log --no-merges --pretty=format:"- %s (%h)" --reverse $range) - } else { - # First release tag / missing history: include a small recent window. - $lines = @(git log --no-merges --pretty=format:"- %s (%h)" --reverse -n 50) - } + - name: Set desktop app version + working-directory: desktop + run: npm version $env:VERSION --no-git-tag-version --allow-same-version - if (-not $lines -or $lines.Count -eq 0) { - $lines = @("- 修复了一些已知问题,提升了稳定性。") - } + - name: Build Windows installer + working-directory: desktop + run: npm run dist:win - $max = 60 - if ($lines.Count -gt $max) { - $total = $lines.Count - $lines = @($lines | Select-Object -First $max) - $lines += "- ...(共 $total 条提交,更多请查看完整变更链接)" - } + - name: Run desktop tests + working-directory: desktop + run: node --test tests/*.test.cjs - $body = @() - $body += "## 更新内容 ($tag)" - $body += "" - $body += $lines + - name: Upload Windows release files + uses: actions/upload-artifact@v4 + with: + name: release-windows + if-no-files-found: error + path: | + desktop/dist/*Setup*.exe + desktop/dist/*Setup*.exe.blockmap + desktop/dist/latest.yml - if (-not [string]::IsNullOrWhiteSpace($prev)) { - $body += "" - $body += "完整变更: https://github.com/$repo/compare/$prev...$tag" - } + build-macos-arm64: + runs-on: macos-15 + steps: + - name: Checkout + uses: actions/checkout@v4 - ($body -join "`n") | Out-File -FilePath release-notes.md -Encoding utf8 + - name: Verify Apple Silicon runner + shell: bash + run: | + test "$(uname -m)" = "arm64" + echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20" - cache: "npm" + cache: npm cache-dependency-path: | frontend/package-lock.json desktop/package-lock.json @@ -101,41 +101,114 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version-file: ".python-version" + python-version-file: .python-version + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + native/wce_integrity/target + key: macos-arm64-rust-${{ hashFiles('native/wce_integrity/Cargo.lock') }} - name: Install uv - shell: pwsh - run: | - python -m pip install --upgrade pip - python -m pip install uv + run: python -m pip install uv - - name: Install frontend deps + - name: Install frontend dependencies working-directory: frontend run: npm ci - - name: Install desktop deps + - name: Install desktop dependencies working-directory: desktop run: npm ci - - name: Set desktop app version (electron-builder) + - name: Restore macOS native helper permissions + run: chmod +x src/wechat_decrypt_tool/native/macos/universal/image_scan_helper + + - name: Set desktop app version working-directory: desktop - shell: pwsh - run: | - npm version $env:VERSION --no-git-tag-version --allow-same-version + run: npm version "$VERSION" --no-git-tag-version --allow-same-version - - name: Build Windows installer + - name: Build macOS DMG and ZIP working-directory: desktop - run: npm run dist + env: + CSC_IDENTITY_AUTO_DISCOVERY: "false" + run: npm run dist:mac + + - name: Run Python tests on macOS + env: + PYTHONPATH: src + run: uv run pytest -q - - name: Create GitHub Release and upload installer + - name: Run desktop tests + working-directory: desktop + run: node --test tests/*.test.cjs + + - name: Smoke-test packaged macOS runtime + working-directory: desktop + run: npm run smoke:mac + + - name: Upload macOS release files + uses: actions/upload-artifact@v4 + with: + name: release-macos-arm64 + if-no-files-found: error + path: | + desktop/dist/*.dmg + desktop/dist/*.zip + desktop/dist/*.blockmap + desktop/dist/latest-mac.yml + + publish-release: + needs: + - build-windows + - build-macos-arm64 + runs-on: ubuntu-latest + steps: + - name: Checkout release history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download release files + uses: actions/download-artifact@v4 + with: + path: release-assets + merge-multiple: true + + - name: Generate release notes from commits + shell: bash + run: | + set -euo pipefail + tag="${GITHUB_REF_NAME}" + previous="$(git tag --list 'v*' --sort=-v:refname | grep -Fvx "$tag" | head -n 1 || true)" + if [[ -n "$previous" ]]; then + range="$previous..$tag" + else + range="$tag" + fi + { + echo "## 更新内容 ($tag)" + echo + git log --no-merges --pretty=format:'- %s (%h)' --reverse -n 60 "$range" + if [[ -n "$previous" ]]; then + echo + echo + echo "完整变更: https://github.com/${GITHUB_REPOSITORY}/compare/${previous}...${tag}" + fi + } > release-notes.md + + - name: Create GitHub Release uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: ${{ env.TAG_NAME }} - name: ${{ env.TAG_NAME }} + tag_name: ${{ github.ref_name }} + name: ${{ github.ref_name }} body_path: release-notes.md - files: | - desktop/dist/*Setup*.exe - desktop/dist/*Setup*.exe.blockmap - desktop/dist/latest.yml + fail_on_unmatched_files: true + files: release-assets/* diff --git a/.gitignore b/.gitignore index f53f88d7..c71d8318 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Python-generated files __pycache__/ *.py[oc] +.DS_Store build/ dist/ wheels/ @@ -20,7 +21,8 @@ wheels/ .ace-tool/ pnpm-lock.yaml /tools/tmp_isaac64_compare.js -/native/wce_integrity/ +/native/wce_integrity/target/ +/native/wce_integrity/target-*/ /.claude/settings.local.json .env .env.* @@ -34,6 +36,7 @@ pnpm-lock.yaml /wx_key/ /refs/ /WeFlow/ +/WeFlow-*/ /win95/ /py_wx_key/ @@ -44,9 +47,7 @@ pnpm-lock.yaml /desktop/build/ /desktop/resources/ui/* !/desktop/resources/ui/.gitkeep -/desktop/resources/backend/*.exe -/desktop/resources/backend/native/* -/desktop/resources/backend/pyproject.toml +/desktop/resources/backend/* !/desktop/resources/backend/.gitkeep /desktop/resources/icon.ico diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000..4e3728a5 --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +2977094657 <2977094657@qq.com> Wynn-WUKO <305315954+Wynn-WUKO@users.noreply.github.com> diff --git a/README.md b/README.md index b7aeda21..d628f950 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,58 @@ > Excel 格式生成 `.xlsx` 文件;聊天记录、朋友圈和收藏会将对应格式文件与必要资源一起打包为 ZIP。 +### 本地语音转文字(Whisper) + +聊天页可以在保留原语音播放的同时,按需将语音识别为中文;HTML、JSON、TXT 和 Excel 导出也可以选择把转写文字与原语音一起写入归档。转写结果按账号、消息 ID、音频内容和模型缓存在账号目录的 `_cache/voice_transcripts.sqlite3`,重复查看或导出不会再次识别。 + +该能力是可选依赖,从源码运行时安装: + +```powershell +uv sync --no-editable --extra voice-transcription +``` + +默认配置使用 CPU、`medium` 模型和中文识别,并且不会自动联网下载模型。推荐提前下载模型并指定本地目录: + +```powershell +$env:WECHAT_TOOL_WHISPER_MODEL = 'D:\models\faster-whisper-medium' +$env:WECHAT_TOOL_WHISPER_DEVICE = 'cpu' +$env:WECHAT_TOOL_WHISPER_COMPUTE_TYPE = 'int8' +``` + +也可以明确允许首次识别时下载模型,此操作会访问 Hugging Face,模型文件较大: + +```powershell +$env:WECHAT_TOOL_WHISPER_MODEL = 'medium' +$env:WECHAT_TOOL_WHISPER_ALLOW_DOWNLOAD = '1' +``` + +使用 NVIDIA GPU 时可改为 `WECHAT_TOOL_WHISPER_DEVICE=cuda` 和 `WECHAT_TOOL_WHISPER_COMPUTE_TYPE=float16`。设置 `WECHAT_TOOL_WHISPER_ENABLED=0` 可以完全关闭该能力。隐私模式导出始终禁用语音转写,不会把语音内容写入归档。 + +应用设置页也可以选择 CPU 或 NVIDIA GPU。GPU 探测、CUDA 初始化失败自动回退 CPU,以及 RTX 5060 验收步骤见 [RTX 5060 faster-whisper CUDA 验收说明](docs/rtx5060-faster-whisper-gpu.md)。 + +## Windows / macOS 兼容性 + +| 功能 | Windows | macOS | +| --- | --- | --- | +| 数据库密钥自动获取 | 支持 | 不提供;请用支持 Mac 的同类本地工具获取后手动填写 64 位密钥 | +| 数据库解密与离线分析 | 支持 | 支持 | +| 图片密钥内存扫描 | 支持 | 支持;首次使用可能需要授予辅助功能或管理员权限 | +| WCDB 实时消息、联系人和朋友圈 | 支持 | Apple Silicon 支持;Intel Mac 暂不支持实时 WCDB | +| 聊天、朋友圈、联系人、收藏等导出 | 支持 | 支持 | +| 账号全量归档 ZIP 导入与导出 | 支持 | 支持,并可与 Windows 双向迁移 | +| 微信进程大图 Hook | 支持 | 不提供,这是 Windows 专属能力 | + +macOS 版本不会从微信进程提取数据库密钥。应用检测到 Mac 后只会显示获取方式提示;取得您本人账号的密钥并手动填写后,Apple Silicon Mac 上的数据库解密、实时消息、联系人、朋友圈、媒体、导出和迁移流程与 Windows 保持一致。 + +### 从 Windows 迁移到 Mac + +1. 在 Windows 端打开全局导出,选择“账号数据归档”,同时包含数据库和资源文件。 +2. 将生成的 `wechat_archive_*.zip` 传到 Mac,不要手动解压或修改归档内容。 +3. 在 Mac 端进入“导入数据”,选择“账号归档 ZIP”,预览账号后确认导入。 +4. 导入器会校验每个文件的 SHA-256;若本地已有同名账号,会在完整导入成功后保留旧目录备份。 + +已经由本项目解密并归档的数据,迁移后可直接离线查看;只有连接 Mac 上微信原始 WCDB 做实时读取时才需要手动填写数据库密钥。 + ## 加入群聊 也欢迎加入下方 QQ 群一起讨论。 @@ -124,13 +176,15 @@ ## 快速开始 -### 1. 下载并安装 EXE(Windows,推荐) +### 1. 下载桌面安装包(推荐) 1. 打开 Release 页面(最新版):https://github.com/LifeArchiveProject/WeChatDataAnalysis/releases/latest -2. 下载 `WeChatDataAnalysis.Setup..exe` 并运行安装 +2. Windows 下载 `Setup.exe`;Apple Silicon Mac 下载 `.dmg` 或 `mac.zip` 3. 安装完成后启动 `WeChatDataAnalysis` > 如果 Windows 弹出“未知发布者/更多信息”等提示,请确认下载来源为本仓库 Release 后再选择“仍要运行”。 +> +> macOS 首次打开若提示来源限制,请在“系统设置 → 隐私与安全性”中确认来自本仓库的应用。图片密钥扫描还可能需要授予终端或应用辅助功能权限。 ### 2. 从源码运行(开发者/高级用户) @@ -145,7 +199,7 @@ cd WeChatDataAnalysis ```bash # 使用uv (推荐) -uv sync +uv sync --no-editable ``` #### 2.3 安装前端依赖 @@ -160,9 +214,11 @@ npm install #### 启动后端API服务 ```bash # 在项目根目录 -uv run main.py +uv run --no-sync main.py ``` +`main.py` 会在任何项目包导入前优先加入当前仓库的 `src`,并在启动日志中打印 `wechat_decrypt_tool` 的代码来源。开发运行时应指向当前仓库的 `src/wechat_decrypt_tool/__init__.py`,而不是 `.venv/Lib/site-packages` 中的旧副本。Windows 中文路径下不要改用 editable 安装;Python 3.11 可能按系统代码页读取 uv 生成的 UTF-8 `.pth`,导致解释器启动失败。 + #### 启动前端开发服务器 ```bash # 在frontend目录 @@ -195,6 +251,20 @@ npm run dist 输出位置:`desktop/dist/WeChatDataAnalysis Setup .exe` +## 打包 macOS 桌面端(Apple Silicon) + +需要 Xcode Command Line Tools、Rust、Python 3.11、Node.js 20+ 和 `uv`。 + +```bash +cd desktop +npm install +npm run dist:mac +``` + +该命令会生成静态前端、编译 macOS `wce_integrity` 模块、使用 PyInstaller 打包 ARM64 后端,并生成 DMG 与 ZIP。输出位于 `desktop/dist/`。 + +内置 Mac 原生资源的来源、哈希、修改内容和许可见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。运行和打包均不依赖仓库外或根目录下的 WeFlow 副本。 + ## 安全说明 **重要提醒**: @@ -227,4 +297,3 @@ npm run dist --- **免责声明**: 本工具仅供学习研究使用,使用者需自行承担使用风险。开发者不对因使用本工具造成的任何损失负责。 - diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..fdd71b16 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,45 @@ +# Third-Party Notices + +## WeFlow macOS native resources + +This project includes selected macOS native resources derived from WeFlow: + +- Upstream project: https://github.com/hicccc77/WeFlow +- Upstream version used for this import: `5.1.0` +- Upstream license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International +- Complete license copy in distributions: `wechat_decrypt_tool/native/macos/WEFLOW_LICENSE.txt` + +The application does not load files from a WeFlow checkout at runtime. The files below are copied into this project so source builds, wheels, PyInstaller backends, and desktop packages remain self-contained. + +| Distributed file | Upstream file | Upstream SHA-256 | Distributed SHA-256 | Local changes | +| --- | --- | --- | --- | --- | +| `native/macos/universal/image_scan_helper` | `resources/key/macos/universal/image_scan_helper` | `d0044463721b393cf4812dce0c711d26a602ecb0a251b7920ef2bb57a8921829` | `d0044463721b393cf4812dce0c711d26a602ecb0a251b7920ef2bb57a8921829` | None | +| `native/macos/universal/libwx_key.dylib` | `resources/key/macos/universal/libwx_key.dylib` | `2b734f802c56c913edcd8ae33cff8ee25022acd9b4ab9c4f53d2246fe36f59cd` | `2b734f802c56c913edcd8ae33cff8ee25022acd9b4ab9c4f53d2246fe36f59cd` | None | +| `native/macos/arm64/libwcdb_api.dylib` | `resources/wcdb/macos/universal/libwcdb_api.dylib` | `9917b74e6723efea63ac64927c9f6be1ed53133a62ff2c694c68d647690cead1` | `0013c406be9894b6fbf69e7e8de7e273d603826f48e4fde53a30b0d9a7f262e7` | Install ID changed to `@loader_path/libwcdb_api.dylib`; WCDB dependency changed to `@loader_path/../universal/libWCDB.dylib`; ad-hoc re-signed | +| `native/macos/universal/libWCDB.dylib` | `resources/welive/macos/arm64/resources/macos/universal/libWCDB.dylib` | `f751ef9fe3412160584cc872b038fbb85b3b9cb1c6f0a05f99fa9e26bc6e6c34` | `e228a216d532d497ea30ebcd9764c6a37127dd2e87abc505e54b1519103de589` | Install ID changed to `@loader_path/libWCDB.dylib`; ad-hoc re-signed | + +The `libwcdb_api.dylib` C API used here is ARM64. Consequently, full WCDB realtime support on macOS is currently limited to Apple Silicon. The image scanning helper and `libWCDB.dylib` are universal binaries. + +## ffmpeg-static + +Desktop distributions include the platform-specific FFmpeg executable from `ffmpeg-static` so voice messages can be converted to browser-compatible MP3 without a separate system install. + +- Package: https://github.com/eugeneware/ffmpeg-static +- Binary builds: https://github.com/ffbinaries/ffbinaries-prebuilt/releases +- Package license: GPL-3.0-or-later +- Distributed license files: `ffmpeg/LICENSE` and `ffmpeg/ffmpeg.LICENSE` + +## Optional voice transcription runtime + +The optional local voice transcription feature uses the following Python packages. Windows standalone backends built with the `voice-transcription` extra bundle these packages and their required runtime data. + +| Package | Upstream project | License | +| --- | --- | --- | +| `faster-whisper` | https://github.com/SYSTRAN/faster-whisper | MIT | +| `CTranslate2` | https://github.com/OpenNMT/CTranslate2 | MIT | +| `PyAV` | https://github.com/PyAV-Org/PyAV | BSD-3-Clause | +| `OpenCC Python Reimplemented` | https://github.com/yichen0831/opencc-python | Apache-2.0 | +| `ONNX Runtime` | https://github.com/microsoft/onnxruntime | MIT | +| `Hugging Face tokenizers` | https://github.com/huggingface/tokenizers | Apache-2.0 | + +Whisper model weights are not included in this repository or its pull request. Users download or provide model weights separately and must follow the license and usage terms published with the selected model. diff --git a/desktop/entitlements.mac.plist b/desktop/entitlements.mac.plist new file mode 100644 index 00000000..efe3e54e --- /dev/null +++ b/desktop/entitlements.mac.plist @@ -0,0 +1,16 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.debugger + + com.apple.security.get-task-allow + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/desktop/package-lock.json b/desktop/package-lock.json index cdf66e17..b255ca42 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -8,7 +8,9 @@ "name": "wechat-data-analysis-desktop", "version": "1.18.5", "dependencies": { - "electron-updater": "^6.7.3" + "electron-updater": "^6.7.3", + "ffmpeg-static": "^5.3.0", + "koffi": "^2.15.2" }, "devDependencies": { "concurrently": "^9.2.1", @@ -18,9 +20,24 @@ "png-to-ico": "^3.0.1" } }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "license": "MIT", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@electron/asar": { "version": "3.4.1", - "resolved": "https://registry.npmmirror.com/@electron/asar/-/asar-3.4.1.tgz", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, "license": "MIT", @@ -38,14 +55,14 @@ }, "node_modules/@electron/asar/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/@electron/asar/node_modules/brace-expansion": { "version": "1.1.16", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", @@ -56,7 +73,7 @@ }, "node_modules/@electron/asar/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", @@ -69,7 +86,7 @@ }, "node_modules/@electron/fuses": { "version": "1.8.0", - "resolved": "https://registry.npmmirror.com/@electron/fuses/-/fuses-1.8.0.tgz", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", @@ -84,7 +101,7 @@ }, "node_modules/@electron/fuses/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", @@ -100,7 +117,7 @@ }, "node_modules/@electron/fuses/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -113,7 +130,7 @@ }, "node_modules/@electron/fuses/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -145,7 +162,7 @@ }, "node_modules/@electron/notarize": { "version": "2.5.0", - "resolved": "https://registry.npmmirror.com/@electron/notarize/-/notarize-2.5.0.tgz", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", @@ -160,7 +177,7 @@ }, "node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", @@ -176,7 +193,7 @@ }, "node_modules/@electron/notarize/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -189,7 +206,7 @@ }, "node_modules/@electron/notarize/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -199,7 +216,7 @@ }, "node_modules/@electron/osx-sign": { "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", "dev": true, "license": "BSD-2-Clause", @@ -221,7 +238,7 @@ }, "node_modules/@electron/osx-sign/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", @@ -236,7 +253,7 @@ }, "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { "version": "4.0.10", - "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "license": "MIT", @@ -249,7 +266,7 @@ }, "node_modules/@electron/osx-sign/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -262,7 +279,7 @@ }, "node_modules/@electron/osx-sign/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -272,7 +289,7 @@ }, "node_modules/@electron/rebuild": { "version": "4.2.0", - "resolved": "https://registry.npmmirror.com/@electron/rebuild/-/rebuild-4.2.0.tgz", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", "dev": true, "license": "MIT", @@ -293,7 +310,7 @@ }, "node_modules/@electron/universal": { "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/@electron/universal/-/universal-2.0.3.tgz", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", "dev": true, "license": "MIT", @@ -312,14 +329,14 @@ }, "node_modules/@electron/universal/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/@electron/universal/node_modules/brace-expansion": { "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.2.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", @@ -329,7 +346,7 @@ }, "node_modules/@electron/universal/node_modules/fs-extra": { "version": "11.3.6", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.6.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "dev": true, "license": "MIT", @@ -344,7 +361,7 @@ }, "node_modules/@electron/universal/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -357,7 +374,7 @@ }, "node_modules/@electron/universal/node_modules/minimatch": { "version": "9.0.9", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", @@ -373,7 +390,7 @@ }, "node_modules/@electron/universal/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -383,7 +400,7 @@ }, "node_modules/@electron/windows-sign": { "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", "dev": true, "license": "BSD-2-Clause", @@ -405,7 +422,7 @@ }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { "version": "11.3.6", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.6.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", "dev": true, "license": "MIT", @@ -422,7 +439,7 @@ }, "node_modules/@electron/windows-sign/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -437,7 +454,7 @@ }, "node_modules/@electron/windows-sign/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -456,7 +473,7 @@ }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, "license": "ISC", @@ -469,7 +486,7 @@ }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", "dev": true, "funding": [ @@ -492,7 +509,7 @@ }, "node_modules/@malept/flatpak-bundler": { "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", "dev": true, "license": "MIT", @@ -508,7 +525,7 @@ }, "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "license": "MIT", @@ -524,7 +541,7 @@ }, "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -537,7 +554,7 @@ }, "node_modules/@malept/flatpak-bundler/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -547,7 +564,7 @@ }, "node_modules/@noble/hashes": { "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-2.2.0.tgz", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", "dev": true, "license": "MIT", @@ -560,7 +577,7 @@ }, "node_modules/@peculiar/asn1-schema": { "version": "2.8.0", - "resolved": "https://registry.npmmirror.com/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", "dev": true, "license": "MIT", @@ -572,7 +589,7 @@ }, "node_modules/@peculiar/json-schema": { "version": "1.1.12", - "resolved": "https://registry.npmmirror.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", "dev": true, "license": "MIT", @@ -585,7 +602,7 @@ }, "node_modules/@peculiar/utils": { "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/@peculiar/utils/-/utils-2.0.3.tgz", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", "dev": true, "license": "MIT", @@ -595,7 +612,7 @@ }, "node_modules/@peculiar/webcrypto": { "version": "1.7.1", - "resolved": "https://registry.npmmirror.com/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", "dev": true, "license": "MIT", @@ -651,7 +668,7 @@ }, "node_modules/@types/debug": { "version": "4.1.13", - "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "dev": true, "license": "MIT", @@ -661,7 +678,7 @@ }, "node_modules/@types/fs-extra": { "version": "9.0.13", - "resolved": "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, "license": "MIT", @@ -688,7 +705,7 @@ }, "node_modules/@types/ms": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "dev": true, "license": "MIT" @@ -726,7 +743,7 @@ }, "node_modules/@xmldom/xmldom": { "version": "0.8.13", - "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", @@ -736,7 +753,7 @@ }, "node_modules/abbrev": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, "license": "ISC", @@ -746,7 +763,7 @@ }, "node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", @@ -756,7 +773,7 @@ }, "node_modules/ajv": { "version": "8.20.0", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", @@ -799,7 +816,7 @@ }, "node_modules/app-builder-lib": { "version": "26.15.3", - "resolved": "https://registry.npmmirror.com/app-builder-lib/-/app-builder-lib-26.15.3.tgz", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", "dev": true, "license": "MIT", @@ -856,7 +873,7 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/@electron/get/-/get-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", "dev": true, "license": "MIT", @@ -878,7 +895,7 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "license": "MIT", @@ -893,7 +910,7 @@ }, "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", @@ -903,7 +920,7 @@ }, "node_modules/app-builder-lib/node_modules/builder-util-runtime": { "version": "9.7.0", - "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", "dev": true, "license": "MIT", @@ -917,7 +934,7 @@ }, "node_modules/app-builder-lib/node_modules/ci-info": { "version": "4.3.1", - "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-4.3.1.tgz", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", "dev": true, "funding": [ @@ -933,7 +950,7 @@ }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", @@ -948,7 +965,7 @@ }, "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -961,7 +978,7 @@ }, "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -971,7 +988,7 @@ }, "node_modules/app-builder-lib/node_modules/isexe": { "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-3.1.5.tgz", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, "license": "BlueOak-1.0.0", @@ -981,7 +998,7 @@ }, "node_modules/app-builder-lib/node_modules/semver": { "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", @@ -994,7 +1011,7 @@ }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/which/-/which-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", @@ -1016,7 +1033,7 @@ }, "node_modules/asn1js": { "version": "3.0.10", - "resolved": "https://registry.npmmirror.com/asn1js/-/asn1js-3.0.10.tgz", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", "dev": true, "license": "BSD-3-Clause", @@ -1031,14 +1048,14 @@ }, "node_modules/async": { "version": "3.2.6", - "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, "node_modules/async-exit-hook": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true, "license": "MIT", @@ -1048,14 +1065,14 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, "license": "ISC", @@ -1065,14 +1082,14 @@ }, "node_modules/aws4": { "version": "1.13.2", - "resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.13.2.tgz", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "dev": true, "license": "MIT" }, "node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", @@ -1082,7 +1099,7 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ @@ -1103,7 +1120,7 @@ }, "node_modules/bluebird": { "version": "3.7.2", - "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true, "license": "MIT" @@ -1119,7 +1136,7 @@ }, "node_modules/brace-expansion": { "version": "5.0.7", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.7.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", @@ -1144,12 +1161,11 @@ "version": "1.1.2", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/builder-util": { "version": "26.15.3", - "resolved": "https://registry.npmmirror.com/builder-util/-/builder-util-26.15.3.tgz", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", "dev": true, "license": "MIT", @@ -1188,7 +1204,7 @@ }, "node_modules/builder-util/node_modules/builder-util-runtime": { "version": "9.7.0", - "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", "dev": true, "license": "MIT", @@ -1202,7 +1218,7 @@ }, "node_modules/builder-util/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", @@ -1217,7 +1233,7 @@ }, "node_modules/builder-util/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -1230,7 +1246,7 @@ }, "node_modules/builder-util/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -1240,7 +1256,7 @@ }, "node_modules/bytestreamjs": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "dev": true, "license": "BSD-3-Clause", @@ -1279,7 +1295,7 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", @@ -1291,6 +1307,12 @@ "node": ">= 0.4" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -1323,7 +1345,7 @@ }, "node_modules/chownr": { "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/chownr/-/chownr-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, "license": "BlueOak-1.0.0", @@ -1333,14 +1355,14 @@ }, "node_modules/chromium-pickle-js": { "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true, "license": "MIT" }, "node_modules/ci-info": { "version": "4.4.0", - "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-4.4.0.tgz", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ @@ -1404,7 +1426,7 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", @@ -1417,7 +1439,7 @@ }, "node_modules/commander": { "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-5.1.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true, "license": "MIT", @@ -1427,7 +1449,7 @@ }, "node_modules/compare-version": { "version": "0.1.2", - "resolved": "https://registry.npmmirror.com/compare-version/-/compare-version-0.1.2.tgz", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true, "license": "MIT", @@ -1437,11 +1459,26 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/concurrently": { "version": "9.2.1", "resolved": "https://registry.npmmirror.com/concurrently/-/concurrently-9.2.1.tgz", @@ -1469,14 +1506,14 @@ }, "node_modules/core-util-is": { "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, "license": "MIT" }, "node_modules/cross-dirname": { "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/cross-dirname/-/cross-dirname-0.1.0.tgz", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", @@ -1612,7 +1649,7 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", @@ -1630,7 +1667,7 @@ }, "node_modules/dir-compare": { "version": "4.2.0", - "resolved": "https://registry.npmmirror.com/dir-compare/-/dir-compare-4.2.0.tgz", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "dev": true, "license": "MIT", @@ -1641,14 +1678,14 @@ }, "node_modules/dir-compare/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/dir-compare/node_modules/brace-expansion": { "version": "1.1.16", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", @@ -1659,7 +1696,7 @@ }, "node_modules/dir-compare/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", @@ -1672,7 +1709,7 @@ }, "node_modules/dmg-builder": { "version": "26.15.3", - "resolved": "https://registry.npmmirror.com/dmg-builder/-/dmg-builder-26.15.3.tgz", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", "dev": true, "license": "MIT", @@ -1685,7 +1722,7 @@ }, "node_modules/dmg-builder/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", @@ -1700,7 +1737,7 @@ }, "node_modules/dmg-builder/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -1713,7 +1750,7 @@ }, "node_modules/dmg-builder/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -1723,7 +1760,7 @@ }, "node_modules/dotenv": { "version": "16.6.1", - "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, "license": "BSD-2-Clause", @@ -1736,7 +1773,7 @@ }, "node_modules/dotenv-expand": { "version": "11.0.7", - "resolved": "https://registry.npmmirror.com/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dev": true, "license": "BSD-2-Clause", @@ -1752,7 +1789,7 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", @@ -1767,7 +1804,7 @@ }, "node_modules/duplexer2": { "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/duplexer2/-/duplexer2-0.1.4.tgz", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", "dev": true, "license": "BSD-3-Clause", @@ -1775,9 +1812,42 @@ "readable-stream": "^2.0.2" } }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "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" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/ejs": { "version": "3.1.10", - "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "license": "Apache-2.0", @@ -1812,7 +1882,7 @@ }, "node_modules/electron-builder": { "version": "26.15.3", - "resolved": "https://registry.npmmirror.com/electron-builder/-/electron-builder-26.15.3.tgz", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", "dev": true, "license": "MIT", @@ -1838,7 +1908,7 @@ }, "node_modules/electron-builder-squirrel-windows": { "version": "26.15.3", - "resolved": "https://registry.npmmirror.com/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", "dev": true, "license": "MIT", @@ -1851,7 +1921,7 @@ }, "node_modules/electron-builder/node_modules/builder-util-runtime": { "version": "9.7.0", - "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", "dev": true, "license": "MIT", @@ -1903,7 +1973,7 @@ }, "node_modules/electron-publish": { "version": "26.15.3", - "resolved": "https://registry.npmmirror.com/electron-publish/-/electron-publish-26.15.3.tgz", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", "dev": true, "license": "MIT", @@ -1921,7 +1991,7 @@ }, "node_modules/electron-publish/node_modules/builder-util-runtime": { "version": "9.7.0", - "resolved": "https://registry.npmmirror.com/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", "dev": true, "license": "MIT", @@ -1935,7 +2005,7 @@ }, "node_modules/electron-publish/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", @@ -1950,7 +2020,7 @@ }, "node_modules/electron-publish/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -1963,7 +2033,7 @@ }, "node_modules/electron-publish/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -2036,7 +2106,7 @@ }, "node_modules/electron-winstaller": { "version": "5.4.0", - "resolved": "https://registry.npmmirror.com/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", "dev": true, "hasInstallScript": true, @@ -2058,7 +2128,7 @@ }, "node_modules/electron-winstaller/node_modules/fs-extra": { "version": "7.0.1", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-7.0.1.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", @@ -2093,7 +2163,6 @@ "version": "2.2.1", "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2101,7 +2170,7 @@ }, "node_modules/err-code": { "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/err-code/-/err-code-2.0.3.tgz", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "dev": true, "license": "MIT" @@ -2128,7 +2197,7 @@ }, "node_modules/es-object-atoms": { "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", @@ -2141,7 +2210,7 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", @@ -2189,7 +2258,7 @@ }, "node_modules/exponential-backoff": { "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "dev": true, "license": "Apache-2.0" @@ -2217,14 +2286,14 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.4", - "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.4.tgz", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ @@ -2251,7 +2320,7 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", @@ -2267,9 +2336,50 @@ } } }, + "node_modules/ffmpeg-static": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.3.0.tgz", + "integrity": "sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==", + "hasInstallScript": true, + "license": "GPL-3.0-or-later", + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ffmpeg-static/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ffmpeg-static/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/filelist": { "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", @@ -2279,14 +2389,14 @@ }, "node_modules/filelist/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/filelist/node_modules/brace-expansion": { "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.2.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", @@ -2296,7 +2406,7 @@ }, "node_modules/filelist/node_modules/minimatch": { "version": "5.1.9", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", @@ -2309,7 +2419,7 @@ }, "node_modules/form-data": { "version": "4.0.6", - "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", @@ -2341,14 +2451,14 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, "license": "ISC" }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", @@ -2368,7 +2478,7 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", @@ -2393,7 +2503,7 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", @@ -2423,9 +2533,9 @@ }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -2445,14 +2555,14 @@ }, "node_modules/glob/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.16", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.16.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", @@ -2463,7 +2573,7 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", @@ -2596,7 +2706,7 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", @@ -2609,7 +2719,7 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", @@ -2625,7 +2735,7 @@ }, "node_modules/hasown": { "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, "license": "MIT", @@ -2638,7 +2748,7 @@ }, "node_modules/hosted-git-info": { "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", @@ -2658,7 +2768,7 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", @@ -2670,6 +2780,21 @@ "node": ">= 14" } }, + "node_modules/http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "license": "MIT", + "dependencies": { + "@types/node": "^10.0.3" + } + }, + "node_modules/http-response-object/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "license": "MIT" + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -2686,7 +2811,7 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", @@ -2700,7 +2825,7 @@ }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, @@ -2714,7 +2839,6 @@ "version": "2.0.4", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/is-fullwidth-code-point": { @@ -2729,14 +2853,14 @@ }, "node_modules/isarray": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.7", - "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", "dev": true, "license": "MIT", @@ -2756,7 +2880,7 @@ }, "node_modules/jake": { "version": "10.9.4", - "resolved": "https://registry.npmmirror.com/jake/-/jake-10.9.4.tgz", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "dev": true, "license": "Apache-2.0", @@ -2774,7 +2898,7 @@ }, "node_modules/jiti": { "version": "2.7.0", - "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.7.0.tgz", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", @@ -2803,7 +2927,7 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" @@ -2818,7 +2942,7 @@ }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", @@ -2849,6 +2973,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/koffi": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", + "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz", @@ -2857,7 +2991,7 @@ }, "node_modules/lodash": { "version": "4.18.1", - "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" @@ -2887,7 +3021,7 @@ }, "node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", @@ -2914,7 +3048,7 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", @@ -2924,7 +3058,7 @@ }, "node_modules/mime": { "version": "2.6.0", - "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, "license": "MIT", @@ -2937,7 +3071,7 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", @@ -2947,7 +3081,7 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", @@ -2970,7 +3104,7 @@ }, "node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", @@ -2996,7 +3130,7 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", @@ -3006,7 +3140,7 @@ }, "node_modules/minizlib": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", @@ -3019,7 +3153,7 @@ }, "node_modules/mkdirp": { "version": "0.5.6", - "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", @@ -3039,7 +3173,7 @@ }, "node_modules/node-abi": { "version": "4.33.0", - "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-4.33.0.tgz", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.33.0.tgz", "integrity": "sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==", "dev": true, "license": "MIT", @@ -3052,7 +3186,7 @@ }, "node_modules/node-abi/node_modules/semver": { "version": "7.8.5", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", @@ -3065,7 +3199,7 @@ }, "node_modules/node-api-version": { "version": "0.2.1", - "resolved": "https://registry.npmmirror.com/node-api-version/-/node-api-version-0.2.1.tgz", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", "dev": true, "license": "MIT", @@ -3075,7 +3209,7 @@ }, "node_modules/node-api-version/node_modules/semver": { "version": "7.8.5", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", @@ -3088,7 +3222,7 @@ }, "node_modules/node-gyp": { "version": "12.4.0", - "resolved": "https://registry.npmmirror.com/node-gyp/-/node-gyp-12.4.0.tgz", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", "dev": true, "license": "MIT", @@ -3113,7 +3247,7 @@ }, "node_modules/node-gyp/node_modules/isexe": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, "license": "BlueOak-1.0.0", @@ -3123,7 +3257,7 @@ }, "node_modules/node-gyp/node_modules/semver": { "version": "7.8.5", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", @@ -3136,7 +3270,7 @@ }, "node_modules/node-gyp/node_modules/which": { "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/which/-/which-6.0.1.tgz", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", @@ -3152,14 +3286,14 @@ }, "node_modules/node-int64": { "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true, "license": "MIT" }, "node_modules/nopt": { "version": "9.0.0", - "resolved": "https://registry.npmmirror.com/nopt/-/nopt-9.0.0.tgz", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", @@ -3219,7 +3353,7 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", @@ -3233,9 +3367,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", @@ -3255,7 +3394,7 @@ }, "node_modules/pe-library": { "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/pe-library/-/pe-library-0.4.1.tgz", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", "dev": true, "license": "MIT", @@ -3277,14 +3416,14 @@ }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.5", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", @@ -3297,7 +3436,7 @@ }, "node_modules/pkijs": { "version": "3.4.0", - "resolved": "https://registry.npmmirror.com/pkijs/-/pkijs-3.4.0.tgz", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", "dev": true, "license": "BSD-3-Clause", @@ -3315,7 +3454,7 @@ }, "node_modules/pkijs/node_modules/@noble/hashes": { "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.4.0.tgz", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true, "license": "MIT", @@ -3328,7 +3467,7 @@ }, "node_modules/plist": { "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/plist/-/plist-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dev": true, "license": "MIT", @@ -3388,7 +3527,7 @@ }, "node_modules/postject": { "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmmirror.com/postject/-/postject-1.0.0-alpha.6.tgz", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", "dev": true, "license": "MIT", @@ -3406,7 +3545,7 @@ }, "node_modules/postject/node_modules/commander": { "version": "9.5.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, "license": "MIT", @@ -3418,7 +3557,7 @@ }, "node_modules/proc-log": { "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/proc-log/-/proc-log-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", @@ -3428,7 +3567,7 @@ }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, "license": "MIT" @@ -3437,7 +3576,6 @@ "version": "2.0.3", "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3445,7 +3583,7 @@ }, "node_modules/promise-retry": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "dev": true, "license": "MIT", @@ -3459,7 +3597,7 @@ }, "node_modules/proper-lockfile": { "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", "dev": true, "license": "MIT", @@ -3482,7 +3620,7 @@ }, "node_modules/pvtsutils": { "version": "1.3.6", - "resolved": "https://registry.npmmirror.com/pvtsutils/-/pvtsutils-1.3.6.tgz", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", "dev": true, "license": "MIT", @@ -3492,7 +3630,7 @@ }, "node_modules/pvutils": { "version": "1.1.5", - "resolved": "https://registry.npmmirror.com/pvutils/-/pvutils-1.1.5.tgz", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", "dev": true, "license": "MIT", @@ -3515,7 +3653,7 @@ }, "node_modules/read-binary-file-arch": { "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", "dev": true, "license": "MIT", @@ -3527,19 +3665,17 @@ } }, "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", "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" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/require-directory": { @@ -3554,7 +3690,7 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", @@ -3564,7 +3700,7 @@ }, "node_modules/resedit": { "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/resedit/-/resedit-1.7.2.tgz", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", "dev": true, "license": "MIT", @@ -3602,7 +3738,7 @@ }, "node_modules/retry": { "version": "0.12.0", - "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, "license": "MIT", @@ -3612,7 +3748,7 @@ }, "node_modules/rimraf": { "version": "2.6.3", - "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.6.3.tgz", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, @@ -3655,15 +3791,28 @@ } }, "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.4", - "resolved": "https://registry.npmmirror.com/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", "dev": true, "license": "WTFPL OR ISC", @@ -3753,7 +3902,7 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" @@ -3786,7 +3935,7 @@ }, "node_modules/source-map": { "version": "0.6.1", - "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", @@ -3796,7 +3945,7 @@ }, "node_modules/source-map-support": { "version": "0.5.21", - "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", @@ -3815,7 +3964,7 @@ }, "node_modules/stat-mode": { "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", "dev": true, "license": "MIT", @@ -3824,13 +3973,12 @@ } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { @@ -3892,7 +4040,7 @@ }, "node_modules/tar": { "version": "7.5.21", - "resolved": "https://registry.npmmirror.com/tar/-/tar-7.5.21.tgz", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.21.tgz", "integrity": "sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==", "dev": true, "license": "BlueOak-1.0.0", @@ -3909,7 +4057,7 @@ }, "node_modules/tar/node_modules/yallist": { "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-5.0.0.tgz", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, "license": "BlueOak-1.0.0", @@ -3919,7 +4067,7 @@ }, "node_modules/temp": { "version": "0.9.4", - "resolved": "https://registry.npmmirror.com/temp/-/temp-0.9.4.tgz", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", @@ -3934,7 +4082,7 @@ }, "node_modules/temp-file": { "version": "3.4.0", - "resolved": "https://registry.npmmirror.com/temp-file/-/temp-file-3.4.0.tgz", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", "dev": true, "license": "MIT", @@ -3945,7 +4093,7 @@ }, "node_modules/temp-file/node_modules/fs-extra": { "version": "10.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", @@ -3960,7 +4108,7 @@ }, "node_modules/temp-file/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -3973,7 +4121,7 @@ }, "node_modules/temp-file/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -3983,7 +4131,7 @@ }, "node_modules/tiny-async-pool": { "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", "dev": true, "license": "MIT", @@ -3993,7 +4141,7 @@ }, "node_modules/tiny-async-pool/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", @@ -4009,7 +4157,7 @@ }, "node_modules/tinyglobby": { "version": "0.2.17", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", @@ -4026,7 +4174,7 @@ }, "node_modules/tmp": { "version": "0.2.7", - "resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", @@ -4036,7 +4184,7 @@ }, "node_modules/tmp-promise": { "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/tmp-promise/-/tmp-promise-3.0.3.tgz", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", "dev": true, "license": "MIT", @@ -4056,7 +4204,7 @@ }, "node_modules/truncate-utf8-bytes": { "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, "license": "WTFPL", @@ -4085,9 +4233,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undici": { "version": "6.27.0", - "resolved": "https://registry.npmmirror.com/undici/-/undici-6.27.0.tgz", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", "dev": true, "license": "MIT", @@ -4114,7 +4268,7 @@ }, "node_modules/unzipper": { "version": "0.12.5", - "resolved": "https://registry.npmmirror.com/unzipper/-/unzipper-0.12.5.tgz", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", "dev": true, "license": "MIT", @@ -4128,7 +4282,7 @@ }, "node_modules/unzipper/node_modules/fs-extra": { "version": "11.3.1", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.1.tgz", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", "dev": true, "license": "MIT", @@ -4143,7 +4297,7 @@ }, "node_modules/unzipper/node_modules/jsonfile": { "version": "6.2.1", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", @@ -4156,7 +4310,7 @@ }, "node_modules/unzipper/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", @@ -4166,7 +4320,7 @@ }, "node_modules/utf8-byte-length": { "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", "dev": true, "license": "(WTFPL OR MIT)" @@ -4175,12 +4329,11 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/webcrypto-core": { "version": "1.9.2", - "resolved": "https://registry.npmmirror.com/webcrypto-core/-/webcrypto-core-1.9.2.tgz", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", "dev": true, "license": "MIT", @@ -4235,7 +4388,7 @@ }, "node_modules/xmlbuilder": { "version": "15.1.1", - "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "dev": true, "license": "MIT", @@ -4255,7 +4408,7 @@ }, "node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" @@ -4302,7 +4455,7 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", diff --git a/desktop/package.json b/desktop/package.json index efe3b3c3..d77ece5a 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -5,14 +5,19 @@ "main": "src/main.cjs", "scripts": { "dev": "node scripts/dev.cjs", - "dev:static": "pushd ..\\\\frontend && npm run generate && popd && cross-env ELECTRON_START_URL=http://127.0.0.1:10392 electron .", - "build:ui": "pushd ..\\\\frontend && npm run generate && popd && node scripts\\\\copy-ui.cjs", - "build:backend": "uv sync --extra build && node scripts/build-backend.cjs", + "dev:static": "npm --prefix ../frontend run generate && cross-env ELECTRON_START_URL=http://127.0.0.1:10392 electron .", + "build:ui": "npm --prefix ../frontend run generate && node scripts/copy-ui.cjs", + "build:backend": "uv sync --no-editable --extra build --extra voice-transcription && node scripts/build-backend.cjs", "build:icon": "node scripts/build-icon.cjs", - "dist": "npm run build:ui && npm run build:backend && npm run build:icon && electron-builder --win --x64 --publish never" + "smoke:mac": "node scripts/smoke-macos-package.cjs", + "dist": "npm run dist:win", + "dist:win": "npm run build:ui && npm run build:backend && npm run build:icon && electron-builder --win --x64 --publish never", + "dist:mac": "npm run build:ui && npm run build:backend && npm run build:icon && electron-builder --mac dmg zip --arm64 --publish never" }, "dependencies": { - "electron-updater": "^6.7.3" + "electron-updater": "^6.7.3", + "ffmpeg-static": "^5.3.0", + "koffi": "^2.15.2" }, "build": { "appId": "com.lifearchive.wechatdataanalysis", @@ -45,10 +50,14 @@ "graceful-fs/**/*", "jsonfile/**/*", "universalify/**/*", - "semver/**/*" + "semver/**/*", + "koffi/**/*" ] } ], + "asarUnpack": [ + "node_modules/koffi/**/*" + ], "extraResources": [ { "from": "resources/ui", @@ -63,8 +72,19 @@ "to": "wcdb-sidecar.cjs" }, { - "from": "vendor/koffi", - "to": "koffi" + "from": "src/icon.png", + "to": "icon.png" + }, + { + "from": "node_modules/ffmpeg-static", + "to": "ffmpeg", + "filter": [ + "ffmpeg", + "ffmpeg.exe", + "ffmpeg.LICENSE", + "ffmpeg.README", + "LICENSE" + ] } ], "win": { @@ -73,6 +93,19 @@ "nsis" ] }, + "mac": { + "icon": "src/icon.png", + "category": "public.app-category.utilities", + "artifactName": "${productName}-${version}-mac-${arch}.${ext}", + "target": [ + "dmg", + "zip" + ], + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "entitlements.mac.plist", + "entitlementsInherit": "entitlements.mac.plist" + }, "publish": { "provider": "github", "owner": "LifeArchiveProject", diff --git a/desktop/scripts/build-backend.cjs b/desktop/scripts/build-backend.cjs index 7df2e5a9..63bd3378 100644 --- a/desktop/scripts/build-backend.cjs +++ b/desktop/scripts/build-backend.cjs @@ -1,9 +1,15 @@ const fs = require("fs"); +const os = require("os"); const path = require("path"); const { spawnSync } = require("child_process"); const repoRoot = path.resolve(__dirname, "..", ".."); const entry = path.join(repoRoot, "src", "wechat_decrypt_tool", "backend_entry.py"); +const venvPythonExecutable = path.join( + repoRoot, + ".venv", + process.platform === "win32" ? "Scripts/python.exe" : "bin/python", +); const distDir = path.join(repoRoot, "desktop", "resources", "backend"); const workDir = path.join(repoRoot, "desktop", "build", "pyinstaller"); @@ -13,16 +19,80 @@ fs.mkdirSync(distDir, { recursive: true }); fs.mkdirSync(workDir, { recursive: true }); fs.mkdirSync(specDir, { recursive: true }); +const integrityManifest = path.join(repoRoot, "native", "wce_integrity", "Cargo.toml"); +const integrityTargetDir = path.join(repoRoot, "native", "wce_integrity", "target", "release"); +let integrityNativeBinary = null; +if (process.platform === "win32") { + const nativeBuild = spawnSync( + "powershell.exe", + [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + path.join(repoRoot, "tools", "build_wce_integrity.ps1"), + ], + { + cwd: repoRoot, + env: { + ...process.env, + WCE_UI_PUBLIC_DIR: path.join(repoRoot, "frontend", ".output", "public"), + }, + stdio: "inherit", + } + ); + if ((nativeBuild.status ?? 1) !== 0) { + console.error("Failed to build the Windows wce_integrity module."); + process.exit(nativeBuild.status ?? 1); + } + const windowsNativeDir = path.join(repoRoot, "src", "wechat_decrypt_tool", "native"); + integrityNativeBinary = path.join(windowsNativeDir, "wce_integrity.pyd"); + if (!fs.existsSync(integrityNativeBinary)) { + console.error("Windows wce_integrity build completed without the canonical wce_integrity.pyd artifact."); + process.exit(1); + } +} else if (process.platform === "darwin" || process.platform === "linux") { + const fileName = process.platform === "darwin" ? "libwce_integrity.dylib" : "libwce_integrity.so"; + const nativeBuild = spawnSync( + "cargo", + ["build", "--manifest-path", integrityManifest, "--release"], + { + cwd: repoRoot, + env: { + ...process.env, + WCE_UI_PUBLIC_DIR: path.join(repoRoot, "frontend", ".output", "public"), + }, + stdio: "inherit", + } + ); + if ((nativeBuild.status ?? 1) !== 0) { + console.error("Failed to build the wce_integrity module for this platform."); + process.exit(nativeBuild.status ?? 1); + } + integrityNativeBinary = path.join(integrityTargetDir, fileName); + if (!fs.existsSync(integrityNativeBinary)) { + console.error(`wce_integrity build completed without expected artifact: ${integrityNativeBinary}`); + process.exit(1); + } +} + +if (!fs.existsSync(venvPythonExecutable)) { + console.error(`Missing virtual environment Python: ${venvPythonExecutable}`); + process.exit(1); +} const integrityPreflight = spawnSync( - "uv", + venvPythonExecutable, [ - "run", - "python", "-c", [ - "from wechat_decrypt_tool.native import wce_integrity as w", + "from wechat_decrypt_tool.export_integrity import load_wce_integrity_native", + "w=load_wce_integrity_native()", "required=('chat','sns','records-project','records-generic','contacts')", "assert all(w.export_css(kind).strip() for kind in required)", + "css=w.export_css('chat')", + "compact=''.join(css.split())", + "assert '.wechat-voice-transcript' in css", + "assert '.wechat-voice-wrapper{display:flex;flex-direction:column' in compact", "assert callable(w.record_file) and callable(w.seal_export)", ].join(";"), ], @@ -36,7 +106,7 @@ const integrityPreflight = spawnSync( } ); if ((integrityPreflight.status ?? 1) !== 0) { - console.error("wce_integrity.pyd is missing or stale. Rebuild it before packaging."); + console.error("wce_integrity runtime is missing or stale. Rebuild or restore the platform implementation before packaging."); process.exit(integrityPreflight.status ?? 1); } @@ -68,12 +138,12 @@ VSVersionInfo( StringTable( '080404B0', [StringStruct('CompanyName', 'LifeArchiveProject'), - StringStruct('FileDescription', 'WeFlow'), + StringStruct('FileDescription', 'WeChatDataAnalysis Backend'), StringStruct('FileVersion', '${versionDot}'), - StringStruct('InternalName', 'weflow'), - StringStruct('LegalCopyright', 'github.com/hicccc77/WeFlow'), - StringStruct('OriginalFilename', 'weflow.exe'), - StringStruct('ProductName', 'WeFlow'), + StringStruct('InternalName', 'wechat-backend'), + StringStruct('LegalCopyright', 'LifeArchiveProject'), + StringStruct('OriginalFilename', 'wechat-backend.exe'), + StringStruct('ProductName', 'WeChatDataAnalysis'), StringStruct('ProductVersion', '${versionDot}')]) ]), VarFileInfo([VarStruct('Translation', [2052, 1200])]) @@ -87,8 +157,37 @@ function pyInstallerAddData(sourcePath, targetPath) { } const nativeDir = path.join(repoRoot, "src", "wechat_decrypt_tool", "native"); +const runtimeNativeDir = path.join(repoRoot, "desktop", "build", "native-runtime"); const skillDir = path.join(repoRoot, "skills", "wechat-mcp-copilot"); const projectToml = path.join(repoRoot, "pyproject.toml"); +const thirdPartyNotices = path.join(repoRoot, "THIRD_PARTY_NOTICES.md"); + +fs.rmSync(runtimeNativeDir, { recursive: true, force: true }); +fs.mkdirSync(runtimeNativeDir, { recursive: true }); + +const wasmDir = path.join(nativeDir, "weflow_wasm"); +if (fs.existsSync(wasmDir)) { + fs.cpSync(wasmDir, path.join(runtimeNativeDir, "weflow_wasm"), { recursive: true, force: true }); +} + +if (process.platform === "win32") { + for (const item of fs.readdirSync(nativeDir, { withFileTypes: true })) { + if (!item.isFile() || !/\.(dll|pyd)$/i.test(item.name)) continue; + if (/^wce_integrity.*\.pyd$/i.test(item.name) && item.name.toLowerCase() !== "wce_integrity.pyd") continue; + fs.copyFileSync(path.join(nativeDir, item.name), path.join(runtimeNativeDir, item.name)); + } +} else if (process.platform === "darwin") { + fs.cpSync(path.join(nativeDir, "macos"), path.join(runtimeNativeDir, "macos"), { + recursive: true, + force: true, + }); + const imageScanHelper = path.join(runtimeNativeDir, "macos", "universal", "image_scan_helper"); + if (!fs.existsSync(imageScanHelper)) { + console.error(`Missing macOS image scan helper: ${imageScanHelper}`); + process.exit(1); + } + fs.chmodSync(imageScanHelper, 0o755); +} const desktopPackageJsonPath = path.join(repoRoot, "desktop", "package.json"); let desktopVersion = "1.3.0"; @@ -99,12 +198,12 @@ try { } catch {} const versionTuple = parseVersionTuple(desktopVersion); const versionDot = versionTuple.join("."); -const versionFilePath = path.join(workDir, "weflow-version.txt"); -fs.writeFileSync(versionFilePath, buildVersionInfoText(versionTuple, versionDot), { encoding: "utf8" }); +const versionFilePath = path.join(workDir, "wechat-data-analysis-version.txt"); +if (process.platform === "win32") { + fs.writeFileSync(versionFilePath, buildVersionInfoText(versionTuple, versionDot), { encoding: "utf8" }); +} const args = [ - "run", - "pyinstaller", "--noconfirm", "--clean", "--name", @@ -116,24 +215,90 @@ const args = [ workDir, "--specpath", specDir, - "--version-file", - versionFilePath, "--add-data", - pyInstallerAddData(nativeDir, "wechat_decrypt_tool/native"), + pyInstallerAddData(runtimeNativeDir, "wechat_decrypt_tool/native"), "--add-data", pyInstallerAddData(skillDir, "skills/wechat-mcp-copilot"), - "--hidden-import", - "wechat_decrypt_tool.key_v4", - "--hidden-import", - "yara", + "--collect-all", + "faster_whisper", + "--collect-all", + "ctranslate2", + "--collect-all", + "av", + "--collect-all", + "opencc", entry, ]; -const r = spawnSync("uv", args, { cwd: repoRoot, stdio: "inherit" }); +if (process.platform === "win32") { + args.splice(args.length - 1, 0, + "--version-file", versionFilePath, + "--hidden-import", "wechat_decrypt_tool.key_v4", + "--hidden-import", "yara" + ); +} +if (integrityNativeBinary) { + args.splice( + args.length - 1, + 0, + "--add-binary", + pyInstallerAddData(integrityNativeBinary, "wechat_decrypt_tool/native") + ); +} + +const pyInstallerExecutable = path.join( + repoRoot, + ".venv", + process.platform === "win32" ? "Scripts/pyinstaller.exe" : "bin/pyinstaller", +); +if (!fs.existsSync(pyInstallerExecutable)) { + console.error(`Missing PyInstaller executable: ${pyInstallerExecutable}`); + process.exit(1); +} +const r = spawnSync(pyInstallerExecutable, args, { cwd: repoRoot, stdio: "inherit" }); if ((r.status ?? 1) !== 0) { process.exit(r.status ?? 1); } +const packagedBackend = path.join( + distDir, + process.platform === "win32" ? "wechat-backend.exe" : "wechat-backend", +); +const openccSmokeDir = fs.mkdtempSync(path.join(os.tmpdir(), "wda-opencc-smoke-")); +try { + const smokeEnv = { ...process.env, PYTHONPATH: "" }; + delete smokeEnv.PYTHONHOME; + const smoke = spawnSync(packagedBackend, ["--smoke-opencc"], { + cwd: openccSmokeDir, + env: smokeEnv, + encoding: "utf8", + windowsHide: true, + }); + if ((smoke.status ?? 1) !== 0) { + process.stderr.write(smoke.stderr || smoke.stdout || "Packaged OpenCC smoke test failed.\n"); + process.exit(smoke.status ?? 1); + } + const outputLines = String(smoke.stdout || "").trim().split(/\r?\n/).filter(Boolean); + let payload; + try { + payload = JSON.parse(outputLines.at(-1) || ""); + } catch { + console.error(`Packaged OpenCC smoke test returned invalid JSON: ${smoke.stdout || ""}`); + process.exit(1); + } + const expected = { + "繁體中文": "繁体中文", + "軟體與資料庫": "软体与资料库", + }; + if (!payload.frozen || JSON.stringify(payload.results) !== JSON.stringify(expected)) { + console.error(`Packaged OpenCC smoke test returned an unexpected result: ${JSON.stringify(payload)}`); + process.exit(1); + } + console.log(`Packaged OpenCC smoke test passed: ${JSON.stringify(payload)}`); +} finally { + fs.rmSync(openccSmokeDir, { recursive: true, force: true }); +} + // Keep a stable external native folder for packaged runtime to avoid relying on // onefile temp extraction paths when wcdb_api.dll performs environment checks. const packagedNativeDir = path.join(distDir, "native"); @@ -142,14 +307,9 @@ try { } catch {} fs.mkdirSync(packagedNativeDir, { recursive: true }); -for (const name of fs.readdirSync(nativeDir)) { - const src = path.join(nativeDir, name); - const dst = path.join(packagedNativeDir, name); - try { - if (fs.statSync(src).isFile()) { - fs.copyFileSync(src, dst); - } - } catch {} +fs.cpSync(runtimeNativeDir, packagedNativeDir, { recursive: true, force: true }); +if (integrityNativeBinary) { + fs.copyFileSync(integrityNativeBinary, path.join(packagedNativeDir, path.basename(integrityNativeBinary))); } // Provide the project marker next to packaged backend resources. @@ -158,6 +318,10 @@ if (fs.existsSync(projectToml)) { fs.copyFileSync(projectToml, path.join(distDir, "pyproject.toml")); } catch {} } +if (fs.existsSync(thirdPartyNotices)) { + try { + fs.copyFileSync(thirdPartyNotices, path.join(distDir, "THIRD_PARTY_NOTICES.md")); + } catch {} +} process.exit(0); - diff --git a/desktop/scripts/build-icon.cjs b/desktop/scripts/build-icon.cjs index be843400..7bef22a4 100644 --- a/desktop/scripts/build-icon.cjs +++ b/desktop/scripts/build-icon.cjs @@ -6,6 +6,7 @@ const { PNG } = require("pngjs"); const repoRoot = path.resolve(__dirname, "..", ".."); const srcPng = path.join(repoRoot, "frontend", "public", "logo.png"); +const dstPng = path.join(repoRoot, "desktop", "src", "icon.png"); // Write the generated ICO to the locations electron-builder expects for app+installer icons. // Also keep a copy in desktop/resources for convenience. const dstIcos = [ @@ -40,7 +41,9 @@ async function main() { const tmpDir = path.join(repoRoot, "desktop", "build", "icon"); fs.mkdirSync(tmpDir, { recursive: true }); const tmpPng = path.join(tmpDir, "logo-square.png"); - fs.writeFileSync(tmpPng, PNG.sync.write(square)); + const squarePng = PNG.sync.write(square); + fs.writeFileSync(tmpPng, squarePng); + fs.writeFileSync(dstPng, squarePng); const buf = await pngToIco(tmpPng); for (const dstIco of dstIcos) { @@ -49,7 +52,7 @@ async function main() { } // eslint-disable-next-line no-console - console.log(`Generated icon(s):\n${dstIcos.map((p) => `- ${p}`).join("\n")}`); + console.log(`Generated icon(s):\n- ${dstPng}\n${dstIcos.map((p) => `- ${p}`).join("\n")}`); } main().catch((err) => { diff --git a/desktop/scripts/smoke-macos-package.cjs b/desktop/scripts/smoke-macos-package.cjs new file mode 100644 index 00000000..90faa94c --- /dev/null +++ b/desktop/scripts/smoke-macos-package.cjs @@ -0,0 +1,423 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const { spawn, spawnSync } = require("node:child_process"); +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const http = require("node:http"); +const net = require("node:net"); +const os = require("node:os"); +const path = require("node:path"); + +const desktopRoot = path.resolve(__dirname, ".."); +const distRoot = path.join(desktopRoot, "dist"); + +function fail(message) { + throw new Error(message); +} + +function findPackagedApp() { + const explicit = String(process.argv[2] || "").trim(); + if (explicit) return path.resolve(explicit); + + for (const entry of fs.readdirSync(distRoot, { withFileTypes: true })) { + if (!entry.isDirectory() || !entry.name.startsWith("mac")) continue; + const root = path.join(distRoot, entry.name); + const appName = fs.readdirSync(root).find((name) => name.endsWith(".app")); + if (appName) return path.join(root, appName); + } + fail(`No packaged .app found under ${distRoot}`); +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + encoding: "utf8", + stdio: options.capture ? "pipe" : "inherit", + ...options, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); + fail(`${command} ${args.join(" ")} failed (${result.status})${details ? `:\n${details}` : ""}`); + } + return String(result.stdout || "") + String(result.stderr || ""); +} + +function requirePath(filePath, { executable = false } = {}) { + assert.ok(fs.existsSync(filePath), `Missing packaged resource: ${filePath}`); + if (executable) fs.accessSync(filePath, fs.constants.X_OK); +} + +function assertArchitecture(filePath, architecture, { universal = false } = {}) { + const output = run("lipo", ["-archs", filePath], { capture: true }).trim().split(/\s+/); + assert.ok(output.includes(architecture), `${filePath} does not contain ${architecture}: ${output.join(" ")}`); + if (universal) { + assert.ok(output.includes("arm64") && output.includes("x86_64"), `${filePath} is not universal2`); + } +} + +function createEncryptedSessionFixture(rootDir) { + const koffi = require("koffi"); + const plainPath = path.join(rootDir, "plain-session.db"); + const encryptedPath = path.join(rootDir, "session.db"); + const sqlite = koffi.load("/usr/lib/libsqlite3.dylib"); + const sqliteOpen = sqlite.func("int sqlite3_open(const char* filename, _Out_ void** db)"); + const sqliteFileControl = sqlite.func( + "int sqlite3_file_control(void* db, const char* dbName, int op, _Inout_ int* value)" + ); + const sqliteExec = sqlite.func( + "int sqlite3_exec(void* db, const char* sql, void* callback, void* context, _Out_ void** error)" + ); + const sqliteFree = sqlite.func("void sqlite3_free(void* value)"); + const sqliteClose = sqlite.func("int sqlite3_close(void* db)"); + + const db = [null]; + assert.equal(sqliteOpen(plainPath, db), 0, "failed to create the native WCDB smoke fixture"); + try { + const reserveBytes = [80]; + assert.equal( + sqliteFileControl(db[0], "main", 38, reserveBytes), + 0, + "failed to configure SQLite reserve bytes" + ); + + const error = [null]; + const sql = ` + PRAGMA page_size=4096; + CREATE TABLE SessionTable ( + username TEXT PRIMARY KEY, + sort_timestamp INTEGER, + last_timestamp INTEGER, + summary TEXT, + unread_count INTEGER, + is_hidden INTEGER, + draft TEXT, + status INTEGER + ); + INSERT INTO SessionTable VALUES ( + 'wxid_friend', 1735689600, 1735689600, 'macOS WCDB smoke', 0, 0, '', 0 + ); + `; + const rc = Number(sqliteExec(db[0], sql, null, null, error)); + try { + const message = error[0] ? koffi.decode(error[0], "char", -1) : ""; + assert.equal(rc, 0, message || "failed to seed the native WCDB smoke fixture"); + } finally { + if (error[0]) sqliteFree(error[0]); + } + } finally { + sqliteClose(db[0]); + } + + const pageSize = 4096; + const saltSize = 16; + const ivSize = 16; + const hmacSize = 64; + const reserveSize = ivSize + hmacSize; + const plain = fs.readFileSync(plainPath); + assert.equal(plain.length % pageSize, 0); + assert.equal(plain.subarray(0, 16).toString("binary"), "SQLite format 3\0"); + assert.equal(plain[20], reserveSize); + + const keyMaterial = Buffer.alloc(32, 0x11); + const salt = crypto.randomBytes(saltSize); + const encryptionKey = crypto.pbkdf2Sync(keyMaterial, salt, 256000, 32, "sha512"); + const macSalt = Buffer.from(salt.map((byte) => byte ^ 0x3a)); + const macKey = crypto.pbkdf2Sync(encryptionKey, macSalt, 2, 32, "sha512"); + const encrypted = Buffer.alloc(plain.length); + + for (let pageOffset = 0, pageNumber = 1; pageOffset < plain.length; pageOffset += pageSize, pageNumber += 1) { + const sourcePage = plain.subarray(pageOffset, pageOffset + pageSize); + const targetPage = encrypted.subarray(pageOffset, pageOffset + pageSize); + const payloadOffset = pageNumber === 1 ? saltSize : 0; + const iv = crypto.randomBytes(ivSize); + const cipher = crypto.createCipheriv("aes-256-cbc", encryptionKey, iv); + cipher.setAutoPadding(false); + const ciphertext = Buffer.concat([ + cipher.update(sourcePage.subarray(payloadOffset, pageSize - reserveSize)), + cipher.final(), + ]); + + if (pageNumber === 1) salt.copy(targetPage, 0); + ciphertext.copy(targetPage, payloadOffset); + iv.copy(targetPage, pageSize - reserveSize); + + const pageNumberBytes = Buffer.alloc(4); + pageNumberBytes.writeUInt32LE(pageNumber); + const digest = crypto + .createHmac("sha512", macKey) + .update(targetPage.subarray(payloadOffset, pageSize - hmacSize)) + .update(pageNumberBytes) + .digest(); + digest.copy(targetPage, pageSize - hmacSize); + } + + fs.writeFileSync(encryptedPath, encrypted); + return { path: encryptedPath, key: keyMaterial.toString("hex") }; +} + +function getFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + const port = address && typeof address === "object" ? address.port : 0; + server.close(() => resolve(port)); + }); + }); +} + +function requestJson(url, { method = "GET", headers = {}, body = null, timeoutMs = 2_000 } = {}) { + return new Promise((resolve, reject) => { + const payload = body == null ? null : Buffer.from(JSON.stringify(body), "utf8"); + const req = http.request(url, { + method, + headers: { + ...headers, + ...(payload + ? { "content-type": "application/json", "content-length": String(payload.length) } + : {}), + }, + }, (res) => { + const chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => { + const text = Buffer.concat(chunks).toString("utf8"); + let decoded = null; + try { + decoded = JSON.parse(text); + } catch {} + resolve({ statusCode: res.statusCode || 0, body: decoded, text }); + }); + }); + req.once("error", reject); + req.setTimeout(timeoutMs, () => req.destroy(new Error(`Request timed out: ${url}`))); + if (payload) req.write(payload); + req.end(); + }); +} + +async function waitForJson(url, options = {}, timeoutMs = 30_000) { + const deadline = Date.now() + timeoutMs; + let lastError = null; + while (Date.now() < deadline) { + try { + const response = await requestJson(url, options); + if (response.statusCode >= 200 && response.statusCode < 500 && response.body) return response; + lastError = new Error(`HTTP ${response.statusCode}: ${response.text}`); + } catch (err) { + lastError = err; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw lastError || new Error(`Timed out waiting for ${url}`); +} + +function startProcess(command, args, options) { + const chunks = []; + const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"], ...options }); + const collect = (chunk) => { + chunks.push(Buffer.from(chunk)); + if (chunks.reduce((total, item) => total + item.length, 0) > 256 * 1024) chunks.shift(); + }; + child.stdout?.on("data", collect); + child.stderr?.on("data", collect); + child.output = () => Buffer.concat(chunks).toString("utf8"); + return child; +} + +async function stopProcess(child) { + if (!child || child.exitCode != null) return; + await new Promise((resolve) => { + const timer = setTimeout(() => { + try { + child.kill("SIGKILL"); + } catch {} + resolve(); + }, 5_000); + child.once("exit", () => { + clearTimeout(timer); + resolve(); + }); + try { + child.kill("SIGTERM"); + } catch { + clearTimeout(timer); + resolve(); + } + }); +} + +async function main() { + if (process.platform !== "darwin") fail("macOS package smoke test must run on macOS"); + if (process.arch !== "arm64") fail(`Apple Silicon runner required, got ${process.arch}`); + + const appPath = findPackagedApp(); + const contents = path.join(appPath, "Contents"); + const resources = path.join(contents, "Resources"); + const electronExecutable = path.join(contents, "MacOS", path.basename(appPath, ".app")); + const backend = path.join(resources, "backend", "wechat-backend"); + const nativeRoot = path.join(resources, "backend", "native"); + const wcdbApi = path.join(nativeRoot, "macos", "arm64", "libwcdb_api.dylib"); + const wcdb = path.join(nativeRoot, "macos", "universal", "libWCDB.dylib"); + const imageLibrary = path.join(nativeRoot, "macos", "universal", "libwx_key.dylib"); + const imageHelper = path.join(nativeRoot, "macos", "universal", "image_scan_helper"); + const integrity = path.join(nativeRoot, "libwce_integrity.dylib"); + const sidecar = path.join(resources, "wcdb-sidecar.cjs"); + const ffmpeg = path.join(resources, "ffmpeg", "ffmpeg"); + const koffiDir = path.join(resources, "app.asar.unpacked", "node_modules", "koffi"); + const koffiNative = path.join(koffiDir, "build", "koffi", "darwin_arm64", "koffi.node"); + + for (const filePath of [electronExecutable, backend, wcdbApi, wcdb, imageLibrary, imageHelper, integrity, sidecar, ffmpeg, koffiNative]) { + requirePath(filePath, { executable: [electronExecutable, backend, imageHelper, ffmpeg].includes(filePath) }); + } + requirePath(path.join(resources, "backend", "THIRD_PARTY_NOTICES.md")); + requirePath(path.join(nativeRoot, "macos", "WEFLOW_LICENSE.txt")); + requirePath(path.join(resources, "ffmpeg", "LICENSE")); + requirePath(path.join(resources, "ffmpeg", "ffmpeg.LICENSE")); + + assertArchitecture(electronExecutable, "arm64"); + assertArchitecture(backend, "arm64"); + assertArchitecture(wcdbApi, "arm64"); + assertArchitecture(integrity, "arm64"); + assertArchitecture(koffiNative, "arm64"); + assertArchitecture(wcdb, "arm64", { universal: true }); + assertArchitecture(imageLibrary, "arm64", { universal: true }); + assertArchitecture(imageHelper, "arm64", { universal: true }); + assertArchitecture(ffmpeg, "arm64"); + + const ffmpegVersion = run(ffmpeg, ["-version"], { capture: true }); + assert.match(ffmpegVersion, /^ffmpeg version/m); + + const imageHelperProbe = spawnSync(imageHelper, [String(process.pid), "0".repeat(32)], { + encoding: "utf8", + stdio: "pipe", + }); + const imageHelperOutput = `${imageHelperProbe.stdout || ""}\n${imageHelperProbe.stderr || ""}`; + assert.doesNotMatch(imageHelperOutput, /dlopen failed|symbol not found/i); + assert.match(String(imageHelperProbe.stdout || ""), /"success":(?:true|false)/); + + run("codesign", ["--verify", "--deep", "--strict", "--verbose=2", appPath]); + const entitlements = run("codesign", ["-d", "--entitlements", "-", electronExecutable], { capture: true }); + assert.match(entitlements, /com\.apple\.security\.cs\.allow-jit/); + + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "wda-macos-smoke-")); + let backendProc = null; + let sidecarProc = null; + try { + const backendPort = await getFreePort(); + const sidecarPort = await getFreePort(); + const sidecarToken = "package-smoke-token"; + const sidecarEnv = { + ...process.env, + ELECTRON_RUN_AS_NODE: "1", + WECHAT_TOOL_WCDB_SIDECAR_HOST: "127.0.0.1", + WECHAT_TOOL_WCDB_SIDECAR_PORT: String(sidecarPort), + WECHAT_TOOL_WCDB_SIDECAR_TOKEN: sidecarToken, + WECHAT_TOOL_WCDB_API_DLL_PATH: wcdbApi, + WECHAT_TOOL_WCDB_DLL_DIR: path.dirname(wcdbApi), + WECHAT_TOOL_WCDB_RESOURCE_PATHS: JSON.stringify([path.dirname(wcdbApi), path.dirname(wcdb), resources]), + WECHAT_TOOL_KOFFI_DIR: koffiDir, + }; + sidecarProc = startProcess(electronExecutable, [sidecar], { cwd: resources, env: sidecarEnv }); + sidecarProc.once("exit", (code, signal) => { + if (code && code !== 0) process.stderr.write(sidecarProc.output()); + }); + const sidecarHealth = await waitForJson(`http://127.0.0.1:${sidecarPort}/health`); + assert.equal(sidecarHealth.body.ok, true); + const sidecarInit = await requestJson(`http://127.0.0.1:${sidecarPort}/call`, { + method: "POST", + headers: { "x-wcdb-sidecar-token": sidecarToken }, + body: { action: "init", payload: {} }, + timeoutMs: 15_000, + }); + assert.equal(sidecarInit.statusCode, 200); + assert.equal(sidecarInit.body?.ok, true, sidecarInit.text || sidecarProc.output()); + assert.equal(sidecarInit.body?.result?.initialized, true); + + const fixture = createEncryptedSessionFixture(tempRoot); + const opened = await requestJson(`http://127.0.0.1:${sidecarPort}/call`, { + method: "POST", + headers: { "x-wcdb-sidecar-token": sidecarToken }, + body: { action: "open_account", payload: { path: fixture.path, key: fixture.key } }, + timeoutMs: 15_000, + }); + assert.equal(opened.statusCode, 200, opened.text || sidecarProc.output()); + assert.equal(opened.body?.ok, true, opened.text || sidecarProc.output()); + const fixtureHandle = Number(opened.body?.result?.handle || 0); + assert.ok(fixtureHandle > 0, "packaged WCDB did not return an account handle"); + try { + const sessions = await requestJson(`http://127.0.0.1:${sidecarPort}/call`, { + method: "POST", + headers: { "x-wcdb-sidecar-token": sidecarToken }, + body: { action: "get_sessions", payload: { handle: fixtureHandle } }, + timeoutMs: 15_000, + }); + assert.equal(sessions.statusCode, 200, sessions.text || sidecarProc.output()); + assert.equal(sessions.body?.ok, true, sessions.text || sidecarProc.output()); + const rows = JSON.parse(String(sessions.body?.result?.payload || "[]")); + assert.ok( + Array.isArray(rows) && rows.some((row) => String(row?.username || "") === "wxid_friend"), + "packaged WCDB did not return the synthetic realtime session" + ); + } finally { + await requestJson(`http://127.0.0.1:${sidecarPort}/call`, { + method: "POST", + headers: { "x-wcdb-sidecar-token": sidecarToken }, + body: { action: "close_account", payload: { handle: fixtureHandle } }, + timeoutMs: 5_000, + }); + } + + const backendEnv = { + ...process.env, + WECHAT_TOOL_HOST: "127.0.0.1", + WECHAT_TOOL_PORT: String(backendPort), + WECHAT_TOOL_DATA_DIR: path.join(tempRoot, "data"), + WECHAT_TOOL_OUTPUT_DIR: path.join(tempRoot, "output"), + WECHAT_TOOL_UI_DIR: path.join(resources, "ui"), + WECHAT_TOOL_WCDB_SIDECAR_URL: `http://127.0.0.1:${sidecarPort}`, + WECHAT_TOOL_WCDB_SIDECAR_TOKEN: sidecarToken, + WECHAT_TOOL_WCDB_API_DLL_PATH: wcdbApi, + WECHAT_TOOL_FFMPEG: ffmpeg, + }; + backendProc = startProcess(backend, [], { cwd: path.dirname(backend), env: backendEnv }); + backendProc.once("exit", (code, signal) => { + if (code && code !== 0) process.stderr.write(backendProc.output()); + }); + const health = await waitForJson(`http://127.0.0.1:${backendPort}/api/health`); + assert.equal(health.body?.status, "healthy"); + assert.equal(health.body?.service, "微信解密工具"); + + const platform = await requestJson(`http://127.0.0.1:${backendPort}/api/system/platform`); + assert.equal(platform.statusCode, 200); + assert.equal(platform.body?.platform, "macos"); + assert.equal(platform.body?.database_key_extraction, false); + assert.equal(platform.body?.database_key_manual_input, true); + assert.equal(platform.body?.database_decryption, true); + assert.equal(platform.body?.image_key_memory_scan, true); + assert.equal(platform.body?.realtime_wcdb, true); + assert.equal(platform.body?.account_archive_cross_platform, true); + + const keys = await requestJson(`http://127.0.0.1:${backendPort}/api/get_keys`); + assert.equal(keys.statusCode, 200); + assert.equal(keys.body?.data?.platform, "macos"); + assert.equal(keys.body?.data?.database_key_extraction, false); + assert.equal(keys.body?.data?.manual_input_supported, true); + assert.match(String(keys.body?.errmsg || ""), /手动填写/); + } finally { + await stopProcess(backendProc); + await stopProcess(sidecarProc); + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + + // Runtime processes must never modify the signed application bundle. + run("codesign", ["--verify", "--deep", "--strict", "--verbose=2", appPath]); + process.stdout.write(`macOS package smoke test passed: ${appPath}\n`); +} + +main().catch((err) => { + process.stderr.write(`${err?.stack || err}\n`); + process.exitCode = 1; +}); diff --git a/desktop/src/icon.png b/desktop/src/icon.png new file mode 100644 index 00000000..cdee9197 Binary files /dev/null and b/desktop/src/icon.png differ diff --git a/desktop/src/main.cjs b/desktop/src/main.cjs index ec524d71..d9e207c5 100644 --- a/desktop/src/main.cjs +++ b/desktop/src/main.cjs @@ -3,6 +3,7 @@ const { BrowserWindow, Menu, Tray, + nativeImage, ipcMain, globalShortcut, dialog, @@ -65,6 +66,8 @@ let wcdbSidecarHealthGeneration = 0; const WCDB_SIDECAR_HEALTH_FAILURE_LIMIT = 15; let resolvedDataDir = null; let mainWindow = null; +let mainWindowLaunchPromise = null; +let initialStartupPromise = null; let tray = null; let isQuitting = false; let desktopSettings = null; @@ -89,6 +92,7 @@ function getTitleBarOverlayOptions(theme) { } function setWindowTitleBarTheme(win, theme) { + if (process.platform === "darwin") return false; if (!win || typeof win.setTitleBarOverlay !== "function") return false; try { win.setTitleBarOverlay(getTitleBarOverlayOptions(theme)); @@ -109,8 +113,8 @@ if (!gotSingleInstanceLock) { } else { app.on("second-instance", () => { try { - if (app.isReady()) showMainWindow(); - else app.whenReady().then(() => showMainWindow()); + if (app.isReady()) requestMainWindow("second-instance"); + else app.whenReady().then(() => requestMainWindow("second-instance")); } catch {} }); } @@ -641,7 +645,10 @@ function ensureOutputLink() { // NOTE: We intentionally avoid creating a junction/symlink inside the install directory. // Some uninstall/update flows may traverse reparse points and delete the target directory, // causing data loss (the install dir is removed on every update/reinstall). - if (!app.isPackaged) return; + // These helper files are Windows installer affordances. Writing beside the + // executable on macOS would mutate Contents/MacOS and invalidate the app's + // code signature after the first launch. + if (!app.isPackaged || process.platform !== "win32") return; const exeDir = getExeDir(); const target = resolveOutputDir(); @@ -1634,6 +1641,18 @@ function checkForUpdatesOnStartup() { } function getTrayIconPath() { + if (process.platform === "darwin") { + const packaged = path.join(process.resourcesPath, "icon.png"); + try { + if (app.isPackaged && fs.existsSync(packaged)) return packaged; + } catch {} + + const devMac = path.resolve(__dirname, "..", "src", "icon.png"); + try { + if (fs.existsSync(devMac)) return devMac; + } catch {} + } + // Prefer an icon shipped in `src/` so it works both in dev and packaged (asar) builds. const shipped = path.join(__dirname, "icon.ico"); try { @@ -1650,7 +1669,7 @@ function getTrayIconPath() { } function showMainWindow() { - if (!mainWindow) return; + if (!mainWindow || mainWindow.isDestroyed()) return false; try { mainWindow.setSkipTaskbar(false); } catch {} @@ -1663,6 +1682,22 @@ function showMainWindow() { try { mainWindow.focus(); } catch {} + return true; +} + +function requestMainWindow(reason = "request") { + if (showMainWindow()) return; + + const startup = initialStartupPromise || Promise.resolve(); + void startup + .then(() => ensureMainWindowReady()) + .catch((err) => { + const message = err?.message || String(err); + logMain(`[main] failed to open window reason=${reason}: ${err?.stack || message}`); + try { + dialog.showErrorBox("WeChatDataAnalysis", `无法打开主窗口:${message}`); + } catch {} + }); } function createTray() { @@ -1676,7 +1711,12 @@ function createTray() { } try { - tray = new Tray(iconPath); + let trayIcon = iconPath; + if (process.platform === "darwin") { + trayIcon = nativeImage.createFromPath(iconPath).resize({ width: 18, height: 18 }); + trayIcon.setTemplateImage(true); + } + tray = new Tray(trayIcon); } catch (err) { tray = null; logMain(`[main] failed to create tray: ${err?.message || err}`); @@ -1692,7 +1732,7 @@ function createTray() { Menu.buildFromTemplate([ { label: "显示", - click: () => showMainWindow(), + click: () => requestMainWindow("tray-menu"), }, { label: "检查更新...", @@ -1791,8 +1831,8 @@ function createTray() { } catch {} try { - tray.on("click", () => showMainWindow()); - tray.on("double-click", () => showMainWindow()); + tray.on("click", () => requestMainWindow("tray-click")); + tray.on("double-click", () => requestMainWindow("tray-double-click")); } catch {} return tray; @@ -1875,15 +1915,36 @@ function repoRoot() { } function getPackagedBackendPath() { - // Placeholder: in step 3 we will bundle a real backend exe into resources. - return path.join(process.resourcesPath, "backend", "wechat-backend.exe"); + const executableName = process.platform === "win32" ? "wechat-backend.exe" : "wechat-backend"; + return path.join(process.resourcesPath, "backend", executableName); +} + +function getFfmpegPath() { + if (app.isPackaged) { + const executableName = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg"; + return path.join(process.resourcesPath, "ffmpeg", executableName); + } + + try { + return String(require("ffmpeg-static") || "").trim(); + } catch { + return ""; + } } function getPackagedWcdbDllPath() { + if (process.platform === "darwin") { + const arch = process.arch === "arm64" ? "arm64" : "x64"; + return path.join(process.resourcesPath, "backend", "native", "macos", arch, "libwcdb_api.dylib"); + } return path.join(process.resourcesPath, "backend", "native", "wcdb_api.dll"); } function getDevWcdbDllPath() { + if (process.platform === "darwin") { + const arch = process.arch === "arm64" ? "arm64" : "x64"; + return path.join(repoRoot(), "src", "wechat_decrypt_tool", "native", "macos", arch, "libwcdb_api.dylib"); + } return path.join(repoRoot(), "src", "wechat_decrypt_tool", "native", "wcdb_api.dll"); } @@ -1902,7 +1963,9 @@ function getWcdbSidecarScriptPath() { } function getKoffiDir() { - return app.isPackaged ? path.join(process.resourcesPath, "koffi") : path.join(repoRoot(), "desktop", "vendor", "koffi"); + return app.isPackaged + ? path.join(process.resourcesPath, "app.asar.unpacked", "node_modules", "koffi") + : path.join(repoRoot(), "desktop", "node_modules", "koffi"); } function getWcdbSidecarStdioLogPath(dataDir) { @@ -2116,13 +2179,13 @@ function scheduleWcdbRuntimeRestart(code, signal) { function startWcdbSidecar() { if (process.env.WECHAT_TOOL_WCDB_SIDECAR === "0") return null; if (wcdbSidecarProc && wcdbSidecarProc.exitCode == null) return wcdbSidecarProc; - if (process.platform !== "win32") return null; + if (!["win32", "darwin"].includes(process.platform)) return null; const dllPath = getWcdbDllPath(); const sidecarScript = getWcdbSidecarScriptPath(); const koffiDir = getKoffiDir(); if (!fs.existsSync(dllPath)) { - logMain(`[wcdb-sidecar] skip: missing wcdb_api.dll ${dllPath}`); + logMain(`[wcdb-sidecar] skip: missing native library ${dllPath}`); return null; } if (!fs.existsSync(sidecarScript)) { @@ -2192,7 +2255,8 @@ function stopWcdbSidecar() { } function startBackend() { - if (backendProc) return backendProc; + if (backendProc && backendProc.exitCode == null) return backendProc; + backendProc = null; startWcdbSidecar(); const resolvedDataPath = resolveDataDir() || getUserDataDir() || repoRoot(); @@ -2216,6 +2280,12 @@ function startBackend() { env.WECHAT_TOOL_UI_DIR = path.join(process.resourcesPath, "ui"); } + const ffmpegPath = getFfmpegPath(); + if (ffmpegPath && fs.existsSync(ffmpegPath)) { + env.WECHAT_TOOL_FFMPEG = ffmpegPath; + logMain(`[main] using bundled ffmpeg: ${ffmpegPath}`); + } + if (app.isPackaged) { try { fs.mkdirSync(env.WECHAT_TOOL_DATA_DIR, { recursive: true }); @@ -2224,16 +2294,14 @@ function startBackend() { const backendExe = getPackagedBackendPath(); if (!fs.existsSync(backendExe)) { - throw new Error( - `Packaged backend not found: ${backendExe}. Build it into desktop/resources/backend/wechat-backend.exe` - ); + throw new Error(`Packaged backend not found: ${backendExe}. Run the platform backend build before packaging.`); } const packagedWcdbDll = getPackagedWcdbDllPath(); if (fs.existsSync(packagedWcdbDll)) { env.WECHAT_TOOL_WCDB_API_DLL_PATH = packagedWcdbDll; - logMain(`[main] using packaged wcdb_api.dll: ${packagedWcdbDll}`); + logMain(`[main] using packaged WCDB native library: ${packagedWcdbDll}`); } else { - logMain(`[main] packaged wcdb_api.dll not found: ${packagedWcdbDll}`); + logMain(`[main] packaged WCDB native library not found: ${packagedWcdbDll}`); } const backendCwd = path.dirname(backendExe); @@ -2593,8 +2661,9 @@ function createMainWindow() { height: 800, minWidth: 980, minHeight: 700, - titleBarStyle: "hidden", - titleBarOverlay: getTitleBarOverlayOptions("light"), + ...(process.platform === "darwin" + ? { titleBarStyle: "hiddenInset", trafficLightPosition: { x: 12, y: 9 } } + : { titleBarStyle: "hidden", titleBarOverlay: getTitleBarOverlayOptions("light") }), backgroundColor: "#EDEDED", webPreferences: { preload: path.join(__dirname, "preload.cjs"), @@ -2627,7 +2696,7 @@ function createMainWindow() { }); win.on("closed", () => { - stopBackend(); + if (mainWindow === win) mainWindow = null; }); setupRendererConsoleLogging(win); @@ -3072,26 +3141,26 @@ function registerWindowIpc() { }; } }); -} - -async function main() { - await app.whenReady(); - await refreshRendererCacheForPackagedUi(); - Menu.setApplicationMenu(null); - registerWindowIpc(); - registerDebugShortcuts(); - - // Resolve/create the data dir early so we can log reliably and place helper files - // next to the installed exe for easier access. - resolveDataDir(); - loadDesktopSettings(); - await applyPendingOutputDirOnStartup(); - ensureOutputLink(); - await ensureBackendPortAvailableOnStartup(); - await prepareWcdbSidecarPort(); - logMain(`[main] app.isPackaged=${app.isPackaged} argv=${JSON.stringify(process.argv)}`); + ipcMain.handle("dialog:chooseArchive", async (_event, options) => { + try { + return await dialog.showOpenDialog({ + title: String(options?.title || "请选择账号归档 ZIP"), + defaultPath: String(options?.defaultPath || "") || undefined, + properties: ["openFile"], + filters: [ + { name: "微信账号归档", extensions: ["zip"] }, + { name: "所有文件", extensions: ["*"] }, + ], + }); + } catch (err) { + logMain(`[main] dialog:chooseArchive failed: ${err?.message || err}`); + throw err; + } + }); +} +async function ensureBackendReadyWithFallback() { startBackend(); const startupTimeoutMs = getBackendStartupTimeoutMs(); try { @@ -3144,31 +3213,74 @@ async function main() { throw err; } } +} - const win = createMainWindow(); - mainWindow = win; - ensureTrayForCloseBehavior(); +async function ensureMainWindowReady() { + if (showMainWindow()) return mainWindow; + if (mainWindowLaunchPromise) return mainWindowLaunchPromise; - const startUrl = getDesktopUiUrl(); + mainWindowLaunchPromise = (async () => { + await ensureBackendReadyWithFallback(); - logMain(`[main] debugEnabled=${debugEnabled()} startUrl=${startUrl}`); - await loadWithRetry(win, startUrl); + if (showMainWindow()) return mainWindow; - // Auto-check updates after the UI has loaded (packaged builds only). - checkForUpdatesOnStartup(); + const win = createMainWindow(); + mainWindow = win; + ensureTrayForCloseBehavior(); - // If debug mode is enabled, auto-open DevTools so the user doesn't need menu/shortcuts. - if (debugEnabled()) { - try { - win.webContents.openDevTools({ mode: "detach" }); - } catch {} + const startUrl = getDesktopUiUrl(); + logMain(`[main] debugEnabled=${debugEnabled()} startUrl=${startUrl}`); + await loadWithRetry(win, startUrl); + + if (debugEnabled()) { + try { + win.webContents.openDevTools({ mode: "detach" }); + } catch {} + } + + return win; + })(); + + try { + return await mainWindowLaunchPromise; + } finally { + mainWindowLaunchPromise = null; } } +async function main() { + await app.whenReady(); + await refreshRendererCacheForPackagedUi(); + Menu.setApplicationMenu(null); + registerWindowIpc(); + registerDebugShortcuts(); + + // Resolve/create the data dir early so we can log reliably and place helper files + // next to the installed exe for easier access. + resolveDataDir(); + loadDesktopSettings(); + await applyPendingOutputDirOnStartup(); + ensureOutputLink(); + await ensureBackendPortAvailableOnStartup(); + await prepareWcdbSidecarPort(); + + logMain(`[main] app.isPackaged=${app.isPackaged} argv=${JSON.stringify(process.argv)}`); + + await ensureMainWindowReady(); + + // Auto-check updates once after the first UI load (packaged builds only). + checkForUpdatesOnStartup(); +} + app.on("window-all-closed", () => { - stopBackend(); - stopWcdbSidecar(); - if (process.platform !== "darwin") app.quit(); + // Standard macOS lifecycle: keep the app runtime alive so clicking the Dock + // icon can recreate a window. All child processes are stopped in before-quit. + if (process.platform === "darwin" && getCloseBehavior() !== "exit") return; + app.quit(); +}); + +app.on("activate", () => { + requestMainWindow("activate"); }); app.on("will-quit", () => { @@ -3185,7 +3297,8 @@ app.on("before-quit", () => { }); if (gotSingleInstanceLock) { - main().catch((err) => { + initialStartupPromise = main(); + initialStartupPromise.catch((err) => { // eslint-disable-next-line no-console console.error(err); logMain(`[main] fatal: ${err?.stack || String(err)}`); @@ -3202,7 +3315,7 @@ if (gotSingleInstanceLock) { "文件:desktop-main.log / backend-stdio.log", ]; if (outputDir) { - detailLines.push("", `当前 output 目录:${outputDir}`, "其中 output\\logs\\... 也在这里"); + detailLines.push("", `当前 output 目录:${outputDir}`, `其中 output${path.sep}logs${path.sep}... 也在这里`); } dialog.showErrorBox( "WeChatDataAnalysis 启动失败", diff --git a/desktop/src/preload.cjs b/desktop/src/preload.cjs index cefde974..98be3d5a 100644 --- a/desktop/src/preload.cjs +++ b/desktop/src/preload.cjs @@ -63,6 +63,7 @@ if (typeof window !== "undefined") { contextBridge.exposeInMainWorld("wechatDesktop", { // Marker used by the frontend to distinguish the Electron desktop shell from the pure web build. __brand: "WeChatDataAnalysisDesktop", + platform: process.platform, windowControlsMode: "overlay", minimize: () => ipcRenderer.invoke("window:minimize"), toggleMaximize: () => ipcRenderer.invoke("window:toggleMaximize"), @@ -84,6 +85,7 @@ contextBridge.exposeInMainWorld("wechatDesktop", { setMcpLanAccess: (enabled) => ipcRenderer.invoke("backend:setMcpLanAccess", !!enabled), chooseDirectory: (options = {}) => ipcRenderer.invoke("dialog:chooseDirectory", options), + chooseArchive: (options = {}) => ipcRenderer.invoke("dialog:chooseArchive", options), // Data/output folder helpers getOutputDirInfo: () => ipcRenderer.invoke("app:getOutputDirInfo"), diff --git a/desktop/src/wcdb-sidecar.cjs b/desktop/src/wcdb-sidecar.cjs index 763f0d2a..ad758505 100644 --- a/desktop/src/wcdb-sidecar.cjs +++ b/desktop/src/wcdb-sidecar.cjs @@ -10,7 +10,7 @@ const TOKEN = String(process.env.WECHAT_TOOL_WCDB_SIDECAR_TOKEN || "").trim(); let koffi = null; let nativeLib = null; -let nativeDllPath = ""; +let nativeLibraryPath = ""; let initialized = false; let protectionResults = []; const preloadedLibs = []; @@ -110,6 +110,7 @@ function getDllDir() { function getDllPath() { const fromEnv = String(process.env.WECHAT_TOOL_WCDB_API_DLL_PATH || "").trim(); if (fromEnv) return path.resolve(fromEnv); + if (process.platform === "darwin") return path.join(getDllDir(), "libwcdb_api.dylib"); return path.join(getDllDir(), "wcdb_api.dll"); } @@ -138,17 +139,19 @@ function loadNative() { const ffi = loadKoffi(); const dllDir = getDllDir(); - nativeDllPath = getDllPath(); - if (!fs.existsSync(nativeDllPath)) { - throw new ApiError(`wcdb_api.dll not found: ${nativeDllPath}`); + nativeLibraryPath = getDllPath(); + if (!fs.existsSync(nativeLibraryPath)) { + throw new ApiError(`WCDB native library not found: ${nativeLibraryPath}`); } try { process.chdir(dllDir); } catch {} - for (const dep of ["WCDB.dll", "SDL2.dll", "VoipEngine.dll"]) { - const depPath = path.join(dllDir, dep); + const dependencyPaths = process.platform === "darwin" + ? [path.join(dllDir, "..", "universal", "libWCDB.dylib")] + : ["WCDB.dll", "SDL2.dll", "VoipEngine.dll"].map((name) => path.join(dllDir, name)); + for (const depPath of dependencyPaths) { if (!fs.existsSync(depPath)) continue; try { preloadedLibs.push(ffi.load(depPath)); @@ -158,7 +161,7 @@ function loadNative() { } } - nativeLib = ffi.load(nativeDllPath); + nativeLib = ffi.load(nativeLibraryPath); funcs.InitProtection = tryFunc("int32 InitProtection(const char* resourcePath)"); funcs.wcdb_init = tryFunc("int32 wcdb_init()"); funcs.wcdb_shutdown = tryFunc("int32 wcdb_shutdown()"); @@ -224,10 +227,10 @@ function loadNative() { funcs.wcdb_free_string = tryFunc("void wcdb_free_string(void* ptr)"); if (!funcs.wcdb_init || !funcs.wcdb_open_account || !funcs.wcdb_get_logs || !funcs.wcdb_free_string) { - throw new ApiError("wcdb_api.dll is missing required exports"); + throw new ApiError("WCDB native library is missing required exports"); } - log(`loaded ${nativeDllPath}`); + log(`loaded ${nativeLibraryPath}`); return nativeLib; } @@ -238,6 +241,19 @@ function requireFunc(name) { return fn; } +function selectMessageCursorFunction(action, availableFuncs) { + const preferredName = action === "open_message_cursor_lite" + ? "wcdb_open_message_cursor_lite" + : "wcdb_open_message_cursor"; + if (availableFuncs[preferredName]) { + return { fnName: preferredName, fn: availableFuncs[preferredName] }; + } + if (preferredName === "wcdb_open_message_cursor_lite" && availableFuncs.wcdb_open_message_cursor) { + return { fnName: "wcdb_open_message_cursor", fn: availableFuncs.wcdb_open_message_cursor }; + } + throw new ApiError(`${preferredName} not exported`, -404); +} + function ptrToString(ptr) { if (!ptr) return ""; return loadKoffi().decode(ptr, "char", -1); @@ -298,7 +314,7 @@ function callOutError(name, args) { function ensureInitialized() { loadNative(); if (initialized) { - return { initialized: true, dllPath: nativeDllPath, protection: protectionResults }; + return { initialized: true, libraryPath: nativeLibraryPath, protection: protectionResults }; } protectionResults = []; @@ -320,7 +336,7 @@ function ensureInitialized() { throw new ApiError("wcdb_init failed", rc, { protection: protectionResults }); } initialized = true; - return { initialized: true, dllPath: nativeDllPath, protection: protectionResults }; + return { initialized: true, libraryPath: nativeLibraryPath, protection: protectionResults }; } function normalizeHandle(value) { @@ -382,10 +398,11 @@ function handleAction(action, payload) { case "open_message_cursor": case "open_message_cursor_lite": { - const fnName = action === "open_message_cursor_lite" ? "wcdb_open_message_cursor_lite" : "wcdb_open_message_cursor"; + loadNative(); + const { fnName, fn } = selectMessageCursorFunction(action, funcs); const out = [0]; const rc = Number( - requireFunc(fnName)( + fn( normalizeHandle(data.handle), String(data.session_id || "").trim(), Number.parseInt(String(data.batch_size || 0), 10) || 1, @@ -559,7 +576,7 @@ function handleAction(action, payload) { async function handleRequest(req, res) { try { if (req.method === "GET" && req.url === "/health") { - jsonResponse(res, 200, { ok: true, initialized, dllPath: nativeDllPath || getDllPath() }); + jsonResponse(res, 200, { ok: true, initialized, libraryPath: nativeLibraryPath || getDllPath() }); return; } @@ -591,31 +608,36 @@ async function handleRequest(req, res) { } } -if (!Number.isInteger(PORT) || PORT <= 0 || PORT > 65535) { - log(`invalid sidecar port: ${process.env.WECHAT_TOOL_WCDB_SIDECAR_PORT || ""}`); - process.exit(2); -} +function startServer() { + if (!Number.isInteger(PORT) || PORT <= 0 || PORT > 65535) { + log(`invalid sidecar port: ${process.env.WECHAT_TOOL_WCDB_SIDECAR_PORT || ""}`); + process.exit(2); + } -const server = http.createServer((req, res) => { - void handleRequest(req, res); -}); + const server = http.createServer((req, res) => { + void handleRequest(req, res); + }); -server.listen(PORT, HOST, () => { - log(`listening http://${HOST}:${PORT} dll=${getDllPath()} koffi=${process.env.WECHAT_TOOL_KOFFI_DIR || "node_modules"}`); -}); + server.listen(PORT, HOST, () => { + log(`listening http://${HOST}:${PORT} dll=${getDllPath()} koffi=${process.env.WECHAT_TOOL_KOFFI_DIR || "node_modules"}`); + }); -process.on("SIGTERM", () => { - try { - server.close(() => process.exit(0)); - } catch { - process.exit(0); + for (const signal of ["SIGTERM", "SIGINT"]) { + process.on(signal, () => { + try { + server.close(() => process.exit(0)); + } catch { + process.exit(0); + } + }); } -}); -process.on("SIGINT", () => { - try { - server.close(() => process.exit(0)); - } catch { - process.exit(0); - } -}); + return server; +} + +if (require.main === module) startServer(); + +module.exports = { + selectMessageCursorFunction, + startServer, +}; diff --git a/desktop/tests/package-config.test.cjs b/desktop/tests/package-config.test.cjs new file mode 100644 index 00000000..058b912b --- /dev/null +++ b/desktop/tests/package-config.test.cjs @@ -0,0 +1,88 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("fs"); +const path = require("path"); +const { pathToFileURL } = require("url"); + +const desktopRoot = path.resolve(__dirname, ".."); +const repoRoot = path.resolve(desktopRoot, ".."); +const packageJson = JSON.parse(fs.readFileSync(path.join(desktopRoot, "package.json"), "utf8")); + +test("desktop package includes and unpacks the Koffi native runtime", () => { + const nodeModulesRule = packageJson.build.files.find( + (item) => item && typeof item === "object" && item.from === "node_modules" + ); + assert.ok(nodeModulesRule); + assert.ok(nodeModulesRule.filter.includes("koffi/**/*")); + assert.ok(packageJson.build.asarUnpack.includes("node_modules/koffi/**/*")); +}); + +test("desktop package ships the platform ffmpeg binary and license", () => { + const resource = packageJson.build.extraResources.find( + (item) => item && item.from === "node_modules/ffmpeg-static" + ); + assert.ok(resource); + assert.equal(resource.to, "ffmpeg"); + assert.ok(resource.filter.includes("ffmpeg")); + assert.ok(resource.filter.includes("ffmpeg.exe")); + assert.ok(resource.filter.includes("LICENSE")); +}); + +test("macOS package resources required by realtime WCDB and image scanning exist", () => { + const nativeRoot = path.join(repoRoot, "src", "wechat_decrypt_tool", "native", "macos"); + const required = [ + path.join(nativeRoot, "arm64", "libwcdb_api.dylib"), + path.join(nativeRoot, "universal", "libWCDB.dylib"), + path.join(nativeRoot, "universal", "libwx_key.dylib"), + path.join(nativeRoot, "universal", "image_scan_helper"), + path.join(desktopRoot, "src", "wcdb-sidecar.cjs"), + ]; + for (const resource of required) assert.ok(fs.existsSync(resource), resource); + fs.accessSync(path.join(nativeRoot, "universal", "image_scan_helper"), fs.constants.X_OK); +}); + +test("macOS release config emits architecture-specific DMG and ZIP assets", () => { + assert.deepEqual(packageJson.build.mac.target, ["dmg", "zip"]); + assert.match(packageJson.build.mac.artifactName, /mac-\$\{arch\}/); + assert.equal(packageJson.build.mac.hardenedRuntime, true); +}); + +test("macOS release exposes a reusable packaged smoke test", () => { + const smokeScript = path.join(desktopRoot, "scripts", "smoke-macos-package.cjs"); + assert.equal(packageJson.scripts["smoke:mac"], "node scripts/smoke-macos-package.cjs"); + assert.ok(fs.existsSync(smokeScript), smokeScript); +}); + +test("release workflow builds, smoke-tests, and uploads macOS artifacts", () => { + const workflow = fs.readFileSync(path.join(repoRoot, ".github", "workflows", "release.yml"), "utf8"); + const macJob = workflow.match(/\n build-macos-arm64:\n([\s\S]*?)(?=\n publish-release:\n)/)?.[1] || ""; + + assert.match(macJob, /runs-on:\s*macos-15/); + assert.match(macJob, /run:\s*npm run dist:mac/); + assert.match(macJob, /run:\s*npm run smoke:mac/); + assert.match(macJob, /uses:\s*actions\/upload-artifact@v4/); + assert.match(macJob, /desktop\/dist\/\*\.dmg/); + assert.match(macJob, /desktop\/dist\/\*\.zip/); +}); + +test("macOS native window controls reserve the sidebar title-bar area", () => { + const preload = fs.readFileSync(path.join(desktopRoot, "src", "preload.cjs"), "utf8"); + const main = fs.readFileSync(path.join(desktopRoot, "src", "main.cjs"), "utf8"); + const sidebar = fs.readFileSync(path.join(repoRoot, "frontend", "components", "SidebarRail.vue"), "utf8"); + + assert.match(preload, /platform:\s*process\.platform/); + assert.match(main, /titleBarStyle:\s*"hiddenInset"/); + assert.match(main, /trafficLightPosition/); + assert.match(sidebar, /isMacosDesktop/); + assert.match(sidebar, /macos-sidebar-titlebar-spacer/); + assert.match(sidebar, /--desktop-titlebar-height/); +}); + +test("frontend joins copied output paths using the native path style", async () => { + const modulePath = path.join(repoRoot, "frontend", "lib", "native-path.js"); + const { joinNativePath } = await import(pathToFileURL(modulePath).href); + + assert.equal(joinNativePath("/Users/demo/output/", "wxid_demo"), "/Users/demo/output/wxid_demo"); + assert.equal(joinNativePath("D:\\wechat\\output\\", "wxid_demo"), "D:\\wechat\\output\\wxid_demo"); + assert.equal(joinNativePath("\\\\server\\share\\output", "wxid_demo"), "\\\\server\\share\\output\\wxid_demo"); +}); diff --git a/desktop/tests/wcdb-sidecar.test.cjs b/desktop/tests/wcdb-sidecar.test.cjs new file mode 100644 index 00000000..c60b2fcf --- /dev/null +++ b/desktop/tests/wcdb-sidecar.test.cjs @@ -0,0 +1,26 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const { selectMessageCursorFunction } = require("../src/wcdb-sidecar.cjs"); + +test("WCDB sidecar falls back from the lite cursor to the regular cursor", () => { + const regular = () => 0; + const selected = selectMessageCursorFunction("open_message_cursor_lite", { + wcdb_open_message_cursor: regular, + }); + + assert.equal(selected.fnName, "wcdb_open_message_cursor"); + assert.equal(selected.fn, regular); +}); + +test("WCDB sidecar prefers the lite cursor when the native library exports it", () => { + const regular = () => 0; + const lite = () => 0; + const selected = selectMessageCursorFunction("open_message_cursor_lite", { + wcdb_open_message_cursor: regular, + wcdb_open_message_cursor_lite: lite, + }); + + assert.equal(selected.fnName, "wcdb_open_message_cursor_lite"); + assert.equal(selected.fn, lite); +}); diff --git a/docs/rtx5060-faster-whisper-gpu.md b/docs/rtx5060-faster-whisper-gpu.md new file mode 100644 index 00000000..e4d56f68 --- /dev/null +++ b/docs/rtx5060-faster-whisper-gpu.md @@ -0,0 +1,92 @@ +# RTX 5060 faster-whisper CUDA 验收说明 + +状态:实现完成,待 RTX 5060 实机验收。更新:2026-07-29。 + +## 适用范围 + +本项目的语音转文字使用 `faster-whisper` 和 CTranslate2,不使用 PyTorch。因此,同类 PyTorch 音色克隆项目采用的 `cu128` wheel 安装路线不能直接复制到这里;本说明只复用其 RTX 50 系显卡驱动、磁盘、模型缓存与验收原则。 + +默认设备是 CPU `int8`。在有 NVIDIA GPU 的机器上,设置页可选择 NVIDIA GPU;首次识别会使用 CUDA `float16` 加载模型。CUDA 探测或模型初始化失败时,会自动改用 CPU `int8`,不影响语音播放、聊天查看或导出任务。 + +## 服务器前置检查 + +先在目标机上执行只读检查: + +```bash +nvidia-smi +python3 --version +ffmpeg -version +df -h +``` + +`nvidia-smi` 必须能列出 RTX 5060。若它不可用,先由服务器管理员修复 NVIDIA driver;不要通过反复重装 Python 包排查驱动问题。 + +本项目不需要安装 `torch`、`torchaudio` 或复用音色克隆项目的 wheelhouse。安装当前项目的语音依赖: + +```bash +uv sync --no-editable --extra voice-transcription +``` + +当前锁定的 `CTranslate2 4.8.1` 在 Linux GPU 环境需要 NVIDIA driver、CUDA 12.x 和 cuDNN 9 运行库;PyTorch 的 `cu128` wheel 不会为 CTranslate2 提供这些库。先完成只读探测,再由服务器管理员或已获授权的维护人员按 CTranslate2 对应版本的官方安装说明补齐运行库。若采用 PyPI 的用户级 CUDA/cuDNN 包,也应只安装到本项目虚拟环境,并在启动后端的同一用户进程中设置其库路径;不要修改系统 `/usr`、全局 `LD_LIBRARY_PATH` 或共享 CUDA 安装。 + +## 模型准备 + +当前默认模型是 `Systran/faster-whisper-medium`,磁盘约 1.43 GiB。生产验收前应将模型放进目标机 Hugging Face 缓存,或将 `WECHAT_TOOL_WHISPER_MODEL` 指向完整的本地模型目录;默认禁止首次识别联网下载。 + +建议把缓存放在数据盘,例如: + +```bash +export HF_HOME=/mnt/sdb/wechat-data-analysis/hf-cache +export WECHAT_TOOL_WHISPER_MODEL=medium +export WECHAT_TOOL_WHISPER_ALLOW_DOWNLOAD=0 +``` + +如果缓存尚未准备好,可在已获联网许可的维护窗口临时把 `WECHAT_TOOL_WHISPER_ALLOW_DOWNLOAD=1`,完成下载后恢复为 `0`。模型与缓存目录包含本地运行资产,不要提交到 Git。 + +## CUDA 验收 + +启动后端后,先检查 CTranslate2 是否能识别 CUDA: + +```bash +python - <<'PY' +import ctranslate2 +print("cuda device count:", ctranslate2.get_cuda_device_count()) +PY +``` + +期望设备数量大于零。然后打开应用的“设置 -> 语音转文字”: + +1. 页面应显示检测到的 NVIDIA GPU;RTX 5060 名称会由 `nvidia-smi` 提供。 +2. 选择“NVIDIA GPU”。该偏好会保存到应用 `output/runtime_settings.json`。 +3. 对一条清晰中文微信语音点击“转文字”,或用同一会话开启“语音转文字”导出。 +4. 重新打开设置页,确认“实际”显示为“NVIDIA GPU”。 +5. 导出结果仍需包含可播放的 `media/voices/*.mp3` 与中文转写文本。 + +若 CUDA 探测通过但模型初始化失败,设置页会保留“NVIDIA GPU”为已选设备,并显示自动回退原因;“实际”应显示为 CPU。此时语音转写仍应成功,随后应记录 CTranslate2、NVIDIA driver 和 CUDA 运行库版本再排查。 + +## 配置优先级 + +推理设备优先级如下: + +```text +WECHAT_TOOL_WHISPER_DEVICE 环境变量 +> 设置页保存的设备偏好 +> 默认 CPU +``` + +部署脚本若设置了 `WECHAT_TOOL_WHISPER_DEVICE`,界面会标记为“由启动环境变量固定”,避免用户误以为修改已生效。未设置该环境变量时,使用设置页选择 CPU 或 NVIDIA GPU。 + +## 验收记录 + +在 RTX 5060 服务器完成实测后,记录以下信息: + +```text +nvidia-smi 的 GPU 名称、Driver Version、CUDA Version +Python / faster-whisper / CTranslate2 版本 +CTranslate2 CUDA device count +设置页检测结果、已选设备、实际设备、是否发生回退 +单条语音首次识别耗时与缓存后二次识别耗时 +导出中的语音数量、成功/失败/缓存统计 +``` + +不要为了制造回退场景而在共享服务器上卸载驱动、修改系统 CUDA、重启机器或停止他人进程。自动回退路径由本地单元测试覆盖;服务器只验证真实 NVIDIA GPU 成功路径。 diff --git a/frontend/assets/css/chat.css b/frontend/assets/css/chat.css index 8fedaae6..8dc1aa0c 100644 --- a/frontend/assets/css/chat.css +++ b/frontend/assets/css/chat.css @@ -213,8 +213,18 @@ /* 语音消息样式 - 微信风格 */ .wechat-voice-wrapper { display: flex; + flex-direction: column; width: 100%; position: relative; + gap: 6px; +} + +.wechat-voice-wrapper--received { + align-items: flex-start; +} + +.wechat-voice-wrapper--sent { + align-items: flex-end; } .wechat-voice-bubble { @@ -267,6 +277,68 @@ border-radius: 2px; } +/* 语音转文字气泡 - 微信风格:语音下方独立气泡,无尖角,随内容收缩 */ +.wechat-voice-transcript { + width: fit-content; + max-width: min(404px, 100%); + padding: 8px 12px; + border-radius: var(--message-radius); + font-size: 14px; + line-height: 1.55; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.wechat-voice-transcript--received { + background: var(--chat-bubble-received); + color: var(--chat-bubble-received-text); +} + +.wechat-voice-transcript--sent { + background: var(--chat-bubble-sent); + color: var(--chat-bubble-sent-text); +} + +.wechat-voice-transcript__action, +.wechat-voice-transcript__retry { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0; + border: none; + background: none; + color: inherit; + font: inherit; + opacity: 0.85; + cursor: pointer; +} + +.wechat-voice-transcript__action:hover, +.wechat-voice-transcript__retry:hover { + opacity: 1; +} + +.wechat-voice-transcript__status { + display: inline-flex; + align-items: center; + gap: 6px; + opacity: 0.75; +} + +.wechat-voice-transcript__text { + margin: 0; +} + +.wechat-voice-transcript--error .wechat-voice-transcript__error { + opacity: 0.85; +} + +.wechat-voice-transcript__retry { + margin-left: 8px; + text-decoration: underline; + text-underline-offset: 2px; +} + .wechat-voice-content { display: flex; align-items: center; diff --git a/frontend/components/SettingsDialog.vue b/frontend/components/SettingsDialog.vue index bb6ce8f2..d5cd7d93 100644 --- a/frontend/components/SettingsDialog.vue +++ b/frontend/components/SettingsDialog.vue @@ -251,6 +251,69 @@ +
+
语音转文字
+
+
+
+
+
推理设备
+
CPU 兼容所有设备;NVIDIA GPU 使用 CUDA 加速,初始化失败会自动回退 CPU。
+
+
+ + +
+
+ +
正在检测本地 Whisper 与 CUDA 状态...
+ +
{{ voiceDeviceMessage }}
+ +
+
+
+
MCP 接入
@@ -457,6 +520,7 @@ const api = useApi() const settingNavItems = [ { key: 'desktop', label: '桌面行为', hint: '启动 / 关闭 / 端口' }, + { key: 'voice', label: '语音转文字', hint: 'CPU / NVIDIA GPU' }, { key: 'mcp', label: 'MCP 接入', hint: '手机 / Skill / 工具' }, { key: 'startup', label: '启动偏好', hint: '默认页面' }, { key: 'updates', label: '更新', hint: '版本信息 / 检查更新' }, @@ -467,6 +531,7 @@ const activeSection = ref(settingNavItems[0].key) const contentScrollRef = ref(null) const desktopSectionRef = ref(null) const desktopLogFileRef = ref(null) +const voiceSectionRef = ref(null) const mcpSectionRef = ref(null) const startupSectionRef = ref(null) const updatesSectionRef = ref(null) @@ -580,6 +645,43 @@ const desktopLogFileText = computed(() => { return v || '—' }) +const voiceStatusLoading = ref(false) +const voiceDeviceBusy = ref(false) +const voiceDeviceError = ref('') +const voiceDeviceMessage = ref('') +const voiceDevicePreference = ref('cpu') +const voiceDeviceSource = ref('default') +const voiceActiveDevice = ref('') +const voiceModel = ref('medium') +const voiceModelReady = ref(false) +const voiceModelDownloadRequired = ref(false) +const voiceStatusReason = ref('') +const voiceCuda = ref(null) +const voiceFallbackReason = ref('') +const voiceDeviceLocked = computed(() => voiceDeviceSource.value === 'env') +const voiceCudaAvailable = computed(() => !!voiceCuda.value?.available) +const voiceCudaReason = computed(() => String(voiceCuda.value?.reason || '').trim()) +const voiceModelText = computed(() => String(voiceModel.value || 'medium').trim() || 'medium') +const voiceModelStatusText = computed(() => { + if (voiceModelReady.value) return '已就绪' + if (voiceModelDownloadRequired.value) return '未缓存,首次使用时下载' + return '未准备好' +}) +const voiceDeviceLabel = computed(() => voiceDevicePreference.value === 'cuda' ? 'NVIDIA GPU' : 'CPU') +const voiceActiveDeviceLabel = computed(() => { + if (voiceActiveDevice.value === 'cuda') return 'NVIDIA GPU' + if (voiceActiveDevice.value === 'cpu') return 'CPU' + return '尚未加载' +}) +const voiceCudaStatusText = computed(() => { + const devices = Array.isArray(voiceCuda.value?.devices) ? voiceCuda.value.devices : [] + const labels = devices.map((item) => String(item?.name || '').trim()).filter(Boolean) + if (voiceCudaAvailable.value) { + return labels.length ? `已检测到 NVIDIA GPU:${labels.join('、')}` : '已检测到可用的 NVIDIA CUDA 设备。' + } + return voiceCudaReason.value || '未检测到可用的 NVIDIA CUDA 设备。' +}) + const mcpLanAccessEnabled = ref(false) const mcpLanAccessLoading = ref(false) const mcpLanAccessError = ref('') @@ -691,6 +793,7 @@ const refreshDesktopOutputDirProgress = async () => { const sectionElements = computed(() => [ { key: 'desktop', el: desktopSectionRef.value }, + { key: 'voice', el: voiceSectionRef.value }, { key: 'mcp', el: mcpSectionRef.value }, { key: 'startup', el: startupSectionRef.value }, { key: 'updates', el: updatesSectionRef.value }, @@ -789,6 +892,55 @@ const waitForBackendHealth = async (timeoutMs = 30_000) => { } } +const applyVoiceTranscriptionStatus = (status) => { + if (!status || typeof status !== 'object') return + const requestedDevice = String(status.requestedDevice || status.device || 'cpu').trim().toLowerCase() + voiceDevicePreference.value = requestedDevice === 'cuda' ? 'cuda' : 'cpu' + voiceDeviceSource.value = String(status.deviceSource || 'default').trim() || 'default' + voiceActiveDevice.value = String(status.activeDevice || '').trim().toLowerCase() + voiceModel.value = String(status.model || 'medium').trim() || 'medium' + voiceModelReady.value = status.modelReady === true + voiceModelDownloadRequired.value = status.modelDownloadRequired === true + voiceStatusReason.value = String(status.reason || '').trim() + voiceCuda.value = status.cuda && typeof status.cuda === 'object' ? status.cuda : null + voiceFallbackReason.value = String(status.fallbackReason || '').trim() +} + +const refreshVoiceTranscriptionStatus = async () => { + if (!process.client || typeof window === 'undefined') return + voiceStatusLoading.value = true + voiceDeviceError.value = '' + try { + applyVoiceTranscriptionStatus(await api.getVoiceTranscriptionStatus()) + } catch (e) { + voiceDeviceError.value = e?.message || '读取语音转文字运行状态失败' + } finally { + voiceStatusLoading.value = false + } +} + +const setVoiceDevice = async (device) => { + const next = String(device || '').trim().toLowerCase() + if (!['cpu', 'cuda'].includes(next) || voiceDeviceBusy.value || voiceDeviceLocked.value) return + if (next === 'cuda' && !voiceCudaAvailable.value) return + + voiceDeviceBusy.value = true + voiceDeviceError.value = '' + voiceDeviceMessage.value = '' + try { + const resp = await api.setVoiceTranscriptionDevice(next) + applyVoiceTranscriptionStatus(resp?.configuration || resp) + voiceDeviceMessage.value = next === 'cuda' + ? '已选择 NVIDIA GPU;下一次语音识别会尝试 CUDA,失败时自动回退 CPU。' + : '已切换为 CPU;下一次语音识别会使用 CPU int8。' + } catch (e) { + voiceDeviceError.value = e?.message || '设置语音转文字推理设备失败' + await refreshVoiceTranscriptionStatus() + } finally { + voiceDeviceBusy.value = false + } +} + const copyMcpText = async (key, text) => { if (!process.client || typeof window === 'undefined') return const value = String(text || '').trim() @@ -1290,6 +1442,7 @@ const refreshSettingsDialogData = async () => { const tasks = [ refreshDesktopBackendPort(), + refreshVoiceTranscriptionStatus(), refreshMcpLanAccess(), refreshMcpToken(), refreshBackendLogFileInfo(), diff --git a/frontend/components/chat/ChatExportDialog.vue b/frontend/components/chat/ChatExportDialog.vue index 11241764..cce0a401 100644 --- a/frontend/components/chat/ChatExportDialog.vue +++ b/frontend/components/chat/ChatExportDialog.vue @@ -206,6 +206,33 @@ {{ opt.label }}
+ +
定位引用消息 + + 正在检查本地模型… + {{ voiceTranscriptionUnavailableReason }} + + + 正在转文字… + +

{{ message.voiceTranscript || '未识别到文字' }}

+ +
+ + diff --git a/frontend/components/wrapped/cards/Card01CyberSchedule.vue b/frontend/components/wrapped/cards/Card01CyberSchedule.vue index 7bdf2e07..c85cef58 100644 --- a/frontend/components/wrapped/cards/Card01CyberSchedule.vue +++ b/frontend/components/wrapped/cards/Card01CyberSchedule.vue @@ -108,15 +108,15 @@

今年的第一条消息({{ yearFirstDateLabel }} {{ yearFirstSent.time }})发给了 {{ yearFirstSent.displayName }}:「{{ yearFirstSent.content || '...' }}。 @@ -137,7 +137,7 @@ :date="earliestSent.date" :display-name="earliestSent.displayName" :masked-name="earliestSent.maskedName" - :avatar-url="earliestSent.avatarUrl" + :avatar-url="resolveMediaUrl(earliestSent.avatarUrl)" :content="earliestSent.content" label="最早的一条" :delay="0" @@ -149,7 +149,7 @@ :date="latestSent.date" :display-name="latestSent.displayName" :masked-name="latestSent.maskedName" - :avatar-url="latestSent.avatarUrl" + :avatar-url="resolveMediaUrl(latestSent.avatarUrl)" :content="latestSent.content" label="最晚的一条" :delay="600" @@ -163,19 +163,87 @@ :hour-labels="card.data?.hourLabels" :matrix="card.data?.matrix" :total-messages="card.data?.totalMessages || 0" + :is-active="isActive" />

+ + +
+ + +
+
+
NIGHT WATCH
+

深夜守夜人

+

+ 今年凌晨 0:00 - 5:59,你留下了 + {{ formatInt(nightTotal) }} + 条深夜单聊消息,其中 + {{ formatInt(nightMine) }} + 条由你发出。 +

+
+ + +
+ + {{ nightAvatarFallback }} +
+
+ +

+ 陪你守夜最多的是「{{ nightPartner.displayName }}」, + 夜空被 TA 点亮了 + {{ nightShareDisplay }}%。 +

+ + +
+
+
{{ nightMoment.time }}
+
{{ nightMomentDateLabel }} · 最晚的一刻
+
+
+
+ {{ nightMoment.direction === 'sent' ? '你说' : 'TA 说' }} +
+
{{ nightMoment.content || '...' }}
+
+
+
+ + diff --git a/frontend/components/wrapped/cards/Card02MessageChars.vue b/frontend/components/wrapped/cards/Card02MessageChars.vue index bb5fd0a6..e21e4d80 100644 --- a/frontend/components/wrapped/cards/Card02MessageChars.vue +++ b/frontend/components/wrapped/cards/Card02MessageChars.vue @@ -21,7 +21,7 @@ - + @@ -30,7 +30,9 @@ import MessageCharsChart from '~/components/wrapped/visualizations/MessageCharsC const props = defineProps({ card: { type: Object, required: true }, - variant: { type: String, default: 'panel' } // 'panel' | 'slide' + variant: { type: String, default: 'panel' }, // 'panel' | 'slide' + // deck 翻到本页时置 true,首次为 true 触发入场动画;false 时暂停循环动画。 + isActive: { type: Boolean, default: true } }) const nfInt = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }) @@ -39,4 +41,3 @@ const formatInt = (n) => nfInt.format(Math.round(Number(n) || 0)) const sentChars = computed(() => Number(props.card?.data?.sentChars || 0)) const receivedChars = computed(() => Number(props.card?.data?.receivedChars || 0)) - diff --git a/frontend/components/wrapped/cards/Card03ReplySpeed.vue b/frontend/components/wrapped/cards/Card03ReplySpeed.vue index 6d27cb51..7edb7708 100644 --- a/frontend/components/wrapped/cards/Card03ReplySpeed.vue +++ b/frontend/components/wrapped/cards/Card03ReplySpeed.vue @@ -1,5 +1,11 @@ + + diff --git a/frontend/composables/chat/useChatExport.js b/frontend/composables/chat/useChatExport.js index 746bdcdc..66b08ef8 100644 --- a/frontend/composables/chat/useChatExport.js +++ b/frontend/composables/chat/useChatExport.js @@ -11,6 +11,7 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte const exportFormat = ref('html') const exportDownloadRemoteMedia = ref(true) const exportHtmlPageSize = ref(1000) + const exportTranscribeVoice = ref(false) const exportMessageTypeOptions = [ { value: 'text', label: '文本' }, { value: 'image', label: '图片' }, @@ -570,6 +571,7 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte exportFormat.value = 'html' exportDownloadRemoteMedia.value = true exportHtmlPageSize.value = 1000 + exportTranscribeVoice.value = false const defaultListTab = selectedContact.value?.username ? 'current' : 'all' exportScope.value = 'selected' exportListTab.value = defaultListTab @@ -694,10 +696,21 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte const mediaKinds = Array.from(mediaKindSet) const includeMedia = !privacyMode.value && mediaKinds.length > 0 + const transcribeVoice = ( + !privacyMode.value + && selectedTypeSet.has('voice') + && !!exportTranscribeVoice.value + ) isExportCreating.value = true exportAutoSavedFor.value = '' try { + if (transcribeVoice) { + const status = await api.getVoiceTranscriptionStatus() + if (!status?.available) { + throw new Error(String(status?.reason || '本地 Whisper 当前不可用,请检查模型配置。')) + } + } const response = await api.createChatExport({ account: selectedAccount.value, source: 'auto', @@ -715,7 +728,8 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte html_page_size: Math.max(0, Math.floor(Number(exportHtmlPageSize.value || 1000))), output_dir: isDesktopExportRuntime() ? String(exportFolder.value || '').trim() : null, privacy_mode: !!privacyMode.value, - file_name: exportFileName.value || null + file_name: exportFileName.value || null, + transcribe_voice: transcribeVoice }) exportJob.value = response?.job || null @@ -753,6 +767,7 @@ export const useChatExport = ({ api, apiBase, contacts, selectedAccount, selecte exportFormat, exportDownloadRemoteMedia, exportHtmlPageSize, + exportTranscribeVoice, exportMessageTypeOptions, exportMessageTypes, areAllExportMessageTypesSelected, diff --git a/frontend/composables/chat/useChatMessages.js b/frontend/composables/chat/useChatMessages.js index 60c69905..3a62082e 100644 --- a/frontend/composables/chat/useChatMessages.js +++ b/frontend/composables/chat/useChatMessages.js @@ -84,6 +84,35 @@ export const useChatMessages = ({ const voiceRefs = new Map() const currentPlayingVoice = ref(null) const playingVoiceId = ref(null) + const voiceTranscriptionStatus = ref(null) + const voiceTranscriptionStatusLoading = ref(false) + let voiceTranscriptionStatusPromise = null + const voiceTranscriptionStatusKnown = computed(() => !!voiceTranscriptionStatus.value) + const voiceTranscriptionAvailable = computed(() => voiceTranscriptionStatus.value?.available === true) + const voiceTranscriptionUnavailableReason = computed(() => String( + voiceTranscriptionStatus.value?.reason || '本地 Whisper 模型尚未准备好。' + ).trim()) + + const refreshVoiceTranscriptionStatus = async ({ force = false } = {}) => { + if (!force && voiceTranscriptionStatus.value) return voiceTranscriptionStatus.value + if (voiceTranscriptionStatusPromise) return await voiceTranscriptionStatusPromise + voiceTranscriptionStatusLoading.value = true + voiceTranscriptionStatusPromise = (async () => { + try { + voiceTranscriptionStatus.value = await api.getVoiceTranscriptionStatus() + } catch (error) { + voiceTranscriptionStatus.value = { + available: false, + reason: String(error?.message || '无法读取本地语音转文字状态').trim() + } + } finally { + voiceTranscriptionStatusLoading.value = false + voiceTranscriptionStatusPromise = null + } + return voiceTranscriptionStatus.value + })() + return await voiceTranscriptionStatusPromise + } const highlightServerIdStr = ref('') const highlightMessageId = ref('') @@ -694,6 +723,84 @@ export const useChatMessages = ({ await playVoiceById(message?.id) } + // 批量读取当前会话语音消息的转写缓存并合并进消息列表(仅恢复展示,不触发识别)。 + // serverIdStr 为精确字符串,禁止转 Number(19 位 svr_id 超出 JS Number 安全范围)。 + const restoreVoiceTranscripts = async (username) => { + const key = String(username || '').trim() + if (!key || privacyMode?.value) return + void refreshVoiceTranscriptionStatus() + const list = allMessages.value[key] + if (!Array.isArray(list) || !list.length) return + + const pending = list.filter((m) => { + if (String(m?.renderType || '') !== 'voice') return false + if (!String(m?.serverIdStr || '').trim()) return false + const status = String(m?.voiceTranscriptStatus || 'idle') + return !status || status === 'idle' + }) + if (!pending.length) return + if (typeof api?.lookupChatVoiceTranscriptionCache !== 'function') return + + try { + const resp = await api.lookupChatVoiceTranscriptionCache({ + account: selectedAccount.value, + server_ids: pending.map((m) => String(m.serverIdStr).trim()) + }) + const items = resp?.items + if (!items || typeof items !== 'object') return + + const current = allMessages.value[key] + if (!Array.isArray(current) || !current.length) return + for (const m of current) { + const sid = String(m?.serverIdStr || '').trim() + if (!sid) continue + const status = String(m?.voiceTranscriptStatus || 'idle') + if (status && status !== 'idle') continue + const hit = items[sid] + if (!hit) continue + m.voiceTranscript = String(hit.text || '').trim() + m.voiceTranscriptLanguage = String(hit.language || '').trim() + m.voiceTranscriptModel = String(hit.model || '').trim() + m.voiceTranscriptStatus = 'success' + } + } catch {} + } + + const transcribeVoice = async (message, { force = false } = {}) => { + // 语音消息的 svr_id 是 19 位大整数,超出 JS Number 安全范围(2^53), + // 必须使用精确字符串 serverIdStr,否则后端会查不到语音数据。 + const serverId = String(message?.serverIdStr || message?.serverId || '').trim() + if (!message || !serverId || serverId === '0' || message.voiceTranscriptStatus === 'loading') return + + // 模板渲染的是 renderMessages 的浅拷贝(非响应式),直接改拷贝不会驱动 UI 更新, + // 且 computed 重算时拷贝会被替换导致状态丢失。必须修改 allMessages 中的原对象。 + const key = String(selectedContact.value?.username || '').trim() + const list = key ? allMessages.value[key] : null + const target = + (Array.isArray(list) ? list.find((item) => item?.id === message.id) : null) || message + + const capability = await refreshVoiceTranscriptionStatus({ force: true }) + if (!capability?.available) return + + target.voiceTranscriptStatus = 'loading' + target.voiceTranscriptError = '' + try { + const result = await api.transcribeChatVoice({ + account: selectedAccount.value, + server_id: serverId, + force + }) + target.voiceTranscript = String(result?.text || '').trim() + target.voiceTranscriptLanguage = String(result?.language || '').trim() + target.voiceTranscriptModel = String(result?.model || '').trim() + target.voiceTranscriptStatus = 'success' + } catch (error) { + const detail = error?.data?.detail || error?.detail + target.voiceTranscriptError = String(detail?.message || error?.message || '语音识别失败').trim() + target.voiceTranscriptStatus = 'error' + } + } + const getQuoteVoiceId = (message) => `quote-${String(message?.quoteServerId || message?.id || '')}` const playQuoteVoice = async (message) => { @@ -1146,6 +1253,8 @@ export const useChatMessages = ({ storedCount: (allMessages.value[username] || []).length }) + void restoreVoiceTranscripts(username) + messagesMeta.value = { ...messagesMeta.value, [username]: { @@ -1854,6 +1963,12 @@ export const useChatMessages = ({ voiceRefs, currentPlayingVoice, playingVoiceId, + voiceTranscriptionStatus, + voiceTranscriptionStatusLoading, + voiceTranscriptionStatusKnown, + voiceTranscriptionAvailable, + voiceTranscriptionUnavailableReason, + refreshVoiceTranscriptionStatus, highlightServerIdStr, highlightMessageId, contactProfileCardOpen, @@ -1903,6 +2018,7 @@ export const useChatMessages = ({ openResourcePreview, setVoiceRef, playVoice, + transcribeVoice, playQuoteVoice, getQuoteVoiceId, getVoiceDurationInSeconds, diff --git a/frontend/composables/useApi.js b/frontend/composables/useApi.js index a3114e9b..f3025612 100644 --- a/frontend/composables/useApi.js +++ b/frontend/composables/useApi.js @@ -447,6 +447,43 @@ export const useApi = () => { }) } + const getVoiceTranscriptionStatus = async () => { + return await request('/chat/media/voice/transcription/status') + } + + const setVoiceTranscriptionDevice = async (device) => { + return await request('/chat/media/voice/transcription/settings', { + method: 'PUT', + body: { device: String(device || '').trim().toLowerCase() } + }) + } + + const transcribeChatVoice = async (data = {}) => { + return await request('/chat/media/voice/transcription', { + method: 'POST', + body: { + account: data.account || null, + // svr_id 是 19 位大整数,超出 JS Number 安全范围,必须以字符串原样传输, + // 避免精度丢失导致后端查不到语音数据(后端 pydantic 会将精确字符串解析为 int)。 + server_id: String(data.server_id ?? '').trim(), + force: !!data.force + } + }) + } + + // 批量读取语音转写缓存(仅恢复展示,不触发识别;serverIdStr 精确字符串数组) + const lookupChatVoiceTranscriptionCache = async (data = {}) => { + return await request('/chat/media/voice/transcription/cache_lookup', { + method: 'POST', + body: { + account: data.account || null, + server_ids: Array.isArray(data.server_ids) + ? data.server_ids.map((v) => String(v ?? '').trim()).filter(Boolean) + : [] + } + }) + } + // 聊天记录导出(离线zip) const createChatExport = async (data = {}) => { return await request('/chat/exports', { @@ -469,7 +506,8 @@ export const useApi = () => { download_remote_media: !!data.download_remote_media, html_page_size: data.html_page_size != null ? Number(data.html_page_size) : 1000, privacy_mode: !!data.privacy_mode, - file_name: data.file_name || null + file_name: data.file_name || null, + transcribe_voice: !!data.transcribe_voice } }) } @@ -862,6 +900,10 @@ export const useApi = () => { saveMediaKeys, getSavedKeys, decryptAllMedia, + getVoiceTranscriptionStatus, + setVoiceTranscriptionDevice, + transcribeChatVoice, + lookupChatVoiceTranscriptionCache, createChatExport, getChatExport, listChatExports, diff --git a/frontend/composables/useCountUp.js b/frontend/composables/useCountUp.js new file mode 100644 index 00000000..b74c38b1 --- /dev/null +++ b/frontend/composables/useCountUp.js @@ -0,0 +1,62 @@ +import { ref, computed, unref, onBeforeUnmount } from 'vue' +import { gsap } from 'gsap' +import { useReducedMotion } from './useReducedMotion' + +/** + * 数字滚动(count-up)动画。 + * + * const { display, play } = useCountUp(() => props.total, { duration: 1.2 }) + * 模板中使用 {{ display }}(已做 toLocaleString 千分位格式化), + * 卡片入场(isActive 首次为 true)时调用 play()。 + * reduced-motion 偏好命中时 play() 直接呈现终值。 + */ +export function useCountUp(source, options = {}) { + const { + duration = 1.2, + ease = 'power2.out', + delay = 0, + decimals = 0, + format = (n) => Number(n).toLocaleString('zh-CN', { + maximumFractionDigits: decimals, + minimumFractionDigits: decimals, + }), + } = options + + const reducedMotion = useReducedMotion() + const current = ref(0) + let tween = null + + const targetValue = () => { + const v = Number(typeof source === 'function' ? source() : unref(source)) + return Number.isFinite(v) ? v : 0 + } + + const play = () => { + const target = targetValue() + if (tween) { tween.kill(); tween = null } + if (reducedMotion.value) { + current.value = target + return + } + const obj = { v: current.value } + tween = gsap.to(obj, { + v: target, + duration, + ease, + delay, + onUpdate: () => { current.value = obj.v }, + onComplete: () => { current.value = target; tween = null }, + }) + } + + // 立即定格到终值(跳过/离屏时使用)。 + const finish = () => { + if (tween) { tween.kill(); tween = null } + current.value = targetValue() + } + + onBeforeUnmount(() => { if (tween) tween.kill() }) + + const display = computed(() => format(decimals > 0 ? current.value : Math.round(current.value))) + return { display, value: current, play, finish } +} diff --git a/frontend/composables/useReducedMotion.js b/frontend/composables/useReducedMotion.js new file mode 100644 index 00000000..b5cf48d0 --- /dev/null +++ b/frontend/composables/useReducedMotion.js @@ -0,0 +1,22 @@ +import { ref } from 'vue' + +// 全局单例:整个应用只挂一个 matchMedia 监听。 +let _prefersReducedMotion = null + +/** + * 用户系统级"减少动态效果"偏好。 + * 命中时各卡片应跳过入场动画(直接呈现终态)并暂停循环动画。 + */ +export function useReducedMotion() { + if (_prefersReducedMotion) return _prefersReducedMotion + const prefers = ref(false) + _prefersReducedMotion = prefers + if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') { + const mq = window.matchMedia('(prefers-reduced-motion: reduce)') + prefers.value = mq.matches + const onChange = (e) => { prefers.value = e.matches } + if (typeof mq.addEventListener === 'function') mq.addEventListener('change', onChange) + else if (typeof mq.addListener === 'function') mq.addListener(onChange) + } + return prefers +} diff --git a/frontend/lib/chat/message-normalizer.js b/frontend/lib/chat/message-normalizer.js index 5f9f2911..62fb9f5e 100644 --- a/frontend/lib/chat/message-normalizer.js +++ b/frontend/lib/chat/message-normalizer.js @@ -336,6 +336,11 @@ export const createMessageNormalizer = ({ transferReceived: msg.paySubType === '3' || msg.transferStatus === '已收款' || msg.transferStatus === '已被接收', voiceUrl: normalizedVoiceUrl || '', voiceDuration: msg.voiceLength || msg.voiceDuration || '', + voiceTranscript: String(msg.voiceTranscript || '').trim(), + voiceTranscriptStatus: String(msg.voiceTranscriptStatus || (msg.voiceTranscript ? 'success' : 'idle')).trim() || 'idle', + voiceTranscriptError: String(msg.voiceTranscriptError || '').trim(), + voiceTranscriptLanguage: String(msg.voiceTranscriptLanguage || '').trim(), + voiceTranscriptModel: String(msg.voiceTranscriptModel || '').trim(), locationLat: msg.locationLat ?? null, locationLng: msg.locationLng ?? null, locationPoiname: String(msg.locationPoiname || '').trim(), diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ee60af1d..88a53a93 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,16 +9,21 @@ "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", "@pinia/nuxt": "^0.11.2", - "@vueuse/motion": "^3.0.3", "axios": "^1.11.0", "gsap": "^3.14.2", + "html-to-image": "^1.11.13", "nuxt": "^4.0.1", - "ogl": "^1.0.11", "vue": "^3.5.17", "vue-router": "^4.5.1" }, "devDependencies": { - "tailwindcss": "3.4.17" + "@vitejs/plugin-vue": "^6.0.8", + "@vue/test-utils": "^2.4.11", + "happy-dom": "^18.0.1", + "tailwindcss": "3.4.17", + "typescript": "5.9.3", + "vitest": "^3.2.7", + "vue-tsc": "3.1.4" } }, "node_modules/@alloc/quick-lru": { @@ -1661,6 +1666,13 @@ "node": ">=18.12.0" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-minify/binding-android-arm-eabi": { "version": "0.112.0", "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.112.0.tgz", @@ -3042,9 +3054,9 @@ "license": "MIT" }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, "node_modules/@rollup/plugin-alias": { @@ -3604,22 +3616,51 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, - "node_modules/@types/web-bluetooth": { - "version": "0.0.21", - "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, "license": "MIT" }, "node_modules/@unhead/vue": { @@ -3755,18 +3796,18 @@ } }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", - "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.2" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, @@ -3790,6 +3831,121 @@ "vue": "^3.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.28", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", @@ -3805,6 +3961,35 @@ "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", "license": "MIT" }, + "node_modules/@volar/typescript": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", + "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/typescript/node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/@volar/typescript/node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@vue-macros/common": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz", @@ -4104,95 +4289,25 @@ "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "license": "MIT" }, - "node_modules/@vueuse/core": { - "version": "13.9.0", - "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-13.9.0.tgz", - "integrity": "sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==", + "node_modules/@vue/test-utils": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.11.tgz", + "integrity": "sha512-GDqaqZsA6m2E5vNzej0aYiIb6BX8xV9pNSbbbXKOfEYwg7ZNblVX8suyqmUBThq8VIrgAJNxn+z72hVtUeiWHA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "13.9.0", - "@vueuse/shared": "13.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^3.0.0" }, "peerDependencies": { - "vue": "^3.5.0" - } - }, - "node_modules/@vueuse/metadata": { - "version": "13.9.0", - "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-13.9.0.tgz", - "integrity": "sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@vueuse/motion": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/@vueuse/motion/-/motion-3.0.3.tgz", - "integrity": "sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==", - "license": "MIT", - "dependencies": { - "@vueuse/core": "^13.0.0", - "@vueuse/shared": "^13.0.0", - "defu": "^6.1.4", - "framesync": "^6.1.2", - "popmotion": "^11.0.5", - "style-value-types": "^5.1.2" - }, - "optionalDependencies": { - "@nuxt/kit": "^3.13.0" - }, - "peerDependencies": { - "vue": ">=3.0.0" - } - }, - "node_modules/@vueuse/motion/node_modules/@nuxt/kit": { - "version": "3.21.0", - "resolved": "https://registry.npmmirror.com/@nuxt/kit/-/kit-3.21.0.tgz", - "integrity": "sha512-KMTLK/dsGaQioZzkYUvgfN9le4grNW54aNcA1jqzgVZLcFVy4jJfrJr5WZio9NT2EMfajdoZ+V28aD7BRr4Zfw==", - "license": "MIT", - "optional": true, - "dependencies": { - "c12": "^3.3.3", - "consola": "^3.4.2", - "defu": "^6.1.4", - "destr": "^2.0.5", - "errx": "^0.1.0", - "exsolve": "^1.0.8", - "ignore": "^7.0.5", - "jiti": "^2.6.1", - "klona": "^2.0.6", - "knitwork": "^1.3.0", - "mlly": "^1.8.0", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "rc9": "^2.1.2", - "scule": "^1.3.0", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ufo": "^1.6.3", - "unctx": "^2.5.0", - "untyped": "^2.0.0" + "@vue/compiler-dom": "3.x", + "@vue/server-renderer": "3.x", + "vue": "3.x" }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/@vueuse/shared": { - "version": "13.9.0", - "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.9.0.tgz", - "integrity": "sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "vue": "^3.5.0" + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } } }, "node_modules/abbrev": { @@ -4383,6 +4498,16 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "license": "MIT" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-kit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", @@ -4837,6 +4962,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -4892,6 +5034,16 @@ "node": ">=8" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -5084,17 +5236,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -5147,6 +5288,24 @@ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz", @@ -5540,6 +5699,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/deep-equal/-/deep-equal-1.0.1.tgz", @@ -5791,6 +5960,35 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", @@ -6032,6 +6230,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", @@ -6181,21 +6389,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/framesync": { - "version": "6.1.2", - "resolved": "https://registry.npmmirror.com/framesync/-/framesync-6.1.2.tgz", - "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==", - "license": "MIT", - "dependencies": { - "tslib": "2.4.0" - } - }, - "node_modules/framesync/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "license": "0BSD" - }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -6484,6 +6677,21 @@ "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", "license": "MIT" }, + "node_modules/happy-dom": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.1.tgz", + "integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", @@ -6532,18 +6740,18 @@ "node": ">= 0.4" } }, - "node_modules/hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", - "license": "MIT" - }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, "node_modules/http-assert": { "version": "1.5.0", "resolved": "https://registry.npmmirror.com/http-assert/-/http-assert-1.5.0.tgz", @@ -7039,6 +7247,61 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-beautify/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7445,6 +7708,13 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8175,12 +8445,6 @@ "ufo": "^1.6.1" } }, - "node_modules/ogl": { - "version": "1.0.11", - "resolved": "https://registry.npmmirror.com/ogl/-/ogl-1.0.11.tgz", - "integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==", - "license": "Unlicense" - }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", @@ -8480,6 +8744,16 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -8566,24 +8840,6 @@ "pathe": "^2.0.3" } }, - "node_modules/popmotion": { - "version": "11.0.5", - "resolved": "https://registry.npmmirror.com/popmotion/-/popmotion-11.0.5.tgz", - "integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==", - "license": "MIT", - "dependencies": { - "framesync": "6.1.2", - "hey-listen": "^1.0.8", - "style-value-types": "5.1.2", - "tslib": "2.4.0" - } - }, - "node_modules/popmotion/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "license": "0BSD" - }, "node_modules/portfinder": { "version": "1.0.37", "resolved": "https://registry.npmmirror.com/portfinder/-/portfinder-1.0.37.tgz", @@ -9236,6 +9492,13 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -9894,6 +10157,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", @@ -10020,6 +10290,13 @@ "node": ">=20.16.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -10193,22 +10470,6 @@ "integrity": "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==", "license": "ISC" }, - "node_modules/style-value-types": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/style-value-types/-/style-value-types-5.1.2.tgz", - "integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==", - "license": "MIT", - "dependencies": { - "hey-listen": "^1.0.8", - "tslib": "2.4.0" - } - }, - "node_modules/style-value-types/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "license": "0BSD" - }, "node_modules/stylehacks": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.7.tgz", @@ -10655,6 +10916,13 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -10680,6 +10948,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10773,12 +11071,11 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10817,6 +11114,13 @@ "unplugin": "^2.3.11" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/unenv": { "version": "2.0.0-rc.24", "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", @@ -11553,6 +11857,116 @@ "vue": "^3.5.0" } }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -11589,6 +12003,13 @@ "ufo": "^1.6.1" } }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.8.tgz", + "integrity": "sha512-troqCMmQodQDqUqn63NQaFi+CDSclSe7sc8VEBFqf5GFLqmGR2Ph3P2WEC7qwpRVyEWsTi/aAr4vyOe/B1hU3g==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-devtools-stub": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", @@ -11610,6 +12031,64 @@ "vue": "^3.5.0" } }, + "node_modules/vue-tsc": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.4.tgz", + "integrity": "sha512-GsRJxttj4WkmXW/zDwYPGMJAN3np/4jTzoDFQTpTsI5Vg/JKMWamBwamlmLihgSVHO66y9P7GX+uoliYxeI4Hw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.23", + "@vue/language-core": "3.1.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue-tsc/node_modules/@volar/language-core": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", + "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.23" + } + }, + "node_modules/vue-tsc/node_modules/@volar/source-map": { + "version": "2.4.23", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", + "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vue-tsc/node_modules/@vue/language-core": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.4.tgz", + "integrity": "sha512-n/58wm8SkmoxMWkUNUH/PwoovWe4hmdyPJU2ouldr3EPi1MLoS7iDN46je8CsP95SnVBs2axInzRglPNKvqMcg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.23", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -11622,6 +12101,16 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -11647,6 +12136,23 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index b527734d..f3031b6e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,21 +6,28 @@ "build": "nuxt build", "dev": "nuxt dev", "generate": "nuxt generate", + "test": "vitest run", + "typecheck": "nuxi typecheck", "preview": "nuxt preview", "postinstall": "nuxt prepare" }, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", "@pinia/nuxt": "^0.11.2", - "@vueuse/motion": "^3.0.3", "axios": "^1.11.0", "gsap": "^3.14.2", + "html-to-image": "^1.11.13", "nuxt": "^4.0.1", - "ogl": "^1.0.11", "vue": "^3.5.17", "vue-router": "^4.5.1" }, "devDependencies": { - "tailwindcss": "3.4.17" + "@vitejs/plugin-vue": "^6.0.8", + "@vue/test-utils": "^2.4.11", + "happy-dom": "^18.0.1", + "tailwindcss": "3.4.17", + "typescript": "5.9.3", + "vitest": "^3.2.7", + "vue-tsc": "3.1.4" } } diff --git a/frontend/pages/wrapped/index.vue b/frontend/pages/wrapped/index.vue index bf7e7306..20122147 100644 --- a/frontend/pages/wrapped/index.vue +++ b/frontend/pages/wrapped/index.vue @@ -4,6 +4,9 @@ class="wrapped-deck-root relative h-screen w-full overflow-hidden transition-colors duration-500" :class="{ 'wrapped-privacy': privacyMode }" :style="{ backgroundColor: currentBg }" + role="region" + aria-roledescription="carousel" + aria-label="微信年度总结" > @@ -113,8 +116,51 @@ + +
{{ slideAnnouncement }}
+ + + + + + +
@@ -124,6 +170,7 @@ @@ -175,48 +222,56 @@ @@ -242,6 +297,7 @@ import { useApi } from '~/composables/useApi' import { storeToRefs } from 'pinia' import { usePrivacyStore } from '~/stores/privacy' +import { useReducedMotion } from '~/composables/useReducedMotion' useHead({ title: '年度总结 · WeChat Wrapped', @@ -271,6 +327,8 @@ const report = ref(null) // If user clicks "强制刷新", pass refresh=true for subsequent per-card requests in this session. const refreshCards = ref(false) let reportToken = 0 +// reload 中后端 snap 年份回写 year 时置位,抑制 watch(year) 的二次 reload。 +let suppressYearWatch = false const availableYears = ref([]) const yearOptions = computed(() => { @@ -284,10 +342,25 @@ const yearOptions = computed(() => { }) const deckEl = ref(null) +const trackEl = ref(null) const viewportHeight = ref(0) const activeIndex = ref(0) const navLocked = ref(false) const wheelAcc = ref(0) +let lastWheelAt = 0 + +const reducedMotion = useReducedMotion() + +// 触屏/笔跟手拖拽状态(鼠标仍走滚轮翻页) +const dragging = ref(false) +const dragOffset = ref(0) +let dragPointerId = null +let dragStartY = 0 +let dragLastY = 0 +let dragLastT = 0 +let dragVelocity = 0 // px/ms,向下为正 + +const exporting = ref(false) // 允许子卡片隐藏 deck 顶部 UI(如关键词卡片 storm 阶段) const deckChromeHidden = ref(false) @@ -305,7 +378,11 @@ const slides = computed(() => { // 年度总结沿用旧版浅绿色底色,避免继承聊天页灰底或引导页绿底。 const currentBg = '#F3FFF8' -const deckTrackClass = computed(() => 'z-10') +// reduced-motion 时把 700ms 翻页过渡降为 150ms +const deckTrackClass = computed(() => [ + 'z-10', + reducedMotion.value ? 'duration-150' : 'duration-700' +]) const applyViewportBg = () => { if (!import.meta.client) return @@ -318,8 +395,11 @@ const slideStyle = computed(() => ( )) const trackStyle = computed(() => { - const dy = viewportHeight.value > 0 ? -activeIndex.value * viewportHeight.value : 0 - return { transform: `translate3d(0, ${dy}px, 0)` } + const base = viewportHeight.value > 0 ? -activeIndex.value * viewportHeight.value : 0 + const style = { transform: `translate3d(0, ${base + dragOffset.value}px, 0)` } + // 拖拽期间关闭过渡以保证跟手;松手后恢复类上的过渡完成收尾/回弹 + if (dragging.value) style.transition = 'none' + return style }) const clampIndex = (i) => { @@ -338,6 +418,49 @@ const goBack = async () => { const next = () => goTo(activeIndex.value + 1) const prev = () => goTo(activeIndex.value - 1) +// 进度圆点数据:封面 + 各卡片(标题用于 tooltip,loading 用于细环 spinner) +const dotItems = computed(() => { + const cards = Array.isArray(report.value?.cards) ? report.value.cards : [] + return [ + { title: '封面', loading: false }, + ...cards.map((c, i) => ({ + title: String(c?.title || `第 ${i + 2} 页`), + loading: c?.status === 'loading' + })) + ] +}) + +const onDotSelect = (i) => { + goTo(i) + lockNav() +} + +// 翻页后播报给屏幕阅读器的文案 +const slideAnnouncement = computed(() => { + const item = dotItems.value[activeIndex.value] + return `第 ${activeIndex.value + 1} 页 · ${item?.title || ''}` +}) + +// 导出当前 slide 为 PNG(html-to-image 按需加载) +const exportActiveSlide = async () => { + if (exporting.value) return + const el = trackEl.value?.children?.[activeIndex.value] + if (!el) return + exporting.value = true + try { + const { toPng } = await import('html-to-image') + const dataUrl = await toPng(el, { pixelRatio: 2, backgroundColor: currentBg }) + const a = document.createElement('a') + a.href = dataUrl + a.download = `wechat-wrapped-${year.value}-${activeIndex.value + 1}.png` + a.click() + } catch (e) { + window.alert(`保存图片失败:${e?.message || e}`) + } finally { + exporting.value = false + } +} + const lockNav = () => { navLocked.value = true if (navUnlockTimer) clearTimeout(navUnlockTimer) @@ -384,6 +507,11 @@ const onWheel = (e) => { e.preventDefault() if (navLocked.value) return + // 慢滚间隔过久时清零累积量,避免跨时间误触发翻页 + const now = e.timeStamp || Date.now() + if (now - lastWheelAt > 160) wheelAcc.value = 0 + lastWheelAt = now + wheelAcc.value += e.deltaY const threshold = 80 if (Math.abs(wheelAcc.value) < threshold) return @@ -399,13 +527,14 @@ const onKeydown = (e) => { if (!slides.value || slides.value.length <= 1) return if (isEditable(e.target)) return - if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ') { + // Shift+左右键留给年份选择器(WrappedYearSelector) + if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === ' ' || (e.key === 'ArrowRight' && !e.shiftKey)) { e.preventDefault() next() lockNav() return } - if (e.key === 'ArrowUp' || e.key === 'PageUp') { + if (e.key === 'ArrowUp' || e.key === 'PageUp' || (e.key === 'ArrowLeft' && !e.shiftKey)) { e.preventDefault() prev() lockNav() @@ -424,21 +553,90 @@ const onKeydown = (e) => { } } -let touchStartY = 0 -const onTouchStart = (e) => { +// —— 触屏/笔全程跟手拖拽翻页 —— +const onPointerDown = (e) => { + // 鼠标仍走滚轮翻页,只接管触屏/笔 + if (e.pointerType !== 'touch' && e.pointerType !== 'pen') return if (!slides.value || slides.value.length <= 1) return - touchStartY = e.touches?.[0]?.clientY ?? 0 + if (dragPointerId !== null) return + if (isEditable(e.target)) return + // 卡内自带 pointer 拖拽(如好友墙拍立得)已 preventDefault,deck 不抢手势; + // data-deck-nodrag 供卡内拖拽区显式声明豁免。 + if (e.defaultPrevented) return + if (e.target instanceof Element && e.target.closest('[data-deck-nodrag]')) return + + // 复用 onWheel 的内部可滚动区检测:还能滚的区域交还给原生滚动 + const scrollX = e.target instanceof Element ? e.target.closest('[data-wrapped-scroll-x]') : null + if (scrollX && scrollX.scrollWidth > scrollX.clientWidth + 1) return + const scrollY = findScrollableYAncestor(e.target) + if (scrollY && (scrollY.scrollTop > 0 || scrollY.scrollTop + scrollY.clientHeight < scrollY.scrollHeight - 1)) return + + dragPointerId = e.pointerId + dragStartY = e.clientY + dragLastY = e.clientY + dragLastT = e.timeStamp + dragVelocity = 0 + dragOffset.value = 0 + dragging.value = true + try { + deckEl.value?.setPointerCapture?.(e.pointerId) + } catch { + // 部分环境(如旧 WebView)不支持指针捕获,降级为普通监听 + } } -const onTouchEnd = (e) => { - if (!slides.value || slides.value.length <= 1) return - const endY = e.changedTouches?.[0]?.clientY ?? 0 - const dy = endY - touchStartY - if (Math.abs(dy) < 50) return - if (dy < 0) next() - else prev() + +const onPointerMove = (e) => { + if (!dragging.value || e.pointerId !== dragPointerId) return + const dy = e.clientY - dragStartY + const dt = e.timeStamp - dragLastT + if (dt > 0) dragVelocity = (e.clientY - dragLastY) / dt + dragLastY = e.clientY + dragLastT = e.timeStamp + + // 首/末页越界拖拽加 0.35 阻尼 + const overFirst = activeIndex.value <= 0 && dy > 0 + const overLast = activeIndex.value >= slides.value.length - 1 && dy < 0 + dragOffset.value = (overFirst || overLast) ? dy * 0.35 : dy +} + +// commit=false(pointercancel)时仅回弹不翻页 +const finishDrag = (commit, upTimeStamp = 0) => { + if (!dragging.value) return + const dy = dragOffset.value + dragging.value = false + dragPointerId = null + dragOffset.value = 0 + + if (!commit) return + // 手指停顿后释放:速度值已过期,视为 0,避免误翻页 + if (upTimeStamp && upTimeStamp - dragLastT > 100) dragVelocity = 0 + const threshold = Math.max(1, viewportHeight.value) * 0.25 + const byDistance = Math.abs(dy) > threshold + // 速度判定加最小位移门槛,抖动轻点不触发翻页 + const byVelocity = Math.abs(dragVelocity) > 0.5 && Math.abs(dy) > 15 + if (!byDistance && !byVelocity) return + + // 距离达标看位移方向,否则看松手瞬间速度方向(上滑=下一页) + const dir = byDistance ? (dy < 0 ? 1 : -1) : (dragVelocity < 0 ? 1 : -1) + goTo(activeIndex.value + dir) lockNav() } +const onPointerUp = (e) => { + if (e.pointerId !== dragPointerId) return + finishDrag(true, e.timeStamp) +} + +const onPointerCancel = (e) => { + if (e.pointerId !== dragPointerId) return + finishDrag(false) +} + +// 拖拽期间阻止浏览器接管触摸手势,否则会触发 pointercancel 丢失跟手 +const onDeckTouchMove = (e) => { + if (dragging.value) e.preventDefault() +} + const updateViewport = () => { const h = Math.round(deckEl.value?.getBoundingClientRect?.().height || deckEl.value?.clientHeight || window.innerHeight || 0) if (!h) return @@ -524,6 +722,15 @@ const retryCard = async (cardId) => { provide('wrappedRetryCard', retryCard) +// slide 索引 → 卡片数据加载(slide 0 为封面,无需加载) +const loadCardAtSlide = (slideIdx) => { + const cardIdx = Number(slideIdx) - 1 + if (!Number.isFinite(cardIdx) || cardIdx < 0) return + const id = Number(report.value?.cards?.[cardIdx]?.id) + if (!Number.isFinite(id)) return + void ensureCardLoaded(id) +} + const reload = async (forceRefresh = false, preserveIndex = false) => { const token = ++reportToken const keepIndex = preserveIndex ? activeIndex.value : 0 @@ -559,6 +766,8 @@ const reload = async (forceRefresh = false, preserveIndex = false) => { // Backend may snap the year to the latest available year (only years with data are selectable). const respYear = Number(resp?.year) if (Number.isFinite(respYear)) { + // 回写 snap 年份时抑制 watch(year),避免二次 reload(双请求 + 卡片闪烁) + if (respYear !== year.value) suppressYearWatch = true year.value = respYear try { await router.replace({ query: { ...route.query, year: String(respYear) } }) @@ -571,12 +780,10 @@ const reload = async (forceRefresh = false, preserveIndex = false) => { if (preserveIndex) { activeIndex.value = clampIndex(keepIndex) - const cardIdx = Number(activeIndex.value) - 1 - if (cardIdx >= 0) { - const id = Number(report.value?.cards?.[cardIdx]?.id) - if (Number.isFinite(id)) void ensureCardLoaded(id) - } + loadCardAtSlide(activeIndex.value) } + // 报告就绪后立即预取第一张卡,封面翻下来时无需等待 + loadCardAtSlide(1) } catch (e) { if (token !== reportToken) return report.value = null @@ -587,14 +794,13 @@ const reload = async (forceRefresh = false, preserveIndex = false) => { } } -// Lazy-load the active slide's card data. +// Lazy-load the active slide's card data. 同时 fire-and-forget 预取相邻卡,减少翻页等待。 watch(activeIndex, (i) => { - const cardIdx = Number(i) - 1 - if (!Number.isFinite(cardIdx) || cardIdx < 0) return - const c = report.value?.cards?.[cardIdx] - const id = Number(c?.id) - if (!Number.isFinite(id)) return - void ensureCardLoaded(id) + // reload 进行中 manifest 尚未就绪,跳过(reload 末尾已有首卡预取) + if (loading.value) return + loadCardAtSlide(i) + loadCardAtSlide(i + 1) + loadCardAtSlide(i - 1) }) onMounted(async () => { @@ -611,8 +817,12 @@ onMounted(async () => { // passive:false 才能 preventDefault,避免外层容器产生滚动/回弹 deckEl.value?.addEventListener('wheel', onWheel, { passive: false }) window.addEventListener('keydown', onKeydown) - deckEl.value?.addEventListener('touchstart', onTouchStart, { passive: true }) - deckEl.value?.addEventListener('touchend', onTouchEnd, { passive: true }) + deckEl.value?.addEventListener('pointerdown', onPointerDown) + deckEl.value?.addEventListener('pointermove', onPointerMove) + deckEl.value?.addEventListener('pointerup', onPointerUp) + deckEl.value?.addEventListener('pointercancel', onPointerCancel) + // passive:false:拖拽期间 preventDefault 阻止浏览器把触摸手势判定为滚动 + deckEl.value?.addEventListener('touchmove', onDeckTouchMove, { passive: false }) await loadAccounts() // Auto-generate once if we already have chat accounts (direct WCDB or legacy), to match "one click" expectations. @@ -631,8 +841,11 @@ onBeforeUnmount(() => { window.removeEventListener('resize', updateViewport) deckEl.value?.removeEventListener('wheel', onWheel) window.removeEventListener('keydown', onKeydown) - deckEl.value?.removeEventListener('touchstart', onTouchStart) - deckEl.value?.removeEventListener('touchend', onTouchEnd) + deckEl.value?.removeEventListener('pointerdown', onPointerDown) + deckEl.value?.removeEventListener('pointermove', onPointerMove) + deckEl.value?.removeEventListener('pointerup', onPointerUp) + deckEl.value?.removeEventListener('pointercancel', onPointerCancel) + deckEl.value?.removeEventListener('touchmove', onDeckTouchMove) if (navUnlockTimer) clearTimeout(navUnlockTimer) }) @@ -646,6 +859,10 @@ watch( // 监听年份变化(由 WrappedYearSelector v-model 触发) watch(year, async (newYear, oldYear) => { + if (suppressYearWatch) { + suppressYearWatch = false + return + } if (newYear === oldYear) return // 仅允许切换到后端报告有数据的年份 if (Array.isArray(availableYears.value) && availableYears.value.length > 0 && !availableYears.value.includes(newYear)) { diff --git a/frontend/tests/voice-transcription-message.test.js b/frontend/tests/voice-transcription-message.test.js new file mode 100644 index 00000000..44c96c53 --- /dev/null +++ b/frontend/tests/voice-transcription-message.test.js @@ -0,0 +1,127 @@ +import { mount } from '@vue/test-utils' +import { nextTick } from 'vue' +import { describe, expect, it, vi } from 'vitest' + +import MessageContent from '~/components/chat/MessageContent.vue' +import MessageItem from '~/components/chat/MessageItem.vue' + +const makeMessage = (overrides = {}) => ({ + id: 'voice-1', + serverIdStr: '1234567890123456789', + renderType: 'voice', + sender: '好友', + senderDisplayName: '好友', + isSent: false, + isGroup: false, + voiceUrl: '/api/chat/media/voice?server_id=1234567890123456789', + voiceDuration: 3000, + voiceTranscript: '', + voiceTranscriptStatus: 'idle', + voiceTranscriptError: '', + ...overrides +}) + +const makeState = () => ({ + privacyMode: false, + voiceTranscriptionStatusKnown: true, + voiceTranscriptionStatusLoading: false, + voiceTranscriptionAvailable: true, + voiceTranscriptionUnavailableReason: '', + selectedContact: { username: 'wxid_friend' }, + transcribeVoice: vi.fn(), + getVoiceWidth: () => '96px', + getVoiceDurationInSeconds: () => 3, + playVoice: vi.fn(), + setVoiceRef: vi.fn(), + playingVoiceId: null, + openMediaContextMenu: vi.fn(), + onMessageAvatarMouseEnter: vi.fn(), + onMessageAvatarMouseLeave: vi.fn(), + isMentionContactProfileCardForMessage: () => false, + contactProfileCardOpen: false, + contactProfileCardMessageId: '', + highlightServerIdStr: '', + highlightMessageId: '' +}) + +const mountOptions = { + global: { + stubs: { + ContactProfileCard: true, + ChatLocationCard: true, + FileTypeIcon: true, + LinkCard: true, + ErrorNotice: true + }, + directives: { + chatLazySrc: () => {}, + chatMediaPerf: () => {} + } + } +} + +describe('语音消息转写状态', () => { + it('MessageContent 在 message prop 被替换后立即刷新缓存文字', async () => { + const wrapper = mount(MessageContent, { + ...mountOptions, + props: { state: makeState(), message: makeMessage() } + }) + + expect(wrapper.text()).toContain('转文字') + await wrapper.setProps({ + message: makeMessage({ + voiceTranscriptStatus: 'success', + voiceTranscript: '缓存恢复的简体文字' + }) + }) + + expect(wrapper.text()).toContain('缓存恢复的简体文字') + expect(wrapper.text()).not.toContain('转文字') + }) + + it('MessageItem 跟随父级替换消息对象展示 loading、成功、失败和重试', async () => { + const state = makeState() + const wrapper = mount(MessageItem, { + ...mountOptions, + props: { state, message: makeMessage() } + }) + + await wrapper.get('.wechat-voice-transcript__action').trigger('click') + expect(state.transcribeVoice).toHaveBeenCalledTimes(1) + + await wrapper.setProps({ message: makeMessage({ voiceTranscriptStatus: 'loading' }) }) + expect(wrapper.text()).toContain('正在转文字') + + await wrapper.setProps({ + message: makeMessage({ voiceTranscriptStatus: 'success', voiceTranscript: '识别成功的文字' }) + }) + expect(wrapper.text()).toContain('识别成功的文字') + + const failedMessage = makeMessage({ + voiceTranscriptStatus: 'error', + voiceTranscriptError: 'CUDA 不可用,已回退失败' + }) + await wrapper.setProps({ message: failedMessage }) + expect(wrapper.text()).toContain('CUDA 不可用,已回退失败') + expect(wrapper.text()).toContain('重试') + + await wrapper.get('.wechat-voice-transcript__retry').trigger('click') + await nextTick() + expect(state.transcribeVoice).toHaveBeenLastCalledWith(failedMessage, { force: true }) + }) + + it('模型缺失且禁止下载时不提供转写按钮并显示原因', () => { + const state = { + ...makeState(), + voiceTranscriptionAvailable: false, + voiceTranscriptionUnavailableReason: 'Whisper 模型尚未下载到本机缓存。 当前已禁止自动下载。' + } + const wrapper = mount(MessageContent, { + ...mountOptions, + props: { state, message: makeMessage() } + }) + + expect(wrapper.find('.wechat-voice-transcript__action').exists()).toBe(false) + expect(wrapper.text()).toContain('当前已禁止自动下载') + }) +}) diff --git a/frontend/vitest.config.js b/frontend/vitest.config.js new file mode 100644 index 00000000..d4d2482c --- /dev/null +++ b/frontend/vitest.config.js @@ -0,0 +1,17 @@ +import { fileURLToPath, URL } from 'node:url' +import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '~': fileURLToPath(new URL('.', import.meta.url)), + '@': fileURLToPath(new URL('.', import.meta.url)) + } + }, + test: { + environment: 'happy-dom', + include: ['tests/**/*.test.js'] + } +}) diff --git a/key_v4.py b/key_v4.py index c0c38c1f..34d24a30 100644 --- a/key_v4.py +++ b/key_v4.py @@ -7,10 +7,18 @@ from multiprocessing import freeze_support import sys -import pymem from Crypto.Protocol.KDF import PBKDF2 from Crypto.Hash import SHA512 -import yara + +try: + import pymem +except ImportError: + pymem = None + +try: + import yara +except ImportError: + yara = None # 定义必要的常量 PROCESS_ALL_ACCESS = 0x1F0FFF @@ -50,21 +58,33 @@ def verify_worker(task): """Pool worker wrapper for imap_unordered.""" return check_chunk(*task) -# Load Windows DLLs -kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) +if os.name == 'nt': + kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) + + OpenProcess = kernel32.OpenProcess + OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] + OpenProcess.restype = wintypes.HANDLE -OpenProcess = kernel32.OpenProcess -OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] -OpenProcess.restype = wintypes.HANDLE + ReadProcessMemory = kernel32.ReadProcessMemory + ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.LPVOID, ctypes.c_size_t, + ctypes.POINTER(ctypes.c_size_t)] + ReadProcessMemory.restype = wintypes.BOOL -ReadProcessMemory = kernel32.ReadProcessMemory -ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t, - ctypes.POINTER(ctypes.c_size_t)] -ReadProcessMemory.restype = wintypes.BOOL + CloseHandle = kernel32.CloseHandle + CloseHandle.argtypes = [wintypes.HANDLE] + CloseHandle.restype = wintypes.BOOL +else: + kernel32 = None + OpenProcess = None + ReadProcessMemory = None + CloseHandle = None -CloseHandle = kernel32.CloseHandle -CloseHandle.argtypes = [wintypes.HANDLE] -CloseHandle.restype = wintypes.BOOL + +def _require_windows_runtime(): + if os.name != 'nt': + raise RuntimeError('V4 数据库密钥提取仅支持 Windows。') + if pymem is None or yara is None: + raise RuntimeError('V4 数据库密钥提取缺少 Windows 运行时依赖。') # 定义 MEMORY_BASIC_INFORMATION 结构 @@ -215,6 +235,7 @@ def is_potential_key(key: bytes) -> bool: def get_key_inner(pid, process_infos): """扫描可能为key的内存,返回密钥候选列表""" + _require_windows_runtime() process_handle = open_process(pid) rules_v4_key = r''' rule GetKeyAddrStub @@ -323,6 +344,12 @@ def recover_key(pid, db_file_path=None, internal_db_key=None): """ 主函数:从 WeChat 进程恢复密钥 """ + try: + _require_windows_runtime() + except RuntimeError as exc: + print(f"[-] {exc}") + return None + process_handle = open_process(pid) if not process_handle: print(f"[-] Failed to open process {pid}") @@ -361,6 +388,12 @@ def recover_key(pid, db_file_path=None, internal_db_key=None): if __name__ == '__main__': freeze_support() + + try: + _require_windows_runtime() + except RuntimeError as exc: + print(f"[-] {exc}") + sys.exit(1) try: pm = pymem.Pymem("Weixin.exe") diff --git a/main.py b/main.py index e828b986..34f8c94f 100644 --- a/main.py +++ b/main.py @@ -10,13 +10,23 @@ import multiprocessing import os +import sys from pathlib import Path +REPO_ROOT = Path(__file__).resolve().parent +SOURCE_ROOT = REPO_ROOT / "src" +if SOURCE_ROOT.is_dir(): + source_path = str(SOURCE_ROOT) + if source_path in sys.path: + sys.path.remove(source_path) + sys.path.insert(0, source_path) + # Keep standalone/frozen launches safe when scanner code uses multiprocessing. if __name__ == "__main__": multiprocessing.freeze_support() import uvicorn +import wechat_decrypt_tool from wechat_decrypt_tool.network_access import get_lan_access_host from wechat_decrypt_tool.runtime_settings import read_effective_backend_host, read_effective_backend_port @@ -44,6 +54,7 @@ def main(): else: print("监听地址来源: 默认值") print(f"监听地址: {host}") + print(f"代码来源: {Path(wechat_decrypt_tool.__file__).resolve()}") print(f"API文档: http://{access_host}:{port}/docs") print(f"健康检查: http://{access_host}:{port}/api/health") if lan_access_host != access_host: @@ -51,7 +62,6 @@ def main(): print("按 Ctrl+C 停止服务") print("=" * 60) - repo_root = Path(__file__).resolve().parent enable_reload = os.environ.get("WECHAT_TOOL_RELOAD", "0") == "1" # 启动API服务 @@ -60,7 +70,7 @@ def main(): host=host, port=port, reload=enable_reload, - reload_dirs=[str(repo_root / "src")] if enable_reload else None, + reload_dirs=[str(REPO_ROOT / "src")] if enable_reload else None, reload_excludes=[ "output/*", "output/**", diff --git a/native/wce_integrity/Cargo.lock b/native/wce_integrity/Cargo.lock new file mode 100644 index 00000000..9eda271a --- /dev/null +++ b/native/wce_integrity/Cargo.lock @@ -0,0 +1,845 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wce_integrity" +version = "0.1.0" +dependencies = [ + "base64", + "hex", + "html5ever", + "markup5ever_rcdom", + "once_cell", + "p256", + "pyo3", + "regex", + "serde_json", + "sha2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/native/wce_integrity/Cargo.toml b/native/wce_integrity/Cargo.toml new file mode 100644 index 00000000..a6e9f3f2 --- /dev/null +++ b/native/wce_integrity/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "wce_integrity" +version = "0.1.0" +edition = "2021" +build = "build.rs" + +[lib] +name = "wce_integrity" +crate-type = ["cdylib"] + +[dependencies] +base64 = "0.22" +hex = "0.4" +html5ever = "0.27" +markup5ever_rcdom = "0.3" +once_cell = "1.19" +p256 = { version = "0.13", features = ["ecdsa"] } +pyo3 = { version = "0.22", features = ["extension-module", "abi3-py311"] } +regex = "1.10" +serde_json = "1.0" +sha2 = "0.10" + +[build-dependencies] +regex = "1.10" diff --git a/native/wce_integrity/README.md b/native/wce_integrity/README.md new file mode 100644 index 00000000..796afd7c --- /dev/null +++ b/native/wce_integrity/README.md @@ -0,0 +1,16 @@ +# wce_integrity + +Python HTML 导出完整性组件的原生扩展源码。发布包只需要携带编译后的 `wce_integrity.pyd`,不要把本目录随用户版一起分发。 + +构建: + +```powershell +powershell -ExecutionPolicy Bypass -File .\tools\build_wce_integrity.ps1 +``` + +生产构建可以通过环境变量注入签名私钥: + +```powershell +$env:WCE_SIGNING_KEY_HEX = "<32-byte-p256-private-key-hex>" +powershell -ExecutionPolicy Bypass -File .\tools\build_wce_integrity.ps1 +``` diff --git a/native/wce_integrity/build.rs b/native/wce_integrity/build.rs new file mode 100644 index 00000000..0fab7caa --- /dev/null +++ b/native/wce_integrity/build.rs @@ -0,0 +1,113 @@ +use regex::Regex; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +const STYLE_KEY: &[u8] = b"wce-export-style-202607"; + +fn read(path: &Path) -> String { + println!("cargo:rerun-if-changed={}", path.display()); + fs::read_to_string(path) + .unwrap_or_else(|e| panic!("failed to read export style {}: {e}", path.display())) +} + +fn ui_public_dir(crate_dir: &Path) -> PathBuf { + if let Ok(value) = env::var("WCE_UI_PUBLIC_DIR") { + let path = PathBuf::from(value); + if path.is_dir() { + return path; + } + } + crate_dir + .join("..") + .join("..") + .join("frontend") + .join(".output") + .join("public") +} + +fn generated_ui_css(crate_dir: &Path) -> String { + let nuxt_dir = ui_public_dir(crate_dir).join("_nuxt"); + let mut entries: Vec = fs::read_dir(&nuxt_dir) + .unwrap_or_else(|e| panic!("Nuxt UI output missing at {}: {e}", nuxt_dir.display())) + .filter_map(Result::ok) + .map(|item| item.path()) + .filter(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .map(|name| name.starts_with("entry.") && name.ends_with(".css")) + .unwrap_or(false) + }) + .collect(); + entries.sort_by_key(|path| fs::metadata(path).map(|meta| meta.len()).unwrap_or(0)); + let entry = entries + .pop() + .unwrap_or_else(|| panic!("Nuxt entry CSS missing at {}", nuxt_dir.display())); + println!("cargo:rerun-if-changed={}", entry.display()); + + let scoped = Regex::new(r"\[data-v-[0-9a-fA-F]{8}\]").expect("valid scoped selector regex"); + let mut css = scoped.replace_all(&read(&entry), "").into_owned(); + + let mut chunks: Vec = fs::read_dir(&nuxt_dir) + .expect("Nuxt UI output remains readable") + .filter_map(Result::ok) + .map(|item| item.path()) + .filter(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .map(|name| name.contains("_username_") && name.ends_with(".css")) + .unwrap_or(false) + }) + .collect(); + chunks.sort(); + for path in chunks { + css.push('\n'); + css.push_str(&scoped.replace_all(&read(&path), "")); + } + css +} + +fn encoded_const(name: &str, value: &str) -> String { + let bytes: Vec = value + .as_bytes() + .iter() + .enumerate() + .map(|(index, byte)| (byte ^ STYLE_KEY[index % STYLE_KEY.len()]).to_string()) + .collect(); + format!("const {name}: &[u8] = &[{}];\n", bytes.join(",")) +} + +fn main() { + if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { + println!("cargo:rustc-link-arg=-undefined"); + println!("cargo:rustc-link-arg=dynamic_lookup"); + } + + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + let src = crate_dir.join("src"); + + let fallback = read(&src.join("css_fallback.css")); + let patch = read(&src.join("css_patch.css")); + let ui = generated_ui_css(&crate_dir); + let chat = format!("{ui}\n{patch}"); + let sns = format!("{chat}\n{}", read(&src.join("css_sns.css"))); + let records_project = read(&src.join("css_records_project.css")); + let records_generic = read(&src.join("css_records_generic.css")); + let contacts = read(&src.join("css_contacts.css")); + + let mut generated = format!("const STYLE_KEY: &[u8] = &{:?};\n", STYLE_KEY); + for (name, value) in [ + ("STYLE_FALLBACK", fallback.as_str()), + ("STYLE_PATCH", patch.as_str()), + ("STYLE_CHAT", chat.as_str()), + ("STYLE_SNS", sns.as_str()), + ("STYLE_RECORDS_PROJECT", records_project.as_str()), + ("STYLE_RECORDS_GENERIC", records_generic.as_str()), + ("STYLE_CONTACTS", contacts.as_str()), + ] { + generated.push_str(&encoded_const(name, value)); + } + + let out = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR")).join("styles.rs"); + fs::write(&out, generated).unwrap_or_else(|e| panic!("failed to write {}: {e}", out.display())); +} diff --git a/native/wce_integrity/src/css_contacts.css b/native/wce_integrity/src/css_contacts.css new file mode 100644 index 00000000..b443a3b9 --- /dev/null +++ b/native/wce_integrity/src/css_contacts.css @@ -0,0 +1 @@ +:root{--page:#ededed;--panel:#fff;--soft:#f7f7f7;--hover:#f5f7f5;--line:#e2e5e2;--line-strong:#d3d7d3;--text:#191919;--secondary:#555b56;--muted:#6d746e;--green:#07c160;--green-dark:#058f48;--green-soft:#e9f8ef}*{box-sizing:border-box;letter-spacing:0}html,body{margin:0;min-height:100%;background:var(--page);color:var(--text);font:13px/1.45 "Segoe UI","PingFang SC","Microsoft YaHei UI",sans-serif}.records-page{width:100%;min-height:100vh;padding:16px}.records-frame{width:min(100%,1400px);min-height:calc(100vh - 32px);margin:0 auto;overflow:hidden;border:1px solid var(--line-strong);border-radius:8px;background:var(--panel)}.masthead{position:sticky;z-index:5;top:0;display:flex;min-height:76px;align-items:center;justify-content:space-between;gap:24px;padding:14px 18px;border-bottom:1px solid var(--line);background:rgba(255,255,255,.96)}.masthead h1{margin:0;font-size:21px;font-weight:500;line-height:1.3}.masthead .count{display:block;margin-top:2px;color:var(--muted);font-size:13px}.masthead .count strong{margin:0 3px;color:var(--secondary);font-size:14px;font-weight:500}.export-meta{max-width:55%;color:var(--muted);font-size:12px;text-align:right;overflow-wrap:anywhere}.section-bar{display:flex;min-height:50px;align-items:center;justify-content:space-between;gap:16px;padding:0 18px;border-bottom:1px solid var(--line);background:var(--soft)}.section-bar strong{font-size:14px;font-weight:500}.section-bar span{color:var(--muted)}.contact-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0 14px;padding:2px 14px 18px}.contact-card{min-width:0;padding:14px 8px 12px;border-bottom:1px solid var(--line);border-radius:6px}.contact-card:hover{background:var(--hover)}.contact-head{display:grid;grid-template-columns:50px minmax(0,1fr);align-items:start;gap:11px;min-width:0}figure{width:50px;margin:0;text-align:center}.avatar-frame{position:relative;width:50px;height:50px;overflow:hidden;border-radius:8px;background:var(--green-soft)}.contact-avatar{position:absolute;inset:0;display:grid;width:100%;height:100%;place-items:center;object-fit:cover}.contact-avatar.fallback{color:var(--green-dark);font-size:15px;font-weight:500}.identity-fields,.contact-details{min-width:0}.contact-details{margin-top:8px}.contact-field{display:grid;grid-template-columns:76px minmax(0,1fr);gap:7px;min-width:0;padding:3px 0}.contact-field span{color:var(--muted);font-size:12px;white-space:nowrap}.contact-field b{min-width:0;color:var(--secondary);font-size:12px;font-weight:400;line-height:1.45;overflow-wrap:anywhere}.empty-value{color:#a6aca7}.empty-state{display:grid;min-height:260px;place-items:center;color:var(--muted)}@media(max-width:1100px){.contact-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.records-page{padding:0}.records-frame{min-height:100vh;border:0;border-radius:0}.masthead{align-items:flex-start;flex-direction:column;gap:7px;padding:14px 16px}.export-meta{max-width:100%;text-align:left}.contact-grid{grid-template-columns:minmax(0,1fr);padding-inline:9px}} diff --git a/native/wce_integrity/src/css_fallback.css b/native/wce_integrity/src/css_fallback.css new file mode 100644 index 00000000..232fb148 --- /dev/null +++ b/native/wce_integrity/src/css_fallback.css @@ -0,0 +1,11 @@ + +/* Fallback styles for chat export HTML (Nuxt build CSS not found). */ +html, body { height: 100%; } +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", + "Helvetica Neue", Helvetica, Arial, sans-serif; + background: #EDEDED; + color: #111827; +} +a { color: inherit; } diff --git a/native/wce_integrity/src/css_patch.css b/native/wce_integrity/src/css_patch.css new file mode 100644 index 00000000..a009b7fc --- /dev/null +++ b/native/wce_integrity/src/css_patch.css @@ -0,0 +1,228 @@ + +/* Offline HTML viewer patch */ +:root { + /* Keep aligned with frontend defaults (see `frontend/app.vue`). */ + --dpr: 1; + --message-radius: 4px; + --sidebar-rail-step: 48px; + --sidebar-rail-btn: 32px; + --sidebar-rail-icon: 24px; +} +html, body { height: 100%; } +body { background: #EDEDED; } + +/* Layout helpers (used by exported HTML). */ +.wce-root { height: 100vh; display: flex; overflow: hidden; background: #EDEDED; } +.wce-rail { width: 60px; min-width: 60px; max-width: 60px; background: #e8e7e7; border-right: 1px solid #e5e7eb; display: flex; flex-direction: column; } +.wce-session-panel { width: calc(var(--session-list-width, 295px) / var(--dpr)); min-width: calc(var(--session-list-width, 295px) / var(--dpr)); max-width: calc(var(--session-list-width, 295px) / var(--dpr)); background: #F7F7F7; border-right: 1px solid #e5e7eb; display: flex; flex-direction: column; min-height: 0; } +.wce-chat-area { flex: 1; display: flex; flex-direction: column; min-height: 0; background: #EDEDED; } +.wce-chat-main { flex: 1; display: flex; min-height: 0; } +.wce-chat-col { flex: 1; display: flex; flex-direction: column; min-height: 0; min-width: 0; position: relative; } +.wce-chat-header { height: calc(56px / var(--dpr)); padding: 0 calc(20px / var(--dpr)); display: flex; align-items: center; border-bottom: 1px solid #e5e7eb; background: #EDEDED; } +.wce-chat-title { font-size: 1rem; font-weight: 500; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.wce-filter-select { font-size: 0.75rem; padding: calc(6px / var(--dpr)) calc(8px / var(--dpr)); border: 0; border-radius: calc(8px / var(--dpr)); background: transparent; color: #374151; } +.wce-message-container { flex: 1; overflow: auto; padding: 16px; min-height: 0; } +.wce-pager { display: flex; align-items: center; justify-content: center; gap: calc(12px / var(--dpr)); padding: calc(6px / var(--dpr)) 0 calc(12px / var(--dpr)); } +.wce-pager-btn { font-size: 0.75rem; padding: calc(6px / var(--dpr)) calc(10px / var(--dpr)); border-radius: calc(8px / var(--dpr)); border: 1px solid #e5e7eb; background: #fff; color: #374151; cursor: pointer; } +.wce-pager-btn:hover { background: #f9fafb; } +.wce-pager-btn:disabled { opacity: 0.6; cursor: not-allowed; } +.wce-pager-status { font-size: 0.75rem; color: #6b7280; } +.wce-chat-tools { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; min-width: 0; } +.wce-tool-group { display: inline-flex; align-items: center; gap: 4px; min-width: 0; } +.wce-tool-input { height: 30px; border: 1px solid #d1d5db; border-radius: 7px; background: rgba(255,255,255,0.9); color: #111827; font-size: 12px; padding: 0 8px; outline: none; } +.wce-tool-input:focus { border-color: #07c160; box-shadow: 0 0 0 2px rgba(7,193,96,0.14); } +.wce-tool-search { width: 180px; max-width: 24vw; } +.wce-tool-date { width: 128px; } +.wce-tool-btn { height: 30px; border: 1px solid #d1d5db; border-radius: 7px; background: rgba(255,255,255,0.9); color: #374151; font-size: 12px; padding: 0 8px; cursor: pointer; white-space: nowrap; } +.wce-tool-btn:hover { background: #f9fafb; color: #111827; } +.wce-tool-btn:disabled { opacity: 0.5; cursor: not-allowed; } +.wce-tool-status { font-size: 11px; color: #6b7280; max-width: 110px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.wce-search-hit .msg-bubble, +.wce-search-hit .wechat-special-card, +.wce-search-hit .wechat-voice-bubble { outline: 2px solid rgba(250, 173, 20, 0.55); outline-offset: 2px; } +.wce-search-current .msg-bubble, +.wce-search-current .wechat-special-card, +.wce-search-current .wechat-voice-bubble { outline: 3px solid rgba(7, 193, 96, 0.85); outline-offset: 3px; } +.wce-date-located { animation: wce-date-pulse 1.2s ease-out 1; } +@keyframes wce-date-pulse { + 0% { box-shadow: 0 0 0 0 rgba(7,193,96,0.38); } + 100% { box-shadow: 0 0 0 18px rgba(7,193,96,0); } +} + +/* Attribution gate: static HTML cannot be truly tamper-proof, but this couples rendering to the signed badge. */ +html:not([data-wce-brand-ok="1"]) .wce-root, +html:not([data-wce-brand-ok="1"]) .wce-index, +html:not([data-wce-integrity-ok="1"]) .wce-root, +html:not([data-wce-integrity-ok="1"]) .wce-index { visibility: hidden; } +.wce-brand-attribution { position: fixed; right: 14px; bottom: 10px; z-index: 2000; display: flex; align-items: center; gap: 8px; max-width: min(760px, calc(100vw - 28px)); padding: 7px 12px; border: 1px solid rgba(17,24,39,0.10); border-radius: 999px; background: rgba(255,255,255,0.84); box-shadow: 0 8px 24px rgba(15,23,42,0.10); backdrop-filter: blur(8px); color: #4b5563; font-size: 11px; line-height: 1.35; white-space: nowrap; overflow: hidden; } +.wce-brand-attribution span { white-space: nowrap; } +.wce-brand-attribution a { color: #047857; text-decoration: none; font-weight: 600; } +.wce-brand-attribution a:hover { text-decoration: underline; } +.wce-brand-dot { width: 5px; height: 5px; border-radius: 999px; background: #07c160; flex: 0 0 auto; } +.wce-brand-blocker { position: fixed; inset: 0; z-index: 99999; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(237,237,237,0.96); color: #111827; } +.wce-brand-blocker-card { max-width: 520px; border: 1px solid #e5e7eb; border-radius: 16px; background: #fff; box-shadow: 0 18px 52px rgba(15,23,42,0.18); padding: 22px; } +.wce-brand-blocker-title { font-size: 16px; font-weight: 700; margin-bottom: 8px; } +.wce-brand-blocker-body { font-size: 13px; color: #4b5563; line-height: 1.7; } + +/* Single session item (middle column). */ +.wce-session-item { display: flex; align-items: center; gap: 12px; padding: 0 12px; height: 80px; border-bottom: 1px solid #f3f4f6; background: #DEDEDE; text-decoration: none; color: inherit; } +.wce-session-avatar { width: 45px; height: 45px; border-radius: 6px; overflow: hidden; background: #d1d5db; flex-shrink: 0; } +.wce-session-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } +.wce-session-meta { min-width: 0; flex: 1; } +.wce-session-name { font-size: 0.875rem; font-weight: 600; color: #111827; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.wce-session-sub { font-size: 0.75rem; color: #6b7280; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: calc(2px / var(--dpr)); } + +/* Message rows (right column). */ +.wce-msg-row { display: flex; align-items: flex-start; margin-bottom: 24px; } +.wce-msg-row-sent { justify-content: flex-end; } +.wce-msg-row-received { justify-content: flex-start; } +.wce-msg { display: flex; align-items: flex-start; max-width: 640px; } +.wce-msg-sent { flex-direction: row-reverse; } +.wce-avatar { width: calc(42px / var(--dpr)); height: calc(42px / var(--dpr)); border-radius: 6px; overflow: hidden; background: #d1d5db; flex-shrink: 0; } +.wce-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } +.wce-avatar-sent { margin-left: 12px; } +.wce-avatar-received { margin-right: 12px; } +.wce-sender-name { font-size: 0.75rem; color: #6b7280; margin-bottom: calc(4px / var(--dpr)); max-width: calc(320px / var(--dpr)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* Bubble basics (tailwind classes may override when Nuxt CSS is present). */ +.wce-bubble { padding: calc(8px / var(--dpr)) calc(12px / var(--dpr)); border-radius: var(--message-radius); font-size: 0.875rem; line-height: 1.6; white-space: pre-wrap; word-break: break-word; max-width: calc(320px / var(--dpr)); position: relative; } +.wce-bubble-sent { background: #95EC69; color: #000; } +.wce-bubble-received { background: #fff; color: #1f2937; } + +/* WeChat-like bubble tail (fallback). */ +.bubble-tail-l, .bubble-tail-r { position: relative; } +.bubble-tail-l::after { + content: ''; + position: absolute; + left: -4px; + top: 12px; + width: 12px; + height: 12px; + background: #FFFFFF; + transform: rotate(45deg); + border-radius: 2px; +} +.bubble-tail-r::after { + content: ''; + position: absolute; + right: -4px; + top: 12px; + width: 12px; + height: 12px; + background: #95EC69; + transform: rotate(45deg); + border-radius: 2px; +} + +/* System messages. */ +.wce-system { display: flex; justify-content: center; margin: 16px 0; } +.wce-system > div { font-size: 0.75rem; color: #9e9e9e; padding: calc(4px / var(--dpr)) 0; } + +/* Media blocks. */ +.wce-media-img { max-width: 240px; max-height: 240px; border-radius: var(--message-radius); display: block; object-fit: cover; } +.wce-emoji-img { width: 96px; height: 96px; object-fit: contain; display: block; } +.wce-video-wrap { position: relative; display: inline-block; border-radius: var(--message-radius); overflow: hidden; background: rgba(0,0,0,0.05); } +.wce-video-thumb { display: block; width: 220px; max-width: 260px; height: auto; max-height: 260px; object-fit: cover; } +.wce-video-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; } +.wce-video-play > div { width: 48px; height: 48px; border-radius: 9999px; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; } + +.wce-file { border: 1px solid #e5e7eb; border-radius: 10px; padding: 10px 12px; background: #fff; max-width: 320px; } +.wce-file-name { font-size: 0.8125rem; color: #111827; word-break: break-all; } +.wce-file-meta { font-size: 0.75rem; color: #6b7280; margin-top: calc(4px / var(--dpr)); } +.wce-file-actions { margin-top: 8px; } +.wce-file-actions a { font-size: 0.75rem; color: #07c160; text-decoration: none; } +.wce-file-actions a:hover { text-decoration: underline; } + +.wce-audio { width: 260px; max-width: 92vw; } +.wce-audio-actions { margin-top: 6px; } +.wce-audio-actions a { font-size: 0.75rem; color: #07c160; text-decoration: none; } +.wce-audio-actions a:hover { text-decoration: underline; } + +/* Voice message fallback styles (keep close to `frontend/pages/chat/[[username]].vue`). */ +.wechat-voice-wrapper { display: flex; flex-direction: column; width: 100%; position: relative; gap: 6px; } +.wechat-voice-wrapper--received { align-items: flex-start; } +.wechat-voice-wrapper--sent { align-items: flex-end; } +.wechat-voice-bubble { + border-radius: var(--message-radius); + position: relative; + transition: opacity 0.15s ease; + min-width: 80px; + max-width: 200px; + cursor: pointer; +} +.wechat-voice-bubble:hover { opacity: 0.85; } +.wechat-voice-bubble:active { opacity: 0.7; } +.wechat-voice-sent { background: #95EC69; } +.wechat-voice-sent::after { + content: ''; + position: absolute; + top: 50%; + right: -4px; + transform: translateY(-50%) rotate(45deg); + width: 10px; + height: 10px; + background: #95EC69; + border-radius: 2px; +} +.wechat-voice-received { background: #fff; } +.wechat-voice-received::before { + content: ''; + position: absolute; + top: 50%; + left: -4px; + transform: translateY(-50%) rotate(45deg); + width: 10px; + height: 10px; + background: #fff; + border-radius: 2px; +} +.wechat-voice-content { display: flex; align-items: center; padding: 8px 12px; gap: 8px; } +.wechat-voice-icon { width: 18px; height: 18px; flex-shrink: 0; color: #1a1a1a; } +.wechat-quote-voice-icon { width: 14px; height: 14px; color: inherit; } +.voice-icon-sent { transform: scaleX(-1); } +.wechat-voice-icon.voice-playing .voice-wave-2 { animation: voice-wave-2 1s infinite; } +.wechat-voice-icon.voice-playing .voice-wave-3 { animation: voice-wave-3 1s infinite; } +@keyframes voice-wave-2 { + 0%, 33% { opacity: 0; } + 34%, 100% { opacity: 1; } +} +@keyframes voice-wave-3 { + 0%, 66% { opacity: 0; } + 67%, 100% { opacity: 1; } +} +.wechat-voice-duration { font-size: 14px; color: #1a1a1a; } + +/* Voice transcript bubble (export page: static light theme, values mirror voice bubbles). */ +.wechat-voice-transcript { + width: fit-content; + max-width: min(404px, 100%); + padding: 8px 12px; + border-radius: var(--message-radius); + font-size: 14px; + line-height: 1.55; + white-space: pre-wrap; + overflow-wrap: anywhere; +} +.wechat-voice-transcript--received { background: #fff; color: #1a1a1a; } +.wechat-voice-transcript--sent { background: #95EC69; color: #1a1a1a; } +.wechat-voice-transcript--error { background: #fff; color: #b45309; } +.wechat-voice-unread { + position: absolute; + top: 50%; + right: -20px; + transform: translateY(-50%); + width: 8px; + height: 8px; + border-radius: 50%; + background: #e75e58; +} + +/* Index page helpers. */ +.wce-index { min-height: 100vh; background: #EDEDED; } +.wce-index-container { max-width: 880px; margin: 0 auto; padding: 24px; } +.wce-index-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; } +.wce-index-item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-bottom: 1px solid #f3f4f6; text-decoration: none; color: inherit; } +.wce-index-item:last-child { border-bottom: 0; } +.wce-index-item:hover { background: #f9fafb; } +.wce-index-title { font-size: 1.125rem; font-weight: 700; color: #111827; margin: 0 0 calc(6px / var(--dpr)) 0; } +.wce-index-sub { font-size: 0.75rem; color: #6b7280; margin: 0 0 calc(16px / var(--dpr)) 0; } diff --git a/native/wce_integrity/src/css_records_generic.css b/native/wce_integrity/src/css_records_generic.css new file mode 100644 index 00000000..db3afcde --- /dev/null +++ b/native/wce_integrity/src/css_records_generic.css @@ -0,0 +1 @@ +:root{--bg:#ededed;--panel:#fff;--text:#1f2329;--muted:#8a8f99;--green:#07c160;--line:#dde1e6}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 system-ui,-apple-system,"Microsoft YaHei",sans-serif}.page{width:min(900px,100%);margin:auto;min-height:100vh;background:#f4f4f4}.mast{position:sticky;top:0;z-index:2;padding:18px 24px;border-bottom:1px solid var(--line);background:rgba(255,255,255,.95)}h1{margin:0;font-size:20px}.meta{margin-top:5px;color:var(--muted);font-size:12px}main{padding:20px 24px 50px}.message{display:grid;grid-template-columns:42px minmax(0,1fr);gap:11px;margin:0 0 22px}.avatar{display:grid;width:40px;height:40px;place-items:center;overflow:hidden;border-radius:5px;background:#627d68;color:#fff;font-weight:700}.avatar img{width:100%;height:100%;object-fit:cover}.message-body{min-width:0}header{display:flex;align-items:center;gap:12px;margin-bottom:6px;color:#576b95}header time{color:var(--muted);font-size:11px;font-weight:400}.content{display:flex;align-items:flex-start;flex-direction:column;gap:7px}.bubble,.voice,.file,.location,.link,.payment{max-width:min(540px,100%);border-radius:6px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,.04)}.bubble{padding:9px 12px;white-space:pre-wrap;overflow-wrap:anywhere}.media{display:block;max-width:min(360px,100%);max-height:420px;border-radius:6px;object-fit:contain;background:#ddd}.image img,.video-poster img{display:block;max-width:100%;max-height:420px;border-radius:6px}.video-poster{position:relative}.video-poster span{position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-size:38px}.voice{display:flex;align-items:center;gap:10px;min-width:150px;padding:10px 12px}.voice audio{width:230px;max-width:55vw;height:30px}.file,.payment{display:flex;align-items:center;gap:12px;width:300px;padding:13px}.file small,.link small,.location small{display:block;color:var(--muted)}.file-icon{display:grid;width:46px;height:46px;place-items:center;background:#f2f3f5;color:#576b95;font-size:11px;font-weight:700}.location,.link{display:flex;width:340px;overflow:hidden;color:inherit;text-decoration:none}.location{flex-direction:column;padding:12px}.location span{color:#555}.link img{width:112px;min-height:90px;object-fit:cover}.link>div{min-width:0;padding:11px}.link p,.payment p{margin:4px 0 0;color:#666}.payment{background:#fa9d3b;color:#fff}.payment>span{display:grid;width:44px;height:44px;place-items:center;border:2px solid rgba(255,255,255,.8);border-radius:50%;font-size:20px}.payment p{color:rgba(255,255,255,.85)}.tags{display:flex;gap:5px}.tag{padding:2px 7px;border-radius:4px;background:#e9f8ef;color:#168447;font-size:11px}pre{max-width:100%;overflow:auto;padding:12px;background:#fff;border-radius:6px}@media(max-width:600px){.mast,main{padding-left:12px;padding-right:12px}.message{grid-template-columns:36px minmax(0,1fr)}.avatar{width:34px;height:34px}} diff --git a/native/wce_integrity/src/css_records_project.css b/native/wce_integrity/src/css_records_project.css new file mode 100644 index 00000000..28904ced --- /dev/null +++ b/native/wce_integrity/src/css_records_project.css @@ -0,0 +1 @@ +:root{--page:#ededed;--panel:#fff;--soft:#f7f7f7;--muted-surface:#f3f3f3;--hover:#f5f7f5;--line:#e2e5e2;--line-strong:#d3d7d3;--text:#191919;--secondary:#555b56;--muted:#6d746e;--green:#07c160;--green-dark:#058f48;--green-soft:#e9f8ef;--blue:#576b95;--blue-soft:#eef1f6;--red:#9a5050;--red-soft:#f8eded;--amber:#a2673e;--amber-soft:#fbf1e8}*{box-sizing:border-box;letter-spacing:0}html,body{margin:0;min-height:100%;background:var(--page);color:var(--text);font:14px/1.55 "Segoe UI","PingFang SC","Microsoft YaHei UI",sans-serif}a{color:inherit;text-decoration:none}.records-page{width:100%;min-height:100vh;padding:16px}.records-frame{width:min(100%,1400px);min-height:calc(100vh - 32px);margin:0 auto;overflow:hidden;border:1px solid var(--line-strong);border-radius:8px;background:var(--panel)}.masthead{position:sticky;z-index:5;top:0;display:flex;min-height:76px;align-items:center;justify-content:space-between;gap:24px;padding:14px 18px;border-bottom:1px solid var(--line);background:rgba(255,255,255,.96)}.masthead h1{margin:0;font-size:21px;font-weight:500;line-height:1.3}.masthead .count{display:block;margin-top:2px;color:var(--muted);font-size:13px}.masthead .count strong{margin:0 3px;color:var(--secondary);font-size:14px;font-weight:500}.export-meta{max-width:55%;color:var(--muted);font-size:12px;text-align:right;overflow-wrap:anywhere}.section-bar{display:flex;min-height:50px;align-items:center;justify-content:space-between;gap:16px;padding:0 18px;border-bottom:1px solid var(--line);background:var(--soft)}.section-bar strong{font-size:14px;font-weight:500}.section-bar span{color:var(--muted);font-size:13px}.empty{display:grid;min-height:260px;place-items:center;color:var(--muted)}.records-grid{display:grid;padding:2px 14px 18px}.records-grid.mini,.records-grid.payments{grid-template-columns:repeat(4,minmax(0,1fr));gap:0 14px}.records-grid.finder{width:min(100%,980px);margin:0 auto;padding-inline:18px}.records-grid.biz{grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;padding:18px}.mini-entry{display:grid;grid-template-columns:50px minmax(0,1fr);gap:12px;min-width:0;min-height:118px;align-content:center;padding:15px 8px;border-bottom:1px solid var(--line);border-radius:6px}.mini-entry:hover,.finder-entry:hover,.ledger-row:hover{background:var(--hover)}.entry-visual,.finder-visual,.ledger-avatar,.biz-avatar{display:grid;place-items:center;overflow:hidden;color:var(--green-dark);background:var(--green-soft)}.entry-visual{width:50px;height:50px;border-radius:8px}.entry-image{display:grid;width:100%;height:100%;place-items:center;object-fit:cover}.entry-image.fallback{font-size:14px;font-weight:500}.entry-content{min-width:0}.entry-head,.entry-title-row{display:flex;min-width:0;align-items:center;justify-content:space-between;gap:9px}.entry-head strong,.entry-title-row strong{min-width:0;overflow:hidden;font-size:15px;font-weight:500;text-overflow:ellipsis;white-space:nowrap}.entry-head time{flex:0 0 auto;color:var(--muted);font-size:12px}.entry-content p{display:-webkit-box;margin:5px 0 0;overflow:hidden;color:var(--secondary);font-size:13px;line-height:1.5;overflow-wrap:anywhere;-webkit-box-orient:vertical;-webkit-line-clamp:2}.entry-meta{margin-top:5px;color:var(--secondary);font-size:12px;overflow-wrap:anywhere}.entry-meta.accent{color:var(--blue)}.finder-entry{display:grid;grid-template-columns:76px minmax(0,1fr) 88px 36px;align-items:center;gap:16px;min-width:0;min-height:104px;padding:16px 6px;border-bottom:1px solid var(--line);border-radius:6px}.finder-visual{width:76px;aspect-ratio:1;border-radius:7px;background:var(--muted-surface)}.finder-visual .entry-image{padding:2px;object-fit:contain}.tag{flex:0 0 auto;padding:3px 7px;border-radius:4px;color:var(--secondary);background:var(--muted-surface);font-size:12px;font-weight:400;white-space:nowrap}.tag.live{color:var(--red);background:var(--red-soft)}.tag.replay{color:var(--blue);background:var(--blue-soft)}.tag.paid{color:var(--amber);background:var(--amber-soft)}.tag.received{color:var(--green-dark);background:var(--green-soft)}.tag.returned{color:var(--amber);background:var(--amber-soft)}.tag.expired,.tag.unknown{color:var(--muted);background:var(--muted-surface)}.open-action{display:grid;width:34px;height:34px;place-items:center;border-radius:6px;color:var(--green-dark);background:var(--green-soft);font-size:17px}.open-action.muted{color:var(--muted);background:transparent}.ledger-row{display:grid;grid-template-columns:44px minmax(0,1fr);grid-template-areas:"avatar content" "amount amount";align-items:start;align-content:start;gap:10px 11px;min-width:0;min-height:150px;padding:15px 7px;border-bottom:1px solid var(--line);border-radius:6px}.ledger-avatar{grid-area:avatar;width:44px;height:44px;border-radius:7px}.ledger-row>.entry-content{grid-area:content}.ledger-route{display:flex;min-width:0;flex-wrap:wrap;align-items:center;gap:5px 9px;margin-top:5px;color:var(--secondary);font-size:12px}.ledger-route span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ledger-route i{margin-right:5px;color:var(--muted);font-style:normal}.ledger-route b{color:var(--green-dark);font-size:11px}.ledger-meta{display:flex;min-width:0;flex-wrap:wrap;gap:5px 12px;margin-top:6px;color:var(--muted);font-size:12px}.ledger-amount{grid-area:amount;justify-self:end;color:#ff9017;font-size:20px;font-weight:500;text-align:right;overflow-wrap:anywhere}.ledger-amount.red{color:var(--red)}.ledger-amount.muted{max-width:130px;color:var(--muted);font-size:12px;font-weight:400}.biz-entry{min-width:0;overflow:hidden;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.biz-entry>p,.biz-entry>time{display:block;margin:0;padding:10px 14px}.biz-entry>p{color:var(--secondary);font-size:13px;white-space:pre-wrap}.biz-entry>time{padding-top:0;color:var(--muted);font-size:12px}.biz-main{position:relative;display:block;height:210px;overflow:hidden;background:var(--muted-surface)}.biz-main>img{width:100%;height:100%;object-fit:cover}.biz-cover-fallback{display:grid;width:100%;height:100%;place-items:center;color:var(--green-dark);background:var(--green-soft)}.biz-cover-shade{position:absolute;inset:40% 0 0;background:linear-gradient(transparent,rgba(0,0,0,.72))}.biz-main h2{position:absolute;right:14px;bottom:12px;left:14px;margin:0;color:#fff;font-size:16px;font-weight:500}.biz-sub-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 14px;border-top:1px solid var(--line);font-size:13px}.biz-sub-row span{color:var(--green-dark)}.payment-entry{padding:16px}.biz-source{display:flex;align-items:center;gap:9px;color:var(--secondary);font-size:13px}.biz-avatar{width:28px;height:28px;border-radius:50%}.payment-entry h2{margin:22px 0 12px;text-align:center;font-size:20px;font-weight:500}.payment-entry>p,.payment-entry>time{padding-inline:0}@media(max-width:1000px){.records-grid.mini,.records-grid.payments{grid-template-columns:repeat(2,minmax(0,1fr))}.records-grid.biz{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:640px){.records-page{padding:0}.records-frame{min-height:100vh;border:0;border-radius:0}.masthead{align-items:flex-start;flex-direction:column;gap:7px;padding:14px 16px}.export-meta{max-width:100%;text-align:left}.records-grid.mini,.records-grid.payments,.records-grid.biz{grid-template-columns:minmax(0,1fr);padding-inline:9px}.records-grid.finder{padding-inline:9px}.finder-entry{grid-template-columns:64px minmax(0,1fr) 34px;grid-template-areas:"visual content action" "visual status action";gap:7px 10px}.finder-visual{grid-area:visual;width:64px}.finder-entry .entry-content{grid-area:content}.finder-entry>.tag{grid-area:status;justify-self:start}.finder-entry>.open-action{grid-area:action}.finder-entry .entry-meta{display:none}.biz-main{height:190px}} diff --git a/native/wce_integrity/src/css_sns.css b/native/wce_integrity/src/css_sns.css new file mode 100644 index 00000000..e0266ce3 --- /dev/null +++ b/native/wce_integrity/src/css_sns.css @@ -0,0 +1 @@ +body{background-color:#ededed}.wse-sns-post-list>.wse-sns-post:first-child{padding-top:0}.wse-sns-post-list>.wse-sns-post:first-child>.wse-sns-post-row{padding-top:12px}.wse-live-photo video{display:none}.wse-live-photo:hover video{display:block}.wse-live-photo:hover img{display:none} diff --git a/native/wce_integrity/src/lib.rs b/native/wce_integrity/src/lib.rs new file mode 100644 index 00000000..f97f58ff --- /dev/null +++ b/native/wce_integrity/src/lib.rs @@ -0,0 +1,707 @@ +use base64::{engine::general_purpose, Engine as _}; +use html5ever::tendril::TendrilSink; +use html5ever::{local_name, namespace_url, ns, parse_document, parse_fragment, QualName}; +use markup5ever_rcdom::{Handle, NodeData, RcDom}; +use once_cell::sync::Lazy; +use p256::ecdsa::{signature::Signer, Signature, SigningKey}; +use pyo3::exceptions::PyRuntimeError; +use pyo3::prelude::*; +use regex::Regex; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::fs; +use std::io::{Cursor, Read}; + +const ASSET_SALT: &str = "wce-html-export-assets-202607"; +const PROJECT_URL: &str = "https://github.com/LifeArchiveProject/WeChatDataAnalysis"; +const BRAND_TOKEN: &str = "wce-attr-202607"; +const DEFAULT_SIGNING_KEY_HEX: &str = + "931d8704d656acc64ca808b925b1c1c1b0688b2b70ca4f3fb33de1b659a3345e"; +const RUNTIME_JS: &str = include_str!("runtime.js"); +include!(concat!(env!("OUT_DIR"), "/styles.rs")); + +static PAGE_FRAGMENT_JSON_RE: Lazy = + Lazy::new(|| Regex::new(r"(?s)\bconst\s+html\s*=\s*(?P.*?);\s*\n").expect("valid regex")); +static PAGE_FRAGMENT_GUARD_RE: Lazy = Lazy::new(|| { + Regex::new(r#"(?i)\bconst\s+seal\s*=\s*['\"](?P[0-9a-f]{64})['\"]\s*;"#) + .expect("valid regex") +}); + +fn py_err(msg: impl Into) -> PyErr { + PyRuntimeError::new_err(msg.into()) +} + +fn sha256_hex(data: &[u8]) -> String { + let digest = Sha256::digest(data); + hex::encode(digest) +} + +fn sha256_bytes(data: &[u8]) -> Vec { + Sha256::digest(data).to_vec() +} + +fn escape_attr(value: &str) -> String { + value + .replace('&', "&") + .replace('"', """) + .replace('<', "<") + .replace('>', ">") +} + +fn zip_arcname(value: &str) -> String { + let mut s = value.trim().replace('\\', "/"); + while s.starts_with('/') { + s.remove(0); + } + while s.contains("//") { + s = s.replace("//", "/"); + } + s +} + +fn base64_url_no_pad(data: &[u8]) -> String { + general_purpose::URL_SAFE_NO_PAD.encode(data) +} + +fn decode_style(data: &[u8]) -> PyResult { + let raw: Vec = data + .iter() + .enumerate() + .map(|(index, byte)| byte ^ STYLE_KEY[index % STYLE_KEY.len()]) + .collect(); + String::from_utf8(raw).map_err(|e| py_err(format!("decode export style failed: {e}"))) +} + +fn signing_key() -> PyResult { + let raw_hex = option_env!("WCE_SIGNING_KEY_HEX") + .filter(|v| !v.trim().is_empty()) + .unwrap_or(DEFAULT_SIGNING_KEY_HEX) + .trim(); + let bytes = + hex::decode(raw_hex).map_err(|e| py_err(format!("invalid signing key hex: {e}")))?; + SigningKey::from_slice(&bytes).map_err(|e| py_err(format!("invalid signing key: {e}"))) +} + +fn public_key_value() -> PyResult { + let sk = signing_key()?; + let vk = sk.verifying_key(); + let point = vk.to_encoded_point(false); + let x = point.x().ok_or_else(|| py_err("public key missing x"))?; + let y = point.y().ok_or_else(|| py_err("public key missing y"))?; + Ok(json!({ + "kty": "EC", + "crv": "P-256", + "x": base64_url_no_pad(x), + "y": base64_url_no_pad(y), + })) +} + +fn asset_paths_value(export_id: &str) -> Value { + let seed = sha256_hex(format!("{}:{}", ASSET_SALT, export_id).as_bytes()); + let short = &seed[..16]; + json!([ + format!("assets/_wce/c-{short}.css"), + format!("assets/_wce/r-{short}.js"), + format!("assets/_wce/i-{short}.js") + ]) +} + +fn sidecar_paths_value(export_id: &str) -> Value { + let seed = sha256_hex(format!("{}:manifest:{}", ASSET_SALT, export_id).as_bytes()); + let short = &seed[..16]; + json!([ + format!("assets/_wce/m-{short}.wce"), + format!("assets/_wce/s-{short}.wce") + ]) +} + +fn runtime_source() -> PyResult { + let key = public_key_value()?; + let x = key.get("x").and_then(Value::as_str).unwrap_or(""); + let y = key.get("y").and_then(Value::as_str).unwrap_or(""); + if x.is_empty() || y.is_empty() { + return Err(py_err("empty public key")); + } + Ok(RUNTIME_JS + .replace("__WCE_PUBLIC_KEY_X__", x) + .replace("__WCE_PUBLIC_KEY_Y__", y)) +} + +fn obfuscate_runtime_js(source: &str) -> String { + let key_full = sha256_bytes(format!("{}:runtime", ASSET_SALT).as_bytes()); + let key = &key_full[..17]; + let packed: Vec = source + .as_bytes() + .iter() + .enumerate() + .map(|(i, b)| b ^ key[i % key.len()]) + .collect(); + let b64 = general_purpose::STANDARD.encode(packed); + let chunks: Vec = b64 + .as_bytes() + .chunks(79) + .map(|c| String::from_utf8_lossy(c).to_string()) + .collect(); + let chunks_json = serde_json::to_string(&chunks).unwrap_or_else(|_| "[]".to_string()); + let key_json = serde_json::to_string(&key.to_vec()).unwrap_or_else(|_| "[]".to_string()); + format!( + "(()=>{{const _0={},_1={};let _2=atob(_0.join('')),_3=new Uint8Array(_2.length);for(let _4=0;_4<_2.length;_4++)_3[_4]=_2.charCodeAt(_4)^_1[_4%_1.length];let _5=(typeof TextDecoder!='undefined'?new TextDecoder('utf-8').decode(_3):String.fromCharCode.apply(null,_3));(0,Function)(_5)();}})();\n", + chunks_json, key_json + ) +} + +fn gate_style_value() -> String { + "\n".to_string() +} + +fn attribution_html_value() -> String { + format!( + "
通过 WeChatDataAnalysis 导出项目地址仅用于个人数据备份、迁移与研究,请尊重聊天参与者隐私。
\n", + escape_attr(BRAND_TOKEN), + escape_attr(PROJECT_URL) + ) +} + +fn integrity_script_tag_value(src: &str) -> String { + format!( + " \n", + escape_attr(src) + ) +} + +fn page_fragment_js_value( + export_id: &str, + arc_js: &str, + page_no: i64, + fragment_html: &str, +) -> PyResult { + let arc = zip_arcname(arc_js); + let guard = page_fragment_guard_inner(export_id, &arc, page_no, fragment_html); + let html_json = serde_json::to_string(fragment_html) + .map_err(|e| py_err(format!("encode page fragment failed: {e}")))?; + let seal_json = serde_json::to_string(&guard) + .map_err(|e| py_err(format!("encode page guard failed: {e}")))?; + Ok(format!( + "(() => {{\n const pageNo = {};\n const seal = {};\n const html = {};\n try {{\n const fn = window.__WCE_PAGE_LOADED__;\n if (typeof fn === 'function') fn(pageNo, html, seal);\n else {{\n const q = (window.__WCE_PAGE_QUEUE__ = window.__WCE_PAGE_QUEUE__ || []);\n q.push([pageNo, html, seal]);\n }}\n }} catch {{}}\n}})();\n", + page_no, seal_json, html_json + )) +} + +fn sign_manifest(canonical: &str) -> PyResult { + let sk = signing_key()?; + let sig: Signature = sk.sign(canonical.as_bytes()); + let sig_bytes = sig.to_bytes(); + Ok(base64_url_no_pad(sig_bytes.as_ref())) +} + +fn push_dom_tokens(handle: &Handle, out: &mut Vec) { + match &handle.data { + NodeData::Document => { + for child in handle.children.borrow().iter() { + push_dom_tokens(child, out); + } + } + NodeData::Element { name, attrs, .. } => { + let tag = name.local.to_string().to_ascii_lowercase(); + let mut attr_rows: Vec<(String, String)> = Vec::new(); + for attr in attrs.borrow().iter() { + let key = attr.name.local.to_string().to_ascii_lowercase(); + if key.is_empty() || key == "style" { + continue; + } + attr_rows.push((key, attr.value.to_string())); + } + attr_rows.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + let attrs_json: Vec = + attr_rows.into_iter().map(|(k, v)| json!([k, v])).collect(); + out.push(json!(["E", tag, attrs_json])); + if tag == "script" || tag == "style" { + return; + } + for child in handle.children.borrow().iter() { + push_dom_tokens(child, out); + } + } + NodeData::Text { contents } => { + let text = contents.borrow().to_string(); + if !text.trim().is_empty() { + out.push(json!(["T", text])); + } + } + _ => { + for child in handle.children.borrow().iter() { + push_dom_tokens(child, out); + } + } + } +} + +fn find_body(handle: &Handle) -> Option { + if let NodeData::Element { name, .. } = &handle.data { + if name.local.as_ref().eq_ignore_ascii_case("body") { + return Some(handle.clone()); + } + } + for child in handle.children.borrow().iter() { + if let Some(found) = find_body(child) { + return Some(found); + } + } + None +} + +fn push_fragment_document_tokens(document: &Handle, out: &mut Vec) { + if let Some(body) = find_body(document) { + for child in body.children.borrow().iter() { + push_dom_tokens(child, out); + } + return; + } + let children = document.children.borrow(); + if children.len() == 1 { + if let NodeData::Element { name, .. } = &children[0].data { + if name.local.as_ref().eq_ignore_ascii_case("html") { + for child in children[0].children.borrow().iter() { + push_dom_tokens(child, out); + } + return; + } + } + } + for child in children.iter() { + push_dom_tokens(child, out); + } +} + +fn markup_seal(text: &str, body_only: bool) -> String { + let mut tokens: Vec = Vec::new(); + if body_only { + let mut cursor = Cursor::new(text.as_bytes()); + if let Ok(dom) = parse_document(RcDom::default(), Default::default()) + .from_utf8() + .read_from(&mut cursor) + { + if let Some(body) = find_body(&dom.document) { + push_dom_tokens(&body, &mut tokens); + } + } + } else { + let context = QualName::new(None, ns!(html), local_name!("template")); + let mut cursor = Cursor::new(text.as_bytes()); + if let Ok(dom) = parse_fragment(RcDom::default(), Default::default(), context, Vec::new()) + .from_utf8() + .read_from(&mut cursor) + { + push_fragment_document_tokens(&dom.document, &mut tokens); + } + } + let payload = serde_json::to_string(&tokens).unwrap_or_else(|_| "[]".to_string()); + sha256_hex(payload.as_bytes()) +} + +fn page_fragment_guard_inner( + export_id: &str, + arc_js: &str, + page_no: i64, + fragment_html: &str, +) -> String { + let raw = format!( + "{}:page:{}:{}:{}:{}", + ASSET_SALT, + export_id, + zip_arcname(arc_js), + page_no, + fragment_html + ); + sha256_hex(raw.as_bytes()) +} + +fn page_fragment_seal_from_js(text: &str) -> String { + let Some(caps) = PAGE_FRAGMENT_JSON_RE.captures(text) else { + return String::new(); + }; + let Some(raw_json) = caps.name("json").map(|m| m.as_str()) else { + return String::new(); + }; + match serde_json::from_str::(raw_json) { + Ok(fragment) => markup_seal(&fragment, false), + Err(_) => String::new(), + } +} + +fn page_fragment_guard_from_js(text: &str) -> String { + PAGE_FRAGMENT_GUARD_RE + .captures(text) + .and_then(|caps| caps.name("seal").map(|m| m.as_str().to_ascii_lowercase())) + .unwrap_or_default() +} + +fn entry_for_bytes(arcname: &str, raw: &[u8]) -> Value { + let arc = zip_arcname(arcname); + let lower = arc.to_ascii_lowercase(); + let mut obj = serde_json::Map::new(); + obj.insert("path".to_string(), json!(arc)); + obj.insert("size".to_string(), json!(raw.len())); + obj.insert("sha256".to_string(), json!(sha256_hex(raw))); + if lower.ends_with(".html") || lower.ends_with(".htm") { + let text = String::from_utf8_lossy(raw); + obj.insert("domSha256".to_string(), json!(markup_seal(&text, true))); + } else if lower.contains("/pages/page-") && lower.ends_with(".js") { + let text = String::from_utf8_lossy(raw); + obj.insert( + "fragmentDomSha256".to_string(), + json!(page_fragment_seal_from_js(&text)), + ); + obj.insert( + "fragmentGuard".to_string(), + json!(page_fragment_guard_from_js(&text)), + ); + } + Value::Object(obj) +} + +fn valid_hex64(value: &str) -> bool { + value.len() == 64 && value.as_bytes().iter().all(|b| b.is_ascii_hexdigit()) +} + +fn obfuscate_integrity_bundle(payload: &Value, export_id: &str) -> PyResult { + let raw = serde_json::to_string(payload) + .map_err(|e| py_err(format!("encode integrity payload failed: {e}")))? + .into_bytes(); + let seed_raw = format!("{}:integrity:{}", ASSET_SALT, export_id); + let seed = Sha256::digest(seed_raw.as_bytes()); + let left: Vec = seed[..19].to_vec(); + let mut right_hasher = Sha256::new(); + right_hasher.update(&seed); + right_hasher.update(b":split"); + let right_seed = right_hasher.finalize(); + let right: Vec = right_seed[..23].to_vec(); + let key_len = left.len().max(right.len()); + let mut key = Vec::with_capacity(key_len); + for i in 0..key_len { + key.push(left[i % left.len()] ^ right[(i * 7 + 3) % right.len()] ^ ((i * 13) as u8)); + } + let packed: Vec = raw + .iter() + .enumerate() + .map(|(i, b)| b ^ key[i % key.len()]) + .collect(); + let b64 = general_purpose::STANDARD.encode(packed); + let chunks: Vec = b64 + .as_bytes() + .chunks(83) + .map(|c| String::from_utf8_lossy(c).to_string()) + .collect(); + let chunks_json = serde_json::to_string(&chunks).unwrap_or_else(|_| "[]".to_string()); + let left_json = serde_json::to_string(&left).unwrap_or_else(|_| "[]".to_string()); + let right_json = serde_json::to_string(&right).unwrap_or_else(|_| "[]".to_string()); + Ok(format!( + "(()=>{{const _p={};const _a={};const _b={};const _n=['__','WCE','_I'].join('');try{{Object.defineProperty(window,_n,{{value:{{p:_p,a:_a,b:_b}},configurable:false,writable:false}});}}catch(e){{window[_n]={{p:_p,a:_a,b:_b}};}}}})();\n", + chunks_json, left_json, right_json + )) +} + +fn build_signed_manifest( + export_id: &str, + entries: Vec, + html_assets: Value, +) -> PyResult<(Value, String, String)> { + let css_path = zip_arcname( + html_assets + .get("cssPath") + .and_then(Value::as_str) + .unwrap_or(""), + ); + let js_path = zip_arcname( + html_assets + .get("jsPath") + .and_then(Value::as_str) + .unwrap_or(""), + ); + let integrity_path = zip_arcname( + html_assets + .get("integrityPath") + .and_then(Value::as_str) + .unwrap_or(""), + ); + let manifest_path = zip_arcname( + html_assets + .get("manifestPath") + .and_then(Value::as_str) + .unwrap_or(""), + ); + let signature_path = zip_arcname( + html_assets + .get("signaturePath") + .and_then(Value::as_str) + .unwrap_or(""), + ); + + let mut pages: Vec = Vec::new(); + let mut page_seals: Vec = Vec::new(); + let mut fragment_seals: Vec = Vec::new(); + let mut files: Vec = Vec::new(); + + for item in entries.iter() { + let Some(path_raw) = item.get("path").and_then(Value::as_str) else { + continue; + }; + let path = zip_arcname(path_raw); + if path.is_empty() + || path == integrity_path + || path == manifest_path + || path == signature_path + { + continue; + } + let lower = path.to_ascii_lowercase(); + let size = item.get("size").and_then(Value::as_u64).unwrap_or(0); + let sha = item + .get("sha256") + .and_then(Value::as_str) + .unwrap_or("") + .to_ascii_lowercase(); + if valid_hex64(&sha) { + files.push(json!({"path": path, "size": size, "sha256": sha})); + } + if lower.ends_with(".html") || lower.ends_with(".htm") { + pages.push(path.clone()); + let seal = item + .get("domSha256") + .and_then(Value::as_str) + .unwrap_or("") + .to_ascii_lowercase(); + if valid_hex64(&seal) { + page_seals.push(json!([path, seal])); + } + } else if lower.contains("/pages/page-") && lower.ends_with(".js") { + let seal = item + .get("fragmentDomSha256") + .and_then(Value::as_str) + .unwrap_or("") + .to_ascii_lowercase(); + let guard = item + .get("fragmentGuard") + .and_then(Value::as_str) + .unwrap_or("") + .to_ascii_lowercase(); + if valid_hex64(&seal) { + if valid_hex64(&guard) { + fragment_seals.push(json!([path, seal, guard])); + } else { + fragment_seals.push(json!([path, seal])); + } + } + } + } + + pages.sort(); + pages.dedup(); + page_seals.sort_by(|a, b| { + a.get(0) + .and_then(Value::as_str) + .cmp(&b.get(0).and_then(Value::as_str)) + }); + fragment_seals.sort_by(|a, b| { + a.get(0) + .and_then(Value::as_str) + .cmp(&b.get(0).and_then(Value::as_str)) + }); + files.sort_by(|a, b| { + a.get("path") + .and_then(Value::as_str) + .cmp(&b.get("path").and_then(Value::as_str)) + }); + + let manifest = json!({ + "v": 2, + "e": export_id, + "a": { + "c": css_path, + "r": js_path, + "i": integrity_path, + "m": manifest_path, + "s": signature_path, + "cs": html_assets.get("cssIntegrity").and_then(Value::as_str).unwrap_or(""), + "rs": html_assets.get("jsIntegrity").and_then(Value::as_str).unwrap_or(""), + }, + "p": pages, + "h": page_seals, + "q": fragment_seals, + "f": files, + }); + let canonical = serde_json::to_string(&manifest) + .map_err(|e| py_err(format!("encode signed manifest failed: {e}")))?; + let signature = sign_manifest(&canonical)?; + Ok((manifest, canonical, signature)) +} + +#[pyfunction] +fn asset_paths(export_id: &str) -> PyResult { + serde_json::to_string(&asset_paths_value(export_id)) + .map_err(|e| py_err(format!("encode asset paths failed: {e}"))) +} + +#[pyfunction] +fn integrity_sidecar_paths(export_id: &str) -> PyResult { + serde_json::to_string(&sidecar_paths_value(export_id)) + .map_err(|e| py_err(format!("encode sidecar paths failed: {e}"))) +} + +#[pyfunction] +fn runtime_js() -> PyResult { + runtime_source().map(|source| obfuscate_runtime_js(&source)) +} + +#[pyfunction] +fn css_fallback() -> PyResult { + decode_style(STYLE_FALLBACK) +} + +#[pyfunction] +fn css_patch() -> PyResult { + decode_style(STYLE_PATCH) +} + +#[pyfunction] +fn export_css(kind: &str) -> PyResult { + let style = match kind.trim().to_ascii_lowercase().as_str() { + "chat" => STYLE_CHAT, + "sns" => STYLE_SNS, + "records-project" => STYLE_RECORDS_PROJECT, + "records-generic" => STYLE_RECORDS_GENERIC, + "contacts" => STYLE_CONTACTS, + _ => return Err(py_err(format!("unsupported export style: {kind}"))), + }; + decode_style(style) +} + +#[pyfunction] +fn gate_style() -> PyResult { + Ok(gate_style_value()) +} + +#[pyfunction] +fn attribution_html() -> PyResult { + Ok(attribution_html_value()) +} + +#[pyfunction] +fn integrity_script_tag(src: &str) -> PyResult { + Ok(integrity_script_tag_value(src)) +} + +#[pyfunction] +fn page_fragment_js( + export_id: &str, + arc_js: &str, + page_no: i64, + fragment_html: &str, +) -> PyResult { + page_fragment_js_value(export_id, arc_js, page_no, fragment_html) +} + +#[pyfunction] +fn public_key_jwk_json() -> PyResult { + let value = public_key_value()?; + serde_json::to_string(&value).map_err(|e| py_err(format!("encode public key failed: {e}"))) +} + +#[pyfunction] +fn page_fragment_guard( + export_id: &str, + arc_js: &str, + page_no: i64, + fragment_html: &str, +) -> PyResult { + Ok(page_fragment_guard_inner( + export_id, + arc_js, + page_no, + fragment_html, + )) +} + +#[pyfunction] +fn record_bytes(arcname: &str, data: &[u8]) -> PyResult { + serde_json::to_string(&entry_for_bytes(arcname, data)) + .map_err(|e| py_err(format!("encode entry failed: {e}"))) +} + +#[pyfunction] +fn record_file(filename: &str, arcname: &str) -> PyResult { + let arc = zip_arcname(arcname); + let lower = arc.to_ascii_lowercase(); + if lower.ends_with(".html") + || lower.ends_with(".htm") + || (lower.contains("/pages/page-") && lower.ends_with(".js")) + { + let raw = fs::read(filename).map_err(|e| py_err(format!("read file failed: {e}")))?; + return record_bytes(&arc, &raw); + } + + let mut file = fs::File::open(filename).map_err(|e| py_err(format!("open file failed: {e}")))?; + let mut hasher = Sha256::new(); + let mut size: u64 = 0; + let mut buffer = vec![0u8; 1024 * 1024]; + loop { + let count = file + .read(&mut buffer) + .map_err(|e| py_err(format!("read file failed: {e}")))?; + if count == 0 { + break; + } + hasher.update(&buffer[..count]); + size += count as u64; + } + let entry = json!({ + "path": arc, + "size": size, + "sha256": hex::encode(hasher.finalize()), + }); + serde_json::to_string(&entry).map_err(|e| py_err(format!("encode entry failed: {e}"))) +} + +#[pyfunction] +fn seal_export(export_id: &str, entries_json: &str, html_assets_json: &str) -> PyResult { + let entries: Vec = serde_json::from_str(entries_json) + .map_err(|e| py_err(format!("invalid entries json: {e}")))?; + let html_assets: Value = serde_json::from_str(html_assets_json) + .map_err(|e| py_err(format!("invalid html assets json: {e}")))?; + let (manifest, canonical_manifest, signature) = + build_signed_manifest(export_id, entries, html_assets)?; + let payload = json!({ + "v": 2, + "g": canonical_manifest, + "s": signature, + }); + let bundle = obfuscate_integrity_bundle(&payload, export_id)?; + let result = json!({ + "bundle": bundle, + "manifestJson": serde_json::to_string_pretty(&manifest).unwrap_or_else(|_| canonical_manifest.clone()), + "manifestCanonical": canonical_manifest, + "signature": signature, + "publicKey": public_key_value()?, + }); + serde_json::to_string(&result).map_err(|e| py_err(format!("encode seal result failed: {e}"))) +} + +#[pymodule] +fn wce_integrity(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(asset_paths, m)?)?; + m.add_function(wrap_pyfunction!(integrity_sidecar_paths, m)?)?; + m.add_function(wrap_pyfunction!(runtime_js, m)?)?; + m.add_function(wrap_pyfunction!(css_fallback, m)?)?; + m.add_function(wrap_pyfunction!(css_patch, m)?)?; + m.add_function(wrap_pyfunction!(export_css, m)?)?; + m.add_function(wrap_pyfunction!(gate_style, m)?)?; + m.add_function(wrap_pyfunction!(attribution_html, m)?)?; + m.add_function(wrap_pyfunction!(integrity_script_tag, m)?)?; + m.add_function(wrap_pyfunction!(page_fragment_js, m)?)?; + m.add_function(wrap_pyfunction!(public_key_jwk_json, m)?)?; + m.add_function(wrap_pyfunction!(page_fragment_guard, m)?)?; + m.add_function(wrap_pyfunction!(record_bytes, m)?)?; + m.add_function(wrap_pyfunction!(record_file, m)?)?; + m.add_function(wrap_pyfunction!(seal_export, m)?)?; + Ok(()) +} diff --git a/native/wce_integrity/src/runtime.js b/native/wce_integrity/src/runtime.js new file mode 100644 index 00000000..8deacde1 --- /dev/null +++ b/native/wce_integrity/src/runtime.js @@ -0,0 +1,2258 @@ +(() => { + const updateDprVar = () => { + try { + document.documentElement.style.setProperty('--dpr', '1') + } catch {} + } + + const hideJsMissingBanner = () => { + try { + const el = document.getElementById('wceJsMissing') + if (el) el.style.display = 'none' + } catch {} + } + + let wceTamperLocked = false + + const showTamperBlock = () => { + wceTamperLocked = true + try { document.documentElement.setAttribute('data-wce-brand-ok', '0') } catch {} + try { document.documentElement.setAttribute('data-wce-integrity-ok', '0') } catch {} + if (document.getElementById('wceBrandBlocker')) return + const wrap = document.createElement('div') + wrap.id = 'wceBrandBlocker' + wrap.className = 'wce-brand-blocker' + wrap.innerHTML = '
导出页校验失败
该 HTML 导出页已被修改或不完整。请使用原始完整导出文件,或重新导出。
' + try { document.body.appendChild(wrap) } catch {} + } + + const initBrandAttribution = () => { + const decode = (value) => { + try { + const bin = atob(String(value || '')) + const bytes = new Uint8Array(bin.length) + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i) + if (typeof TextDecoder !== 'undefined') return new TextDecoder('utf-8').decode(bytes) + let out = '' + for (let i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]) + return out + } catch { + return '' + } + } + + const requiredName = decode(['V2VDaGF0', 'RGF0YUFu', 'YWx5c2lz'].join('')) + const requiredPath = decode(['TGlmZUFyY2hpdmVQcm9qZWN0', 'L1dlQ2hhdERhdGFBbmFseXNpcw=='].join('')) + const requiredToken = decode(['d2Nl', 'LWF0dHIt', 'MjAyNjA3'].join('')) + + const block = () => { + showTamperBlock() + } + + const verify = () => { + const el = document.getElementById('wceBrandAttribution') + if (!el) return false + const token = String(el.getAttribute('data-wce-brand') || '').trim() + const text = String(el.textContent || '') + const hrefs = Array.from(el.querySelectorAll('a[href]') || []).map((a) => String(a.getAttribute('href') || a.href || '')) + return token === requiredToken && text.includes(requiredName) && hrefs.some((href) => href.includes(requiredPath)) + } + + const apply = () => { + if (wceTamperLocked) { + showTamperBlock() + return false + } + if (verify()) { + try { document.documentElement.setAttribute('data-wce-brand-ok', '1') } catch {} + return true + } + block() + return false + } + + apply() + try { + const observer = new MutationObserver(() => { apply() }) + observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true }) + } catch {} + } + + const initExportIntegrity = async () => { + const toUtf8 = (bytes) => { + try { + if (typeof TextDecoder !== 'undefined') return new TextDecoder('utf-8').decode(bytes) + let out = '' + for (let i = 0; i < bytes.length; i += 8192) out += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + 8192))) + return out + } catch { + return '' + } + } + + const utf8Bytes = (text) => { + try { + if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(String(text || '')) + } catch {} + const encoded = unescape(encodeURIComponent(String(text || ''))) + const bytes = new Uint8Array(encoded.length) + for (let i = 0; i < encoded.length; i++) bytes[i] = encoded.charCodeAt(i) + return bytes + } + + const sha256Hex = (text) => { + const bytes = utf8Bytes(text) + const K = [ + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 + ] + let H = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19] + const paddedLen = (((bytes.length + 9 + 63) >> 6) << 6) + const msg = new Uint8Array(paddedLen) + msg.set(bytes) + msg[bytes.length] = 0x80 + const dv = new DataView(msg.buffer) + dv.setUint32(paddedLen - 8, Math.floor(bytes.length / 0x20000000), false) + dv.setUint32(paddedLen - 4, (bytes.length << 3) >>> 0, false) + const w = new Uint32Array(64) + const rotr = (x, n) => (x >>> n) | (x << (32 - n)) + for (let off = 0; off < paddedLen; off += 64) { + for (let i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4, false) + for (let i = 16; i < 64; i++) { + const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3) + const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10) + w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0 + } + let [a,b,c,d,e,f,g,h] = H + for (let i = 0; i < 64; i++) { + const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25) + const ch = (e & f) ^ (~e & g) + const t1 = (h + S1 + ch + K[i] + w[i]) >>> 0 + const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22) + const maj = (a & b) ^ (a & c) ^ (b & c) + const t2 = (S0 + maj) >>> 0 + h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0 + } + H = [ + (H[0] + a) >>> 0,(H[1] + b) >>> 0,(H[2] + c) >>> 0,(H[3] + d) >>> 0, + (H[4] + e) >>> 0,(H[5] + f) >>> 0,(H[6] + g) >>> 0,(H[7] + h) >>> 0 + ] + } + return H.map((x) => x.toString(16).padStart(8, '0')).join('') + } + + const markupPayload = (roots) => { + const out = [] + const pushNode = (node) => { + if (!node) return + if (node.nodeType === 1) { + const attrs = Array.from(node.attributes || []) + .map((a) => [String(a.name || '').toLowerCase(), String(a.value || '')]) + .filter((a) => !!a[0] && a[0] !== 'style') + .sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) : (a[0] < b[0] ? -1 : 1))) + out.push(['E', String(node.tagName || '').toLowerCase(), attrs]) + if (['script', 'style'].includes(String(node.tagName || '').toLowerCase())) return + Array.from(node.childNodes || []).forEach(pushNode) + } else if (node.nodeType === 3) { + const text = String(node.nodeValue || '') + if (text.trim()) out.push(['T', text]) + } + } + Array.from(roots || []).forEach(pushNode) + return JSON.stringify(out) + } + + const markupSealForNode = (node) => sha256Hex(markupPayload([node])) + const markupSealForHtml = (html) => { + const tpl = document.createElement('template') + tpl.innerHTML = String(html || '') + return sha256Hex(markupPayload(Array.from(tpl.content.childNodes || []))) + } + + const normalizePath = (value) => { + let s = String(value || '').replace(/\\/g, '/').replace(/^file:\/+/i, '') + try { s = decodeURIComponent(s) } catch {} + s = s.replace(/\/+/g, '/').replace(/^\/+/, '') + return s + } + + const decodeBundle = () => { + try { + const name = ['__', 'WCE', '_I'].join('') + const box = window[name] + if (!box || !Array.isArray(box.p) || !Array.isArray(box.a) || !Array.isArray(box.b)) return null + const left = box.a.map((v) => Number(v) & 255) + const right = box.b.map((v) => Number(v) & 255) + if (!left.length || !right.length) return null + const key = new Uint8Array(Math.max(left.length, right.length)) + for (let i = 0; i < key.length; i++) key[i] = (left[i % left.length] ^ right[(i * 7 + 3) % right.length] ^ ((i * 13) & 255)) & 255 + const bin = atob(box.p.join('')) + const data = new Uint8Array(bin.length) + for (let i = 0; i < bin.length; i++) data[i] = bin.charCodeAt(i) ^ key[i % key.length] + const parsed = JSON.parse(toUtf8(data)) + if (!parsed || parsed.v !== 2 || typeof parsed.g !== 'string' || typeof parsed.s !== 'string') return null + return parsed + } catch { + return null + } + } + + const b64uBytes = (value) => { + const raw = String(value || '').replace(/-/g, '+').replace(/_/g, '/') + const pad = raw.length % 4 ? '='.repeat(4 - (raw.length % 4)) : '' + const bin = atob(raw + pad) + const out = new Uint8Array(bin.length) + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i) + return out + } + + const verifySignedManifest = async (box) => { + try { + if (!window.crypto || !window.crypto.subtle) return null + const jwk = { + kty: 'EC', + crv: 'P-256', + x: '__WCE_PUBLIC_KEY_X__', + y: '__WCE_PUBLIC_KEY_Y__', + ext: true + } + const key = await window.crypto.subtle.importKey( + 'jwk', + jwk, + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['verify'] + ) + const ok = await window.crypto.subtle.verify( + { name: 'ECDSA', hash: 'SHA-256' }, + key, + b64uBytes(box.s), + utf8Bytes(box.g) + ) + if (!ok) return null + const manifest = JSON.parse(box.g) + if (!manifest || manifest.v !== 2 || !manifest.a || !Array.isArray(manifest.p)) return null + return manifest + } catch { + return null + } + } + + const signedBox = decodeBundle() + const manifest = signedBox ? await verifySignedManifest(signedBox) : null + if (!manifest) { + showTamperBlock() + return false + } + const pageSeals = new Map() + const fragmentSeals = new Map() + try { + for (const row of manifest.h || []) { + if (row && row.length >= 2) pageSeals.set(normalizePath(row[0]), String(row[1] || '').toLowerCase()) + } + for (const row of manifest.q || []) { + if (row && row.length >= 2) { + fragmentSeals.set(normalizePath(row[0]), { + dom: String(row[1] || '').toLowerCase(), + guard: String(row[2] || '').toLowerCase() + }) + } + } + } catch {} + + const currentPagePath = () => { + try { + const loc = normalizePath(window.location && window.location.pathname) + if (!loc) return '' + const pages = (manifest.p || []).map((p) => normalizePath(p)).filter(Boolean) + return pages.find((path) => loc.endsWith('/' + path) || loc.endsWith(path)) || '' + } catch {} + return '' + } + + const assetTagOk = () => { + try { + const assets = manifest.a || {} + const cssPath = normalizePath(assets.c || '') + const jsPath = normalizePath(assets.r || '') + const iPath = normalizePath(assets.i || '') + const cssSri = String(assets.cs || '') + const jsSri = String(assets.rs || '') + const hasHref = (selector, path, sri, requireSheet) => { + const nodes = Array.from(document.querySelectorAll(selector) || []) + return nodes.some((el) => { + const raw = String(el.getAttribute('href') || el.getAttribute('src') || '') + const okPath = raw.replace(/\\/g, '/').endsWith(path) + const attrSri = String(el.getAttribute('data-wce-sri') || el.getAttribute('integrity') || '') + const okSri = !sri || attrSri === sri + const okSheet = !requireSheet || !!el.sheet + return okPath && okSri && okSheet + }) + } + const hasInline = (selector, sri) => { + const nodes = Array.from(document.querySelectorAll(selector) || []) + return nodes.some((el) => String(el.getAttribute('data-wce-sri') || '') === String(sri || '')) + } + if (!cssPath || !jsPath || !iPath) return false + if (cssPath === '@inline-style') { + if (!hasInline('style[data-wce-style="1"]', cssSri)) return false + } else if (!hasHref('link[rel="stylesheet"]', cssPath, cssSri, true)) return false + if (jsPath === '@inline-runtime') { + if (!hasInline('script[data-wce-runtime="1"]', jsSri)) return false + } else if (!hasHref('script[src]', jsPath, jsSri)) return false + if (!hasHref('script[data-wce-integrity-bundle]', iPath, '')) return false + } catch { + return false + } + return true + } + + const pagePath = currentPagePath() + const pageSealOk = () => { + try { + const expected = String(pageSeals.get(pagePath) || '').toLowerCase() + if (!pagePath || !expected || !document.body) return false + return markupSealForNode(document.body) === expected + } catch { + return false + } + } + + if (!pagePath || !pageSealOk() || !assetTagOk()) { + showTamperBlock() + return false + } + try { document.documentElement.setAttribute('data-wce-integrity-ok', '1') } catch {} + + try { + const base = pagePath.includes('/') ? pagePath.slice(0, pagePath.lastIndexOf('/') + 1) : '' + window.__WCE_VERIFY_FRAGMENT__ = (relPath, html, seal) => { + try { + const key = normalizePath(base + normalizePath(relPath)) + const expected = fragmentSeals.get(key) || {} + const expectedDom = String(expected.dom || '').toLowerCase() + const expectedGuard = String(expected.guard || '').toLowerCase() + if (!expectedDom) return false + if (expectedGuard && String(seal || '').toLowerCase() !== expectedGuard) return false + return markupSealForHtml(String(html || '')) === expectedDom + } catch { + return false + } + } + } catch {} + return true + } + + const initSessionSearch = () => { + const input = document.getElementById('sessionSearchInput') + if (!input) return + + const clearBtn = document.getElementById('sessionSearchClear') + const items = Array.from(document.querySelectorAll('[data-wce-session-item=\"1\"]')) + + const apply = () => { + const q = String(input.value || '').trim().toLowerCase() + try { if (clearBtn) clearBtn.style.display = q ? '' : 'none' } catch {} + + items.forEach((el) => { + if (!el) return + const isActive = String(el.getAttribute('aria-current') || '') === 'page' + const name = String(el.getAttribute('data-wce-session-name') || '').toLowerCase() + const username = String(el.getAttribute('data-wce-session-username') || '').toLowerCase() + const show = !q || isActive || name.includes(q) || username.includes(q) + try { el.style.display = show ? '' : 'none' } catch {} + }) + } + + input.addEventListener('input', apply) + if (clearBtn) { + clearBtn.addEventListener('click', () => { + try { input.value = '' } catch {} + try { input.focus() } catch {} + apply() + }) + } + apply() + } + + const initVoicePlayback = () => { + let activeAudio = null + let activeIcon = null + + const stopAudio = (audio, icon) => { + if (!audio) return + try { audio.pause() } catch {} + try { audio.currentTime = 0 } catch {} + try { if (icon) icon.classList.remove('voice-playing') } catch {} + } + + const bindAudioEnd = (audio) => { + if (!audio) return + try { + if (audio.dataset && audio.dataset.wceVoiceBound === '1') return + if (audio.dataset) audio.dataset.wceVoiceBound = '1' + } catch {} + + try { + audio.addEventListener('ended', () => { + try { + const wrapper = audio.closest('.wechat-voice-wrapper') || audio.parentElement + const icon = wrapper ? wrapper.querySelector('.wechat-voice-icon') : null + if (icon) icon.classList.remove('voice-playing') + } catch {} + + if (activeAudio === audio) { + activeAudio = null + activeIcon = null + } + }) + } catch {} + } + + document.addEventListener('click', (ev) => { + const target = ev && ev.target + + const quoteBtn = target && target.closest ? target.closest('[data-wce-quote-voice-btn=\"1\"]') : null + if (quoteBtn) { + if (quoteBtn.hasAttribute && quoteBtn.hasAttribute('disabled')) return + + const wrapper = quoteBtn.closest ? (quoteBtn.closest('[data-wce-quote-voice-wrapper=\"1\"]') || quoteBtn.parentElement) : quoteBtn.parentElement + if (!wrapper) return + + const audio = wrapper.querySelector ? (wrapper.querySelector('audio[data-wce-quote-voice-audio=\"1\"]') || wrapper.querySelector('audio')) : null + if (!audio) return + + bindAudioEnd(audio) + + const icon = (quoteBtn.querySelector && quoteBtn.querySelector('.wechat-voice-icon')) || (wrapper.querySelector && wrapper.querySelector('.wechat-voice-icon')) + + if (activeAudio && activeAudio !== audio) stopAudio(activeAudio, activeIcon) + + const isPlaying = !audio.paused && !audio.ended + if (activeAudio === audio && isPlaying) { + stopAudio(audio, icon) + activeAudio = null + activeIcon = null + return + } + + activeAudio = audio + activeIcon = icon + try { if (icon) icon.classList.add('voice-playing') } catch {} + try { + const p = audio.play() + if (p && typeof p.catch === 'function') { + p.catch(() => { + stopAudio(audio, icon) + if (activeAudio === audio) { + activeAudio = null + activeIcon = null + } + }) + } + } catch { + stopAudio(audio, icon) + if (activeAudio === audio) { + activeAudio = null + activeIcon = null + } + } + return + } + + const bubble = target && target.closest ? target.closest('.wechat-voice-bubble') : null + if (!bubble) return + + const wrapper = bubble.closest('.wechat-voice-wrapper') || bubble.parentElement + if (!wrapper) return + + const audio = wrapper.querySelector('audio') + if (!audio) return + + bindAudioEnd(audio) + + const icon = bubble.querySelector('.wechat-voice-icon') || wrapper.querySelector('.wechat-voice-icon') + + if (activeAudio && activeAudio !== audio) stopAudio(activeAudio, activeIcon) + + const isPlaying = !audio.paused && !audio.ended + if (activeAudio === audio && isPlaying) { + stopAudio(audio, icon) + activeAudio = null + activeIcon = null + return + } + + activeAudio = audio + activeIcon = icon + try { if (icon) icon.classList.add('voice-playing') } catch {} + try { + const p = audio.play() + if (p && typeof p.catch === 'function') { + p.catch(() => { + stopAudio(audio, icon) + if (activeAudio === audio) { + activeAudio = null + activeIcon = null + } + }) + } + } catch { + stopAudio(audio, icon) + if (activeAudio === audio) { + activeAudio = null + activeIcon = null + } + } + }) + } + + const updateVisibleTimeDividers = () => { + const list = document.getElementById('wceMessageList') || document.getElementById('messageContainer') + if (!list) return + + let currentDivider = null + let groupHasVisibleMessage = false + + const flush = () => { + if (!currentDivider) return + try { currentDivider.style.display = groupHasVisibleMessage ? '' : 'none' } catch {} + } + + const children = Array.from(list.children || []) + children.forEach((el) => { + if (!el) return + if (String(el.getAttribute && el.getAttribute('data-wce-time-divider') || '') === '1') { + flush() + currentDivider = el + groupHasVisibleMessage = false + return + } + if (el.hasAttribute && el.hasAttribute('data-render-type')) { + const visible = String(el.style && el.style.display || '') !== 'none' + if (visible) groupHasVisibleMessage = true + } + }) + flush() + } + + const applyMessageTypeFilter = () => { + const select = document.getElementById('messageTypeFilter') + if (!select) return + const selected = String(select.value || 'all') + const nodes = document.querySelectorAll('[data-render-type]') + nodes.forEach((el) => { + const rt = String(el.getAttribute('data-render-type') || 'text') + const show = selected === 'all' ? true : rt === selected + el.style.display = show ? '' : 'none' + }) + try { updateVisibleTimeDividers() } catch {} + } + + const scrollToBottom = () => { + const container = document.getElementById('messageContainer') + if (!container) return + container.scrollTop = container.scrollHeight + } + + const updateSessionMessageCount = () => { + const el = document.getElementById('sessionMessageCount') + const container = document.getElementById('messageContainer') + if (!el || !container) return + const items = container.querySelectorAll('[data-render-type]') + el.textContent = String(items.length) + } + + const ensureAllMessagePagesLoaded = async (statusEl) => { + try { + const loadedAll = typeof window.__WCE_ARE_ALL_PAGES_LOADED__ === 'function' ? window.__WCE_ARE_ALL_PAGES_LOADED__() : true + const loader = window.__WCE_LOAD_ALL_PAGES__ + if (loadedAll || typeof loader !== 'function') return true + if (statusEl) statusEl.textContent = '加载分页…' + const ok = await loader() + try { applyMessageTypeFilter() } catch {} + try { updateSessionMessageCount() } catch {} + if (!ok && statusEl) statusEl.textContent = '分页加载失败' + return !!ok + } catch { + if (statusEl) statusEl.textContent = '分页加载失败' + return false + } + } + + const getVisibleMessageNodes = () => { + const list = document.getElementById('wceMessageList') || document.getElementById('messageContainer') + if (!list) return [] + return Array.from(list.querySelectorAll('[data-render-type]') || []).filter((el) => { + try { return String(el.style && el.style.display || '') !== 'none' } catch { return true } + }) + } + + const scrollToMessageNode = (el) => { + if (!el) return + try { el.scrollIntoView({ block: 'center', behavior: 'smooth' }) } catch { + try { el.scrollIntoView(true) } catch {} + } + } + + const clearSearchMarks = () => { + try { + document.querySelectorAll('.wce-search-hit,.wce-search-current').forEach((el) => { + try { + el.classList.remove('wce-search-hit') + el.classList.remove('wce-search-current') + } catch {} + }) + } catch {} + } + + const initMessageSearchAndDateJump = () => { + const searchInput = document.getElementById('wceMessageSearchInput') + const searchBtn = document.getElementById('wceMessageSearchBtn') + const prevBtn = document.getElementById('wceMessageSearchPrev') + const nextBtn = document.getElementById('wceMessageSearchNext') + const searchStatus = document.getElementById('wceMessageSearchStatus') + const dateInput = document.getElementById('wceDateJumpInput') + const dateBtn = document.getElementById('wceDateJumpBtn') + const dateStatus = document.getElementById('wceDateJumpStatus') + const state = { query: '', hits: [], index: -1 } + + const setSearchStatus = (text) => { + try { if (searchStatus) searchStatus.textContent = String(text || '') } catch {} + } + const setDateStatus = (text) => { + try { if (dateStatus) dateStatus.textContent = String(text || '') } catch {} + } + const updateSearchButtons = () => { + const disabled = !state.hits.length + try { if (prevBtn) prevBtn.disabled = disabled } catch {} + try { if (nextBtn) nextBtn.disabled = disabled } catch {} + } + + const focusSearchHit = (idx) => { + if (!state.hits.length) { + updateSearchButtons() + return + } + state.index = ((idx % state.hits.length) + state.hits.length) % state.hits.length + state.hits.forEach((el, i) => { + try { + el.classList.toggle('wce-search-current', i === state.index) + el.classList.add('wce-search-hit') + } catch {} + }) + const current = state.hits[state.index] + scrollToMessageNode(current) + setSearchStatus(`${state.index + 1}/${state.hits.length}`) + updateSearchButtons() + } + + const runSearch = async () => { + const q = String(searchInput ? searchInput.value : '').trim().toLowerCase() + clearSearchMarks() + state.query = q + state.hits = [] + state.index = -1 + updateSearchButtons() + if (!q) { + setSearchStatus('') + return + } + await ensureAllMessagePagesLoaded(searchStatus) + const nodes = getVisibleMessageNodes() + state.hits = nodes.filter((el) => String(el.textContent || '').toLowerCase().includes(q)) + if (!state.hits.length) { + setSearchStatus('无结果') + return + } + focusSearchHit(0) + } + + const jumpDate = async () => { + const target = String(dateInput ? dateInput.value : '').trim() + if (!target) { + setDateStatus('') + return + } + await ensureAllMessagePagesLoaded(dateStatus) + const nodes = getVisibleMessageNodes() + let exact = null + let nearest = null + for (const el of nodes) { + const d = String(el.getAttribute('data-wce-date') || '').trim() + if (!d) continue + if (d === target) { + exact = el + break + } + if (!nearest && d > target) nearest = el + } + const found = exact || nearest + if (!found) { + setDateStatus('无消息') + return + } + try { + found.classList.add('wce-date-located') + setTimeout(() => { + try { found.classList.remove('wce-date-located') } catch {} + }, 1400) + } catch {} + scrollToMessageNode(found) + setDateStatus(exact ? '已定位' : '最近之后') + } + + if (searchBtn) searchBtn.addEventListener('click', () => { runSearch() }) + if (searchInput) { + searchInput.addEventListener('keydown', (ev) => { + if (String(ev?.key || '') !== 'Enter') return + try { ev.preventDefault() } catch {} + runSearch() + }) + searchInput.addEventListener('input', () => { + if (!String(searchInput.value || '').trim()) { + clearSearchMarks() + state.query = '' + state.hits = [] + state.index = -1 + setSearchStatus('') + updateSearchButtons() + } + }) + } + if (prevBtn) prevBtn.addEventListener('click', () => focusSearchHit(state.index - 1)) + if (nextBtn) nextBtn.addEventListener('click', () => focusSearchHit(state.index + 1)) + if (dateBtn) dateBtn.addEventListener('click', () => { jumpDate() }) + if (dateInput) { + dateInput.addEventListener('keydown', (ev) => { + if (String(ev?.key || '') !== 'Enter') return + try { ev.preventDefault() } catch {} + jumpDate() + }) + } + + updateSearchButtons() + } + + const safeJsonParse = (text) => { + try { return JSON.parse(String(text || '')) } catch { return null } + } + + const readMediaIndex = () => { + const el = document.getElementById('wceMediaIndex') + const obj = safeJsonParse(el ? el.textContent : '') + if (!obj || typeof obj !== 'object') return {} + return obj + } + + const readPageMeta = () => { + const el = document.getElementById('wcePageMeta') + const obj = safeJsonParse(el ? el.textContent : '') + if (!obj || typeof obj !== 'object') return null + return obj + } + + const initPagedMessageLoading = () => { + const meta = readPageMeta() + if (!meta) return + + const totalPages = Number(meta.totalPages || 0) + if (!Number.isFinite(totalPages) || totalPages <= 1) return + + const initialPage = Number(meta.initialPage || totalPages || 1) + const padWidth = Number(meta.padWidth || 0) || 0 + const prefix = String(meta.pageFilePrefix || 'pages/page-') + const suffix = String(meta.pageFileSuffix || '.js') + + const container = document.getElementById('messageContainer') + const list = document.getElementById('wceMessageList') || container + const pager = document.getElementById('wcePager') + const btn = document.getElementById('wceLoadPrevBtn') + const status = document.getElementById('wceLoadPrevStatus') + if (!container || !list || !pager || !btn) return + + try { pager.style.display = '' } catch {} + + const loaded = new Set() + loaded.add(initialPage) + let nextPage = initialPage - 1 + let loading = false + const pendingPageLoads = new Map() + + const setStatus = (text) => { + try { if (status) status.textContent = String(text || '') } catch {} + } + + const updateUi = (overrideText) => { + if (overrideText != null) { + setStatus(overrideText) + try { btn.disabled = false } catch {} + return + } + if (nextPage < 1) { + setStatus('已到底') + try { btn.disabled = true } catch {} + return + } + if (loading) { + setStatus('加载中...') + try { btn.disabled = true } catch {} + return + } + setStatus('点击加载更早消息') + try { btn.disabled = false } catch {} + } + + const settlePageLoad = (pageNo, ok, errorText) => { + const n = Number(pageNo) + const pending = pendingPageLoads.get(n) + if (pending) { + try { pendingPageLoads.delete(n) } catch {} + try { clearTimeout(pending.timer) } catch {} + } + loading = false + if (ok) updateUi() + else updateUi(errorText || '加载失败,可重试') + if (pending) { + try { pending.resolve(!!ok) } catch {} + } + } + + const pageSrc = (n) => { + const num = padWidth > 0 ? String(n).padStart(padWidth, '0') : String(n) + return prefix + num + suffix + } + + window.__WCE_PAGE_SEEN__ = (pageNo, html, seal) => { + const n = Number(pageNo) + if (!Number.isFinite(n) || n < 1) { + showTamperBlock() + return false + } + try { + const verifier = window.__WCE_VERIFY_FRAGMENT__ + if (typeof verifier !== 'function' || !verifier(pageSrc(n), String(html || ''), String(seal || ''))) { + showTamperBlock() + return false + } + return true + } catch { + showTamperBlock() + return false + } + } + + window.__WCE_PAGE_QUEUE__ = window.__WCE_PAGE_QUEUE__ || [] + window.__WCE_PAGE_LOADED__ = (pageNo, html, seal) => { + const n = Number(pageNo) + if (!Number.isFinite(n) || n < 1) { + settlePageLoad(n, false, '分页数据无效,可重试') + return + } + if (loaded.has(n)) { + settlePageLoad(n, true) + return + } + try { + if (typeof window.__WCE_PAGE_SEEN__ === 'function' && !window.__WCE_PAGE_SEEN__(n, html, seal)) throw new Error('page verification failed') + } catch { + showTamperBlock() + settlePageLoad(n, false, '分页校验失败,请重新导出') + return + } + + let inserted = false + try { + const prevH = container.scrollHeight + const prevTop = container.scrollTop + list.insertAdjacentHTML('afterbegin', String(html || '')) + inserted = true + const newH = container.scrollHeight + container.scrollTop = prevTop + (newH - prevH) + } catch { + try { + list.insertAdjacentHTML('afterbegin', String(html || '')) + inserted = true + } catch {} + } + if (!inserted) { + settlePageLoad(n, false, '分页渲染失败,可重试') + return + } + + loaded.add(n) + nextPage = n - 1 + try { applyMessageTypeFilter() } catch {} + try { updateSessionMessageCount() } catch {} + settlePageLoad(n, true) + } + + // Flush any queued pages (should be rare, but keeps behavior robust). + try { + const q = window.__WCE_PAGE_QUEUE__ + if (Array.isArray(q) && q.length) { + const items = q.slice(0) + q.length = 0 + items.forEach((it) => { + try { + if (it && it.length >= 2) window.__WCE_PAGE_LOADED__(it[0], it[1], it[2]) + } catch {} + }) + } + } catch {} + + // Historical fragments are verified when requested; opening an export must not scan every page. + const requestLoadPage = (pageNumber) => new Promise((resolve) => { + const n = Number(pageNumber) + if (!Number.isFinite(n) || n < 1) { + resolve(false) + return + } + if (loaded.has(n)) { + resolve(true) + return + } + if (loading) { + const wait = () => { + if (!loading) requestLoadPage(n).then(resolve) + else setTimeout(wait, 60) + } + wait() + return + } + + loading = true + updateUi() + const pending = { resolve, timer: 0 } + pendingPageLoads.set(n, pending) + pending.timer = setTimeout(() => { + if (pendingPageLoads.has(n)) settlePageLoad(n, false, '加载超时,可重试') + }, 15000) + + const s = document.createElement('script') + s.async = true + s.src = pageSrc(n) + s.onerror = () => { + settlePageLoad(n, false, '加载失败,可重试') + } + s.onload = () => { + setTimeout(() => { + if (pendingPageLoads.has(n)) settlePageLoad(n, false, '分页脚本未响应,可重试') + }, 0) + } + try { document.body.appendChild(s) } catch { + settlePageLoad(n, false, '加载失败,可重试') + } + }) + + const requestLoad = () => { + if (loading) return + if (nextPage < 1) return + requestLoadPage(nextPage) + } + + window.__WCE_LOAD_ALL_PAGES__ = async () => { + while (nextPage >= 1) { + const ok = await requestLoadPage(nextPage) + if (!ok) return false + } + return true + } + window.__WCE_ARE_ALL_PAGES_LOADED__ = () => nextPage < 1 + + btn.addEventListener('click', () => requestLoad()) + + let lastScrollAt = 0 + container.addEventListener('scroll', () => { + const now = Date.now() + if (now - lastScrollAt < 200) return + lastScrollAt = now + if (container.scrollTop < 120) requestLoad() + }) + + updateUi() + } + + const isMaybeMd5 = (value) => /^[0-9a-f]{32}$/i.test(String(value || '').trim()) + const pickFirstMd5 = (...values) => { + for (const v of values) { + const s = String(v || '').trim() + if (isMaybeMd5(s)) return s.toLowerCase() + } + return '' + } + + const normalizeChatHistoryUrl = (value) => String(value || '').trim().replace(/\s+/g, '') + + const decodeBase64Utf8 = (b64) => { + try { + const bin = atob(String(b64 || '')) + const bytes = new Uint8Array(bin.length) + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i) + if (typeof TextDecoder !== 'undefined') { + return new TextDecoder('utf-8', { fatal: false }).decode(bytes) + } + let out = '' + for (let i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]) + return out + } catch { + return '' + } + } + + const resolveMediaMd5 = (index, kind, md5) => { + const key = String(md5 || '').trim().toLowerCase() + if (!key) return '' + const mapNames = { + preview: ['images', 'emojis', 'videoThumbs'], + image: ['images', 'videoThumbs'], + emoji: ['emojis', 'images'], + video: ['videos'], + videoThumb: ['videoThumbs', 'images'], + } + const maps = (mapNames[String(kind || '')] || []).map((name) => index && index[name]) + for (const m of maps) { + try { + if (m && m[key]) return String(m[key] || '') + } catch {} + } + return '' + } + + const resolveServerMd5 = (index, serverId) => { + const key = String(serverId || '').trim() + if (!key) return '' + try { + const v = index && index.serverMd5 && index.serverMd5[key] + return isMaybeMd5(v) ? String(v || '').trim().toLowerCase() : '' + } catch {} + return '' + } + + const resolveRemoteAny = (index, ...urls) => { + for (const u0 of urls) { + const u = normalizeChatHistoryUrl(u0) + if (!u) continue + try { + const local = index && index.remote && index.remote[u] + if (local) return String(local || '') + } catch {} + const ul = String(u || '').trim().toLowerCase() + if (ul.startsWith('http://') || ul.startsWith('https://')) return u + } + return '' + } + + const parseChatHistoryRecord = (recordItemXml) => { + const xml = String(recordItemXml || '').trim() + if (!xml) return { info: null, items: [] } + + const normalized = xml + .replace(/ /g, ' ') + .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '') + .replace(/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[\da-fA-F]+;)/g, '&') + + let doc + try { + doc = new DOMParser().parseFromString(normalized, 'text/xml') + } catch { + return { info: null, items: [] } + } + + const parserErrors = doc.getElementsByTagName('parsererror') + if (parserErrors && parserErrors.length) return { info: null, items: [] } + + const getText = (node, tag) => { + try { + if (!node) return '' + const els = Array.from(node.getElementsByTagName(tag) || []) + const direct = els.find((el) => el && el.parentNode === node) + const el = direct || els[0] + return String(el?.textContent || '').trim() + } catch { + return '' + } + } + + const getDirectChildXml = (node, tag) => { + try { + if (!node) return '' + const children = Array.from(node.children || []) + const el = children.find((c) => String(c?.tagName || '').toLowerCase() === String(tag || '').toLowerCase()) + if (!el) return '' + + const raw = String(el.textContent || '').trim() + if (raw && raw.startsWith('<') && raw.endsWith('>')) return raw + + if (typeof XMLSerializer !== 'undefined') { + return new XMLSerializer().serializeToString(el) + } + } catch {} + return '' + } + + const getAnyXml = (node, tag) => { + try { + if (!node) return '' + const els = Array.from(node.getElementsByTagName(tag) || []) + const direct = els.find((el) => el && el.parentNode === node) + const el = direct || els[0] + if (!el) return '' + + const raw = String(el.textContent || '').trim() + if (raw && raw.startsWith('<') && raw.endsWith('>')) return raw + if (typeof XMLSerializer !== 'undefined') return new XMLSerializer().serializeToString(el) + } catch {} + return '' + } + + const sameTag = (el, tag) => String(el?.tagName || '').toLowerCase() === String(tag || '').toLowerCase() + + const closestAncestorByTag = (node, tag) => { + const lower = String(tag || '').toLowerCase() + let cur = node + while (cur) { + if (cur.nodeType === 1 && String(cur.tagName || '').toLowerCase() === lower) return cur + cur = cur.parentNode + } + return null + } + + const root = doc?.documentElement + const isChatRoom = String(getText(root, 'isChatRoom') || '').trim() === '1' + const title = getText(root, 'title') + const desc = getText(root, 'desc') || getText(root, 'info') + + const datalist = (() => { + try { + const all = Array.from(doc.getElementsByTagName('datalist') || []) + const top = root ? all.find((el) => closestAncestorByTag(el, 'recorditem') === root) : null + return top || all[0] || null + } catch { + return null + } + })() + + const itemNodes = (() => { + if (datalist) return Array.from(datalist.children || []).filter((el) => sameTag(el, 'dataitem')) + return Array.from(root?.children || []).filter((el) => sameTag(el, 'dataitem')) + })() + + const parsed = itemNodes.map((node, idx) => { + const datatype = String(node.getAttribute('datatype') || getText(node, 'datatype') || '').trim() + const dataid = String(node.getAttribute('dataid') || getText(node, 'dataid') || '').trim() || String(idx) + + const sourcename = getText(node, 'sourcename') + const sourcetime = getText(node, 'sourcetime') + const sourceheadurl = normalizeChatHistoryUrl(getText(node, 'sourceheadurl')) + const datatitle = getText(node, 'datatitle') + const datadesc = getText(node, 'datadesc') + const link = normalizeChatHistoryUrl(getText(node, 'link') || getText(node, 'dataurl') || getText(node, 'url')) + const datafmt = getText(node, 'datafmt') + const duration = getText(node, 'duration') + + const fullmd5 = getText(node, 'fullmd5') + const thumbfullmd5 = getText(node, 'thumbfullmd5') + const md5 = getText(node, 'md5') || getText(node, 'emoticonmd5') || getText(node, 'emojimd5') || getText(node, 'emojiMd5') + const cdnthumbmd5 = getText(node, 'cdnthumbmd5') + const cdnurlstring = normalizeChatHistoryUrl(getText(node, 'cdnurlstring')) + const encrypturlstring = normalizeChatHistoryUrl(getText(node, 'encrypturlstring')) + const externurl = normalizeChatHistoryUrl(getText(node, 'externurl')) + const aeskey = getText(node, 'aeskey') + const fromnewmsgid = getText(node, 'fromnewmsgid') + const srcMsgLocalid = getText(node, 'srcMsgLocalid') + const srcMsgCreateTime = getText(node, 'srcMsgCreateTime') + const nestedRecordItem = ( + getAnyXml(node, 'recorditem') + || getDirectChildXml(node, 'recorditem') + || getText(node, 'recorditem') + || getAnyXml(node, 'recordxml') + || getDirectChildXml(node, 'recordxml') + || getText(node, 'recordxml') + ) + + let content = datatitle || datadesc + if (!content) { + if (datatype === '4') content = '[视频]' + else if (datatype === '2' || datatype === '3') content = '[图片]' + else if (datatype === '47' || datatype === '37') content = '[表情]' + else if (datatype) content = `[消息 ${datatype}]` + else content = '[消息]' + } + + const fmt = String(datafmt || '').trim().toLowerCase().replace(/^\./, '') + const imageFormats = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic', 'heif']) + + let renderType = 'text' + if (datatype === '17') { + renderType = 'chatHistory' + } else if (datatype === '5' || link) { + renderType = 'link' + } else if (datatype === '4' || String(duration || '').trim() || fmt === 'mp4') { + renderType = 'video' + } else if (datatype === '47' || datatype === '37') { + renderType = 'emoji' + } else if ( + datatype === '2' + || datatype === '3' + || imageFormats.has(fmt) + || (datatype !== '1' && isMaybeMd5(fullmd5)) + ) { + renderType = 'image' + } else if (isMaybeMd5(md5) && /表情/.test(String(content || ''))) { + renderType = 'emoji' + } + + let outTitle = '' + let outUrl = '' + let recordItem = '' + if (renderType === 'chatHistory') { + outTitle = datatitle || content || '聊天记录' + content = datadesc || '' + recordItem = nestedRecordItem + } else if (renderType === 'link') { + outTitle = datatitle || content || '' + outUrl = link || externurl || '' + // datadesc can be an invisible filler; only keep as description when meaningful. + const cleanDesc = String(datadesc || '').replace(/[\\u3164\\u2800]/g, '').trim() + const cleanTitle = String(outTitle || '').replace(/[\\u3164\\u2800]/g, '').trim() + if (!cleanDesc || (cleanTitle && cleanDesc === cleanTitle)) content = '' + else content = String(datadesc || '').trim() + } + + return { + id: dataid, + datatype, + sourcename, + sourcetime, + sourceheadurl, + datafmt, + duration, + fullmd5, + thumbfullmd5, + md5, + cdnthumbmd5, + cdnurlstring, + encrypturlstring, + externurl, + aeskey, + fromnewmsgid, + srcMsgLocalid, + srcMsgCreateTime, + renderType, + title: outTitle, + recordItem, + url: outUrl, + content + } + }) + + return { + info: { isChatRoom, title, desc }, + items: parsed + } + } + + const initChatHistoryModal = () => { + const modal = document.getElementById('chatHistoryModal') + const titleEl = document.getElementById('chatHistoryModalTitle') + const closeBtn = document.getElementById('chatHistoryModalClose') + const emptyEl = document.getElementById('chatHistoryModalEmpty') + const listEl = document.getElementById('chatHistoryModalList') + if (!modal || !titleEl || !closeBtn || !emptyEl || !listEl) return + + const mediaIndex = readMediaIndex() + let historyStack = [] + let currentState = null + let backBtn = null + + const updateBackVisibility = () => { + if (!backBtn) return + const show = Array.isArray(historyStack) && historyStack.length > 0 + try { backBtn.classList.toggle('hidden', !show) } catch {} + } + + // Add a back button next to the title (created at runtime to avoid changing the HTML template). + try { + const header = titleEl.parentElement + if (header) { + const wrap = document.createElement('div') + wrap.className = 'flex items-center gap-2 min-w-0' + + backBtn = document.createElement('button') + backBtn.type = 'button' + backBtn.className = 'p-2 rounded hover:bg-black/5 flex-shrink-0 hidden' + try { backBtn.setAttribute('aria-label', '返回') } catch {} + try { backBtn.setAttribute('title', '返回') } catch {} + backBtn.innerHTML = '' + + header.insertBefore(wrap, titleEl) + wrap.appendChild(backBtn) + wrap.appendChild(titleEl) + } + } catch {} + + const close = () => { + try { modal.classList.add('hidden') } catch {} + try { modal.style.display = 'none' } catch {} + try { modal.setAttribute('aria-hidden', 'true') } catch {} + try { document.body.style.overflow = '' } catch {} + try { titleEl.textContent = '聊天记录' } catch {} + try { listEl.textContent = '' } catch {} + try { emptyEl.style.display = '' } catch {} + historyStack = [] + currentState = null + updateBackVisibility() + } + + const buildChatHistoryState = (payload) => { + const title = String(payload?.title || '聊天记录').trim() || '聊天记录' + const xml = String(payload?.recordItem || '').trim() + const parsed = parseChatHistoryRecord(xml) + const info = (parsed && parsed.info) ? parsed.info : { isChatRoom: false } + let records = (parsed && Array.isArray(parsed.items)) ? parsed.items : [] + + if (!records.length) { + const lines = Array.isArray(payload?.fallbackLines) + ? payload.fallbackLines + : String(payload?.content || '').trim().split(/\r?\n/).map((x) => String(x || '').trim()).filter(Boolean) + records = lines.map((line, idx) => ({ id: String(idx), renderType: 'text', content: line, sourcename: '', sourcetime: '' })) + } + + return { title, info, records } + } + + const renderRecordRow = (rec, info) => { + const row = document.createElement('div') + row.className = 'px-4 py-3 flex gap-3 border-b border-gray-100' + + const avatarWrap = document.createElement('div') + avatarWrap.className = 'w-9 h-9 rounded-md overflow-hidden bg-gray-200 flex-shrink-0' + const name0 = String(rec?.sourcename || '').trim() || '?' + const avatarUrlRaw = normalizeChatHistoryUrl(rec?.sourceheadurl) + const avatarLocal = (mediaIndex && mediaIndex.remote && mediaIndex.remote[avatarUrlRaw]) ? String(mediaIndex.remote[avatarUrlRaw] || '') : '' + const avatarUrlLower = String(avatarUrlRaw || '').trim().toLowerCase() + const avatarUrl = avatarLocal || ((avatarUrlLower.startsWith('http://') || avatarUrlLower.startsWith('https://')) ? avatarUrlRaw : '') + if (avatarUrl) { + const img = document.createElement('img') + img.src = avatarUrl + img.alt = '头像' + img.className = 'w-full h-full object-cover' + try { img.referrerPolicy = 'no-referrer' } catch {} + img.onerror = () => { + try { avatarWrap.textContent = '' } catch {} + const fb = document.createElement('div') + fb.className = 'w-full h-full flex items-center justify-center text-xs font-bold text-gray-600' + fb.textContent = String(name0.charAt(0) || '?') + avatarWrap.appendChild(fb) + } + avatarWrap.appendChild(img) + } else { + const fb = document.createElement('div') + fb.className = 'w-full h-full flex items-center justify-center text-xs font-bold text-gray-600' + fb.textContent = String(name0.charAt(0) || '?') + avatarWrap.appendChild(fb) + } + + const main = document.createElement('div') + main.className = 'min-w-0 flex-1' + + const header = document.createElement('div') + header.className = 'flex items-start gap-2' + + const headerLeft = document.createElement('div') + headerLeft.className = 'min-w-0 flex-1' + const senderName = String(rec?.sourcename || '').trim() + if (info && info.isChatRoom && senderName) { + const sn = document.createElement('div') + sn.className = 'text-xs text-gray-500 leading-none truncate mb-1' + sn.textContent = senderName + headerLeft.appendChild(sn) + } + + const headerRight = document.createElement('div') + headerRight.className = 'text-xs text-gray-400 flex-shrink-0 leading-none' + const timeText = String(rec?.sourcetime || '').trim() + headerRight.textContent = timeText + + header.appendChild(headerLeft) + if (timeText) header.appendChild(headerRight) + + const body = document.createElement('div') + body.className = 'mt-1' + + const rt = String(rec?.renderType || 'text') + const content = String(rec?.content || '').trim() + const serverId = String(rec?.fromnewmsgid || '').trim() + const serverMd5 = resolveServerMd5(mediaIndex, serverId) + + if (rt === 'chatHistory') { + const card = document.createElement('div') + card.className = 'wechat-chat-history-card wechat-special-card msg-radius' + + const chBody = document.createElement('div') + chBody.className = 'wechat-chat-history-body' + + const chTitle = document.createElement('div') + chTitle.className = 'wechat-chat-history-title' + chTitle.textContent = String(rec?.title || '聊天记录') + chBody.appendChild(chTitle) + + const raw = String(rec?.content || '').trim() + const lines = raw ? raw.split(/\r?\n/).map((x) => String(x || '').trim()).filter(Boolean).slice(0, 4) : [] + if (lines.length) { + const preview = document.createElement('div') + preview.className = 'wechat-chat-history-preview' + for (const line of lines) { + const el = document.createElement('div') + el.className = 'wechat-chat-history-line' + el.textContent = line + preview.appendChild(el) + } + chBody.appendChild(preview) + } + + card.appendChild(chBody) + + const bottom = document.createElement('div') + bottom.className = 'wechat-chat-history-bottom' + const label = document.createElement('span') + label.textContent = '聊天记录' + bottom.appendChild(label) + card.appendChild(bottom) + + const nestedXml = String(rec?.recordItem || '').trim() + if (nestedXml) { + card.classList.add('cursor-pointer') + card.addEventListener('click', (ev) => { + try { ev.preventDefault() } catch {} + try { ev.stopPropagation() } catch {} + openNestedChatHistory(rec) + }) + } + + body.appendChild(card) + } else if (rt === 'link') { + const href = normalizeChatHistoryUrl(rec?.url) || normalizeChatHistoryUrl(rec?.externurl) + const heading = String(rec?.title || '').trim() || content || href || '链接' + const desc = String(rec?.content || '').trim() + + const thumbMd5 = pickFirstMd5(rec?.fullmd5, rec?.thumbfullmd5, rec?.cdnthumbmd5, rec?.md5, rec?.id) + let previewUrl = resolveMediaMd5(mediaIndex, 'preview', thumbMd5) + if (!previewUrl && serverMd5) previewUrl = resolveMediaMd5(mediaIndex, 'preview', serverMd5) + if (!previewUrl) previewUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + + const card = document.createElement(href ? 'a' : 'div') + card.className = 'wechat-link-card wechat-special-card msg-radius cursor-pointer' + if (href) { + card.href = href + card.target = '_blank' + card.rel = 'noreferrer noopener' + } + try { card.style.textDecoration = 'none' } catch {} + try { card.style.outline = 'none' } catch {} + + const linkContent = document.createElement('div') + linkContent.className = 'wechat-link-content' + + const linkInfo = document.createElement('div') + linkInfo.className = 'wechat-link-info' + const titleEl = document.createElement('div') + titleEl.className = 'wechat-link-title' + titleEl.textContent = heading + linkInfo.appendChild(titleEl) + if (desc) { + const descEl = document.createElement('div') + descEl.className = 'wechat-link-desc' + descEl.textContent = desc + linkInfo.appendChild(descEl) + } + linkContent.appendChild(linkInfo) + + if (previewUrl) { + const thumb = document.createElement('div') + thumb.className = 'wechat-link-thumb' + const img = document.createElement('img') + img.src = previewUrl + img.alt = heading || '链接预览' + img.className = 'wechat-link-thumb-img' + try { img.referrerPolicy = 'no-referrer' } catch {} + thumb.appendChild(img) + linkContent.appendChild(thumb) + } + + card.appendChild(linkContent) + + const fromRow = document.createElement('div') + fromRow.className = 'wechat-link-from' + const fromText = (() => { + const f0 = String(rec?.from || '').trim() + if (f0) return f0 + try { return href ? (new URL(href).hostname || '') : '' } catch { return '' } + })() + const fromAvatarText = fromText ? (Array.from(fromText)[0] || '') : '' + const fromAvatar = document.createElement('div') + fromAvatar.className = 'wechat-link-from-avatar' + fromAvatar.textContent = fromAvatarText || '\u200B' + const fromName = document.createElement('div') + fromName.className = 'wechat-link-from-name' + fromName.textContent = fromText || '\u200B' + fromRow.appendChild(fromAvatar) + fromRow.appendChild(fromName) + card.appendChild(fromRow) + + body.appendChild(card) + } else if (rt === 'video') { + const videoMd5 = pickFirstMd5(rec?.fullmd5, rec?.md5, rec?.id) + const thumbMd5 = pickFirstMd5(rec?.thumbfullmd5, rec?.cdnthumbmd5) || videoMd5 + let videoUrl = resolveMediaMd5(mediaIndex, 'video', videoMd5) + if (!videoUrl && serverMd5) videoUrl = resolveMediaMd5(mediaIndex, 'video', serverMd5) + if (!videoUrl) videoUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + + let thumbUrl = resolveMediaMd5(mediaIndex, 'videoThumb', thumbMd5) + if (!thumbUrl && serverMd5) thumbUrl = resolveMediaMd5(mediaIndex, 'videoThumb', serverMd5) + if (!thumbUrl) thumbUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + + const wrap = document.createElement('div') + wrap.className = 'msg-radius overflow-hidden relative bg-black/5 inline-block' + + if (thumbUrl) { + const img = document.createElement('img') + img.src = thumbUrl + img.alt = '视频' + img.className = 'block w-[220px] max-w-[260px] h-auto max-h-[260px] object-cover' + wrap.appendChild(img) + } else { + const t = document.createElement('div') + t.className = 'px-3 py-2 text-sm text-gray-700' + t.textContent = content || '[视频]' + wrap.appendChild(t) + } + + if (thumbUrl) { + const overlay = document.createElement(videoUrl ? 'a' : 'div') + if (videoUrl) { + overlay.href = videoUrl + overlay.target = '_blank' + overlay.rel = 'noreferrer noopener' + } + overlay.className = 'absolute inset-0 flex items-center justify-center' + const btn = document.createElement('div') + btn.className = 'w-12 h-12 rounded-full bg-black/45 flex items-center justify-center' + btn.innerHTML = '' + overlay.appendChild(btn) + wrap.appendChild(overlay) + } + + body.appendChild(wrap) + } else if (rt === 'image') { + const imageMd5 = pickFirstMd5(rec?.fullmd5, rec?.thumbfullmd5, rec?.cdnthumbmd5, rec?.md5, rec?.id) + let imgUrl = resolveMediaMd5(mediaIndex, 'image', imageMd5) + if (!imgUrl && serverMd5) imgUrl = resolveMediaMd5(mediaIndex, 'image', serverMd5) + if (!imgUrl) imgUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + if (imgUrl) { + const outer = document.createElement('div') + outer.className = 'msg-radius overflow-hidden cursor-pointer inline-block' + const a = document.createElement('a') + a.href = imgUrl + a.target = '_blank' + a.rel = 'noreferrer noopener' + const img = document.createElement('img') + img.src = imgUrl + img.alt = '图片' + img.className = 'max-w-[240px] max-h-[240px] object-cover' + a.appendChild(img) + outer.appendChild(a) + body.appendChild(outer) + } else { + const t = document.createElement('div') + t.className = 'px-3 py-2 text-sm text-gray-700 whitespace-pre-wrap break-words' + t.textContent = content || '[图片]' + body.appendChild(t) + } + } else if (rt === 'emoji') { + const emojiMd5 = pickFirstMd5(rec?.md5, rec?.fullmd5, rec?.thumbfullmd5, rec?.cdnthumbmd5, rec?.id) + let emojiUrl = resolveMediaMd5(mediaIndex, 'emoji', emojiMd5) + if (!emojiUrl && serverMd5) emojiUrl = resolveMediaMd5(mediaIndex, 'emoji', serverMd5) + if (!emojiUrl) emojiUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + if (emojiUrl) { + const img = document.createElement('img') + img.src = emojiUrl + img.alt = '表情' + img.className = 'w-24 h-24 object-contain' + body.appendChild(img) + } else { + const t = document.createElement('div') + t.className = 'px-3 py-2 text-sm text-gray-700 whitespace-pre-wrap break-words' + t.textContent = content || '[表情]' + body.appendChild(t) + } + } else { + const t = document.createElement('div') + t.className = 'px-3 py-2 text-sm text-gray-700 whitespace-pre-wrap break-words' + t.textContent = content || '' + body.appendChild(t) + } + + main.appendChild(header) + main.appendChild(body) + + row.appendChild(avatarWrap) + row.appendChild(main) + return row + } + + const applyChatHistoryState = (state) => { + currentState = state + const title = String(state?.title || '聊天记录').trim() || '聊天记录' + const info = state?.info || { isChatRoom: false } + const records = Array.isArray(state?.records) ? state.records : [] + + try { titleEl.textContent = title } catch {} + try { listEl.textContent = '' } catch {} + + if (!records.length) { + try { emptyEl.style.display = '' } catch {} + } else { + try { emptyEl.style.display = 'none' } catch {} + for (const rec of records) { + try { + listEl.appendChild(renderRecordRow(rec, info)) + } catch {} + } + } + + updateBackVisibility() + } + + const openNestedChatHistory = (rec) => { + const xml = String(rec?.recordItem || '').trim() + if (!xml) return + if (currentState) { + historyStack = [...historyStack, currentState] + } + const state = buildChatHistoryState({ + title: String(rec?.title || '聊天记录'), + recordItem: xml, + content: String(rec?.content || ''), + }) + applyChatHistoryState(state) + } + + if (backBtn) { + backBtn.addEventListener('click', (ev) => { + try { ev.preventDefault() } catch {} + if (!Array.isArray(historyStack) || !historyStack.length) return + const prev = historyStack[historyStack.length - 1] + historyStack = historyStack.slice(0, -1) + applyChatHistoryState(prev) + }) + } + + const openFromCard = (card) => { + const title = String(card?.getAttribute('data-title') || '聊天记录').trim() || '聊天记录' + const b64 = String(card?.getAttribute('data-record-item-b64') || '').trim() + const xml = decodeBase64Utf8(b64) + const lines = Array.from(card.querySelectorAll('.wechat-chat-history-line') || []) + .map((el) => String(el?.textContent || '').trim()) + .filter(Boolean) + + historyStack = [] + const state = buildChatHistoryState({ title, recordItem: xml, fallbackLines: lines }) + applyChatHistoryState(state) + + try { modal.classList.remove('hidden') } catch {} + try { modal.style.display = 'flex' } catch {} + try { modal.setAttribute('aria-hidden', 'false') } catch {} + try { document.body.style.overflow = 'hidden' } catch {} + } + + closeBtn.addEventListener('click', (ev) => { + try { ev.preventDefault() } catch {} + close() + }) + modal.addEventListener('click', (ev) => { + const t = ev && ev.target + if (t === modal) close() + }) + + document.addEventListener('keydown', (ev) => { + const key = String(ev?.key || '') + if (key === 'Escape' && !modal.classList.contains('hidden')) close() + + if ((key === 'Enter' || key === ' ') && modal.classList.contains('hidden')) { + const target = ev && ev.target + const card = target && target.closest ? target.closest('[data-wce-chat-history=\"1\"]') : null + if (!card) return + try { ev.preventDefault() } catch {} + openFromCard(card) + } + }, true) + + document.addEventListener('click', (ev) => { + const target = ev && ev.target + const card = target && target.closest ? target.closest('[data-wce-chat-history=\"1\"]') : null + if (!card) return + try { ev.preventDefault() } catch {} + openFromCard(card) + }, true) + } + + const initChatHistoryFloatingWindows = () => { + const mediaIndex = readMediaIndex() + let zIndex = 1000 + let cascade = 0 + let idSeed = 0 + + const clampNumber = (value, min, max) => { + const n = Number(value) + if (!Number.isFinite(n)) return min + return Math.min(max, Math.max(min, n)) + } + + const getViewport = () => { + const w = Math.max(320, window.innerWidth || 0) + const h = Math.max(240, window.innerHeight || 0) + return { w, h } + } + + const getPoint = (ev) => { + try { + return (ev && ev.touches && ev.touches[0]) ? ev.touches[0] : ev + } catch { + return ev + } + } + + const buildChatHistoryState = (payload) => { + const title = String(payload?.title || '聊天记录').trim() || '聊天记录' + const xml = String(payload?.recordItem || '').trim() + const parsed = parseChatHistoryRecord(xml) + const info = (parsed && parsed.info) ? parsed.info : { isChatRoom: false } + let records = (parsed && Array.isArray(parsed.items)) ? parsed.items : [] + + if (!records.length) { + const lines = Array.isArray(payload?.fallbackLines) + ? payload.fallbackLines + : String(payload?.content || '').trim().split(/\r?\n/).map((x) => String(x || '').trim()).filter(Boolean) + records = lines.map((line, idx) => ({ id: String(idx), renderType: 'text', content: line, sourcename: '', sourcetime: '' })) + } + + return { title, info, records } + } + + const renderRecordRow = (rec, info, onOpenNested) => { + const row = document.createElement('div') + row.className = 'px-4 py-3 flex gap-3 border-b border-gray-100 bg-[#f7f7f7]' + + const avatarWrap = document.createElement('div') + avatarWrap.className = 'w-9 h-9 rounded-md overflow-hidden bg-gray-200 flex-shrink-0' + const name0 = String(rec?.sourcename || '').trim() || '?' + const avatarUrlRaw = normalizeChatHistoryUrl(rec?.sourceheadurl) + const avatarLocal = (mediaIndex && mediaIndex.remote && mediaIndex.remote[avatarUrlRaw]) ? String(mediaIndex.remote[avatarUrlRaw] || '') : '' + const avatarUrlLower = String(avatarUrlRaw || '').trim().toLowerCase() + const avatarUrl = avatarLocal || ((avatarUrlLower.startsWith('http://') || avatarUrlLower.startsWith('https://')) ? avatarUrlRaw : '') + if (avatarUrl) { + const img = document.createElement('img') + img.src = avatarUrl + img.alt = '头像' + img.className = 'w-full h-full object-cover' + try { img.referrerPolicy = 'no-referrer' } catch {} + img.onerror = () => { + try { avatarWrap.textContent = '' } catch {} + const fb = document.createElement('div') + fb.className = 'w-full h-full flex items-center justify-center text-xs font-bold text-gray-600' + fb.textContent = String(name0.charAt(0) || '?') + avatarWrap.appendChild(fb) + } + avatarWrap.appendChild(img) + } else { + const fb = document.createElement('div') + fb.className = 'w-full h-full flex items-center justify-center text-xs font-bold text-gray-600' + fb.textContent = String(name0.charAt(0) || '?') + avatarWrap.appendChild(fb) + } + + const main = document.createElement('div') + main.className = 'min-w-0 flex-1' + + const header = document.createElement('div') + header.className = 'flex items-start gap-2' + + const headerLeft = document.createElement('div') + headerLeft.className = 'min-w-0 flex-1' + const senderName = String(rec?.sourcename || '').trim() + if (info && info.isChatRoom && senderName) { + const sn = document.createElement('div') + sn.className = 'text-xs text-gray-500 leading-none truncate mb-1' + sn.textContent = senderName + headerLeft.appendChild(sn) + } + + const headerRight = document.createElement('div') + headerRight.className = 'text-xs text-gray-400 flex-shrink-0 leading-none' + const timeText = String(rec?.sourcetime || '').trim() + headerRight.textContent = timeText + + header.appendChild(headerLeft) + if (timeText) header.appendChild(headerRight) + + const body = document.createElement('div') + body.className = 'mt-1' + + const rt = String(rec?.renderType || 'text') + const content = String(rec?.content || '').trim() + const serverId = String(rec?.fromnewmsgid || '').trim() + const serverMd5 = resolveServerMd5(mediaIndex, serverId) + + if (rt === 'chatHistory') { + const card = document.createElement('div') + card.className = 'wechat-chat-history-card wechat-special-card msg-radius' + + const chBody = document.createElement('div') + chBody.className = 'wechat-chat-history-body' + + const chTitle = document.createElement('div') + chTitle.className = 'wechat-chat-history-title' + chTitle.textContent = String(rec?.title || '聊天记录') + chBody.appendChild(chTitle) + + const raw = String(rec?.content || '').trim() + const lines = raw ? raw.split(/\r?\n/).map((x) => String(x || '').trim()).filter(Boolean).slice(0, 4) : [] + if (lines.length) { + const preview = document.createElement('div') + preview.className = 'wechat-chat-history-preview' + for (const line of lines) { + const el = document.createElement('div') + el.className = 'wechat-chat-history-line' + el.textContent = line + preview.appendChild(el) + } + chBody.appendChild(preview) + } + + card.appendChild(chBody) + + const bottom = document.createElement('div') + bottom.className = 'wechat-chat-history-bottom' + const label = document.createElement('span') + label.textContent = '聊天记录' + bottom.appendChild(label) + card.appendChild(bottom) + + const nestedXml = String(rec?.recordItem || '').trim() + if (nestedXml) { + card.classList.add('cursor-pointer') + card.addEventListener('click', (ev) => { + try { ev.preventDefault() } catch {} + try { ev.stopPropagation() } catch {} + if (typeof onOpenNested === 'function') onOpenNested(rec) + }) + } + + body.appendChild(card) + } else if (rt === 'link') { + const href = normalizeChatHistoryUrl(rec?.url) || normalizeChatHistoryUrl(rec?.externurl) + const heading = String(rec?.title || '').trim() || content || href || '链接' + const desc = String(rec?.content || '').trim() + + const thumbMd5 = pickFirstMd5(rec?.fullmd5, rec?.thumbfullmd5, rec?.cdnthumbmd5, rec?.md5, rec?.id) + let previewUrl = resolveMediaMd5(mediaIndex, 'preview', thumbMd5) + if (!previewUrl && serverMd5) previewUrl = resolveMediaMd5(mediaIndex, 'preview', serverMd5) + if (!previewUrl) previewUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + + const card = document.createElement(href ? 'a' : 'div') + card.className = 'wechat-link-card wechat-special-card msg-radius cursor-pointer' + if (href) { + card.href = href + card.target = '_blank' + card.rel = 'noreferrer noopener' + } + try { card.style.textDecoration = 'none' } catch {} + try { card.style.outline = 'none' } catch {} + + const linkContent = document.createElement('div') + linkContent.className = 'wechat-link-content' + + const linkInfo = document.createElement('div') + linkInfo.className = 'wechat-link-info' + const titleEl = document.createElement('div') + titleEl.className = 'wechat-link-title' + titleEl.textContent = heading + linkInfo.appendChild(titleEl) + if (desc) { + const descEl = document.createElement('div') + descEl.className = 'wechat-link-desc' + descEl.textContent = desc + linkInfo.appendChild(descEl) + } + linkContent.appendChild(linkInfo) + + if (previewUrl) { + const thumb = document.createElement('div') + thumb.className = 'wechat-link-thumb' + const img = document.createElement('img') + img.src = previewUrl + img.alt = heading || '链接预览' + img.className = 'wechat-link-thumb-img' + try { img.referrerPolicy = 'no-referrer' } catch {} + thumb.appendChild(img) + linkContent.appendChild(thumb) + } + + card.appendChild(linkContent) + + const fromRow = document.createElement('div') + fromRow.className = 'wechat-link-from' + const fromAvatar = document.createElement('div') + fromAvatar.className = 'wechat-link-from-avatar' + + const fromUrlRaw = normalizeChatHistoryUrl(rec?.sourceheadurl) + const fromLocal = (mediaIndex && mediaIndex.remote && mediaIndex.remote[fromUrlRaw]) ? String(mediaIndex.remote[fromUrlRaw] || '') : '' + const fromLower = String(fromUrlRaw || '').trim().toLowerCase() + const fromUrl = fromLocal || ((fromLower.startsWith('http://') || fromLower.startsWith('https://')) ? fromUrlRaw : '') + const fromText = String(rec?.sourcename || '').trim() + if (fromUrl) { + const img = document.createElement('img') + img.src = fromUrl + img.alt = '' + img.className = 'wechat-link-from-avatar-img' + try { img.referrerPolicy = 'no-referrer' } catch {} + img.onerror = () => { + try { fromAvatar.textContent = '' } catch {} + const span = document.createElement('span') + span.textContent = String(fromText ? fromText.charAt(0) : '\u200B') + fromAvatar.appendChild(span) + } + fromAvatar.appendChild(img) + } else { + const span = document.createElement('span') + span.textContent = String(fromText ? fromText.charAt(0) : '\u200B') + fromAvatar.appendChild(span) + } + const fromName = document.createElement('div') + fromName.className = 'wechat-link-from-name' + fromName.textContent = fromText || '\u200B' + fromRow.appendChild(fromAvatar) + fromRow.appendChild(fromName) + card.appendChild(fromRow) + + body.appendChild(card) + } else if (rt === 'video') { + const videoMd5 = pickFirstMd5(rec?.fullmd5, rec?.md5, rec?.id) + const thumbMd5 = pickFirstMd5(rec?.thumbfullmd5, rec?.cdnthumbmd5) || videoMd5 + let videoUrl = resolveMediaMd5(mediaIndex, 'video', videoMd5) + if (!videoUrl && serverMd5) videoUrl = resolveMediaMd5(mediaIndex, 'video', serverMd5) + if (!videoUrl) videoUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + + let thumbUrl = resolveMediaMd5(mediaIndex, 'videoThumb', thumbMd5) + if (!thumbUrl && serverMd5) thumbUrl = resolveMediaMd5(mediaIndex, 'videoThumb', serverMd5) + if (!thumbUrl) thumbUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + + const wrap = document.createElement('div') + wrap.className = 'msg-radius overflow-hidden relative bg-black/5 inline-block' + + if (thumbUrl) { + const img = document.createElement('img') + img.src = thumbUrl + img.alt = '视频' + img.className = 'block w-[220px] max-w-[260px] h-auto max-h-[260px] object-cover' + wrap.appendChild(img) + } else { + const t = document.createElement('div') + t.className = 'px-3 py-2 text-sm text-gray-700' + t.textContent = content || '[视频]' + wrap.appendChild(t) + } + + if (thumbUrl) { + const overlay = document.createElement(videoUrl ? 'a' : 'div') + if (videoUrl) { + overlay.href = videoUrl + overlay.target = '_blank' + overlay.rel = 'noreferrer noopener' + } + overlay.className = 'absolute inset-0 flex items-center justify-center' + const btn = document.createElement('div') + btn.className = 'w-12 h-12 rounded-full bg-black/45 flex items-center justify-center' + btn.innerHTML = '' + overlay.appendChild(btn) + wrap.appendChild(overlay) + } + + body.appendChild(wrap) + } else if (rt === 'image') { + const imageMd5 = pickFirstMd5(rec?.fullmd5, rec?.thumbfullmd5, rec?.cdnthumbmd5, rec?.md5, rec?.id) + let imgUrl = resolveMediaMd5(mediaIndex, 'image', imageMd5) + if (!imgUrl && serverMd5) imgUrl = resolveMediaMd5(mediaIndex, 'image', serverMd5) + if (!imgUrl) imgUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + if (imgUrl) { + const outer = document.createElement('div') + outer.className = 'msg-radius overflow-hidden cursor-pointer inline-block' + const a = document.createElement('a') + a.href = imgUrl + a.target = '_blank' + a.rel = 'noreferrer noopener' + const img = document.createElement('img') + img.src = imgUrl + img.alt = '图片' + img.className = 'max-w-[240px] max-h-[240px] object-cover' + a.appendChild(img) + outer.appendChild(a) + body.appendChild(outer) + } else { + const t = document.createElement('div') + t.className = 'px-3 py-2 text-sm text-gray-700 whitespace-pre-wrap break-words' + t.textContent = content || '[图片]' + body.appendChild(t) + } + } else if (rt === 'emoji') { + const emojiMd5 = pickFirstMd5(rec?.md5, rec?.fullmd5, rec?.thumbfullmd5, rec?.cdnthumbmd5, rec?.id) + let emojiUrl = resolveMediaMd5(mediaIndex, 'emoji', emojiMd5) + if (!emojiUrl && serverMd5) emojiUrl = resolveMediaMd5(mediaIndex, 'emoji', serverMd5) + if (!emojiUrl) emojiUrl = resolveRemoteAny(mediaIndex, rec?.externurl, rec?.cdnurlstring, rec?.encrypturlstring) + if (emojiUrl) { + const img = document.createElement('img') + img.src = emojiUrl + img.alt = '表情' + img.className = 'w-24 h-24 object-contain' + body.appendChild(img) + } else { + const t = document.createElement('div') + t.className = 'px-3 py-2 text-sm text-gray-700 whitespace-pre-wrap break-words' + t.textContent = content || '[表情]' + body.appendChild(t) + } + } else { + const t = document.createElement('div') + t.className = 'px-3 py-2 text-sm text-gray-700 whitespace-pre-wrap break-words' + t.textContent = content || '' + body.appendChild(t) + } + + main.appendChild(header) + main.appendChild(body) + + row.appendChild(avatarWrap) + row.appendChild(main) + return row + } + + const focusWindow = (wrap) => { + zIndex += 1 + try { wrap.style.zIndex = String(zIndex) } catch {} + } + + const openChatHistoryWindow = (payload, opts) => { + const state = buildChatHistoryState(payload || {}) + const info = state.info || { isChatRoom: false } + const records = Array.isArray(state.records) ? state.records : [] + + const vp = getViewport() + const width = Math.min(560, Math.max(320, Math.floor(vp.w * 0.92))) + const height = Math.min(560, Math.max(240, Math.floor(vp.h * 0.8))) + + let x = Math.max(8, Math.floor((vp.w - width) / 2)) + let y = Math.max(8, Math.floor((vp.h - height) / 2)) + + const spawnFrom = opts && opts.spawnFrom + if (spawnFrom) { + x = Number(spawnFrom.x || x) + 24 + y = Number(spawnFrom.y || y) + 24 + } else { + x += cascade + y += cascade + cascade = (cascade + 24) % 120 + } + + x = clampNumber(x, 8, Math.max(8, vp.w - width - 8)) + y = clampNumber(y, 8, Math.max(8, vp.h - height - 8)) + + const win = { id: String(++idSeed), x, y, width, height } + + const wrap = document.createElement('div') + wrap.className = 'fixed' + wrap.style.left = `${win.x}px` + wrap.style.top = `${win.y}px` + wrap.style.zIndex = String(++zIndex) + + const box = document.createElement('div') + box.className = 'bg-[#f7f7f7] rounded-xl shadow-xl overflow-hidden border border-gray-200 flex flex-col' + box.style.width = `${win.width}px` + box.style.height = `${win.height}px` + wrap.appendChild(box) + + const header = document.createElement('div') + header.className = 'px-3 py-2 bg-[#f7f7f7] border-b border-gray-200 flex items-center justify-between select-none cursor-move' + box.appendChild(header) + + const titleEl = document.createElement('div') + titleEl.className = 'text-sm text-[#161616] truncate min-w-0' + titleEl.textContent = String(state.title || '聊天记录') + header.appendChild(titleEl) + + const closeBtn = document.createElement('button') + closeBtn.type = 'button' + closeBtn.className = 'p-2 rounded hover:bg-black/5 flex-shrink-0' + try { closeBtn.setAttribute('aria-label', '关闭') } catch {} + try { closeBtn.setAttribute('title', '关闭') } catch {} + closeBtn.innerHTML = '' + header.appendChild(closeBtn) + + const body = document.createElement('div') + body.className = 'flex-1 overflow-auto bg-[#f7f7f7]' + box.appendChild(body) + + if (!records.length) { + const empty = document.createElement('div') + empty.className = 'text-sm text-gray-500 text-center py-10' + empty.textContent = '没有可显示的聊天记录' + body.appendChild(empty) + } else { + const onOpenNested = (rec) => { + const xml = String(rec?.recordItem || '').trim() + if (!xml) return + openChatHistoryWindow({ + title: String(rec?.title || '聊天记录'), + recordItem: xml, + content: String(rec?.content || ''), + }, { spawnFrom: win }) + } + for (const rec of records) { + try { + body.appendChild(renderRecordRow(rec, info, onOpenNested)) + } catch {} + } + } + + const updatePos = () => { + try { wrap.style.left = `${win.x}px` } catch {} + try { wrap.style.top = `${win.y}px` } catch {} + } + + closeBtn.addEventListener('click', (ev) => { + try { ev.preventDefault() } catch {} + try { ev.stopPropagation() } catch {} + try { wrap.remove() } catch { + try { if (wrap.parentElement) wrap.parentElement.removeChild(wrap) } catch {} + } + }) + + const startDrag = (ev) => { + const t = ev && ev.target + if (t && t.closest && t.closest('button')) return + + focusWindow(wrap) + const p0 = getPoint(ev) + const ox = Number(p0?.clientX || 0) - win.x + const oy = Number(p0?.clientY || 0) - win.y + + const onMove = (e2) => { + const p = getPoint(e2) + if (!p) return + try { if (e2 && typeof e2.preventDefault === 'function') e2.preventDefault() } catch {} + + const vp2 = getViewport() + const nx = Number(p.clientX || 0) - ox + const ny = Number(p.clientY || 0) - oy + win.x = clampNumber(nx, 8, Math.max(8, vp2.w - win.width - 8)) + win.y = clampNumber(ny, 8, Math.max(8, vp2.h - win.height - 8)) + updatePos() + } + + const stop = () => { + try { document.removeEventListener('mousemove', onMove) } catch {} + try { document.removeEventListener('touchmove', onMove) } catch {} + } + + try { document.addEventListener('mousemove', onMove) } catch {} + try { document.addEventListener('mouseup', () => stop(), { once: true }) } catch {} + try { document.addEventListener('touchmove', onMove, { passive: false }) } catch {} + try { document.addEventListener('touchend', () => stop(), { once: true }) } catch {} + + try { ev.preventDefault() } catch {} + } + + header.addEventListener('mousedown', startDrag) + header.addEventListener('touchstart', startDrag, { passive: false }) + + wrap.addEventListener('mousedown', () => focusWindow(wrap)) + wrap.addEventListener('touchstart', () => focusWindow(wrap), { passive: true }) + + try { document.body.appendChild(wrap) } catch {} + return win + } + + document.addEventListener('keydown', (ev) => { + const key = String(ev?.key || '') + if (key !== 'Enter' && key !== ' ') return + const target = ev && ev.target + const card = target && target.closest ? target.closest('[data-wce-chat-history=\"1\"]') : null + if (!card) return + try { ev.preventDefault() } catch {} + const title = String(card?.getAttribute('data-title') || '聊天记录').trim() || '聊天记录' + const b64 = String(card?.getAttribute('data-record-item-b64') || '').trim() + const xml = decodeBase64Utf8(b64) + const lines = Array.from(card.querySelectorAll('.wechat-chat-history-line') || []) + .map((el) => String(el?.textContent || '').trim()) + .filter(Boolean) + openChatHistoryWindow({ title, recordItem: xml, fallbackLines: lines }) + }, true) + + document.addEventListener('click', (ev) => { + const target = ev && ev.target + const card = target && target.closest ? target.closest('[data-wce-chat-history=\"1\"]') : null + if (!card) return + try { ev.preventDefault() } catch {} + const title = String(card?.getAttribute('data-title') || '聊天记录').trim() || '聊天记录' + const b64 = String(card?.getAttribute('data-record-item-b64') || '').trim() + const xml = decodeBase64Utf8(b64) + const lines = Array.from(card.querySelectorAll('.wechat-chat-history-line') || []) + .map((el) => String(el?.textContent || '').trim()) + .filter(Boolean) + openChatHistoryWindow({ title, recordItem: xml, fallbackLines: lines }) + }, true) + } + + document.addEventListener('DOMContentLoaded', async () => { + initBrandAttribution() + const integrityOk = await initExportIntegrity() + if (!integrityOk) return + hideJsMissingBanner() + updateDprVar() + try { + window.addEventListener('resize', updateDprVar) + } catch {} + + initSessionSearch() + initVoicePlayback() + initMessageSearchAndDateJump() + initChatHistoryFloatingWindows() + initPagedMessageLoading() + + const select = document.getElementById('messageTypeFilter') + if (select) { + select.addEventListener('change', applyMessageTypeFilter) + applyMessageTypeFilter() + } + + updateSessionMessageCount() + scrollToBottom() + try { + window.addEventListener('load', () => { + updateSessionMessageCount() + scrollToBottom() + setTimeout(scrollToBottom, 60) + }) + } catch {} + }) + + // Best-effort: defer scripts execute after the DOM is parsed, so we can hide the banner immediately. + hideJsMissingBanner() +})() diff --git a/pyproject.toml b/pyproject.toml index 9077a5f0..fcd52e21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "pilk>=0.2.4", "pypinyin>=0.53.0", "jieba>=0.42.1", - "wx_key>=2.0.1", + "wx_key>=2.0.1; sys_platform == 'win32'", "pefile>=2024.8.26; sys_platform == 'win32'", "pymem>=1.14.0; sys_platform == 'win32'", "yara-python>=4.5.2; sys_platform == 'win32'", @@ -32,6 +32,15 @@ dependencies = [ build = [ "pyinstaller>=6.0.0", ] +voice-transcription = [ + "faster-whisper>=1.1.0", + "opencc-python-reimplemented>=0.1.7", +] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", +] [project.scripts] wechat-decrypt = "main:main" @@ -42,11 +51,15 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/wechat_decrypt_tool"] +artifacts = [ + "src/wechat_decrypt_tool/native/*.pyd", +] include = [ "src/wechat_decrypt_tool/native/VoipEngine.dll", "src/wechat_decrypt_tool/native/wcdb_api.dll", "src/wechat_decrypt_tool/native/WCDB.dll", "src/wechat_decrypt_tool/native/wce_integrity*.pyd", + "src/wechat_decrypt_tool/native/macos/**/*", "src/wechat_decrypt_tool/native/weflow_wasm/weflow_wasm_keystream.js", "src/wechat_decrypt_tool/native/weflow_wasm/wasm_video_decode.js", "src/wechat_decrypt_tool/native/weflow_wasm/wasm_video_decode.wasm", @@ -55,3 +68,6 @@ include = [ [tool.uv] find-links = ["./tools/key_wheels/"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/src/wechat_decrypt_tool/backend_entry.py b/src/wechat_decrypt_tool/backend_entry.py index a5c79015..16f615b2 100644 --- a/src/wechat_decrypt_tool/backend_entry.py +++ b/src/wechat_decrypt_tool/backend_entry.py @@ -5,7 +5,9 @@ """ import multiprocessing +import json import os +import sys # PyInstaller/frozen Windows builds re-launch this executable for # multiprocessing workers. The memory/DLL key scanners use process pools; if @@ -14,13 +16,32 @@ if __name__ == "__main__": multiprocessing.freeze_support() -import uvicorn +def _run_opencc_smoke() -> None: + from opencc import OpenCC + import opencc -from wechat_decrypt_tool.api import app -from wechat_decrypt_tool.runtime_settings import read_effective_backend_host, read_effective_backend_port + converter = OpenCC("t2s") + payload = { + "frozen": bool(getattr(sys, "frozen", False)), + "openccModule": str(getattr(opencc, "__file__", "") or ""), + "results": { + "繁體中文": converter.convert("繁體中文"), + "軟體與資料庫": converter.convert("軟體與資料庫"), + }, + } + print(json.dumps(payload, ensure_ascii=True)) def main() -> None: + if "--smoke-opencc" in sys.argv[1:]: + _run_opencc_smoke() + return + + import uvicorn + + from wechat_decrypt_tool.api import app + from wechat_decrypt_tool.runtime_settings import read_effective_backend_host, read_effective_backend_port + host, _ = read_effective_backend_host(default="127.0.0.1") port, _ = read_effective_backend_port(default=10392) uvicorn.run(app, host=host, port=port, log_level="info") diff --git a/src/wechat_decrypt_tool/chat_export_service.py b/src/wechat_decrypt_tool/chat_export_service.py index 2ee7fda6..d898ded7 100644 --- a/src/wechat_decrypt_tool/chat_export_service.py +++ b/src/wechat_decrypt_tool/chat_export_service.py @@ -88,6 +88,7 @@ get_sessions as _wcdb_get_sessions, open_message_cursor as _wcdb_open_message_cursor, ) +from .voice_transcription import VoiceTranscriptionError, get_voice_transcription_service logger = get_logger(__name__) @@ -1020,6 +1021,7 @@ def create_job( html_page_size: int = 1000, privacy_mode: bool, file_name: Optional[str], + transcribe_voice: bool = False, ) -> ExportJob: account_dir = _resolve_account_dir(account) source_requested = _normalize_chat_source(source, default="auto") @@ -1071,6 +1073,7 @@ def create_job( "htmlPageSize": int(html_page_size) if int(html_page_size or 0) > 0 else int(html_page_size or 0), "privacyMode": bool(privacy_mode), "fileName": str(file_name or "").strip(), + "transcribeVoice": bool(transcribe_voice), }, ) @@ -1145,6 +1148,7 @@ def run_prepared_archive( "htmlPageSize": 1000, "privacyMode": False, "fileName": str(file_name or "").strip(), + "transcribeVoice": False, "_archiveTitle": str(title or "").strip() or "聊天记录", "_preparedConversations": prepared, }, @@ -1242,6 +1246,8 @@ def _run_job(self, job: ExportJob, account_dir: Path) -> None: allow_process_key_extract = bool(opts.get("allowProcessKeyExtract")) download_remote_media = bool(opts.get("downloadRemoteMedia")) privacy_mode = bool(opts.get("privacyMode")) + transcribe_voice = bool(opts.get("transcribeVoice")) and not privacy_mode + job.options["_transcribeVoiceActive"] = transcribe_voice try: html_page_size = int(opts.get("htmlPageSize") or 1000) except Exception: @@ -1292,6 +1298,7 @@ def _run_job(self, job: ExportJob, account_dir: Path) -> None: htmlPageSize=html_page_size, downloadRemoteMedia=download_remote_media, privacyMode=privacy_mode, + transcribeVoice=transcribe_voice, ) _raise_if_job_cancelled(job, "options_resolved", trace) @@ -2115,6 +2122,7 @@ def esc_attr(v: Any) -> str: "downloadRemoteMedia": bool(download_remote_media), "htmlPageSize": int(html_page_size) if export_format == "html" else None, "privacyMode": privacy_mode, + "transcribeVoice": transcribe_voice, "sourceRequested": source_requested, }, "stats": { @@ -3719,6 +3727,13 @@ def lookup_alias(username: str) -> str: serverId=msg.get("serverId"), ) + _attach_voice_transcript( + account_dir=account_dir, + msg=msg, + report=report, + job=job, + ) + if not first: tw.write(",\n") tw.write(" " + json.dumps(msg, ensure_ascii=False)) @@ -3780,13 +3795,17 @@ def write_workbook(json_path: Path) -> None: rows: list[list[str]] = [] for index, message_raw in enumerate(messages if isinstance(messages, list) else [], start=1): message = message_raw if isinstance(message_raw, dict) else {"value": message_raw} - content = str( - message.get("content") - or message.get("title") - or message.get("description") - or message.get("fileName") - or "" - ).strip() + if str(message.get("renderType") or "") == "voice" and message.get("voiceTranscriptStatus") == "success": + transcript = str(message.get("voiceTranscript") or "").strip() or "[未识别到文字]" + content = f"{message.get('content') or '[语音]'} 转写:{transcript}" + else: + content = str( + message.get("content") + or message.get("title") + or message.get("description") + or message.get("fileName") + or "" + ).strip() if not content: content = json.dumps(message, ensure_ascii=False, default=str, sort_keys=True) rows.append( @@ -4064,6 +4083,13 @@ def lookup_alias(username: str) -> str: serverId=msg.get("serverId"), ) + _attach_voice_transcript( + account_dir=account_dir, + msg=msg, + report=report, + job=job, + ) + tw.write(_format_message_line_txt(msg=msg) + "\n") exported += 1 @@ -4949,6 +4975,13 @@ def _mark_exported() -> None: serverId=msg.get("serverId"), ) + _attach_voice_transcript( + account_dir=account_dir, + msg=msg, + report=report, + job=job, + ) + rt = str(msg.get("renderType") or "text").strip() or "text" create_time_text = str(msg.get("createTimeText") or "").strip() try: @@ -5091,9 +5124,11 @@ def _mark_exported() -> None: voice_dir_cls = "wechat-voice-sent" if is_sent else "wechat-voice-received" content_dir_cls = " flex-row-reverse" if is_sent else "" icon_dir_cls = "voice-icon-sent" if is_sent else "voice-icon-received" + wrapper_dir_cls = "wechat-voice-wrapper--sent" if is_sent else "wechat-voice-wrapper--received" + transcript_dir_cls = "wechat-voice-transcript--sent" if is_sent else "wechat-voice-transcript--received" voice_id = str(msg.get("id") or "").strip() - tw.write('
\n') + tw.write(f'
\n') tw.write( f'
\n' ) @@ -5116,6 +5151,22 @@ def _mark_exported() -> None: tw.write("
\n") if voice: tw.write(f' \n') + transcript = str(msg.get("voiceTranscript") or "").strip() + transcript_status = str(msg.get("voiceTranscriptStatus") or "").strip() + transcript_error = str(msg.get("voiceTranscriptError") or "").strip() + if transcript_status == "success": + display_transcript = transcript or "未识别到文字" + tw.write( + '
' + f'{esc_text(display_transcript)}
\n' + ) + elif transcript_status == "error" and transcript_error: + tw.write( + '
' + f'转写失败:{esc_text(transcript_error)}
\n' + ) tw.write("
\n") elif rt == "location": title = str( @@ -5797,6 +5848,15 @@ def _format_message_line_txt(*, msg: dict[str, Any]) -> str: if lat and lng: details.append(f"坐标={lng},{lat}") extra = (" " + " ".join(details)) if details else "" + elif rt == "voice": + transcript = str(msg.get("voiceTranscript") or "").strip() + transcript_error = str(msg.get("voiceTranscriptError") or "").strip() + if transcript: + extra = f" 转写={transcript}" + elif str(msg.get("voiceTranscriptStatus") or "") == "success": + extra = " 转写=[未识别到文字]" + elif transcript_error: + extra = f" 转写失败={transcript_error}" media = msg.get("offlineMedia") or [] media_desc = "" @@ -5908,6 +5968,11 @@ def _privacy_scrub_message( "locationLng", "locationPoiname", "locationLabel", + "voiceTranscript", + "voiceTranscriptStatus", + "voiceTranscriptError", + "voiceTranscriptLanguage", + "voiceTranscriptModel", ): if k in msg: msg[k] = "" @@ -5915,6 +5980,52 @@ def _privacy_scrub_message( msg.pop("offlineMedia", None) +def _attach_voice_transcript( + *, + account_dir: Path, + msg: dict[str, Any], + report: dict[str, Any], + job: ExportJob, +) -> None: + if not bool((job.options or {}).get("_transcribeVoiceActive")): + return + if str(msg.get("renderType") or "").strip() != "voice": + return + + server_id = int(msg.get("serverId") or 0) + if server_id <= 0: + return + _raise_if_job_cancelled(job, "voice_transcription.start", serverId=server_id) + + stats = report.setdefault( + "voiceTranscription", + {"success": 0, "failed": 0, "cached": 0}, + ) + try: + result = get_voice_transcription_service().transcribe_voice( + account_dir=account_dir, + server_id=server_id, + ) + msg["voiceTranscript"] = str(result.get("text") or "").strip() + msg["voiceTranscriptStatus"] = "success" + msg["voiceTranscriptError"] = "" + msg["voiceTranscriptLanguage"] = str(result.get("language") or "").strip() + msg["voiceTranscriptModel"] = str(result.get("model") or "").strip() + stats["success"] = int(stats.get("success") or 0) + 1 + if result.get("cached"): + stats["cached"] = int(stats.get("cached") or 0) + 1 + except VoiceTranscriptionError as exc: + msg["voiceTranscript"] = "" + msg["voiceTranscriptStatus"] = "error" + msg["voiceTranscriptError"] = exc.user_message + stats["failed"] = int(stats.get("failed") or 0) + 1 + except Exception as exc: + msg["voiceTranscript"] = "" + msg["voiceTranscriptStatus"] = "error" + msg["voiceTranscriptError"] = f"语音识别失败:{type(exc).__name__}" + stats["failed"] = int(stats.get("failed") or 0) + 1 + + def _attach_offline_media( *, zf: zipfile.ZipFile, diff --git a/src/wechat_decrypt_tool/export_integrity.py b/src/wechat_decrypt_tool/export_integrity.py index 79a62da9..593435ad 100644 --- a/src/wechat_decrypt_tool/export_integrity.py +++ b/src/wechat_decrypt_tool/export_integrity.py @@ -9,6 +9,7 @@ import re import sys import zipfile +import platform from pathlib import Path from typing import Any, Iterable @@ -23,14 +24,49 @@ def load_wce_integrity_native() -> Any: return existing repo_root = Path(__file__).resolve().parents[2] - candidates = [ - repo_root / "native" / "wce_integrity" / "target" / "release" / "wce_integrity.dll", - repo_root / "native" / "wce_integrity" / "target-next" / "release" / "wce_integrity.dll", - Path(__file__).resolve().parent / "native" / "wce_integrity.pyd", - ] - candidates = [path for path in candidates if path.is_file()] - candidates.sort(key=lambda path: path.stat().st_mtime_ns, reverse=True) + package_native_dir = Path(__file__).resolve().parent / "native" + candidates: list[Path] = [] + architecture = (platform.machine() or "").lower() + if architecture in {"arm64", "aarch64"}: + candidates.append( + Path(__file__).resolve().parent / "native" / "macos" / "arm64" / "libwce_integrity.dylib" + ) + if getattr(sys, "frozen", False): + executable_native = Path(sys.executable).resolve().parent / "native" + candidates.extend( + ( + executable_native / "wce_integrity.pyd", + executable_native / "libwce_integrity.dylib", + executable_native / "libwce_integrity.so", + ) + ) + if sys.platform == "win32": + candidates.append(package_native_dir / "wce_integrity.pyd") + else: + candidates.extend( + ( + repo_root / "native" / "wce_integrity" / "target" / "release" / "libwce_integrity.dylib", + repo_root / "native" / "wce_integrity" / "target" / "release" / "libwce_integrity.so", + package_native_dir / "libwce_integrity.dylib", + package_native_dir / "libwce_integrity.so", + ) + ) + bundle_root = getattr(sys, "_MEIPASS", None) + if bundle_root: + bundled_native = Path(bundle_root) / "wechat_decrypt_tool" / "native" + candidates.extend( + ( + bundled_native / "wce_integrity.pyd", + bundled_native / "libwce_integrity.dylib", + bundled_native / "libwce_integrity.so", + ) + ) + seen: set[str] = set() for build_path in candidates: + candidate_key = str(build_path) + if candidate_key in seen or not build_path.is_file(): + continue + seen.add(candidate_key) try: loader = importlib.machinery.ExtensionFileLoader(module_name, str(build_path)) spec = importlib.util.spec_from_file_location(module_name, build_path, loader=loader) diff --git a/src/wechat_decrypt_tool/key_v4.py b/src/wechat_decrypt_tool/key_v4.py index c0c38c1f..34d24a30 100644 --- a/src/wechat_decrypt_tool/key_v4.py +++ b/src/wechat_decrypt_tool/key_v4.py @@ -7,10 +7,18 @@ from multiprocessing import freeze_support import sys -import pymem from Crypto.Protocol.KDF import PBKDF2 from Crypto.Hash import SHA512 -import yara + +try: + import pymem +except ImportError: + pymem = None + +try: + import yara +except ImportError: + yara = None # 定义必要的常量 PROCESS_ALL_ACCESS = 0x1F0FFF @@ -50,21 +58,33 @@ def verify_worker(task): """Pool worker wrapper for imap_unordered.""" return check_chunk(*task) -# Load Windows DLLs -kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) +if os.name == 'nt': + kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) + + OpenProcess = kernel32.OpenProcess + OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] + OpenProcess.restype = wintypes.HANDLE -OpenProcess = kernel32.OpenProcess -OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD] -OpenProcess.restype = wintypes.HANDLE + ReadProcessMemory = kernel32.ReadProcessMemory + ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.LPVOID, ctypes.c_size_t, + ctypes.POINTER(ctypes.c_size_t)] + ReadProcessMemory.restype = wintypes.BOOL -ReadProcessMemory = kernel32.ReadProcessMemory -ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t, - ctypes.POINTER(ctypes.c_size_t)] -ReadProcessMemory.restype = wintypes.BOOL + CloseHandle = kernel32.CloseHandle + CloseHandle.argtypes = [wintypes.HANDLE] + CloseHandle.restype = wintypes.BOOL +else: + kernel32 = None + OpenProcess = None + ReadProcessMemory = None + CloseHandle = None -CloseHandle = kernel32.CloseHandle -CloseHandle.argtypes = [wintypes.HANDLE] -CloseHandle.restype = wintypes.BOOL + +def _require_windows_runtime(): + if os.name != 'nt': + raise RuntimeError('V4 数据库密钥提取仅支持 Windows。') + if pymem is None or yara is None: + raise RuntimeError('V4 数据库密钥提取缺少 Windows 运行时依赖。') # 定义 MEMORY_BASIC_INFORMATION 结构 @@ -215,6 +235,7 @@ def is_potential_key(key: bytes) -> bool: def get_key_inner(pid, process_infos): """扫描可能为key的内存,返回密钥候选列表""" + _require_windows_runtime() process_handle = open_process(pid) rules_v4_key = r''' rule GetKeyAddrStub @@ -323,6 +344,12 @@ def recover_key(pid, db_file_path=None, internal_db_key=None): """ 主函数:从 WeChat 进程恢复密钥 """ + try: + _require_windows_runtime() + except RuntimeError as exc: + print(f"[-] {exc}") + return None + process_handle = open_process(pid) if not process_handle: print(f"[-] Failed to open process {pid}") @@ -361,6 +388,12 @@ def recover_key(pid, db_file_path=None, internal_db_key=None): if __name__ == '__main__': freeze_support() + + try: + _require_windows_runtime() + except RuntimeError as exc: + print(f"[-] {exc}") + sys.exit(1) try: pm = pymem.Pymem("Weixin.exe") diff --git a/src/wechat_decrypt_tool/native/wce_integrity.pyd b/src/wechat_decrypt_tool/native/wce_integrity.pyd index 27c51a7d..05834db3 100644 Binary files a/src/wechat_decrypt_tool/native/wce_integrity.pyd and b/src/wechat_decrypt_tool/native/wce_integrity.pyd differ diff --git a/src/wechat_decrypt_tool/routers/chat_export.py b/src/wechat_decrypt_tool/routers/chat_export.py index 21853195..e4b9f770 100644 --- a/src/wechat_decrypt_tool/routers/chat_export.py +++ b/src/wechat_decrypt_tool/routers/chat_export.py @@ -9,6 +9,7 @@ from ..chat_export_service import CHAT_EXPORT_MANAGER, get_chat_export_targets_preview from ..path_fix import PathFixRoute +from ..voice_transcription import VoiceTranscriptionError, get_voice_transcription_service router = APIRouter(route_class=PathFixRoute) @@ -70,10 +71,19 @@ class ChatExportCreateRequest(BaseModel): description="隐私模式导出:隐藏会话/用户名/内容,不打包头像与媒体", ) file_name: Optional[str] = Field(None, description="导出 zip 文件名(可选,不含/含 .zip 都可)") + transcribe_voice: bool = Field(False, description="使用本地 Whisper 将语音消息转成中文并写入导出文件") @router.post("/api/chat/exports", summary="创建聊天记录导出任务(离线 zip)") async def create_chat_export(req: ChatExportCreateRequest): + if req.transcribe_voice and not req.privacy_mode: + try: + await asyncio.to_thread(get_voice_transcription_service().ensure_available) + except VoiceTranscriptionError as exc: + raise HTTPException( + status_code=503, + detail={"code": exc.code, "message": exc.user_message}, + ) from exc try: job = CHAT_EXPORT_MANAGER.create_job( account=req.account, @@ -94,6 +104,7 @@ async def create_chat_export(req: ChatExportCreateRequest): html_page_size=req.html_page_size, privacy_mode=req.privacy_mode, file_name=req.file_name, + transcribe_voice=req.transcribe_voice, ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) diff --git a/src/wechat_decrypt_tool/routers/chat_media.py b/src/wechat_decrypt_tool/routers/chat_media.py index 2e03f7d4..97e0eda5 100644 --- a/src/wechat_decrypt_tool/routers/chat_media.py +++ b/src/wechat_decrypt_tool/routers/chat_media.py @@ -67,6 +67,12 @@ from ..path_fix import PathFixRoute from ..perf_trace import create_perf_trace from ..wcdb_realtime import WCDB_REALTIME, exec_query as _wcdb_exec_query, get_avatar_urls as _wcdb_get_avatar_urls +from ..voice_transcription import ( + VoiceTranscriptionError, + get_voice_transcription_service, + load_voice_data, + set_voice_transcription_device, +) logger = get_logger(__name__) @@ -80,6 +86,21 @@ _VIDEO_DIR_INDEX_MAX_ENTRIES = 32 +class VoiceTranscriptionRequest(BaseModel): + server_id: int = Field(..., description="语音消息服务端 ID") + account: Optional[str] = Field(None, description="账号目录名") + force: bool = Field(False, description="忽略缓存并重新识别") + + +class VoiceTranscriptionCacheLookupRequest(BaseModel): + server_ids: list[str] = Field(default_factory=list, description="语音消息服务端 ID 列表(精确字符串,避免 JS Number 精度丢失)") + account: Optional[str] = Field(None, description="账号目录名") + + +class VoiceTranscriptionSettingsRequest(BaseModel): + device: str = Field(..., description="推理设备:cpu 或 cuda") + + def _avatar_trace_enabled() -> bool: return str(os.environ.get("WECHAT_TOOL_AVATAR_TRACE", "")).strip().lower() in {"1", "true", "yes", "on"} @@ -3335,29 +3356,10 @@ async def get_chat_voice(server_id: int, account: Optional[str] = None): if not server_id: raise HTTPException(status_code=400, detail="Missing server_id.") account_dir = _resolve_account_dir(account) - media_db_path = account_dir / "media_0.db" - if not media_db_path.exists(): - raise HTTPException(status_code=404, detail="media_0.db not found.") - - conn = sqlite3.connect(str(media_db_path)) - conn.row_factory = sqlite3.Row - try: - row = conn.execute( - "SELECT voice_data FROM VoiceInfo WHERE svr_id = ? ORDER BY create_time DESC LIMIT 1", - (int(server_id),), - ).fetchone() - except Exception: - row = None - finally: - conn.close() - - if not row or row[0] is None: + data = await asyncio.to_thread(load_voice_data, account_dir, int(server_id)) + if not data: raise HTTPException(status_code=404, detail="Voice not found.") - data = bytes(row[0]) if isinstance(row[0], (memoryview, bytearray)) else row[0] - if not isinstance(data, (bytes, bytearray)): - data = bytes(data) - payload, ext, media_type = _convert_silk_to_browser_audio(data, preferred_format="mp3") if payload and ext != "silk": return Response( @@ -3374,6 +3376,77 @@ async def get_chat_voice(server_id: int, account: Optional[str] = None): ) +@router.get("/api/chat/media/voice/transcription/status", summary="检查本地 Whisper 语音转文字能力") +async def get_chat_voice_transcription_status(): + return await asyncio.to_thread(get_voice_transcription_service().status) + + +@router.put("/api/chat/media/voice/transcription/settings", summary="设置本地 Whisper 推理设备") +async def set_chat_voice_transcription_settings(req: VoiceTranscriptionSettingsRequest): + try: + configuration = await asyncio.to_thread(set_voice_transcription_device, req.device) + except VoiceTranscriptionError as exc: + status_code = 409 if exc.code == "device_locked" else 400 + raise HTTPException( + status_code=status_code, + detail={"code": exc.code, "message": exc.user_message}, + ) from exc + return {"status": "success", "configuration": configuration} + + +@router.post("/api/chat/media/voice/transcription", summary="将语音消息转成中文文字") +async def transcribe_chat_voice(req: VoiceTranscriptionRequest): + if int(req.server_id or 0) <= 0: + raise HTTPException(status_code=400, detail="Missing server_id.") + service = get_voice_transcription_service() + + def run_transcription(): + service.ensure_available() + account_dir = _resolve_account_dir(req.account) + return service.transcribe_voice( + account_dir=account_dir, + server_id=int(req.server_id), + force=bool(req.force), + ) + + try: + return await asyncio.to_thread(run_transcription) + except VoiceTranscriptionError as exc: + status_code = { + "invalid_server_id": 400, + "voice_not_found": 404, + "voice_decode_failed": 422, + "disabled": 503, + "model_not_configured": 503, + "model_not_ready": 503, + "dependency_missing": 503, + "model_load_failed": 503, + }.get(exc.code, 500) + raise HTTPException( + status_code=status_code, + detail={"code": exc.code, "message": exc.user_message}, + ) from exc + + +@router.post("/api/chat/media/voice/transcription/cache_lookup", summary="批量读取语音转写缓存(不触发识别)") +async def lookup_chat_voice_transcription_cache(req: VoiceTranscriptionCacheLookupRequest): + account_dir = _resolve_account_dir(req.account) + parsed_ids: list[int] = [] + for raw in (req.server_ids or [])[:512]: + try: + sid = int(str(raw or "").strip()) + except Exception: + continue + if sid > 0: + parsed_ids.append(sid) + service = get_voice_transcription_service() + items = await asyncio.to_thread(service.lookup_cached_transcripts, account_dir, parsed_ids) + return { + "status": "success", + "items": {str(sid): value for sid, value in items.items()}, + } + + @router.post("/api/chat/media/open_folder", summary="在资源管理器中打开媒体文件所在位置") async def open_chat_media_folder( kind: str, diff --git a/src/wechat_decrypt_tool/runtime_settings.py b/src/wechat_decrypt_tool/runtime_settings.py index b7196d42..23b645df 100644 --- a/src/wechat_decrypt_tool/runtime_settings.py +++ b/src/wechat_decrypt_tool/runtime_settings.py @@ -11,13 +11,17 @@ BACKEND_PORT_KEY = "backend_port" BACKEND_HOST_KEY = "backend_host" MCP_TOKEN_KEY = "mcp_token" +VOICE_TRANSCRIPTION_DEVICE_KEY = "voice_transcription_device" ENV_PORT_KEY = "WECHAT_TOOL_PORT" ENV_HOST_KEY = "WECHAT_TOOL_HOST" ENV_MCP_TOKEN_KEY = "WECHAT_TOOL_MCP_TOKEN" +ENV_VOICE_TRANSCRIPTION_DEVICE_KEY = "WECHAT_TOOL_WHISPER_DEVICE" ENV_FILE_KEY = "WECHAT_TOOL_ENV_FILE" DEFAULT_ENV_FILENAME = ".env" LOOPBACK_BACKEND_HOST = "127.0.0.1" LAN_BACKEND_HOST = "0.0.0.0" +VOICE_TRANSCRIPTION_DEVICE_CPU = "cpu" +VOICE_TRANSCRIPTION_DEVICE_CUDA = "cuda" def _parse_port(value: object) -> int | None: @@ -62,6 +66,16 @@ def _normalize_mcp_token(value: object) -> str | None: return raw +def _normalize_voice_transcription_device(value: object) -> str | None: + try: + raw = str(value or "").strip().lower() + except Exception: + return None + if raw in {VOICE_TRANSCRIPTION_DEVICE_CPU, VOICE_TRANSCRIPTION_DEVICE_CUDA}: + return raw + return None + + def generate_mcp_token() -> str: return secrets.token_urlsafe(32) @@ -212,6 +226,41 @@ def read_effective_mcp_token() -> tuple[str | None, str]: return None, "missing" +def read_voice_transcription_device_setting() -> str | None: + try: + data = _read_runtime_settings() + return _normalize_voice_transcription_device(data.get(VOICE_TRANSCRIPTION_DEVICE_KEY)) + except Exception: + return None + + +def write_voice_transcription_device_setting(device: str | None) -> None: + safe_device = _normalize_voice_transcription_device(device) + try: + data = _read_runtime_settings() + if safe_device is None: + data.pop(VOICE_TRANSCRIPTION_DEVICE_KEY, None) + else: + data[VOICE_TRANSCRIPTION_DEVICE_KEY] = safe_device + _write_runtime_settings(data) + except Exception: + return + + +def read_effective_voice_transcription_device(default: str = VOICE_TRANSCRIPTION_DEVICE_CPU) -> tuple[str, str]: + """Return the voice device preference and its source: env | settings | default.""" + + env_device = _normalize_voice_transcription_device(os.environ.get(ENV_VOICE_TRANSCRIPTION_DEVICE_KEY, "")) + if env_device is not None: + return env_device, "env" + + settings_device = read_voice_transcription_device_setting() + if settings_device is not None: + return settings_device, "settings" + + return _normalize_voice_transcription_device(default) or VOICE_TRANSCRIPTION_DEVICE_CPU, "default" + + def ensure_mcp_token() -> tuple[str, str]: token, source = read_effective_mcp_token() if token: diff --git a/src/wechat_decrypt_tool/voice_transcription.py b/src/wechat_decrypt_tool/voice_transcription.py new file mode 100644 index 00000000..1425c29f --- /dev/null +++ b/src/wechat_decrypt_tool/voice_transcription.py @@ -0,0 +1,943 @@ +from __future__ import annotations + +import hashlib +import gc +import importlib.util +import os +import re +import shutil +import sqlite3 +import subprocess +import tempfile +import threading +import time +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Any, Callable, Optional + + +TRANSCRIPT_TEXT_VERSION = 1 +_OPENCC_CONVERTER: Any = None +_OPENCC_LOOKED_UP = False +_CUDA_PROBE_CACHE_TTL_SECONDS = 5.0 +_CUDA_PROBE_CACHE_LOCK = threading.Lock() +_CUDA_PROBE_CACHE: Optional[tuple[float, dict[str, Any]]] = None + +from .runtime_settings import ( + VOICE_TRANSCRIPTION_DEVICE_CPU, + VOICE_TRANSCRIPTION_DEVICE_CUDA, + read_effective_voice_transcription_device, + write_voice_transcription_device_setting, +) + +class VoiceTranscriptionError(RuntimeError): + def __init__(self, code: str, message: str) -> None: + super().__init__(message) + self.code = str(code or "voice_transcription_failed") + self.user_message = str(message or "Voice transcription failed.") + + +@dataclass(frozen=True) +class VoiceTranscriptionConfig: + enabled: bool = True + model: str = "medium" + language: str = "zh" + device: str = "cpu" + compute_type: str = "int8" + device_source: str = "default" + allow_download: bool = False + beam_size: int = 5 + + @classmethod + def from_env(cls) -> "VoiceTranscriptionConfig": + enabled = _env_bool("WECHAT_TOOL_WHISPER_ENABLED", True) + model = str(os.environ.get("WECHAT_TOOL_WHISPER_MODEL") or "medium").strip() + language = str(os.environ.get("WECHAT_TOOL_WHISPER_LANGUAGE") or "zh").strip() or "zh" + device, device_source = read_effective_voice_transcription_device() + compute_type = str(os.environ.get("WECHAT_TOOL_WHISPER_COMPUTE_TYPE") or "").strip() + if not compute_type: + compute_type = "float16" if device == VOICE_TRANSCRIPTION_DEVICE_CUDA else "int8" + allow_download = _env_bool("WECHAT_TOOL_WHISPER_ALLOW_DOWNLOAD", False) + try: + beam_size = max(1, min(10, int(os.environ.get("WECHAT_TOOL_WHISPER_BEAM_SIZE") or 5))) + except Exception: + beam_size = 5 + return cls( + enabled=enabled, + model=model, + language=language, + device=device, + compute_type=compute_type, + device_source=device_source, + allow_download=allow_download, + beam_size=beam_size, + ) + + def cpu_fallback(self) -> "VoiceTranscriptionConfig": + return replace( + self, + device=VOICE_TRANSCRIPTION_DEVICE_CPU, + compute_type="int8", + device_source="fallback", + ) + + +def _env_bool(name: str, default: bool) -> bool: + value = os.environ.get(name) + if value is None: + return bool(default) + return str(value).strip().lower() in {"1", "true", "yes", "on"} + + +def _find_nvidia_smi() -> Optional[str]: + executable = shutil.which("nvidia-smi") + if executable: + return executable + if os.name == "nt": + candidate = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "nvidia-smi.exe" + if candidate.is_file(): + return str(candidate) + candidate = Path(r"C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe") + if candidate.is_file(): + return str(candidate) + return None + + +def _read_nvidia_smi_devices() -> list[dict[str, str]]: + executable = _find_nvidia_smi() + if not executable: + return [] + try: + completed = subprocess.run( + [executable, "--query-gpu=name,driver_version,memory.total", "--format=csv,noheader"], + capture_output=True, + text=True, + timeout=2.0, + check=False, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + except Exception: + return [] + if completed.returncode != 0: + return [] + + devices: list[dict[str, str]] = [] + for line in str(completed.stdout or "").splitlines(): + fields = [part.strip() for part in line.split(",")] + if not fields or not fields[0]: + continue + devices.append( + { + "name": fields[0], + "driverVersion": fields[1] if len(fields) > 1 else "", + "memoryTotal": fields[2] if len(fields) > 2 else "", + } + ) + return devices + + +def _probe_cuda_uncached() -> dict[str, Any]: + result: dict[str, Any] = { + "available": False, + "deviceCount": 0, + "devices": _read_nvidia_smi_devices(), + "reason": "", + } + try: + import ctranslate2 + except Exception: + result["reason"] = "未安装 CTranslate2 CUDA 运行依赖。" + return result + + try: + count = max(0, int(ctranslate2.get_cuda_device_count())) + except Exception: + result["reason"] = "未检测到可用的 NVIDIA CUDA 设备或驱动。" + return result + + result["deviceCount"] = count + if count <= 0: + result["reason"] = "未检测到可用的 NVIDIA CUDA 设备或驱动。" + return result + + result["available"] = True + return result + + +def _copy_cuda_report(report: dict[str, Any]) -> dict[str, Any]: + copied = dict(report) + copied["devices"] = [dict(item) for item in (report.get("devices") or [])] + return copied + + +def invalidate_cuda_probe_cache() -> None: + global _CUDA_PROBE_CACHE + with _CUDA_PROBE_CACHE_LOCK: + _CUDA_PROBE_CACHE = None + + +def probe_cuda() -> dict[str, Any]: + """Return a short-lived CUDA capability report without loading a Whisper model.""" + + global _CUDA_PROBE_CACHE + now = time.monotonic() + with _CUDA_PROBE_CACHE_LOCK: + cached = _CUDA_PROBE_CACHE + if cached is not None and now < cached[0]: + return _copy_cuda_report(cached[1]) + report = _probe_cuda_uncached() + _CUDA_PROBE_CACHE = (time.monotonic() + _CUDA_PROBE_CACHE_TTL_SECONDS, report) + return _copy_cuda_report(report) + + +def _public_model_name(value: str) -> str: + raw = str(value or "").strip() + if not raw: + return "" + if "/" in raw or "\\" in raw: + return Path(raw.rstrip("/\\")).name or "local-model" + return raw + + +def _model_directory_is_ready(path: Path) -> bool: + try: + if not path.is_dir(): + return False + required = ("model.bin", "config.json", "tokenizer.json") + if not all((path / name).is_file() for name in required): + return False + return any(candidate.is_file() for candidate in path.glob("vocabulary.*")) + except OSError: + return False + + +def _looks_like_local_model_path(value: str) -> bool: + raw = str(value or "").strip() + if not raw: + return False + expanded = Path(os.path.expandvars(os.path.expanduser(raw))) + return bool( + expanded.exists() + or expanded.is_absolute() + or raw.startswith((".", "~")) + or "\\" in raw + or re.match(r"^[A-Za-z]:[/\\]", raw) + ) + + +def inspect_model_readiness(model: str) -> dict[str, Any]: + """Inspect a local directory or Hugging Face cache without loading or downloading a model.""" + + raw = str(model or "").strip() + if not raw: + return { + "ready": False, + "downloadable": False, + "source": "unavailable", + "reason": "未配置 Whisper 模型。", + } + + if _looks_like_local_model_path(raw): + model_dir = Path(os.path.expandvars(os.path.expanduser(raw))) + if _model_directory_is_ready(model_dir): + return {"ready": True, "downloadable": False, "source": "local-directory", "reason": ""} + return { + "ready": False, + "downloadable": False, + "source": "local-directory", + "reason": "配置的本地 Whisper 模型目录不存在或文件不完整。", + } + + try: + from faster_whisper.utils import download_model + except Exception: + return { + "ready": False, + "downloadable": True, + "source": "huggingface-cache", + "reason": "未安装 faster-whisper,无法检查模型缓存。", + } + + try: + cached_dir = Path(download_model(raw, local_files_only=True)) + except ValueError: + return { + "ready": False, + "downloadable": False, + "source": "unavailable", + "reason": "Whisper 模型名称无效。", + } + except Exception: + return { + "ready": False, + "downloadable": True, + "source": "huggingface-cache", + "reason": "Whisper 模型尚未下载到本机缓存。", + } + + if _model_directory_is_ready(cached_dir): + return {"ready": True, "downloadable": True, "source": "huggingface-cache", "reason": ""} + return { + "ready": False, + "downloadable": True, + "source": "huggingface-cache", + "reason": "本机 Whisper 模型缓存文件不完整。", + } + + +def _join_transcript_segments(values: Any) -> str: + output = "" + for value in values: + part = str(value or "").strip() + if not part: + continue + needs_space = bool( + output + and output[-1].isascii() + and output[-1].isalnum() + and part[0].isascii() + and part[0].isalnum() + ) + output += (" " if needs_space else "") + part + return output.strip() + + +def normalize_transcript_text(value: Any) -> str: + """使用 OpenCC 将转写文本统一为简体中文。""" + + global _OPENCC_CONVERTER, _OPENCC_LOOKED_UP + text = str(value or "").strip() + if not text: + return "" + if not _OPENCC_LOOKED_UP: + _OPENCC_LOOKED_UP = True + try: + from opencc import OpenCC + + _OPENCC_CONVERTER = OpenCC("t2s") + except Exception: + _OPENCC_CONVERTER = None + if _OPENCC_CONVERTER is None: + raise VoiceTranscriptionError( + "dependency_missing", + "未安装 OpenCC 简繁转换依赖,无法保证转写结果为简体中文。", + ) + try: + return str(_OPENCC_CONVERTER.convert(text) or "").strip() + except Exception as exc: + raise VoiceTranscriptionError("text_normalization_failed", "转写文字转换为简体中文失败。") from exc + + +def _is_cuda_runtime_error(exc: BaseException) -> bool: + """只识别 CUDA/cuDNN 运行时故障,避免把普通音频错误误判为 GPU 故障。""" + + seen: set[int] = set() + current: Optional[BaseException] = exc + markers = ( + "cuda", + "cudnn", + "cublas", + "cufft", + "curand", + "libcudart", + "no kernel image", + "device-side assert", + ) + while current is not None and id(current) not in seen: + seen.add(id(current)) + message = str(current).lower() + if any(marker in message for marker in markers): + return True + current = current.__cause__ or current.__context__ + return False + + +def _coerce_blob(value: Any) -> bytes: + if value is None: + return b"" + if isinstance(value, bytes): + return value + if isinstance(value, bytearray): + return bytes(value) + if isinstance(value, memoryview): + return value.tobytes() + text = str(value or "").strip() + if not text: + return b"" + compact = re.sub(r"\s+", "", text) + if compact.lower().startswith("0x"): + compact = compact[2:] + if len(compact) >= 2 and len(compact) % 2 == 0 and re.fullmatch(r"[0-9a-fA-F]+", compact): + try: + return bytes.fromhex(compact) + except Exception: + return b"" + return text.encode("utf-8", "replace") + + +def _convert_silk_to_browser_audio(data: bytes, *, preferred_format: str) -> tuple[bytes, str, str]: + from .media_helpers import _convert_silk_to_browser_audio as convert + + return convert(data, preferred_format=preferred_format) + + +def load_voice_data(account_dir: Path, server_id: int) -> bytes: + account_path = Path(account_dir) + sid = int(server_id or 0) + if sid <= 0: + return b"" + + media_db_path = account_path / "media_0.db" + if media_db_path.exists(): + conn: Optional[sqlite3.Connection] = None + try: + conn = sqlite3.connect(str(media_db_path)) + row = conn.execute( + "SELECT voice_data FROM VoiceInfo WHERE svr_id = ? ORDER BY create_time DESC LIMIT 1", + (sid,), + ).fetchone() + if row: + data = _coerce_blob(row[0]) + if data: + return data + except Exception: + pass + finally: + if conn is not None: + conn.close() + + try: + from .wcdb_realtime import WCDB_REALTIME, exec_query as _wcdb_exec_query + + realtime = WCDB_REALTIME.ensure_connected(account_path) + media_dir = Path(realtime.db_storage_dir) / "message" + sql = f"SELECT voice_data FROM VoiceInfo WHERE svr_id = {sid} ORDER BY create_time DESC LIMIT 1" + for realtime_db_path in sorted(media_dir.glob("media_*.db")): + if not realtime_db_path.is_file(): + continue + try: + with realtime.lock: + rows = _wcdb_exec_query( + realtime.handle, + kind="message", + path=str(realtime_db_path), + sql=sql, + ) + except Exception: + rows = [] + if rows: + data = _coerce_blob(rows[0].get("voice_data")) + if data: + return data + except Exception: + pass + return b"" + + +class VoiceTranscriptionService: + def __init__( + self, + config: Optional[VoiceTranscriptionConfig] = None, + *, + model_loader: Optional[Callable[[VoiceTranscriptionConfig], Any]] = None, + ) -> None: + self.config = config or VoiceTranscriptionConfig.from_env() + self._model_loader = model_loader or self._load_faster_whisper_model + self._model: Any = None + self._active_device = "" + self._active_compute_type = "" + self._fallback_reason = "" + self._model_lock = threading.Lock() + self._inference_lock = threading.Lock() + self._cache_lock = threading.Lock() + self._model_readiness_lock = threading.Lock() + self._model_readiness_cache: Optional[tuple[float, dict[str, Any]]] = None + + def _model_readiness(self) -> dict[str, Any]: + now = time.monotonic() + with self._model_readiness_lock: + cached = self._model_readiness_cache + if cached is not None and now < cached[0]: + return dict(cached[1]) + result = inspect_model_readiness(self.config.model) + self._model_readiness_cache = (time.monotonic() + 5.0, result) + return dict(result) + + def status(self) -> dict[str, Any]: + try: + dependency_available = importlib.util.find_spec("faster_whisper") is not None + except Exception: + dependency_available = False + try: + text_normalizer_available = importlib.util.find_spec("opencc") is not None + except Exception: + text_normalizer_available = False + model_readiness = self._model_readiness() + model_ready = bool(model_readiness.get("ready")) + model_downloadable = bool(model_readiness.get("downloadable")) + can_prepare_model = bool(self.config.allow_download and model_downloadable) + cuda = probe_cuda() + fallback_reason = self._fallback_reason + if not fallback_reason and self.config.device == VOICE_TRANSCRIPTION_DEVICE_CUDA and not cuda["available"]: + fallback_reason = f"{cuda['reason']} 首次识别会自动回退到 CPU。" + available = bool( + self.config.enabled + and dependency_available + and text_normalizer_available + and self.config.model + and (model_ready or can_prepare_model) + ) + reason = "" + if not self.config.enabled: + reason = "语音转文字功能未启用。" + elif not dependency_available: + reason = "未安装 faster-whisper,请安装语音转文字可选依赖。" + elif not text_normalizer_available: + reason = "未安装 OpenCC,无法保证输出为简体中文。" + elif not str(self.config.model or "").strip(): + reason = "未配置 Whisper 模型。" + elif not model_ready: + reason = str(model_readiness.get("reason") or "Whisper 模型尚未准备好。") + if can_prepare_model: + reason = f"{reason} 首次转写时会联网下载模型。" + elif model_downloadable and not self.config.allow_download: + reason = f"{reason} 当前已禁止自动下载。" + return { + "enabled": bool(self.config.enabled), + "available": available, + "dependencyAvailable": dependency_available, + "textNormalizerAvailable": text_normalizer_available, + "modelReady": model_ready, + "modelSource": str(model_readiness.get("source") or "unavailable"), + "modelDownloadRequired": bool(not model_ready and can_prepare_model), + "model": _public_model_name(self.config.model), + "language": self.config.language, + "device": self.config.device, + "computeType": self.config.compute_type, + "requestedDevice": self.config.device, + "requestedComputeType": self.config.compute_type, + "deviceSource": self.config.device_source, + "activeDevice": self._active_device or None, + "activeComputeType": self._active_compute_type or None, + "modelLoaded": self._model is not None, + "cuda": cuda, + "requestedDeviceAvailable": bool( + self.config.device != VOICE_TRANSCRIPTION_DEVICE_CUDA or cuda["available"] + ), + "usingFallback": bool(self._fallback_reason), + "fallbackReason": fallback_reason, + "allowDownload": bool(self.config.allow_download), + "reason": reason, + } + + def ensure_available(self) -> dict[str, Any]: + status = self.status() + if status["available"]: + return status + if not status["enabled"]: + code = "disabled" + elif not status["dependencyAvailable"] or not status["textNormalizerAvailable"]: + code = "dependency_missing" + elif not str(self.config.model or "").strip(): + code = "model_not_configured" + else: + code = "model_not_ready" + raise VoiceTranscriptionError(code, str(status.get("reason") or "本地 Whisper 当前不可用。")) + + def transcribe_voice( + self, + *, + account_dir: Path, + server_id: int, + force: bool = False, + voice_data: Optional[bytes] = None, + ) -> dict[str, Any]: + if not self.config.enabled: + raise VoiceTranscriptionError("disabled", "语音转文字功能未启用。") + if not str(self.config.model or "").strip(): + raise VoiceTranscriptionError("model_not_configured", "未配置 Whisper 模型。") + + sid = int(server_id or 0) + if sid <= 0: + raise VoiceTranscriptionError("invalid_server_id", "语音消息 ID 无效。") + + data = bytes(voice_data) if voice_data is not None else load_voice_data(Path(account_dir), sid) + if not data: + raise VoiceTranscriptionError("voice_not_found", "未找到语音数据。") + + source_hash = hashlib.sha256(data).hexdigest() + if not force: + cached = self._read_cache(Path(account_dir), sid, source_hash) + if cached is not None: + cached["cached"] = True + return cached + + payload, ext, _media_type = _convert_silk_to_browser_audio(data, preferred_format="wav") + if not payload or ext == "silk": + raise VoiceTranscriptionError("voice_decode_failed", "语音解码失败,无法交给 Whisper 识别。") + + temp_path: Optional[Path] = None + try: + suffix = ".wav" if ext == "wav" else f".{ext}" + with tempfile.NamedTemporaryFile(prefix="wechat_voice_", suffix=suffix, delete=False) as temp_file: + temp_file.write(payload) + temp_path = Path(temp_file.name) + + with self._inference_lock: + if not force: + cached = self._read_cache(Path(account_dir), sid, source_hash) + if cached is not None: + cached["cached"] = True + return cached + model = self._get_model() + try: + text, info = self._transcribe_once(model, temp_path) + except VoiceTranscriptionError: + raise + except Exception as exc: + if self._active_device != VOICE_TRANSCRIPTION_DEVICE_CUDA or not _is_cuda_runtime_error(exc): + raise VoiceTranscriptionError( + "transcription_failed", + f"语音识别失败:{type(exc).__name__}", + ) from exc + + model = None + self._release_loaded_model() + cpu_model = self._load_cpu_fallback("CUDA 推理初始化失败,已自动回退到 CPU。") + try: + text, info = self._transcribe_once(cpu_model, temp_path) + except VoiceTranscriptionError: + raise + except Exception as retry_exc: + raise VoiceTranscriptionError( + "transcription_failed", + f"CPU 回退识别失败:{type(retry_exc).__name__}", + ) from retry_exc + + result = { + "status": "success", + "serverId": sid, + "text": text, + "language": str(getattr(info, "language", "") or self.config.language), + "duration": float(getattr(info, "duration", 0.0) or 0.0), + "model": _public_model_name(self.config.model), + "device": self._active_device or self.config.device, + "computeType": self._active_compute_type or self.config.compute_type, + "cached": False, + } + try: + self._write_cache(Path(account_dir), sid, source_hash, result) + except Exception: + pass + return result + finally: + if temp_path is not None: + try: + temp_path.unlink(missing_ok=True) + except Exception: + pass + + def _transcribe_once(self, model: Any, path: Path) -> tuple[str, Any]: + segments, info = model.transcribe( + str(path), + language=self.config.language, + beam_size=self.config.beam_size, + vad_filter=True, + condition_on_previous_text=False, + ) + text = _join_transcript_segments(getattr(segment, "text", "") for segment in segments) + return normalize_transcript_text(text), info + + def _release_loaded_model(self) -> None: + self._model = None + self._active_device = "" + self._active_compute_type = "" + gc.collect() + + def _get_model(self) -> Any: + if self._model is not None: + return self._model + with self._model_lock: + if self._model is not None: + return self._model + + if self.config.device == VOICE_TRANSCRIPTION_DEVICE_CUDA: + cuda = probe_cuda() + if not cuda["available"]: + return self._load_cpu_fallback(cuda["reason"]) + try: + self._model = self._model_loader(self.config) + self._active_device = VOICE_TRANSCRIPTION_DEVICE_CUDA + self._active_compute_type = self.config.compute_type + self._fallback_reason = "" + return self._model + except Exception: + return self._load_cpu_fallback("NVIDIA CUDA 初始化失败,已自动回退到 CPU。") + + return self._load_model(self.config) + + def _load_cpu_fallback(self, reason: str) -> Any: + self._fallback_reason = str(reason or "NVIDIA CUDA 不可用,已自动回退到 CPU。") + return self._load_model(self.config.cpu_fallback()) + + def _load_model(self, config: VoiceTranscriptionConfig) -> Any: + try: + self._model = self._model_loader(config) + self._active_device = config.device + self._active_compute_type = config.compute_type + return self._model + except VoiceTranscriptionError: + raise + except ImportError as exc: + raise VoiceTranscriptionError( + "dependency_missing", + "未安装 faster-whisper,请安装语音转文字可选依赖。", + ) from exc + except Exception as exc: + raise VoiceTranscriptionError( + "model_load_failed", + f"Whisper 模型加载失败:{type(exc).__name__}", + ) from exc + + @staticmethod + def _load_faster_whisper_model(config: VoiceTranscriptionConfig) -> Any: + try: + from faster_whisper import WhisperModel + except ImportError as exc: + raise VoiceTranscriptionError( + "dependency_missing", + "未安装 faster-whisper,请安装语音转文字可选依赖。", + ) from exc + return WhisperModel( + config.model, + device=config.device, + compute_type=config.compute_type, + local_files_only=not config.allow_download, + ) + + def _cache_path(self, account_dir: Path) -> Path: + return Path(account_dir) / "_cache" / "voice_transcripts.sqlite3" + + @staticmethod + def _ensure_cache_schema(conn: sqlite3.Connection) -> None: + conn.execute( + "CREATE TABLE IF NOT EXISTS transcript (" + "server_id INTEGER NOT NULL, source_hash TEXT NOT NULL, model TEXT NOT NULL, " + "language TEXT NOT NULL, text TEXT NOT NULL, detected_language TEXT NOT NULL, " + "duration REAL NOT NULL, updated_at REAL NOT NULL, text_version INTEGER NOT NULL DEFAULT 0, " + "PRIMARY KEY (server_id, source_hash, model, language))" + ) + columns = {str(row[1]) for row in conn.execute("PRAGMA table_info(transcript)")} + if "text_version" not in columns: + conn.execute("ALTER TABLE transcript ADD COLUMN text_version INTEGER NOT NULL DEFAULT 0") + + @staticmethod + def _normalize_cached_text(raw_text: Any, raw_version: Any) -> tuple[str, bool]: + text = str(raw_text or "") + normalized = normalize_transcript_text(text) + try: + version = int(raw_version or 0) + except Exception: + version = 0 + return normalized, normalized != text or version < TRANSCRIPT_TEXT_VERSION + + def _read_cache(self, account_dir: Path, server_id: int, source_hash: str) -> Optional[dict[str, Any]]: + path = self._cache_path(account_dir) + if not path.exists(): + return None + with self._cache_lock: + conn: Optional[sqlite3.Connection] = None + try: + conn = sqlite3.connect(str(path)) + self._ensure_cache_schema(conn) + row = conn.execute( + "SELECT text, detected_language, duration, text_version FROM transcript " + "WHERE server_id = ? AND source_hash = ? AND model = ? AND language = ? LIMIT 1", + (int(server_id), source_hash, self.config.model, self.config.language), + ).fetchone() + if row: + normalized_text, needs_update = self._normalize_cached_text(row[0], row[3]) + if needs_update: + conn.execute( + "UPDATE transcript SET text = ?, text_version = ?, updated_at = ? " + "WHERE server_id = ? AND source_hash = ? AND model = ? AND language = ?", + ( + normalized_text, + TRANSCRIPT_TEXT_VERSION, + time.time(), + int(server_id), + source_hash, + self.config.model, + self.config.language, + ), + ) + conn.commit() + row = (normalized_text, row[1], row[2], TRANSCRIPT_TEXT_VERSION) + except VoiceTranscriptionError: + raise + except Exception: + row = None + finally: + if conn is not None: + conn.close() + if not row: + return None + return { + "status": "success", + "serverId": int(server_id), + "text": str(row[0] or ""), + "language": str(row[1] or self.config.language), + "duration": float(row[2] or 0.0), + "model": _public_model_name(self.config.model), + "cached": True, + } + + def lookup_cached_transcripts( + self, + account_dir: Path, + server_ids: list[int], + ) -> dict[int, dict[str, Any]]: + """批量读取已缓存的转写结果,并按当前文本版本原位升级。 + + 不加载模型、不触发识别;旧缓存可能写回简体文本和版本号。 + 缓存主键包含 source_hash(音频内容哈希),批量场景下无法预先计算, + 而同一 svr_id 的语音内容唯一,因此按 (server_id, model, language) 匹配最新记录。 + """ + result: dict[int, dict[str, Any]] = {} + ids: list[int] = [] + seen: set[int] = set() + for raw in server_ids or []: + try: + sid = int(raw) + except Exception: + continue + if sid <= 0 or sid in seen: + continue + seen.add(sid) + ids.append(sid) + if not ids: + return result + + path = self._cache_path(Path(account_dir)) + if not path.exists(): + return result + + placeholders = ",".join("?" for _ in ids) + with self._cache_lock: + conn: Optional[sqlite3.Connection] = None + try: + conn = sqlite3.connect(str(path)) + self._ensure_cache_schema(conn) + rows = conn.execute( + "SELECT server_id, source_hash, text, detected_language, duration, text_version FROM transcript " + f"WHERE model = ? AND language = ? AND server_id IN ({placeholders}) " + "ORDER BY updated_at DESC", + (self.config.model, self.config.language, *ids), + ).fetchall() + normalized_rows = [] + for row in rows or []: + normalized_text, needs_update = self._normalize_cached_text(row[2], row[5]) + if needs_update: + conn.execute( + "UPDATE transcript SET text = ?, text_version = ?, updated_at = ? " + "WHERE server_id = ? AND source_hash = ? AND model = ? AND language = ?", + ( + normalized_text, + TRANSCRIPT_TEXT_VERSION, + time.time(), + int(row[0]), + str(row[1]), + self.config.model, + self.config.language, + ), + ) + normalized_rows.append((row[0], normalized_text, row[3], row[4])) + conn.commit() + rows = normalized_rows + except VoiceTranscriptionError: + raise + except Exception: + rows = [] + finally: + if conn is not None: + conn.close() + + for row in rows or []: + sid = int(row[0]) + if sid in result: + continue + result[sid] = { + "status": "success", + "serverId": sid, + "text": str(row[1] or ""), + "language": str(row[2] or self.config.language), + "duration": float(row[3] or 0.0), + "model": _public_model_name(self.config.model), + "cached": True, + } + return result + + def _write_cache(self, account_dir: Path, server_id: int, source_hash: str, result: dict[str, Any]) -> None: + path = self._cache_path(account_dir) + path.parent.mkdir(parents=True, exist_ok=True) + normalized_text = normalize_transcript_text(result.get("text")) + result["text"] = normalized_text + with self._cache_lock: + conn = sqlite3.connect(str(path)) + try: + self._ensure_cache_schema(conn) + conn.execute( + "INSERT OR REPLACE INTO transcript " + "(server_id, source_hash, model, language, text, detected_language, duration, updated_at, text_version) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + ( + int(server_id), + source_hash, + self.config.model, + self.config.language, + normalized_text, + str(result.get("language") or self.config.language), + float(result.get("duration") or 0.0), + time.time(), + TRANSCRIPT_TEXT_VERSION, + ), + ) + conn.commit() + finally: + conn.close() + + +_VOICE_TRANSCRIPTION_SERVICE: Optional[VoiceTranscriptionService] = None +_VOICE_TRANSCRIPTION_SERVICE_LOCK = threading.Lock() + + +def get_voice_transcription_service() -> VoiceTranscriptionService: + global _VOICE_TRANSCRIPTION_SERVICE + if _VOICE_TRANSCRIPTION_SERVICE is not None: + return _VOICE_TRANSCRIPTION_SERVICE + with _VOICE_TRANSCRIPTION_SERVICE_LOCK: + if _VOICE_TRANSCRIPTION_SERVICE is None: + _VOICE_TRANSCRIPTION_SERVICE = VoiceTranscriptionService() + return _VOICE_TRANSCRIPTION_SERVICE + + +def set_voice_transcription_device(device: str) -> dict[str, Any]: + """Persist a user preference and make it effective for subsequent requests.""" + + normalized = str(device or "").strip().lower() + if normalized not in {VOICE_TRANSCRIPTION_DEVICE_CPU, VOICE_TRANSCRIPTION_DEVICE_CUDA}: + raise VoiceTranscriptionError("invalid_device", "推理设备只支持 CPU 或 NVIDIA GPU。") + + _configured, source = read_effective_voice_transcription_device() + if source == "env": + raise VoiceTranscriptionError( + "device_locked", + "当前推理设备由 WECHAT_TOOL_WHISPER_DEVICE 环境变量锁定,无法在界面中修改。", + ) + + write_voice_transcription_device_setting(normalized) + invalidate_cuda_probe_cache() + global _VOICE_TRANSCRIPTION_SERVICE + with _VOICE_TRANSCRIPTION_SERVICE_LOCK: + _VOICE_TRANSCRIPTION_SERVICE = VoiceTranscriptionService() + return _VOICE_TRANSCRIPTION_SERVICE.status() diff --git a/src/wechat_decrypt_tool/wcdb_realtime.py b/src/wechat_decrypt_tool/wcdb_realtime.py index d5d490ec..fbf07ea0 100644 --- a/src/wechat_decrypt_tool/wcdb_realtime.py +++ b/src/wechat_decrypt_tool/wcdb_realtime.py @@ -224,7 +224,7 @@ def _resolve_wcdb_api_dll_path() -> Path: _WCDB_API_DLL_SELECTED = _DEFAULT_WCDB_API_DLL return _WCDB_API_DLL_SELECTED -_lib_lock = threading.Lock() +_lib_lock = threading.RLock() _lib: Optional[ctypes.CDLL] = None _initialized = False _inprocess_runtime_poisoned_reason = "" diff --git a/src/wechat_decrypt_tool/wrapped/cards/card_00_global_overview.py b/src/wechat_decrypt_tool/wrapped/cards/card_00_global_overview.py index 5fa9ebd4..8ff72a23 100644 --- a/src/wechat_decrypt_tool/wrapped/cards/card_00_global_overview.py +++ b/src/wechat_decrypt_tool/wrapped/cards/card_00_global_overview.py @@ -1,12 +1,13 @@ from __future__ import annotations import hashlib +import json import re import sqlite3 import time from collections import Counter from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from typing import Any, Optional @@ -404,6 +405,181 @@ def _weekday_name_zh(weekday_index: int) -> str: return "" +def _readable_index_text(token_text: Any, payload_json: Any, *, max_len: int = 60) -> str: + """Recover readable message text from a search-index row. + + `message_fts.text` stores char-tokenized text (lowercased, whitespace removed, + chars joined by single spaces). Prefer the original content preserved in + `payload_json` (newer indexes); fall back to de-tokenizing. + """ + + content = "" + try: + obj = json.loads(_decode_sqlite_text(payload_json)) + if isinstance(obj, dict): + content = str(obj.get("content") or "").strip() + except Exception: + content = "" + + if content: + s = re.sub(r"\s+", " ", content).strip() + else: + s = "".join(ch for ch in _decode_sqlite_text(token_text) if not ch.isspace()) + + if not s: + return "" + if len(s) > max_len: + s = s[:max_len] + return s + + +def _compute_peak_day_details( + *, + account_dir: Path, + year: int, + doy: int, + sender_username: str | None, +) -> dict[str, Any]: + """Best-effort details for the peak day (top conversation + my first/last text). + + Only implemented on top of the unified search index; when the index is + missing, all fields stay None and the frontend hides them. + """ + + out: dict[str, Any] = { + "topContact": None, + "firstTime": None, + "firstText": None, + "lastTime": None, + "lastText": None, + } + + try: + day_dt = datetime(int(year), 1, 1) + timedelta(days=int(doy)) + next_dt = day_dt + timedelta(days=1) + day_start = int(datetime(day_dt.year, day_dt.month, day_dt.day).timestamp()) + day_end = int(datetime(next_dt.year, next_dt.month, next_dt.day).timestamp()) + except Exception: + return out + + index_path = get_chat_search_index_db_path(account_dir) + if not index_path.exists(): + return out + + sender = str(sender_username or "").strip() + + conn = sqlite3.connect(str(index_path)) + try: + has_fts = ( + conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='message_fts' LIMIT 1" + ).fetchone() + is not None + ) + if not has_fts: + return out + + # Convert millisecond timestamps defensively (some datasets store ms). + ts_expr = ( + "CASE " + "WHEN CAST(create_time AS INTEGER) > 1000000000000 " + "THEN CAST(CAST(create_time AS INTEGER)/1000 AS INTEGER) " + "ELSE CAST(create_time AS INTEGER) " + "END" + ) + where = f"{ts_expr} >= ? AND {ts_expr} < ? AND db_stem NOT LIKE 'biz_message%'" + params: tuple[Any, ...] = (day_start, day_end) + + # 当日消息最多的会话(含群,双向计数)。 + try: + rows_u = conn.execute( + f"SELECT username, COUNT(1) AS cnt FROM message_fts WHERE {where} " + "GROUP BY username ORDER BY cnt DESC, username ASC LIMIT 50", + params, + ).fetchall() + except Exception: + rows_u = [] + + top_username = "" + top_count = 0 + for rr in rows_u: + if not rr or not rr[0]: + continue + u = str(rr[0] or "").strip() + try: + cnt = int(rr[1] or 0) + except Exception: + cnt = 0 + if not u or cnt <= 0: + continue + if not _should_keep_session(u, include_official=False): + continue + top_username, top_count = u, cnt + break + + if top_username: + contact_rows = _load_contact_rows(account_dir / "contact.db", [top_username]) + row = contact_rows.get(top_username) + display = _pick_display_name(row, top_username) + out["topContact"] = { + "username": top_username, + "displayName": display, + "maskedName": _mask_name(display), + "avatarUrl": _build_avatar_url(str(account_dir.name or ""), top_username), + "messages": int(top_count), + "isGroup": top_username.endswith("@chatroom"), + } + + # 当日本人发出的首/末条文本消息。 + if sender: + where_text = ( + where + + " AND sender_username = ? AND render_type = 'text' " + + "AND \"text\" IS NOT NULL AND TRIM(CAST(\"text\" AS TEXT)) != ''" + ) + params_text = (day_start, day_end, sender) + + def fetch_edge(order: str) -> Optional[tuple[int, str]]: + # payload_json 仅新版索引存在;缺列时退化为只取 token text。 + for cols in ('"text", payload_json', '"text"'): + sql = ( + f"SELECT {ts_expr} AS ts, {cols} FROM message_fts " + f"WHERE {where_text} " + f"ORDER BY ts {order}, CAST(sort_seq AS INTEGER) {order} LIMIT 1" + ) + try: + r = conn.execute(sql, params_text).fetchone() + except Exception: + continue + if not r: + return None + try: + ts = int(r[0] or 0) + except Exception: + ts = 0 + if ts <= 0: + return None + text = _readable_index_text(r[1], r[2] if len(r) > 2 else None) + return ts, text + return None + + first_edge = fetch_edge("ASC") + last_edge = fetch_edge("DESC") + if first_edge: + out["firstTime"] = datetime.fromtimestamp(first_edge[0]).strftime("%H:%M") + out["firstText"] = first_edge[1] or None + if last_edge: + out["lastTime"] = datetime.fromtimestamp(last_edge[0]).strftime("%H:%M") + out["lastText"] = last_edge[1] or None + finally: + try: + conn.close() + except Exception: + pass + + return out + + def _kind_label_zh(kind: str) -> str: return { "text": "文字", @@ -989,14 +1165,48 @@ def build_card_00_global_overview( } daily_counts = compute_annual_daily_counts(account_dir=account_dir, year=year, sender_username=sender) + + # 年度峰值日:全年逐日计数的 argmax(并列时取更早的一天;全 0 时无峰值日)。 + peak_day: Optional[dict[str, Any]] = None + peak_highlights: list[dict[str, Any]] = [] + if daily_counts: + peak_doy = max(range(len(daily_counts)), key=lambda i: (daily_counts[i], -i)) + peak_count = int(daily_counts[peak_doy]) + if peak_count > 0: + peak_dt = datetime(int(year), 1, 1) + timedelta(days=int(peak_doy)) + peak_multiple: Optional[float] = None + if messages_per_day > 0: + peak_multiple = round(float(peak_count) / float(messages_per_day), 1) + details = _compute_peak_day_details( + account_dir=account_dir, + year=year, + doy=peak_doy, + sender_username=sender, + ) + peak_day = { + "date": peak_dt.strftime("%Y-%m-%d"), + "weekdayName": _weekday_name_zh(peak_dt.weekday()), + "count": peak_count, + "multiple": peak_multiple, + **details, + } + # Shape must match AnnualCalendarHeatmap.vue highlight parsing: {key, doy, label, valueLabel}. + peak_highlights = [ + { + "key": "sent_messages_max", + "doy": int(peak_doy), + "label": "全年发消息最多", + "valueLabel": f"{peak_count:,} 条", + } + ] + annual_heatmap = { "year": int(year), "startDate": f"{int(year)}-01-01", "endDate": f"{int(year)}-12-31", "days": int(len(daily_counts)), "dailyCounts": daily_counts, - # Product decision: keep the calendar heatmap lightweight (no extra "best day" markers). - "highlights": [], + "highlights": peak_highlights, } lines: list[str] = [] @@ -1055,6 +1265,7 @@ def build_card_00_global_overview( "topContact": top_contact_obj, "topGroup": top_group_obj, "topKind": top_kind, + "peakDay": peak_day, "annualHeatmap": annual_heatmap, "topPhrase": {"phrase": stats.top_phrase[0], "count": int(stats.top_phrase[1])} if stats.top_phrase else None, "topEmoji": {"emoji": stats.top_emoji[0], "count": int(stats.top_emoji[1])} if stats.top_emoji else None, diff --git a/src/wechat_decrypt_tool/wrapped/cards/card_01_cyber_schedule.py b/src/wechat_decrypt_tool/wrapped/cards/card_01_cyber_schedule.py index 4a665809..70fbbcf4 100644 --- a/src/wechat_decrypt_tool/wrapped/cards/card_01_cyber_schedule.py +++ b/src/wechat_decrypt_tool/wrapped/cards/card_01_cyber_schedule.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import json import re import sqlite3 import time @@ -18,6 +19,7 @@ _pick_display_name, _quote_ident, _row_to_search_hit, + _should_keep_session, ) from ...logging_config import get_logger @@ -968,6 +970,430 @@ def compute_weekday_hour_heatmap(*, account_dir: Path, year: int, sender_usernam ) +def _empty_night_companion() -> dict[str, Any]: + return { + "nightMessagesTotal": 0, + "myNightMessages": 0, + "partner": None, + "latestMoment": None, + } + + +def _readable_night_index_text(token_text: Any, payload_json: Any) -> str: + """message_fts.text 存的是逐字符分词文本(小写、单空格连接)。 + + 优先取 payload_json 里保留的原文(新版索引),缺失时去空格拼接还原。 + 与 card_00 的 _readable_index_text 同口径,保持卡片文件自包含。 + """ + + content = "" + try: + obj = json.loads(_decode_sqlite_text(payload_json)) + if isinstance(obj, dict): + content = str(obj.get("content") or "").strip() + except Exception: + content = "" + + if content: + return content + return "".join(ch for ch in _decode_sqlite_text(token_text) if not ch.isspace()) + + +def _night_moment_payload(*, ts: int, direction: str, content: str) -> dict[str, Any]: + dt = datetime.fromtimestamp(int(ts)) + text = re.sub(r"\s+", " ", str(content or "")).strip() + if len(text) > 60: + text = text[:57] + "..." + return { + "timestamp": int(ts), + "date": dt.strftime("%Y-%m-%d"), + "time": dt.strftime("%H:%M"), + "direction": "sent" if direction == "sent" else "received", + "content": text, + } + + +def _night_partner_payload( + *, + account_dir: Path, + username: str, + night_messages: int, + total: int, +) -> dict[str, Any]: + contact_rows = _load_contact_rows(account_dir / "contact.db", [username]) + display = _pick_display_name(contact_rows.get(username), username) + avatar = _build_avatar_url(str(account_dir.name or ""), username) if username else "" + share = round(night_messages * 100.0 / total, 1) if total > 0 else 0.0 + return { + "username": username, + "displayName": display, + "maskedName": _mask_name(display), + "avatarUrl": avatar, + "nightMessages": int(night_messages), + "sharePct": float(share), + } + + +def _compute_night_companion_from_index( + *, + account_dir: Path, + year: int, + my_username: str, +) -> Optional[dict[str, Any]]: + """Index path for the night companion stats. Returns None so caller can fall back.""" + + start_ts, end_ts = _year_range_epoch_seconds(year) + me = str(my_username or "").strip() + + index_path = get_chat_search_index_db_path(account_dir) + if not index_path.exists(): + return None + + conn = sqlite3.connect(str(index_path)) + try: + has_fts = ( + conn.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='message_fts' LIMIT 1").fetchone() + is not None + ) + if not has_fts: + return None + + # Convert millisecond timestamps defensively (some datasets store ms). + ts_expr = ( + "CASE " + "WHEN CAST(create_time AS INTEGER) > 1000000000000 " + "THEN CAST(CAST(create_time AS INTEGER)/1000 AS INTEGER) " + "ELSE CAST(create_time AS INTEGER) " + "END" + ) + + where = ( + f"{ts_expr} >= ? AND {ts_expr} < ? " + "AND db_stem NOT LIKE 'biz_message%' " + "AND username NOT LIKE '%@chatroom' " + "AND CAST(local_type AS INTEGER) != 10000" + ) + # 深夜窗口:本地时间 0:00-5:59(小时判定必须先做 ms->s 换算,故放在外层)。 + night_where = "CAST(strftime('%H', datetime(ts, 'unixepoch', 'localtime')) AS INTEGER) < 6" + + sql_counts = ( + "SELECT username, COUNT(1) AS cnt, " + "SUM(CASE WHEN sender_username = ? THEN 1 ELSE 0 END) AS mine " + "FROM (" + f" SELECT {ts_expr} AS ts, username, sender_username " + " FROM message_fts " + f" WHERE {where}" + ") sub " + f"WHERE {night_where} " + "GROUP BY username" + ) + + try: + rows = conn.execute(sql_counts, (me, start_ts, end_ts)).fetchall() + except Exception: + return None + + total = 0 + mine = 0 + best_user = "" + best_cnt = 0 + for r in rows: + if not r: + continue + u = str(r[0] or "").strip() + try: + cnt = int(r[1] or 0) + sent = int(r[2] or 0) + except Exception: + continue + if not u or cnt <= 0: + continue + # 排除公众号/服务号/企业微信等非真人会话,计数与 partner 口径保持一致。 + if not _should_keep_session(u, include_official=False): + continue + total += cnt + mine += sent + # Deterministic: pick lexicographically smallest username on ties. + if cnt > best_cnt or (cnt == best_cnt and (not best_user or u < best_user)): + best_cnt = cnt + best_user = u + + if total <= 0 or not best_user: + return _empty_night_companion() + + # payload_json 仅新版索引存在;缺列时退化为只取分词 text 再还原。 + moment: Optional[dict[str, Any]] = None + row = None + row_has_payload = False + for cols in ('"text", payload_json', '"text"'): + sql_moment = ( + f"SELECT ts, sender_username, {cols}, " + "CAST(strftime('%H', datetime(ts, 'unixepoch', 'localtime')) AS INTEGER) AS h, " + "CAST(strftime('%M', datetime(ts, 'unixepoch', 'localtime')) AS INTEGER) AS m, " + "CAST(strftime('%S', datetime(ts, 'unixepoch', 'localtime')) AS INTEGER) AS s " + "FROM (" + f" SELECT {ts_expr} AS ts, sender_username, {cols} " + " FROM message_fts " + f" WHERE {where} AND username = ?" + ") sub " + f"WHERE {night_where} " + # 复用现有深夜计分:0-4 点 +24h,使 4:59 排在 0:00 与 5:xx 之后。 + "ORDER BY (h*3600 + m*60 + s + CASE WHEN h < 5 THEN 86400 ELSE 0 END) DESC, ts DESC " + "LIMIT 1" + ) + try: + row = conn.execute(sql_moment, (start_ts, end_ts, best_user)).fetchone() + row_has_payload = "payload_json" in cols + break + except Exception: + row = None + if row: + try: + ts = int(row[0] or 0) + except Exception: + ts = 0 + sender = str(row[1] or "").strip() + content = _readable_night_index_text(row[2], row[3] if row_has_payload else None) + if ts > 0: + moment = _night_moment_payload( + ts=ts, + direction="sent" if (me and sender == me) else "received", + content=content, + ) + + return { + "nightMessagesTotal": int(total), + "myNightMessages": int(mine), + "partner": _night_partner_payload( + account_dir=account_dir, + username=best_user, + night_messages=best_cnt, + total=total, + ), + "latestMoment": moment, + } + except Exception: + return None + finally: + try: + conn.close() + except Exception: + pass + + +def _compute_night_companion_fallback( + *, + account_dir: Path, + year: int, + my_username: str, +) -> dict[str, Any]: + """Fallback implementation when no search index is present.""" + + start_ts, end_ts = _year_range_epoch_seconds(year) + me = str(my_username or "").strip() + + session_usernames = _list_session_usernames(account_dir / "session.db") + md5_to_username: dict[str, str] = {} + table_to_username: dict[str, str] = {} + for u in session_usernames: + md5_hex = hashlib.md5(u.encode("utf-8")).hexdigest().lower() + md5_to_username[md5_hex] = u + table_to_username[f"msg_{md5_hex}"] = u + table_to_username[f"chat_{md5_hex}"] = u + + def resolve_username_from_table(table_name: str) -> Optional[str]: + ln = str(table_name or "").lower() + u = table_to_username.get(ln) + if u: + return u + m = _MD5_HEX_RE.search(ln) + if m: + return md5_to_username.get(m.group(0).lower()) + return None + + db_paths = _iter_message_db_paths(account_dir) + db_paths = [p for p in db_paths if not p.name.lower().startswith("biz_message")] + + ts_expr = ( + "CASE WHEN create_time > 1000000000000 THEN CAST(create_time/1000 AS INTEGER) ELSE create_time END" + ) + night_where = "CAST(strftime('%H', datetime(ts, 'unixepoch', 'localtime')) AS INTEGER) < 6" + + total = 0 + mine = 0 + per_user_counts: dict[str, int] = {} + # username -> [(shard 路径, 表名, 本人在该分片的 Name2Id rowid)],供 partner 确定后回查最晚一刻。 + per_user_tables: dict[str, list[tuple[Path, str, Optional[int]]]] = {} + + for db_path in db_paths: + if not db_path.exists(): + continue + + conn: sqlite3.Connection | None = None + try: + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + conn.text_factory = bytes + + try: + r2 = conn.execute("SELECT rowid FROM Name2Id WHERE user_name = ? LIMIT 1", (me,)).fetchone() + my_rowid = int(r2[0]) if r2 and r2[0] is not None else None + except Exception: + my_rowid = None + + tables = _list_message_tables(conn) + if not tables: + continue + + for table_name in tables: + username = resolve_username_from_table(table_name) + if not username or username.endswith("@chatroom"): + continue + # 排除公众号/服务号/企业微信等非真人会话,与索引路径口径一致。 + if not _should_keep_session(username, include_official=False): + continue + + qt = _quote_ident(table_name) + sql = ( + "SELECT COUNT(1) AS cnt, " + "SUM(CASE WHEN real_sender_id = ? THEN 1 ELSE 0 END) AS mine " + "FROM (" + f" SELECT {ts_expr} AS ts, real_sender_id" + f" FROM {qt}" + f" WHERE {ts_expr} >= ? AND {ts_expr} < ? AND local_type != 10000" + ") sub " + f"WHERE {night_where}" + ) + params = (int(my_rowid) if my_rowid is not None else -1, start_ts, end_ts) + try: + r = conn.execute(sql, params).fetchone() + except Exception: + continue + try: + cnt = int(r["cnt"] or 0) + sent = int(r["mine"] or 0) + except Exception: + continue + if cnt <= 0: + continue + + total += cnt + mine += sent + per_user_counts[username] = per_user_counts.get(username, 0) + cnt + per_user_tables.setdefault(username, []).append((db_path, table_name, my_rowid)) + finally: + try: + if conn is not None: + conn.close() + except Exception: + pass + + if total <= 0 or not per_user_counts: + return _empty_night_companion() + + # Deterministic: pick lexicographically smallest username on ties. + best_user = sorted(per_user_counts.items(), key=lambda kv: (-kv[1], kv[0]))[0][0] + best_cnt = per_user_counts[best_user] + + best_ref: Optional[_SentMomentRef] = None + best_direction = "received" + for db_path, table_name, my_rowid in per_user_tables.get(best_user, []): + conn2: sqlite3.Connection | None = None + try: + conn2 = sqlite3.connect(str(db_path)) + conn2.row_factory = sqlite3.Row + conn2.text_factory = bytes + + qt = _quote_ident(table_name) + sql = ( + "SELECT local_id, ts, real_sender_id, " + "CAST(strftime('%H', datetime(ts, 'unixepoch', 'localtime')) AS INTEGER) AS h, " + "CAST(strftime('%M', datetime(ts, 'unixepoch', 'localtime')) AS INTEGER) AS m, " + "CAST(strftime('%S', datetime(ts, 'unixepoch', 'localtime')) AS INTEGER) AS s " + "FROM (" + f" SELECT local_id, {ts_expr} AS ts, real_sender_id" + f" FROM {qt}" + f" WHERE {ts_expr} >= ? AND {ts_expr} < ? AND local_type != 10000" + ") sub " + f"WHERE {night_where} " + "ORDER BY (h*3600 + m*60 + s + CASE WHEN h < 5 THEN 86400 ELSE 0 END) DESC, ts DESC " + "LIMIT 1" + ) + try: + r = conn2.execute(sql, (start_ts, end_ts)).fetchone() + except Exception: + r = None + if not r: + continue + try: + local_id = int(r["local_id"] or 0) + ts = int(r["ts"] or 0) + h = int(r["h"] or 0) + m = int(r["m"] or 0) + s = int(r["s"] or 0) + sender_id = int(r["real_sender_id"] or 0) + except Exception: + continue + if local_id <= 0 or ts <= 0: + continue + + score = (h * 3600 + m * 60 + s) + (86400 if h < 5 else 0) + if best_ref is None or score > best_ref.score or (score == best_ref.score and ts > best_ref.ts): + best_ref = _SentMomentRef( + ts=int(ts), + score=int(score), + username=str(best_user), + db_stem=str(db_path.stem), + table_name=str(table_name), + local_id=int(local_id), + ) + best_direction = "sent" if (my_rowid is not None and sender_id == int(my_rowid)) else "received" + finally: + try: + if conn2 is not None: + conn2.close() + except Exception: + pass + + moment: Optional[dict[str, Any]] = None + if best_ref is not None: + payload = _fetch_message_moment_payload(account_dir=account_dir, ref=best_ref, contact_rows={}) + content = str((payload or {}).get("content") or "") + moment = _night_moment_payload(ts=best_ref.ts, direction=best_direction, content=content) + + return { + "nightMessagesTotal": int(total), + "myNightMessages": int(mine), + "partner": _night_partner_payload( + account_dir=account_dir, + username=best_user, + night_messages=best_cnt, + total=total, + ), + "latestMoment": moment, + } + + +def _compute_night_companion(*, account_dir: Path, year: int, my_username: str) -> dict[str, Any]: + """深夜守夜人:凌晨 0:00-5:59 的单聊双向消息统计(排除群聊 / biz 分片 / 系统消息)。""" + + result = _compute_night_companion_from_index( + account_dir=account_dir, + year=year, + my_username=my_username, + ) + if result is not None: + return result + try: + return _compute_night_companion_fallback( + account_dir=account_dir, + year=year, + my_username=my_username, + ) + except Exception: + return _empty_night_companion() + + def build_card_01_cyber_schedule( *, account_dir: Path, @@ -1080,6 +1506,18 @@ def build_card_01_cyber_schedule( time.time() - t0, ) + # 深夜守夜人(双向消息统计,不依赖上面的“仅本人发出”结果)。 + t0 = time.time() + night_companion = _compute_night_companion(account_dir=account_dir, year=year, my_username=sender) + logger.info( + "Wrapped card#1 night companion computed: account=%s year=%s total=%s partner=%s elapsed=%.2fs", + str(account_dir.name or "").strip(), + year, + int(night_companion.get("nightMessagesTotal") or 0), + "ok" if night_companion.get("partner") else "none", + time.time() - t0, + ) + return { "id": 1, "title": "你是「早八人」还是「夜猫子」?", @@ -1097,5 +1535,6 @@ def build_card_01_cyber_schedule( "latestSent": latest_sent, "yearFirstSent": year_first_sent, "yearLastSent": year_last_sent, + "nightCompanion": night_companion, }, } diff --git a/src/wechat_decrypt_tool/wrapped/cards/card_02_message_chars.py b/src/wechat_decrypt_tool/wrapped/cards/card_02_message_chars.py index 631ebffa..bb500fe4 100644 --- a/src/wechat_decrypt_tool/wrapped/cards/card_02_message_chars.py +++ b/src/wechat_decrypt_tool/wrapped/cards/card_02_message_chars.py @@ -1,7 +1,9 @@ from __future__ import annotations +import hashlib import math import random +import re import sqlite3 import time from collections import Counter @@ -11,7 +13,18 @@ from pypinyin import lazy_pinyin, Style -from ...chat_helpers import _decode_message_content, _decode_sqlite_text, _iter_message_db_paths, _quote_ident +from ...chat_helpers import ( + _build_avatar_url, + _decode_message_content, + _decode_sqlite_text, + _extract_xml_attr, + _extract_xml_tag_text, + _iter_message_db_paths, + _load_contact_rows, + _pick_display_name, + _quote_ident, + _should_keep_session, +) from ...chat_search_index import get_chat_search_index_db_path from ...logging_config import get_logger @@ -769,6 +782,376 @@ def compute_text_message_char_counts(*, account_dir: Path, year: int) -> tuple[i return int(sent_total), int(recv_total) +# --------------------------------------------------------------------------- +# 语音与通话统计(local_type=34 语音消息 / local_type=50 VoIP 通话) +# --------------------------------------------------------------------------- + +_MD5_HEX_RE = re.compile(r"(?i)[0-9a-f]{32}") +_VOIP_BUBBLE_RE = re.compile(r"(]*>.*?)", flags=re.IGNORECASE | re.DOTALL) +# 兼容 时:分:秒 与 分:秒 两种格式,如 "通话时长 00:19" / "通话时长 1:02:03"。 +_VOIP_DURATION_RE = re.compile(r"通话时长\s*(\d+):(\d+)(?::(\d+))?") +# 用不含前缀的子串以同时覆盖「已拒绝/对方已拒绝」「对方无应答」「忙线未接听」等变体。 +_VOIP_MISSED_MARKERS = ("已取消", "已拒绝", "未接听", "无应答") + + +def _mask_name(name: str) -> str: + s = str(name or "").strip() + if not s: + return "" + if len(s) == 1: + return "*" + if len(s) == 2: + return s[0] + "*" + return s[0] + ("*" * (len(s) - 2)) + s[-1] + + +def _list_session_usernames(session_db_path: Path) -> list[str]: + if not session_db_path.exists(): + return [] + conn = sqlite3.connect(str(session_db_path)) + try: + try: + rows = conn.execute("SELECT username FROM SessionTable").fetchall() + except sqlite3.OperationalError: + rows = conn.execute("SELECT username FROM Session").fetchall() + except Exception: + rows = [] + finally: + try: + conn.close() + except Exception: + pass + + out: list[str] = [] + for r in rows: + if not r or not r[0]: + continue + u = str(r[0]).strip() + if u: + out.append(u) + return out + + +def _voicelength_to_seconds(raw: Any) -> int: + """ + 语音时长换算,语义与 chat_export_service.get_voice_duration_in_seconds 一致: + voicelength 正常口径为毫秒,四舍五入到秒。 + + 防御:微信语音上限 60 秒,毫秒口径下有效值 >= 1000;个别历史数据直接存秒 + (很小的值),此时按秒口径返回,避免被 /1000 抹成 0。 + """ + try: + v = int(str(raw or "0").strip() or "0") + except Exception: + v = 0 + if v <= 0: + return 0 + if v <= 60: + # 秒口径防御:60 以内视为已经是秒。 + return int(v) + return int(round(v / 1000.0)) + + +def _parse_voip_duration_seconds(msg_text: str) -> Optional[int]: + m = _VOIP_DURATION_RE.search(str(msg_text or "")) + if not m: + return None + try: + a = int(m.group(1)) + b = int(m.group(2)) + c = m.group(3) + except Exception: + return None + if c is not None: + return a * 3600 + b * 60 + int(c) + return a * 60 + b + + +def compute_voice_call_stats(*, account_dir: Path, year: int) -> dict[str, Any]: + """ + 扫描 message_*.db 分片(排除 biz_message*),统计单聊里的语音消息与音视频通话。 + + 说明:message_fts 索引不含原始 XML(voicelength / VoIPBubbleMsg),必须走分片; + local_type IN (34, 50) 的消息量小,全年扫描成本可控。 + """ + start_ts, end_ts = _year_range_epoch_seconds(year) + my_username = str(account_dir.name or "").strip() + + # 会话 username 从表名反解(msg_ / chat_)。 + session_usernames = _list_session_usernames(account_dir / "session.db") + md5_to_username: dict[str, str] = {} + table_to_username: dict[str, str] = {} + for u in session_usernames: + md5_hex = hashlib.md5(u.encode("utf-8")).hexdigest().lower() + md5_to_username[md5_hex] = u + table_to_username[f"msg_{md5_hex}"] = u + table_to_username[f"chat_{md5_hex}"] = u + + def resolve_username_from_table(table_name: str) -> str: + ln = str(table_name or "").lower() + x = table_to_username.get(ln) + if x: + return x + m = _MD5_HEX_RE.search(ln) + if m: + return str(md5_to_username.get(m.group(0).lower()) or "") + return "" + + voice_sent_count = 0 + voice_sent_seconds = 0 + voice_recv_count = 0 + voice_recv_seconds = 0 + voice_sent_seconds_by_user: Counter[str] = Counter() + voice_sent_count_by_user: Counter[str] = Counter() + voice_recv_seconds_by_user: Counter[str] = Counter() + voice_recv_count_by_user: Counter[str] = Counter() + # (seconds, direction, username, ts) + longest_voice: Optional[tuple[int, str, str, int]] = None + + call_total_count = 0 + call_video_count = 0 + call_voice_count = 0 + call_connected_count = 0 + call_total_seconds = 0 + call_missed_count = 0 + call_seconds_by_user: Counter[str] = Counter() + call_count_by_user: Counter[str] = Counter() + + ts_expr = ( + "CASE " + "WHEN CAST(create_time AS INTEGER) > 1000000000000 " + "THEN CAST(CAST(create_time AS INTEGER)/1000 AS INTEGER) " + "ELSE CAST(create_time AS INTEGER) " + "END" + ) + + db_paths = _iter_message_db_paths(account_dir) + for db_path in db_paths: + try: + if db_path.name.lower().startswith("biz_message"): + continue + except Exception: + pass + if not db_path.exists(): + continue + + conn: sqlite3.Connection | None = None + try: + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + conn.text_factory = bytes + + my_rowid: Optional[int] + try: + r2 = conn.execute("SELECT rowid FROM Name2Id WHERE user_name = ? LIMIT 1", (my_username,)).fetchone() + my_rowid = int(r2[0]) if r2 and r2[0] is not None else None + except Exception: + my_rowid = None + + # 本人不在该分片 Name2Id(从未在此分片发过消息)时不跳库: + # 用 -1 兜底参与比较,方向恒判为 received,保证接收侧语音/通话统计不丢。 + if my_rowid is None: + my_rowid = -1 + + tables = _list_message_tables(conn) + if not tables: + continue + + for table in tables: + username = resolve_username_from_table(table) + # 只统计单聊:无法反解会话或群聊(@chatroom)一律跳过。 + if not username or username.endswith("@chatroom"): + continue + + qt = _quote_ident(table) + sql = ( + "SELECT local_type, real_sender_id, create_time, message_content, compress_content " + f"FROM {qt} " + "WHERE CAST(local_type AS INTEGER) IN (34, 50) " + f" AND {ts_expr} >= ? AND {ts_expr} < ?" + ) + try: + cur = conn.execute(sql, (start_ts, end_ts)) + except Exception: + continue + + for r in cur: + try: + local_type = int(r["local_type"] or 0) + except Exception: + continue + + try: + rsid = int(r["real_sender_id"] or 0) + except Exception: + rsid = 0 + is_sent = rsid == my_rowid + + ts = 0 + try: + ts = int(r["create_time"] or 0) + except Exception: + ts = 0 + if ts > 1_000_000_000_000: + ts = int(ts / 1000) + + raw_text = "" + try: + raw_text = _decode_message_content(r["compress_content"], r["message_content"]).strip() + except Exception: + raw_text = "" + + if local_type == 34: + duration_raw = _extract_xml_attr(raw_text, "voicelength") or _extract_xml_tag_text( + raw_text, "voicelength" + ) + seconds = _voicelength_to_seconds(duration_raw) + if is_sent: + voice_sent_count += 1 + voice_sent_seconds += seconds + voice_sent_count_by_user[username] += 1 + voice_sent_seconds_by_user[username] += seconds + else: + voice_recv_count += 1 + voice_recv_seconds += seconds + voice_recv_count_by_user[username] += 1 + voice_recv_seconds_by_user[username] += seconds + if seconds > 0 and (longest_voice is None or seconds > longest_voice[0]): + longest_voice = ( + int(seconds), + "sent" if is_sent else "received", + username, + int(ts), + ) + continue + + # local_type == 50: VoIP 通话(VoIPBubbleMsg 块)。 + block = raw_text + m_voip = _VOIP_BUBBLE_RE.search(raw_text) + if m_voip: + block = m_voip.group(1) or raw_text + room_type = str(_extract_xml_tag_text(block, "room_type") or "").strip() + voip_msg = str(_extract_xml_tag_text(block, "msg") or "").strip() + + call_total_count += 1 + call_count_by_user[username] += 1 + if room_type == "0": + call_video_count += 1 + elif room_type == "1": + call_voice_count += 1 + + if any(marker in voip_msg for marker in _VOIP_MISSED_MARKERS): + call_missed_count += 1 + continue + + duration = _parse_voip_duration_seconds(voip_msg) + if duration is not None: + call_connected_count += 1 + call_total_seconds += int(duration) + call_seconds_by_user[username] += int(duration) + else: + # 文案既无 marker 也无「通话时长」:视为未接通, + # 保证 totalCount == connectedCount + missedOrCanceledCount 恒等。 + call_missed_count += 1 + finally: + if conn is not None: + try: + conn.close() + except Exception: + pass + + def pick_top_partner(seconds_by_user: Counter[str], count_by_user: Counter[str]) -> Optional[tuple[str, int, int]]: + candidates = [ + (u, int(seconds_by_user.get(u, 0)), int(count_by_user.get(u, 0))) + for u in set(seconds_by_user) | set(count_by_user) + if u and (not u.endswith("@chatroom")) and _should_keep_session(u, include_official=False) + ] + candidates = [c for c in candidates if c[1] > 0 or c[2] > 0] + if not candidates: + return None + return sorted(candidates, key=lambda c: (-c[1], -c[2], c[0]))[0] + + top_voice_sent = pick_top_partner(voice_sent_seconds_by_user, voice_sent_count_by_user) + top_voice_recv = pick_top_partner(voice_recv_seconds_by_user, voice_recv_count_by_user) + top_call = pick_top_partner(call_seconds_by_user, call_count_by_user) + + contact_usernames: list[str] = [] + for item in (top_voice_sent, top_voice_recv, top_call): + if item is not None and item[0]: + contact_usernames.append(item[0]) + if longest_voice is not None and longest_voice[2]: + contact_usernames.append(longest_voice[2]) + contact_rows = _load_contact_rows(account_dir / "contact.db", contact_usernames) + + def build_partner_obj(item: Optional[tuple[str, int, int]]) -> Optional[dict[str, Any]]: + if item is None: + return None + username, seconds, count = item + row = contact_rows.get(username) + display = _pick_display_name(row, username) + return { + "username": username, + "displayName": display, + "maskedName": _mask_name(display), + "avatarUrl": _build_avatar_url(str(account_dir.name or ""), username), + "seconds": int(seconds), + "count": int(count), + } + + longest_obj: Optional[dict[str, Any]] = None + if longest_voice is not None: + seconds, direction, username, ts = longest_voice + row = contact_rows.get(username) + display = _pick_display_name(row, username) + date_str = "" + if ts > 0: + try: + date_str = datetime.fromtimestamp(ts).strftime("%Y-%m-%d") + except Exception: + date_str = "" + longest_obj = { + "seconds": int(seconds), + "direction": str(direction), + "username": username, + "displayName": display, + "maskedName": _mask_name(display), + "avatarUrl": _build_avatar_url(str(account_dir.name or ""), username), + "date": date_str, + } + + voice = { + "sentCount": int(voice_sent_count), + "sentSeconds": int(voice_sent_seconds), + "receivedCount": int(voice_recv_count), + "receivedSeconds": int(voice_recv_seconds), + "longest": longest_obj, + "topSentPartner": build_partner_obj(top_voice_sent), + "topReceivedPartner": build_partner_obj(top_voice_recv), + } + calls = { + "totalCount": int(call_total_count), + "videoCount": int(call_video_count), + "voiceCount": int(call_voice_count), + "connectedCount": int(call_connected_count), + "totalSeconds": int(call_total_seconds), + "missedOrCanceledCount": int(call_missed_count), + "topPartner": build_partner_obj(top_call), + } + + logger.info( + "Wrapped card#2 voice/call stats: account=%s year=%s voice_sent=%d voice_recv=%d calls=%d connected=%d missed=%d", + my_username, + year, + int(voice_sent_count), + int(voice_recv_count), + int(call_total_count), + int(call_connected_count), + int(call_missed_count), + ) + + return {"voice": voice, "calls": calls} + + def build_card_02_message_chars(*, account_dir: Path, year: int) -> dict[str, Any]: sent_chars, recv_chars = compute_text_message_char_counts(account_dir=account_dir, year=year) @@ -778,6 +1161,9 @@ def build_card_02_message_chars(*, account_dir: Path, year: int) -> dict[str, An # 计算键盘敲击统计 keyboard_stats = compute_keyboard_stats(account_dir=account_dir, year=year, sample_rate=1.0) + # 计算语音与通话统计 + voice_call_stats = compute_voice_call_stats(account_dir=account_dir, year=year) + if sent_chars > 0 and recv_chars > 0: narrative = f"你今年在微信里打了 {sent_chars:,} 个字,也收到了 {recv_chars:,} 个字。" elif sent_chars > 0: @@ -802,5 +1188,7 @@ def build_card_02_message_chars(*, account_dir: Path, year: int) -> dict[str, An "sentBook": sent_book, "receivedA4": recv_a4, "keyboard": keyboard_stats, + "voice": voice_call_stats["voice"], + "calls": voice_call_stats["calls"], }, } diff --git a/src/wechat_decrypt_tool/wrapped/cards/card_03_reply_speed.py b/src/wechat_decrypt_tool/wrapped/cards/card_03_reply_speed.py index 2c316654..b5d9c1aa 100644 --- a/src/wechat_decrypt_tool/wrapped/cards/card_03_reply_speed.py +++ b/src/wechat_decrypt_tool/wrapped/cards/card_03_reply_speed.py @@ -280,6 +280,20 @@ def compute_reply_speed_stats(*, account_dir: Path, year: int) -> dict[str, Any] # Keep more than 10 so the bar-race "TOP10" can actually evolve (members can enter/leave over time). top_total_n = 100 + # ---- 「谁先开口」:相邻消息间隔 ≥1 小时切分为一次“对话”,首条消息发送方即发起者 ---- + conv_gap_seconds = 3600 + initiative_conversations = 0 + initiated_by_me = 0 + initiated_by_others = 0 + init_by_me_counts: dict[str, int] = {} + init_to_me_counts: dict[str, int] = {} + + # ---- 「势均力敌」:双方消息量均 ≥50 且 min/max 比值最接近 1 的好友 ---- + mutual_min_each = 50 + mutual_best_agg: _ConvAgg | None = None + # (ratio, total, username):比值优先,其次总量更大者,最后用户名字典序保证确定性。 + mutual_best_key: tuple[float, int, str] | None = None + def consider_conv(agg: _ConvAgg) -> None: nonlocal best_score, best_agg if not agg.username: @@ -303,6 +317,7 @@ def consider_conv(agg: _ConvAgg) -> None: heapq.heappushpop(top_heap, key) def consider_total(agg: _ConvAgg) -> None: + nonlocal mutual_best_agg, mutual_best_key if not agg.username: return if agg.total <= 0: @@ -314,6 +329,23 @@ def consider_total(agg: _ConvAgg) -> None: if agg.outgoing > 0: sent_to_contacts.add(agg.username) + sent = int(agg.outgoing) + received = int(agg.incoming) + if sent >= mutual_min_each and received >= mutual_min_each: + ratio = float(min(sent, received)) / float(max(sent, received)) + if ( + mutual_best_key is None + or ratio > mutual_best_key[0] + or (ratio == mutual_best_key[0] and sent + received > mutual_best_key[1]) + or ( + ratio == mutual_best_key[0] + and sent + received == mutual_best_key[1] + and str(agg.username) < mutual_best_key[2] + ) + ): + mutual_best_key = (float(ratio), int(sent + received), str(agg.username)) + mutual_best_agg = agg + total = int(agg.total) all_totals[agg.username] = int(total) key = (total, str(agg.username), agg) @@ -380,6 +412,7 @@ def consider_total(agg: _ConvAgg) -> None: min_gap = 0 max_gap = 0 prev_other_ts: int | None = None + prev_any_ts: int | None = None def flush() -> None: nonlocal cur_username, incoming, outgoing, replies, sum_gap, sum_gap_capped, min_gap, max_gap @@ -417,12 +450,25 @@ def flush() -> None: sum_gap = sum_gap_capped = 0 min_gap = max_gap = 0 prev_other_ts = None + prev_any_ts = None # Drop system/official-ish sessions (best-effort). if not _should_keep_session(username, include_official=False): continue is_me = sender == my_username + + # 「谁先开口」:会话首条消息、或与上一条消息间隔 ≥ conv_gap_seconds,均视为新对话开始。 + if prev_any_ts is None or ts - prev_any_ts >= conv_gap_seconds: + initiative_conversations += 1 + if is_me: + initiated_by_me += 1 + init_by_me_counts[username] = init_by_me_counts.get(username, 0) + 1 + else: + initiated_by_others += 1 + init_to_me_counts[username] = init_to_me_counts.get(username, 0) + 1 + prev_any_ts = ts + if is_me: outgoing += 1 if prev_other_ts is not None and ts >= prev_other_ts: @@ -518,6 +564,10 @@ def flush() -> None: key=lambda x: (-x[0], x[1].username), ) + # 「谁先开口」双榜:按次数降序(并列时用户名字典序),各取前 3。 + top_init_by_me = sorted(init_by_me_counts.items(), key=lambda kv: (-int(kv[1]), str(kv[0])))[:3] + top_init_to_me = sorted(init_to_me_counts.items(), key=lambda kv: (-int(kv[1]), str(kv[0])))[:3] + # Resolve contact display names/avatars for a small set (bestBuddy + extremes + top list). need_usernames: list[str] = [] if best_agg is not None: @@ -530,6 +580,12 @@ def flush() -> None: need_usernames.append(agg.username) for _, agg in top_totals: need_usernames.append(agg.username) + for u, _ in top_init_by_me: + need_usernames.append(u) + for u, _ in top_init_to_me: + need_usernames.append(u) + if mutual_best_agg is not None: + need_usernames.append(mutual_best_agg.username) uniq_usernames = [] seen = set() @@ -625,6 +681,47 @@ def conv_to_obj(score: float | None, agg: _ConvAgg) -> dict[str, Any]: for total, agg in top_totals ] + def initiative_person(u: str, count: int) -> dict[str, Any]: + row = contact_rows.get(u) + display = _pick_display_name(row, u) + return { + "username": u, + "displayName": display, + "maskedName": _mask_name(display), + "avatarUrl": _build_avatar_url(str(account_dir.name or ""), u) if u else "", + "count": int(count), + } + + mutual_friend_obj: dict[str, Any] | None = None + if mutual_best_agg is not None: + sent = int(mutual_best_agg.outgoing) + received = int(mutual_best_agg.incoming) + row = contact_rows.get(mutual_best_agg.username) + display = _pick_display_name(row, mutual_best_agg.username) + mutual_friend_obj = { + "username": mutual_best_agg.username, + "displayName": display, + "maskedName": _mask_name(display), + "avatarUrl": _build_avatar_url(str(account_dir.name or ""), mutual_best_agg.username), + "sentCount": sent, + "receivedCount": received, + "ratio": round(float(min(sent, received)) / float(max(sent, received)), 2), + } + + initiative_obj: dict[str, Any] = { + "conversationCount": int(initiative_conversations), + "initiatedByMe": int(initiated_by_me), + "initiatedByOthers": int(initiated_by_others), + "initiationRatePct": ( + round(float(initiated_by_me) * 100.0 / float(initiative_conversations), 1) + if initiative_conversations > 0 + else None + ), + "topInitiatedByMe": [initiative_person(u, c) for u, c in top_init_by_me], + "topInitiatedToMe": [initiative_person(u, c) for u, c in top_init_to_me], + "mutualFriend": mutual_friend_obj, + } + # Prepare "bar race" data: all 1v1 sessions (exclude official/system), cumulative per day. race = None if used_index and all_totals: @@ -821,6 +918,7 @@ def conv_to_obj(score: float | None, agg: _ConvAgg) -> dict[str, Any]: "topBuddies": top_list, "topTotals": top_totals_list, "allContacts": all_contacts_list, + "initiative": initiative_obj, "race": race, "settings": { "gapCapSeconds": int(gap_cap_seconds), diff --git a/src/wechat_decrypt_tool/wrapped/service.py b/src/wechat_decrypt_tool/wrapped/service.py index 16b7c1ce..5ad3265a 100644 --- a/src/wechat_decrypt_tool/wrapped/service.py +++ b/src/wechat_decrypt_tool/wrapped/service.py @@ -28,7 +28,7 @@ # an older partial cache. _IMPLEMENTED_UPTO_ID = 7 # Bump this when we change card payloads/ordering while keeping the same implemented_upto. -_CACHE_VERSION = 26 +_CACHE_VERSION = 27 # "Manifest" is used by the frontend to render the deck quickly, then lazily fetch each card. diff --git a/tests/test_chat_export_html_format.py b/tests/test_chat_export_html_format.py index 58f5ac2b..0bcdb469 100644 --- a/tests/test_chat_export_html_format.py +++ b/tests/test_chat_export_html_format.py @@ -1,4 +1,5 @@ import os +import io import json import hashlib import logging @@ -268,13 +269,21 @@ def _insert_missing_voice_message(self, account_dir: Path, *, username: str, ser finally: conn.close() - def _create_job(self, manager, *, account: str, username: str): + def _create_job( + self, + manager, + *, + account: str, + username: str, + transcribe_voice: bool = False, + export_format: str = "html", + ): job = manager.create_job( account=account, source="decrypted", scope="selected", usernames=[username], - export_format="html", + export_format=export_format, start_time=None, end_time=None, include_hidden=False, @@ -287,6 +296,7 @@ def _create_job(self, manager, *, account: str, username: str): download_remote_media=False, privacy_mode=False, file_name=None, + transcribe_voice=transcribe_voice, ) for _ in range(200): @@ -458,6 +468,144 @@ def test_html_export_prefers_mp3_for_voice_assets(self): else: os.environ["WECHAT_TOOL_DATA_DIR"] = prev_data + def test_html_export_keeps_voice_audio_and_adds_whisper_transcript(self): + with TemporaryDirectory() as td: + root = Path(td) + account = "wxid_test" + username = "wxid_friend" + self._prepare_account(root, account=account, username=username) + + prev_data = os.environ.get("WECHAT_TOOL_DATA_DIR") + try: + os.environ["WECHAT_TOOL_DATA_DIR"] = str(root) + svc = self._reload_export_modules() + original_converter = svc._convert_silk_to_browser_audio + original_service_getter = svc.get_voice_transcription_service + svc._convert_silk_to_browser_audio = ( + lambda data, preferred_format="mp3": (b"ID3FAKE_MP3_DATA", "mp3", "audio/mpeg") + ) + + class FakeTranscriptionService: + @staticmethod + def transcribe_voice(**_kwargs): + return { + "status": "success", + "text": "下午三点把资料准备好", + "language": "zh", + "model": "small", + "duration": 3.0, + "cached": False, + } + + svc.get_voice_transcription_service = lambda: FakeTranscriptionService() + try: + job = self._create_job( + svc.CHAT_EXPORT_MANAGER, + account=account, + username=username, + transcribe_voice=True, + ) + finally: + svc._convert_silk_to_browser_audio = original_converter + svc.get_voice_transcription_service = original_service_getter + + self.assertEqual(job.status, "done", msg=job.error) + with zipfile.ZipFile(job.zip_path, "r") as zf: + names = set(zf.namelist()) + voice_path = f"media/voices/voice_{self._VOICE_SERVER_ID}.mp3" + self.assertIn(voice_path, names) + html_path = next((n for n in names if n.endswith("/messages.html")), "") + html_text = zf.read(html_path).decode("utf-8") + self.assertIn("下午三点把资料准备好", html_text) + self.assertIn(f"../../{voice_path}", html_text) + voice_wrapper_start = html_text.index('class="wechat-voice-wrapper') + voice_bubble_start = html_text.index('class="wechat-voice-bubble', voice_wrapper_start) + transcript_start = html_text.index('class="wechat-voice-transcript', voice_bubble_start) + self.assertLess(voice_wrapper_start, voice_bubble_start) + self.assertLess(voice_bubble_start, transcript_start) + css_path = next((n for n in names if n.startswith("assets/_wce/c-") and n.endswith(".css")), "") + self.assertTrue(css_path) + css_text = zf.read(css_path).decode("utf-8") + self.assertIn(".wechat-voice-transcript", css_text) + self.assertIn( + ".wechat-voice-wrapper{display:flex;flex-direction:column", + "".join(css_text.split()), + ) + manifest = json.loads(zf.read("manifest.json").decode("utf-8")) + self.assertTrue(manifest["options"]["transcribeVoice"]) + report = json.loads(zf.read("report.json").decode("utf-8")) + self.assertEqual(report["voiceTranscription"]["success"], 1) + finally: + logging.shutdown() + if prev_data is None: + os.environ.pop("WECHAT_TOOL_DATA_DIR", None) + else: + os.environ["WECHAT_TOOL_DATA_DIR"] = prev_data + + def test_voice_transcript_is_shared_by_json_txt_and_excel_exports(self): + with TemporaryDirectory() as td: + root = Path(td) + account = "wxid_test" + username = "wxid_friend" + self._prepare_account(root, account=account, username=username) + + prev_data = os.environ.get("WECHAT_TOOL_DATA_DIR") + try: + os.environ["WECHAT_TOOL_DATA_DIR"] = str(root) + svc = self._reload_export_modules() + original_service_getter = svc.get_voice_transcription_service + + class FakeTranscriptionService: + @staticmethod + def transcribe_voice(**_kwargs): + return { + "status": "success", + "text": "后台服务已经启动", + "language": "zh", + "model": "small", + "duration": 3.0, + "cached": False, + } + + svc.get_voice_transcription_service = lambda: FakeTranscriptionService() + try: + for export_format in ("json", "txt", "excel"): + with self.subTest(export_format=export_format): + job = self._create_job( + svc.CHAT_EXPORT_MANAGER, + account=account, + username=username, + transcribe_voice=True, + export_format=export_format, + ) + self.assertEqual(job.status, "done", msg=job.error) + with zipfile.ZipFile(job.zip_path, "r") as archive: + message_path = next( + name + for name in archive.namelist() + if name.endswith(f"/messages.{export_format if export_format != 'excel' else 'xlsx'}") + ) + payload = archive.read(message_path) + if export_format == "json": + messages = json.loads(payload.decode("utf-8"))["messages"] + voice = next(item for item in messages if item.get("renderType") == "voice") + rendered = str(voice.get("voiceTranscript") or "") + elif export_format == "txt": + rendered = payload.decode("utf-8-sig") + else: + with zipfile.ZipFile(io.BytesIO(payload), "r") as workbook: + rendered = workbook.read("xl/worksheets/sheet1.xml").decode("utf-8") + self.assertIn("后台服务已经启动", rendered) + self.assertNotIn("後臺服務已經啟動", rendered) + finally: + svc.get_voice_transcription_service = original_service_getter + finally: + logging.shutdown() + if prev_data is None: + os.environ.pop("WECHAT_TOOL_DATA_DIR", None) + else: + os.environ["WECHAT_TOOL_DATA_DIR"] = prev_data + def test_html_export_keeps_voice_bubble_when_audio_file_missing(self): with TemporaryDirectory() as td: root = Path(td) diff --git a/tests/test_export_integrity.py b/tests/test_export_integrity.py index c163f345..bbc41224 100644 --- a/tests/test_export_integrity.py +++ b/tests/test_export_integrity.py @@ -37,6 +37,11 @@ def test_native_module_owns_every_export_stylesheet(self): with self.subTest(kind=kind): self.assertIn(selector, export_css(kind)) + chat_css = export_css("chat") + compact_chat_css = "".join(chat_css.split()) + self.assertIn(".wechat-voice-transcript", chat_css) + self.assertIn(".wechat-voice-wrapper{display:flex;flex-direction:column", compact_chat_css) + exposed_sources = [ ROOT / "frontend" / "pages" / "contacts.vue", ROOT / "src" / "wechat_decrypt_tool" / "routers" / "chat_contacts.py", diff --git a/tests/test_voice_transcription.py b/tests/test_voice_transcription.py new file mode 100644 index 00000000..9d72e8d9 --- /dev/null +++ b/tests/test_voice_transcription.py @@ -0,0 +1,465 @@ +from __future__ import annotations + +import sqlite3 +import sys +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + +from wechat_decrypt_tool.voice_transcription import ( + TRANSCRIPT_TEXT_VERSION, + VoiceTranscriptionConfig, + VoiceTranscriptionError, + VoiceTranscriptionService, + inspect_model_readiness, + invalidate_cuda_probe_cache, + load_voice_data, + normalize_transcript_text, + probe_cuda, +) + + +class _FakeModel: + def __init__(self) -> None: + self.calls = 0 + + def transcribe(self, path, **kwargs): + self.calls += 1 + self.last_path = path + self.last_kwargs = kwargs + return iter([SimpleNamespace(text=" 你好"), SimpleNamespace(text="世界 ")]), SimpleNamespace( + language="zh", + duration=2.5, + ) + + +class TestVoiceTranscription(unittest.TestCase): + @staticmethod + def _create_ready_model_dir(path: Path) -> None: + for name in ("model.bin", "config.json", "tokenizer.json", "vocabulary.txt"): + (path / name).write_bytes(b"test") + + def test_local_model_directory_is_detected_without_loading_model(self): + with tempfile.TemporaryDirectory() as tmp: + model_dir = Path(tmp) + self._create_ready_model_dir(model_dir) + readiness = inspect_model_readiness(str(model_dir)) + service = VoiceTranscriptionService(VoiceTranscriptionConfig(model=str(model_dir))) + with patch("wechat_decrypt_tool.voice_transcription.probe_cuda", return_value={ + "available": False, "deviceCount": 0, "devices": [], "reason": "" + }): + status = service.status() + + self.assertTrue(readiness["ready"]) + self.assertEqual(readiness["source"], "local-directory") + self.assertTrue(status["modelReady"]) + self.assertTrue(status["available"]) + + def test_missing_local_model_is_unavailable_even_when_download_is_allowed(self): + with tempfile.TemporaryDirectory() as tmp: + missing = Path(tmp) / "missing-model" + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model=str(missing), allow_download=True) + ) + with patch("wechat_decrypt_tool.voice_transcription.probe_cuda", return_value={ + "available": False, "deviceCount": 0, "devices": [], "reason": "" + }): + status = service.status() + + self.assertFalse(status["modelReady"]) + self.assertFalse(status["available"]) + self.assertFalse(status["modelDownloadRequired"]) + self.assertIn("本地 Whisper 模型目录", status["reason"]) + + def test_huggingface_cached_model_is_ready(self): + with tempfile.TemporaryDirectory() as tmp: + model_dir = Path(tmp) + self._create_ready_model_dir(model_dir) + with patch("faster_whisper.utils.download_model", return_value=str(model_dir)) as resolver: + readiness = inspect_model_readiness("medium") + + self.assertTrue(readiness["ready"]) + self.assertEqual(readiness["source"], "huggingface-cache") + resolver.assert_called_once_with("medium", local_files_only=True) + + def test_missing_huggingface_model_is_blocked_when_download_is_disabled(self): + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model="medium", allow_download=False) + ) + with patch("faster_whisper.utils.download_model", side_effect=OSError("cache miss")), patch( + "wechat_decrypt_tool.voice_transcription.probe_cuda", + return_value={"available": False, "deviceCount": 0, "devices": [], "reason": ""}, + ): + status = service.status() + + self.assertFalse(status["modelReady"]) + self.assertFalse(status["available"]) + self.assertFalse(status["allowDownload"]) + self.assertIn("禁止自动下载", status["reason"]) + + def test_missing_huggingface_model_can_be_prepared_when_download_is_allowed(self): + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model="medium", allow_download=True) + ) + with patch("faster_whisper.utils.download_model", side_effect=OSError("cache miss")), patch( + "wechat_decrypt_tool.voice_transcription.probe_cuda", + return_value={"available": False, "deviceCount": 0, "devices": [], "reason": ""}, + ): + status = service.status() + + self.assertFalse(status["modelReady"]) + self.assertTrue(status["available"]) + self.assertTrue(status["allowDownload"]) + self.assertTrue(status["modelDownloadRequired"]) + self.assertIn("联网下载", status["reason"]) + + def test_load_voice_data_from_decrypted_database(self): + with tempfile.TemporaryDirectory() as tmp: + account_dir = Path(tmp) + conn = sqlite3.connect(str(account_dir / "media_0.db")) + try: + conn.execute("CREATE TABLE VoiceInfo (svr_id INTEGER, create_time INTEGER, voice_data BLOB)") + conn.execute("INSERT INTO VoiceInfo VALUES (?, ?, ?)", (123, 456, b"voice-data")) + conn.commit() + finally: + conn.close() + + self.assertEqual(load_voice_data(account_dir, 123), b"voice-data") + + def test_transcription_is_cached_by_message_audio_and_model(self): + with tempfile.TemporaryDirectory() as tmp: + account_dir = Path(tmp) + fake_model = _FakeModel() + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model="small", language="zh"), + model_loader=lambda _config: fake_model, + ) + + with patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ): + first = service.transcribe_voice(account_dir=account_dir, server_id=99, voice_data=b"SILK") + second = service.transcribe_voice(account_dir=account_dir, server_id=99, voice_data=b"SILK") + + self.assertEqual(first["text"], "你好世界") + self.assertFalse(first["cached"]) + self.assertTrue(second["cached"]) + self.assertEqual(fake_model.calls, 1) + self.assertEqual(fake_model.last_kwargs["language"], "zh") + self.assertTrue((account_dir / "_cache" / "voice_transcripts.sqlite3").exists()) + + def test_transcript_is_normalized_to_simplified_chinese_before_cache(self): + class TraditionalModel(_FakeModel): + def transcribe(self, path, **kwargs): + self.calls += 1 + return iter([SimpleNamespace(text=" 這是一段繁體中文,"), SimpleNamespace(text="後臺服務已經啟動。")]), SimpleNamespace( + language="zh", + duration=2.5, + ) + + with tempfile.TemporaryDirectory() as tmp: + account_dir = Path(tmp) + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model="small", language="zh"), + model_loader=lambda _config: TraditionalModel(), + ) + with patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ): + first = service.transcribe_voice(account_dir=account_dir, server_id=99, voice_data=b"SILK") + second = service.transcribe_voice(account_dir=account_dir, server_id=99, voice_data=b"SILK") + + self.assertEqual(normalize_transcript_text("這是一段繁體中文,後臺服務已經啟動。"), "这是一段繁体中文,后台服务已经启动。") + self.assertEqual(first["text"], "这是一段繁体中文,后台服务已经启动。") + self.assertEqual(second["text"], first["text"]) + self.assertTrue(second["cached"]) + + def test_opencc_t2s_conversion_uses_character_level_rules(self): + self.assertEqual(normalize_transcript_text("繁體中文"), "繁体中文") + self.assertEqual(normalize_transcript_text("軟體與資料庫"), "软体与资料库") + + def test_old_cache_is_normalized_and_migrated_on_read(self): + with tempfile.TemporaryDirectory() as tmp: + account_dir = Path(tmp) + cache_path = account_dir / "_cache" / "voice_transcripts.sqlite3" + cache_path.parent.mkdir(parents=True) + conn = sqlite3.connect(str(cache_path)) + try: + conn.execute( + "CREATE TABLE transcript (" + "server_id INTEGER NOT NULL, source_hash TEXT NOT NULL, model TEXT NOT NULL, " + "language TEXT NOT NULL, text TEXT NOT NULL, detected_language TEXT NOT NULL, " + "duration REAL NOT NULL, updated_at REAL NOT NULL, " + "PRIMARY KEY (server_id, source_hash, model, language))" + ) + conn.execute( + "INSERT INTO transcript VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (99, "old-hash", "small", "zh", "後臺服務已經啟動。", "zh", 1.5, 1.0), + ) + conn.commit() + finally: + conn.close() + + service = VoiceTranscriptionService(VoiceTranscriptionConfig(model="small", language="zh")) + restored = service.lookup_cached_transcripts(account_dir, [99]) + cached = service._read_cache(account_dir, 99, "old-hash") + + conn = sqlite3.connect(str(cache_path)) + try: + columns = {row[1] for row in conn.execute("PRAGMA table_info(transcript)")} + stored_text, stored_version = conn.execute( + "SELECT text, text_version FROM transcript WHERE server_id = 99" + ).fetchone() + finally: + conn.close() + + self.assertEqual(restored[99]["text"], "后台服务已经启动。") + self.assertEqual(cached["text"], "后台服务已经启动。") + self.assertIn("text_version", columns) + self.assertEqual(stored_text, cached["text"]) + self.assertEqual(stored_version, TRANSCRIPT_TEXT_VERSION) + + def test_force_retranscribes_cached_voice(self): + with tempfile.TemporaryDirectory() as tmp: + account_dir = Path(tmp) + fake_model = _FakeModel() + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model="small"), + model_loader=lambda _config: fake_model, + ) + with patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ): + service.transcribe_voice(account_dir=account_dir, server_id=99, voice_data=b"SILK") + result = service.transcribe_voice(account_dir=account_dir, server_id=99, voice_data=b"SILK", force=True) + + self.assertFalse(result["cached"]) + self.assertEqual(fake_model.calls, 2) + + def test_raw_silk_is_rejected_when_decode_fails(self): + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model="small"), + model_loader=lambda _config: _FakeModel(), + ) + with tempfile.TemporaryDirectory() as tmp, patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"SILK", "silk", "audio/silk"), + ): + with self.assertRaises(VoiceTranscriptionError) as cm: + service.transcribe_voice(account_dir=Path(tmp), server_id=99, voice_data=b"SILK") + + self.assertEqual(cm.exception.code, "voice_decode_failed") + + def test_disabled_service_does_not_load_model(self): + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(enabled=False), + model_loader=lambda _config: self.fail("model must not load"), + ) + with tempfile.TemporaryDirectory() as tmp: + with self.assertRaises(VoiceTranscriptionError) as cm: + service.transcribe_voice(account_dir=Path(tmp), server_id=99, voice_data=b"SILK") + self.assertEqual(cm.exception.code, "disabled") + + def test_cache_write_failure_does_not_discard_transcript(self): + with tempfile.TemporaryDirectory() as tmp: + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model="small"), + model_loader=lambda _config: _FakeModel(), + ) + with patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ), patch.object(service, "_write_cache", side_effect=OSError("read only")): + result = service.transcribe_voice( + account_dir=Path(tmp), + server_id=99, + voice_data=b"SILK", + ) + self.assertEqual(result["text"], "你好世界") + + def test_public_result_does_not_expose_local_model_path(self): + with tempfile.TemporaryDirectory() as tmp: + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(model=r"D:\models\faster-whisper-small"), + model_loader=lambda _config: _FakeModel(), + ) + with patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ): + result = service.transcribe_voice( + account_dir=Path(tmp), + server_id=99, + voice_data=b"SILK", + ) + self.assertEqual(result["model"], "faster-whisper-small") + + def test_cuda_initialization_failure_falls_back_to_cpu(self): + with tempfile.TemporaryDirectory() as tmp: + calls = [] + fake_model = _FakeModel() + + def model_loader(config): + calls.append((config.device, config.compute_type)) + if config.device == "cuda": + raise RuntimeError("CUDA initialization failed") + return fake_model + + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(device="cuda", compute_type="float16"), + model_loader=model_loader, + ) + cuda_report = {"available": True, "deviceCount": 1, "devices": [], "reason": ""} + with patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ), patch("wechat_decrypt_tool.voice_transcription.probe_cuda", return_value=cuda_report): + result = service.transcribe_voice( + account_dir=Path(tmp), + server_id=99, + voice_data=b"SILK", + ) + status = service.status() + + self.assertEqual(calls, [("cuda", "float16"), ("cpu", "int8")]) + self.assertEqual(result["device"], "cpu") + self.assertEqual(result["computeType"], "int8") + self.assertTrue(status["usingFallback"]) + self.assertEqual(status["activeDevice"], "cpu") + + def test_cuda_first_inference_failure_falls_back_to_cpu_once(self): + class CudaModel: + calls = 0 + + def transcribe(self, _path, **_kwargs): + self.calls += 1 + + def fail_during_iteration(): + raise RuntimeError("cuDNN CUDA initialization failed") + yield None + + return fail_during_iteration(), SimpleNamespace(language="zh", duration=2.5) + + cuda_model = CudaModel() + cpu_model = _FakeModel() + loads = [] + + def model_loader(config): + loads.append((config.device, config.compute_type)) + return cuda_model if config.device == "cuda" else cpu_model + + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(device="cuda", compute_type="float16"), + model_loader=model_loader, + ) + cuda_report = {"available": True, "deviceCount": 1, "devices": [], "reason": ""} + with tempfile.TemporaryDirectory() as tmp, patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ), patch("wechat_decrypt_tool.voice_transcription.probe_cuda", return_value=cuda_report): + result = service.transcribe_voice(account_dir=Path(tmp), server_id=99, voice_data=b"SILK") + + self.assertEqual(loads, [("cuda", "float16"), ("cpu", "int8")]) + self.assertEqual(cuda_model.calls, 1) + self.assertEqual(cpu_model.calls, 1) + self.assertEqual(result["device"], "cpu") + self.assertEqual(result["text"], "你好世界") + + def test_non_cuda_inference_error_does_not_trigger_cpu_fallback(self): + class InvalidAudioModel: + def transcribe(self, _path, **_kwargs): + raise ValueError("invalid audio frame") + + loads = [] + + def model_loader(config): + loads.append(config.device) + return InvalidAudioModel() + + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(device="cuda", compute_type="float16"), + model_loader=model_loader, + ) + cuda_report = {"available": True, "deviceCount": 1, "devices": [], "reason": ""} + with tempfile.TemporaryDirectory() as tmp, patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ), patch("wechat_decrypt_tool.voice_transcription.probe_cuda", return_value=cuda_report): + with self.assertRaises(VoiceTranscriptionError) as cm: + service.transcribe_voice(account_dir=Path(tmp), server_id=99, voice_data=b"SILK") + + self.assertEqual(cm.exception.code, "transcription_failed") + self.assertEqual(loads, ["cuda"]) + + def test_cuda_probe_reports_available_nvidia_device(self): + fake_ctranslate2 = SimpleNamespace(get_cuda_device_count=lambda: 1) + invalidate_cuda_probe_cache() + try: + with patch.dict(sys.modules, {"ctranslate2": fake_ctranslate2}), patch( + "wechat_decrypt_tool.voice_transcription._read_nvidia_smi_devices", + return_value=[{"name": "NVIDIA GeForce RTX 5060", "driverVersion": "570.0", "memoryTotal": "8192 MiB"}], + ): + report = probe_cuda() + finally: + invalidate_cuda_probe_cache() + + self.assertTrue(report["available"]) + self.assertEqual(report["deviceCount"], 1) + self.assertEqual(report["devices"][0]["name"], "NVIDIA GeForce RTX 5060") + + def test_cuda_probe_uses_short_lived_cache(self): + fake_ctranslate2 = SimpleNamespace(get_cuda_device_count=lambda: 1) + invalidate_cuda_probe_cache() + try: + with patch.dict(sys.modules, {"ctranslate2": fake_ctranslate2}), patch( + "wechat_decrypt_tool.voice_transcription._read_nvidia_smi_devices", + return_value=[], + ) as nvidia_smi: + first = probe_cuda() + second = probe_cuda() + finally: + invalidate_cuda_probe_cache() + + self.assertTrue(first["available"]) + self.assertEqual(second, first) + nvidia_smi.assert_called_once_with() + + def test_model_load_error_can_be_retried(self): + with tempfile.TemporaryDirectory() as tmp: + fake_model = _FakeModel() + attempts = 0 + + def model_loader(_config): + nonlocal attempts + attempts += 1 + if attempts == 1: + raise RuntimeError("temporary failure") + return fake_model + + service = VoiceTranscriptionService( + VoiceTranscriptionConfig(), + model_loader=model_loader, + ) + with patch( + "wechat_decrypt_tool.voice_transcription._convert_silk_to_browser_audio", + return_value=(b"RIFF-WAV", "wav", "audio/wav"), + ): + with self.assertRaises(VoiceTranscriptionError) as cm: + service.transcribe_voice(account_dir=Path(tmp), server_id=99, voice_data=b"SILK") + result = service.transcribe_voice(account_dir=Path(tmp), server_id=99, voice_data=b"SILK") + + self.assertEqual(cm.exception.code, "model_load_failed") + self.assertEqual(attempts, 2) + self.assertEqual(result["text"], "你好世界") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_voice_transcription_contract.py b/tests/test_voice_transcription_contract.py new file mode 100644 index 00000000..11b65576 --- /dev/null +++ b/tests/test_voice_transcription_contract.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class TestVoiceTranscriptionContract(unittest.TestCase): + def test_chat_api_exposes_status_and_transcription_endpoints(self): + source = (ROOT / "src" / "wechat_decrypt_tool" / "routers" / "chat_media.py").read_text(encoding="utf-8") + self.assertIn('/api/chat/media/voice/transcription/status', source) + self.assertIn('/api/chat/media/voice/transcription', source) + self.assertIn('/api/chat/media/voice/transcription/settings', source) + self.assertIn('set_voice_transcription_device', source) + self.assertIn('await asyncio.to_thread(', source) + + def test_settings_ui_exposes_cpu_and_nvidia_gpu_selection(self): + settings = (ROOT / "frontend" / "components" / "SettingsDialog.vue").read_text(encoding="utf-8") + api = (ROOT / "frontend" / "composables" / "useApi.js").read_text(encoding="utf-8") + self.assertIn("语音转文字", settings) + self.assertIn("NVIDIA GPU", settings) + self.assertIn("setVoiceTranscriptionDevice", settings) + self.assertIn("voiceFallbackReason", settings) + self.assertIn("/chat/media/voice/transcription/settings", api) + + def test_chat_ui_keeps_audio_and_adds_transcription_states(self): + content = (ROOT / "frontend" / "components" / "chat" / "MessageContent.vue").read_text(encoding="utf-8") + messages = (ROOT / "frontend" / "composables" / "chat" / "useChatMessages.js").read_text(encoding="utf-8") + self.assertIn(':src="message.voiceUrl"', content) + self.assertIn("message.voiceTranscriptStatus === 'loading'", content) + self.assertIn("message.voiceTranscriptStatus === 'success'", content) + self.assertIn("transcribeVoice(message, { force: true })", content) + self.assertIn("const transcribeVoice = async", messages) + self.assertIn("api.transcribeChatVoice", messages) + + def test_export_option_is_wired_from_dialog_to_backend(self): + dialog = (ROOT / "frontend" / "components" / "chat" / "ChatExportDialog.vue").read_text(encoding="utf-8") + export_state = (ROOT / "frontend" / "composables" / "chat" / "useChatExport.js").read_text(encoding="utf-8") + api = (ROOT / "frontend" / "composables" / "useApi.js").read_text(encoding="utf-8") + router = (ROOT / "src" / "wechat_decrypt_tool" / "routers" / "chat_export.py").read_text(encoding="utf-8") + service = (ROOT / "src" / "wechat_decrypt_tool" / "chat_export_service.py").read_text(encoding="utf-8") + + self.assertIn('v-model="exportTranscribeVoice"', dialog) + self.assertIn("selectedTypeSet.has('voice')", export_state) + self.assertIn("transcribe_voice:", export_state) + self.assertIn("transcribe_voice: !!data.transcribe_voice", api) + self.assertIn("transcribe_voice: bool = Field(False", router) + self.assertGreaterEqual(service.count("_attach_voice_transcript("), 4) + self.assertIn('"transcribeVoice": transcribe_voice', service) + + def test_privacy_mode_disables_export_transcription(self): + service = (ROOT / "src" / "wechat_decrypt_tool" / "chat_export_service.py").read_text(encoding="utf-8") + export_state = (ROOT / "frontend" / "composables" / "chat" / "useChatExport.js").read_text(encoding="utf-8") + self.assertIn('bool(opts.get("transcribeVoice")) and not privacy_mode', service) + self.assertIn("!privacyMode.value", export_state) + + def test_desktop_backend_build_includes_optional_runtime(self): + package = (ROOT / "desktop" / "package.json").read_text(encoding="utf-8") + build = (ROOT / "desktop" / "scripts" / "build-backend.cjs").read_text(encoding="utf-8") + pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + self.assertIn("--no-editable --extra build --extra voice-transcription", package) + self.assertIn('"faster_whisper"', build) + self.assertIn('"ctranslate2"', build) + self.assertIn('"av"', build) + self.assertIn('"opencc"', build) + self.assertIn('"--smoke-opencc"', build) + self.assertIn('"wce_integrity.pyd"', build) + self.assertNotIn("windowsNativeCandidates", build) + self.assertIn('"Scripts/pyinstaller.exe"', build) + self.assertIn("spawnSync(pyInstallerExecutable", build) + self.assertIn('"Scripts/python.exe"', build) + self.assertIn("const integrityPreflight = spawnSync(\n venvPythonExecutable", build) + self.assertNotIn('spawnSync(\n "uv"', build) + self.assertIn('"src/wechat_decrypt_tool/native/*.pyd"', pyproject) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_voice_transcription_settings.py b/tests/test_voice_transcription_settings.py new file mode 100644 index 00000000..7f649546 --- /dev/null +++ b/tests/test_voice_transcription_settings.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import importlib +import logging +import os +import sys +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace +from unittest.mock import Mock, patch + +from fastapi import FastAPI +from fastapi.testclient import TestClient + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + + +def _close_logging_handlers() -> None: + for logger_name in ("", "uvicorn", "uvicorn.access", "uvicorn.error", "fastapi"): + logger = logging.getLogger(logger_name) + for handler in logger.handlers[:]: + try: + handler.close() + except Exception: + pass + try: + logger.removeHandler(handler) + except Exception: + pass + + +class TestVoiceTranscriptionSettings(unittest.TestCase): + def setUp(self) -> None: + self._tmp = TemporaryDirectory() + self._previous_data_dir = os.environ.get("WECHAT_TOOL_DATA_DIR") + self._previous_device = os.environ.get("WECHAT_TOOL_WHISPER_DEVICE") + os.environ["WECHAT_TOOL_DATA_DIR"] = self._tmp.name + os.environ.pop("WECHAT_TOOL_WHISPER_DEVICE", None) + + import wechat_decrypt_tool.app_paths as app_paths + import wechat_decrypt_tool.runtime_settings as runtime_settings + import wechat_decrypt_tool.voice_transcription as voice_transcription + import wechat_decrypt_tool.routers.chat_export as chat_export + import wechat_decrypt_tool.routers.chat_media as chat_media + + importlib.reload(app_paths) + importlib.reload(runtime_settings) + importlib.reload(voice_transcription) + importlib.reload(chat_media) + importlib.reload(chat_export) + + self.runtime_settings = runtime_settings + self.voice_transcription = voice_transcription + self.chat_media = chat_media + self.chat_export = chat_export + + def tearDown(self) -> None: + _close_logging_handlers() + + if self._previous_data_dir is None: + os.environ.pop("WECHAT_TOOL_DATA_DIR", None) + else: + os.environ["WECHAT_TOOL_DATA_DIR"] = self._previous_data_dir + + if self._previous_device is None: + os.environ.pop("WECHAT_TOOL_WHISPER_DEVICE", None) + else: + os.environ["WECHAT_TOOL_WHISPER_DEVICE"] = self._previous_device + self._tmp.cleanup() + + def test_runtime_setting_is_used_when_environment_is_not_set(self): + self.runtime_settings.write_voice_transcription_device_setting("cuda") + + device, source = self.runtime_settings.read_effective_voice_transcription_device() + config = self.voice_transcription.VoiceTranscriptionConfig.from_env() + + self.assertEqual((device, source), ("cuda", "settings")) + self.assertEqual(config.device, "cuda") + self.assertEqual(config.compute_type, "float16") + self.assertEqual(config.device_source, "settings") + + def test_default_model_is_medium(self): + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("WECHAT_TOOL_WHISPER_MODEL", None) + config = self.voice_transcription.VoiceTranscriptionConfig.from_env() + + self.assertEqual(config.model, "medium") + + def test_environment_device_takes_precedence_over_saved_setting(self): + self.runtime_settings.write_voice_transcription_device_setting("cuda") + os.environ["WECHAT_TOOL_WHISPER_DEVICE"] = "cpu" + + device, source = self.runtime_settings.read_effective_voice_transcription_device() + + self.assertEqual((device, source), ("cpu", "env")) + + def test_setting_api_returns_updated_configuration(self): + app = FastAPI() + app.include_router(self.chat_media.router) + configuration = { + "requestedDevice": "cuda", + "deviceSource": "settings", + "cuda": {"available": True, "deviceCount": 1, "devices": [], "reason": ""}, + } + with patch.object(self.chat_media, "set_voice_transcription_device", return_value=configuration) as setter: + client = TestClient(app) + response = client.put("/api/chat/media/voice/transcription/settings", json={"device": "cuda"}) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["configuration"], configuration) + setter.assert_called_once_with("cuda") + + def test_setting_api_maps_locked_environment_to_conflict(self): + app = FastAPI() + app.include_router(self.chat_media.router) + with patch.object( + self.chat_media, + "set_voice_transcription_device", + side_effect=self.voice_transcription.VoiceTranscriptionError("device_locked", "设备已锁定"), + ): + client = TestClient(app) + response = client.put("/api/chat/media/voice/transcription/settings", json={"device": "cuda"}) + + self.assertEqual(response.status_code, 409) + self.assertEqual(response.json()["detail"]["code"], "device_locked") + + def test_status_api_executes_service_status(self): + app = FastAPI() + app.include_router(self.chat_media.router) + status = { + "available": False, + "modelReady": False, + "allowDownload": False, + "reason": "Whisper 模型尚未下载到本机缓存。 当前已禁止自动下载。", + } + service = SimpleNamespace(status=Mock(return_value=status)) + with patch.object(self.chat_media, "get_voice_transcription_service", return_value=service): + response = TestClient(app).get("/api/chat/media/voice/transcription/status") + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), status) + service.status.assert_called_once_with() + + def test_transcription_api_blocks_missing_model_before_reading_voice(self): + app = FastAPI() + app.include_router(self.chat_media.router) + service = SimpleNamespace( + ensure_available=Mock(side_effect=self.voice_transcription.VoiceTranscriptionError( + "model_not_ready", "模型未准备好" + )), + transcribe_voice=Mock(), + ) + with patch.object(self.chat_media, "get_voice_transcription_service", return_value=service): + response = TestClient(app).post( + "/api/chat/media/voice/transcription", + json={"account": "test", "server_id": "123", "force": False}, + ) + + self.assertEqual(response.status_code, 503) + self.assertEqual(response.json()["detail"]["code"], "model_not_ready") + service.transcribe_voice.assert_not_called() + + def test_export_api_blocks_missing_model_before_creating_job(self): + app = FastAPI() + app.include_router(self.chat_export.router) + service = SimpleNamespace( + ensure_available=Mock(side_effect=self.voice_transcription.VoiceTranscriptionError( + "model_not_ready", "模型未准备好" + )) + ) + with patch.object(self.chat_export, "get_voice_transcription_service", return_value=service), patch.object( + self.chat_export.CHAT_EXPORT_MANAGER, "create_job" + ) as create_job: + response = TestClient(app).post("/api/chat/exports", json={"transcribe_voice": True}) + + self.assertEqual(response.status_code, 503) + self.assertEqual(response.json()["detail"]["code"], "model_not_ready") + create_job.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wrapped_cyber_schedule_night_companion.py b/tests/test_wrapped_cyber_schedule_night_companion.py new file mode 100644 index 00000000..2fe58ed8 --- /dev/null +++ b/tests/test_wrapped_cyber_schedule_night_companion.py @@ -0,0 +1,317 @@ +import json +import sqlite3 +import unittest +from datetime import datetime +from pathlib import Path +from tempfile import TemporaryDirectory +import sys + +# Ensure "src/" is importable when running tests from repo root. +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + + +class TestWrappedCyberScheduleNightCompanion(unittest.TestCase): + def _ts(self, y: int, m: int, d: int, hh: int, mm: int, ss: int = 0) -> int: + return int(datetime(y, m, d, hh, mm, ss).timestamp()) + + def _seed_contact_db(self, path: Path, usernames: list[str]) -> None: + conn = sqlite3.connect(str(path)) + try: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS contact ( + username TEXT PRIMARY KEY, + remark TEXT, + nick_name TEXT, + alias TEXT, + big_head_url TEXT, + small_head_url TEXT + ) + """ + ) + for u in usernames: + conn.execute( + "INSERT INTO contact(username, nick_name) VALUES(?, ?)", + (u, f"Nick_{u}"), + ) + conn.commit() + finally: + conn.close() + + def _seed_index_db(self, path: Path, rows: list[dict], *, with_payload: bool = True) -> None: + # 模拟真实索引:text 列存逐字符分词文本,原文保留在 payload_json(旧索引无该列)。 + from wechat_decrypt_tool.chat_helpers import _to_char_token_text + + conn = sqlite3.connect(str(path)) + try: + payload_col = ",\n payload_json TEXT" if with_payload else "" + conn.execute( + f""" + CREATE TABLE IF NOT EXISTS message_fts ( + text TEXT, + username TEXT, + sender_username TEXT, + create_time INTEGER, + sort_seq INTEGER, + local_id INTEGER, + local_type INTEGER, + db_stem TEXT, + table_name TEXT{payload_col} + ) + """ + ) + for r in rows: + original = str(r.get("text", "hi")) + values = [ + _to_char_token_text(original), + r["username"], + r["sender_username"], + int(r["create_time"]), + int(r.get("sort_seq", r["local_id"])), + int(r["local_id"]), + int(r.get("local_type", 1)), + str(r.get("db_stem", "message_0")), + str(r.get("table_name", "msg_abc")), + ] + cols = ( + "text, username, sender_username, create_time, sort_seq, " + "local_id, local_type, db_stem, table_name" + ) + if with_payload: + cols += ", payload_json" + values.append(json.dumps({"content": original}, ensure_ascii=False)) + placeholders = ", ".join("?" for _ in values) + conn.execute(f"INSERT INTO message_fts({cols}) VALUES({placeholders})", values) + conn.commit() + finally: + conn.close() + + def _row(self, username: str, sender: str, t: int, lid: int, **kw) -> dict: + return { + "username": username, + "sender_username": sender, + "create_time": t, + "local_id": lid, + **kw, + } + + def test_aggregation_excludes_group_biz_and_system_messages(self): + from wechat_decrypt_tool.wrapped.cards.card_01_cyber_schedule import _compute_night_companion + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + + friend_a = "wxid_friend_a" + friend_b = "wxid_friend_b" + group = "12345678@chatroom" + self._seed_contact_db(account_dir / "contact.db", [friend_a, friend_b]) + + rows: list[dict] = [] + lid = 1 + + # friend_a 深夜双向消息 6 条:4 条对方发来 + 2 条本人发出。 + for hh, mm in [(0, 30), (1, 10), (2, 5), (3, 40)]: + rows.append(self._row(friend_a, friend_a, self._ts(2025, 3, 8, hh, mm), lid)) + lid += 1 + for hh, mm in [(1, 20), (2, 15)]: + rows.append(self._row(friend_a, account, self._ts(2025, 3, 9, hh, mm), lid)) + lid += 1 + + # friend_b 深夜双向消息 2 条:1 条对方 + 1 条本人。 + rows.append(self._row(friend_b, friend_b, self._ts(2025, 5, 1, 0, 45), lid)) + lid += 1 + rows.append(self._row(friend_b, account, self._ts(2025, 5, 1, 0, 50), lid)) + lid += 1 + + # 应排除:群聊、biz 分片、系统消息、白天消息、非目标年份。 + rows.append(self._row(group, friend_a, self._ts(2025, 3, 8, 1, 0), lid)) + lid += 1 + rows.append(self._row(friend_a, friend_a, self._ts(2025, 3, 8, 2, 0), lid, db_stem="biz_message_0")) + lid += 1 + rows.append(self._row(friend_a, friend_a, self._ts(2025, 3, 8, 3, 0), lid, local_type=10000)) + lid += 1 + rows.append(self._row(friend_a, friend_a, self._ts(2025, 3, 8, 12, 0), lid)) + lid += 1 + rows.append(self._row(friend_a, friend_a, self._ts(2024, 3, 8, 1, 0), lid)) + lid += 1 + + self._seed_index_db(account_dir / "chat_search_index.db", rows) + + data = _compute_night_companion(account_dir=account_dir, year=2025, my_username=account) + self.assertEqual(data["nightMessagesTotal"], 8) + self.assertEqual(data["myNightMessages"], 3) + + partner = data["partner"] + self.assertIsNotNone(partner) + self.assertEqual(partner["username"], friend_a) + self.assertEqual(partner["nightMessages"], 6) + self.assertAlmostEqual(partner["sharePct"], 75.0) + self.assertEqual(partner["displayName"], f"Nick_{friend_a}") + # 打码:首尾保留,中间打星。 + self.assertTrue(partner["maskedName"].startswith("N")) + self.assertIn("*", partner["maskedName"]) + self.assertTrue(partner["avatarUrl"]) + + def test_latest_moment_uses_late_night_scoring(self): + from wechat_decrypt_tool.wrapped.cards.card_01_cyber_schedule import _compute_night_companion + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + + friend = "wxid_night_friend" + self._seed_contact_db(account_dir / "contact.db", [friend]) + + rows = [ + # 5:30 的消息按 +24h 计分反而排最前(最接近清晨),不应被选中。 + self._row(friend, friend, self._ts(2025, 7, 2, 5, 30), 1, text="快天亮了"), + self._row(friend, friend, self._ts(2025, 7, 2, 0, 20), 2, text="刚过零点"), + # 3:45 本人发出,+24h 计分最大,应为“最晚一刻”。 + self._row(friend, account, self._ts(2025, 7, 3, 3, 45), 3, text="还没睡"), + ] + self._seed_index_db(account_dir / "chat_search_index.db", rows) + + data = _compute_night_companion(account_dir=account_dir, year=2025, my_username=account) + moment = data["latestMoment"] + self.assertIsNotNone(moment) + self.assertEqual(moment["time"], "03:45") + self.assertEqual(moment["direction"], "sent") + self.assertEqual(moment["content"], "还没睡") + self.assertEqual(moment["date"], "2025-07-03") + + def test_latest_moment_content_truncated_to_60(self): + from wechat_decrypt_tool.wrapped.cards.card_01_cyber_schedule import _compute_night_companion + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + + friend = "wxid_talkative" + self._seed_contact_db(account_dir / "contact.db", [friend]) + + long_text = "夜" * 70 + rows = [self._row(friend, friend, self._ts(2025, 1, 5, 2, 0), 1, text=long_text)] + self._seed_index_db(account_dir / "chat_search_index.db", rows) + + data = _compute_night_companion(account_dir=account_dir, year=2025, my_username=account) + moment = data["latestMoment"] + self.assertIsNotNone(moment) + self.assertEqual(moment["content"], "夜" * 57 + "...") + self.assertEqual(len(moment["content"]), 60) + self.assertEqual(moment["direction"], "received") + + def test_latest_moment_readable_on_legacy_index_without_payload(self): + # 旧索引没有 payload_json 列:分词文本应去空格还原(小写/空格损失可接受),不得逐字带空格展示。 + from wechat_decrypt_tool.wrapped.cards.card_01_cyber_schedule import _compute_night_companion + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + + friend = "wxid_legacy_friend" + self._seed_contact_db(account_dir / "contact.db", [friend]) + + rows = [self._row(friend, friend, self._ts(2025, 6, 1, 2, 0), 1, text="晚安 Good Night")] + self._seed_index_db(account_dir / "chat_search_index.db", rows, with_payload=False) + + data = _compute_night_companion(account_dir=account_dir, year=2025, my_username=account) + moment = data["latestMoment"] + self.assertIsNotNone(moment) + self.assertEqual(moment["content"], "晚安goodnight") + + def test_excludes_official_and_service_sessions(self): + # 公众号/企业微信/服务号会话不得成为守夜人,也不得进入 sharePct 分母。 + from wechat_decrypt_tool.wrapped.cards.card_01_cyber_schedule import _compute_night_companion + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + + friend = "wxid_real_friend" + self._seed_contact_db(account_dir / "contact.db", [friend]) + + rows: list[dict] = [] + lid = 1 + for hh, mm in [(1, 0), (2, 0)]: + rows.append(self._row(friend, friend, self._ts(2025, 9, 9, hh, mm), lid)) + lid += 1 + # 企业微信联系人 5 条、服务推送 3 条、公众号 2 条,全部应被排除。 + for hh in range(5): + rows.append(self._row("1688850001@openim", "1688850001@openim", self._ts(2025, 9, 10, 1, hh), lid)) + lid += 1 + for hh in range(3): + rows.append(self._row("notifymessage", "notifymessage", self._ts(2025, 9, 11, 2, hh), lid)) + lid += 1 + for hh in range(2): + rows.append(self._row("gh_news123", "gh_news123", self._ts(2025, 9, 12, 3, hh), lid)) + lid += 1 + + self._seed_index_db(account_dir / "chat_search_index.db", rows) + + data = _compute_night_companion(account_dir=account_dir, year=2025, my_username=account) + self.assertEqual(data["nightMessagesTotal"], 2) + partner = data["partner"] + self.assertIsNotNone(partner) + self.assertEqual(partner["username"], friend) + self.assertAlmostEqual(partner["sharePct"], 100.0) + + def test_no_night_messages_returns_zero_shape(self): + from wechat_decrypt_tool.wrapped.cards.card_01_cyber_schedule import _compute_night_companion + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + + friend = "wxid_day_friend" + self._seed_contact_db(account_dir / "contact.db", [friend]) + + # 只有白天消息。 + rows = [ + self._row(friend, friend, self._ts(2025, 4, 1, 9, 0), 1), + self._row(friend, account, self._ts(2025, 4, 1, 21, 30), 2), + ] + self._seed_index_db(account_dir / "chat_search_index.db", rows) + + data = _compute_night_companion(account_dir=account_dir, year=2025, my_username=account) + self.assertEqual(data["nightMessagesTotal"], 0) + self.assertEqual(data["myNightMessages"], 0) + self.assertIsNone(data["partner"]) + self.assertIsNone(data["latestMoment"]) + + def test_card_payload_contains_night_companion(self): + from wechat_decrypt_tool.wrapped.cards.card_01_cyber_schedule import build_card_01_cyber_schedule + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + + friend = "wxid_friend" + self._seed_contact_db(account_dir / "contact.db", [friend]) + + rows = [ + self._row(friend, friend, self._ts(2025, 2, 2, 1, 0), 1), + self._row(friend, account, self._ts(2025, 2, 2, 1, 5), 2), + self._row(friend, account, self._ts(2025, 2, 2, 14, 0), 3), + ] + self._seed_index_db(account_dir / "chat_search_index.db", rows) + + card = build_card_01_cyber_schedule(account_dir=account_dir, year=2025) + self.assertEqual(card["id"], 1) + nc = card["data"]["nightCompanion"] + self.assertEqual(nc["nightMessagesTotal"], 2) + self.assertEqual(nc["myNightMessages"], 1) + self.assertEqual(nc["partner"]["username"], friend) + self.assertAlmostEqual(nc["partner"]["sharePct"], 100.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wrapped_global_overview_peak_day.py b/tests/test_wrapped_global_overview_peak_day.py new file mode 100644 index 00000000..b5dabb82 --- /dev/null +++ b/tests/test_wrapped_global_overview_peak_day.py @@ -0,0 +1,215 @@ +import json +import sqlite3 +import unittest +from datetime import datetime +from pathlib import Path +from tempfile import TemporaryDirectory +import sys + +# Ensure "src/" is importable when running tests from repo root. +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + + +def _char_tokens(s: str) -> str: + # Mirror chat_helpers._to_char_token_text: lowercased chars joined by spaces. + return " ".join(ch for ch in str(s).lower() if not ch.isspace()) + + +class TestWrappedGlobalOverviewPeakDay(unittest.TestCase): + def _ts(self, y: int, m: int, d: int, hh: int, mm: int, ss: int = 0) -> int: + return int(datetime(y, m, d, hh, mm, ss).timestamp()) + + def _seed_contact_db(self, path: Path, usernames: list[str]) -> None: + conn = sqlite3.connect(str(path)) + try: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS contact ( + username TEXT PRIMARY KEY, + remark TEXT, + nick_name TEXT, + alias TEXT, + big_head_url TEXT, + small_head_url TEXT + ) + """ + ) + for u in usernames: + conn.execute( + "INSERT INTO contact(username, nick_name) VALUES(?, ?)", + (u, f"Nick_{u}"), + ) + conn.commit() + finally: + conn.close() + + def _seed_index_db(self, path: Path, rows: list[dict]) -> None: + conn = sqlite3.connect(str(path)) + try: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS message_fts ( + text TEXT, + username TEXT, + render_type TEXT, + create_time INTEGER, + sort_seq INTEGER, + local_id INTEGER, + server_id INTEGER, + local_type INTEGER, + db_stem TEXT, + table_name TEXT, + sender_username TEXT, + is_hidden INTEGER, + is_official INTEGER, + payload_json TEXT + ) + """ + ) + for r in rows: + conn.execute( + """ + INSERT INTO message_fts( + text, username, render_type, create_time, sort_seq, local_id, + server_id, local_type, db_stem, table_name, sender_username, + is_hidden, is_official, payload_json + ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + r.get("text", ""), + r["username"], + r.get("render_type", "text"), + int(r["create_time"]), + int(r.get("sort_seq", r.get("local_id", 0))), + int(r.get("local_id", 0)), + int(r.get("server_id", 0)), + int(r.get("local_type", 1)), + str(r.get("db_stem", "message_0")), + str(r.get("table_name", "msg_abc")), + r["sender_username"], + 0, + 0, + r.get("payload_json"), + ), + ) + conn.commit() + finally: + conn.close() + + def test_peak_day_payload_and_highlights(self): + from wechat_decrypt_tool.wrapped.cards.card_00_global_overview import ( + build_card_00_global_overview, + ) + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + + friend = "wxid_friend" + group = "12345@chatroom" + official = "gh_service" + self._seed_contact_db(account_dir / "contact.db", [friend, group]) + + rows: list[dict] = [] + lid = 1 + + def add(username: str, sender: str, ts: int, text: str = "hi", payload: bool = True) -> None: + nonlocal lid + rows.append( + { + "username": username, + "sender_username": sender, + "create_time": ts, + "local_id": lid, + "sort_seq": lid, + "text": _char_tokens(text), + "payload_json": json.dumps({"content": text}, ensure_ascii=False) if payload else None, + } + ) + lid += 1 + + # 普通日:2025-01-10 / 2025-02-01 各发 1 条。 + add(friend, account, self._ts(2025, 1, 10, 12, 0), "普通日消息") + add(friend, account, self._ts(2025, 2, 1, 12, 0), "普通日消息") + + # 峰值日 2025-03-05(周三,doy0=63):本人发 6 条。 + long_first = "好" * 80 + add(group, account, self._ts(2025, 3, 5, 8, 15), long_first) + add(group, account, self._ts(2025, 3, 5, 10, 0), "上午继续聊") + add(friend, account, self._ts(2025, 3, 5, 12, 0), "中午的消息") + add(group, account, self._ts(2025, 3, 5, 15, 0), "下午的消息") + add(friend, account, self._ts(2025, 3, 5, 20, 0), "晚上的消息") + # 末条文本消息缺 payload_json:应回退到去空格拼接 token text。 + add(group, account, self._ts(2025, 3, 5, 23, 30), "晚安啦", payload=False) + + # 峰值日他人发来的消息:群聊 3 条、好友 1 条 -> 群聊全天 7 条为当日主角。 + add(group, "wxid_other1", self._ts(2025, 3, 5, 9, 0), "群友消息1") + add(group, "wxid_other2", self._ts(2025, 3, 5, 9, 5), "群友消息2") + add(group, "wxid_other3", self._ts(2025, 3, 5, 9, 10), "群友消息3") + add(friend, friend, self._ts(2025, 3, 5, 12, 5), "好友回复") + + # 公众号消息更多,但必须被会话过滤规则排除,不能成为当日主角。 + for i in range(20): + add(official, official, self._ts(2025, 3, 5, 11, i), f"推送{i}") + + self._seed_index_db(account_dir / "chat_search_index.db", rows) + + card = build_card_00_global_overview(account_dir=account_dir, year=2025) + data = card["data"] + + peak = data["peakDay"] + self.assertIsNotNone(peak) + self.assertEqual(peak["date"], "2025-03-05") + self.assertEqual(peak["weekdayName"], "周三") + self.assertEqual(peak["count"], 6) + + # multiple = count / messagesPerDay(保留 1 位小数)。 + mpd = float(data["messagesPerDay"]) + self.assertGreater(mpd, 0) + self.assertAlmostEqual(peak["multiple"], round(6 / mpd, 1)) + + top = peak["topContact"] + self.assertIsNotNone(top) + self.assertEqual(top["username"], group) + self.assertTrue(top["isGroup"]) + self.assertEqual(top["messages"], 7) + self.assertEqual(top["displayName"], f"Nick_{group}") + self.assertTrue(top["maskedName"]) + self.assertTrue(top["avatarUrl"]) + + # 首条:超长文本截断到 60 字;末条:无 payload 时回退 token 拼接。 + self.assertEqual(peak["firstTime"], "08:15") + self.assertEqual(peak["firstText"], "好" * 60) + self.assertEqual(peak["lastTime"], "23:30") + self.assertEqual(peak["lastText"], "晚安啦") + + highlights = data["annualHeatmap"]["highlights"] + self.assertEqual(len(highlights), 1) + h = highlights[0] + self.assertEqual(h["key"], "sent_messages_max") + self.assertEqual(h["doy"], 63) + self.assertTrue(str(h["label"]).strip()) + self.assertIn("6", str(h["valueLabel"])) + + def test_peak_day_null_when_no_messages(self): + from wechat_decrypt_tool.wrapped.cards.card_00_global_overview import ( + build_card_00_global_overview, + ) + + with TemporaryDirectory() as td: + account = "wxid_me" + account_dir = Path(td) / account + account_dir.mkdir(parents=True, exist_ok=True) + self._seed_contact_db(account_dir / "contact.db", []) + self._seed_index_db(account_dir / "chat_search_index.db", []) + + card = build_card_00_global_overview(account_dir=account_dir, year=2025) + data = card["data"] + self.assertIsNone(data["peakDay"]) + self.assertEqual(data["annualHeatmap"]["highlights"], []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wrapped_message_chars_voice_calls.py b/tests/test_wrapped_message_chars_voice_calls.py new file mode 100644 index 00000000..2913f83c --- /dev/null +++ b/tests/test_wrapped_message_chars_voice_calls.py @@ -0,0 +1,486 @@ +import hashlib +import sqlite3 +import sys +import unittest +from datetime import datetime +from pathlib import Path +from tempfile import TemporaryDirectory + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + + +def _voice_xml(voicelength: str) -> str: + return f'' + + +def _voip_xml(*, room_type: str, msg: str) -> str: + return ( + '' + "42" + f"" + f"{room_type}" + "" + ) + + +class TestWrappedMessageCharsVoiceCalls(unittest.TestCase): + def _ts(self, y: int, m: int, d: int, h: int = 0, mi: int = 0, s: int = 0) -> int: + return int(datetime(y, m, d, h, mi, s).timestamp()) + + def _seed_contact_db(self, path: Path, *, account: str, usernames: list[str]) -> None: + conn = sqlite3.connect(str(path)) + try: + conn.execute( + """ + CREATE TABLE contact ( + username TEXT, + remark TEXT, + nick_name TEXT, + alias TEXT, + local_type INTEGER, + verify_flag INTEGER, + big_head_url TEXT, + small_head_url TEXT + ) + """ + ) + conn.execute( + "INSERT INTO contact VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (account, "", "我", "", 1, 0, "", ""), + ) + for idx, username in enumerate(usernames): + conn.execute( + "INSERT INTO contact VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (username, "", f"好友{idx + 1}", "", 1, 0, "", ""), + ) + conn.commit() + finally: + conn.close() + + def _seed_session_db(self, path: Path, *, usernames: list[str]) -> None: + conn = sqlite3.connect(str(path)) + try: + conn.execute( + """ + CREATE TABLE SessionTable ( + username TEXT, + is_hidden INTEGER, + sort_timestamp INTEGER + ) + """ + ) + for username in usernames: + conn.execute("INSERT INTO SessionTable VALUES (?, ?, ?)", (username, 0, 1735689600)) + conn.commit() + finally: + conn.close() + + def _seed_message_db( + self, + path: Path, + *, + account: str, + rows_by_username: dict[str, list[dict[str, object]]], + include_account_in_name2id: bool = True, + ) -> None: + """ + 为每个会话 username 建一张 msg_ 表。 + + 行内用 direction: "sent"/"received" 表达方向: + - sent -> real_sender_id = 1(account 在 Name2Id 的 rowid) + - received -> real_sender_id = 该会话好友的 rowid + """ + conn = sqlite3.connect(str(path)) + try: + conn.execute("CREATE TABLE Name2Id (rowid INTEGER PRIMARY KEY, user_name TEXT)") + if include_account_in_name2id: + conn.execute("INSERT INTO Name2Id(rowid, user_name) VALUES (?, ?)", (1, account)) + for idx, (username, rows) in enumerate(rows_by_username.items()): + friend_rowid = idx + 2 + conn.execute("INSERT INTO Name2Id(rowid, user_name) VALUES (?, ?)", (friend_rowid, username)) + table_name = f"msg_{hashlib.md5(username.encode('utf-8')).hexdigest()}" + conn.execute( + f""" + CREATE TABLE {table_name} ( + local_id INTEGER, + server_id INTEGER, + local_type INTEGER, + sort_seq INTEGER, + real_sender_id INTEGER, + create_time INTEGER, + message_content TEXT, + compress_content BLOB + ) + """ + ) + for row in rows: + direction = str(row.get("direction", "sent")) + real_sender_id = 1 if direction == "sent" else friend_rowid + conn.execute( + f""" + INSERT INTO {table_name} + (local_id, server_id, local_type, sort_seq, real_sender_id, create_time, message_content, compress_content) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + int(row.get("local_id", 0)), + int(row.get("server_id", 0)), + int(row.get("local_type", 0)), + int(row.get("sort_seq", row.get("local_id", 0))), + int(real_sender_id), + int(row.get("create_time", 0)), + str(row.get("message_content", "")), + row.get("compress_content"), + ), + ) + conn.commit() + finally: + conn.close() + + def _make_account_dir(self, root: Path, *, account: str, usernames: list[str]) -> Path: + account_dir = root / account + account_dir.mkdir(parents=True, exist_ok=True) + self._seed_contact_db(account_dir / "contact.db", account=account, usernames=usernames) + self._seed_session_db(account_dir / "session.db", usernames=usernames) + return account_dir + + def test_voicelength_ms_and_seconds_units_with_direction(self): + from wechat_decrypt_tool.wrapped.cards.card_02_message_chars import compute_voice_call_stats + + with TemporaryDirectory() as td: + root = Path(td) + account = "wxid_me" + friend = "wxid_friend_voice" + account_dir = self._make_account_dir(root, account=account, usernames=[friend]) + + rows = [ + { + # 毫秒口径:6080ms -> 6s + "local_id": 1, + "local_type": 34, + "direction": "sent", + "create_time": self._ts(2025, 3, 1, 10, 0, 0), + "message_content": _voice_xml("6080"), + }, + { + # 秒口径防御:45 -> 45s(且 create_time 为毫秒,验证毫秒/秒防御) + "local_id": 2, + "local_type": 34, + "direction": "sent", + "create_time": self._ts(2025, 3, 2, 11, 0, 0) * 1000, + "message_content": _voice_xml("45"), + }, + { + # 对方发来的语音:12000ms -> 12s + "local_id": 3, + "local_type": 34, + "direction": "received", + "create_time": self._ts(2025, 3, 3, 12, 0, 0), + "message_content": _voice_xml("12000"), + }, + { + # 年份外消息:不计 + "local_id": 4, + "local_type": 34, + "direction": "sent", + "create_time": self._ts(2024, 6, 1, 9, 0, 0), + "message_content": _voice_xml("30000"), + }, + ] + self._seed_message_db(account_dir / "message_0.db", account=account, rows_by_username={friend: rows}) + + data = compute_voice_call_stats(account_dir=account_dir, year=2025) + voice = data["voice"] + + self.assertEqual(voice["sentCount"], 2) + self.assertEqual(voice["sentSeconds"], 6 + 45) + self.assertEqual(voice["receivedCount"], 1) + self.assertEqual(voice["receivedSeconds"], 12) + + top_sent = voice["topSentPartner"] + self.assertIsNotNone(top_sent) + self.assertEqual(top_sent["username"], friend) + self.assertEqual(top_sent["seconds"], 51) + self.assertEqual(top_sent["count"], 2) + self.assertEqual(top_sent["displayName"], "好友1") + self.assertTrue(str(top_sent["avatarUrl"]).startswith("/api/chat/avatar")) + + top_recv = voice["topReceivedPartner"] + self.assertIsNotNone(top_recv) + self.assertEqual(top_recv["username"], friend) + self.assertEqual(top_recv["seconds"], 12) + self.assertEqual(top_recv["count"], 1) + + # 没有任何通话消息时 calls 仍存在且为空值状态 + self.assertEqual(data["calls"]["totalCount"], 0) + self.assertIsNone(data["calls"]["topPartner"]) + + def test_longest_voice_attribution(self): + from wechat_decrypt_tool.wrapped.cards.card_02_message_chars import compute_voice_call_stats + + with TemporaryDirectory() as td: + root = Path(td) + account = "wxid_me" + friend_a = "wxid_friend_a" + friend_b = "wxid_friend_b" + account_dir = self._make_account_dir(root, account=account, usernames=[friend_a, friend_b]) + + rows_by_username = { + friend_a: [ + { + "local_id": 1, + "local_type": 34, + "direction": "sent", + "create_time": self._ts(2025, 2, 1, 8, 0, 0), + "message_content": _voice_xml("6080"), + }, + ], + friend_b: [ + { + "local_id": 2, + "local_type": 34, + "direction": "received", + "create_time": self._ts(2025, 5, 20, 21, 30, 0), + "message_content": _voice_xml("58000"), + }, + ], + } + self._seed_message_db(account_dir / "message_0.db", account=account, rows_by_username=rows_by_username) + + data = compute_voice_call_stats(account_dir=account_dir, year=2025) + longest = data["voice"]["longest"] + + self.assertIsNotNone(longest) + self.assertEqual(longest["seconds"], 58) + self.assertEqual(longest["direction"], "received") + self.assertEqual(longest["username"], friend_b) + self.assertEqual(longest["displayName"], "好友2") + self.assertEqual(longest["maskedName"], "好*2") + self.assertTrue(str(longest["avatarUrl"]).startswith("/api/chat/avatar")) + self.assertEqual(longest["date"], "2025-05-20") + + def test_call_types_duration_regex_and_missed(self): + from wechat_decrypt_tool.wrapped.cards.card_02_message_chars import compute_voice_call_stats + + with TemporaryDirectory() as td: + root = Path(td) + account = "wxid_me" + friend = "wxid_friend_call" + account_dir = self._make_account_dir(root, account=account, usernames=[friend]) + + rows = [ + { + # 视频通话,时:分:秒 -> 3723s + "local_id": 1, + "local_type": 50, + "direction": "sent", + "create_time": self._ts(2025, 4, 1, 20, 0, 0), + "message_content": _voip_xml(room_type="0", msg="通话时长 1:02:03"), + }, + { + # 语音通话,分:秒 -> 19s + "local_id": 2, + "local_type": 50, + "direction": "received", + "create_time": self._ts(2025, 4, 2, 20, 0, 0), + "message_content": _voip_xml(room_type="1", msg="通话时长 00:19"), + }, + { + "local_id": 3, + "local_type": 50, + "direction": "sent", + "create_time": self._ts(2025, 4, 3, 20, 0, 0), + "message_content": _voip_xml(room_type="1", msg="已取消"), + }, + { + "local_id": 4, + "local_type": 50, + "direction": "sent", + "create_time": self._ts(2025, 4, 4, 20, 0, 0), + "message_content": _voip_xml(room_type="0", msg="对方已拒绝"), + }, + { + "local_id": 5, + "local_type": 50, + "direction": "received", + "create_time": self._ts(2025, 4, 5, 20, 0, 0), + "message_content": _voip_xml(room_type="1", msg="未接听"), + }, + { + # 自己拒接:文案「已拒绝」(不含「对方」前缀)也须计入未接通。 + "local_id": 6, + "local_type": 50, + "direction": "received", + "create_time": self._ts(2025, 4, 6, 20, 0, 0), + "message_content": _voip_xml(room_type="0", msg="已拒绝"), + }, + { + # 拨出无人接听:「对方无应答」。 + "local_id": 7, + "local_type": 50, + "direction": "sent", + "create_time": self._ts(2025, 4, 7, 20, 0, 0), + "message_content": _voip_xml(room_type="1", msg="对方无应答"), + }, + ] + self._seed_message_db(account_dir / "message_0.db", account=account, rows_by_username={friend: rows}) + + data = compute_voice_call_stats(account_dir=account_dir, year=2025) + calls = data["calls"] + + self.assertEqual(calls["totalCount"], 7) + self.assertEqual(calls["videoCount"], 3) + self.assertEqual(calls["voiceCount"], 4) + self.assertEqual(calls["connectedCount"], 2) + self.assertEqual(calls["totalSeconds"], 3723 + 19) + self.assertEqual(calls["missedOrCanceledCount"], 5) + # 恒等式:任何文案变体都不得让通话在两侧计数中同时丢失。 + self.assertEqual(calls["totalCount"], calls["connectedCount"] + calls["missedOrCanceledCount"]) + + top = calls["topPartner"] + self.assertIsNotNone(top) + self.assertEqual(top["username"], friend) + self.assertEqual(top["seconds"], 3742) + self.assertEqual(top["count"], 7) + self.assertEqual(top["displayName"], "好友1") + self.assertEqual(top["maskedName"], "好*1") + + def test_shard_without_my_name2id_still_counts_received(self): + # 本人不在某分片的 Name2Id(该分片里从未发过消息)时,收到的语音/通话不得丢失。 + from wechat_decrypt_tool.wrapped.cards.card_02_message_chars import compute_voice_call_stats + + with TemporaryDirectory() as td: + root = Path(td) + account = "wxid_me" + friend = "wxid_recv_only" + account_dir = self._make_account_dir(root, account=account, usernames=[friend]) + + rows = [ + { + "local_id": 1, + "local_type": 34, + "direction": "received", + "create_time": self._ts(2025, 8, 1, 10, 0, 0), + "message_content": _voice_xml("15000"), + }, + { + "local_id": 2, + "local_type": 50, + "direction": "received", + "create_time": self._ts(2025, 8, 2, 10, 0, 0), + "message_content": _voip_xml(room_type="1", msg="通话时长 02:00"), + }, + ] + self._seed_message_db( + account_dir / "message_0.db", + account=account, + rows_by_username={friend: rows}, + include_account_in_name2id=False, + ) + + data = compute_voice_call_stats(account_dir=account_dir, year=2025) + voice = data["voice"] + calls = data["calls"] + + self.assertEqual(voice["receivedCount"], 1) + self.assertEqual(voice["receivedSeconds"], 15) + self.assertEqual(voice["sentCount"], 0) + self.assertEqual(calls["totalCount"], 1) + self.assertEqual(calls["connectedCount"], 1) + self.assertEqual(calls["totalSeconds"], 120) + + def test_chatroom_messages_are_excluded(self): + from wechat_decrypt_tool.wrapped.cards.card_02_message_chars import compute_voice_call_stats + + with TemporaryDirectory() as td: + root = Path(td) + account = "wxid_me" + friend = "wxid_friend_single" + chatroom = "123456789@chatroom" + account_dir = self._make_account_dir(root, account=account, usernames=[friend, chatroom]) + + rows_by_username = { + friend: [ + { + "local_id": 1, + "local_type": 34, + "direction": "sent", + "create_time": self._ts(2025, 6, 1, 10, 0, 0), + "message_content": _voice_xml("5000"), + }, + ], + chatroom: [ + { + "local_id": 2, + "local_type": 34, + "direction": "sent", + "create_time": self._ts(2025, 6, 1, 11, 0, 0), + "message_content": _voice_xml("20000"), + }, + { + "local_id": 3, + "local_type": 34, + "direction": "received", + "create_time": self._ts(2025, 6, 1, 12, 0, 0), + "message_content": _voice_xml("30000"), + }, + { + "local_id": 4, + "local_type": 50, + "direction": "sent", + "create_time": self._ts(2025, 6, 1, 13, 0, 0), + "message_content": _voip_xml(room_type="1", msg="通话时长 10:00"), + }, + ], + } + self._seed_message_db(account_dir / "message_0.db", account=account, rows_by_username=rows_by_username) + + data = compute_voice_call_stats(account_dir=account_dir, year=2025) + voice = data["voice"] + calls = data["calls"] + + self.assertEqual(voice["sentCount"], 1) + self.assertEqual(voice["sentSeconds"], 5) + self.assertEqual(voice["receivedCount"], 0) + self.assertEqual(voice["receivedSeconds"], 0) + self.assertEqual(voice["longest"]["username"], friend) + self.assertEqual(calls["totalCount"], 0) + self.assertEqual(calls["totalSeconds"], 0) + self.assertIsNone(calls["topPartner"]) + + def test_empty_state_keeps_fields_with_zero_and_null(self): + from wechat_decrypt_tool.wrapped.cards.card_02_message_chars import build_card_02_message_chars + + with TemporaryDirectory() as td: + root = Path(td) + account = "wxid_me" + account_dir = self._make_account_dir(root, account=account, usernames=[]) + + card = build_card_02_message_chars(account_dir=account_dir, year=2025) + self.assertEqual(card["id"], 2) + self.assertEqual(card["status"], "ok") + + voice = card["data"]["voice"] + calls = card["data"]["calls"] + + self.assertEqual(voice["sentCount"], 0) + self.assertEqual(voice["sentSeconds"], 0) + self.assertEqual(voice["receivedCount"], 0) + self.assertEqual(voice["receivedSeconds"], 0) + self.assertIsNone(voice["longest"]) + self.assertIsNone(voice["topSentPartner"]) + self.assertIsNone(voice["topReceivedPartner"]) + + self.assertEqual(calls["totalCount"], 0) + self.assertEqual(calls["videoCount"], 0) + self.assertEqual(calls["voiceCount"], 0) + self.assertEqual(calls["connectedCount"], 0) + self.assertEqual(calls["totalSeconds"], 0) + self.assertEqual(calls["missedOrCanceledCount"], 0) + self.assertIsNone(calls["topPartner"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wrapped_reply_speed.py b/tests/test_wrapped_reply_speed.py index 9da41ecf..03b08fdf 100644 --- a/tests/test_wrapped_reply_speed.py +++ b/tests/test_wrapped_reply_speed.py @@ -1,5 +1,8 @@ +import sqlite3 import unittest +from datetime import datetime from pathlib import Path +from tempfile import TemporaryDirectory import sys # Ensure "src/" is importable when running tests from repo root. @@ -69,5 +72,232 @@ def test_score_penalizes_extremely_slow_reply(self): self.assertGreater(_score_conv(agg=fast_few, tau_seconds=tau), _score_conv(agg=slow_many, tau_seconds=tau)) +class TestWrappedReplySpeedInitiative(unittest.TestCase): + """「谁先开口 + 势均力敌」(data.initiative)统计口径。""" + + ACCOUNT = "wxid_me" + + def _ts(self, y: int, m: int, d: int, hh: int, mm: int, ss: int) -> int: + return int(datetime(y, m, d, hh, mm, ss).timestamp()) + + def _seed_contact_db(self, path: Path, usernames: list[str]) -> None: + conn = sqlite3.connect(str(path)) + try: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS contact ( + username TEXT PRIMARY KEY, + remark TEXT, + nick_name TEXT, + alias TEXT, + big_head_url TEXT, + small_head_url TEXT + ) + """ + ) + for u in usernames: + conn.execute( + "INSERT INTO contact(username, nick_name) VALUES(?, ?)", + (u, f"Nick_{u}"), + ) + conn.commit() + finally: + conn.close() + + def _seed_index_db(self, path: Path, rows: list[dict]) -> None: + conn = sqlite3.connect(str(path)) + try: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS message_fts ( + username TEXT, + sender_username TEXT, + create_time INTEGER, + sort_seq INTEGER, + local_id INTEGER, + local_type INTEGER, + db_stem TEXT + ) + """ + ) + for r in rows: + conn.execute( + """ + INSERT INTO message_fts( + username, sender_username, create_time, sort_seq, local_id, local_type, db_stem + ) VALUES(?, ?, ?, ?, ?, ?, ?) + """, + ( + r["username"], + r["sender_username"], + int(r["create_time"]), + int(r["sort_seq"]), + int(r["local_id"]), + int(r.get("local_type", 1)), + str(r.get("db_stem", "message_0")), + ), + ) + conn.commit() + finally: + conn.close() + + def _make_row(self, username: str, sender: str, ts: int, lid: int, **extra) -> dict: + return { + "username": username, + "sender_username": sender, + "create_time": ts, + "sort_seq": lid, + "local_id": lid, + **extra, + } + + def _compute_initiative(self, rows: list[dict], contacts: list[str]) -> dict: + from wechat_decrypt_tool.wrapped.cards.card_03_reply_speed import compute_reply_speed_stats + + with TemporaryDirectory() as td: + account_dir = Path(td) / self.ACCOUNT + account_dir.mkdir(parents=True, exist_ok=True) + self._seed_contact_db(account_dir / "contact.db", contacts) + self._seed_index_db(account_dir / "chat_search_index.db", rows) + stats = compute_reply_speed_stats(account_dir=account_dir, year=2025) + return stats["initiative"] + + def test_conversation_split_boundary_exactly_3600(self): + buddy = "wxid_x" + base = self._ts(2025, 3, 1, 8, 0, 0) + rows = [ + # 对方开启第一次对话;间隔 3599 秒不切分,恰好 3600 秒切分为新对话。 + self._make_row(buddy, buddy, base, 1), + self._make_row(buddy, self.ACCOUNT, base + 3599, 2), + self._make_row(buddy, buddy, base + 3599 + 3600, 3), + # 群聊与 biz 分片不参与统计。 + self._make_row("123@chatroom", buddy, base + 100, 4), + self._make_row("wxid_biz", "wxid_biz", base + 200, 5, db_stem="biz_message_0"), + ] + initiative = self._compute_initiative(rows, [buddy]) + + self.assertEqual(initiative["conversationCount"], 2) + self.assertEqual(initiative["initiatedByMe"], 0) + self.assertEqual(initiative["initiatedByOthers"], 2) + self.assertEqual(initiative["initiationRatePct"], 0.0) + + def test_initiator_attribution_and_rate(self): + a = "wxid_aaa" + b = "wxid_bbb" + rows = [] + lid = 0 + + def add(username: str, sender: str, t: int) -> None: + nonlocal lid + lid += 1 + rows.append(self._make_row(username, sender, t, lid)) + + # A 会话:我先开口 2 次(10:00 / 14:00),A 先开口 1 次(18:00)。 + t1 = self._ts(2025, 5, 1, 10, 0, 0) + add(a, self.ACCOUNT, t1) + add(a, a, t1 + 60) + t2 = self._ts(2025, 5, 1, 14, 0, 0) + add(a, self.ACCOUNT, t2) + add(a, a, t2 + 30) + t3 = self._ts(2025, 5, 1, 18, 0, 0) + add(a, a, t3) + add(a, self.ACCOUNT, t3 + 30) + + # B 会话:B 先开口 1 次。 + t4 = self._ts(2025, 5, 2, 9, 0, 0) + add(b, b, t4) + add(b, self.ACCOUNT, t4 + 60) + + initiative = self._compute_initiative(rows, [a, b]) + + self.assertEqual(initiative["conversationCount"], 4) + self.assertEqual(initiative["initiatedByMe"], 2) + self.assertEqual(initiative["initiatedByOthers"], 2) + self.assertEqual(initiative["initiationRatePct"], 50.0) + + top_by_me = initiative["topInitiatedByMe"] + self.assertEqual(len(top_by_me), 1) + self.assertEqual(top_by_me[0]["username"], a) + self.assertEqual(top_by_me[0]["count"], 2) + self.assertEqual(top_by_me[0]["displayName"], f"Nick_{a}") + self.assertTrue(top_by_me[0]["maskedName"].startswith("N")) + self.assertIn("*", top_by_me[0]["maskedName"]) + + top_to_me = initiative["topInitiatedToMe"] + self.assertEqual([(x["username"], x["count"]) for x in top_to_me], [(a, 1), (b, 1)]) + + def test_initiation_rate_rounding_one_decimal(self): + buddy = "wxid_rate" + rows = [] + lid = 0 + # 3 次对话(间隔 2 小时),其中 2 次由我先开口 -> 66.7%。 + starts = [ + (self._ts(2025, 6, 1, 9, 0, 0), self.ACCOUNT), + (self._ts(2025, 6, 1, 11, 0, 0), self.ACCOUNT), + (self._ts(2025, 6, 1, 13, 0, 0), buddy), + ] + for t, first_sender in starts: + other = buddy if first_sender == self.ACCOUNT else self.ACCOUNT + lid += 1 + rows.append(self._make_row(buddy, first_sender, t, lid)) + lid += 1 + rows.append(self._make_row(buddy, other, t + 20, lid)) + + initiative = self._compute_initiative(rows, [buddy]) + self.assertEqual(initiative["conversationCount"], 3) + self.assertEqual(initiative["initiationRatePct"], 66.7) + + def _mutual_pair_rows(self, buddy: str, base: int, lid: int, sent: int, received: int) -> tuple[list[dict], int]: + rows = [] + n = max(sent, received) + for i in range(n): + t = base + i * 40 + if i < received: + lid += 1 + rows.append(self._make_row(buddy, buddy, t, lid)) + if i < sent: + lid += 1 + rows.append(self._make_row(buddy, self.ACCOUNT, t + 10, lid)) + return rows, lid + + def test_mutual_friend_threshold_and_pick(self): + a = "wxid_even" # 50 / 50 -> ratio 1.0,达标且最接近 1 + b = "wxid_short" # 49 / 60 -> 我方不足 50,不达标 + c = "wxid_biased" # 60 / 80 -> ratio 0.75,达标但更不均衡 + rows = [] + lid = 1 + part, lid = self._mutual_pair_rows(a, self._ts(2025, 7, 1, 8, 0, 0), lid, sent=50, received=50) + rows += part + part, lid = self._mutual_pair_rows(b, self._ts(2025, 7, 2, 8, 0, 0), lid, sent=49, received=60) + rows += part + part, lid = self._mutual_pair_rows(c, self._ts(2025, 7, 3, 8, 0, 0), lid, sent=60, received=80) + rows += part + + initiative = self._compute_initiative(rows, [a, b, c]) + mutual = initiative["mutualFriend"] + self.assertIsNotNone(mutual) + self.assertEqual(mutual["username"], a) + self.assertEqual(mutual["sentCount"], 50) + self.assertEqual(mutual["receivedCount"], 50) + self.assertEqual(mutual["ratio"], 1.0) + self.assertEqual(mutual["displayName"], f"Nick_{a}") + + def test_mutual_friend_none_when_below_threshold(self): + buddy = "wxid_onesided" + rows, _ = self._mutual_pair_rows(buddy, self._ts(2025, 8, 1, 8, 0, 0), 1, sent=49, received=120) + initiative = self._compute_initiative(rows, [buddy]) + self.assertIsNone(initiative["mutualFriend"]) + + def test_empty_index_still_returns_initiative_shape(self): + initiative = self._compute_initiative([], []) + self.assertEqual(initiative["conversationCount"], 0) + self.assertEqual(initiative["initiatedByMe"], 0) + self.assertEqual(initiative["initiatedByOthers"], 0) + self.assertIsNone(initiative["initiationRatePct"]) + self.assertEqual(initiative["topInitiatedByMe"], []) + self.assertEqual(initiative["topInitiatedToMe"], []) + self.assertIsNone(initiative["mutualFriend"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/build_wce_integrity.ps1 b/tools/build_wce_integrity.ps1 index d47d55b7..6c7f1956 100644 --- a/tools/build_wce_integrity.ps1 +++ b/tools/build_wce_integrity.ps1 @@ -8,15 +8,38 @@ $crate = Join-Path $repo 'native\wce_integrity' $outDir = Join-Path $repo 'src\wechat_decrypt_tool\native' $profile = if ($Debug) { 'debug' } else { 'release' } $targetDir = Join-Path $crate 'target-package' +$python = Join-Path $repo '.venv\Scripts\python.exe' +if (-not (Test-Path -LiteralPath $python)) { + throw "未找到项目虚拟环境 Python:$python" +} $args = @('build') if (-not $Debug) { $args += '--release' } +$cargoCommand = Get-Command cargo -ErrorAction SilentlyContinue +$cargoExe = if ($cargoCommand) { $cargoCommand.Source } else { $null } +$localCargoHome = Join-Path $repo 'output\tools\cargo-home' +$localRustupHome = Join-Path $repo 'output\tools\rustup' +$localCargoExe = Join-Path $localCargoHome 'bin\cargo.exe' +if (-not $cargoExe -and (Test-Path -LiteralPath $localCargoExe)) { + $cargoExe = $localCargoExe +} +if (-not $cargoExe) { + throw '未找到 Cargo。请安装 Rust,或将项目隔离工具链放到 output\tools\cargo-home。' +} +$previousCargoHome = $env:CARGO_HOME +$previousRustupHome = $env:RUSTUP_HOME +$previousPyo3Python = $env:PYO3_PYTHON Push-Location $crate try { + if ($cargoExe -eq $localCargoExe) { + $env:CARGO_HOME = $localCargoHome + $env:RUSTUP_HOME = $localRustupHome + } $previousTargetDir = $env:CARGO_TARGET_DIR try { $env:CARGO_TARGET_DIR = $targetDir - cargo @args + $env:PYO3_PYTHON = $python + & $cargoExe @args if ($LASTEXITCODE -ne 0) { throw "cargo build failed with exit code $LASTEXITCODE" } @@ -25,6 +48,9 @@ try { } } finally { Pop-Location + $env:CARGO_HOME = $previousCargoHome + $env:RUSTUP_HOME = $previousRustupHome + $env:PYO3_PYTHON = $previousPyo3Python } $dll = Join-Path $targetDir "$profile\wce_integrity.dll" @@ -33,5 +59,35 @@ if (-not (Test-Path $dll)) { } New-Item -ItemType Directory -Force $outDir | Out-Null $pyd = Join-Path $outDir 'wce_integrity.pyd' -Copy-Item -Force $dll $pyd +try { + Copy-Item -Force $dll $pyd +} catch [System.IO.IOException] { + throw '标准 wce_integrity.pyd 正被运行中的后端占用。请仅停止本项目后端后重新构建。' +} Write-Host "wce_integrity.pyd -> $pyd" + +$previousPythonPath = $env:PYTHONPATH +try { + $env:PYTHONPATH = Join-Path $repo 'src' + $preflightScript = @' +from pathlib import Path + +from wechat_decrypt_tool.export_integrity import load_wce_integrity_native + +native = load_wce_integrity_native() +css = native.export_css("chat") +compact = "".join(css.split()) +expected = (Path.cwd() / "src" / "wechat_decrypt_tool" / "native" / "wce_integrity.pyd").resolve() +actual = Path(native.__file__).resolve() +assert actual == expected, f"loaded unexpected native module: {actual}" +assert ".wechat-voice-transcript" in css +assert ".wechat-voice-wrapper{display:flex;flex-direction:column" in compact +print(f"wce_integrity loaded from: {native.__file__}") +'@ + $preflightScript | & $python - + if ($LASTEXITCODE -ne 0) { + throw "wce_integrity CSS 预检失败,退出码:$LASTEXITCODE" + } +} finally { + $env:PYTHONPATH = $previousPythonPath +} diff --git a/uv.lock b/uv.lock index 43d61f2b..61f5e22e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] [[package]] name = "aiofiles" @@ -43,6 +47,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] +[[package]] +name = "av" +version = "18.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/a4/570a5a35c8638aba01e739925846c35fdd6b0756a15526766d0a4dd3b7df/av-18.0.0.tar.gz", hash = "sha256:4ef7e72c3d3a872584a1215173b16e0226811037f40dcdbf75992631098df1ba", size = 4340222, upload-time = "2026-07-02T06:37:58.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/4a/9e3463df030e063d757fa12f0f39be6541b45b06b5bad48c2ce361b924bf/av-18.0.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:149289d40e732a6e49c9530bc245b49d9964cfd1c8c9e06778703b7d5bba6b25", size = 22499354, upload-time = "2026-07-02T06:36:58.751Z" }, + { url = "https://files.pythonhosted.org/packages/77/b3/2576a44b4f39c7462ced4c17fec04c756f7b0f3c5cb940d124173e417d6a/av-18.0.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:35274c20d2ad3b4774fe632bcef2e34af79858ddf899352339cc3babbc13a484", size = 18175248, upload-time = "2026-07-02T06:37:01.741Z" }, + { url = "https://files.pythonhosted.org/packages/84/74/6732f17b96dc23fd23b876b2805435855abdc8a3b397142be4e581165de8/av-18.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4d683b7747a0ba9222b8a5f81e41db5f796e7f64473454ec4fe2548e083c2fa0", size = 33387843, upload-time = "2026-07-02T06:37:05.097Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b9/7708c43fed7ae28b4a1bad060b4221e3334cd827cec24f7165902a6ac1f4/av-18.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ae56b40b6f8b067a8ad2dac664fbfbabac7f7a55b9a7bb031eb99289252bc017", size = 35536910, upload-time = "2026-07-02T06:37:08.806Z" }, + { url = "https://files.pythonhosted.org/packages/5a/94/eba99691d184f6a395a242d54dc370e2fd2265e95bbc98e2963a0fdbdd6c/av-18.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ea2e8ebbce521f21b55df9400e00d721623c9020ef158f5a188a96130be0743f", size = 38984619, upload-time = "2026-07-02T06:37:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/0d7aee07fe16aa9ffdf96043c14bed5485a52c0dea4259de87aa306ecab4/av-18.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef96dabb3e50dac249913145dff5424b302b257fd95dcb64be3c7b7a8aef16d1", size = 34451176, upload-time = "2026-07-02T06:37:15.154Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/810da80b12680d4c4fe235bd1b4003289be9213ac7f114b77b8ecf0e3b3e/av-18.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0f65518a184613e41536f29e8758c8e3d8293e46bf5bef108f04f925bbfa3f44", size = 36619869, upload-time = "2026-07-02T06:37:18.495Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/0f121ff43dc5a70696676c98a8f1674e2fa787614c2abaacb15fa1a9bc99/av-18.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:aaf4d354d2beaa6651e4f92e54409a578bde64f79c0beef9a30b388d06f7c629", size = 27556236, upload-time = "2026-07-02T06:37:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f6/2509754d4d2356abc6fc0ea3d57c12ade29bac23a1fb7fc215a53ca518fb/av-18.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:adac2b3833b6cb9bd6cb52664a522b94db453615b3675b1dbb26e13fe1c80da6", size = 20221133, upload-time = "2026-07-02T06:37:23.88Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/4ee23a7f1609adf9b2f140c7a8ffade64a1449d89ab431d922a809eebf19/av-18.0.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:88dd8e35e9242662b409a6a05fd24a6775d949eb05da0ba31cab4f250eacbab5", size = 22740741, upload-time = "2026-07-02T06:37:26.659Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f0/b9f8363d07aa4521913e483f6a30c7c164973ef01de62769bf9b97049cd8/av-18.0.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f8f454349c402e2c8d6fa80b54eb2a3f86c00f414d2b399f01ae6dab075c6fd8", size = 18384189, upload-time = "2026-07-02T06:37:29.518Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e5/69397019aed280a72a43e97a252dee4295df1a9e608848452e5300ec4dab/av-18.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:88ce194c2201c6a6d40336adee8a5ddde46ed743eacb500e3ae9368d1c6d889e", size = 36749881, upload-time = "2026-07-02T06:37:33.096Z" }, + { url = "https://files.pythonhosted.org/packages/37/3a/1614d74f0d676ea6745eb59553c9ad01ca25db523cba808d522e838f4f5b/av-18.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:aa15e567a018cc94a26b0ab45da676dee70c4146ace6e92e47d30cc9689cbfbe", size = 38645927, upload-time = "2026-07-02T06:37:37.086Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3c/5f54710d69b0ea93634134f92b49c7a2a7fd27da5486a8a7e6251ac1cfb4/av-18.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:613153e48cefc91700746dde0ad0282d4677b194cba22cc771de14c78411cf8b", size = 40454783, upload-time = "2026-07-02T06:37:40.904Z" }, + { url = "https://files.pythonhosted.org/packages/26/92/8293e6a267e0591b543abd96ae01e7e8ed228509bdb4e4644a8a8395d90f/av-18.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:30404f53ca1ea7f350ac86ff22a2c04f903014758e9b33f398c5a62de34bd84f", size = 37573117, upload-time = "2026-07-02T06:37:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/0c/38ed7601277ae57dfe857d040be4762530fd728efff45c2fb8f035fef96a/av-18.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6882a48f7aec2863c96cddee3256ff2da98f7fb6cbed83cee9d7e70a8f186a6b", size = 39669026, upload-time = "2026-07-02T06:37:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/0636ca04d5d89d01c49bd366d2b660cc85d1f8117c476b2be62eb0c70855/av-18.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:55a646e9afce9fdc5de5224205a8a12c7ed1ba9803145dcc876c40bfc03a109b", size = 28448336, upload-time = "2026-07-02T06:37:52.477Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/1e24450ea981c44ed328691496fd2774dfa9fa3c3b00fd07f72fd5614abe/av-18.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:96f594ff506a09475e5549359352332049a25d37a08f00b4623f7f6e92e45b9c", size = 21377289, upload-time = "2026-07-02T06:37:55.935Z" }, +] + [[package]] name = "certifi" version = "2025.6.15" @@ -147,14 +177,14 @@ wheels = [ [[package]] name = "click" -version = "8.2.1" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -207,6 +237,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/49/0ab9774f64555a1b50102757811508f5ace451cf5dc0a2d074a4b9deca6a/cryptography-45.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bbc505d1dc469ac12a0a064214879eac6294038d6b24ae9f71faae1448a9608d", size = 3337594, upload-time = "2025-06-10T00:03:45.523Z" }, ] +[[package]] +name = "ctranslate2" +version = "4.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pyyaml" }, + { name = "setuptools" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c4/0e450796f90e54f3325697fc67db4f4ecd397aef96d7b3924e26fb8bd04b/ctranslate2-4.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c2db633a06e3b34bbfb72fd26eee58053d9df1f9c1610ac4df3a6a1e25af7d7", size = 1270559, upload-time = "2026-07-03T12:39:01.154Z" }, + { url = "https://files.pythonhosted.org/packages/b7/54/7b6db16470d0788fb8ab43a99e3e18ba9d41a9b50b7fef7dec353eafbe20/ctranslate2-4.8.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:079976cbce3a68de04bf9948d08c96beb86df44e5cd2974e4187bc9c9bb388f3", size = 11928069, upload-time = "2026-07-03T12:39:02.6Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/8fee1366631d224bf26b34db9063a0c88ce358d58331c2393689b0ea27ff/ctranslate2-4.8.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74bae0a8dc9f98c5a6100bf1c17a91782b384ea53b83e2606030ebf9f25318fe", size = 16707971, upload-time = "2026-07-03T12:39:05.09Z" }, + { url = "https://files.pythonhosted.org/packages/30/84/f610e90bb419707632b9b668476b9fd4cdb090c9b53c119ce017699b58ca/ctranslate2-4.8.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0a584c17f21779eb9035bcbc1ec280998f90b36725b70a5ff911f33e343199a", size = 39351971, upload-time = "2026-07-03T12:39:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/76/6c/7230ecbdd23ab867715e1b6ffe99211c39c11cae8ec2d6c3ec9208c38ee2/ctranslate2-4.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:82982f07a7d615d2248d17d6ec4c43cd50e534b094aa27cda62125a5e3a6e3fc", size = 19219248, upload-time = "2026-07-03T12:39:11.329Z" }, + { url = "https://files.pythonhosted.org/packages/6d/09/9a50eeab00db68aeac08f6ab7f98b5c36abd26b89cbd707ea39e70656500/ctranslate2-4.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9de0dddd91ae68da0a7323441e90708d14b31d31cd443004dda0e1198b5bf11e", size = 1270522, upload-time = "2026-07-03T12:39:13.368Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/6c41c4d3ae539ec76b1943c362184677befd7c1d5290d2ec361182cdb1e0/ctranslate2-4.8.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:82e0e6eb7d4301fd79a714495c8faf34242e09542cef04c9e9794c3fe90014a1", size = 11930367, upload-time = "2026-07-03T12:39:14.896Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d4/03428106134a0a58922461074f8942f92c5ed0bb3a8d018677ad64a9c476/ctranslate2-4.8.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ca144b93035b9f53e6d67b7cdf5802c3fffca9aa0247940eecbd4592c68ce2f", size = 16882768, upload-time = "2026-07-03T12:39:17.425Z" }, + { url = "https://files.pythonhosted.org/packages/47/c9/976a565398a03fb2973cbe5edd5ca03c4332d86b634799e0ee562420d3bc/ctranslate2-4.8.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dacc408f716ebc73b3b3c6ddd937700e776c4c68b6d9c81862990150ff0f6af6", size = 39529060, upload-time = "2026-07-03T12:39:20.468Z" }, + { url = "https://files.pythonhosted.org/packages/c0/82/0a5f7f2b03b4e10aacb3146715724e1b96bb993cc7d199be28c9825aa120/ctranslate2-4.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:49f96e861b57301f0b76a082109bde2cac8204a6b4fedc870883008271e82251", size = 19220789, upload-time = "2026-07-03T12:39:23.356Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/3101c3a0785253a8ef386f39744ad19c28c75b7f227e7c232aee7a5c416a/ctranslate2-4.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba628835e6ad4ad399261ab6cb51bf152de563e6b122a9e8eb0c61e69f925931", size = 1270478, upload-time = "2026-07-03T12:39:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/89/b9/e50c7558e96a054d6b1e6a6c5e729dda4a4f05584e065f2902aa5f1bc4c8/ctranslate2-4.8.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:85ef15ce0b2172ec471975b8a30d5c5bc71e7cffcd163ad6c07ea32f1943d940", size = 11930241, upload-time = "2026-07-03T12:39:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2f/ea7a19c6d7e949b731fb034664633184bbfc7882846d107f4d790693fb76/ctranslate2-4.8.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0030670278a73cae09dff9bca72cdd248af61f9367257f18db9b3b94fbb3a50d", size = 16883512, upload-time = "2026-07-03T12:39:29.302Z" }, + { url = "https://files.pythonhosted.org/packages/99/4a/21f325a9d0925d8ad24b04249adf29bf9909442967603634f7f6d4acbb79/ctranslate2-4.8.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4242a7f8e285f922525f4cffd5b1fb43cbacc61d0611cf54832e9c447d030840", size = 39529085, upload-time = "2026-07-03T12:39:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/37da1a7500b57496a5269318c4f57962ea0c26dcac06b85222d7831acf00/ctranslate2-4.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:d52499f05a60a791aeadee28d609efa130142f376d1ea76b2b1c593bb01f8827", size = 19220784, upload-time = "2026-07-03T12:39:35.74Z" }, + { url = "https://files.pythonhosted.org/packages/c6/66/39111224e418400d97fd79fbc9e72329c51f91a3e7a9c9a1a182e4f88022/ctranslate2-4.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b4c3246aa4a7f309109a841ca743a72cc4abad4f93c0bf7da691023323215621", size = 1271321, upload-time = "2026-07-03T12:39:37.907Z" }, + { url = "https://files.pythonhosted.org/packages/ef/89/13f827fae226eea51315729c00111f716813d7736ebb827fecb8f361fe0d/ctranslate2-4.8.1-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:c989f747789e8619cbc2e06443b3674c31bc71bad0369652485bd894b627360a", size = 11930735, upload-time = "2026-07-03T12:39:39.534Z" }, + { url = "https://files.pythonhosted.org/packages/c9/94/4b73f9bbaba29df4227cc65114f11d83fe6d696ef3705cb1ade79eb118fd/ctranslate2-4.8.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90eb0bd67b6bb183712cc3fd14bf01ec4f622cd625c5b33cc6c56be7d1c9c34", size = 16872460, upload-time = "2026-07-03T12:39:42.272Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d0/9816494d5ff0745bdf9abe5af04e57a103a416444e604cbe83a6eb0aed7b/ctranslate2-4.8.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e3e3aef4670a6c8dcea367401675f82b49b02c18f5837221bcd7cca90b1707a8", size = 39494736, upload-time = "2026-07-03T12:39:45.733Z" }, + { url = "https://files.pythonhosted.org/packages/6c/dc/22a2c874ca8bb6caa7018dfefdff92dddd487db31cf169891c4c6d408091/ctranslate2-4.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:a2dcce0a57beee984a691d9daa8fc3fd389f5b6cada2644c34571011833bd5b1", size = 19477164, upload-time = "2026-07-03T12:39:48.952Z" }, + { url = "https://files.pythonhosted.org/packages/77/39/7b8d47bf49748ba73182742683eef74b46608beb879765d9d4efc46bc345/ctranslate2-4.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a28c5889585cd17ee3649dfd46d9002ddf50204173f8bff476b9f76d6585795", size = 1293935, upload-time = "2026-07-03T12:39:50.924Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/434e30c752c433eaef5deccd4de54775bc1f205a6fe6c9e756b737018209/ctranslate2-4.8.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:911a5cdef8a405c1804330613a1865f616eb9c092a0e932ee4648128eb20b627", size = 11951789, upload-time = "2026-07-03T12:39:52.886Z" }, + { url = "https://files.pythonhosted.org/packages/85/f2/d716426220b462bbb5bb354b9c6c8d9a41285f067203c860cc79f9f19917/ctranslate2-4.8.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84723cae6f802551bbf2438e5e4810722631a2183b89a82c31df26566b54821d", size = 16860414, upload-time = "2026-07-03T12:39:55.54Z" }, + { url = "https://files.pythonhosted.org/packages/69/11/cdab0e7e2ad4e547f15ab227c09207569f1272abae05816900ecebb0797a/ctranslate2-4.8.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1910752ec541980644191fa3b407bc61dee00e88070b0aed29b4cef75010b3ea", size = 39465200, upload-time = "2026-07-03T12:39:59.017Z" }, + { url = "https://files.pythonhosted.org/packages/c0/03/126e963fc3237a416f3085b8a663ebd8ab449ed6c37195b4e0b49597ba0c/ctranslate2-4.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dc9f1abef55579cc02cdc74b3a55df38491ec56d177d6e6039609d61d09ed30e", size = 19499597, upload-time = "2026-07-03T12:40:01.68Z" }, +] + [[package]] name = "fastapi" version = "0.115.13" @@ -221,6 +289,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" }, ] +[[package]] +name = "faster-whisper" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "av" }, + { name = "ctranslate2" }, + { name = "huggingface-hub" }, + { name = "onnxruntime" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -230,6 +340,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/39/67be8d71f900d9a55761b6022821d6679fb56c64f1b6063d5af2c2606727/hf_xet-1.5.2.tar.gz", hash = "sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47", size = 903674, upload-time = "2026-07-16T17:29:56.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/be/525eabac5d1736b679c39e342ecd4292534012546a2d18f0043c8e3b6021/hf_xet-1.5.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4a5ecb9cda8512ba2aa8ee5d37c87a1422992165892d653098c7b90247481c3b", size = 4064284, upload-time = "2026-07-16T17:29:29.907Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3f/699749dd78442480eda4e4fca494284b0e3542e4063cc37654d5fdc929e6/hf_xet-1.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8764488197c1d7b1378c8438c18d2eea902e150dbca0b0f0d2d32603fb9b5576", size = 3828537, upload-time = "2026-07-16T17:29:31.549Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/2658ac0a5b9f4664ca27ce31bd015044fe9dea50ed455fb5197aba819c11/hf_xet-1.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d7446f72abbf7e01ca5ff131786bc2e74a56393462c17a6bf1e303fbab81db4", size = 4417133, upload-time = "2026-07-16T17:29:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/d9/58/8343f3cb63c8fa058d576136df3871550f7d5214a8f048a7ea2eab6ac906/hf_xet-1.5.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:580e59e29bf37aece1f2b68537de1e3fb04f43a23d910dcf6f128280b5bfbba4", size = 4212613, upload-time = "2026-07-16T17:29:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/0c/33/a968f4e4535037b36941ec00714625fb60e026302407e7e26ca9f3e65f4e/hf_xet-1.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bee28c619622d36968056532fd49cf2b35ca75099b1d616c31a618a893491380", size = 4412710, upload-time = "2026-07-16T17:29:36.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/9e33981173dbaf194ba0015202b02d467b624d44d4eba89e1bf06c0d2995/hf_xet-1.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e396ab0faf6298199ad7a95305c3ca8498cb825978a6485be6d00587ee4ec577", size = 4628455, upload-time = "2026-07-16T17:29:38.352Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4b/cc682832de4264a03880a2d1b5ec3e1fab3bf307f508817250baafdb9996/hf_xet-1.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fd3add255549e8ef58fa35b2e42dc016961c050600444e7d77d030ba6b57120e", size = 3979044, upload-time = "2026-07-16T17:29:40.329Z" }, + { url = "https://files.pythonhosted.org/packages/ea/09/b2cdf2a0fb39a08af3222b96092a36bd3b40c54123eef07de4422e870971/hf_xet-1.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d6f9c58549407b84b9a5383afd68db0acc42345326a3159990b36a5ca8a20e4e", size = 3808037, upload-time = "2026-07-16T17:29:42.357Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/2b70603c7552db82baeb2623e2336898304a17328845151be4fe1f48d420/hf_xet-1.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799", size = 4033760, upload-time = "2026-07-16T17:29:43.884Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/b097a86a1e4a6098f3a79382643ab09d5733d87ccc864877ad1e12b49b70/hf_xet-1.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed", size = 3841438, upload-time = "2026-07-16T17:29:45.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/35/db860aa3a0780660324a506ad4b3d322ddc6ecbba4b9340aed0942cbf21c/hf_xet-1.5.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c", size = 4428006, upload-time = "2026-07-16T17:29:46.996Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/832dd980af4b0c3ae0660e309285f2ffcdff2faa38129390dbb47aa4a3f9/hf_xet-1.5.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025", size = 4221099, upload-time = "2026-07-16T17:29:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/9e/05/ae50f0d34e3254e6c3e208beb2519f6b8673016fc4b3643badaf6450d186/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f", size = 4420766, upload-time = "2026-07-16T17:29:50.092Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/c050bc2743a2bcd68928bfee157b08681667a164a24ec95fbfcfcd717e08/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097", size = 4636716, upload-time = "2026-07-16T17:29:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/68b01c5c2edb56ac9a67b3d076ffddcb90867abaee923923eb34e7a14e76/hf_xet-1.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65", size = 3988373, upload-time = "2026-07-16T17:29:53.395Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/988383e9dc17294d536fcbcd6fd16eed882e411ad16c954984a53e47b09c/hf_xet-1.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d", size = 3816957, upload-time = "2026-07-16T17:29:54.991Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -287,6 +421,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "huggingface-hub" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/50/db3771a6e4fad4bd28fb055d4363b51cb0ae98c1aa504b79d41fdcab5483/huggingface_hub-1.25.1.tar.gz", hash = "sha256:21129595ca7a753be479b319913e22cc8808361ac118bd76cc413db831b28a99", size = 928426, upload-time = "2026-07-27T09:24:10.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/21e816831c6d16f88a6c784974413fa0421ce8a5d04380c2666ed5b503e5/huggingface_hub-1.25.1-py3-none-any.whl", hash = "sha256:004d4e70350517e24c68a7dbb7dc5e40b2b6aefef8f94bf7a85f6f9835102ea5", size = 774909, upload-time = "2026-07-27T09:24:08.079Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -296,6 +450,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jieba" version = "0.42.1" @@ -327,6 +490,189 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, ] +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/4d/5014667e2a3a77d6e1b74cc3d88948d06163b8e0a33a84c85073322b5dec/onnxruntime-1.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f5c5daabd28aad610f83fdcf32acec8fb57e6adc6c6a39fe2a3c755db957b410", size = 19130506, upload-time = "2026-07-25T01:22:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/97/b7ce1bc8bb6048b5fe9129f55d6506dc19499068ef2e0a0af1ae3c8aa4e7/onnxruntime-1.28.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d66f9ceb29909c70839e4e4fb3435c7b490050d8f162bd5f3aba4ca01ee517f", size = 17039880, upload-time = "2026-07-25T01:21:37.538Z" }, + { url = "https://files.pythonhosted.org/packages/f3/17/4e5ecd8764f87573c495d834ce79e61ecca47f7a01d1e444a606e570edcb/onnxruntime-1.28.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a166b78ee04f3a37fa1ef82034b6a3ce96d9684e582d4d30b296de83e9998bb5", size = 19193162, upload-time = "2026-07-25T01:21:59.151Z" }, + { url = "https://files.pythonhosted.org/packages/9f/10/3d946d5d5f2cdcc3c8da36cae63190c516d16349edaffd944bda60ca4c3e/onnxruntime-1.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:0d650aeee29368414367b65529e90afe4bf1bab76254789063b8b2f7ea3013c8", size = 13752539, upload-time = "2026-07-25T01:22:24.524Z" }, + { url = "https://files.pythonhosted.org/packages/8f/74/1c440be7af1e026280b139caa1be5d11bd4dc368011ddbe8f5362b58e12f/onnxruntime-1.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:0faf85fb447a663c9cdadc39bd6b19bdf7bedded6699e45731b9b36c46fd993d", size = 13449940, upload-time = "2026-07-25T01:22:14.97Z" }, + { url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362, upload-time = "2026-07-25T01:22:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628, upload-time = "2026-07-25T01:21:40.481Z" }, + { url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257, upload-time = "2026-07-25T01:22:01.695Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036, upload-time = "2026-07-25T01:22:26.89Z" }, + { url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462, upload-time = "2026-07-25T01:22:17.38Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759, upload-time = "2026-07-25T01:21:53.765Z" }, + { url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339, upload-time = "2026-07-25T01:21:43.005Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329, upload-time = "2026-07-25T01:22:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033, upload-time = "2026-07-25T01:22:29.302Z" }, + { url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175, upload-time = "2026-07-25T01:22:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307, upload-time = "2026-07-25T01:21:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954, upload-time = "2026-07-25T01:22:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748, upload-time = "2026-07-25T01:21:56.297Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950, upload-time = "2026-07-25T01:21:48.606Z" }, + { url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924, upload-time = "2026-07-25T01:22:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738, upload-time = "2026-07-25T01:22:31.629Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117, upload-time = "2026-07-25T01:22:22.387Z" }, + { url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518, upload-time = "2026-07-25T01:21:51.08Z" }, + { url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976, upload-time = "2026-07-25T01:22:12.474Z" }, +] + +[[package]] +name = "opencc-python-reimplemented" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/6d/c6f37eed651dd6b752e50f80a93396cdaa42a6acc6ce05ad7452303ea511/opencc-python-reimplemented-0.1.7.tar.gz", hash = "sha256:4f777ea3461a25257a7b876112cfa90bb6acabc6dfb843bf4d11266e43579dee", size = 482566, upload-time = "2023-02-11T03:58:42.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/6b/055b7806f320cc8f2cdf23c5f70221c0dc1683fca9ffaf76dfc2ad4b91b6/opencc_python_reimplemented-0.1.7-py2.py3-none-any.whl", hash = "sha256:41b3b92943c7bed291f448e9c7fad4b577c8c2eae30fcfe5a74edf8818493aa6", size = 481813, upload-time = "2023-02-11T03:58:39.66Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -357,6 +703,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/06/80cac61bc7f791bcaae552ba90fb868f7af7503ef1c74e423cc20fca1a53/pilk-0.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:a6692096607e8d77d348aeec00df633788c85b527aa83cbd803ddf508936db7f", size = 127024, upload-time = "2023-05-26T03:32:24.524Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + [[package]] name = "psutil" version = "7.0.0" @@ -491,6 +861,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyinstaller" version = "6.18.0" @@ -550,6 +929,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/7b/4cabc76fcc21c3c7d5c671d8783984d30ac9d3bb387c4ba784fca3cdfa3a/pypinyin-0.55.0-py2.py3-none-any.whl", hash = "sha256:d53b1e8ad2cdb815fb2cb604ed3123372f5a28c6f447571244aca36fc62a286f", size = 840203, upload-time = "2025-07-20T12:01:48.535Z" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.0" @@ -673,6 +1068,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, ] +[[package]] +name = "tokenizers" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, + { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + [[package]] name = "typing-extensions" version = "4.14.0" @@ -902,7 +1336,7 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, { name = "uvicorn", extra = ["standard"] }, - { name = "wx-key" }, + { name = "wx-key", marker = "sys_platform == 'win32'" }, { name = "yara-python", marker = "sys_platform == 'win32'" }, { name = "zstandard" }, ] @@ -911,15 +1345,26 @@ dependencies = [ build = [ { name = "pyinstaller" }, ] +voice-transcription = [ + { name = "faster-whisper" }, + { name = "opencc-python-reimplemented" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] [package.metadata] requires-dist = [ { name = "aiofiles", specifier = ">=23.2.1" }, { name = "cryptography", specifier = ">=41.0.0" }, { name = "fastapi", specifier = ">=0.104.0" }, + { name = "faster-whisper", marker = "extra == 'voice-transcription'", specifier = ">=1.1.0" }, { name = "httpx" }, { name = "jieba", specifier = ">=0.42.1" }, { name = "loguru", specifier = ">=0.7.0" }, + { name = "opencc-python-reimplemented", marker = "extra == 'voice-transcription'", specifier = ">=0.1.7" }, { name = "packaging" }, { name = "pefile", marker = "sys_platform == 'win32'", specifier = ">=2024.8.26" }, { name = "pilk", specifier = ">=0.2.4" }, @@ -933,11 +1378,14 @@ requires-dist = [ { name = "requests", specifier = ">=2.32.4" }, { name = "typing-extensions", specifier = ">=4.8.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" }, - { name = "wx-key", specifier = ">=2.0.1" }, + { name = "wx-key", marker = "sys_platform == 'win32'", specifier = ">=2.0.1" }, { name = "yara-python", marker = "sys_platform == 'win32'", specifier = ">=4.5.2" }, { name = "zstandard", specifier = ">=0.23.0" }, ] -provides-extras = ["build"] +provides-extras = ["build", "voice-transcription"] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.0.0" }] [[package]] name = "win32-setctime" diff --git a/website/.nojekyll b/website/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/website/assets/css/main.css b/website/assets/css/main.css new file mode 100644 index 00000000..51011639 --- /dev/null +++ b/website/assets/css/main.css @@ -0,0 +1,1010 @@ +/* ════════════════════════════════════════════════════════════ + WeChatDataAnalysis — 官网 + Dark decrypt-lab · cinematic scroll narrative + ════════════════════════════════════════════════════════════ */ + +:root { + --bg: #050807; + --bg-2: #070b09; + --panel: rgba(13, 20, 16, 0.72); + --panel-solid: #0a0f0c; + --ink: #e9f1eb; + --ink-dim: #9aa8a0; + --ink-faint: #5c6a63; + --green: #07c160; + --neon: #3df28d; + --neon-soft: rgba(61, 242, 141, 0.14); + --glow: rgba(61, 242, 141, 0.5); + --amber: #ffc24b; + --red: #ff5d5d; + --line: rgba(150, 180, 160, 0.14); + --line-strong: rgba(150, 180, 160, 0.28); + + --font-sans: "Noto Sans SC", "PingFang SC", "Hiragino Sans GB", + "Microsoft YaHei", "Source Han Sans SC", -apple-system, "Segoe UI", sans-serif; + --font-serif: "Noto Serif SC", "Songti SC", "STSong", serif; + --font-disp: "Unbounded", "Noto Sans SC", sans-serif; + --font-mono: "JetBrains Mono", "SF Mono", ui-monospace, Menlo, Consolas, monospace; + + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +html { scrollbar-width: thin; scrollbar-color: rgba(61, 242, 141, 0.25) transparent; } +html.lenis, html.lenis body { height: auto; } +.lenis.lenis-smooth { scroll-behavior: auto !important; } + +body { + background: var(--bg); + color: var(--ink); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.6; + overflow-x: clip; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} +body.is-loading { overflow: hidden; } +body.is-loading #page, body.is-loading .nav, body.is-loading .rail { visibility: hidden; } + +::selection { background: rgba(61, 242, 141, 0.28); color: #fff; } +a { color: inherit; text-decoration: none; } +img { max-width: 100%; display: block; } +.mono { font-family: var(--font-mono); } +em { font-style: normal; } + +@media (hover: hover) and (pointer: fine) { + body { cursor: none; } + body a, body button, body [data-cursor] { cursor: none; } +} + +/* ─────────────────────────── preloader ─────────────────────────── */ + +#loader { + position: fixed; inset: 0; z-index: 200; + background: transparent; + display: grid; place-items: center; + overflow: hidden; +} +#loader-hex { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0.5; z-index: 1; } +.loader__panel { + position: absolute; left: 0; width: 100%; height: 51%; + background: var(--bg); z-index: 0; pointer-events: none; +} +.loader__panel.top { top: 0; } +.loader__panel.bottom { bottom: 0; } + +.loader__center { position: relative; text-align: center; z-index: 3; width: min(720px, 88vw); } +.loader__toplabel { + font-size: 10px; letter-spacing: 0.34em; color: var(--ink-dim); +} +.loader__keylabel { + margin-top: 34px; display: flex; align-items: center; justify-content: center; gap: 14px; + font-size: 10px; letter-spacing: 0.3em; color: var(--neon); +} +.loader__keylabel .tick { width: 44px; opacity: 0.5; } +.loader__key { + margin: 18px auto 26px; max-width: 36.5ch; + font-size: clamp(13px, 1.45vw, 18px); letter-spacing: 0.12em; line-height: 2.1; + word-break: break-all; min-height: 4.2em; + color: var(--ink-faint); +} +.loader__key b { + font-weight: 400; color: var(--neon); + text-shadow: 0 0 16px rgba(61, 242, 141, 0.55); +} +.loader__line { + margin: 0 auto; width: min(520px, 76vw); height: 1px; + background: var(--line); position: relative; +} +.loader__line i { + position: absolute; left: 0; top: 0; height: 100%; width: 0%; + background: var(--neon); box-shadow: 0 0 14px var(--glow); display: block; +} +.loader__meta { + margin: 16px auto 0; width: min(520px, 76vw); + display: flex; justify-content: space-between; align-items: baseline; + font-size: 11px; letter-spacing: 0.14em; color: var(--ink-dim); text-transform: lowercase; +} +.loader__pnum { color: var(--neon); font-variant-numeric: tabular-nums; } +.loader__corner { + position: absolute; font-size: 10px; letter-spacing: 0.22em; + color: var(--ink-faint); z-index: 3; +} +.loader__corner.tl { top: 26px; left: 30px; } +.loader__corner.tr { top: 26px; right: 30px; } +.loader__corner.bl { bottom: 24px; left: 30px; } +.loader__corner.br { bottom: 24px; right: 30px; } + +/* ─────────────────────────── global chrome ─────────────────────────── */ + +#gl { position: fixed; inset: 0; z-index: 0; width: 100%; height: 100%; pointer-events: none; } + +.grain { + position: fixed; inset: -100px; z-index: 90; pointer-events: none; + opacity: 0.07; mix-blend-mode: overlay; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 240 240' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + animation: grain 0.9s steps(3) infinite; +} +@keyframes grain { + 0% { transform: translate(0, 0); } + 33% { transform: translate(-40px, 26px); } + 66% { transform: translate(30px, -34px); } + 100% { transform: translate(0, 0); } +} + +/* 光标 = 解密扫描框:四角取景括号 + 跟随的内存地址读数,而不是通用的圆圈加点 */ +.cursor, .cursor-dot, .cursor-addr { display: none; } +@media (hover: hover) and (pointer: fine) { + .cursor { + display: block; position: fixed; z-index: 300; top: 0; left: 0; + width: 34px; height: 34px; margin: -17px 0 0 -17px; + pointer-events: none; + transition: width 0.35s var(--ease-out), height 0.35s var(--ease-out), margin 0.35s var(--ease-out); + } + .cursor__c { + position: absolute; width: 8px; height: 8px; + border: 1px solid var(--neon); box-shadow: 0 0 8px rgba(61, 242, 141, 0.45); + transition: border-color 0.3s, width 0.3s var(--ease-out), height 0.3s var(--ease-out); + } + .cursor__c.tl { top: 0; left: 0; border-right: 0; border-bottom: 0; } + .cursor__c.tr { top: 0; right: 0; border-left: 0; border-bottom: 0; } + .cursor__c.bl { bottom: 0; left: 0; border-right: 0; border-top: 0; } + .cursor__c.br { bottom: 0; right: 0; border-left: 0; border-top: 0; } + .cursor__label { + position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(0.6); + font-size: 10px; letter-spacing: 0.18em; color: #032b16; + opacity: 0; transition: 0.25s var(--ease-out); font-weight: 600; white-space: nowrap; + } + .cursor.is-hover { width: 56px; height: 56px; margin: -28px 0 0 -28px; } + .cursor.is-hover .cursor__c { width: 13px; height: 13px; } + .cursor.is-drag { width: 78px; height: 78px; margin: -39px 0 0 -39px; } + .cursor.is-drag .cursor__label { opacity: 1; transform: translate(-50%, -50%) scale(1); } + .cursor-dot { + display: block; position: fixed; z-index: 301; top: 0; left: 0; + width: 3px; height: 3px; margin: -1.5px 0 0 -1.5px; + background: var(--neon); pointer-events: none; box-shadow: 0 0 8px var(--glow); + } + .cursor-addr { + display: block; position: fixed; z-index: 300; top: 0; left: 0; + margin: 14px 0 0 16px; pointer-events: none; + font-size: 9px; letter-spacing: 0.2em; color: rgba(61, 242, 141, 0.66); + white-space: nowrap; text-shadow: 0 0 12px rgba(61, 242, 141, 0.35); + transition: color 0.5s; + } + /* 逐幕换装:年度总结转琥珀,本机幕四角合拢成封印 */ + .cursor[data-act="05"] .cursor__c { border-color: var(--amber); box-shadow: 0 0 8px rgba(255, 194, 75, 0.45); } + .cursor-addr[data-act="05"] { color: rgba(255, 194, 75, 0.72); text-shadow: 0 0 12px rgba(255, 194, 75, 0.35); } + .cursor-dot[data-act="05"] { background: var(--amber); box-shadow: 0 0 8px rgba(255, 194, 75, 0.7); } + .cursor[data-act="06"] .cursor__c { transform: rotate(45deg); } + .cursor[data-act="02"] .cursor__c { width: 6px; height: 6px; } +} + +.nav { + position: fixed; z-index: 100; top: 0; left: 0; right: 0; + display: flex; align-items: center; justify-content: space-between; + padding: 22px clamp(20px, 3.4vw, 46px); +} +.nav__brand { display: flex; align-items: center; gap: 12px; } +.nav__logo { width: 30px; height: 30px; border-radius: 8px; filter: drop-shadow(0 0 10px rgba(7, 193, 96, 0.5)); } +.nav__word { font-size: 12px; letter-spacing: 0.14em; color: var(--ink-dim); } +.nav__links { display: flex; gap: 10px; align-items: center; } +.nav__gh, .nav__dl { + display: inline-flex; align-items: center; gap: 8px; + font-size: 12px; letter-spacing: 0.08em; + padding: 9px 16px; border: 1px solid var(--line-strong); border-radius: 99px; + color: var(--ink); background: rgba(5, 8, 7, 0.55); backdrop-filter: blur(12px); + transition: border-color 0.3s, background 0.3s, color 0.3s; +} +.nav__gh:hover { border-color: var(--neon); color: var(--neon); } +.nav__dl { background: var(--neon); border-color: var(--neon); color: #04140b; font-weight: 600; } +.nav__dl:hover { background: #63ffab; } + +.rail { + position: fixed; z-index: 100; right: clamp(16px, 2.4vw, 34px); bottom: 6vh; + display: flex; flex-direction: column; align-items: center; gap: 14px; +} +.rail__idx { font-size: 11px; letter-spacing: 0.1em; color: var(--ink-dim); writing-mode: vertical-rl; } +.rail__idx em { font-style: normal; color: var(--ink-faint); } +.rail__idx span { color: var(--neon); } +.rail__line { width: 1px; height: 30vh; background: var(--line); position: relative; overflow: hidden; } +.rail__line i { + position: absolute; top: 0; left: 0; width: 100%; height: 100%; + background: var(--neon); transform: scaleY(0); transform-origin: top; + box-shadow: 0 0 8px var(--glow); display: block; +} + +/* shared bits */ +main { position: relative; z-index: 1; } +.act { position: relative; } +.sec-tag { + display: flex; align-items: center; gap: 12px; + font-size: 12px; letter-spacing: 0.22em; color: var(--ink-dim); +} +.tick { width: 34px; height: 1px; background: var(--neon); box-shadow: 0 0 8px var(--glow); display: inline-block; } +.hl { color: var(--neon); } + +/* ─────────────────────────── act 01 · hero ─────────────────────────── */ + +.hero { position: relative; min-height: 100vh; overflow: clip; } +.hero__vert { + position: absolute; z-index: 2; left: clamp(14px, 2.4vw, 38px); top: 50%; + transform: translateY(-50%); + writing-mode: vertical-rl; text-orientation: mixed; + font-size: 10px; letter-spacing: 0.34em; color: var(--ink-faint); +} +.hero__coord { + position: absolute; z-index: 2; right: clamp(20px, 3.2vw, 46px); top: 16vh; + text-align: right; font-size: 10px; letter-spacing: 0.2em; line-height: 2.2; + color: var(--ink-faint); +} +.hero__coord span { display: block; white-space: nowrap; } +.hero__coord #hc-1.is-live { color: var(--neon); } +.hero__title { + position: absolute; z-index: 2; left: 50%; top: 41%; + transform: translate(-50%, -50%); + width: min(88vw, 1520px); + font-weight: 900; line-height: 1.02; letter-spacing: -0.015em; + /* 暗辉光:把巨字从粒子云里托出来,又不落成一块半透明底板 */ + text-shadow: 0 2px 30px rgba(4, 8, 6, 0.9), 0 0 72px rgba(4, 8, 6, 0.55); +} +.ht-line { display: block; overflow: hidden; padding-block: 0.08em; margin-block: -0.08em; } +.ht-mask { display: inline-block; will-change: transform; } +.ht-line--1 { text-align: left; font-size: clamp(2.2rem, 6.4vw, 6rem); } +.ht-line--2 { text-align: right; font-size: clamp(3.4rem, 12.8vw, 12.4rem); margin-top: 0.02em; } +/* 实心霓虹(描边中文在这个字号下会显乱),与终幕统一 */ +.hero__title em { color: var(--neon); text-shadow: 0 0 52px rgba(61, 242, 141, 0.5), 0 2px 26px rgba(4, 8, 6, 0.8); } + +/* 开屏解密装置:密文巨字 + 横扫的解密光刃 */ +.hero__decrypt { + --bw: clamp(90px, 10vw, 180px); + position: absolute; z-index: 4; top: -38%; bottom: -38%; left: 0; + width: var(--bw); margin-left: calc(-1 * var(--bw)); + pointer-events: none; opacity: 0; will-change: transform; + /* 辉光用贴在前缘的椭圆径向层,不用 box-shadow/drop-shadow —— 那两者都会露出矩形边 */ + background: + radial-gradient(58% 46% at 100% 50%, rgba(61, 242, 141, 0.3), transparent 72%), + linear-gradient(90deg, transparent 0%, rgba(61, 242, 141, 0.05) 56%, rgba(61, 242, 141, 0.34) 89%, rgba(222, 255, 237, 0.95) 99%, transparent 100%); + /* 上下渐隐成一道光刃,而不是一块方光 */ + -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 30%, #000 70%, transparent 100%); + mask-image: linear-gradient(180deg, transparent 0%, #000 30%, #000 70%, transparent 100%); +} +.hero__title .hc { display: inline-block; width: 1em; text-align: center; } +.hero__title .hc.is-cipher { + color: rgba(61, 242, 141, 0.42); text-shadow: 0 0 26px rgba(4, 8, 6, 0.9); + font-family: var(--font-mono); font-weight: 600; +} +.hero__title .hc.is-solved { animation: hcSolve 0.5s var(--ease-out); } +@keyframes hcSolve { + 0% { color: #eafff2; text-shadow: 0 0 46px rgba(61, 242, 141, 0.95); transform: scale(1.09); } + 100% { color: inherit; transform: none; } +} +.hero__sub { + position: absolute; z-index: 2; left: clamp(20px, 6vw, 92px); bottom: 20vh; + font-size: clamp(12px, 1.35vw, 15px); letter-spacing: 0.14em; color: var(--ink-dim); +} +.hero__cta { + position: absolute; z-index: 3; right: clamp(20px, 6vw, 92px); bottom: 14vh; + display: flex; flex-direction: column; align-items: flex-end; gap: 22px; +} +.hero__orb { + position: relative; width: clamp(116px, 11.5vw, 152px); aspect-ratio: 1; + border-radius: 50%; display: grid; place-items: center; text-align: center; + font-size: clamp(1.05rem, 1.5vw, 1.35rem); font-weight: 900; letter-spacing: 0.16em; line-height: 1.6; + color: var(--ink); background: rgba(61, 242, 141, 0.06); + backdrop-filter: blur(6px); + transition: background 0.4s, color 0.4s; + will-change: transform; +} +.hero__orb i { display: block; font-style: normal; font-size: 9.5px; letter-spacing: 0.24em; opacity: 0.7; margin-top: 4px; } +.hero__orb-ring { position: absolute; inset: -3px; color: rgba(61, 242, 141, 0.7); animation: spin 12s linear infinite; } +.hero__orb-ring circle { stroke-dasharray: 12 8; } +@keyframes spin { to { transform: rotate(360deg); } } +.hero__orb:hover { background: var(--neon); color: #04140b; } +.hero__orb span { position: relative; z-index: 1; } +.hero__gh { + font-size: 12px; letter-spacing: 0.16em; color: var(--ink-dim); + border-bottom: 1px dashed var(--line-strong); padding-bottom: 5px; + transition: color 0.3s, border-color 0.3s; +} +.hero__gh i { font-style: normal; color: var(--neon); } +.hero__gh:hover { color: var(--neon); border-color: var(--neon); } +.hero__ticker { + position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; + border-top: 1px solid var(--line); padding: 15px 0; + overflow: clip; background: rgba(5, 8, 7, 0.4); backdrop-filter: blur(8px); +} +.hero__ticker-row { + display: flex; align-items: center; gap: 42px; width: max-content; + font-size: 11px; letter-spacing: 0.2em; color: var(--ink-faint); + will-change: transform; +} +.hero__ticker-row i { font-style: normal; color: var(--neon); opacity: 0.6; } + +/* ─────────────────────────── act 02 · manifesto ─────────────────────────── */ + +.manifesto__pin { + height: 100vh; display: grid; place-items: center; text-align: center; + padding: 0 clamp(20px, 6vw, 80px); position: relative; overflow: hidden; +} + +/* 第二幕装置:真实消息浮起 → 被加密锋线吞没 → 结成一堵密文墙 */ +.mf__field { position: absolute; inset: 0; z-index: 1; pointer-events: none; } +.mf__wall { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 2; pointer-events: none; } +/* 风暴里的一条消息:0.18s 弹入(与年度总结 storm 同款) */ +.mfrag { + position: absolute; display: grid; text-align: left; transform-origin: center; + opacity: 0; transform: translate3d(0, 10px, 0) scale(0.94); + transition: opacity 0.18s ease-out, transform 0.18s ease-out; + will-change: transform, opacity; +} +.mfrag.is-in { opacity: 1; transform: translate3d(0, 0, 0) scale(1); } +.mfrag > * { grid-area: 1 / 1; } +.mfrag .wc-bubble { max-width: none; font-size: 13px; padding: 8px 11px; line-height: 1.45; } +.mfrag .wc-img { width: 96px; height: 68px; } +.mfrag .wc-redpacket { width: 186px; } +.mfrag .wc-rp-main { min-height: 44px; padding: 8px 10px; } +.mfrag .wc-rp-main b { font-size: 11.5px; white-space: nowrap; } +.mfrag .wc-rp-main i { font-size: 10px; } +.mfrag .wc-rp-foot { font-size: 9.5px; } +.mfrag .wc-rp-ic { font-size: 20px; } +.mfrag__cipher { + align-self: center; justify-self: center; + font-style: normal; font-size: 12px; letter-spacing: 0.28em; white-space: nowrap; + color: var(--neon); text-shadow: 0 0 18px var(--glow); opacity: 0; +} + +.m-line { + position: absolute; max-width: 1100px; z-index: 3; + font-family: var(--font-serif); font-weight: 900; + font-size: clamp(2rem, 5.6vw, 4.6rem); line-height: 1.35; letter-spacing: 0.01em; + text-wrap: balance; + text-shadow: 0 2px 28px rgba(4, 8, 6, 0.92), 0 0 62px rgba(4, 8, 6, 0.7); +} +.m-line--up { align-self: start; margin-top: 8vh; } +.m-line--last { font-size: clamp(3rem, 9vw, 8rem); line-height: 1.1; } +.m-line--last em { color: var(--neon); text-shadow: 0 0 60px rgba(61, 242, 141, 0.55); } + +/* ─────────────────────────── act 03 · decrypt ─────────────────────────── */ + +.decrypt__pin { height: 100vh; position: relative; overflow: hidden; background: radial-gradient(120% 90% at 50% 10%, rgba(7, 16, 11, 0.35), rgba(5, 8, 7, 0.92) 70%); } +.decrypt__hex { position: absolute; inset: 0; width: 100%; height: 100%; } +/* 上下压暗,让 HUD 与 .db 清单从字符场里坐稳(不是一块可见的底板) */ +.decrypt__pin::after { + content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none; + background: + linear-gradient(180deg, rgba(5, 8, 7, 0.9) 0%, rgba(5, 8, 7, 0) 20%), + linear-gradient(0deg, rgba(5, 8, 7, 0.93) 0%, rgba(5, 8, 7, 0) 18%); +} +.decrypt__hud { + position: absolute; z-index: 5; top: 96px; left: clamp(20px, 5vw, 70px); right: clamp(20px, 5vw, 70px); + display: flex; justify-content: space-between; align-items: baseline; + font-size: 11px; letter-spacing: 0.18em; color: var(--ink-dim); +} +.dhud__left { display: flex; gap: 28px; } +#dstep-file { color: var(--ink-faint); } +.dhud__right { font-size: clamp(1.6rem, 3.4vw, 2.6rem); color: var(--neon); font-weight: 600; text-shadow: 0 0 24px rgba(61, 242, 141, 0.4); font-variant-numeric: tabular-nums; } + +/* 承接第二幕的墙:中线插钥匙 → 解密波向两侧推开。文案退为 HUD 与巨字,不再左文右图 */ +.dtitle { + position: absolute; z-index: 5; left: 0; right: 0; top: 17vh; text-align: center; + font-size: min(clamp(2.1rem, 5.4vw, 4.6rem), 8.6vh); font-weight: 900; line-height: 1.1; letter-spacing: -0.015em; + text-shadow: 0 2px 30px rgba(4, 8, 6, 0.94), 0 0 66px rgba(4, 8, 6, 0.72); +} +.dsub { + position: absolute; z-index: 5; left: 0; right: 0; top: calc(17vh + min(clamp(2.6rem, 6.4vw, 5.6rem), 10.4vh)); + text-align: center; font-size: clamp(12px, 1.2vw, 15px); letter-spacing: 0.14em; color: var(--ink-dim); + text-shadow: 0 2px 22px rgba(4, 8, 6, 0.95); +} +.dkey { + position: absolute; z-index: 6; left: 50%; top: 50%; transform: translate(-50%, -50%); + font-size: clamp(8px, 0.74vw, 12px); letter-spacing: 0.26em; white-space: nowrap; + color: rgba(92, 106, 99, 0.75); +} +.dkey b { font-weight: 400; color: var(--neon); text-shadow: 0 0 16px var(--glow); } +.ddbs { + position: absolute; z-index: 5; left: 0; right: 0; bottom: 5.5vh; + display: flex; justify-content: center; flex-wrap: wrap; gap: 9px clamp(14px, 2.2vw, 34px); + list-style: none; padding: 0 20px; + font-size: 11px; letter-spacing: 0.14em; color: var(--ink-faint); + text-shadow: 0 2px 18px rgba(4, 8, 6, 0.95); +} +.ddbs li { display: flex; align-items: center; gap: 8px; transition: color 0.45s; } +.ddbs li i { width: 6px; height: 6px; border-radius: 50%; background: var(--line-strong); transition: background 0.45s, box-shadow 0.45s; } +.ddbs li.on { color: var(--ink); } +.ddbs li.on i { background: var(--neon); box-shadow: 0 0 10px var(--glow); } + +/* 终点不是一个"模拟窗口",而是墙被揭开后露出的产品本体:满幅、无窗框, + 用产品自己的暗色主题令牌(--chat-page-bg #191919 / sent #3eb575 / received #2e2e2e) */ +.decrypt__chat { + position: absolute; z-index: 4; left: 0; right: 0; top: 42%; bottom: 9.5vh; + background: #191919; border-top: 1px solid rgba(61, 242, 141, 0.5); + box-shadow: 0 -22px 70px rgba(0, 0, 0, 0.7), 0 -1px 30px rgba(61, 242, 141, 0.18); + display: flex; flex-direction: column; + padding: clamp(10px, 1.6vh, 18px) clamp(20px, 6vw, 90px) clamp(8px, 1.4vh, 16px); + overflow: hidden; +} +.dchat__tag { + flex: none; align-self: flex-end; font-size: 9.5px; letter-spacing: 0.22em; + color: #6f6f6f; margin-bottom: clamp(4px, 0.8vh, 10px); +} +.dchat__body { + flex: 1; min-height: 0; width: min(760px, 100%); margin: 0 auto; + display: flex; flex-direction: column; justify-content: center; + gap: clamp(5px, 0.95vh, 10px); +} +/* 产品暗色主题令牌 */ +.decrypt__chat .wc-time { color: #9f9f9f; font-size: clamp(9.5px, 1.2vh, 11px); } +.decrypt__chat .wc-bubble { font-size: clamp(11.5px, 1.5vh, 13.5px); padding: clamp(6px, 0.95vh, 9px) 12px; } +.decrypt__chat .wc-bubble--l { background: #2e2e2e; color: #f5f5f5; } +.decrypt__chat .wc-bubble--l::before { background: #2e2e2e; } +.decrypt__chat .wc-bubble--r { background: #3eb575; color: #fff; } +.decrypt__chat .wc-bubble--r::after { background: #3eb575; } +.decrypt__chat .wc-avatar { width: clamp(26px, 3.8vh, 34px); height: clamp(26px, 3.8vh, 34px); font-size: clamp(11px, 1.5vh, 13px); } +.decrypt__chat .wc-img { width: clamp(80px, 11vh, 104px); height: clamp(56px, 8vh, 74px); background: #2e2e2e; color: #6f6f6f; } +.decrypt__chat .wc-rp-main { min-height: clamp(42px, 6vh, 54px); } +.decrypt__chat .wc-unread { right: -14px; } +.wc-time { text-align: center; font-size: 11px; color: #9b9b9b; } +.wc-row { display: flex; gap: 10px; align-items: flex-start; } +.wc-row--r { flex-direction: row-reverse; } +.wc-avatar { + width: 34px; height: 34px; border-radius: 4px; flex: none; + background: var(--av, #5d6a75); color: #fff; + display: grid; place-items: center; font-size: 13px; font-weight: 500; +} +.wc-bubble { + position: relative; max-width: 68%; padding: 9px 12px; + border-radius: 4px; font-size: 13.5px; line-height: 1.5; color: #191919; +} +.wc-bubble--l { background: #fff; } +.wc-bubble--l::before { + content: ""; position: absolute; top: 13px; left: -4px; + width: 10px; height: 10px; transform: rotate(45deg); + background: #fff; border-radius: 2px; +} +.wc-bubble--r { background: #95ec69; color: #000; } /* 产品令牌 --chat-bubble-sent-text */ +.wc-bubble--r::after { + content: ""; position: absolute; top: 13px; right: -4px; + width: 10px; height: 10px; transform: rotate(45deg); + background: #95ec69; border-radius: 2px; +} +.wc-voice { display: flex; align-items: center; gap: 8px; min-width: 84px; } +.wc-voice-ic { width: 17px; height: 17px; flex: none; } +.wc-voice .voice-wave-2 { animation: voice-wave-2 1.2s infinite; } +.wc-voice .voice-wave-3 { animation: voice-wave-3 1.2s infinite; } +@keyframes voice-wave-2 { 0%, 33% { opacity: 0; } 34%, 100% { opacity: 1; } } +@keyframes voice-wave-3 { 0%, 66% { opacity: 0; } 67%, 100% { opacity: 1; } } +.wc-unread { + position: absolute; right: -15px; top: 50%; margin-top: -3.5px; + width: 7px; height: 7px; border-radius: 50%; background: #f43530; +} +.wc-img { + width: 128px; height: 92px; border-radius: 4px; + background: #e4e6e8; color: #8a919b; display: grid; place-items: center; +} +.wc-img svg { width: 30px; height: 30px; } +.wc-redpacket { width: 205px; border-radius: 4px; background: #fa9d3b; color: #fff; position: relative; } +.wc-redpacket::before { + content: ""; position: absolute; top: 13px; left: -4px; + width: 10px; height: 10px; transform: rotate(45deg); + background: #fa9d3b; border-radius: 2px; +} +.wc-rp-main { display: flex; align-items: center; gap: 10px; padding: 10px 12px; min-height: 54px; } +.wc-rp-ic { font-size: 25px; line-height: 1; } +.wc-rp-main b { display: block; font-size: 13.5px; font-weight: 500; } +.wc-rp-main i { display: block; font-style: normal; font-size: 11px; opacity: 0.85; margin-top: 2px; } +.wc-rp-foot { position: relative; padding: 5px 12px 6px; font-size: 10.5px; opacity: 0.82; } +.wc-rp-foot::before { + content: ""; position: absolute; top: 0; left: 12px; right: 12px; + height: 1px; background: rgba(255, 255, 255, 0.25); +} + +/* ─────────────────────────── act 04 · features ─────────────────────────── */ + +.features__pin { + height: 100vh; overflow: hidden; position: relative; + background: linear-gradient(180deg, rgba(5, 8, 7, 0.5), rgba(5, 8, 7, 0.88)); +} + +/* 功能区 = 编辑式跨页:左侧大字文栏(刊头/巨号/标题/引文)+ 右侧贴边堆叠截图卡 */ +.deck { + position: absolute; inset: 0; + display: grid; grid-template-columns: minmax(260px, 31%) 1fr; align-items: center; + gap: clamp(30px, 5vw, 96px); + padding: clamp(80px, 12vh, 130px) clamp(28px, 5vw, 100px) clamp(48px, 8vh, 96px); +} + +/* 左:编辑文栏 */ +.deck__text { display: flex; flex-direction: column; } +.deck__kicker { display: flex; align-items: center; gap: 12px; font-size: 11px; letter-spacing: 0.26em; color: var(--ink-dim); } +.deck__kicker .tick { width: 30px; } +.deck__num { + font-family: var(--font-disp); font-weight: 900; line-height: 0.76; + font-size: clamp(4.2rem, 9vw, 9rem); margin: clamp(18px, 3.4vh, 40px) 0 clamp(16px, 2.6vh, 30px); + color: transparent; -webkit-text-stroke: 1.6px rgba(61, 242, 141, 0.72); + text-shadow: 0 0 46px rgba(61, 242, 141, 0.2); font-variant-numeric: tabular-nums; +} +.deck__name { font-size: clamp(2rem, 3.4vw, 3.5rem); font-weight: 900; line-height: 1.04; letter-spacing: -0.03em; } +.deck__desc { margin-top: clamp(16px, 2.4vh, 26px); font-size: clamp(13px, 1.05vw, 15.5px); line-height: 1.75; color: var(--ink-dim); max-width: 40ch; } +.deck__meta { margin-top: clamp(20px, 3vh, 34px); padding-top: 16px; border-top: 1px solid var(--line); font-size: 10.5px; letter-spacing: 0.18em; color: var(--neon); opacity: 0.9; } +.deck__dots { display: flex; gap: 7px; align-items: center; margin-top: 18px; } +.deck__dots i { width: 16px; height: 2px; background: var(--line-strong); transition: width 0.4s var(--ease-out), background 0.4s, box-shadow 0.4s; } +.deck__dots i.on { width: 34px; background: var(--neon); box-shadow: 0 0 8px var(--glow); } + +/* 右:堆叠截图卡 */ +.deck__stack { position: relative; width: 100%; aspect-ratio: 1919 / 866; align-self: center; } +.deck__card { + position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); + width: 100%; aspect-ratio: 1919 / 866; + border-radius: 14px; overflow: hidden; background: #0d130f; + border: 1px solid var(--line-strong); + box-shadow: 0 60px 150px rgba(0, 0, 0, 0.72), 0 0 100px rgba(7, 193, 96, 0.08), inset 0 0 0 1px rgba(61, 242, 141, 0.06); + will-change: transform; backface-visibility: hidden; +} +.deck__card img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; } +.deck__card--code { display: grid; place-content: center; background: #060a08; padding: clamp(18px, 3vw, 52px); } +.deck__card--code pre { font-size: clamp(12px, 1.4vw, 18px); line-height: 2.4; } +.c-dim { color: var(--ink-faint); } +.c-green { color: var(--neon); } +.c-ok { color: var(--green); font-weight: 600; } + +/* ─────────────────────────── act 05 · wrapped ─────────────────────────── */ + +.wrapped { background: linear-gradient(180deg, rgba(5, 8, 7, 0) 0%, rgba(8, 14, 11, 0.6) 18%, rgba(5, 8, 7, 0.2) 100%); } +.wrapped__pin { height: 100vh; overflow: hidden; position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; } +.wrapped__intro { position: absolute; z-index: 6; top: 12vh; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 16px; } +.wrapped__title { + font-family: var(--font-serif); font-weight: 900; + font-size: clamp(2.1rem, 5.4vw, 4.6rem); line-height: 1.25; letter-spacing: 0.01em; +} +.wrapped__title em { color: var(--amber); text-shadow: 0 0 46px rgba(255, 194, 75, 0.4); } + +/* 年度总结 = 居中宽银幕影厅:大幅影格 + 上下细胶片条(HUD / 进度轴),底部给滚动数字留位 */ +.viewer { + --film: min(88vw, calc(56vh * 2.216), 1480px); + position: absolute; inset: 0; z-index: 3; + display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: clamp(12px, 1.8vh, 20px); + padding: clamp(54px, 8vh, 90px) clamp(20px, 4vw, 70px) clamp(108px, 17vh, 152px); +} +.cinema__hud { width: var(--film); display: flex; align-items: center; justify-content: space-between; font-size: 11px; letter-spacing: 0.24em; color: var(--ink-dim); } +.cinema__hud .hudl { display: flex; align-items: center; gap: 12px; } +.cinema__hud .tick { width: 28px; } +.cinema__hud b { font-family: var(--font-disp); font-weight: 900; color: var(--neon); font-size: 16px; letter-spacing: 0; text-shadow: 0 0 20px rgba(61, 242, 141, 0.35); } +.cinema__hud i { font-style: normal; color: var(--ink-faint); margin-left: 4px; } +.viewer__frame { + position: relative; width: var(--film); flex: none; + border-radius: 18px; overflow: hidden; + border: 1px solid rgba(61, 242, 141, 0.3); + background: #0a0f0c; padding: clamp(9px, 0.9vw, 15px); + box-shadow: 0 60px 140px rgba(0, 0, 0, 0.72), 0 0 100px rgba(61, 242, 141, 0.13), inset 0 0 60px rgba(0, 0, 0, 0.5); +} +.viewer__frame::after { + content: ""; position: absolute; inset: clamp(9px, 0.9vw, 15px); z-index: 5; pointer-events: none; border-radius: 10px; + background: + repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.05) 0 1px, transparent 1px 3px), + radial-gradient(135% 115% at 50% 48%, transparent 58%, rgba(5, 8, 7, 0.55) 100%); +} +.viewer__slides { position: relative; aspect-ratio: 1000 / 452; background: #0d130f; border-radius: 10px; overflow: hidden; } +.cinema__foot { width: var(--film); display: flex; align-items: center; gap: clamp(16px, 2vw, 34px); } +.viewer__ticks { display: flex; gap: 6px; flex: 1; } +.viewer__ticks i { height: 2px; flex: 1; background: var(--line-strong); transition: background 0.35s, box-shadow 0.35s; } +.viewer__ticks i.on { background: var(--neon); box-shadow: 0 0 10px var(--glow); } + +.viewer__slides img { + position: absolute; inset: 0; width: 100%; height: 100%; + object-fit: cover; object-position: top; display: block; + will-change: clip-path, transform, opacity, filter; +} + +.wrapped__stats { + position: absolute; z-index: 6; bottom: 11vh; + display: flex; gap: clamp(20px, 4.5vw, 74px); flex-wrap: wrap; justify-content: center; + padding: 0 20px; +} +.wstat { text-align: center; } +.wstat b { + display: block; font-size: clamp(1.7rem, 3.6vw, 3.1rem); font-weight: 600; line-height: 1.1; + color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; + text-shadow: 0 0 30px rgba(61, 242, 141, 0.25); +} +.wstat span { display: block; margin-top: 8px; font-size: 12px; color: var(--ink-dim); letter-spacing: 0.08em; } +.wrapped__note { position: absolute; z-index: 6; bottom: 4.5vh; font-size: 10.5px; letter-spacing: 0.14em; color: var(--ink-faint); text-align: center; padding: 0 20px; } + +/* ─────────────────────────── act 06 · privacy ─────────────────────────── */ + +.privacy__pin { + height: 100vh; position: relative; overflow: hidden; + display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: clamp(16px, 3.2vh, 32px); + padding: clamp(70px, 9vh, 100px) clamp(20px, 5vw, 80px) 0; +} +.privacy__claim { font-size: clamp(2rem, 4.7vw, 4.3rem); font-weight: 900; line-height: 1.15; letter-spacing: -0.01em; text-align: center; } +.privacy__claim em { color: var(--neon); text-shadow: 0 0 46px rgba(61, 242, 141, 0.45); } + +.fw { display: flex; align-items: center; justify-content: center; gap: clamp(24px, 4vw, 72px); width: 100%; } +.fwchips { display: flex; flex-direction: column; gap: clamp(20px, 4vh, 36px); width: clamp(190px, 22vw, 300px); flex: none; } +.fwchips--l { align-items: flex-end; text-align: right; } +.fwchip code { display: block; font-family: var(--font-mono); font-size: clamp(12px, 1.15vw, 14px); letter-spacing: 0.04em; color: var(--ink); } +.fwchip code b { color: var(--neon); font-weight: 600; } +.fwchip span { display: block; margin-top: 7px; font-size: 11px; letter-spacing: 0.1em; color: var(--ink-faint); } + +.fw__stage { --fwr: min(20.5vh, 186px); position: relative; width: min(46vh, 420px); aspect-ratio: 1; flex: none; } +.fw__ring { position: absolute; inset: 0; color: rgba(61, 242, 141, 0.55); overflow: visible; } +.fw__sweep { + position: absolute; inset: 4.5%; border-radius: 50%; + background: conic-gradient(from 0deg, rgba(61, 242, 141, 0.26), transparent 74deg); + -webkit-mask: radial-gradient(circle, transparent 40%, #000 41%); + mask: radial-gradient(circle, transparent 40%, #000 41%); + animation: fwspin 4.6s linear infinite; +} +@keyframes fwspin { to { transform: rotate(360deg); } } +.fw__blip { + position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; margin: -3px 0 0 -3px; + border-radius: 50%; background: var(--amber); + box-shadow: 0 0 12px rgba(255, 194, 75, 0.85); + opacity: 0; animation: fwblip 3.4s ease-in infinite; animation-delay: var(--d); +} +@keyframes fwblip { + 0% { opacity: 0; transform: rotate(var(--a)) translateY(0) scale(0.5); } + 10% { opacity: 0.95; } + 68% { opacity: 0.95; transform: rotate(var(--a)) translateY(calc(var(--fwr) * -0.86)) scale(1); background: var(--amber); } + 80% { opacity: 1; transform: rotate(var(--a)) translateY(calc(var(--fwr) * -0.97)) scale(1.9); background: var(--red); box-shadow: 0 0 16px rgba(255, 93, 93, 0.95); } + 100% { opacity: 0; transform: rotate(var(--a)) translateY(calc(var(--fwr) * -1)) scale(0.2); } +} +.fw__zero { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; gap: 7px; z-index: 2; } +.fw__zero b { + font-family: var(--font-disp); font-weight: 900; line-height: 1; + font-size: clamp(3.2rem, 9vh, 5.4rem); color: var(--ink); + text-shadow: 0 0 44px rgba(61, 242, 141, 0.45); + font-variant-numeric: tabular-nums; +} +.fw__zero span { font-size: 9.5px; letter-spacing: 0.3em; color: var(--ink-faint); } +.fw__zero em { margin-top: 6px; font-size: 11px; letter-spacing: 0.22em; color: var(--green); font-weight: 600; opacity: 0; } +.privacy__legal { position: absolute; bottom: 3.4vh; left: 0; right: 0; text-align: center; font-size: 11px; letter-spacing: 0.18em; color: var(--ink-faint); } + +/* ─────────────────────────── act 07 · stack ─────────────────────────── */ + +.stack { overflow: clip; background: linear-gradient(180deg, transparent, rgba(7, 11, 9, 0.72) 30%, transparent); } +.stack__pin { + min-height: 100vh; display: flex; flex-direction: column; justify-content: center; + gap: clamp(20px, 3.6vh, 38px); padding: clamp(84px, 12vh, 130px) 0 clamp(40px, 6vh, 70px); +} +.stack__head { padding: 0 clamp(20px, 6vw, 90px); display: flex; flex-direction: column; gap: 14px; } +.stack__title { font-size: clamp(1.9rem, 4vw, 3.6rem); font-weight: 900; line-height: 1.15; letter-spacing: -0.01em; } +.stack__title em { color: var(--neon); } + +.river-wrap { position: relative; width: min(1280px, 94vw); margin: 0 auto 26px; } +#river { width: 100%; height: clamp(280px, 42vh, 430px); display: block; } +.river-gates span { + position: absolute; top: calc(100% + 8px); transform: translateX(-50%); + text-align: center; font-size: 10.5px; letter-spacing: 0.2em; color: var(--ink-dim); + white-space: nowrap; +} +.river-gates i { display: block; font-style: normal; margin-top: 4px; font-size: 9px; letter-spacing: 0.16em; color: var(--ink-faint); } +.river-exits { + position: absolute; right: 0; top: 7%; bottom: 7%; + display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; + pointer-events: none; +} +.river-exits span { + display: inline-flex; align-items: center; gap: 8px; + padding: 8px 15px; border: 1px solid var(--line-strong); border-radius: 99px; + background: rgba(7, 11, 9, 0.88); backdrop-filter: blur(6px); + font-size: 11px; letter-spacing: 0.12em; color: var(--ink); +} +.river-exits span i { width: 6px; height: 6px; border-radius: 50%; background: var(--neon); box-shadow: 0 0 8px var(--glow); } + +.stack__hud { + list-style: none; display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 1px; + background: var(--line); border-block: 1px solid var(--line); + margin-top: clamp(14px, 2.6vh, 26px); +} +.stack__hud li { background: var(--bg); padding: 16px clamp(16px, 2.4vw, 30px); font-size: 11px; line-height: 1.85; color: var(--ink-dim); letter-spacing: 0.05em; } +.stack__hud b { display: block; color: var(--neon); font-size: 9.5px; letter-spacing: 0.26em; margin-bottom: 5px; } + +/* ─────────────────────────── act 06 · the machine(隐私 × 引擎 合并)─────────────────────────── */ + +.mac__pin { + height: 100vh; position: relative; overflow: hidden; + display: grid; grid-template-rows: auto auto 1fr auto auto; row-gap: clamp(12px, 2.2vh, 26px); + padding: clamp(84px, 12vh, 140px) clamp(20px, 6vw, 90px) clamp(26px, 4vh, 54px); +} +.mac__title { margin-top: 14px; font-size: clamp(1.9rem, 4.2vw, 3.7rem); font-weight: 900; line-height: 1.14; letter-spacing: -0.01em; } +.mac__title em { color: var(--neon); text-shadow: 0 0 42px rgba(61, 242, 141, 0.4); } + +/* 演算台与定格共用同一行,交叉淡出 */ +.mac__stage { position: relative; align-self: center; width: 100%; display: grid; } +.mac__stage > * { grid-area: 1 / 1; align-self: center; } +.mac__run { display: flex; align-items: center; gap: clamp(20px, 4vw, 64px); } +.mac__act { + flex: none; font-size: clamp(2.4rem, 6.6vw, 5.6rem); font-weight: 900; + line-height: 1; letter-spacing: -0.02em; color: var(--ink); + text-shadow: 0 0 54px rgba(61, 242, 141, 0.28); +} +.mac__trace { + flex: 1; min-width: 0; list-style: none; overflow: hidden; + height: clamp(84px, 13vh, 132px); + display: flex; flex-direction: column; justify-content: flex-end; gap: 4px; + -webkit-mask-image: linear-gradient(180deg, transparent, #000 42%); + mask-image: linear-gradient(180deg, transparent, #000 42%); +} +.mac__trace li { + font-size: 11px; letter-spacing: 0.08em; color: var(--ink-faint); white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; +} +.mac__trace li:last-child { color: var(--neon); } + +.mac__meters { + display: flex; flex-wrap: wrap; gap: clamp(16px, 4vw, 72px); + border-top: 1px solid var(--line); padding-top: clamp(12px, 2vh, 20px); +} +.mac__meter { display: flex; flex-direction: column; gap: 6px; } +.mac__meter span { font-size: 9.5px; letter-spacing: 0.24em; color: var(--ink-faint); } +.mac__meter b { + font-size: clamp(1.2rem, 2.4vw, 2rem); font-weight: 600; letter-spacing: -0.01em; + color: var(--ink); font-variant-numeric: tabular-nums; +} +.mac__meter--out { margin-left: auto; text-align: right; } +.mac__meter--out b { color: var(--neon); text-shadow: 0 0 26px rgba(61, 242, 141, 0.4); } + +.mac__hero { display: flex; align-items: center; gap: clamp(18px, 2.4vw, 34px); } +.mac__seal { position: relative; flex: none; } +.mac__zero { + font-family: var(--font-disp); font-weight: 900; line-height: 0.78; display: block; + font-size: min(clamp(4.5rem, 13vw, 12rem), 26vh); color: var(--ink); + text-shadow: 0 0 70px rgba(61, 242, 141, 0.45); font-variant-numeric: tabular-nums; +} +.mac__sealtxt { display: flex; flex-direction: column; gap: 10px; } +.mac__sealtxt span { font-size: 12px; letter-spacing: 0.16em; color: var(--ink-faint); } +.mac__sealtxt em { font-size: 13px; letter-spacing: 0.14em; color: var(--green); font-weight: 600; font-style: normal; } + +.mac__guards { list-style: none; display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(20px, 3vw, 50px); border-top: 1px solid var(--line); padding-top: clamp(20px, 3vh, 32px); } +.mac__guards li { position: relative; padding-left: 16px; } +.mac__guards li::before { content: ""; position: absolute; left: 0; top: 7px; width: 6px; height: 6px; border-radius: 50%; background: var(--neon); box-shadow: 0 0 8px var(--glow); } +.mac__guards b { display: block; font-size: clamp(15px, 1.7vw, 21px); font-weight: 900; letter-spacing: -0.01em; } +.mac__guards span { display: block; margin-top: 9px; font-size: clamp(11px, 1vw, 12.5px); line-height: 1.65; color: var(--ink-dim); } + +.mac__foot { display: flex; align-items: baseline; justify-content: space-between; gap: 30px; flex-wrap: wrap; padding-top: clamp(14px, 2.4vh, 24px); border-top: 1px solid var(--line); } +.mac__stack { font-size: 11px; letter-spacing: 0.08em; color: var(--ink-dim); display: flex; align-items: baseline; gap: 14px; } +.mac__stack span { color: var(--neon); letter-spacing: 0.24em; font-size: 9.5px; } +.mac__legal { font-size: 11px; letter-spacing: 0.16em; color: var(--ink-faint); } +.mac__escapee { + position: absolute; left: 6%; top: 20%; width: 8px; height: 8px; border-radius: 50%; + background: var(--amber); box-shadow: 0 0 12px rgba(255, 194, 75, 0.85); + animation: macEscape 3s ease-in-out infinite; +} +@keyframes macEscape { + 0%, 8% { transform: translate(0, 0) scale(1); opacity: 0.9; background: var(--amber); } + 42% { transform: translate(118px, -66px) scale(1); opacity: 1; background: var(--amber); box-shadow: 0 0 12px rgba(255, 194, 75, 0.85); } + 50% { transform: translate(146px, -82px) scale(1.9); background: var(--red); box-shadow: 0 0 16px rgba(255, 93, 93, 0.95); } + 74% { transform: translate(36px, -18px) scale(1); background: var(--amber); } + 100% { transform: translate(0, 0) scale(1); opacity: 0.9; } +} + +.mac__pipe-tag { display: flex; align-items: center; gap: 10px; font-size: 11px; letter-spacing: 0.18em; color: var(--ink-dim); margin-bottom: 15px; } +.mac__pipe-tag b { margin-left: auto; font-weight: 400; font-size: 9.5px; letter-spacing: 0.2em; color: var(--neon); border: 1px solid rgba(61, 242, 141, 0.4); border-radius: 99px; padding: 3px 11px; } +.mac__track { + position: relative; display: flex; gap: 10px; padding: 15px; + border: 1px solid var(--line-strong); border-radius: 14px; background: rgba(8, 13, 10, 0.62); overflow: hidden; +} +.mac__pulse { + position: absolute; top: 0; left: -160px; width: 160px; height: 100%; z-index: 0; + background: linear-gradient(90deg, transparent, rgba(61, 242, 141, 0.16), transparent); + animation: macPulse 3s linear infinite; +} +@keyframes macPulse { to { left: 100%; } } +.mac__node { + position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; gap: 4px; + align-items: center; justify-content: center; text-align: center; padding: 14px 6px; + border-radius: 9px; background: rgba(16, 24, 19, 0.55); border: 1px solid var(--line); +} +.mac__node b { font-size: clamp(11px, 1.15vw, 14px); font-weight: 700; color: var(--ink); } +.mac__node i { font-style: normal; font-size: 9px; letter-spacing: 0.06em; color: var(--ink-faint); } +.mac__node--core { border-color: rgba(61, 242, 141, 0.6); box-shadow: inset 0 0 22px rgba(61, 242, 141, 0.08); } +.mac__node--core b { color: var(--neon); } + +.mac__ledger { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } +.mac__col { background: var(--bg); padding: clamp(15px, 2.2vw, 26px); } +.mac__col-h { display: flex; align-items: center; gap: 10px; font-size: 11px; letter-spacing: 0.18em; color: var(--ink-dim); margin-bottom: 15px; } +.mac__col ul { list-style: none; display: flex; flex-direction: column; gap: 9px; } +.mac__col li { display: flex; align-items: baseline; gap: 9px; font-size: clamp(10.5px, 1vw, 12.5px); } +.mac__col li b { color: var(--neon); font-weight: 600; } +.mac__col li i { margin-left: auto; font-style: normal; color: var(--ink-dim); font-size: 10px; letter-spacing: 0.03em; } +.mac__chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 15px; } +.mac__chips span { font-size: 10.5px; letter-spacing: 0.03em; color: var(--ink-dim); padding: 4px 10px; border: 1px solid var(--line-strong); border-radius: 99px; } +.mac__compat li b { display: inline-block; min-width: 32px; color: var(--neon); letter-spacing: 0.12em; } +.mac__legal { text-align: center; font-size: 11px; letter-spacing: 0.16em; color: var(--ink-faint); } + +/* ─────────────────────────── act 07 · cta(终幕 · 归档落款)─────────────────────────── */ + +/* 一屏严格容纳:slug / 巨字(弹性行)/ 下载闸 / 索引 / 版权 */ +.cta { + --cta-pad: clamp(20px, 5vw, 96px); /* 与 9.15vw 的巨字宽度互锁,使标题右端齐闸门右端 */ + position: relative; height: 100vh; overflow: clip; + display: grid; grid-template-rows: auto 1fr auto auto auto; + row-gap: clamp(14px, 2.5vh, 30px); + padding: clamp(60px, 9vh, 96px) 0 clamp(16px, 2.8vh, 30px); +} +.cta > *:not(.gate) { padding-inline: var(--cta-pad); } + +/* 刊头行:左标记 —— 点线引导 —— 右封存戳 */ +.cta__slug { display: flex; align-items: center; gap: clamp(14px, 2vw, 28px); } +.cta__slug-t { + display: flex; align-items: center; gap: 12px; flex: none; white-space: nowrap; + font-size: 10.5px; letter-spacing: 0.26em; color: var(--ink-dim); +} +.cta__slug-t:last-child { color: var(--ink-faint); } +.cta__slug-t .tick { width: 30px; } +.cta__leader { + flex: 1; height: 1px; min-width: 20px; + background-image: repeating-linear-gradient(90deg, var(--line-strong) 0 2px, transparent 2px 9px); +} + +/* 落款巨字:单行齐左横贯全幅 */ +.cta__title { + align-self: end; margin-bottom: clamp(10px, 5.5vh, 60px); + font-size: min(9.15vw, 19vh); font-weight: 900; + line-height: 1.02; letter-spacing: -0.02em; white-space: nowrap; +} +/* 换行时「自己手里」整体成句,不被拆开 */ +.cta__title em { color: var(--neon); text-shadow: 0 0 58px rgba(61, 242, 141, 0.45); white-space: nowrap; } + +/* 下载闸:横贯的巨型条,hover 时霓虹自左灌满并反白 */ +.gate { + position: relative; display: grid; margin-inline: var(--cta-pad); + min-height: clamp(92px, 14.5vh, 152px); overflow: hidden; + border-top: 1px solid var(--line-strong); border-bottom: 1px solid var(--line-strong); + background: linear-gradient(180deg, rgba(14, 26, 19, 0.6), rgba(7, 11, 9, 0.72)); + transition: border-color 0.5s var(--ease-out), box-shadow 0.5s var(--ease-out); +} +.gate:hover { border-color: var(--neon); box-shadow: 0 0 90px rgba(61, 242, 141, 0.2); } +.gate__fill { + position: absolute; inset: 0; z-index: 0; background: var(--neon); + transform: scaleX(0); transform-origin: left; + transition: transform 0.66s var(--ease-out); +} +.gate:hover .gate__fill { transform: scaleX(1); } +.gate__scan { + position: absolute; top: 0; bottom: 0; left: 0; z-index: 1; width: 34%; + background: linear-gradient(90deg, transparent, rgba(61, 242, 141, 0.14), transparent); + animation: gateScan 5.4s linear infinite; transition: opacity 0.4s; +} +.gate:hover .gate__scan { opacity: 0; } +@keyframes gateScan { from { transform: translateX(-110%); } to { transform: translateX(400%); } } +.gate__glow { + position: absolute; inset: 0; z-index: 2; opacity: 0; transition: opacity 0.45s; + background: radial-gradient(260px circle at var(--mx, 50%) var(--my, 50%), rgba(255, 255, 255, 0.42), transparent 66%); +} +.gate:hover .gate__glow { opacity: 1; } +.gate__row { + grid-area: 1 / 1; position: relative; z-index: 3; + display: flex; align-items: center; gap: clamp(14px, 2.4vw, 38px); + padding: clamp(12px, 2.2vh, 24px) 0; /* 条身已内缩到版心,文字与巨字齐左 */ + will-change: transform; +} +.gate__row--ink { z-index: 4; color: #04140b; clip-path: inset(0 100% 0 0); transition: clip-path 0.66s var(--ease-out); } +.gate:hover .gate__row--ink { clip-path: inset(0 0 0 0); } +.gate__label { + font-size: clamp(1.55rem, 3.5vw, 3.1rem); font-weight: 900; + letter-spacing: 0.02em; line-height: 1.1; white-space: nowrap; + transition: letter-spacing 0.6s var(--ease-out); +} +.gate:hover .gate__label { letter-spacing: 0.06em; } +/* 从标签牵到右侧的输送轨,一枚光点持续推向下载箭头 */ +.gate__track { + position: relative; flex: 1; min-width: 24px; height: 1px; + background: linear-gradient(90deg, rgba(150, 180, 160, 0.05), var(--line-strong)); +} +.gate__pip { + position: absolute; top: 50%; left: 0; width: 5px; height: 5px; margin-top: -2.5px; + border-radius: 50%; background: var(--neon); box-shadow: 0 0 12px var(--glow); + animation: gatePip 3.6s cubic-bezier(0.5, 0, 0.25, 1) infinite; +} +@keyframes gatePip { + 0% { left: 0; opacity: 0; } + 14% { opacity: 1; } + 86% { opacity: 1; } + 100% { left: 100%; opacity: 0; } +} +.gate__row--ink .gate__track { background: linear-gradient(90deg, rgba(4, 20, 11, 0.12), rgba(4, 20, 11, 0.4)); } +.gate__row--ink .gate__pip { background: #04140b; box-shadow: none; } +.gate__meta { + flex: none; font-size: 10.5px; letter-spacing: 0.24em; + text-align: right; line-height: 1.9; +} +.gate__arrow { + flex: none; font-style: normal; line-height: 1; + font-size: clamp(1.5rem, 3vw, 2.5rem); font-weight: 300; + animation: gateBob 2.6s ease-in-out infinite; +} +@keyframes gateBob { 0%, 100% { transform: translateY(-5px); } 50% { transform: translateY(5px); } } +.gate__row--base .gate__label { color: var(--ink); } +.gate__row--base .gate__meta { color: var(--ink-dim); } +.gate__row--base .gate__arrow { color: var(--neon); text-shadow: 0 0 22px var(--glow); } +.gate__row--ink .gate__meta { color: rgba(4, 20, 11, 0.7); } + +/* 索引:编号 + 细线 + 阶梯错位的四则条目 */ +.cta__index { + display: grid; grid-template-columns: repeat(4, 1fr); + gap: clamp(14px, 2.6vw, 46px); align-items: start; +} +.ix { display: flex; flex-direction: column; gap: clamp(6px, 1vh, 11px); } +.ix:nth-child(2) { margin-top: 9px; } +.ix:nth-child(3) { margin-top: 18px; } +.ix:nth-child(4) { margin-top: 27px; } +.ix__top { display: flex; align-items: center; gap: 11px; } +.ix__n { font-size: 10px; letter-spacing: 0.24em; color: var(--neon); flex: none; } +.ix__rule { flex: 1; height: 1px; background: var(--line-strong); transform-origin: left; transition: background 0.4s, box-shadow 0.4s; } +.ix__arw { flex: none; font-style: normal; font-size: 12px; color: var(--ink-faint); transition: transform 0.4s var(--ease-out), color 0.4s; } +.ix__name { + font-size: clamp(14px, 1.45vw, 20px); font-weight: 900; letter-spacing: -0.01em; line-height: 1.2; + transition: color 0.35s, transform 0.45s var(--ease-out); +} +.ix__desc { font-size: 11px; letter-spacing: 0.06em; color: var(--ink-faint); line-height: 1.5; } +.ix:hover .ix__rule { background: var(--neon); box-shadow: 0 0 10px var(--glow); } +.ix:hover .ix__arw { color: var(--neon); transform: translate(3px, -3px); } +.ix:hover .ix__name { color: var(--neon); transform: translateX(4px); } +.ix:hover .ix__desc { color: var(--ink-dim); } + +/* 版权:细线 + 三段散排 */ +.cta__colophon { display: flex; flex-direction: column; gap: clamp(9px, 1.6vh, 15px); } +.cta__colophon-rule { height: 1px; background: var(--line); transform-origin: left; } +.cta__colophon-row { + display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px clamp(18px, 3vw, 40px); + font-size: 10px; letter-spacing: 0.18em; color: var(--ink-faint); +} +.cta__colophon-row .cc-a { display: flex; align-items: center; gap: 9px; color: var(--ink-dim); } +.cta__colophon-row .cc-a i { width: 5px; height: 5px; background: var(--neon); box-shadow: 0 0 8px var(--glow); flex: none; } + +/* ─────────────────────────── responsive ─────────────────────────── */ + +@media (max-width: 960px) { + .rail { display: none; } + .nav__word { display: none; } + .hero__vert, .hero__coord { display: none; } + .hero__title { top: 34%; } + .ht-line--1 { font-size: clamp(2rem, 8.4vw, 3.4rem); } + .ht-line--2 { text-align: left; font-size: clamp(3rem, 15vw, 6rem); } + .hero__sub { bottom: 26vh; } + .hero__cta { left: 20px; right: auto; bottom: 9vh; flex-direction: row; align-items: center; gap: 26px; } + .deck { grid-template-columns: 1fr; gap: clamp(18px, 3vh, 30px); align-content: center; } + .deck__num { font-size: clamp(3rem, 12vw, 4.4rem); margin: 12px 0; } + .deck__desc { display: none; } + .deck__meta { margin-top: 14px; padding-top: 12px; } + .decrypt__chat { width: min(360px, 88vw); } + .decrypt__hud { top: 84px; } + .dtitle { top: 17vh; } + .ddbs { gap: 7px 16px; font-size: 10px; } + .wrapped__stats { gap: 18px 30px; bottom: 9vh; } + .wstat span { font-size: 10.5px; } + .fw { flex-direction: column; gap: 18px; } + .fwchips { width: min(92vw, 560px); display: grid; grid-template-columns: 1fr 1fr; gap: 14px 26px; } + .fwchips--l { align-items: start; text-align: left; } + .fw__stage { --fwr: min(24vw, 132px); width: min(54vw, 300px); } + .viewer { --film: min(94vw, calc(46vh * 2.216)); } + .cta__title { white-space: normal; font-size: min(13.5vw, 11vh); line-height: 1.08; margin-bottom: clamp(10px, 3vh, 30px); } + .cta__index { grid-template-columns: 1fr 1fr; gap: clamp(12px, 3vh, 22px) 26px; } + .ix:nth-child(2), .ix:nth-child(3), .ix:nth-child(4) { margin-top: 0; } + .gate { min-height: clamp(78px, 12vh, 108px); } + .gate__meta { display: none; } + .cta__colophon-row { justify-content: flex-start; } +} +@media (max-width: 560px) { + .cta__slug-t:last-child { display: none; } + .cta__leader { display: none; } + .wrapped__stats { gap: 14px 22px; } + .dhud__left { flex-direction: column; gap: 4px; } + .river-gates i { display: none; } + .river-exits span { padding: 6px 10px; font-size: 10px; } +} + +/* ─────────────────────────── reduced motion ─────────────────────────── */ + +@media (prefers-reduced-motion: reduce) { + .grain, .gate__scan, .gate__arrow { animation: none; } + #gl { opacity: 0.35; } + .live-dot { animation: none; } + .term__caret { animation: none; } +} diff --git a/website/assets/img/AnnualSummary1.png b/website/assets/img/AnnualSummary1.png new file mode 100644 index 00000000..1d05ca48 Binary files /dev/null and b/website/assets/img/AnnualSummary1.png differ diff --git a/website/assets/img/AnnualSummary2.png b/website/assets/img/AnnualSummary2.png new file mode 100644 index 00000000..e01d13b2 Binary files /dev/null and b/website/assets/img/AnnualSummary2.png differ diff --git a/website/assets/img/AnnualSummary3.png b/website/assets/img/AnnualSummary3.png new file mode 100644 index 00000000..cc57330e Binary files /dev/null and b/website/assets/img/AnnualSummary3.png differ diff --git a/website/assets/img/AnnualSummary6.png b/website/assets/img/AnnualSummary6.png new file mode 100644 index 00000000..3a159327 Binary files /dev/null and b/website/assets/img/AnnualSummary6.png differ diff --git a/website/assets/img/AnnualSummary7.png b/website/assets/img/AnnualSummary7.png new file mode 100644 index 00000000..37177c06 Binary files /dev/null and b/website/assets/img/AnnualSummary7.png differ diff --git a/website/assets/img/AnnualSummary8.png b/website/assets/img/AnnualSummary8.png new file mode 100644 index 00000000..1c370ddd Binary files /dev/null and b/website/assets/img/AnnualSummary8.png differ diff --git a/website/assets/img/RealTimeMessages.gif b/website/assets/img/RealTimeMessages.gif new file mode 100644 index 00000000..51862983 Binary files /dev/null and b/website/assets/img/RealTimeMessages.gif differ diff --git a/website/assets/img/edit.gif b/website/assets/img/edit.gif new file mode 100644 index 00000000..48547a64 Binary files /dev/null and b/website/assets/img/edit.gif differ diff --git a/website/assets/img/export.png b/website/assets/img/export.png new file mode 100644 index 00000000..b9de9a96 Binary files /dev/null and b/website/assets/img/export.png differ diff --git a/website/assets/img/logo.png b/website/assets/img/logo.png new file mode 100644 index 00000000..1efeb870 Binary files /dev/null and b/website/assets/img/logo.png differ diff --git a/website/assets/img/message.png b/website/assets/img/message.png new file mode 100644 index 00000000..304ae11e Binary files /dev/null and b/website/assets/img/message.png differ diff --git a/website/assets/img/search.png b/website/assets/img/search.png new file mode 100644 index 00000000..663b5831 Binary files /dev/null and b/website/assets/img/search.png differ diff --git a/website/assets/img/sns.png b/website/assets/img/sns.png new file mode 100644 index 00000000..bdd32165 Binary files /dev/null and b/website/assets/img/sns.png differ diff --git a/website/assets/js/main.js b/website/assets/js/main.js new file mode 100644 index 00000000..3c27e8a1 --- /dev/null +++ b/website/assets/js/main.js @@ -0,0 +1,1428 @@ +/* ════════════════════════════════════════════════════════════ + main.js — 滚动叙事总编排 + loader → hero → manifesto → decrypt → features → wrapped + → privacy → stack → cta,一条时间轴讲完整个故事。 + ════════════════════════════════════════════════════════════ */ +import { createStage } from "./particles.js"; + +const { gsap } = window; +gsap.registerPlugin(ScrollTrigger, ScrollToPlugin, SplitText, ScrambleTextPlugin, CustomEase, DrawSVGPlugin); + +CustomEase.create("silk", "0.45,0.05,0.55,0.95"); +CustomEase.create("flow", "0.33,0,0.2,1"); +CustomEase.create("cine", "0.25,0.1,0.25,1"); + +const REDUCED = matchMedia("(prefers-reduced-motion: reduce)").matches; +const TOUCH = matchMedia("(hover: none)").matches; +const SCRAMBLE_CN = "解密留痕数据档案01ABCDEF#<>/"; +const $ = (s, c = document) => c.querySelector(s); +const $$ = (s, c = document) => [...c.querySelectorAll(s)]; + +/* ─────────────────────────── 十六进制字符墙 ─────────────────────────── */ + +class HexWall { + constructor(canvas, { dim = 0.34 } = {}) { + this.cv = canvas; + this.ctx = canvas.getContext("2d"); + this.dim = dim; + this.hexset = "0123456789ABCDEF"; + this.cnset = "周五晚上老地方见带上照片我都存着呢哈哈哈红包已领取晚安好梦明天见谢谢你一直都在刚落地就这么定了"; + this.reveal = 0; // 0-1 扫描进度 + this.alpha = 1; // 整体透明度 + this.last = 0; + this.resize(); + addEventListener("resize", () => this.resize()); + } + resize() { + const dpr = Math.min(devicePixelRatio || 1, 1.6); + const { clientWidth: w, clientHeight: h } = this.cv; + this.cv.width = w * dpr; + this.cv.height = h * dpr; + this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + this.cw = 19; this.ch = 24; + this.cols = Math.ceil(w / this.cw); + this.rows = Math.ceil(h / this.ch) + 1; + this.cells = []; + for (let i = 0; i < this.cols * this.rows; i++) { + this.cells.push({ + ch: this.hexset[(Math.random() * 16) | 0], + cn: this.cnset[(Math.random() * this.cnset.length) | 0], + fl: Math.random(), // 闪烁相位 + th: Math.random() * 0.18 - 0.09, // 解锁阈值抖动 + }); + } + this.w = w; this.h = h; + } + draw(t) { + if (this.alpha <= 0.01) { if (!this._cleared) { this.ctx.clearRect(0, 0, this.w, this.h); this._cleared = true; } return; } + this._cleared = false; + if (t - this.last < 1 / 22) return; // ~22fps 足矣 + this.last = t; + const { ctx } = this; + ctx.clearRect(0, 0, this.w, this.h); + ctx.font = "13px 'JetBrains Mono', monospace"; + ctx.textBaseline = "top"; + const scanY = this.reveal * (this.rows + 2) - 1; + for (let r = 0; r < this.rows; r++) { + for (let c = 0; c < this.cols; c++) { + const cell = this.cells[r * this.cols + c]; + const flick = 0.55 + 0.45 * Math.sin(t * (1.5 + cell.fl * 2.5) + cell.fl * 40); + const unlocked = r + cell.th * this.rows < scanY; + if ((t * (0.4 + cell.fl)) % 3 < 0.05) cell.ch = this.hexset[(Math.random() * 16) | 0]; + if (unlocked) { + const heat = Math.max(0, 1 - (scanY - r) * 0.12); + const rr = Math.round(61 + heat * 194), gg = Math.round(242 + heat * 13), bb = Math.round(141 + heat * 114); + ctx.fillStyle = `rgba(${rr},${gg},${bb},${(0.16 + heat * 0.7) * this.alpha})`; + ctx.fillText(cell.cn, c * this.cw, r * this.ch); + } else { + ctx.fillStyle = `rgba(90,140,110,${(0.05 + 0.13 * flick * cell.fl) * this.dim * 2.6 * this.alpha})`; + ctx.fillText(cell.ch, c * this.cw, r * this.ch); + } + } + } + } +} + +/* ─────────────────────────── 密文之墙(act 02) ─────────────────────────── */ + +// 加密锋线自上而下推进:锋线以上结成密文,指针经过能擦开巴掌大一块看见原文 +class CipherWall { + constructor(canvas) { + this.cv = canvas; + this.ctx = canvas.getContext("2d"); + this.hexset = "0123456789ABCDEF"; + this.cnset = "多年的对话照片红包与告别都锁在一个你打不开的加密数据库里到了跟我说一声刚落地还是老地方见保重晚安明天回家生日快乐路上小心哈哈哈我都存着呢谢谢你一直都在"; + this.fill = 0; // 锋线位置 0-1 + this.alpha = 0; + this.crack = 0; // 裂缝张开 0-1 + this.unlock = 0; // 解密波自中线向两侧推开 0-1(第三幕用) + this.visible = true; // 幕不在视口里就别画 + this.mx = -1e4; this.my = -1e4; + this.last = 0; + addEventListener("pointermove", (e) => { + const r = this.cv.getBoundingClientRect(); + this.mx = e.clientX - r.left; this.my = e.clientY - r.top; + }, { passive: true }); + addEventListener("resize", () => this.resize()); + this.resize(); + } + resize() { + const dpr = Math.min(devicePixelRatio || 1, 1.6); + const w = this.cv.clientWidth, h = this.cv.clientHeight; + if (!w || !h) return; + this.cv.width = w * dpr; this.cv.height = h * dpr; + this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + this.w = w; this.h = h; + this.cw = 19; this.ch = 24; + this.cols = Math.ceil(w / this.cw); + this.rows = Math.ceil(h / this.ch) + 1; + this.cells = []; + for (let i = 0; i < this.cols * this.rows; i++) { + this.cells.push({ + hex: this.hexset[(Math.random() * 16) | 0], + cn: this.cnset[(Math.random() * this.cnset.length) | 0], + fl: Math.random(), + th: Math.random() * 0.13 - 0.065, // 锋线锯齿,避免一条直线推过来 + }); + } + } + draw(t) { + if (!this.cells) return; + const { ctx } = this; + if (this.alpha <= 0.005 || !this.visible) { + if (!this._cleared) { ctx.clearRect(0, 0, this.w, this.h); this._cleared = true; } + return; + } + if (t - this.last < 1 / 24) return; + this.last = t; this._cleared = false; + ctx.clearRect(0, 0, this.w, this.h); + ctx.font = "13px 'JetBrains Mono', monospace"; + ctx.textBaseline = "top"; + const front = this.fill * (this.h + 80) - 40; + const cx = this.w * 0.5; + const half = this.crack * this.w * 0.23; + for (let r = 0; r < this.rows; r++) { + const y = r * this.ch; + for (let c = 0; c < this.cols; c++) { + const cell = this.cells[r * this.cols + c]; + const edge = front + cell.th * this.h; + if (y > edge) continue; // 锋线还没到,仍是原文的地盘 + const x = c * this.cw; + const dx = Math.abs(x + this.cw * 0.5 - cx); + const uHalf = this.unlock * this.w * 0.62; + const unlocked = uHalf > 0 && dx < uHalf + cell.th * 90; // 波前带锯齿 + if (half > 0 && dx < half && !unlocked) continue; // 裂缝(解密波填进来之前) + const near = Math.max(0, 1 - (edge - y) / 95); // 锋线附近最亮 + const d = Math.hypot(x - this.mx, y - this.my); + const erase = d < 125 ? 1 - d / 125 : 0; + if ((t * (0.4 + cell.fl)) % 3 < 0.05) cell.hex = this.hexset[(Math.random() * 16) | 0]; + if (unlocked) { // 已解开:变回原文,波前一线最亮 + const wave = Math.max(0, 1 - (uHalf - dx) / 120); + ctx.fillStyle = `rgba(${(176 + 79 * wave) | 0},255,${(202 + 53 * wave) | 0},${((0.32 + wave * 0.62) * this.alpha).toFixed(3)})`; + ctx.fillText(cell.cn, x, y); + } else if (erase > 0.34) { + ctx.fillStyle = `rgba(${(200 + 55 * erase) | 0},255,${(220 + 35 * erase) | 0},${((0.3 + erase * 0.6) * this.alpha).toFixed(3)})`; + ctx.fillText(cell.cn, x, y); + } else { + const flick = 0.55 + 0.45 * Math.sin(t * (1.5 + cell.fl * 2.5) + cell.fl * 40); + const a = (0.1 + 0.2 * flick * cell.fl + near * 0.7) * this.alpha; + ctx.fillStyle = `rgba(${(61 + near * 160) | 0},242,${(141 + near * 95) | 0},${a.toFixed(3)})`; + ctx.fillText(cell.hex, x, y); + } + } + } + if (half > 2 && this.unlock * this.w * 0.62 < half) { // 裂缝的两道亮边(解密波盖过就撤) + const g = ctx.createLinearGradient(0, 0, 0, this.h); + g.addColorStop(0, "rgba(61,242,141,0)"); + g.addColorStop(0.5, `rgba(61,242,141,${(0.9 * this.alpha).toFixed(3)})`); + g.addColorStop(1, "rgba(61,242,141,0)"); + ctx.fillStyle = g; + ctx.fillRect(cx - half - 1, 0, 2, this.h); + ctx.fillRect(cx + half - 1, 0, 2, this.h); + } + } +} + +/* ─────────────────────────── 启动 ─────────────────────────── */ + +let stage, lenis, hexwall, loaderWall, river, mwall, isoTick = null; +let scrollProgress = 0; +let __vt = 0; + +function boot() { + stage = createStage($("#gl")); + + lenis = new Lenis({ lerp: 0.09, wheelMultiplier: 1.0, smoothWheel: !REDUCED }); + lenis.stop(); + lenis.on("scroll", ScrollTrigger.update); + lenis.on("scroll", (e) => { + scrollProgress = e.limit ? e.animatedScroll / e.limit : 0; + stage.setScroll(scrollProgress); + }); + gsap.ticker.add((time) => { + __vt = Math.max(__vt, time); + lenis.raf(time * 1000); + stage.update(time); + if (loaderWall) loaderWall.draw(time); + if (hexwall) hexwall.draw(time); + if (mwall) mwall.draw(time); + if (river) river.draw(time); + if (isoTick) isoTick(); + }); + gsap.ticker.lagSmoothing(0); + + // 自动化/调试:手动推进帧(应对宿主环境 rAF 节流) + window.__go = (y, frames = 150) => { + lenis.resize(); + lenis.scrollTo(y, { immediate: true, force: true }); + return window.__step(frames); + }; + window.__step = (frames = 60) => { + __vt = Math.max(__vt, gsap.ticker.time); + for (let i = 0; i < frames; i++) { + __vt += 1 / 60; + gsap.updateRoot(__vt); + lenis.raf(__vt * 1000); + stage.update(__vt); + if (hexwall) hexwall.draw(__vt); + if (mwall) mwall.draw(__vt); + if (river) river.draw(__vt); + if (isoTick) isoTick(); + } + return Math.round(scrollY); + }; + + setupCursor(); + setupMagnetic(); + fetchStars(); + + const minWait = new Promise((res) => setTimeout(res, REDUCED ? 300 : 2400)); + runLoader(); + Promise.all([document.fonts.ready, minWait]).then(() => { + buildHero(); + buildManifesto(); + buildDecrypt(); + buildFeatures(); + buildWrapped(); + buildMachine(); + buildCTA(); + buildRail(); + exitLoader(); + addEventListener("load", () => ScrollTrigger.refresh()); + }); +} + +/* ─────────────────────────── 预加载解密序列 ─────────────────────────── */ + +function runLoader() { + loaderWall = new HexWall($("#loader-hex"), { dim: 0.16 }); + const keyEl = $("#loader-key"); + const HEXC = "0123456789ABCDEF"; + const target = Array.from({ length: 64 }, () => HEXC[(Math.random() * 16) | 0]).join(""); + if (REDUCED) { keyEl.innerHTML = "" + target + ""; return; } + + const num = $("#loader-num"); + const fill = $("#loader-fill"); + const status = $("#loader-status"); + const steps = [ + "locating db_storage …", + "scanning memory pages …", + "aligning key pattern …", + "verifying against message_0.db …", + "key locked · decrypting …", + ]; + const st = { v: 0 }; + gsap.to(st, { + v: 100, duration: 2.7, ease: "expo.inOut", + onUpdate() { + if (!loaderWall) return; + const resolved = Math.floor((st.v / 100) * 64); + let tail = ""; + for (let i = resolved; i < 64; i++) tail += HEXC[(Math.random() * 16) | 0]; + keyEl.innerHTML = "" + target.slice(0, resolved) + "" + tail; + num.textContent = String(Math.round(st.v)).padStart(3, "0"); + fill.style.width = st.v + "%"; + loaderWall.reveal = st.v / 100; + const i = Math.min(steps.length - 1, Math.floor((st.v / 100) * steps.length)); + if (status.textContent !== steps[i]) status.textContent = steps[i]; + }, + }); +} + +function exitLoader() { + const loader = $("#loader"); + document.body.classList.remove("is-loading"); + if (REDUCED) { + loader.remove(); loaderWall = null; lenis.start(); + stage.setOpacity(0.5, 0.5); stage.setAmp(0.25, 0.5); + return; + } + const tl = gsap.timeline(); + tl.call(() => { $("#loader-status").textContent = "✓ key accepted — welcome home"; }) + .fromTo("#loader-key", { filter: "brightness(2.6)" }, { filter: "brightness(1)", duration: 0.5, ease: "power2.out" }, 0) + .to(".loader__center, .loader__corner, #loader-hex", { opacity: 0, y: -26, duration: 0.55, ease: "flow", stagger: 0.03 }, "+=0.32") + .add("wipe") + .to(".loader__panel.top", { scaleY: 0, transformOrigin: "top", duration: 1.05, ease: "cine" }, "wipe") + .to(".loader__panel.bottom", { scaleY: 0, transformOrigin: "bottom", duration: 1.05, ease: "cine" }, "wipe") + .add(() => { loader.remove(); loaderWall = null; lenis.start(); }) + .add(heroIntro(), "wipe-=0.15"); +} + +/* ─────────────────────────── act 01 · hero ─────────────────────────── */ + +/* ---------- 开屏解密装置:巨字以密文入场,光刃扫过逐字解开 ---------- */ + +const HEXC = "0123456789ABCDEF"; +let heroChars = [], heroScan = null, heroTitle = null; + +function heroSplit() { + heroTitle = $(".hero__title"); + heroScan = $(".hero__decrypt"); + heroChars = $$(".hero__title .ht-mask").flatMap((m) => new SplitText(m, { type: "chars" }).chars); + const box = heroTitle.getBoundingClientRect(); + heroChars.forEach((c) => { + c.dataset.plain = c.textContent; + // 阈值取字符在标题块内的横向位置 —— 光刃是空间上的扫过,两行会自然交错解开 + const r = c.getBoundingClientRect(); + c._t = box.width ? (r.left + r.width / 2 - box.left) / box.width : Math.random(); + c._state = -1; + }); + heroReveal(-1); +} + +// q = 光刃前缘的归一化位置(<0 全密文,>1 全明文) +function heroReveal(q) { + for (const c of heroChars) { + if (q >= c._t) { + if (c._state !== 1) { + c._state = 1; + c.textContent = c.dataset.plain; + c.classList.remove("is-cipher", "is-solved"); + void c.offsetWidth; // 强制回流以重启定格动画 + c.classList.add("hc", "is-solved"); + } + } else { + if (c._state !== 0) { + c._state = 0; + c.classList.remove("is-solved"); + c.classList.add("hc", "is-cipher"); + c.textContent = HEXC[(Math.random() * 16) | 0]; // 立刻碎掉,别等下一次随机抖动 + } + if (Math.random() < 0.28) c.textContent = HEXC[(Math.random() * 16) | 0]; + } + } +} + +function buildHero() { + gsap.set(".nav", { yPercent: -140, opacity: 0 }); + gsap.set(".rail", { opacity: 0 }); + gsap.set(".hero__sub", { opacity: 0 }); + gsap.set(".hero .ht-mask", { yPercent: 118 }); + gsap.set(".hero__vert", { clipPath: "inset(0 0 100% 0)" }); + gsap.set(".hero__coord", { opacity: 0, y: -14 }); + gsap.set(".hero__orb", { scale: 0.4, opacity: 0 }); + gsap.set(".hero__gh", { opacity: 0, y: 14 }); + gsap.set(".hero__ticker", { yPercent: 110 }); + + if (REDUCED) { + gsap.set([".nav", ".rail", ".hero__sub", ".hero .ht-mask", ".hero__vert", ".hero__coord", ".hero__orb", ".hero__gh", ".hero__ticker"], { clearProps: "all" }); + return; + } + + heroSplit(); + + // 底部数据流 + gsap.to("#hero-ticker", { xPercent: -50, repeat: -1, duration: 30, ease: "none" }); + + // 滚动离场:两行标题反向撕开 + gsap.to(".hero__title", { + yPercent: -26, opacity: 0, ease: "none", + scrollTrigger: { trigger: "#hero", start: "top top", end: "78% top", scrub: 0.6 }, + }); + gsap.to(".ht-line--1 .ht-mask", { + xPercent: -9, ease: "none", + scrollTrigger: { trigger: "#hero", start: "top top", end: "bottom top", scrub: 0.6 }, + }); + gsap.to(".ht-line--2 .ht-mask", { + xPercent: 9, ease: "none", + scrollTrigger: { trigger: "#hero", start: "top top", end: "bottom top", scrub: 0.6 }, + }); + gsap.to([".hero__sub", ".hero__cta", ".hero__coord", ".hero__vert", ".hero__ticker"], { + opacity: 0, ease: "none", + scrollTrigger: { trigger: "#hero", start: "6% top", end: "52% top", scrub: 0.6 }, + }); +} + +function heroIntro() { + if (REDUCED) return gsap.timeline(); + stage.setOpacity(0.9, 2.2); + + const SCAN_AT = 1.5, SCAN_DUR = 1.45; + const hc1 = $("#hc-1"); + const dec = { p: 0 }; + const decUpdate = () => { + const W = heroTitle.offsetWidth || 1; + const q = -0.12 + dec.p * 1.24; + heroScan.style.transform = `translateX(${(q * W).toFixed(1)}px)`; + heroReveal(q); + hc1.textContent = "DECRYPT — " + (gsap.utils.clamp(0, 1, dec.p) * 100).toFixed(1) + "% · SCANNING"; + }; + + const tl = gsap.timeline({ defaults: { ease: "flow" } }); + tl.to(".nav", { yPercent: 0, opacity: 1, duration: 0.9 }, 0.15) + .to(".rail", { opacity: 1, duration: 0.8 }, 0.4) + .to(".hero__vert", { clipPath: "inset(0 0 0% 0)", duration: 1.1, ease: "silk" }, 0.3) + .to(".ht-line--1 .ht-mask", { yPercent: 0, duration: 1.15, ease: "cine" }, 0.36) + .to(".ht-line--2 .ht-mask", { yPercent: 0, duration: 1.3, ease: "cine" }, 0.52) + .to(".hero__coord", { opacity: 1, y: 0, duration: 0.8 }, 0.9) + .to(".hero__sub", { + opacity: 1, duration: 0.01, + onComplete: () => gsap.to("#hero-scramble", { + scrambleText: { text: "解密 · 浏览 · 搜索 · 导出 · 年度总结 —— 全部离线完成", chars: SCRAMBLE_CN, speed: 0.6 }, + duration: 1.6, + }), + }, 1.0) + .to(".hero__orb", { scale: 1, opacity: 1, duration: 1.1, ease: "back.out(1.7)" }, 1.05) + .to(".hero__gh", { opacity: 1, y: 0, duration: 0.7 }, 1.25) + .to(".hero__ticker", { yPercent: 0, duration: 0.9, ease: "cine" }, 1.15) + + // ── 解密序列:密文抖动 → 光刃横扫逐字定格 → 粒子自散乱聚拢 + .call(() => hc1.classList.add("is-live"), [], 0.9) + .to({}, { duration: SCAN_AT - 0.9, onUpdate: decUpdate }, 0.9) + .set(heroScan, { opacity: 1 }, SCAN_AT) + .call(() => { + stage.setAmp(0.55, 0.01); + gsap.fromTo(stage.uniforms.uAmp, { value: 3.4 }, { value: 0.55, duration: SCAN_DUR + 1.1, ease: "expo.out", overwrite: true }); + }, [], SCAN_AT) + .to(dec, { p: 1, duration: SCAN_DUR, ease: "power1.inOut", onUpdate: decUpdate }, SCAN_AT) + .to(heroScan, { opacity: 0, duration: 0.45 }, SCAN_AT + SCAN_DUR - 0.15) + .call(() => { + stage.pulse(1.7); + hc1.classList.remove("is-live"); + gsap.to(hc1, { + duration: 0.9, + scrambleText: { text: "DB — AES-256 · SQLCIPHER COMPAT", chars: HEXC + " ·—", speed: 0.8 }, + }); + $$(".hero .ht-line").forEach((l) => (l.style.overflow = "visible")); // 交还霓虹辉光的外溢空间 + // 向下滚动时反向再加密:先解开的最后碎回去 + ScrollTrigger.create({ + trigger: "#hero", start: "top top", end: "42% top", scrub: 0.5, + onUpdate(self) { heroReveal(1.12 - self.progress * 1.3); }, + }); + }, [], SCAN_AT + SCAN_DUR); + return tl; +} + +/* ─────────────────────────── act 02 · manifesto ─────────────────────────── */ + +/* 碎片一律用产品同款微信组件(.wc-row / .wc-avatar / .wc-bubble / .wc-voice / + .wc-img / .wc-redpacket),与解密幕聊天窗同源,不另造简化气泡 */ +const WC_VOICE_SVG = ''; +const WC_IMG_SVG = '
'; +const WC_RP = '
🧧
恭喜发财,大吉大利领取红包
微信红包
'; + +const MF_L = ["到了跟我说一声", "路上小心", "生日快乐!", "保重。", "我都存着呢", "记得吃饭", "那天的照片我还留着", "有空常联系", "早点睡", "在吗", "谢谢你一直都在", "嗯", "好的", "收到", "哈哈哈哈哈", "明天见", "注意身体", "这几年谢谢你,真的", "晚安", "想你了", "别太累了", "我先睡了"]; +const MF_R = ["刚落地,还是老地方见", "妈,我明天回家", "好", "晚安", "马上到", "我到家了", "哈哈哈", "知道了", "这就出发", "别等我了先睡", "下周回来看你", "谢谢", "嗯嗯", "好想你们", "放心吧", "在忙,回头说", "收到啦", "都挺好的"]; +const MF_SHORT = ["嗯", "好", "在", "好的", "收到", "晚安", "哈哈", "谢谢", "嗯嗯", "好呀", "在的", "明天见", "早", "行", "好累", "😂", "👍", "❤️"]; +const HEXP = "0123456789ABCDEF"; +const rnd = (n) => (Math.random() * n) | 0; +const hexStr = (n) => Array.from({ length: n }, () => HEXP[rnd(16)] + HEXP[rnd(16)]).join(" "); + +// 一条真实微信消息(组件与解密幕聊天窗同源)。风暴里不带头像,与年度总结那版一致 +function mfItem(short) { + const r = short ? 1 : Math.random(); + const side = Math.random() < 0.5 ? "l" : "r"; + if (r < 0.03) return { html: WC_RP, w: 186, h: 62, hex: hexStr(3) }; + if (r < 0.085) return { html: WC_IMG_SVG, w: 96, h: 68, hex: hexStr(2) }; + if (r < 0.16) return { html: '
' + WC_VOICE_SVG + "" + (2 + rnd(48)) + '″
', w: 88, h: 34, hex: hexStr(1) }; + const pool = short ? MF_SHORT : (side === "l" ? MF_L : MF_R); + const t = pool[rnd(pool.length)]; + // 与年度总结 bubbleSizeForText 同款估算:中文 1 单位、西文 0.56 + const chars = Array.from(t); + const raw = chars.reduce((a, ch) => a + (/[^\x00-\xff]/.test(ch) ? 13 : 8.5), 0); + const units = chars.reduce((a, ch) => a + (/[^\x00-\xff]/.test(ch) ? 1 : 0.56), 0); + const minW = units >= 26 ? 182 : units >= 14 ? 122 : 74; + const w = Math.round(Math.min(300, Math.max(minW, raw + 22))); + const lines = Math.max(1, Math.ceil(raw / Math.max(1, w - 22))); + return { + html: '
' + t + "
", + w, h: Math.max(32, lines * 19 + 16), hex: hexStr(Math.min(4, Math.max(1, (units / 4) | 0))), + }; +} + +/* 消息风暴(取自年度总结的 storm):网格装箱把真实消息一条条铺满整屏, + 优先填空网格、填不下才允许最多三层叠放。这里改成滚动驱动 —— 手指往下滚,消息越涌越快。 */ +function buildMFrags(host) { + host.innerHTML = ""; + const vw = innerWidth || 1440, vh = innerHeight || 900; + const CELL = 30, MAXL = 5; + const cols = Math.ceil(vw / CELL), rows = Math.ceil(vh / CELL); + const grid = new Map(); + const boxes = []; + const key = (cx, cy) => cx * 4096 + cy; + + const hit = (a, b, m) => !(a.x - m > b.x + b.w || a.x + a.w + m < b.x || a.y - m > b.y + b.h || a.y + a.h + m < b.y); + function canPlace(box, margin, overlap) { + if (box.x < 0 || box.y < 0 || box.x + box.w > vw || box.y + box.h > vh) return false; + const c0 = Math.floor(box.x / CELL) - 1, c1 = Math.floor((box.x + box.w) / CELL) + 1; + const r0 = Math.floor(box.y / CELL) - 1, r1 = Math.floor((box.y + box.h) / CELL) + 1; + for (let cx = c0; cx <= c1; cx++) { + for (let cy = r0; cy <= r1; cy++) { + const arr = grid.get(key(cx, cy)); + if (!arr) continue; + if (!overlap) { for (const i of arr) if (hit(box, boxes[i], margin)) return false; } + else if (arr.length >= MAXL) return false; + } + } + return true; + } + function addGrid(i, box) { + const c0 = Math.floor(box.x / CELL), c1 = Math.floor((box.x + box.w) / CELL); + const r0 = Math.floor(box.y / CELL), r1 = Math.floor((box.y + box.h) / CELL); + for (let cx = c0; cx <= c1; cx++) for (let cy = r0; cy <= r1; cy++) { + const k = key(cx, cy); + if (!grid.has(k)) grid.set(k, []); + grid.get(k).push(i); + } + } + function emptyCells(avoid) { + const out = []; + for (let cy = 0; cy < rows; cy++) for (let cx = 0; cx < cols; cx++) { + if (grid.has(key(cx, cy))) continue; + if (avoid) { + const x = cx * CELL, y = cy * CELL; + if (x + CELL > avoid.x && x < avoid.x + avoid.w && y + CELL > avoid.y && y < avoid.y + avoid.h) continue; + } + out.push([cx, cy]); + } + return out; + } + function place(w, h, avoid) { + const emp = emptyCells(avoid); + for (let t = 0; t < Math.min(emp.length, 26); t++) { + const [cx, cy] = emp[(Math.random() * emp.length) | 0]; + for (const box of [ + { x: Math.round(cx * CELL + (Math.random() - 0.3) * CELL * 0.5), y: Math.round(cy * CELL + (Math.random() - 0.3) * CELL * 0.5), w, h }, + { x: cx * CELL, y: cy * CELL, w, h }, + ]) { + box.x = Math.max(0, Math.min(box.x, vw - w)); + box.y = Math.max(0, Math.min(box.y, vh - h)); + if (avoid && hit(box, avoid, 4)) continue; + if (canPlace(box, 1, false)) return box; + } + } + for (let i = 0; i < 60; i++) { // 兜底:允许叠层,制造层次 + const box = { x: ((Math.random() * (vw - w)) | 0), y: ((Math.random() * (vh - h)) | 0), w, h }; + if (avoid && hit(box, avoid, 4)) continue; + if (canPlace(box, -8, true)) return box; + } + return null; + } + + // 补缝:专门拿最短的消息往剩下的空网格里塞,铺到几乎不留缝 + function placeGap(w, h, avoid) { + const emp = emptyCells(avoid); + for (let i = 0; i < Math.min(70, emp.length); i++) { + const [cx, cy] = emp[(Math.random() * emp.length) | 0]; + const box = { + x: Math.max(0, Math.min(Math.round(cx * CELL + (CELL - w) / 2), vw - w)), + y: Math.max(0, Math.min(Math.round(cy * CELL + (CELL - h) / 2), vh - h)), + w, h, + }; + if (avoid && hit(box, avoid, 4)) continue; + if (canPlace(box, -4, true)) return box; + } + return null; + } + function coverage() { + let c = 0; + for (let cy = 0; cy < rows; cy++) for (let cx = 0; cx < cols; cx++) if (grid.has(key(cx, cy))) c++; + return c / (cols * rows); + } + + // 与年度总结同量级:area/1900 + const MAX = Math.max(260, Math.min(1150, Math.round((vw * vh) / 1900))); + // 第一轮避开标题所在的横带(不能让消息压住大字),后半程解禁——标题说完就被消息一起淹掉 + const th = document.querySelector(".m-line--up"); + const tr = th ? th.getBoundingClientRect() : null; + const avoid = tr ? { x: 0, y: Math.max(0, tr.top - 10), w: vw, h: tr.height + 20 } : null; + const out = []; + const frag = document.createDocumentFragment(); + let fails = 0; + for (let n = 0; n < MAX; n++) { + if (n > 60 && n % 24 === 0 && coverage() >= 0.995) break; + const guard = n < MAX * 0.5 ? avoid : null; + let it = mfItem(false); + let box = place(it.w, it.h, guard); + if (!box) { it = mfItem(true); box = place(it.w, it.h, guard) || placeGap(it.w, it.h, guard); } + if (!box) { if (++fails > 90) break; continue; } + fails = 0; + const i = boxes.length; + boxes.push(box); + addGrid(i, box); + const el = document.createElement("div"); + el.className = "mfrag"; + el.style.cssText = `left:${box.x}px;top:${box.y}px;width:${box.w}px;z-index:${100 + (i % 7)}`; + el.innerHTML = '
' + it.html + '
' + it.hex + ""; + frag.appendChild(el); + out.push({ + el, real: el.querySelector(".mfrag__real"), cipher: el.querySelector(".mfrag__cipher"), + yn: (box.y + box.h * 0.5) / vh, jit: (Math.random() - 0.5) * 0.05, st: -1, + }); + } + host.appendChild(frag); + return out; +} + +function buildManifesto() { + const lines = $$("[data-mline]"); + mwall = new CipherWall($("#mwall")); + mwall.visible = false; + ScrollTrigger.create({ + trigger: "#manifesto", start: "top bottom", end: "bottom top", + onToggle: (s) => { mwall.visible = s.isActive; }, + }); + const frags = buildMFrags($("#mfield")); + if (REDUCED) { $("#mfield").style.display = "none"; return; } + + const cl = gsap.utils.clamp(0, 1); + const clr = (m) => { m.el.style.opacity = ""; m.el.style.transform = ""; m.real.style.opacity = ""; m.cipher.style.opacity = ""; }; + // 涌来(加速曲线)→ 被锋线逐条吞成乱码。只有正处在吞没带里的那几条逐帧改样式 + function setFrags(p, fill) { + const N = frags.length; + const shown = (N * Math.pow(cl(p / 0.46), 1.7)) | 0; + for (let i = 0; i < N; i++) { + const m = frags[i]; + if (i >= shown) { + if (m.st !== 0) { m.st = 0; m.el.classList.remove("is-in"); clr(m); } + continue; + } + const eat = (fill + m.jit - m.yn) / 0.09; + if (eat <= 0) { + if (m.st !== 1) { m.st = 1; m.el.classList.add("is-in"); clr(m); } + } else if (eat >= 1) { + if (m.st !== 3) { m.st = 3; m.el.classList.add("is-in"); m.el.style.opacity = "0"; } + } else { + m.st = 2; + m.el.classList.add("is-in"); + m.el.style.opacity = (1 - cl((eat - 0.58) / 0.42)).toFixed(3); + m.el.style.transform = `scale(${(1 - eat * 0.12).toFixed(3)})`; + m.real.style.opacity = (1 - cl(eat / 0.42)).toFixed(3); + m.cipher.style.opacity = (eat < 0.42 ? eat / 0.42 : 1 - cl((eat - 0.5) / 0.5)).toFixed(3); + } + } + } + setFrags(0, -0.06); + + const splits = lines.map((l) => new SplitText(l, { type: "chars" })); + splits.slice(0, 2).forEach((s) => gsap.set(s.chars, { opacity: 0.08 })); + gsap.set(lines[1], { opacity: 0 }); + gsap.set(lines[2], { opacity: 0 }); + + // 时间轴总长 10 == 进度 0-1,方便和装置的分幕对齐 + const tl = gsap.timeline({ + scrollTrigger: { + trigger: "#manifesto", pin: true, scrub: 0.7, + start: "top top", end: "+=280%", + onUpdate(self) { + const p = self.progress; + // 前半屏是消息风暴,后半屏加密锋线自上而下把它整个吞掉 + const fill = -0.06 + cl((p - 0.4) / 0.38) * 1.18; + mwall.fill = fill; + mwall.crack = cl((p - 0.82) / 0.18); + mwall.alpha = cl((p - 0.4) / 0.09) * (1 - mwall.crack * 0.3); + setFrags(p, fill); + stage.setAmp(0.55 + p * 0.75, 0.3); + }, + onEnter: () => stage.setOpacity(0.3, 0.9), // 粒子退到背景,让消息与密文墙唱主角 + onEnterBack: () => stage.setOpacity(0.3, 0.9), + onLeave: () => stage.setOpacity(0.75, 0.9), + onLeaveBack: () => { mwall.alpha = 0; stage.setOpacity(0.9, 0.9); }, + }, + }); + + // 第一句在风暴淹到它之前说完,第二句落在密文墙上,第三句落在裂缝里 + tl.to(splits[0].chars, { opacity: 1, stagger: 0.045, duration: 1.1, ease: "none" }, 0.2) + .to(lines[0], { yPercent: -70, opacity: 0, scale: 0.94, duration: 0.8, ease: "silk" }, 3.1) + .to(lines[1], { opacity: 1, duration: 0.01 }, 5.0) + .to(splits[1].chars, { opacity: 1, stagger: 0.045, duration: 1.3, ease: "none" }, 5.0) + .to(lines[1], { yPercent: -70, opacity: 0, scale: 0.94, duration: 0.8, ease: "silk" }, 8.0) + .fromTo(lines[2], { opacity: 0, scale: 1.7 }, { opacity: 1, scale: 1, duration: 1.2, ease: "cine" }, 8.6) + .call(() => stage.pulse(2.1), [], 8.8) + .to({}, { duration: 0.1 }, 9.9); + + // 调试钩子:不滚动也能把第二幕摆到任意进度(内嵌预览深滚会黑屏) + window.__mf = (p) => { + mwall.visible = true; + const fill = -0.06 + cl((p - 0.4) / 0.38) * 1.18; + mwall.fill = fill; + mwall.crack = cl((p - 0.82) / 0.18); + mwall.alpha = cl((p - 0.4) / 0.09) * (1 - mwall.crack * 0.3); + setFrags(p, fill); + tl.progress(p); + return window.__step(2); + }; +} + +/* ─────────────────────────── act 03 · decrypt ─────────────────────────── */ + +/* 第三幕接住第二幕的墙:中线插进 64 位密钥 → 解密波向两侧推开 → 满屏还原成原文 */ +function buildDecrypt() { + hexwall = new CipherWall($("#hexwall")); + // 开幕即满墙(承接第二幕),别从黑里淡入,否则两幕之间会闪一段黑 + hexwall.fill = 1.15; hexwall.crack = 1; hexwall.unlock = 0; hexwall.alpha = 1; + hexwall.visible = false; + ScrollTrigger.create({ + trigger: "#decrypt", start: "top bottom", end: "bottom top", + onToggle: (s) => { hexwall.visible = s.isActive; }, + }); + const dbs = $$("#ddbs [data-db]"); + const bubbles = $$("[data-bub]"); + const stepNum = $("#dstep-num"); + const stepFile = $("#dstep-file"); + const dpct = $("#dpct"); + const dtitle = $("#dtitle"); + const dsub = $("#dsub"); + const dkey = $("#dkey"); + const KEY = Array.from({ length: 64 }, () => HEXC[(Math.random() * 16) | 0]).join(""); + const cl = gsap.utils.clamp(0, 1); + + if (REDUCED) { dkey.innerHTML = "" + KEY + ""; return; } + + // 墙被揭开:产品本体自下而上顶出来,不是一张卡片飞进来 + gsap.set("#dchat", { clipPath: "inset(100% 0 0 0)" }); + gsap.set(bubbles, { opacity: 0, scale: 0.55, y: 16 }); + gsap.set([dtitle, dsub], { opacity: 0, y: 22 }); + gsap.set(dbs, { opacity: 0, y: 12 }); + + const STEPS = [ + { t: "获取密钥", s: "内存扫描自动定位 64 位数据库密钥", n: "STEP 01 / 03", f: "key_pattern @ WeChat.exe" }, + { t: "解密数据库", s: "SQLCipher 兼容算法逐页解密,生成永久可读的副本", n: "STEP 02 / 03", f: "sqlcipher · page 4096 · aes-256-cbc" }, + { t: "离线浏览", s: "高仿微信界面,无需登录、永久可读。微信可以卸载,记忆不会", n: "STEP 03 / 03", f: "message_0.db — readonly · forever" }, + ]; + let step = -1; + function setStep(i) { + if (i === step) return; + step = i; + const S = STEPS[i]; + gsap.to(dtitle, { duration: 0.55, scrambleText: { text: S.t, chars: SCRAMBLE_CN, speed: 0.9 } }); + gsap.to(dsub, { duration: 0.7, scrambleText: { text: S.s, chars: SCRAMBLE_CN, speed: 0.8 } }); + gsap.to(stepNum, { duration: 0.5, scrambleText: { text: S.n, chars: "0123456789/STEP ", speed: 1 } }); + gsap.to(stepFile, { duration: 0.7, scrambleText: { text: S.f, chars: SCRAMBLE_CN, speed: 1 } }); + } + + function dcUpdate(p) { + // 密钥逐位拼装,锁定后裂缝合上,解密波推开 + const kp = cl(p / 0.26); + const solved = (kp * 64) | 0; + let tail = ""; + for (let i = solved; i < 64; i++) tail += HEXC[(Math.random() * 16) | 0]; + dkey.innerHTML = "" + KEY.slice(0, solved) + "" + tail; + hexwall.crack = 1 - cl((p - 0.26) / 0.06); + const u = cl((p - 0.32) / 0.4); + hexwall.unlock = u; + dpct.textContent = (u * 100).toFixed(1) + "%"; + dbs.forEach((li, i) => li.classList.toggle("on", u > (i + 0.55) / dbs.length)); + setStep(p < 0.29 ? 0 : p < 0.75 ? 1 : 2); + // 粒子形态状态机(双向安全) + if (p > 0.04 && p < 0.5) stage.morphTo("key", { duration: 1.5 }); + else if (p >= 0.5) stage.morphTo("bubble", { duration: 1.6 }); + else if (p <= 0.04) stage.morphTo("halo", { duration: 1.4 }); + } + + const tl = gsap.timeline({ + scrollTrigger: { + trigger: "#decrypt", pin: true, scrub: 0.65, + start: "top top", end: "+=380%", + onUpdate(self) { dcUpdate(self.progress); }, + onEnter: () => stage.setOpacity(0.34, 0.9), + onEnterBack: () => stage.setOpacity(0.34, 0.9), + onLeave: () => stage.setOpacity(0.75, 0.9), + onLeaveBack: () => stage.setOpacity(0.3, 0.9), + }, + }); + + // 时间轴总长 10 == 进度 0-1 + tl.to([dtitle, dsub], { opacity: 1, y: 0, duration: 0.5, ease: "flow" }, 0.1) + .call(() => stage.pulse(2.2), [], 2.8) // 密钥咬合 + .to(dbs, { opacity: 1, y: 0, stagger: 0.06, duration: 0.4, ease: "flow" }, 2.9) + .to(dkey, { opacity: 0, duration: 0.4 }, 3.2) // 钥匙被吞进墙里 + .to("#dchat", { clipPath: "inset(0% 0 0 0)", duration: 1.1, ease: "cine" }, 7.4) + .to(bubbles, { opacity: 1, scale: 1, y: 0, duration: 0.5, stagger: 0.16, ease: "back.out(2.2)" }, 7.9) + .to({}, { duration: 0.1 }, 9.9); + + window.__dc = (p) => { hexwall.visible = true; dcUpdate(p); tl.progress(p); return window.__step(2); }; +} + +/* ─────────────────────────── act 04 · features ─────────────────────────── */ + +function buildFeatures() { + const cards = $$("[data-card]"); + const numEl = $("#deck-num"); + const nameEl = $("#deck-name"); + const metaEl = $("#deck-meta"); + const descEl = $("#deck-desc"); + const dots = $$("#deck-dots i"); + const N = cards.length; + const META = [ + { name: "聊天记录 1:1 复刻", meta: "全消息类型 · 时间轴跳转 · 高仿界面", desc: "文本、图片、视频、语音、表情、引用、合并转发……逐一还原,样式尽可能与微信保持一致。" }, + { name: "实时消息同步", meta: "WCDB 直读 · 侧栏闪电 · 零轮询", desc: "直连微信 4.x 的 WCDB——微信开着,新消息、联系人与朋友圈也会实时流进来。" }, + { name: "修改消息 · 随时恢复", meta: "本地改写 · 一键恢复 · 原库零改动", desc: "本地改写任意一条消息,原文永远可以一键找回。" }, + { name: "朋友圈时光机", meta: "删除仍可见 · 历史背景图 · 缓存可调", desc: "看过的朋友圈本地留存:对方改成三天可见、甚至删除动态,你依然能翻回当年的背景图。" }, + { name: "全文搜索", meta: "毫秒级 · 跨会话 · 类型过滤", desc: "多年记录毫秒级跨会话检索,按联系人、消息类型、时间范围任意过滤,一步跳回现场。" }, + { name: "导出万物", meta: "10 类内容 × 4 种格式 · ZIP", desc: "10 类内容 × HTML / JSON / TXT / Excel,从聊天记录到转账红包、全量归档,连资源打包 ZIP。" }, + { name: "MCP · 接给 AI", meta: "MCP Server · Bearer 鉴权 · 一键接入", desc: "内置 MCP 服务,一键复制接入提示词,让 Claude 或任意 MCP 客户端直接查询你的聊天档案。" }, + ]; + + const PEEK = 15; // 旧卡后方每层露出的边条像素 + const riseDist = () => Math.round(innerHeight * 0.72); // 待入场卡在屏下的距离 + + // 连续堆叠:d=i-f。d<=0 当前/已过(叠在上方露边条),d>0 待入场(从屏下升起) + function setStack(f) { + const rd = riseDist(); + cards.forEach((c, i) => { + const d = i - f; + let ty, sc, z, br; + if (d <= 0) { + const ad = -d; + ty = -ad * PEEK; + sc = 1 - ad * 0.045; + z = 200 - Math.round(ad * 12); + br = 1 - Math.min(ad * 0.14, 0.62); + } else { + ty = d * rd; + sc = 1; + z = 200 - Math.round(d * 12); + br = 1; + } + c.style.transform = `translate(-50%, -50%) translateY(${ty.toFixed(1)}px) scale(${sc.toFixed(3)})`; + c.style.zIndex = String(z); + c.style.filter = `brightness(${br.toFixed(3)})`; + }); + } + + let cur = -1; + function label(idx, animate) { + if (idx === cur) return; + const dir = idx > cur ? 1 : -1; + cur = idx; + dots.forEach((dd, i) => dd.classList.toggle("on", i === idx)); + numEl.textContent = String(idx + 1).padStart(2, "0"); + nameEl.textContent = META[idx].name; + metaEl.textContent = META[idx].meta; + descEl.textContent = META[idx].desc; + if (animate) gsap.fromTo(".deck__capmain", { opacity: 0.2, y: dir * 12 }, { opacity: 1, y: 0, duration: 0.5, ease: "flow", overwrite: true }); + } + + setStack(0); label(0, false); + isoTick = null; + if (REDUCED) return; + + const pos = { f: 0 }; + gsap.timeline({ + scrollTrigger: { + trigger: "#features", pin: true, scrub: 0.55, + start: "top top", + end: () => "+=" + Math.round(N * innerHeight * 0.72), + invalidateOnRefresh: true, + onUpdate(self) { + pos.f = self.progress * (N - 1); + setStack(pos.f); + label(Math.round(pos.f), true); + }, + onEnter: () => { stage.setOpacity(0.2, 0.8); stage.setAmp(0.3, 0.8); }, + onEnterBack: () => { stage.setOpacity(0.2, 0.8); }, + onLeaveBack: () => { stage.setOpacity(0.9, 0.8); stage.setAmp(0.55, 0.8); }, + }, + }); + + gsap.from(".deck__stack", { + opacity: 0, y: 40, duration: 0.9, ease: "flow", + scrollTrigger: { trigger: "#features", start: "top 72%" }, + }); +} + +/* ─────────────────────────── act 05 · wrapped ─────────────────────────── */ + +function buildWrapped() { + const slides = $$("#v-slides img"); + const N = slides.length; + const idxEl = $("#v-idx"); + const ticks = $$("#v-ticks i"); + const scene = $("#viewer"); + let activeIdx = -1; + + // 放映室:固定取景框内定向擦除转场,当前帧被推走、下一帧从右侧扫入 + function setFlow(f) { + const fc = Math.max(0, Math.min(N - 1, f)); + const c = Math.min(N - 2, Math.floor(fc)); + const t = Math.max(0, Math.min(1, fc - c)); + slides.forEach((img, i) => { + if (i === c) { + img.style.opacity = "1"; img.style.zIndex = "1"; + img.style.clipPath = "inset(0 0 0 0)"; + img.style.transform = `translateX(${(-t * 6).toFixed(2)}%) scale(1)`; + img.style.filter = `brightness(${(1 - t * 0.3).toFixed(3)})`; + } else if (i === c + 1) { + img.style.opacity = "1"; img.style.zIndex = "2"; + img.style.clipPath = `inset(0 0 0 ${((1 - t) * 100).toFixed(2)}%)`; + img.style.transform = `translateX(${((1 - t) * 3.5).toFixed(2)}%) scale(${(1.045 - t * 0.045).toFixed(4)})`; + img.style.filter = "brightness(1)"; + } else { + img.style.opacity = "0"; img.style.zIndex = "0"; + img.style.clipPath = i < c ? "inset(0 100% 0 0)" : "inset(0 0 0 100%)"; + } + }); + const idx = Math.round(fc); + if (idx !== activeIdx) { + activeIdx = idx; + idxEl.textContent = String(idx + 1).padStart(2, "0"); + ticks.forEach((tk, i) => tk.classList.toggle("on", i <= idx)); + gsap.fromTo(idxEl, { yPercent: 14, opacity: 0.4 }, { yPercent: 0, opacity: 1, duration: 0.45, ease: "flow", overwrite: true }); + } + } + setFlow(0); + + const flowPos = { f: 0 }; + if (REDUCED) { setFlow(0); return; } + + const introChars = new SplitText(".wrapped__title", { type: "chars" }); + gsap.set(introChars.chars, { opacity: 0, yPercent: 60 }); + gsap.set(".wrapped__stats .wstat", { opacity: 0, y: 44 }); + gsap.set(".wrapped__note", { opacity: 0 }); + gsap.set(scene, { opacity: 0, y: 40, scale: 0.94 }); + + const tl = gsap.timeline({ + scrollTrigger: { + trigger: "#wrapped", pin: true, scrub: 0.7, + start: "top top", end: "+=430%", + onEnter: () => { stage.morphTo("year", { duration: 1.7 }); stage.setTint(0x6b4a12, 1.6); stage.setOpacity(0.4, 1); }, + onLeaveBack: () => { stage.morphTo("bubble", { duration: 1.4 }); stage.setTint(0x0b3d24, 1.4); stage.setOpacity(0.9, 1); }, + }, + }); + + // 幕次:标题独占 → 标题退场 → 放映室逐帧走片 → 数字收束 + tl.to(introChars.chars, { opacity: 1, yPercent: 0, stagger: 0.03, duration: 0.9, ease: "flow" }, 0) + .to({}, { duration: 0.45 }) + .to(".wrapped__intro", { yPercent: -46, opacity: 0, scale: 0.92, duration: 0.8, ease: "silk" }, ">") + .to(scene, { opacity: 1, y: 0, scale: 1, duration: 0.7, ease: "cine" }, "<0.25") + .to(flowPos, { f: N - 1, duration: 3.8, ease: "none", onUpdate: () => setFlow(flowPos.f) }, ">-0.1") + .to(scene, { yPercent: -3.5, duration: 0.5, ease: "silk" }, ">") + .to(".wrapped__stats .wstat", { opacity: 1, y: 0, stagger: 0.14, duration: 0.6, ease: "flow" }, "<") + .call(runCounters, [], "<") + .to(".wrapped__note", { opacity: 1, duration: 0.5 }, ">-0.2") + .to({}, { duration: 0.5 }); +} + +let countersDone = false; +function runCounters() { + if (countersDone) return; + countersDone = true; + $$("[data-count]").forEach((el) => { + const target = +el.dataset.count; + const suffix = el.dataset.suffix || ""; + const o = { v: 0 }; + gsap.to(o, { + v: target, duration: 2.2, ease: "expo.out", + onUpdate: () => { el.textContent = Math.round(o.v).toLocaleString("en-US") + suffix; }, + }); + }); + $$("[data-count-time]").forEach((el) => { + const [hh, mm] = el.dataset.countTime.split(":").map(Number); + const total = hh * 60 + mm; + const o = { v: 0 }; + gsap.to(o, { + v: total, duration: 2.2, ease: "expo.out", + onUpdate: () => { + const h = Math.floor(o.v / 60), m = Math.round(o.v % 60); + el.textContent = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; + }, + }); + }); +} + +/* ─────────────────────────── act 06 · privacy ─────────────────────────── */ + +/* 第六幕改为滚动叙事:三个动作在本机真的跑一遍,两个读数一路对照 —— + 本机处理飞涨,出网死死钉在 0,最后定格盖章。 */ +const MAC_RUNS = [ + { + name: "解密", gb: 41.6, + trace: ["db_storage/message/message_0.db", "sqlcipher · page 4096 → plain", "db_storage/contact/contact.db", "db_storage/session/session.db", "sha256 verify · ok", "→ ./decrypted/*.db (local)"], + }, + { + name: "浏览", gb: 12.4, + trace: ["127.0.0.1:10391 · GET /session/list", "127.0.0.1:10391 · GET /message?page=1", "render 1,284 msgs · cache hit", "wcdb live read · no polling", "→ loopback only (local)"], + }, + { + name: "导出", gb: 27.9, + trace: ["export/HTML/2024-06.html", "export/JSON/messages.json", "export/XLSX/transfers.xlsx", "zip resources · 3.2 GB", "→ ~/Documents (local)"], + }, +]; + +function buildMachine() { + const zeroEl = $("#mac-zero"); + const passEl = $("#mac-pass"); + const actEl = $("#mac-act"); + const traceEl = $("#mac-trace"); + const localEl = $("#mac-local"); + const outEl = $("#mac-out"); + const guards = $$("[data-guard]"); + const cl = gsap.utils.clamp(0, 1); + const fmt = (gb) => (gb < 1 ? (gb * 1024).toFixed(0) + " MB" : gb.toFixed(1) + " GB"); + + if (REDUCED) { zeroEl.textContent = "0"; actEl.textContent = "解密"; return; } + + const titleSplit = new SplitText(".mac__title", { type: "chars" }); + gsap.set(titleSplit.chars, { opacity: 0, yPercent: 42 }); + gsap.set(guards, { opacity: 0, y: 22 }); + gsap.set([".mac__foot", ".mac__meters", "#mac-run"], { opacity: 0 }); + gsap.set("#mac-hero", { opacity: 0 }); + gsap.set(passEl, { opacity: 0 }); + + const TOTAL = MAC_RUNS.reduce((a, r) => a + r.gb, 0); + let curRun = -1, curLines = -1; + + function setRun(p) { + // p 0-1 覆盖三个动作 + const f = cl(p) * MAC_RUNS.length; + const i = Math.min(MAC_RUNS.length - 1, f | 0); + const inner = f - i; + const R = MAC_RUNS[i]; + if (i !== curRun) { + curRun = i; curLines = -1; + gsap.fromTo(actEl, { opacity: 0.15, yPercent: 26 }, { opacity: 1, yPercent: 0, duration: 0.45, ease: "flow", overwrite: true }); + actEl.textContent = R.name; + } + const n = Math.max(1, Math.ceil(inner * R.trace.length)); + if (n !== curLines) { + curLines = n; + traceEl.innerHTML = R.trace.slice(0, n).map((t) => "
  • " + t + "
  • ").join(""); + } + // 本机处理累加;出网永远 0 + let done = 0; + for (let k = 0; k < i; k++) done += MAC_RUNS[k].gb; + localEl.textContent = fmt(done + R.gb * inner); + outEl.textContent = "0 B"; + } + function mcUpdate(p) { + if (p < 0.2) setRun(0); + else if (p <= 0.74) setRun((p - 0.2) / 0.54); + else { localEl.textContent = fmt(TOTAL); outEl.textContent = "0 B"; } + } + setRun(0); + + const tl = gsap.timeline({ + scrollTrigger: { + trigger: "#machine", pin: true, scrub: 0.7, + start: "top top", end: "+=260%", + onUpdate(self) { mcUpdate(self.progress); }, + onEnter: () => { stage.morphTo("lock", { duration: 1.7 }); stage.setTint(0x0b3d24, 1.4); stage.setOpacity(0.5, 1); stage.setAmp(0.32, 1); }, + onEnterBack: () => { stage.morphTo("lock", { duration: 1.5 }); stage.setOpacity(0.5, 1); }, + onLeaveBack: () => { stage.morphTo("year", { duration: 1.5 }); stage.setTint(0x6b4a12, 1.4); stage.setOpacity(0.65, 1); }, + }, + }); + + // 时间轴总长 10 == 进度 0-1 + tl.to(titleSplit.chars, { opacity: 1, yPercent: 0, stagger: 0.012, duration: 0.5, ease: "flow" }, 0.1) + .to(["#mac-run", ".mac__meters"], { opacity: 1, duration: 0.5, ease: "flow" }, 1.8) + .to("#mac-run", { opacity: 0, duration: 0.5, ease: "silk" }, 7.5) + .to("#mac-hero", { opacity: 1, duration: 0.5 }, 7.7) + // 大 0:先滚乱数「审计中」→ 砰地定格 + 盖章 + .call(() => { + gsap.to({}, { + duration: 0.07, repeat: 16, overwrite: true, + onRepeat: () => { zeroEl.textContent = ((Math.random() * 640) | 0) + " KB"; }, + onComplete: () => { + zeroEl.textContent = "0"; + gsap.fromTo(zeroEl, { scale: 1.85 }, { scale: 1, duration: 0.7, ease: "back.out(2.4)" }); + gsap.to(passEl, { opacity: 1, duration: 0.4, delay: 0.15 }); + stage.pulse(1.9); + }, + }); + }, [], 7.9) + .to(guards, { opacity: 1, y: 0, stagger: 0.09, duration: 0.5, ease: "flow" }, 8.8) + .to(".mac__foot", { opacity: 1, duration: 0.5 }, 9.4) + .to({}, { duration: 0.1 }, 9.9); + + window.__mc = (p) => { mcUpdate(p); tl.progress(p); return window.__step(2); }; +} + +/* ─────────────────────────── act 07 · stack ─────────────────────────── */ + +/* ---------- 数据河:整条管线的粒子演算 ---------- */ + +class DataRiver { + constructor(canvas) { + this.cv = canvas; + this.ctx = canvas.getContext("2d"); + this.active = false; + this.flow = 1; + this.last = 0; + this.pmx = -1e4; this.pmy = -1e4; + this.HEX = "0123456789ABCDEF"; + this.CN = "周五晚上老地方见带上照片我都存着呢哈哈红包已领取晚安好梦明天见谢谢你一直都在"; + this.WORDS = ["Python", "FastAPI", "SQLite", "WCDB", "Nuxt 4", "Vue 3", "Electron", "Rust", "PyInstaller", "uv", "GSAP", "Tailwind"]; + this.G = [0.14, 0.36, 0.58, 0.76]; // 四道闸门(x 比例) + this.flashes = []; + addEventListener("pointermove", (e) => { this.pmx = e.clientX; this.pmy = e.clientY; }, { passive: true }); + addEventListener("resize", () => this.resize()); + this.resize(); + } + resize() { + const dpr = Math.min(devicePixelRatio || 1, 1.6); + this.W = this.cv.clientWidth; this.H = this.cv.clientHeight; + this.cv.width = this.W * dpr; this.cv.height = this.H * dpr; + this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + this.xEnd = this.W - Math.min(170, this.W * 0.16); + const n = Math.min(320, Math.max(140, Math.floor((this.W * this.H) / 3600))); + this.parts = Array.from({ length: n }, () => this.spawn(Math.random())); + this.words = this.WORDS.map((w, i) => ({ + w, t: i / this.WORDS.length, sp: 0.016 + Math.random() * 0.014, + lane: (Math.random() * 2 - 1) * 0.8, size: 17 + Math.random() * 16, + })); + // 闸门标签对齐到画布坐标 + $$(".river-gates span").forEach((s, i) => { + if (this.G[i] != null) s.style.left = ((this.G[i] * this.xEnd) / this.W) * 100 + "%"; + }); + } + spawn(t = 0) { + return { + t, sp: 0.05 + Math.random() * 0.055, + y0: Math.random() * 2 - 1, ex: (Math.random() * 3) | 0, + g: this.HEX[(Math.random() * 16) | 0], stage: 0, + a: 0.35 + Math.random() * 0.55, s: Math.random() < 0.12 ? 15 : 11 + Math.random() * 3, + }; + } + pos(p, time) { + const { W, H, xEnd } = this; + const cy = H * 0.5; + const x = p.t * xEnd; + const conv = Math.min(1, Math.max(0, p.t / this.G[1])); + const spread = 1 - (conv * conv * (3 - 2 * conv)) * 0.9; + let y = cy + p.y0 * H * 0.36 * spread + Math.sin(p.t * 34 + p.y0 * 9 + time * 1.8) * 3; + if (p.t > this.G[3]) { + const k = (p.t - this.G[3]) / (1 - this.G[3]); + const kk = k * k * (3 - 2 * k); + const ey = cy + (p.ex - 1) * H * 0.31; + y = y * (1 - kk) + ey * kk; + } + return [x, y]; + } + draw(time) { + const { ctx, W, H } = this; + if (!this.active) { if (!this._c) { ctx.clearRect(0, 0, W, H); this._c = 1; } return; } + if (time - this.last < 1 / 30) return; + const dt = Math.min(time - this.last, 0.06); + this.last = time; this._c = 0; + ctx.clearRect(0, 0, W, H); + const cy = H * 0.5; + const rect = this.cv.getBoundingClientRect(); + const mx = this.pmx - rect.left, my = this.pmy - rect.top; + + // 河床辉光 + const grd = ctx.createLinearGradient(0, 0, W, 0); + grd.addColorStop(0, "rgba(61,242,141,0)"); + grd.addColorStop(0.5, "rgba(61,242,141,0.1)"); + grd.addColorStop(1, "rgba(61,242,141,0)"); + ctx.fillStyle = grd; + ctx.fillRect(0, cy - 34, W, 68); + + // 底层:技术栈残影漂流 + ctx.textBaseline = "middle"; + for (const wd of this.words) { + wd.t += wd.sp * dt * this.flow; + if (wd.t > 1.08) { wd.t = -0.12; wd.lane = (Math.random() * 2 - 1) * 0.8; } + ctx.font = `900 ${wd.size}px Unbounded, sans-serif`; + ctx.fillStyle = "rgba(150, 190, 165, 0.075)"; + ctx.fillText(wd.w, wd.t * (W + 260) - 130, cy + wd.lane * H * 0.4); + } + + // 闸门光幕 + 密钥环 + for (let i = 0; i < this.G.length; i++) { + const gx = this.G[i] * this.xEnd; + const gg = ctx.createLinearGradient(0, cy - H * 0.42, 0, cy + H * 0.42); + gg.addColorStop(0, "rgba(61,242,141,0)"); + gg.addColorStop(0.5, i === 1 ? "rgba(61,242,141,0.5)" : "rgba(61,242,141,0.22)"); + gg.addColorStop(1, "rgba(61,242,141,0)"); + ctx.strokeStyle = gg; + ctx.lineWidth = i === 1 ? 1.5 : 1; + ctx.beginPath(); ctx.moveTo(gx, cy - H * 0.42); ctx.lineTo(gx, cy + H * 0.42); ctx.stroke(); + if (i === 1) { // 密钥环:旋转缺口双环 + ctx.strokeStyle = "rgba(61,242,141,0.85)"; + ctx.lineWidth = 2; + ctx.beginPath(); ctx.arc(gx, cy, 26, time * 1.4, time * 1.4 + Math.PI * 1.5); ctx.stroke(); + ctx.strokeStyle = "rgba(61,242,141,0.35)"; + ctx.lineWidth = 1; + ctx.beginPath(); ctx.arc(gx, cy, 34, -time * 0.9, -time * 0.9 + Math.PI * 1.2); ctx.stroke(); + } + } + + // 粒子(分层绘制:密文 → 明文 → 出港) + for (const p of this.parts) { + p.t += p.sp * dt * this.flow; + if (p.t > 1) Object.assign(p, this.spawn(0), { sp: p.sp }); + const stage = p.t < this.G[1] ? 0 : p.t < this.G[3] ? 1 : 2; + if (stage !== p.stage) { + if (stage === 1) { // 过密钥闸:解密瞬间 + p.g = this.CN[(Math.random() * this.CN.length) | 0]; + const [fx, fy] = this.pos(p, time); + if (this.flashes.length < 14) this.flashes.push({ x: fx, y: fy, age: 0 }); + } + p.stage = stage; + } + let [x, y] = this.pos(p, time); + const dxm = x - mx, dym = y - my; + const md = Math.hypot(dxm, dym); + if (md < 80) y += (dym / (md + 0.01)) * (80 - md) * 0.5; + if (stage === 0) { + ctx.font = `${p.s}px 'JetBrains Mono', monospace`; + ctx.fillStyle = `rgba(132, 162, 142, ${(p.a * 0.42).toFixed(3)})`; + } else if (stage === 1) { + ctx.font = `${p.s + 1}px 'JetBrains Mono', monospace`; + ctx.fillStyle = `rgba(61, 242, 141, ${(p.a * 0.85).toFixed(3)})`; + } else { + const cols = ["61, 242, 141", "234, 255, 242", "255, 194, 75"]; + ctx.font = `${p.s}px 'JetBrains Mono', monospace`; + ctx.fillStyle = `rgba(${cols[p.ex]}, ${(p.a * 0.8).toFixed(3)})`; + } + ctx.fillText(p.g, x, y); + } + + // 解密闪光 + this.flashes = this.flashes.filter((f) => (f.age += dt) < 0.5); + for (const f of this.flashes) { + const k = f.age / 0.5; + ctx.strokeStyle = `rgba(61, 242, 141, ${(0.55 * (1 - k)).toFixed(3)})`; + ctx.lineWidth = 1.5; + ctx.beginPath(); ctx.arc(f.x, f.y, 4 + k * 42, 0, Math.PI * 2); ctx.stroke(); + } + } +} + +function buildStack() { + river = new DataRiver($("#river")); + if (REDUCED) { river.active = true; river.flow = 0.12; return; } + + ScrollTrigger.create({ + trigger: "#stack", start: "top 92%", end: "bottom 8%", + onToggle(self) { river.active = self.isActive; }, + }); + ScrollTrigger.create({ + trigger: "#stack", start: "top 60%", + onEnter: () => { stage.morphTo("halo", { duration: 1.8 }); stage.setOpacity(0.35, 1); }, + }); + + gsap.from(".stack__head", { + opacity: 0, y: 40, duration: 0.9, ease: "flow", + scrollTrigger: { trigger: "#stack", start: "top 72%" }, + }); + gsap.from(".river-gates span", { + opacity: 0, y: 14, stagger: 0.1, duration: 0.6, ease: "flow", + scrollTrigger: { trigger: ".river-wrap", start: "top 78%" }, + }); + gsap.from(".river-exits span", { + opacity: 0, x: 20, stagger: 0.12, duration: 0.6, ease: "flow", + scrollTrigger: { trigger: ".river-wrap", start: "top 72%" }, + }); + gsap.from(".stack__hud li", { + opacity: 0, y: 24, stagger: 0.08, duration: 0.6, ease: "flow", + scrollTrigger: { trigger: ".stack__hud", start: "top 94%" }, + }); +} + +/* ─────────────────────────── act 07 · cta(终幕 · 归档落款)─────────────────────────── */ + +function buildCTA() { + const gate = $("#cta-gate"); + + // 下载闸内的指针光斑 + if (!TOUCH) { + gate.addEventListener("pointermove", (e) => { + const r = gate.getBoundingClientRect(); + gate.style.setProperty("--mx", (((e.clientX - r.left) / r.width) * 100).toFixed(1) + "%"); + gate.style.setProperty("--my", (((e.clientY - r.top) / r.height) * 100).toFixed(1) + "%"); + }, { passive: true }); + } + if (REDUCED) return; + + const chars = new SplitText(".cta__title", { type: "chars" }).chars; + gsap.set(chars, { opacity: 0, yPercent: 62, rotateX: -62, transformPerspective: 900, transformOrigin: "50% 100%" }); + gsap.set(".cta__slug-t", { opacity: 0 }); + gsap.set(".cta__leader", { clipPath: "inset(0 100% 0 0)" }); + gsap.set(gate, { clipPath: "inset(0 50% 0 50%)" }); + gsap.set(".gate__row", { opacity: 0, y: 26 }); + gsap.set(".ix", { opacity: 0, y: 24 }); + gsap.set(".ix__rule, .cta__colophon-rule", { scaleX: 0 }); + gsap.set(".cta__colophon-row span", { opacity: 0 }); + + ScrollTrigger.create({ + trigger: "#cta", start: "top 58%", once: true, + onEnter: () => { + const tl = gsap.timeline(); + // 刊头 → 巨字立起 → 闸门自中线拉开 → 索引细线逐条抽出 → 版权落定 + tl.to(".cta__slug-t", { opacity: 1, duration: 0.5, stagger: 0.14 }, 0) + .to(".cta__leader", { clipPath: "inset(0 0% 0 0)", duration: 0.9, ease: "cine" }, 0.08) + .to(chars, { opacity: 1, yPercent: 0, rotateX: 0, duration: 0.95, stagger: 0.045, ease: "cine" }, 0.18) + .to(gate, { clipPath: "inset(0 0% 0 0%)", duration: 1.05, ease: "cine" }, 0.62) + .to(".gate__row", { opacity: 1, y: 0, duration: 0.75, ease: "flow" }, 0.86) + .to("#gate-meta", { + duration: 1.1, + scrambleText: { text: "LATEST RELEASE · WINDOWS & macOS · OPEN SOURCE", chars: "ABCDEF0123456789·", speed: 0.8 }, + }, 1.0) + .call(() => stage.pulse(1.9), [], 1.05) + .set(gate, { clearProps: "clipPath" }, 1.72) // 交还 hover 辉光的外溢空间 + .to(".ix", { opacity: 1, y: 0, stagger: 0.09, duration: 0.6, ease: "flow" }, 1.1) + .to(".ix__rule", { scaleX: 1, stagger: 0.09, duration: 0.75, ease: "cine" }, 1.14) + .to(".cta__colophon-rule", { scaleX: 1, duration: 1.1, ease: "cine" }, 1.45) + .to(".cta__colophon-row span", { opacity: 1, stagger: 0.12, duration: 0.5 }, 1.6); + + stage.morphTo("halo", { duration: 2 }); + stage.setTint(0x0b3d24, 1.6); + stage.setOpacity(0.9, 1.2); + stage.setAmp(0.8, 1.5); + }, + }); +} + +/* ─────────────────────────── 侧栏 · 章节指示 ─────────────────────────── */ + +function buildRail() { + const cur = $("#rail-cur"); + const fill = $("#rail-fill"); + gsap.ticker.add(() => { fill.style.transform = `scaleY(${scrollProgress})`; }); + $$(".act").forEach((act) => { + // 被 pin 的幕外面套了一层 .pin-spacer,必须拿 spacer 当 trigger, + // 否则区间塌成一屏,章节号会卡在上一幕不动 + const p = act.parentElement; + const el = p && p.classList.contains("pin-spacer") ? p : act; + ScrollTrigger.create({ + trigger: el, start: "top 52%", end: "bottom 52%", + onToggle(self) { + if (!self.isActive) return; + if (window.__cursorAct) window.__cursorAct(act.dataset.act); + if (cur.textContent !== act.dataset.act) { + gsap.fromTo(cur, { opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.4 }); + cur.textContent = act.dataset.act; + } + }, + }); + }); +} + +/* ─────────────────────────── 光标 & 磁吸 ─────────────────────────── */ + +function setupCursor() { + if (TOUCH || REDUCED) return; + const ring = $(".cursor"); + const dot = $(".cursor-dot"); + const addr = $("#cursor-addr"); + gsap.set([ring, dot, addr], { opacity: 0 }); // 首次移动前隐藏,避免卡在左上角 + const rx = gsap.quickTo(ring, "x", { duration: 0.4, ease: "expo.out" }); + const ry = gsap.quickTo(ring, "y", { duration: 0.4, ease: "expo.out" }); + const dx = gsap.quickTo(dot, "x", { duration: 0.08 }); + const dy = gsap.quickTo(dot, "y", { duration: 0.08 }); + const ax = gsap.quickTo(addr, "x", { duration: 0.32, ease: "expo.out" }); + const ay = gsap.quickTo(addr, "y", { duration: 0.32, ease: "expo.out" }); + const hx = (v) => v.toString(16).toUpperCase().padStart(3, "0"); + // 读数随幕切换:每一幕光标都是那一幕正在用的那把「工具」 + const MODES = { + "01": (x, y) => "0x" + hx(x) + "·" + hx(y), // 内存扫描 + "02": () => hexStr(3), // 密文探针:乱码随手抖 + "03": (x, y) => "KEY 0x" + hx(x) + hx(y), // 密钥 + "04": (x, y) => "0x" + hx(x) + "·" + hx(y), + "05": () => "WRAPPED 2025", + "06": () => "0 B · EGRESS", + "07": () => "GET LATEST ↓", + }; + let act = "01"; + window.__cursorAct = (id) => { + if (!MODES[id] || id === act) return; + act = id; + ring.dataset.act = id; addr.dataset.act = id; dot.dataset.act = id; + }; + + let shown = false, lastAddr = ""; + addEventListener("pointermove", (e) => { + if (!shown) { shown = true; gsap.set([ring, dot, addr], { x: e.clientX, y: e.clientY }); gsap.to([ring, dot, addr], { opacity: 1, duration: 0.3 }); } + rx(e.clientX); ry(e.clientY); dx(e.clientX); dy(e.clientY); ax(e.clientX); ay(e.clientY); + const s = MODES[act](e.clientX & ~3, e.clientY & ~3); + if (s !== lastAddr) { lastAddr = s; addr.textContent = s; } + }, { passive: true }); + document.addEventListener("pointerover", (e) => { + const t = e.target.closest("[data-cursor], a, button"); + ring.classList.toggle("is-hover", !!t); + }); +} + +function setupMagnetic() { + if (TOUCH || REDUCED) return; + $$("[data-magnetic]").forEach((el) => { + const xTo = gsap.quickTo(el, "x", { duration: 0.9, ease: "elastic.out(1,0.4)" }); + const yTo = gsap.quickTo(el, "y", { duration: 0.9, ease: "elastic.out(1,0.4)" }); + el.addEventListener("pointermove", (e) => { + const r = el.getBoundingClientRect(); + xTo((e.clientX - r.left - r.width / 2) * 0.34); + yTo((e.clientY - r.top - r.height / 2) * 0.34); + }); + el.addEventListener("pointerleave", () => { xTo(0); yTo(0); }); + }); +} + +/* ─────────────────────────── github stars ─────────────────────────── */ + +function fetchStars() { + fetch("https://api.github.com/repos/LifeArchiveProject/WeChatDataAnalysis") + .then((r) => (r.ok ? r.json() : null)) + .then((d) => { + if (!d || !d.stargazers_count) return; + const n = d.stargazers_count; + $("#gh-stars").textContent = "★ " + (n >= 1000 ? (n / 1000).toFixed(1) + "k" : n); + }) + .catch(() => {}); +} + +/* ─────────────────────────── go ─────────────────────────── */ + +if (document.readyState === "loading") addEventListener("DOMContentLoaded", boot); +else boot(); diff --git a/website/assets/js/particles.js b/website/assets/js/particles.js new file mode 100644 index 00000000..a0675328 --- /dev/null +++ b/website/assets/js/particles.js @@ -0,0 +1,429 @@ +/* ════════════════════════════════════════════════════════════ + particles.js — 全站 WebGL 舞台 + 一团会变形的粒子:星云 → 钥匙 → 气泡 → 2025 → 锁 + + FBM 极光背景。全部着色器驱动,CPU 只管 morph 编排。 + ════════════════════════════════════════════════════════════ */ +import * as THREE from "three"; + +const isMobile = matchMedia("(max-width: 768px)").matches; +const COUNT = isMobile ? 9000 : 24000; +const WORLD = 17; // 形状铺开的世界尺寸 + +/* ---------- 形状采样 ---------- */ + +function samplePixels(draw, n) { + const S = 400; + const cv = document.createElement("canvas"); + cv.width = cv.height = S; + const ctx = cv.getContext("2d", { willReadFrequently: true }); + ctx.fillStyle = "#fff"; + ctx.strokeStyle = "#fff"; + draw(ctx, S); + const data = ctx.getImageData(0, 0, S, S).data; + const pts = []; + for (let y = 0; y < S; y += 1) { + for (let x = 0; x < S; x += 1) { + if (data[(y * S + x) * 4 + 3] > 120) pts.push([x / S - 0.5, 0.5 - y / S]); + } + } + const out = new Float32Array(n * 3); + for (let i = 0; i < n; i++) { + // 88% 落在形状上,12% 化作周围游尘 + if (pts.length && Math.random() > 0.12) { + const p = pts[(Math.random() * pts.length) | 0]; + out[i * 3] = p[0] * WORLD + (Math.random() - 0.5) * 0.14; + out[i * 3 + 1] = p[1] * WORLD + (Math.random() - 0.5) * 0.14; + out[i * 3 + 2] = (Math.random() - 0.5) * 1.4; + } else { + const r = 9 + Math.random() * 9; + const a = Math.random() * Math.PI * 2; + out[i * 3] = Math.cos(a) * r; + out[i * 3 + 1] = (Math.random() - 0.5) * 12; + out[i * 3 + 2] = -2 - Math.random() * 6; + } + } + return out; +} + +function shapeHalo(n) { + const out = new Float32Array(n * 3); + for (let i = 0; i < n; i++) { + const arm = (i % 3) * ((Math.PI * 2) / 3); + const t = Math.pow(Math.random(), 0.55); + const r = 2.2 + t * 9.5; + const a = arm + t * 2.6 + (Math.random() - 0.5) * 0.9; + const y = (Math.random() - 0.5) * (1.6 - t * 1.1); + if (Math.random() < 0.85) { + out[i * 3] = Math.cos(a) * r * 1.28; + out[i * 3 + 1] = Math.sin(a) * r * 0.62 + y; + out[i * 3 + 2] = (Math.random() - 0.5) * 3.2; + } else { + const rr = 11 + Math.random() * 7; + const aa = Math.random() * Math.PI * 2; + out[i * 3] = Math.cos(aa) * rr; + out[i * 3 + 1] = (Math.random() - 0.5) * 13; + out[i * 3 + 2] = -3 - Math.random() * 5; + } + } + return out; +} + +function shapeKey(n) { + return samplePixels((c, S) => { + const u = S / 400; + c.lineWidth = 26 * u; + c.lineCap = "round"; + c.beginPath(); // 齿孔圆环 + c.arc(150 * u, 150 * u, 74 * u, 0, Math.PI * 2); + c.stroke(); + c.beginPath(); // 钥匙柄 + c.moveTo(205 * u, 205 * u); + c.lineTo(330 * u, 330 * u); + c.moveTo(282 * u, 282 * u); + c.lineTo(330 * u, 234 * u); + c.moveTo(240 * u, 240 * u); + c.lineTo(282 * u, 198 * u); + c.stroke(); + }, n); +} + +function shapeBubble(n) { + return samplePixels((c, S) => { + const u = S / 400; + const r = 46 * u; + c.beginPath(); + c.roundRect(56 * u, 96 * u, 288 * u, 170 * u, r); + c.fill(); + c.beginPath(); // 尾巴 + c.moveTo(116 * u, 258 * u); + c.lineTo(96 * u, 316 * u); + c.lineTo(172 * u, 264 * u); + c.closePath(); + c.fill(); + c.globalCompositeOperation = "destination-out"; // 打字点 + for (let i = 0; i < 3; i++) { + c.beginPath(); + c.arc((146 + i * 54) * u, 181 * u, 17 * u, 0, Math.PI * 2); + c.fill(); + } + }, n); +} + +function shapeYear(n, text) { + return samplePixels((c, S) => { + c.font = `900 ${S * 0.34}px "Unbounded","Arial Black",sans-serif`; + c.textAlign = "center"; + c.textBaseline = "middle"; + c.fillText(text, S / 2, S / 2); + }, n); +} + +function shapeLock(n) { + return samplePixels((c, S) => { + const u = S / 400; + c.lineWidth = 26 * u; + c.beginPath(); // 锁梁 + c.arc(200 * u, 158 * u, 66 * u, Math.PI, 0); + c.moveTo(134 * u, 158 * u); + c.lineTo(134 * u, 196 * u); + c.moveTo(266 * u, 158 * u); + c.lineTo(266 * u, 196 * u); + c.stroke(); + c.beginPath(); // 锁体 + c.roundRect(96 * u, 196 * u, 208 * u, 148 * u, 26 * u); + c.fill(); + c.globalCompositeOperation = "destination-out"; // 锁孔 + c.beginPath(); + c.arc(200 * u, 254 * u, 20 * u, 0, Math.PI * 2); + c.fill(); + c.fillRect(189 * u, 258 * u, 22 * u, 46 * u); + }, n); +} + +/* ---------- 着色器 ---------- */ + +const VERT = /* glsl */ ` +uniform float uTime, uMorph, uAmp, uFly, uSize, uPR, uMouseF; +uniform vec3 uMouse; +attribute vec3 aTarget; +attribute float aRand, aScale; +varying float vRand, vFlight, vDepth; + +void main() { + float d = clamp((uMorph - aRand * 0.42) / 0.58, 0.0, 1.0); + float tt = d * d * (3.0 - 2.0 * d); + vec3 pos = mix(position, aTarget, tt); + + // morph 中途的爆散飞行 + float flight = sin(tt * 3.14159265); + pos += normalize(pos + vec3(0.001)) * flight * (1.2 + aRand * 3.0) * uFly; + vFlight = flight; + + // 伪 curl 呼吸 + float t = uTime * 0.55; + pos.x += sin(pos.y * 0.62 + t + aRand * 6.283) * uAmp * (0.35 + aRand * 0.65); + pos.y += cos(pos.x * 0.53 - t * 1.18 + aRand * 4.0) * uAmp * (0.35 + aRand * 0.65); + pos.z += sin(pos.x * 0.36 + pos.y * 0.3 + t * 0.8) * uAmp * 0.55; + + // 鼠标斥力 + vec2 diff = pos.xy - uMouse.xy; + float dist = length(diff); + float force = smoothstep(4.2, 0.0, dist) * uMouseF; + pos.xy += normalize(diff + vec2(0.0001)) * force * (0.6 + aRand); + + vec4 mv = modelViewMatrix * vec4(pos, 1.0); + gl_Position = projectionMatrix * mv; + float tw = 0.72 + 0.4 * sin(uTime * (1.2 + aRand * 2.2) + aRand * 40.0); + gl_PointSize = uSize * aScale * tw * uPR * (30.0 / -mv.z); + vRand = aRand; + vDepth = smoothstep(-52.0, -16.0, mv.z); +}`; + +const FRAG = /* glsl */ ` +uniform vec3 uColA, uColB, uColC; +uniform float uOpacity; +varying float vRand, vFlight, vDepth; + +void main() { + vec2 uv = gl_PointCoord - 0.5; + float d = length(uv); + if (d > 0.5) discard; + float soft = smoothstep(0.5, 0.06, d); + float core = pow(smoothstep(0.24, 0.0, d), 2.8); + vec3 col = mix(uColA, uColB, smoothstep(0.15, 0.85, vRand)); + col = mix(col, uColC, core * 0.5); + col += uColB * vFlight * 0.45; // 飞行时提亮 + float a = soft * (0.13 + 0.45 * vDepth) * uOpacity; + gl_FragColor = vec4(col, a); +}`; + +const AURORA_FRAG = /* glsl */ ` +precision highp float; +uniform float uTime, uShift; +uniform vec2 uRes; +uniform vec3 uTint; +varying vec2 vUv; + +float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } +float noise(vec2 p){ + vec2 i = floor(p), f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(mix(hash(i), hash(i + vec2(1,0)), u.x), + mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), u.x), u.y); +} +float fbm(vec2 p){ + float v = 0.0, a = 0.5; + for (int i = 0; i < 4; i++) { v += a * noise(p); p *= 2.13; a *= 0.5; } + return v; +} +void main(){ + vec2 uv = vUv; + vec2 p = uv * vec2(uRes.x / uRes.y, 1.0) * 1.6; + float t = uTime * 0.03; + float f = fbm(p + vec2(t, uShift * 2.2) + fbm(p * 1.7 - t) * 0.9); + vec3 base = vec3(0.012, 0.022, 0.016); + vec3 col = base + uTint * pow(f, 2.6) * 0.55; + col += vec3(0.02, 0.09, 0.05) * pow(fbm(p * 0.5 + t * 0.6), 3.0) * 0.8; + float vig = smoothstep(1.35, 0.25, length(uv - 0.5) * 1.9); + col *= mix(0.35, 1.0, vig); + gl_FragColor = vec4(col, 1.0); +}`; + +/* ---------- 主体 ---------- */ + +export function createStage(canvas) { + const renderer = new THREE.WebGLRenderer({ + canvas, + antialias: false, + alpha: false, + powerPreference: "high-performance", + }); + const DPR = Math.min(devicePixelRatio || 1, 1.8); + renderer.setPixelRatio(DPR); + renderer.setClearColor(0x050807, 1); + + const scene = new THREE.Scene(); + const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 120); + camera.position.set(0, 0, 30); + + // 极光背景(跟随相机的大平面,最先渲染) + const auroraUniforms = { + uTime: { value: 0 }, + uShift: { value: 0 }, + uRes: { value: new THREE.Vector2(1, 1) }, + uTint: { value: new THREE.Color(0x0b3d24) }, + }; + const aurora = new THREE.Mesh( + new THREE.PlaneGeometry(2, 2), + new THREE.ShaderMaterial({ + uniforms: auroraUniforms, + vertexShader: `varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy, 0.9999, 1.0); }`, + fragmentShader: AURORA_FRAG, + depthWrite: false, + depthTest: false, + }) + ); + aurora.renderOrder = -1; + aurora.frustumCulled = false; + scene.add(aurora); + + // 形状库 + const shapes = { + halo: shapeHalo(COUNT), + key: shapeKey(COUNT), + bubble: shapeBubble(COUNT), + year: shapeYear(COUNT, "2025"), + lock: shapeLock(COUNT), + }; + + const geo = new THREE.BufferGeometry(); + const base = new Float32Array(shapes.halo); + const target = new Float32Array(shapes.halo); + const rand = new Float32Array(COUNT); + const scale = new Float32Array(COUNT); + for (let i = 0; i < COUNT; i++) { + rand[i] = Math.random(); + scale[i] = 0.5 + Math.pow(Math.random(), 3.4) * 1.7; + } + geo.setAttribute("position", new THREE.BufferAttribute(base, 3)); + geo.setAttribute("aTarget", new THREE.BufferAttribute(target, 3)); + geo.setAttribute("aRand", new THREE.BufferAttribute(rand, 1)); + geo.setAttribute("aScale", new THREE.BufferAttribute(scale, 1)); + + const uniforms = { + uTime: { value: 0 }, + uMorph: { value: 0 }, + uAmp: { value: 0.55 }, + uFly: { value: 1 }, + uSize: { value: isMobile ? 7.5 : 9.5 }, + uPR: { value: DPR }, + uMouse: { value: new THREE.Vector3(999, 999, 0) }, + uMouseF: { value: 1.6 }, + uOpacity: { value: 0 }, + uColA: { value: new THREE.Color(0x0a5c33) }, + uColB: { value: new THREE.Color(0x35e07f) }, + uColC: { value: new THREE.Color(0xd8ffe9) }, + }; + + const points = new THREE.Points( + geo, + new THREE.ShaderMaterial({ + uniforms, + vertexShader: VERT, + fragmentShader: FRAG, + transparent: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + }) + ); + scene.add(points); + + /* ----- morph 编排 ----- */ + let currentShape = "halo"; + let morphTween = null; + + // 把当前插值状态烘焙进 base,避免打断时跳变 + function bake() { + const m = uniforms.uMorph.value; + if (m <= 0) return; + const p = geo.attributes.position.array; + const t = geo.attributes.aTarget.array; + for (let i = 0; i < COUNT; i++) { + const d = Math.min(1, Math.max(0, (m - rand[i] * 0.42) / 0.58)); + const tt = d * d * (3 - 2 * d); + p[i * 3] += (t[i * 3] - p[i * 3]) * tt; + p[i * 3 + 1] += (t[i * 3 + 1] - p[i * 3 + 1]) * tt; + p[i * 3 + 2] += (t[i * 3 + 2] - p[i * 3 + 2]) * tt; + } + geo.attributes.position.needsUpdate = true; + uniforms.uMorph.value = 0; + } + + function morphTo(name, { duration = 1.7, fly = 1 } = {}) { + if (name === currentShape || !shapes[name]) return; + if (morphTween) morphTween.kill(); + bake(); + currentShape = name; + geo.attributes.aTarget.array.set(shapes[name]); + geo.attributes.aTarget.needsUpdate = true; + uniforms.uFly.value = fly; + morphTween = gsap.to(uniforms.uMorph, { + value: 1, + duration, + ease: "power2.inOut", + overwrite: true, + onComplete() { + geo.attributes.position.array.set(shapes[name]); + geo.attributes.position.needsUpdate = true; + uniforms.uMorph.value = 0; + morphTween = null; + }, + }); + } + + /* ----- 交互 & 帧循环 ----- */ + const mouseNDC = new THREE.Vector2(0, 0); + let parallax = { x: 0, y: 0 }; + + function pointerMove(e) { + mouseNDC.x = (e.clientX / innerWidth) * 2 - 1; + mouseNDC.y = -(e.clientY / innerHeight) * 2 + 1; + } + addEventListener("pointermove", pointerMove, { passive: true }); + + function resize() { + const w = innerWidth, h = innerHeight; + renderer.setSize(w, h, false); + camera.aspect = w / h; + camera.updateProjectionMatrix(); + auroraUniforms.uRes.value.set(w, h); + } + addEventListener("resize", resize); + resize(); + + let scrollRot = 0; + const api = { + uniforms, + aurora: auroraUniforms, + morphTo, + pulse(strength = 1.4) { + gsap.fromTo(uniforms.uAmp, { value: strength }, { value: api._restAmp, duration: 1.6, ease: "expo.out", overwrite: true }); + }, + setAmp(v, dur = 1) { + api._restAmp = v; + gsap.to(uniforms.uAmp, { value: v, duration: dur, ease: "power2.out", overwrite: "auto" }); + }, + setOpacity(v, dur = 1) { + gsap.to(uniforms.uOpacity, { value: v, duration: dur, ease: "power2.out", overwrite: "auto" }); + }, + setTint(hex, dur = 1.4) { + const c = new THREE.Color(hex); + gsap.to(auroraUniforms.uTint.value, { r: c.r, g: c.g, b: c.b, duration: dur, ease: "sine.inOut", overwrite: "auto" }); + }, + setScroll(p) { + scrollRot = p; + auroraUniforms.uShift.value = p; + }, + _restAmp: 0.55, + update(t) { + uniforms.uTime.value = t; + auroraUniforms.uTime.value = t; + // 鼠标 → 世界坐标(z=0 平面) + const v = new THREE.Vector3(mouseNDC.x, mouseNDC.y, 0.5).unproject(camera); + const dir = v.sub(camera.position).normalize(); + const dist = -camera.position.z / dir.z; + const world = camera.position.clone().add(dir.multiplyScalar(dist)); + uniforms.uMouse.value.lerp(world, 0.12); + // 视差 & 滚动旋转 + parallax.x += (mouseNDC.x * 1.6 - parallax.x) * 0.04; + parallax.y += (mouseNDC.y * 1.0 - parallax.y) * 0.04; + camera.position.x = parallax.x; + camera.position.y = parallax.y * 0.7; + camera.lookAt(0, 0, 0); + points.rotation.y = scrollRot * Math.PI * 1.15 + t * 0.02; + points.rotation.x = Math.sin(scrollRot * Math.PI) * 0.14; + renderer.render(scene, camera); + }, + }; + return api; +} diff --git a/website/index.html b/website/index.html new file mode 100644 index 00000000..e66b1e10 --- /dev/null +++ b/website/index.html @@ -0,0 +1,376 @@ + + + + + + WeChatDataAnalysis — 你的微信数据,由你留存 + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + +

    + + 你的聊天记录 + 本就属于 +

    +

    解密 · 浏览 · 搜索 · 导出 · 年度总结 —— 全部离线完成

    + + +
    + + +
    +
    + + +

    多年的对话、照片、
    红包与告别,

    +

    都锁在一个你打不开
    加密数据库里。

    +

    直到现在

    +
    +
    + + +
    +
    + + +
    +
    + STEP 01 / 03 + key_pattern @ WeChat.exe +
    +
    0.0%
    +
    + +

    获取密钥

    +

    内存扫描自动定位 64 位数据库密钥

    + + +
    + +
    +

    昨天 21:47

    +
    + +
    到了跟我说一声
    +
    +
    + +
    刚落地,还是老地方见
    +
    +
    + +
    + + 4″ +
    +
    +
    + +
    +
    +
    + +
    +
    🧧
    恭喜发财,大吉大利领取红包
    +
    微信红包
    +
    +
    +
    + +
    好,就这么定了
    +
    +
    +
    + +
      +
    • message_0.db
    • +
    • contact.db
    • +
    • session.db
    • +
    • sns.db
    • +
    • favorite.db
    • +
    • media_0.db
    • +
    +
    +
    + + +
    +
    +
    +
    +

    FEATURES · 功能全景

    + 01 +

    聊天记录 1:1 复刻

    +

    文本、图片、视频、语音、表情、引用、合并转发……逐一还原,样式尽可能与微信保持一致。

    +

    全消息类型 · 时间轴跳转 · 高仿界面

    +
    +
    + +
    +
    聊天记录页面
    +
    实时消息同步
    +
    修改消息
    +
    朋友圈
    +
    聊天记录搜索
    +
    导出
    +
    $ mcp connect 127.0.0.1:10392/mcp
    + Authorization: Bearer ****************
    + tools: search_messages · get_contacts · …
    +✓ 你的聊天档案,现在可以被 AI 提问
    +
    +
    +
    +
    + + +
    +
    +
    +

    ANNUAL WRAPPED — 年度总结

    +

    而这一年,
    值得被数据温柔复述

    +
    + + +
    +
    + WRAPPED FILM · 2025 + FRAME 01/ 06 +
    +
    +
    + 年度总结卡片:字数统计 + 年度总结卡片:赛博作息 + 年度总结卡片:回复速度 + 年度总结卡片:月度挚友墙 + 年度总结卡片:表情宇宙 + 年度总结卡片:Bento 总览 +
    +
    +
    +
    +
    +
    + +
    +
    0条消息,被完整找回
    +
    00:00最晚的一次夜聊
    +
    0回复 TA 的中位速度
    +
    0个表情包一起走过
    +
    + +

    8 张动态卡片 — 年度总览 · 赛博作息 · 字数统计 · 回复速度 · 表情宇宙 · 月度挚友墙 · 关键词云 · Bento 总结

    +
    +
    + + +
    +
    +
    +

    THE MACHINE — 本机 · 引擎 × 隐私

    +

    解密、浏览、导出
    全程不出这台电脑

    +
    + +
    + +
    + 解密 + +
    + + +
    +
    + 0 + +
    +
    + 字节出网 · BYTES UPLOADED + ✓ AUDIT PASSED · 0 EGRESS +
    +
    +
    + + +
    +
    + 本机处理 · PROCESSED ON THIS MACHINE + 0 B +
    +
    + 出网 · UPLOADED + 0 B +
    +
    + +
      +
    • 本地运行密钥与数据全程不出本机,不存在“云端”
    • +
    • 开源可审计逐行开放,`source.visibility = open`
    • +
    • SHA-256 校验Win ↔ Mac 归档迁移逐文件比对
    • +
    • 微信可卸载解密副本永久可读,记忆不会走
    • +
    + +
    +

    ENGINEPython · FastAPI · SQLite · WCDB · Rust · Nuxt 4 · Vue 3 · Electron · uv

    + +
    +
    +
    + + +
    + +
    + ACT 07 · 终章 + + FILE CLOSED · SEALED 2026 +
    + +

    把记忆,握回自己手里

    + + + + + + + 开始留档 + + LATEST RELEASE · WINDOWS & macOS · OPEN SOURCE + + + + + + + +
    + +
    + © 2026 LifeArchiveProject + 仅限个人合法使用 · 数据请谨慎保管 + Made with GSAP · Three.js · Lenis +
    +
    +
    + +
    + + + + + + + + + + + + + + + diff --git a/website/serve.py b/website/serve.py new file mode 100644 index 00000000..42423b9d --- /dev/null +++ b/website/serve.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +"""官网本地预览服务器:禁用缓存,改完即所见。用法:python3 website/serve.py [port]""" +import functools +import http.server +import sys +from pathlib import Path + + +class NoCacheHandler(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + self.send_header("Cache-Control", "no-store, must-revalidate") + self.send_header("Expires", "0") + super().end_headers() + + +if __name__ == "__main__": + port = int(sys.argv[1]) if len(sys.argv) > 1 else 4321 + handler = functools.partial(NoCacheHandler, directory=str(Path(__file__).resolve().parent)) + http.server.test(HandlerClass=handler, port=port, bind="127.0.0.1") diff --git a/website/vendor/CustomEase.min.js b/website/vendor/CustomEase.min.js new file mode 100644 index 00000000..79d65d74 --- /dev/null +++ b/website/vendor/CustomEase.min.js @@ -0,0 +1,11 @@ +/*! + * CustomEase 3.14.2 + * https://gsap.com + * + * @license Copyright 2025, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function m(e){return Math.round(1e5*e)/1e5||0}function n(e){return e.closed=Math.abs(e[0]-e[e.length-2])<.001&&Math.abs(e[1]-e[e.length-1])<.001}var w=/[achlmqstvz]|(-?\d*\.?\d*(?:e[\-+]?\d+)?)[0-9]/gi,P=/[\+\-]?\d*\.?\d+e[\+\-]?\d+/gi,Y=Math.PI/180,k=Math.sin,B=Math.cos,F=Math.abs,J=Math.sqrt;function arcToSegment(e,t,n,s,a,r,i,o,h){if(e!==o||t!==h){n=F(n),s=F(s);var u=a%360*Y,f=B(u),c=k(u),l=Math.PI,g=2*l,m=(e-o)/2,x=(t-h)/2,d=f*m+c*x,y=-c*m+f*x,p=d*d,M=y*y,v=p/(n*n)+M/(s*s);1u.x||u.y!==h.y&&u.x===h.x||h===u)&&h.x<=1?(u.cx=h.x-u.x,u.cy=h.y-u.y,u.n=h,u.nx=h.x,d&&1f||r===u-1)&&(s.push(c,l),a=(h-l)/(o-c)),c=o,l=h;s="M"+s.join(",")}return p&&p.setAttribute("d",s),s},CustomEase);function CustomEase(e,t,n){s||r(),this.id=e,this.setData(t,n)}i.version="3.14.2",i.headless=!0,q()&&M.registerPlugin(i),e.CustomEase=i,e.default=i;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}}); + diff --git a/website/vendor/DrawSVGPlugin.min.js b/website/vendor/DrawSVGPlugin.min.js new file mode 100644 index 00000000..9174b446 --- /dev/null +++ b/website/vendor/DrawSVGPlugin.min.js @@ -0,0 +1,11 @@ +/*! + * DrawSVGPlugin 3.14.2 + * https://gsap.com + * + * @license Copyright 2025, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function l(){return"undefined"!=typeof window}function m(){return t||l()&&(t=window.gsap)&&t.registerPlugin&&t}function p(e){return Math.round(1e4*e)/1e4}function q(e){return parseFloat(e)||0}function r(e,t){var r=q(e);return~e.indexOf("%")?r/100*t:r}function s(e,t){return q(e.getAttribute(t))}function u(e,t,r,n,s,i){return D(Math.pow((q(r)-q(e))*s,2)+Math.pow((q(n)-q(t))*i,2))}function v(e){return console.warn(e)}function w(e){return"non-scaling-stroke"===e.getAttribute("vector-effect")}function z(e){if(!(e=k(e)[0]))return 0;var t,r,n,i,o,a,f,h=e.tagName.toLowerCase(),l=e.style,d=1,c=1;w(e)&&(c=e.getScreenCTM(),d=D(c.a*c.a+c.b*c.b),c=D(c.d*c.d+c.c*c.c));try{r=e.getBBox()}catch(e){v("Some browsers won't measure invisible elements (like display:none or masks inside defs).")}var g=r||{x:0,y:0,width:0,height:0},_=g.x,y=g.y,x=g.width,m=g.height;if(r&&(x||m)||!M[h]||(x=s(e,M[h][0]),m=s(e,M[h][1]),"rect"!==h&&"line"!==h&&(x*=2,m*=2),"line"===h&&(_=s(e,"x1"),y=s(e,"y1"),x=Math.abs(x-_),m=Math.abs(m-y))),"path"===h)i=l.strokeDasharray,l.strokeDasharray="none",t=e.getTotalLength()||0,p(d)!==p(c)&&!b&&(b=1)&&v("Warning: length cannot be measured when vector-effect is non-scaling-stroke and the element isn't proportionally scaled."),t*=(d+c)/2,l.strokeDasharray=i;else if("rect"===h)t=2*x*d+2*m*c;else if("line"===h)t=u(_,y,_+x,y+m,d,c);else if("polyline"===h||"polygon"===h)for(n=e.getAttribute("points").match(P)||[],"polygon"===h&&n.push(n[0],n[1]),t=0,o=2;ot._length-.05&&(i+=i<0?.005:-.005)&&(r+=.005),a.strokeDashoffset=s?i:i+.001,a.strokeDasharray=r<.1?"none":s?s+"px,"+(t._nowrap?999999:r)+"px":"0px, 999999px"}}else t.styles.revert()},getLength:z,getPosition:A};m()&&t.registerPlugin(n),e.DrawSVGPlugin=n,e.default=n;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}}); + diff --git a/website/vendor/ScrambleTextPlugin.min.js b/website/vendor/ScrambleTextPlugin.min.js new file mode 100644 index 00000000..718ed027 --- /dev/null +++ b/website/vendor/ScrambleTextPlugin.min.js @@ -0,0 +1,11 @@ +/*! + * ScrambleTextPlugin 3.14.2 + * https://gsap.com + * + * @license Copyright 2025, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(D,u){"object"==typeof exports&&"undefined"!=typeof module?u(exports):"function"==typeof define&&define.amd?define(["exports"],u):u((D=D||self).window=D.window||{})}(this,function(D){"use strict";var s=/(?:^\s+|\s+$)/g,a=/([\uD800-\uDBFF][\uDC00-\uDFFF](?:[\u200D\uFE0F][\uD800-\uDBFF][\uDC00-\uDFFF]){2,}|\uD83D\uDC69(?:\u200D(?:(?:\uD83D\uDC69\u200D)?\uD83D\uDC67|(?:\uD83D\uDC69\u200D)?\uD83D\uDC66)|\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D(?:\uD83D\uDC69\u200D)?\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D(?:\uD83D\uDC69\u200D)?\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2642\u2640]\uFE0F|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDD27\uDCBC\uDD2C\uDE80\uDE92])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC6F\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3C-\uDD3E\uDDD6-\uDDDF])\u200D[\u2640\u2642]\uFE0F|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|(?:\u26F9|\uD83C[\uDFCC\uDFCB]|\uD83D\uDD75)(?:\uFE0F\u200D[\u2640\u2642]|(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642])\uFE0F|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2695\u2696\u2708]|\uD83D\uDC69\u200D[\u2695\u2696\u2708]|\uD83D\uDC68(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708]))\uFE0F|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83D\uDC69\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|\uD83D\uDC68(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC66\u200D\uD83D\uDC66|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|(?:\uD83C[\uDFFB-\uDFFF])\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDD1-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\u200D(?:(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC67|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC66)|\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F)/;function getText(D){var u=D.nodeType,F="";if(1===u||9===u||11===u){if("string"==typeof D.textContent)return D.textContent;for(D=D.firstChild;D;D=D.nextSibling)F+=getText(D)}else if(3===u||4===u)return D.nodeValue;return F}function emojiSafeSplit(D,u,F,C,E){if(D+="",F&&(D=D.trim?D.trim():D.replace(s,"")),u&&""!==u)return D.replace(/>/g,">").replace(/"===t?">":"<"===t?"<":!C||" "!==t||" "!==D.charAt(r-1)&&" "!==D.charAt(r+1)?t:" ");return i}var o=(CharSet.prototype.grow=function grow(D){for(var u=0;u<20;u++)this.sets[u]+=F(D-this.length,this.chars);this.length=D},CharSet);function CharSet(D){this.chars=emojiSafeSplit(D),this.sets=[],this.length=50;for(var u=0;u<20;u++)this.sets[u]=F(80,this.chars)}function i(){return u||"undefined"!=typeof window&&(u=window.gsap)&&u.registerPlugin&&u}function p(){B=u=i()}var u,B,l=/\s+/g,F=function _scrambleText(D,u){for(var F=u.length,C="";-1<--D;)C+=u[~~(Math.random()*F)];return C},C="ABCDEFGHIJKLMNOPQRSTUVWXYZ",E=C.toLowerCase(),A={upperCase:new o(C),lowerCase:new o(E),upperAndLowerCase:new o(C+E)},e={version:"3.14.2",name:"scrambleText",register:function register(D){u=D,p()},init:function init(D,u,F){if(B||p(),this.prop="innerHTML"in D?"innerHTML":"textContent"in D?"textContent":0,this.prop){this.target=D,"object"!=typeof u&&(u={text:u});var C,E,e,t,i=u.text||u.value||"",n=!1!==u.trim,r=this;return r.delimiter=C=u.delimiter||"",r.original=emojiSafeSplit(getText(D).replace(l," ").split(" ").join(""),C,n),"{original}"!==i&&!0!==i&&null!=i||(i=r.original.join(C)),r.text=emojiSafeSplit((i||"").replace(l," "),C,n),r.hasClass=!(!u.newClass&&!u.oldClass),r.newClass=u.newClass,r.oldClass=u.oldClass,t=""===C,r.textHasEmoji=t&&!!r.text.emoji,r.charsHaveEmoji=!!u.chars&&!!emojiSafeSplit(u.chars).emoji,r.length=t?r.original.length:r.original.join(C).length,r.lengthDif=(t?r.text.length:r.text.join(C).length)-r.length,r.fillChar=u.fillChar||u.chars&&~u.chars.indexOf(" ")?" ":"",r.charSet=e=A[u.chars||"upperCase"]||new o(u.chars),r.speed=.05/(u.speed||1),r.prevScrambleTime=0,r.setIndex=20*Math.random()|0,(E=r.length+Math.max(r.lengthDif,0))>e.length&&e.grow(E),r.chars=e.sets[r.setIndex],r.revealDelay=u.revealDelay||0,r.tweenLength=!1!==u.tweenLength,r.tween=F,r.rightToLeft=!!u.rightToLeft,r._props.push("scrambleText","text"),1}},render:function render(D,u){var F,C,E,e,t,i,n,r,s,a,o,B=u.target,l=u.prop,A=u.text,h=u.delimiter,p=u.tween,f=u.prevScrambleTime,g=u.revealDelay,c=u.setIndex,d=u.chars,m=u.charSet,x=u.length,S=u.textHasEmoji,j=u.charsHaveEmoji,w=u.lengthDif,b=u.tweenLength,v=u.oldClass,T=u.newClass,_=u.rightToLeft,y=u.fillChar,L=u.speed,M=u.original,H=u.hasClass,I=A.length,P=p._time,O=P-f;g&&(p._from&&(P=p._dur-P),D=0===P?0:P":"")+E+(t?"":"")+((i=(s=_?T:v)&&F!==I)?"":"")+h+e+(i?"":""):E+h+e,B[l]=" "===y&&~n.indexOf(" ")?n.split(" ").join("  "):n}};e.emojiSafeSplit=emojiSafeSplit,e.getText=getText,i()&&u.registerPlugin(e),D.ScrambleTextPlugin=e,D.default=e;if (typeof(window)==="undefined"||window!==D){Object.defineProperty(D,"__esModule",{value:!0})} else {delete D.default}}); + diff --git a/website/vendor/ScrollToPlugin.min.js b/website/vendor/ScrollToPlugin.min.js new file mode 100644 index 00000000..cfe02f30 --- /dev/null +++ b/website/vendor/ScrollToPlugin.min.js @@ -0,0 +1,11 @@ +/*! + * ScrollToPlugin 3.14.2 + * https://gsap.com + * + * @license Copyright 2025, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function l(){return"undefined"!=typeof window}function m(){return f||l()&&(f=window.gsap)&&f.registerPlugin&&f}function n(e){return"string"==typeof e}function o(e){return"function"==typeof e}function p(e,t){var o="x"===t?"Width":"Height",n="scroll"+o,r="client"+o;return e===T||e===i||e===c?Math.max(i[n],c[n])-(T["inner"+o]||i[r]||c[r]):e[n]-e["offset"+o]}function q(e,t){var o="scroll"+("x"===t?"Left":"Top");return e===T&&(null!=e.pageXOffset?o="page"+t.toUpperCase()+"Offset":e=null!=i[o]?i:c),function(){return e[o]}}function s(e,t){if(!(e=y(e)[0])||!e.getBoundingClientRect)return console.warn("scrollTo target doesn't exist. Using 0")||{x:0,y:0};var o=e.getBoundingClientRect(),n=!t||t===T||t===c,r=n?{top:i.clientTop-(T.pageYOffset||i.scrollTop||c.scrollTop||0),left:i.clientLeft-(T.pageXOffset||i.scrollLeft||c.scrollLeft||0)}:t.getBoundingClientRect(),l={x:o.left-r.left,y:o.top-r.top};return!n&&t&&(l.x+=q(t,"x")(),l.y+=q(t,"y")()),l}function t(e,t,o,r,l){return isNaN(e)||"object"==typeof e?n(e)&&"="===e.charAt(1)?parseFloat(e.substr(2))*("-"===e.charAt(0)?-1:1)+r-l:"max"===e?p(t,o)-l:Math.min(p(t,o),s(e,t)[o]-l):parseFloat(e)-l}function u(){f=m(),l()&&f&&"undefined"!=typeof document&&document.body&&(T=window,c=document.body,i=document.documentElement,y=f.utils.toArray,f.config({autoKillThreshold:7}),h=f.config(),a=1)}var f,a,T,i,c,y,h,v,r={version:"3.14.2",name:"scrollTo",rawVars:1,register:function register(e){f=e,u()},init:function init(e,r,l,i,s){a||u();var p=this,c=f.getProperty(e,"scrollSnapType");p.isWin=e===T,p.target=e,p.tween=l,r=function _clean(e,t,r,l){if(o(e)&&(e=e(t,r,l)),"object"!=typeof e)return n(e)&&"max"!==e&&"="!==e.charAt(1)?{x:e,y:e}:{y:e};if(e.nodeType)return{y:e,x:e};var i,s={};for(i in e)s[i]="onAutoKill"!==i&&o(e[i])?e[i](t,r,l):e[i];return s}(r,i,e,s),p.vars=r,p.autoKill=!!("autoKill"in r?r:h).autoKill,p.getX=q(e,"x"),p.getY=q(e,"y"),p.x=p.xPrev=p.getX(),p.y=p.yPrev=p.getY(),v=v||f.core.globals().ScrollTrigger,"smooth"===f.getProperty(e,"scrollBehavior")&&f.set(e,{scrollBehavior:"auto"}),c&&"none"!==c&&(p.snap=1,p.snapInline=e.style.scrollSnapType,e.style.scrollSnapType="none"),null!=r.x?(p.add(p,"x",p.x,t(r.x,e,"x",p.x,r.offsetX||0),i,s),p._props.push("scrollTo_x")):p.skipX=1,null!=r.y?(p.add(p,"y",p.y,t(r.y,e,"y",p.y,r.offsetY||0),i,s),p._props.push("scrollTo_y")):p.skipY=1},render:function render(e,t){for(var o,n,r,l,i,s=t._pt,c=t.target,u=t.tween,f=t.autoKill,a=t.xPrev,y=t.yPrev,d=t.isWin,g=t.snap,x=t.snapInline;s;)s.r(e,s.d),s=s._next;o=d||!t.skipX?t.getX():a,r=(n=d||!t.skipY?t.getY():y)-y,l=o-a,i=h.autoKillThreshold,t.x<0&&(t.x=0),t.y<0&&(t.y=0),f&&(!t.skipX&&(i=Math.abs(r)?t:r}function P(){(Ae=Se.core.globals().ScrollTrigger)&&Ae.core&&function _integrate(){var e=Ae.core,r=e.bridge||{},t=e._scrollers,n=e._proxies;t.push.apply(t,ze),n.push.apply(n,Le),ze=t,Le=n,o=function _bridge(e,t){return r[e](t)}}()}function Q(e){return Se=e||r(),!Te&&Se&&"undefined"!=typeof document&&document.body&&(Ce=window,Me=(ke=document).documentElement,Ee=ke.body,t=[Ce,ke,Me,Ee],Se.utils.clamp,Ie=Se.core.context||function(){},Oe="onpointerenter"in Ee?"pointer":"mouse",Pe=k.isTouch=Ce.matchMedia&&Ce.matchMedia("(hover: none), (pointer: coarse)").matches?1:"ontouchstart"in Ce||0=i,n=Math.abs(t)>=i;T&&(r||n)&&T(se,e,t,me,ye),r&&(m&&0Math.abs(t)?"x":"y",oe=!0),"y"!==ae&&(me[2]+=e,se._vx.update(e,!0)),"x"!==ae&&(ye[2]+=t,se._vy.update(t,!0)),n?ee=ee||requestAnimationFrame(lf):lf()}function of(e){if(!jf(e,1)){var t=(e=N(e,s)).clientX,r=e.clientY,n=t-se.x,i=r-se.y,o=se.isDragging;se.x=t,se.y=r,(o||(n||i)&&(Math.abs(se.startX-t)>=a||Math.abs(se.startY-r)>=a))&&(re=re||(o?2:1),o||(se.isDragging=!0),nf(n,i))}}function rf(e){return e.touches&&1=e)return a[n];return a[n-1]}for(n=a.length,e+=r;n--;)if(a[n]<=e)return a[n];return a[0]}:function(e,t,r){void 0===r&&(r=.001);var n=o(e);return!t||Math.abs(n-e)r&&(n*=t/100),e=e.substr(0,r-1)),e=n+(e in q?q[e]*t:~e.indexOf("%")?parseFloat(e)*t/100:parseFloat(e)||0)}return e}function Eb(e,t,r,n,i,o,a,s){var l=i.startColor,c=i.endColor,u=i.fontSize,f=i.indent,d=i.fontWeight,p=Ue.createElement("div"),g=Ma(r)||"fixed"===z(r,"pinType"),h=-1!==e.indexOf("scroller"),v=g?We:r,b=-1!==e.indexOf("start"),m=b?l:c,y="border-color:"+m+";font-size:"+u+";color:"+m+";font-weight:"+d+";pointer-events:none;white-space:nowrap;font-family:sans-serif,Arial;z-index:1000;padding:4px 8px;border-width:0;border-style:solid;";return y+="position:"+((h||s)&&g?"fixed;":"absolute;"),!h&&!s&&g||(y+=(n===Xe?I:Y)+":"+(o+parseFloat(f))+"px;"),a&&(y+="box-sizing:border-box;text-align:left;width:"+a.offsetWidth+"px;"),p._isStart=b,p.setAttribute("class","gsap-marker-"+e+(t?" marker-"+t:"")),p.style.cssText=y,p.innerText=t||0===t?e+"-"+t:e,v.children[0]?v.insertBefore(p,v.children[0]):v.appendChild(p),p._offset=p["offset"+n.op.d2],U(p,0,n,b),p}function Jb(){return 34We.clientWidth)||(ze.cache++,v?R=R||requestAnimationFrame($):$(),st||V("scrollStart"),st=at())}function Lb(){y=Fe.innerWidth,m=Fe.innerHeight}function Mb(e){ze.cache++,!0!==e&&(Ke||h||Ue.fullscreenElement||Ue.webkitFullscreenElement||b&&y===Fe.innerWidth&&!(Math.abs(Fe.innerHeight-m)>.25*Fe.innerHeight))||c.restart(!0)}function Pb(){return yb(ne,"scrollEnd",Pb)||Mt(!0)}function Sb(e){for(var t=0;tt,n=e._startClamp&&e.start>=t;(r||n)&&e.setPositions(n?t-1:e.start,r?Math.max(n?t:e.start+1,t):e.end,!0)}),_b(!1),et=0,r.forEach(function(e){return e&&e.render&&e.render(-1)}),ze.forEach(function(e){Ua(e)&&(e.smooth&&requestAnimationFrame(function(){return e.target.style.scrollBehavior="smooth"}),e.rec&&e(e.rec))}),Vb(_,1),c.pause(),kt++,$(rt=2),Tt.forEach(function(e){return Ua(e.vars.onRefresh)&&e.vars.onRefresh(e)}),rt=ne.isRefreshing=!1,V("refresh")}else xb(ne,"scrollEnd",Pb)},j=0,Et=1,$=function _updateAll(e){if(2===e||!rt&&!T){ne.isUpdating=!0,it&&it.update(0);var t=Tt.length,r=at(),n=50<=r-D,i=t&&Tt[0].scroll();if(Et=i=Ra(be,he)){if(oe&&Ae()&&!de)for(o=oe.parentNode;o&&o!==We;)o._pinOffset&&(I-=o._pinOffset,B-=o._pinOffset),o=o.parentNode}else i=nb(ae),s=he===Xe,a=Ae(),j=parseFloat(K(he.a))+w,!y&&1=B})},Te.update=function(e,t,r){if(!de||r||e){var n,i,o,a,s,l,c,u=!0===rt?re:Te.scroll(),f=e?0:(u-I)/U,d=f<0?0:1u+(u-D)/(at()-Ge)*E&&(d=.9999)),d!==p&&Te.enabled){if(a=(s=(n=Te.isActive=!!d&&d<1)!=(!!p&&p<1))||!!d!=!!p,Te.direction=p=Ra(be,he),fe)if(e||!n&&!l)qc(ae,G);else{var g=_t(ae,!0),h=u-I;qc(ae,We,g.top+(he===Xe?h:0)+xt,g.left+(he===Xe?0:h)+xt)}Pt(n||l?W:V),Z&&d<1&&n||b(j+(1!==d||l?0:$))}}else b(Ja(j+$*d));!ue||A.tween||Ke||ot||te.restart(!0),C&&(s||ce&&d&&(d<1||!tt))&&Ve(C.targets).forEach(function(e){return e.classList[n||ce?"add":"remove"](C.className)}),!T||ve||e||T(Te),a&&!Ke?(ve&&(c&&("complete"===o?O.pause().totalProgress(1):"reset"===o?O.restart(!0).pause():"restart"===o?O.restart(!0):O[o]()),T&&T(Te)),!s&&tt||(k&&s&&Ya(Te,k),xe[i]&&Ya(Te,xe[i]),ce&&(1===d?Te.kill(!1,1):xe[i]=0),s||xe[i=1===d?1:3]&&Ya(Te,xe[i])),pe&&!n&&Math.abs(Te.getVelocity())>(Va(pe)?pe:2500)&&(Xa(Te.callbackAnimation),ee?ee.progress(1):Xa(O,"reverse"===o?1:!d,1))):ve&&T&&!Ke&&T(Te)}if(x){var v=de?u/de.duration()*(de._caScrollDist||0):u;y(v+(X._isFlipped?1:0)),x(v)}S&&S(-u/de.duration()*(de._caScrollDist||0))}},Te.enable=function(e,t){Te.enabled||(Te.enabled=!0,xb(be,"resize",Mb),me||xb(be,"scroll",Kb),Ce&&xb(ScrollTrigger,"refreshInit",Ce),!1!==e&&(Te.progress=Oe=0,R=D=Ee=Ae()),!1!==t&&Te.refresh())},Te.getTween=function(e){return e&&A?A.tween:ee},Te.setPositions=function(e,t,r,n){if(de){var i=de.scrollTrigger,o=de.duration(),a=i.end-i.start;e=i.start+a*e/o,t=i.start+a*t/o}Te.refresh(!1,!1,{start:Ea(e,r&&!!Te._startClamp),end:Ea(t,r&&!!Te._endClamp)},n),Te.update()},Te.adjustPinSpacing=function(e){if(Q&&e){var t=Q.indexOf(he.d)+1;Q[t]=parseFloat(Q[t])+e+xt,Q[1]=parseFloat(Q[1])+e+xt,Pt(Q)}},Te.disable=function(e,t){if(!1!==e&&Te.revert(!0,!0),Te.enabled&&(Te.enabled=Te.isActive=!1,t||ee&&ee.pause(),re=0,n&&(n.uncache=1),Ce&&yb(ScrollTrigger,"refreshInit",Ce),te&&(te.pause(),A.tween&&A.tween.kill()&&(A.tween=0)),!me)){for(var r=Tt.length;r--;)if(Tt[r].scroller===be&&Tt[r]!==Te)return;yb(be,"resize",Mb),me||yb(be,"scroll",Kb)}},Te.kill=function(e,t){Te.disable(e,t),ee&&!t&&ee.kill(),a&&delete Ct[a];var r=Tt.indexOf(Te);0<=r&&Tt.splice(r,1),r===$e&&0o&&(b()>o?a.progress(1)&&b(o):a.resetTo("scrollY",o))}Wa(e)||(e={}),e.preventDefault=e.isNormalizer=e.allowClicks=!0,e.type||(e.type="wheel,touch"),e.debounce=!!e.debounce,e.id=e.id||"normalizer";var n,o,l,i,a,c,u,s,f=e.normalizeScrollX,t=e.momentum,r=e.allowNestedScroll,d=e.onRelease,p=J(e.target)||He,g=qe.core.globals().ScrollSmoother,h=g&&g.get(),v=E&&(e.content&&J(e.content)||h&&!1!==e.content&&!h.smooth()&&h.content()),b=L(p,Xe),m=L(p,Ne),y=1,x=(k.isTouch&&Fe.visualViewport?Fe.visualViewport.scale*Fe.visualViewport.width:Fe.outerWidth)/Fe.innerWidth,_=0,w=Ua(t)?function(){return t(n)}:function(){return t||2.8},S=zc(p,e.type,!0,r),T=Ia,C=Ia;return v&&qe.set(v,{y:"+=0"}),e.ignoreCheck=function(e){return E&&"touchmove"===e.type&&function ignoreDrag(){if(i){requestAnimationFrame(Hq);var e=Ja(n.deltaY/2),t=C(b.v-e);if(v&&t!==b.v+b.offset){b.offset=t-b.v;var r=Ja((parseFloat(v&&v._gsap.y)||0)-b.offset);v.style.transform="matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, "+r+", 0, 1)",v._gsap.y=r+"px",b.cacheID=ze.cache,$()}return!0}b.offset&&Lq(),i=!0}()||1.05=o||o-1<=r)&&qe.to({},{onUpdate:Rq,duration:i})}else s.restart(!0);d&&d(e)},e.onWheel=function(){a._ts&&a.pause(),1e3K||Y.register(window.gsap),$=typeof Intl!="undefined"&&"Segmenter"in Intl?new Intl.Segmenter:0,q=e=>typeof e=="string"?q(document.querySelectorAll(e)):"length"in e?Array.from(e).reduce((t,i)=>(typeof i=="string"?t.push(...q(i)):t.push(i),t),[]):[e],ee=e=>q(e).filter(t=>t instanceof HTMLElement),Q=[],U=function(){},de={add:e=>e()},pe=/\s+/g,te=new RegExp("\\p{RI}\\p{RI}|\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?(\\u{200D}\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?)*|.","gu"),J={left:0,top:0,width:0,height:0},ue=(e,t)=>{for(;++t{e.innerHTML=t,i?e.setAttribute("aria-label",i):e.removeAttribute("aria-label"),n?e.setAttribute("aria-hidden",n):e.removeAttribute("aria-hidden")},ne=(e,t)=>{if(t){let i=new Set(e.join("").match(t)||Q),n=e.length,a,c,l,o;if(i.size)for(;--n>-1;){c=e[n];for(l of i)if(l.startsWith(c)&&l.length>c.length){for(a=0,o=c;l.startsWith(o+=e[n+ ++a])&&o.lengthwindow.getComputedStyle(e).display==="inline"&&(e.style.display="inline-block"),M=(e,t,i)=>t.insertBefore(typeof e=="string"?document.createTextNode(e):e,i),X=(e,t,i)=>{let n=t[e+"sClass"]||"",{tag:a="div",aria:c="auto",propIndex:l=!1}=t,o=e==="line"?"block":"inline-block",d=n.indexOf("++")>-1,R=b=>{let m=document.createElement(a),E=i.length+1;return n&&(m.className=n+(d?" "+n+E:"")),l&&m.style.setProperty("--"+e,E+""),c!=="none"&&m.setAttribute("aria-hidden","true"),a!=="span"&&(m.style.position="relative",m.style.display=o),m.textContent=b,i.push(m),m};return d&&(n=n.replace("++","")),R.collection=i,R},fe=(e,t,i,n)=>{let a=X("line",i,n),c=window.getComputedStyle(e).textAlign||"left";return(l,o)=>{let d=a("");for(d.style.textAlign=c,e.insertBefore(d,t[l]);l{var b;let m=Array.from(e.childNodes),E=0,{wordDelimiter:_,reduceWhiteSpace:B=!0,prepareText:V}=t,G=e.getBoundingClientRect(),O=G,D=!B&&window.getComputedStyle(e).whiteSpace.substring(0,3)==="pre",S=0,x=i.collection,r,f,H,s,g,y,z,h,p,k,C,T,N,L,w,u,F,v;for(typeof _=="object"?(H=_.delimiter||_,f=_.replaceWith||""):f=_===""?"":_||" ",r=f!==" ";E-1?(y=x[x.length-1],y.appendChild(document.createTextNode(n?"":u))):(y=i(n?"":u),M(y,e,s),S&&p===1&&!z&&y.insertBefore(S,y.firstChild)),n)for(C=$?ne([...$.segment(u)].map(j=>j.segment),d):u.match(o)||Q,v=0;vO.top&&k.left<=O.left){for(T=e.cloneNode(),N=e.childNodes[0];N&&N!==y;)L=N,N=N.nextSibling,T.appendChild(L);e.parentNode.insertBefore(T,e),a&&le(T)}O=k}(p=g.length?" ":r&&u.slice(-1)===" "?" "+f:f,e,s)}e.removeChild(s),S=0}else s.nodeType===1&&(l&&l.indexOf(s)>-1?(x.indexOf(s.previousSibling)>-1&&x[x.length-1].appendChild(s),S=s):(se(s,t,i,n,a,c,l,o,d,!0),S=0),a&&le(s))};const re=class ae{constructor(t,i){this.isSplit=!1,he(),this.elements=ee(t),this.chars=[],this.words=[],this.lines=[],this.masks=[],this.vars=i,this.elements.forEach(l=>{var o;i.overwrite!==!1&&((o=l[Z])==null||o._data.orig.filter(({element:d})=>d===l).forEach(ie)),l[Z]=this}),this._split=()=>this.isSplit&&this.split(this.vars);let n=[],a,c=()=>{let l=n.length,o;for(;l--;){o=n[l];let d=o.element.offsetWidth;if(d!==o.width){o.width=d,this._split();return}}};this._data={orig:n,obs:typeof ResizeObserver!="undefined"&&new ResizeObserver(()=>{clearTimeout(a),a=setTimeout(c,200)})},U(this),this.split(i)}split(t){return(this._ctx||de).add(()=>{this.isSplit&&this.revert(),this.vars=t=t||this.vars||{};let{type:i="chars,words,lines",aria:n="auto",deepSlice:a=!0,smartWrap:c,onSplit:l,autoSplit:o=!1,specialChars:d,mask:R}=this.vars,b=i.indexOf("lines")>-1,m=i.indexOf("chars")>-1,E=i.indexOf("words")>-1,_=m&&!E&&!b,B=d&&("push"in d?new RegExp("(?:"+d.join("|")+")","gu"):d),V=B?new RegExp(B.source+"|"+te.source,"gu"):te,G=!!t.ignore&&ee(t.ignore),{orig:O,animTime:D,obs:S}=this._data,x;(m||E||b)&&(this.elements.forEach((r,f)=>{O[f]={element:r,html:r.innerHTML,ariaL:r.getAttribute("aria-label"),ariaH:r.getAttribute("aria-hidden")},n==="auto"?r.setAttribute("aria-label",(r.textContent||"").trim()):n==="hidden"&&r.setAttribute("aria-hidden","true");let H=[],s=[],g=[],y=m?X("char",t,H):null,z=X("word",t,s),h,p,k,C;if(se(r,t,z,y,_,a&&(b||_),G,V,B,!1),b){let T=q(r.childNodes),N=fe(r,T,t,g),L,w=[],u=0,F=T.map(P=>P.nodeType===1?P.getBoundingClientRect():J),v=J,j;for(h=0;hv.top&&j.left{var oe;return(oe=P.parentNode)==null?void 0:oe.removeChild(P)})}if(!E){for(h=0;h{let f=r.cloneNode();return r.replaceWith(f),f.appendChild(r),r.className&&(f.className=r.className.trim()+"-mask"),f.style.overflow="clip",f}))),this.isSplit=!0,I&&b&&(o?I.addEventListener("loadingdone",this._split):I.status==="loading"&&console.warn("SplitText called before fonts loaded")),(x=l&&l(this))&&x.totalTime&&(this._data.anim=D?x.totalTime(D):x),b&&o&&this.elements.forEach((r,f)=>{O[f].width=r.offsetWidth,S&&S.observe(r)})}),this}kill(){let{obs:t}=this._data;t&&t.disconnect(),I==null||I.removeEventListener("loadingdone",this._split)}revert(){var t,i;if(this.isSplit){let{orig:n,anim:a}=this._data;this.kill(),n.forEach(ie),this.chars.length=this.words.length=this.lines.length=n.length=this.masks.length=0,this.isSplit=!1,a&&(this._data.animTime=a.totalTime(),a.revert()),(i=(t=this.vars).onRevert)==null||i.call(t,this)}return this}static create(t,i){return new ae(t,i)}static register(t){A=A||t||window.gsap,A&&(q=A.utils.toArray,U=A.core.context||U),!K&&window.innerWidth>0&&(I=document.fonts,K=!0)}};re.version="3.14.2";let Y=re;W.SplitText=Y,W.default=Y;if (typeof(window)==="undefined"||window!==W){Object.defineProperty(W,"__esModule",{value:!0})} else {delete W.default}}); diff --git a/website/vendor/gsap.min.js b/website/vendor/gsap.min.js new file mode 100644 index 00000000..fde57af0 --- /dev/null +++ b/website/vendor/gsap.min.js @@ -0,0 +1,11 @@ +/*! + * GSAP 3.14.2 + * https://gsap.com + * + * @license Copyright 2025, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function R(t){return(i=bt(t,ht))&&Fe}function S(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function T(t,e){return!e&&console.warn(t)}function U(t,e){return t&&(ht[t]=e)&&i&&(i[t]=e)||ht}function V(){return 0}function ga(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=yt.length;r--&&!yt[r].targetTest(i););e=yt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Xt(t[r],e)))||t.splice(r,1);return t}function ha(t){return t._gsap||ga(Pt(t))[0]._gsap}function ia(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ja(t,e){return(t=t.split(",")).forEach(e)||t}function ka(t){return Math.round(1e5*t)/1e5||0}function la(t){return Math.round(1e7*t)/1e7||0}function ma(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function na(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++ia;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function Ba(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function Ca(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Da(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Fa(t,e,r,i){return t._startAt&&(I?t._startAt.revert(ft):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ha(t){return t._repeat?wt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ja(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ka(t){return t._end=la(t._start+(t._tDur/Math.abs(t._ts||t._rts||q)||0))}function La(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=la(r._time-(0q)&&e.render(r,!0)),Da(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur(n=Math.abs(n))&&(a=i,o=n);return a}function wb(t){return Ca(t),t.scrollTrigger&&t.scrollTrigger.kill(!!I),t.progress()<1&&Dt(t,"onInterrupt"),t}function zb(t){if(t)if(t=!t.name&&t.default||t,x()||t.headless){var e=t.name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:V,render:ve,add:Jt,kill:Te,modifier:ye,rawVars:0},a={targetTest:0,get:0,getSetter:le,aliases:{},register:0};if(Lt(),t!==i){if(mt[e])return;ta(i,ta(xa(t,n),a)),bt(i.prototype,bt(n,xa(t,a))),mt[i.prop=e]=i,t.targetTest&&(yt.push(i),ct[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}U(e,i),t.register&&t.register(Fe,i,we)}else St.push(t)}function Cb(t,e,r){return(6*(t+=t<0?1:1>16,e>>8&zt,e&zt]:0:Et.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),Et[e])p=Et[e];else if("#"===e.charAt(0)){if(e.length<6&&(e="#"+(n=e.charAt(1))+n+(a=e.charAt(2))+a+(s=e.charAt(3))+s+(5===e.length?e.charAt(4)+e.charAt(4):"")),9===e.length)return[(p=parseInt(e.substr(1,6),16))>>16,p>>8&zt,p&zt,parseInt(e.substr(7),16)/255];p=[(e=parseInt(e.substr(1),16))>>16,e>>8&zt,e&zt]}else if("hsl"===e.substr(0,3))if(p=c=e.match(rt),r){if(~e.indexOf("="))return p=e.match(it),i&&p.length<4&&(p[3]=1),p}else o=+p[0]%360/360,u=p[1]/100,n=2*(h=p[2]/100)-(a=h<=.5?h*(u+1):h+u-h*u),3=X?u.endTime(!1):t._dur;return r(e)&&(isNaN(e)||e in o)?(a=e.charAt(0),s="%"===e.substr(-1),n=e.indexOf("="),"<"===a||">"===a?(0<=n&&(e=e.replace(/=/,"")),("<"===a?u._start:u.endTime(0<=u._repeat))+(parseFloat(e.substr(1))||0)*(s?(n<0?u:i).totalDuration()/100:1)):n<0?(e in o||(o[e]=h),o[e]):(a=parseFloat(e.charAt(n-1)+e.substr(n+1)),s&&i&&(a=a/100*($(i)?i[0]:i).totalDuration()),1=r&&te)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if("isPause"===i.data&&i._start=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0=i&&(a instanceof te?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return r(t)?this.removeLabel(t):s(t)?this.killTweensOf(t):(t.parent===this&&Ba(this,t),t===this._recent&&(this._recent=this._last),Da(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=la(It.time-(0r:!r||s.isActive())&&n.push(s):(i=s.getTweensOf(a,r)).length&&n.push.apply(n,i),s=s._next;return n},e.tweenTo=function tweenTo(t,e){e=e||{};var r,i=this,n=Ot(i,t),a=e.startAt,s=e.onStart,o=e.onStartParams,u=e.immediateRender,h=te.to(i,ta({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:n,overwrite:"auto",duration:e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale())||q,onStart:function onStart(){if(i.pause(),!r){var t=e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale());h._dur!==t&&Ua(h,t,0,1).render(h._time,!0,!0),r=1}s&&s.apply(h,o||[])}},e));return u?h.render(0):h},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,ta({startAt:{time:Ot(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),ub(this,Ot(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),ub(this,Ot(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+q)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);var i,n=this._first,a=this.labels;for(t=la(t);n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return Da(this)},e.invalidate=function invalidate(t){var e=this._first;for(this._lock=0;e;)e.invalidate(t),e=e._next;return i.prototype.invalidate.call(this,t)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Da(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=X;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,Na(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=la(r/a._ts),a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Ua(a,a===L&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(L._ts&&(qa(L,Ja(t,L)),f=It.frame),It.frame>=vt){vt+=N.autoSleep||120;var e=L._first;if((!e||!e._ts)&&N.autoSleep&&It._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||It.sleep()}}},Timeline}(qt);ta(Zt.prototype,{_lock:0,_hasPause:0,_forcing:0});function dc(t,e,i,n,a,o){var u,h,l,f;if(mt[t]&&!1!==(u=new mt[t]).init(a,u.rawVars?e[t]:function _processVars(t,e,i,n,a){if(s(t)&&(t=Gt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||$(t)||K(t))return r(t)?Gt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Gt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new we(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==d))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function jc(t,r,e,i){var n,a,s=r.ease||i||"power1.inOut";if($(r))a=e[t]||(e[t]=[]),r.forEach(function(t,e){return a.push({t:e/(r.length-1)*100,v:t,e:s})});else for(n in r)a=e[n]||(e[n]=[]),"ease"===n||a.push({t:parseFloat(t),v:r[n],e:s})}var Wt,Ht,Jt=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var d,c=t[e],p="get"!==i?i:s(c)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():c,_=s(c)?l?ue:re:ee;if(r(n)&&(~n.indexOf("random(")&&(n=rb(n)),"="===n.charAt(1)&&(!(d=ma(p,n)+(_a(p)||0))&&0!==d||(n=d))),!f||p!==n||Ht)return isNaN(p*n)||""===n?(c||e in t||S(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,d,c,p,_=new we(this._pt,t,e,0,1,ge,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(c=~(i+="").indexOf("random("))&&(i=rb(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(at)||[];o=at.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(d=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:d,c:"="===l.charAt(1)?ma(d,l)-d:parseFloat(l)-d,m:h&&h<4?Math.round:0},m=at.lastIndex);return _.c=m")}),s.duration();else{for(l in u={},k)"ease"===l||"easeEach"===l||jc(l,k[l],u,k.easeEach);for(l in u)for(D=u[l].sort(function(t,e){return t.t-e.t}),o=E=0;o=t._tDur||e<0)&&t.ratio===u&&(u&&Ca(t,1),r||I||(Dt(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(t){return t&&this.vars.runBackwards||(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(t),R.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i,n){c||It.wake(),this._ts||this.play();var a,s=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||Qt(this,s),a=this._ease(s/this._dur),function _updatePropTweens(t,e,r,i,n,a,s,o){var u,h,l,f,d=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!d)for(d=t._ptCache[e]=[],l=t._ptLookup,f=t._targets.length;f--;){if((u=l[f][e])&&u.d&&u.d._pt)for(u=u.d._pt;u&&u.p!==e&&u.fp!==e;)u=u._next;if(!u)return Ht=1,t.vars[e]="+=0",Qt(t,s),Ht=0,o?T(e+" not eligible for reset"):1;d.push(u)}for(f=d.length;f--;)(u=(h=d[f])._pt||h).s=!i&&0!==i||n?u.s+(i||0)+a*u.c:i,u.c=r-u.s,h.e&&(h.e=ka(r)+_a(h.e)),h.b&&(h.b=u.s+_a(h.b))}(this,t,e,r,i,a,s,n)?this.resetTo(t,e,r,i,1):(La(this,0),this.parent||Aa(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?wb(this):this.scrollTrigger&&this.scrollTrigger.kill(!!I),this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Wt&&!0!==Wt.vars.overwrite)._first||wb(this),this.parent&&i!==this.timeline.totalDuration()&&Ua(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,d=t?Pt(t):f,c=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,d))return"all"===e&&(this._pt=0),wb(this);for(n=this._op=this._op||[],"all"!==e&&(r(e)&&(u={},ja(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?ha(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=bt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~d.indexOf(f[l]))for(u in a=c[l],"all"===e?(n[l]=e,o=a,s={}):(s=n[l]=n[l]||{},o=e),o)(h=a&&a[u])&&("kill"in h.d&&!0!==h.d.kill(u)||Ba(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&wb(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Ya(1,arguments)},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return Ya(2,arguments)},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return L.killTweensOf(t,e,r)},Tween}(qt);ta(te.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ja("staggerTo,staggerFrom,staggerFromTo",function(r){te[r]=function(){var t=new Zt,e=Ct.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function rc(t,e,r){return t.setAttribute(e,r)}function zc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var ee=function _setterPlain(t,e,r){return t[e]=r},re=function _setterFunc(t,e,r){return t[e](r)},ue=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},le=function _getSetter(t,e){return s(t[e])?re:u(t[e])&&t.setAttribute?rc:ee},ce=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},_e=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},ge=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},ve=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},ye=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},Te=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?Ba(this,i,"_pt"):i.dep||(e=1),i=r;return!e},be=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},we=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=zc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||ce,this.d=s||this,this.set=o||ee,this.pr=u||0,(this._next=t)&&(t._prev=this)}ja(Tt+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",function(t){return ct[t]=1}),ht.TweenMax=ht.TweenLite=te,ht.TimelineLite=ht.TimelineMax=Zt,L=new Zt({sortChildren:!1,defaults:j,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),N.stringFilter=Ib;function Hc(t){return(Oe[t]||Me).map(function(t){return t()})}function Ic(){var t=Date.now(),o=[];2=1;let i=n?1:this.easing(r);this.value=this.from+(this.to-this.from)*i}else this.lerp?(this.value=r(this.value,this.to,this.lerp*60,e),Math.round(this.value)===Math.round(this.to)&&(this.value=this.to,n=!0)):(this.value=this.to,n=!0);n&&this.stop(),this.onUpdate?.(this.value,n)}stop(){this.isRunning=!1}fromTo(e,t,{lerp:n,duration:r,easing:i,onStart:a,onUpdate:o}){this.from=this.value=e,this.to=t,this.lerp=n,this.duration=r,this.easing=i,this.currentTime=0,this.isRunning=!0,a?.(),this.onUpdate=o}};function o(e,t){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>{n=void 0,e.apply(this,r)},t)}}var s=class{width=0;height=0;scrollHeight=0;scrollWidth=0;debouncedResize;wrapperResizeObserver;contentResizeObserver;constructor(e,t,{autoResize:n=!0,debounce:r=250}={}){this.wrapper=e,this.content=t,n&&(this.debouncedResize=o(this.resize,r),this.wrapper instanceof Window?window.addEventListener(`resize`,this.debouncedResize):(this.wrapperResizeObserver=new ResizeObserver(this.debouncedResize),this.wrapperResizeObserver.observe(this.wrapper)),this.contentResizeObserver=new ResizeObserver(this.debouncedResize),this.contentResizeObserver.observe(this.content)),this.resize()}destroy(){this.wrapperResizeObserver?.disconnect(),this.contentResizeObserver?.disconnect(),this.wrapper===window&&this.debouncedResize&&window.removeEventListener(`resize`,this.debouncedResize)}resize=()=>{this.onWrapperResize(),this.onContentResize()};onWrapperResize=()=>{this.wrapper instanceof Window?(this.width=window.innerWidth,this.height=window.innerHeight):(this.width=this.wrapper.clientWidth,this.height=this.wrapper.clientHeight)};onContentResize=()=>{this.wrapper instanceof Window?(this.scrollHeight=this.content.scrollHeight,this.scrollWidth=this.content.scrollWidth):(this.scrollHeight=this.wrapper.scrollHeight,this.scrollWidth=this.wrapper.scrollWidth)};get limit(){return{x:this.scrollWidth-this.width,y:this.scrollHeight-this.height}}},c=class{events={};emit(e,...t){let n=this.events[e]||[];for(let e=0,r=n.length;e{this.events[e]=this.events[e]?.filter(e=>t!==e)}}off(e,t){this.events[e]=this.events[e]?.filter(e=>t!==e)}destroy(){this.events={}}};let l={passive:!1};function u(e,t){return e===1?16.666666666666668:e===2?t:1}var d=class{touchStart={x:0,y:0};lastDelta={x:0,y:0};window={width:0,height:0};emitter=new c;constructor(e,t={wheelMultiplier:1,touchMultiplier:1}){this.element=e,this.options=t,window.addEventListener(`resize`,this.onWindowResize),this.onWindowResize(),this.element.addEventListener(`wheel`,this.onWheel,l),this.element.addEventListener(`touchstart`,this.onTouchStart,l),this.element.addEventListener(`touchmove`,this.onTouchMove,l),this.element.addEventListener(`touchend`,this.onTouchEnd,l)}on(e,t){return this.emitter.on(e,t)}destroy(){this.emitter.destroy(),window.removeEventListener(`resize`,this.onWindowResize),this.element.removeEventListener(`wheel`,this.onWheel,l),this.element.removeEventListener(`touchstart`,this.onTouchStart,l),this.element.removeEventListener(`touchmove`,this.onTouchMove,l),this.element.removeEventListener(`touchend`,this.onTouchEnd,l)}onTouchStart=e=>{let{clientX:t,clientY:n}=e.targetTouches?e.targetTouches[0]:e;this.touchStart.x=t,this.touchStart.y=n,this.lastDelta={x:0,y:0},this.emitter.emit(`scroll`,{deltaX:0,deltaY:0,event:e})};onTouchMove=e=>{let{clientX:t,clientY:n}=e.targetTouches?e.targetTouches[0]:e,r=-(t-this.touchStart.x)*this.options.touchMultiplier,i=-(n-this.touchStart.y)*this.options.touchMultiplier;this.touchStart.x=t,this.touchStart.y=n,this.lastDelta={x:r,y:i},this.emitter.emit(`scroll`,{deltaX:r,deltaY:i,event:e})};onTouchEnd=e=>{this.emitter.emit(`scroll`,{deltaX:this.lastDelta.x,deltaY:this.lastDelta.y,event:e})};onWheel=e=>{let{deltaX:t,deltaY:n,deltaMode:r}=e,i=u(r,this.window.width),a=u(r,this.window.height);t*=i,n*=a,t*=this.options.wheelMultiplier,n*=this.options.wheelMultiplier,this.emitter.emit(`scroll`,{deltaX:t,deltaY:n,event:e})};onWindowResize=()=>{this.window={width:window.innerWidth,height:window.innerHeight}}};let f=e=>Math.min(1,1.001-2**(-10*e));var p=class{_isScrolling=!1;_isStopped=!1;_isLocked=!1;_preventNextNativeScrollEvent=!1;_resetVelocityTimeout=null;_rafId=null;_isDraggingSelection=!1;isTouching;isIos;time=0;userData={};lastVelocity=0;velocity=0;direction=0;options;targetScroll;animatedScroll;animate=new a;emitter=new c;dimensions;virtualScroll;constructor({wrapper:t=window,content:n=document.documentElement,eventsTarget:r=t,smoothWheel:i=!0,syncTouch:a=!1,syncTouchLerp:o=.075,touchInertiaExponent:c=1.7,duration:l,easing:u,lerp:p=.1,infinite:m=!1,orientation:h=`vertical`,gestureOrientation:g=h===`horizontal`?`both`:`vertical`,touchMultiplier:_=1,wheelMultiplier:v=1,autoResize:y=!0,prevent:b,virtualScroll:x,overscroll:S=!0,autoRaf:C=!1,anchors:w=!1,autoToggle:T=!1,allowNestedScroll:E=!1,__experimental__naiveDimensions:D=!1,naiveDimensions:O=D,stopInertiaOnNavigate:k=!1}={}){window.lenisVersion=e,window.lenis||(window.lenis={}),window.lenis.version=e,h===`horizontal`&&(window.lenis.horizontal=!0),a===!0&&(window.lenis.touch=!0),this.isIos=/(iPad|iPhone|iPod)/g.test(navigator.userAgent),(!t||t===document.documentElement)&&(t=window),typeof l==`number`&&typeof u!=`function`?u=f:typeof u==`function`&&typeof l!=`number`&&(l=1),this.options={wrapper:t,content:n,eventsTarget:r,smoothWheel:i,syncTouch:a,syncTouchLerp:o,touchInertiaExponent:c,duration:l,easing:u,lerp:p,infinite:m,gestureOrientation:g,orientation:h,touchMultiplier:_,wheelMultiplier:v,autoResize:y,prevent:b,virtualScroll:x,overscroll:S,autoRaf:C,anchors:w,autoToggle:T,allowNestedScroll:E,naiveDimensions:O,stopInertiaOnNavigate:k},this.dimensions=new s(t,n,{autoResize:y}),this.updateClassName(),this.targetScroll=this.animatedScroll=this.actualScroll,this.options.wrapper.addEventListener(`scroll`,this.onNativeScroll),this.options.wrapper.addEventListener(`scrollend`,this.onScrollEnd,{capture:!0}),(this.options.anchors||this.options.stopInertiaOnNavigate)&&this.options.wrapper.addEventListener(`click`,this.onClick),this.options.wrapper.addEventListener(`pointerdown`,this.onPointerDown),this.virtualScroll=new d(r,{touchMultiplier:_,wheelMultiplier:v}),this.virtualScroll.on(`scroll`,this.onVirtualScroll),this.options.autoToggle&&(this.checkOverflow(),this.rootElement.addEventListener(`transitionend`,this.onTransitionEnd)),this.options.autoRaf&&(this._rafId=requestAnimationFrame(this.raf))}destroy(){this.emitter.destroy(),this.options.wrapper.removeEventListener(`scroll`,this.onNativeScroll),this.options.wrapper.removeEventListener(`scrollend`,this.onScrollEnd,{capture:!0}),this.options.wrapper.removeEventListener(`pointerdown`,this.onPointerDown),(this.options.anchors||this.options.stopInertiaOnNavigate)&&this.options.wrapper.removeEventListener(`click`,this.onClick),this.virtualScroll.destroy(),this.dimensions.destroy(),this.cleanUpClassName(),this._rafId&&cancelAnimationFrame(this._rafId)}on(e,t){return this.emitter.on(e,t)}off(e,t){return this.emitter.off(e,t)}onScrollEnd=e=>{e instanceof CustomEvent||(this.isScrolling===`smooth`||this.isScrolling===!1)&&e.stopPropagation()};dispatchScrollendEvent=()=>{this.options.wrapper.dispatchEvent(new CustomEvent(`scrollend`,{bubbles:this.options.wrapper===window,detail:{lenisScrollEnd:!0}}))};get overflow(){let e=this.isHorizontal?`overflow-x`:`overflow-y`;return getComputedStyle(this.rootElement)[e]}checkOverflow(){[`hidden`,`clip`].includes(this.overflow)?this.internalStop():this.internalStart()}onTransitionEnd=e=>{e.propertyName?.includes(`overflow`)&&e.target===this.rootElement&&this.checkOverflow()};setScroll(e){this.isHorizontal?this.options.wrapper.scrollTo({left:e,behavior:`instant`}):this.options.wrapper.scrollTo({top:e,behavior:`instant`})}onClick=e=>{let t=e.composedPath().filter(e=>e instanceof HTMLAnchorElement&&e.href).map(e=>new URL(e.href)),n=new URL(window.location.href);if(this.options.anchors){let e=t.find(e=>n.host===e.host&&n.pathname===e.pathname&&e.hash);if(e){let t=typeof this.options.anchors==`object`&&this.options.anchors?this.options.anchors:void 0,n=decodeURIComponent(e.hash);this.scrollTo(n,t);return}}if(this.options.stopInertiaOnNavigate&&t.some(e=>n.host===e.host&&n.pathname!==e.pathname)){this.reset();return}};onPointerDown=e=>{e.button===1&&this.reset()};isTouchOnSelectionHandle(e){let t=window.getSelection();if(!t||t.isCollapsed||t.rangeCount===0)return!1;let n=e.targetTouches[0]??e.changedTouches[0];if(!n)return!1;let r=t.getRangeAt(0).getClientRects();if(r.length===0)return!1;let i=r[0],a=r[r.length-1],o=Math.hypot(n.clientX-i.left,n.clientY-i.top)<=40,s=Math.hypot(n.clientX-a.right,n.clientY-a.bottom)<=40;return o||s}onVirtualScroll=e=>{if(typeof this.options.virtualScroll==`function`&&this.options.virtualScroll(e)===!1)return;let{deltaX:t,deltaY:n,event:r}=e;if(this.emitter.emit(`virtual-scroll`,{deltaX:t,deltaY:n,event:r}),r.ctrlKey||r.lenisStopPropagation)return;let i=r.type.includes(`touch`),a=r.type.includes(`wheel`);if(i&&this.isIos&&(r.type===`touchstart`&&(this._isDraggingSelection=this.isTouchOnSelectionHandle(r)),this._isDraggingSelection)){r.type===`touchend`&&(this._isDraggingSelection=!1);return}this.isTouching=r.type===`touchstart`||r.type===`touchmove`;let o=t===0&&n===0;if(this.options.syncTouch&&i&&r.type===`touchstart`&&o&&!this.isStopped&&!this.isLocked){this.reset();return}let s=this.options.gestureOrientation===`vertical`&&n===0||this.options.gestureOrientation===`horizontal`&&t===0;if(o||s)return;let c=r.composedPath();c=c.slice(0,c.indexOf(this.rootElement));let l=this.options.prevent,u=Math.abs(t)>=Math.abs(n)?`horizontal`:`vertical`;if(c.find(e=>e instanceof HTMLElement&&(typeof l==`function`&&l?.(e)||e.hasAttribute?.(`data-lenis-prevent`)||u===`vertical`&&e.hasAttribute?.(`data-lenis-prevent-vertical`)||u===`horizontal`&&e.hasAttribute?.(`data-lenis-prevent-horizontal`)||i&&e.hasAttribute?.(`data-lenis-prevent-touch`)||a&&e.hasAttribute?.(`data-lenis-prevent-wheel`)||this.options.allowNestedScroll&&this.hasNestedScroll(e,{deltaX:t,deltaY:n}))))return;if(this.isStopped||this.isLocked){r.cancelable&&r.preventDefault();return}if(!(this.options.syncTouch&&i||this.options.smoothWheel&&a)){this.isScrolling=`native`,this.animate.stop(),r.lenisStopPropagation=!0;return}let d=n;this.options.gestureOrientation===`both`?d=Math.abs(n)>Math.abs(t)?n:t:this.options.gestureOrientation===`horizontal`&&(d=t),(!this.options.overscroll||this.options.infinite||this.options.wrapper!==window&&this.limit>0&&(this.animatedScroll>0&&this.animatedScroll0||this.animatedScroll===this.limit&&n<0))&&(r.lenisStopPropagation=!0),r.cancelable&&r.preventDefault();let f=i&&this.options.syncTouch,p=i&&r.type===`touchend`;p&&(d=Math.sign(d)*Math.abs(this.velocity)**this.options.touchInertiaExponent),this.scrollTo(this.targetScroll+d,{programmatic:!1,...f?{lerp:p?this.options.syncTouchLerp:1}:{lerp:this.options.lerp,duration:this.options.duration,easing:this.options.easing}})};resize(){this.dimensions.resize(),this.animatedScroll=this.targetScroll=this.actualScroll,this.emit()}emit(){this.emitter.emit(`scroll`,this)}onNativeScroll=()=>{if(this._resetVelocityTimeout!==null&&(clearTimeout(this._resetVelocityTimeout),this._resetVelocityTimeout=null),this._preventNextNativeScrollEvent){this._preventNextNativeScrollEvent=!1;return}if(this.isScrolling===!1||this.isScrolling===`native`){let e=this.animatedScroll;this.animatedScroll=this.targetScroll=this.actualScroll,this.lastVelocity=this.velocity,this.velocity=this.animatedScroll-e,this.direction=Math.sign(this.animatedScroll-e),this.isStopped||(this.isScrolling=`native`),this.emit(),this.velocity!==0&&(this._resetVelocityTimeout=setTimeout(()=>{this.lastVelocity=this.velocity,this.velocity=0,this.isScrolling=!1,this.emit()},400))}};reset(){this.isLocked=!1,this.isScrolling=!1,this.animatedScroll=this.targetScroll=this.actualScroll,this.lastVelocity=this.velocity=0,this.animate.stop()}start(){if(this.isStopped){if(this.options.autoToggle){this.rootElement.style.removeProperty(`overflow`);return}this.internalStart()}}internalStart(){this.isStopped&&(this.reset(),this.isStopped=!1,this.emit())}stop(){if(!this.isStopped){if(this.options.autoToggle){this.rootElement.style.setProperty(`overflow`,`clip`);return}this.internalStop()}}internalStop(){this.isStopped||(this.reset(),this.isStopped=!0,this.emit())}raf=e=>{let t=e-(this.time||e);this.time=e,this.animate.advance(t*.001),this.options.autoRaf&&(this._rafId=requestAnimationFrame(this.raf))};scrollTo(e,{offset:n=0,immediate:r=!1,lock:i=!1,programmatic:a=!0,lerp:o=a?this.options.lerp:void 0,duration:s=a?this.options.duration:void 0,easing:c=a?this.options.easing:void 0,onStart:l,onComplete:u,force:d=!1,userData:p}={}){if((this.isStopped||this.isLocked)&&!d)return;let m=e,h=n;if(typeof m==`string`&&[`top`,`left`,`start`,`#`].includes(m))m=0;else if(typeof m==`string`&&[`bottom`,`right`,`end`].includes(m))m=this.limit;else{let e=null;if(typeof m==`string`?(e=m.startsWith(`#`)?document.getElementById(m.slice(1)):document.querySelector(m),e||(m===`#top`?m=0:console.warn(`Lenis: Target not found`,m))):m instanceof HTMLElement&&m?.nodeType&&(e=m),e){if(this.options.wrapper!==window){let e=this.rootElement.getBoundingClientRect();h-=this.isHorizontal?e.left:e.top}let t=e.getBoundingClientRect(),n=getComputedStyle(e),r=this.isHorizontal?Number.parseFloat(n.scrollMarginLeft):Number.parseFloat(n.scrollMarginTop),i=getComputedStyle(this.rootElement),a=this.isHorizontal?Number.parseFloat(i.scrollPaddingLeft):Number.parseFloat(i.scrollPaddingTop);m=(this.isHorizontal?t.left:t.top)+this.animatedScroll-(Number.isNaN(r)?0:r)-(Number.isNaN(a)?0:a)}}if(typeof m==`number`){if(m+=h,this.options.infinite){if(a){this.targetScroll=this.animatedScroll=this.scroll;let e=m-this.animatedScroll;e>this.limit/2?m-=this.limit:e<-this.limit/2&&(m+=this.limit)}}else m=t(0,m,this.limit);if(m===this.targetScroll){l?.(this),u?.(this);return}if(this.userData=p??{},r){this.animatedScroll=this.targetScroll=m,this.setScroll(this.scroll),this.reset(),this.preventNextNativeScrollEvent(),this.emit(),u?.(this),this.userData={},requestAnimationFrame(()=>{this.dispatchScrollendEvent()});return}a||(this.targetScroll=m),typeof s==`number`&&typeof c!=`function`?c=f:typeof c==`function`&&typeof s!=`number`&&(s=1),this.animate.fromTo(this.animatedScroll,m,{duration:s,easing:c,lerp:o,onStart:()=>{i&&(this.isLocked=!0),this.isScrolling=`smooth`,l?.(this)},onUpdate:(e,t)=>{this.isScrolling=`smooth`,this.lastVelocity=this.velocity,this.velocity=e-this.animatedScroll,this.direction=Math.sign(this.velocity),this.animatedScroll=e,this.setScroll(this.scroll),a&&(this.targetScroll=e),t||this.emit(),t&&(this.reset(),this.emit(),u?.(this),this.userData={},requestAnimationFrame(()=>{this.dispatchScrollendEvent()}),this.preventNextNativeScrollEvent())}})}}preventNextNativeScrollEvent(){this._preventNextNativeScrollEvent=!0,requestAnimationFrame(()=>{this._preventNextNativeScrollEvent=!1})}hasNestedScroll(e,{deltaX:t,deltaY:n}){let r=Date.now();e._lenis||={};let i=e._lenis,a,o,s,c,l,u,d,f,p,m;if(r-(i.time??0)>2e3){i.time=Date.now();let t=window.getComputedStyle(e);if(i.computedStyle=t,a=[`auto`,`overlay`,`scroll`].includes(t.overflowX),o=[`auto`,`overlay`,`scroll`].includes(t.overflowY),l=[`auto`].includes(t.overscrollBehaviorX),u=[`auto`].includes(t.overscrollBehaviorY),i.hasOverflowX=a,i.hasOverflowY=o,!(a||o))return!1;d=e.scrollWidth,f=e.scrollHeight,p=e.clientWidth,m=e.clientHeight,s=d>p,c=f>m,i.isScrollableX=s,i.isScrollableY=c,i.scrollWidth=d,i.scrollHeight=f,i.clientWidth=p,i.clientHeight=m,i.hasOverscrollBehaviorX=l,i.hasOverscrollBehaviorY=u}else s=i.isScrollableX,c=i.isScrollableY,a=i.hasOverflowX,o=i.hasOverflowY,d=i.scrollWidth,f=i.scrollHeight,p=i.clientWidth,m=i.clientHeight,l=i.hasOverscrollBehaviorX,u=i.hasOverscrollBehaviorY;if(!(a&&s||o&&c))return!1;let h=Math.abs(t)>=Math.abs(n)?`horizontal`:`vertical`,g,_,v,y,b,x;if(h===`horizontal`)g=Math.round(e.scrollLeft),_=d-p,v=t,y=a,b=s,x=l;else if(h===`vertical`)g=Math.round(e.scrollTop),_=f-m,v=n,y=o,b=c,x=u;else return!1;return!x&&(g>=_||g<=0)?!0:(v>0?g<_:g>0)&&y&&b}get rootElement(){return this.options.wrapper===window?document.documentElement:this.options.wrapper}get limit(){return this.options.naiveDimensions?this.isHorizontal?this.rootElement.scrollWidth-this.rootElement.clientWidth:this.rootElement.scrollHeight-this.rootElement.clientHeight:this.dimensions.limit[this.isHorizontal?`x`:`y`]}get isHorizontal(){return this.options.orientation===`horizontal`}get actualScroll(){let e=this.options.wrapper;return this.isHorizontal?e.scrollX??e.scrollLeft:e.scrollY??e.scrollTop}get scroll(){return this.options.infinite?i(this.animatedScroll,this.limit):this.animatedScroll}get progress(){return this.limit===0?1:this.scroll/this.limit}get isScrolling(){return this._isScrolling}set isScrolling(e){this._isScrolling!==e&&(this._isScrolling=e,this.updateClassName())}get isStopped(){return this._isStopped}set isStopped(e){this._isStopped!==e&&(this._isStopped=e,this.updateClassName())}get isLocked(){return this._isLocked}set isLocked(e){this._isLocked!==e&&(this._isLocked=e,this.updateClassName())}get isSmooth(){return this.isScrolling===`smooth`}get className(){let e=`lenis`;return this.options.autoToggle&&(e+=` lenis-autoToggle`),this.isStopped&&(e+=` lenis-stopped`),this.isLocked&&(e+=` lenis-locked`),this.isScrolling&&(e+=` lenis-scrolling`),this.isScrolling===`smooth`&&(e+=` lenis-smooth`),e}updateClassName(){this.cleanUpClassName(),this.className.split(` `).forEach(e=>{this.rootElement.classList.add(e)})}cleanUpClassName(){for(let e of Array.from(this.rootElement.classList))(e===`lenis`||e.startsWith(`lenis-`))&&this.rootElement.classList.remove(e)}};globalThis.Lenis=p,globalThis.Lenis.prototype=p.prototype})(); +//# sourceMappingURL=lenis.min.js.map \ No newline at end of file diff --git a/website/vendor/three.core.min.js b/website/vendor/three.core.min.js new file mode 100644 index 00000000..fcf90d35 --- /dev/null +++ b/website/vendor/three.core.min.js @@ -0,0 +1,6 @@ +/** + * @license + * Copyright 2010-2026 Three.js Authors + * SPDX-License-Identifier: MIT + */ +const t="185",e={LEFT:0,MIDDLE:1,RIGHT:2,ROTATE:0,DOLLY:1,PAN:2},s={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},i=0,r=1,n=2,a=3,o=0,h=1,l=2,c=3,u=0,d=1,p=2,m=0,y=1,g=2,f=3,x=4,b=5,v=6,w=100,M=101,S=102,_=103,A=104,T=200,z=201,C=202,I=203,B=204,k=205,O=206,P=207,R=208,E=209,N=210,V=211,L=212,F=213,D=214,U=0,j=1,W=2,J=3,q=4,H=5,X=6,Y=7,Z=0,G=1,$=2,Q=0,K=1,tt=2,et=3,st=4,it=5,rt=6,nt=7,at="attached",ot="detached",ht=300,lt=301,ct=302,ut=303,dt=304,pt=306,mt=1e3,yt=1001,gt=1002,ft=1003,xt=1004,bt=1004,vt=1005,wt=1005,Mt=1006,St=1007,_t=1007,At=1008,Tt=1008,zt=1009,Ct=1010,It=1011,Bt=1012,kt=1013,Ot=1014,Pt=1015,Rt=1016,Et=1017,Nt=1018,Vt=1020,Lt=35902,Ft=35899,Dt=1021,Ut=1022,jt=1023,Wt=1026,Jt=1027,qt=1028,Ht=1029,Xt=1030,Yt=1031,Zt=1032,Gt=1033,$t=33776,Qt=33777,Kt=33778,te=33779,ee=35840,se=35841,ie=35842,re=35843,ne=36196,ae=37492,oe=37496,he=37488,le=37489,ce=37490,ue=37491,de=37808,pe=37809,me=37810,ye=37811,ge=37812,fe=37813,xe=37814,be=37815,ve=37816,we=37817,Me=37818,Se=37819,_e=37820,Ae=37821,Te=36492,ze=36494,Ce=36495,Ie=36283,Be=36284,ke=36285,Oe=36286,Pe=2200,Re=2201,Ee=2202,Ne=2300,Ve=2301,Le=2302,Fe=2303,De=2400,Ue=2401,je=2402,We=2500,Je=2501,qe=0,He=1,Xe=2,Ye=3200,Ze=3201,Ge=3202,$e=3203,Qe=0,Ke=1,ts="",es="srgb",ss="srgb-linear",is="linear",rs="srgb",ns="",as="rg",os="ga",hs=0,ls=7680,cs=7681,us=7682,ds=7683,ps=34055,ms=34056,ys=5386,gs=512,fs=513,xs=514,bs=515,vs=516,ws=517,Ms=518,Ss=519,_s=512,As=513,Ts=514,zs=515,Cs=516,Is=517,Bs=518,ks=519,Os=35044,Ps=35048,Rs=35040,Es=35045,Ns=35049,Vs=35041,Ls=35046,Fs=35050,Ds=35042,Us="100",js="300 es",Ws=2e3,Js=2001,qs={COMPUTE:"compute",RENDER:"render"},Hs={PERSPECTIVE:"perspective",LINEAR:"linear",FLAT:"flat"},Xs={NORMAL:"normal",CENTROID:"centroid",SAMPLE:"sample",FIRST:"first",EITHER:"either"},Ys={TEXTURE_COMPARE:"depthTextureCompare"};const Zs={Int8Array:Int8Array,Uint8Array:Uint8Array,Uint8ClampedArray:Uint8ClampedArray,Int16Array:Int16Array,Uint16Array:Uint16Array,Int32Array:Int32Array,Uint32Array:Uint32Array,Float32Array:Float32Array,Float64Array:Float64Array};function Gs(t,e){return new Zs[t](e)}function $s(t){return ArrayBuffer.isView(t)&&!(t instanceof DataView)}function Qs(t){return document.createElementNS("http://www.w3.org/1999/xhtml",t)}function Ks(){const t=Qs("canvas");return t.style.display="block",t}const ti={};let ei=null;function si(t){ei=t}function ii(){return ei}function ri(...t){const e="THREE."+t.shift();ei?ei("log",e,...t):console.log(e,...t)}function ni(t){const e=t[0];if("string"==typeof e&&e.startsWith("TSL:")){const e=t[1];e&&e.isStackTrace?t[0]+=" "+e.getLocation():t[1]='Stack trace not available. Enable "THREE.Node.captureStackTrace" to capture stack traces.'}return t}function ai(...t){const e="THREE."+(t=ni(t)).shift();if(ei)ei("warn",e,...t);else{const s=t[0];s&&s.isStackTrace?console.warn(s.getError(e)):console.warn(e,...t)}}function oi(...t){const e="THREE."+(t=ni(t)).shift();if(ei)ei("error",e,...t);else{const s=t[0];s&&s.isStackTrace?console.error(s.getError(e)):console.error(e,...t)}}function hi(...t){const e=t.join(" ");e in ti||(ti[e]=!0,ai(...t))}function li(){return"undefined"!=typeof self&&void 0!==self.scheduler&&void 0!==self.scheduler.yield?self.scheduler.yield():new Promise(t=>{requestAnimationFrame(t)})}function ci(t,e,s){return new Promise(function(i,r){setTimeout(function n(){switch(t.clientWaitSync(e,t.SYNC_FLUSH_COMMANDS_BIT,0)){case t.WAIT_FAILED:r();break;case t.TIMEOUT_EXPIRED:setTimeout(n,s);break;default:i()}},s)})}const ui={[U]:1,[W]:6,[q]:7,[J]:5,[j]:0,[X]:2,[Y]:4,[H]:3};class di{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const s=this._listeners;void 0===s[t]&&(s[t]=[]),-1===s[t].indexOf(e)&&s[t].push(e)}hasEventListener(t,e){const s=this._listeners;return void 0!==s&&(void 0!==s[t]&&-1!==s[t].indexOf(e))}removeEventListener(t,e){const s=this._listeners;if(void 0===s)return;const i=s[t];if(void 0!==i){const t=i.indexOf(e);-1!==t&&i.splice(t,1)}}dispatchEvent(t){const e=this._listeners;if(void 0===e)return;const s=e[t.type];if(void 0!==s){t.target=this;const e=s.slice(0);for(let s=0,i=e.length;s>8&255]+pi[t>>16&255]+pi[t>>24&255]+"-"+pi[255&e]+pi[e>>8&255]+"-"+pi[e>>16&15|64]+pi[e>>24&255]+"-"+pi[63&s|128]+pi[s>>8&255]+"-"+pi[s>>16&255]+pi[s>>24&255]+pi[255&i]+pi[i>>8&255]+pi[i>>16&255]+pi[i>>24&255]).toLowerCase()}function xi(t,e,s){return Math.max(e,Math.min(s,t))}function bi(t,e){return(t%e+e)%e}function vi(t,e,s){return(1-s)*t+s*e}function wi(t,e){switch(e.constructor){case Float32Array:return t;case Uint32Array:return t/4294967295;case Uint16Array:return t/65535;case Uint8Array:return t/255;case Int32Array:return Math.max(t/2147483647,-1);case Int16Array:return Math.max(t/32767,-1);case Int8Array:return Math.max(t/127,-1);default:throw new Error("THREE.MathUtils: Invalid component type.")}}function Mi(t,e){switch(e.constructor){case Float32Array:return t;case Uint32Array:return Math.round(4294967295*t);case Uint16Array:return Math.round(65535*t);case Uint8Array:return Math.round(255*t);case Int32Array:return Math.round(2147483647*t);case Int16Array:return Math.round(32767*t);case Int8Array:return Math.round(127*t);default:throw new Error("THREE.MathUtils: Invalid component type.")}}const Si={DEG2RAD:yi,RAD2DEG:gi,generateUUID:fi,clamp:xi,euclideanModulo:bi,mapLinear:function(t,e,s,i,r){return i+(t-e)*(r-i)/(s-e)},inverseLerp:function(t,e,s){return t!==e?(s-t)/(e-t):0},lerp:vi,damp:function(t,e,s,i){return vi(t,e,1-Math.exp(-s*i))},pingpong:function(t,e=1){return e-Math.abs(bi(t,2*e)-e)},smoothstep:function(t,e,s){return t<=e?0:t>=s?1:(t=(t-e)/(s-e))*t*(3-2*t)},smootherstep:function(t,e,s){return t<=e?0:t>=s?1:(t=(t-e)/(s-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatSpread:function(t){return t*(.5-Math.random())},seededRandom:function(t){void 0!==t&&(mi=t);let e=mi+=1831565813;return e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296},degToRad:function(t){return t*yi},radToDeg:function(t){return t*gi},isPowerOfTwo:function(t){return!(t&t-1)&&0!==t},ceilPowerOfTwo:function(t){return Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))},floorPowerOfTwo:function(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))},setQuaternionFromProperEuler:function(t,e,s,i,r){const n=Math.cos,a=Math.sin,o=n(s/2),h=a(s/2),l=n((e+i)/2),c=a((e+i)/2),u=n((e-i)/2),d=a((e-i)/2),p=n((i-e)/2),m=a((i-e)/2);switch(r){case"XYX":t.set(o*c,h*u,h*d,o*l);break;case"YZY":t.set(h*d,o*c,h*u,o*l);break;case"ZXZ":t.set(h*u,h*d,o*c,o*l);break;case"XZX":t.set(o*c,h*m,h*p,o*l);break;case"YXY":t.set(h*p,o*c,h*m,o*l);break;case"ZYZ":t.set(h*m,h*p,o*c,o*l);break;default:ai("MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+r)}},normalize:Mi,denormalize:wi};class _i{static{_i.prototype.isVector2=!0}constructor(t=0,e=0){this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("THREE.Vector2: index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("THREE.Vector2: index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t){return this.x+=t.x,this.y+=t.y,this}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=this.x,s=this.y,i=t.elements;return this.x=i[0]*e+i[3]*s+i[6],this.y=i[1]*e+i[4]*s+i[7],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this}clamp(t,e){return this.x=xi(this.x,t.x,e.x),this.y=xi(this.y,t.y,e.y),this}clampScalar(t,e){return this.x=xi(this.x,t,e),this.y=xi(this.y,t,e),this}clampLength(t,e){const s=this.length();return this.divideScalar(s||1).multiplyScalar(xi(s,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(0===e)return Math.PI/2;const s=this.dot(t)/e;return Math.acos(xi(s,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,s=this.y-t.y;return e*e+s*s}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this}lerpVectors(t,e,s){return this.x=t.x+(e.x-t.x)*s,this.y=t.y+(e.y-t.y)*s,this}equals(t){return t.x===this.x&&t.y===this.y}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this}rotateAround(t,e){const s=Math.cos(e),i=Math.sin(e),r=this.x-t.x,n=this.y-t.y;return this.x=r*s-n*i+t.x,this.y=r*i+n*s+t.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}}class Ai{constructor(t=0,e=0,s=0,i=1){this.isQuaternion=!0,this._x=t,this._y=e,this._z=s,this._w=i}static slerpFlat(t,e,s,i,r,n,a){let o=s[i+0],h=s[i+1],l=s[i+2],c=s[i+3],u=r[n+0],d=r[n+1],p=r[n+2],m=r[n+3];if(c!==m||o!==u||h!==d||l!==p){let t=o*u+h*d+l*p+c*m;t<0&&(u=-u,d=-d,p=-p,m=-m,t=-t);let e=1-a;if(t<.9995){const s=Math.acos(t),i=Math.sin(s);e=Math.sin(e*s)/i,o=o*e+u*(a=Math.sin(a*s)/i),h=h*e+d*a,l=l*e+p*a,c=c*e+m*a}else{o=o*e+u*a,h=h*e+d*a,l=l*e+p*a,c=c*e+m*a;const t=1/Math.sqrt(o*o+h*h+l*l+c*c);o*=t,h*=t,l*=t,c*=t}}t[e]=o,t[e+1]=h,t[e+2]=l,t[e+3]=c}static multiplyQuaternionsFlat(t,e,s,i,r,n){const a=s[i],o=s[i+1],h=s[i+2],l=s[i+3],c=r[n],u=r[n+1],d=r[n+2],p=r[n+3];return t[e]=a*p+l*c+o*d-h*u,t[e+1]=o*p+l*u+h*c-a*d,t[e+2]=h*p+l*d+a*u-o*c,t[e+3]=l*p-a*c-o*u-h*d,t}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get w(){return this._w}set w(t){this._w=t,this._onChangeCallback()}set(t,e,s,i){return this._x=t,this._y=e,this._z=s,this._w=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(t){return this._x=t.x,this._y=t.y,this._z=t.z,this._w=t.w,this._onChangeCallback(),this}setFromEuler(t,e=!0){const s=t._x,i=t._y,r=t._z,n=t._order,a=Math.cos,o=Math.sin,h=a(s/2),l=a(i/2),c=a(r/2),u=o(s/2),d=o(i/2),p=o(r/2);switch(n){case"XYZ":this._x=u*l*c+h*d*p,this._y=h*d*c-u*l*p,this._z=h*l*p+u*d*c,this._w=h*l*c-u*d*p;break;case"YXZ":this._x=u*l*c+h*d*p,this._y=h*d*c-u*l*p,this._z=h*l*p-u*d*c,this._w=h*l*c+u*d*p;break;case"ZXY":this._x=u*l*c-h*d*p,this._y=h*d*c+u*l*p,this._z=h*l*p+u*d*c,this._w=h*l*c-u*d*p;break;case"ZYX":this._x=u*l*c-h*d*p,this._y=h*d*c+u*l*p,this._z=h*l*p-u*d*c,this._w=h*l*c+u*d*p;break;case"YZX":this._x=u*l*c+h*d*p,this._y=h*d*c+u*l*p,this._z=h*l*p-u*d*c,this._w=h*l*c-u*d*p;break;case"XZY":this._x=u*l*c-h*d*p,this._y=h*d*c-u*l*p,this._z=h*l*p+u*d*c,this._w=h*l*c+u*d*p;break;default:ai("Quaternion: .setFromEuler() encountered an unknown order: "+n)}return!0===e&&this._onChangeCallback(),this}setFromAxisAngle(t,e){const s=e/2,i=Math.sin(s);return this._x=t.x*i,this._y=t.y*i,this._z=t.z*i,this._w=Math.cos(s),this._onChangeCallback(),this}setFromRotationMatrix(t){const e=t.elements,s=e[0],i=e[4],r=e[8],n=e[1],a=e[5],o=e[9],h=e[2],l=e[6],c=e[10],u=s+a+c;if(u>0){const t=.5/Math.sqrt(u+1);this._w=.25/t,this._x=(l-o)*t,this._y=(r-h)*t,this._z=(n-i)*t}else if(s>a&&s>c){const t=2*Math.sqrt(1+s-a-c);this._w=(l-o)/t,this._x=.25*t,this._y=(i+n)/t,this._z=(r+h)/t}else if(a>c){const t=2*Math.sqrt(1+a-s-c);this._w=(r-h)/t,this._x=(i+n)/t,this._y=.25*t,this._z=(o+l)/t}else{const t=2*Math.sqrt(1+c-s-a);this._w=(n-i)/t,this._x=(r+h)/t,this._y=(o+l)/t,this._z=.25*t}return this._onChangeCallback(),this}setFromUnitVectors(t,e){let s=t.dot(e)+1;return s<1e-8?(s=0,Math.abs(t.x)>Math.abs(t.z)?(this._x=-t.y,this._y=t.x,this._z=0,this._w=s):(this._x=0,this._y=-t.z,this._z=t.y,this._w=s)):(this._x=t.y*e.z-t.z*e.y,this._y=t.z*e.x-t.x*e.z,this._z=t.x*e.y-t.y*e.x,this._w=s),this.normalize()}angleTo(t){return 2*Math.acos(Math.abs(xi(this.dot(t),-1,1)))}rotateTowards(t,e){const s=this.angleTo(t);if(0===s)return this;const i=Math.min(1,e/s);return this.slerp(t,i),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(t){return this._x*t._x+this._y*t._y+this._z*t._z+this._w*t._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let t=this.length();return 0===t?(this._x=0,this._y=0,this._z=0,this._w=1):(t=1/t,this._x=this._x*t,this._y=this._y*t,this._z=this._z*t,this._w=this._w*t),this._onChangeCallback(),this}multiply(t){return this.multiplyQuaternions(this,t)}premultiply(t){return this.multiplyQuaternions(t,this)}multiplyQuaternions(t,e){const s=t._x,i=t._y,r=t._z,n=t._w,a=e._x,o=e._y,h=e._z,l=e._w;return this._x=s*l+n*a+i*h-r*o,this._y=i*l+n*o+r*a-s*h,this._z=r*l+n*h+s*o-i*a,this._w=n*l-s*a-i*o-r*h,this._onChangeCallback(),this}slerp(t,e){let s=t._x,i=t._y,r=t._z,n=t._w,a=this.dot(t);a<0&&(s=-s,i=-i,r=-r,n=-n,a=-a);let o=1-e;if(a<.9995){const t=Math.acos(a),h=Math.sin(t);o=Math.sin(o*t)/h,e=Math.sin(e*t)/h,this._x=this._x*o+s*e,this._y=this._y*o+i*e,this._z=this._z*o+r*e,this._w=this._w*o+n*e,this._onChangeCallback()}else this._x=this._x*o+s*e,this._y=this._y*o+i*e,this._z=this._z*o+r*e,this._w=this._w*o+n*e,this.normalize();return this}slerpQuaternions(t,e,s){return this.copy(t).slerp(e,s)}random(){const t=2*Math.PI*Math.random(),e=2*Math.PI*Math.random(),s=Math.random(),i=Math.sqrt(1-s),r=Math.sqrt(s);return this.set(i*Math.sin(t),i*Math.cos(t),r*Math.sin(e),r*Math.cos(e))}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._w===this._w}fromArray(t,e=0){return this._x=t[e],this._y=t[e+1],this._z=t[e+2],this._w=t[e+3],this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._w,t}fromBufferAttribute(t,e){return this._x=t.getX(e),this._y=t.getY(e),this._z=t.getZ(e),this._w=t.getW(e),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}}class Ti{static{Ti.prototype.isVector3=!0}constructor(t=0,e=0,s=0){this.x=t,this.y=e,this.z=s}set(t,e,s){return void 0===s&&(s=this.z),this.x=t,this.y=e,this.z=s,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;default:throw new Error("THREE.Vector3: index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("THREE.Vector3: index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this}multiplyVectors(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this}applyEuler(t){return this.applyQuaternion(Ci.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(Ci.setFromAxisAngle(t,e))}applyMatrix3(t){const e=this.x,s=this.y,i=this.z,r=t.elements;return this.x=r[0]*e+r[3]*s+r[6]*i,this.y=r[1]*e+r[4]*s+r[7]*i,this.z=r[2]*e+r[5]*s+r[8]*i,this}applyNormalMatrix(t){return this.applyMatrix3(t).normalize()}applyMatrix4(t){const e=this.x,s=this.y,i=this.z,r=t.elements,n=1/(r[3]*e+r[7]*s+r[11]*i+r[15]);return this.x=(r[0]*e+r[4]*s+r[8]*i+r[12])*n,this.y=(r[1]*e+r[5]*s+r[9]*i+r[13])*n,this.z=(r[2]*e+r[6]*s+r[10]*i+r[14])*n,this}applyQuaternion(t){const e=this.x,s=this.y,i=this.z,r=t.x,n=t.y,a=t.z,o=t.w,h=2*(n*i-a*s),l=2*(a*e-r*i),c=2*(r*s-n*e);return this.x=e+o*h+n*c-a*l,this.y=s+o*l+a*h-r*c,this.z=i+o*c+r*l-n*h,this}project(t){return this.applyMatrix4(t.matrixWorldInverse).applyMatrix4(t.projectionMatrix)}unproject(t){return this.applyMatrix4(t.projectionMatrixInverse).applyMatrix4(t.matrixWorld)}transformDirection(t){const e=this.x,s=this.y,i=this.z,r=t.elements;return this.x=r[0]*e+r[4]*s+r[8]*i,this.y=r[1]*e+r[5]*s+r[9]*i,this.z=r[2]*e+r[6]*s+r[10]*i,this.normalize()}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this}divideScalar(t){return this.multiplyScalar(1/t)}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this}clamp(t,e){return this.x=xi(this.x,t.x,e.x),this.y=xi(this.y,t.y,e.y),this.z=xi(this.z,t.z,e.z),this}clampScalar(t,e){return this.x=xi(this.x,t,e),this.y=xi(this.y,t,e),this.z=xi(this.z,t,e),this}clampLength(t,e){const s=this.length();return this.divideScalar(s||1).multiplyScalar(xi(s,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this}lerpVectors(t,e,s){return this.x=t.x+(e.x-t.x)*s,this.y=t.y+(e.y-t.y)*s,this.z=t.z+(e.z-t.z)*s,this}cross(t){return this.crossVectors(this,t)}crossVectors(t,e){const s=t.x,i=t.y,r=t.z,n=e.x,a=e.y,o=e.z;return this.x=i*o-r*a,this.y=r*n-s*o,this.z=s*a-i*n,this}projectOnVector(t){const e=t.lengthSq();if(0===e)return this.set(0,0,0);const s=t.dot(this)/e;return this.copy(t).multiplyScalar(s)}projectOnPlane(t){return zi.copy(this).projectOnVector(t),this.sub(zi)}reflect(t){return this.sub(zi.copy(t).multiplyScalar(2*this.dot(t)))}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(0===e)return Math.PI/2;const s=this.dot(t)/e;return Math.acos(xi(s,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,s=this.y-t.y,i=this.z-t.z;return e*e+s*s+i*i}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)+Math.abs(this.z-t.z)}setFromSpherical(t){return this.setFromSphericalCoords(t.radius,t.phi,t.theta)}setFromSphericalCoords(t,e,s){const i=Math.sin(e)*t;return this.x=i*Math.sin(s),this.y=Math.cos(e)*t,this.z=i*Math.cos(s),this}setFromCylindrical(t){return this.setFromCylindricalCoords(t.radius,t.theta,t.y)}setFromCylindricalCoords(t,e,s){return this.x=t*Math.sin(e),this.y=s,this.z=t*Math.cos(e),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this}setFromMatrixScale(t){const e=this.setFromMatrixColumn(t,0).length(),s=this.setFromMatrixColumn(t,1).length(),i=this.setFromMatrixColumn(t,2).length();return this.x=e,this.y=s,this.z=i,this}setFromMatrixColumn(t,e){return this.fromArray(t.elements,4*e)}setFromMatrix3Column(t,e){return this.fromArray(t.elements,3*e)}setFromEuler(t){return this.x=t._x,this.y=t._y,this.z=t._z,this}setFromColor(t){return this.x=t.r,this.y=t.g,this.z=t.b,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){const t=Math.random()*Math.PI*2,e=2*Math.random()-1,s=Math.sqrt(1-e*e);return this.x=s*Math.cos(t),this.y=e,this.z=s*Math.sin(t),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}}const zi=new Ti,Ci=new Ai;class Ii{static{Ii.prototype.isMatrix3=!0}constructor(t,e,s,i,r,n,a,o,h){this.elements=[1,0,0,0,1,0,0,0,1],void 0!==t&&this.set(t,e,s,i,r,n,a,o,h)}set(t,e,s,i,r,n,a,o,h){const l=this.elements;return l[0]=t,l[1]=i,l[2]=a,l[3]=e,l[4]=r,l[5]=o,l[6]=s,l[7]=n,l[8]=h,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=this.elements,s=t.elements;return e[0]=s[0],e[1]=s[1],e[2]=s[2],e[3]=s[3],e[4]=s[4],e[5]=s[5],e[6]=s[6],e[7]=s[7],e[8]=s[8],this}extractBasis(t,e,s){return t.setFromMatrix3Column(this,0),e.setFromMatrix3Column(this,1),s.setFromMatrix3Column(this,2),this}setFromMatrix4(t){const e=t.elements;return this.set(e[0],e[4],e[8],e[1],e[5],e[9],e[2],e[6],e[10]),this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const s=t.elements,i=e.elements,r=this.elements,n=s[0],a=s[3],o=s[6],h=s[1],l=s[4],c=s[7],u=s[2],d=s[5],p=s[8],m=i[0],y=i[3],g=i[6],f=i[1],x=i[4],b=i[7],v=i[2],w=i[5],M=i[8];return r[0]=n*m+a*f+o*v,r[3]=n*y+a*x+o*w,r[6]=n*g+a*b+o*M,r[1]=h*m+l*f+c*v,r[4]=h*y+l*x+c*w,r[7]=h*g+l*b+c*M,r[2]=u*m+d*f+p*v,r[5]=u*y+d*x+p*w,r[8]=u*g+d*b+p*M,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this}determinant(){const t=this.elements,e=t[0],s=t[1],i=t[2],r=t[3],n=t[4],a=t[5],o=t[6],h=t[7],l=t[8];return e*n*l-e*a*h-s*r*l+s*a*o+i*r*h-i*n*o}invert(){const t=this.elements,e=t[0],s=t[1],i=t[2],r=t[3],n=t[4],a=t[5],o=t[6],h=t[7],l=t[8],c=l*n-a*h,u=a*o-l*r,d=h*r-n*o,p=e*c+s*u+i*d;if(0===p)return this.set(0,0,0,0,0,0,0,0,0);const m=1/p;return t[0]=c*m,t[1]=(i*h-l*s)*m,t[2]=(a*s-i*n)*m,t[3]=u*m,t[4]=(l*e-i*o)*m,t[5]=(i*r-a*e)*m,t[6]=d*m,t[7]=(s*o-h*e)*m,t[8]=(n*e-s*r)*m,this}transpose(){let t;const e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this}getNormalMatrix(t){return this.setFromMatrix4(t).invert().transpose()}transposeIntoArray(t){const e=this.elements;return t[0]=e[0],t[1]=e[3],t[2]=e[6],t[3]=e[1],t[4]=e[4],t[5]=e[7],t[6]=e[2],t[7]=e[5],t[8]=e[8],this}setUvTransform(t,e,s,i,r,n,a){const o=Math.cos(r),h=Math.sin(r);return this.set(s*o,s*h,-s*(o*n+h*a)+n+t,-i*h,i*o,-i*(-h*n+o*a)+a+e,0,0,1),this}scale(t,e){return hi("Matrix3: .scale() is deprecated. Use .makeScale() instead."),this.premultiply(Bi.makeScale(t,e)),this}rotate(t){return hi("Matrix3: .rotate() is deprecated. Use .makeRotation() instead."),this.premultiply(Bi.makeRotation(-t)),this}translate(t,e){return hi("Matrix3: .translate() is deprecated. Use .makeTranslation() instead."),this.premultiply(Bi.makeTranslation(t,e)),this}makeTranslation(t,e){return t.isVector2?this.set(1,0,t.x,0,1,t.y,0,0,1):this.set(1,0,t,0,1,e,0,0,1),this}makeRotation(t){const e=Math.cos(t),s=Math.sin(t);return this.set(e,-s,0,s,e,0,0,0,1),this}makeScale(t,e){return this.set(t,0,0,0,e,0,0,0,1),this}equals(t){const e=this.elements,s=t.elements;for(let t=0;t<9;t++)if(e[t]!==s[t])return!1;return!0}fromArray(t,e=0){for(let s=0;s<9;s++)this.elements[s]=t[s+e];return this}toArray(t=[],e=0){const s=this.elements;return t[e]=s[0],t[e+1]=s[1],t[e+2]=s[2],t[e+3]=s[3],t[e+4]=s[4],t[e+5]=s[5],t[e+6]=s[6],t[e+7]=s[7],t[e+8]=s[8],t}clone(){return(new this.constructor).fromArray(this.elements)}}const Bi=new Ii,ki=(new Ii).set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),Oi=(new Ii).set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function Pi(){const t={enabled:!0,workingColorSpace:ss,spaces:{},convert:function(t,e,s){return!1!==this.enabled&&e!==s&&e&&s?(this.spaces[e].transfer===rs&&(t.r=Ei(t.r),t.g=Ei(t.g),t.b=Ei(t.b)),this.spaces[e].primaries!==this.spaces[s].primaries&&(t.applyMatrix3(this.spaces[e].toXYZ),t.applyMatrix3(this.spaces[s].fromXYZ)),this.spaces[s].transfer===rs&&(t.r=Ni(t.r),t.g=Ni(t.g),t.b=Ni(t.b)),t):t},workingToColorSpace:function(t,e){return this.convert(t,this.workingColorSpace,e)},colorSpaceToWorking:function(t,e){return this.convert(t,e,this.workingColorSpace)},getPrimaries:function(t){return this.spaces[t].primaries},getTransfer:function(t){return""===t?is:this.spaces[t].transfer},getToneMappingMode:function(t){return this.spaces[t].outputColorSpaceConfig.toneMappingMode||"standard"},getLuminanceCoefficients:function(t,e=this.workingColorSpace){return t.fromArray(this.spaces[e].luminanceCoefficients)},define:function(t){Object.assign(this.spaces,t)},_getMatrix:function(t,e,s){return t.copy(this.spaces[e].toXYZ).multiply(this.spaces[s].fromXYZ)},_getDrawingBufferColorSpace:function(t){return this.spaces[t].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(t=this.workingColorSpace){return this.spaces[t].workingColorSpaceConfig.unpackColorSpace},fromWorkingColorSpace:function(e,s){return hi("ColorManagement: .fromWorkingColorSpace() has been renamed to .workingToColorSpace()."),t.workingToColorSpace(e,s)},toWorkingColorSpace:function(e,s){return hi("ColorManagement: .toWorkingColorSpace() has been renamed to .colorSpaceToWorking()."),t.colorSpaceToWorking(e,s)}},e=[.64,.33,.3,.6,.15,.06],s=[.2126,.7152,.0722],i=[.3127,.329];return t.define({[ss]:{primaries:e,whitePoint:i,transfer:is,toXYZ:ki,fromXYZ:Oi,luminanceCoefficients:s,workingColorSpaceConfig:{unpackColorSpace:es},outputColorSpaceConfig:{drawingBufferColorSpace:es}},[es]:{primaries:e,whitePoint:i,transfer:rs,toXYZ:ki,fromXYZ:Oi,luminanceCoefficients:s,outputColorSpaceConfig:{drawingBufferColorSpace:es}}}),t}const Ri=Pi();function Ei(t){return t<.04045?.0773993808*t:Math.pow(.9478672986*t+.0521327014,2.4)}function Ni(t){return t<.0031308?12.92*t:1.055*Math.pow(t,.41666)-.055}let Vi;class Li{static getDataURL(t,e="image/png"){if(/^data:/i.test(t.src))return t.src;if("undefined"==typeof HTMLCanvasElement)return t.src;let s;if(t instanceof HTMLCanvasElement)s=t;else{void 0===Vi&&(Vi=Qs("canvas")),Vi.width=t.width,Vi.height=t.height;const e=Vi.getContext("2d");t instanceof ImageData?e.putImageData(t,0,0):e.drawImage(t,0,0,t.width,t.height),s=Vi}return s.toDataURL(e)}static sRGBToLinear(t){if("undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap){const e=Qs("canvas");e.width=t.width,e.height=t.height;const s=e.getContext("2d");s.drawImage(t,0,0,t.width,t.height);const i=s.getImageData(0,0,t.width,t.height),r=i.data;for(let t=0;t1),this.pmremVersion=0,this.normalized=!1}get width(){return this.source.getSize(Wi).x}get height(){return this.source.getSize(Wi).y}get depth(){return this.source.getSize(Wi).z}get image(){return this.source.data}set image(t){this.source.data=t}updateMatrix(){this.matrix.setUvTransform(this.offset.x,this.offset.y,this.repeat.x,this.repeat.y,this.rotation,this.center.x,this.center.y)}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}clone(){return(new this.constructor).copy(this)}copy(t){return this.name=t.name,this.source=t.source,this.mipmaps=t.mipmaps.slice(0),this.mapping=t.mapping,this.channel=t.channel,this.wrapS=t.wrapS,this.wrapT=t.wrapT,this.magFilter=t.magFilter,this.minFilter=t.minFilter,this.anisotropy=t.anisotropy,this.format=t.format,this.internalFormat=t.internalFormat,this.type=t.type,this.normalized=t.normalized,this.offset.copy(t.offset),this.repeat.copy(t.repeat),this.center.copy(t.center),this.rotation=t.rotation,this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrix.copy(t.matrix),this.generateMipmaps=t.generateMipmaps,this.premultiplyAlpha=t.premultiplyAlpha,this.flipY=t.flipY,this.unpackAlignment=t.unpackAlignment,this.colorSpace=t.colorSpace,this.renderTarget=t.renderTarget,this.isRenderTargetTexture=t.isRenderTargetTexture,this.isArrayTexture=t.isArrayTexture,this.userData=JSON.parse(JSON.stringify(t.userData)),this.needsUpdate=!0,this}setValues(t){for(const e in t){const s=t[e];if(void 0===s){ai(`Texture.setValues(): parameter '${e}' has value of undefined.`);continue}const i=this[e];void 0!==i?i&&s&&i.isVector2&&s.isVector2||i&&s&&i.isVector3&&s.isVector3||i&&s&&i.isMatrix3&&s.isMatrix3?i.copy(s):this[e]=s:ai(`Texture.setValues(): property '${e}' does not exist.`)}}toJSON(t){const e=void 0===t||"string"==typeof t;if(!e&&void 0!==t.textures[this.uuid])return t.textures[this.uuid];const s={metadata:{version:4.7,type:"Texture",generator:"Texture.toJSON"},uuid:this.uuid,name:this.name,image:this.source.toJSON(t).uuid,mapping:this.mapping,channel:this.channel,repeat:[this.repeat.x,this.repeat.y],offset:[this.offset.x,this.offset.y],center:[this.center.x,this.center.y],rotation:this.rotation,wrap:[this.wrapS,this.wrapT],format:this.format,internalFormat:this.internalFormat,type:this.type,normalized:this.normalized,colorSpace:this.colorSpace,minFilter:this.minFilter,magFilter:this.magFilter,anisotropy:this.anisotropy,flipY:this.flipY,generateMipmaps:this.generateMipmaps,premultiplyAlpha:this.premultiplyAlpha,unpackAlignment:this.unpackAlignment};return Object.keys(this.userData).length>0&&(s.userData=this.userData),e||(t.textures[this.uuid]=s),s}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(t){if(this.mapping!==ht)return t;if(t.applyMatrix3(this.matrix),t.x<0||t.x>1)switch(this.wrapS){case mt:t.x=t.x-Math.floor(t.x);break;case yt:t.x=t.x<0?0:1;break;case gt:1===Math.abs(Math.floor(t.x)%2)?t.x=Math.ceil(t.x)-t.x:t.x=t.x-Math.floor(t.x)}if(t.y<0||t.y>1)switch(this.wrapT){case mt:t.y=t.y-Math.floor(t.y);break;case yt:t.y=t.y<0?0:1;break;case gt:1===Math.abs(Math.floor(t.y)%2)?t.y=Math.ceil(t.y)-t.y:t.y=t.y-Math.floor(t.y)}return this.flipY&&(t.y=1-t.y),t}set needsUpdate(t){!0===t&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(t){!0===t&&this.pmremVersion++}}Ji.DEFAULT_IMAGE=null,Ji.DEFAULT_MAPPING=ht,Ji.DEFAULT_ANISOTROPY=1;class qi{static{qi.prototype.isVector4=!0}constructor(t=0,e=0,s=0,i=1){this.x=t,this.y=e,this.z=s,this.w=i}get width(){return this.z}set width(t){this.z=t}get height(){return this.w}set height(t){this.w=t}set(t,e,s,i){return this.x=t,this.y=e,this.z=s,this.w=i,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this.w=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setW(t){return this.w=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;case 3:this.w=e;break;default:throw new Error("THREE.Vector4: index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("THREE.Vector4: index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=void 0!==t.w?t.w:1,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this.w+=t.w,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this.w+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this.w=t.w+e.w,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this.w+=t.w*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this.w-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this.w=t.w-e.w,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this.w*=t.w,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this}applyMatrix4(t){const e=this.x,s=this.y,i=this.z,r=this.w,n=t.elements;return this.x=n[0]*e+n[4]*s+n[8]*i+n[12]*r,this.y=n[1]*e+n[5]*s+n[9]*i+n[13]*r,this.z=n[2]*e+n[6]*s+n[10]*i+n[14]*r,this.w=n[3]*e+n[7]*s+n[11]*i+n[15]*r,this}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this.w/=t.w,this}divideScalar(t){return this.multiplyScalar(1/t)}setAxisAngleFromQuaternion(t){this.w=2*Math.acos(t.w);const e=Math.sqrt(1-t.w*t.w);return e<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=t.x/e,this.y=t.y/e,this.z=t.z/e),this}setAxisAngleFromRotationMatrix(t){let e,s,i,r;const n=.01,a=.1,o=t.elements,h=o[0],l=o[4],c=o[8],u=o[1],d=o[5],p=o[9],m=o[2],y=o[6],g=o[10];if(Math.abs(l-u)o&&t>f?tf?o1);this.dispose()}this.viewport.set(0,0,t,e),this.scissor.set(0,0,t,e)}clone(){return(new this.constructor).copy(this)}copy(t){this.width=t.width,this.height=t.height,this.depth=t.depth,this.scissor.copy(t.scissor),this.scissorTest=t.scissorTest,this.viewport.copy(t.viewport),this.textures.length=0;for(let e=0,s=t.textures.length;e>>0}enable(t){this.mask|=1<1){for(let t=0;t1){for(let t=0;t0&&(i.userData=this.userData),i.layers=this.layers.mask,i.matrix=this.matrix.toArray(),i.up=this.up.toArray(),null!==this.pivot&&(i.pivot=this.pivot.toArray()),!1===this.matrixAutoUpdate&&(i.matrixAutoUpdate=!1),void 0!==this.morphTargetDictionary&&(i.morphTargetDictionary=Object.assign({},this.morphTargetDictionary)),void 0!==this.morphTargetInfluences&&(i.morphTargetInfluences=this.morphTargetInfluences.slice()),this.isInstancedMesh&&(i.type="InstancedMesh",i.count=this.count,i.instanceMatrix=this.instanceMatrix.toJSON(),null!==this.instanceColor&&(i.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(i.type="BatchedMesh",i.perObjectFrustumCulled=this.perObjectFrustumCulled,i.sortObjects=this.sortObjects,i.drawRanges=this._drawRanges,i.reservedRanges=this._reservedRanges,i.geometryInfo=this._geometryInfo.map(t=>({...t,boundingBox:t.boundingBox?t.boundingBox.toJSON():void 0,boundingSphere:t.boundingSphere?t.boundingSphere.toJSON():void 0})),i.instanceInfo=this._instanceInfo.map(t=>({...t})),i.availableInstanceIds=this._availableInstanceIds.slice(),i.availableGeometryIds=this._availableGeometryIds.slice(),i.nextIndexStart=this._nextIndexStart,i.nextVertexStart=this._nextVertexStart,i.geometryCount=this._geometryCount,i.maxInstanceCount=this._maxInstanceCount,i.maxVertexCount=this._maxVertexCount,i.maxIndexCount=this._maxIndexCount,i.geometryInitialized=this._geometryInitialized,i.matricesTexture=this._matricesTexture.toJSON(t),i.indirectTexture=this._indirectTexture.toJSON(t),null!==this._colorsTexture&&(i.colorsTexture=this._colorsTexture.toJSON(t)),null!==this.boundingSphere&&(i.boundingSphere=this.boundingSphere.toJSON()),null!==this.boundingBox&&(i.boundingBox=this.boundingBox.toJSON())),this.isScene)this.background&&(this.background.isColor?i.background=this.background.toJSON():this.background.isTexture&&(i.background=this.background.toJSON(t).uuid)),this.environment&&this.environment.isTexture&&!0!==this.environment.isRenderTargetTexture&&(i.environment=this.environment.toJSON(t).uuid);else if(this.isMesh||this.isLine||this.isPoints){i.geometry=r(t.geometries,this.geometry);const e=this.geometry.parameters;if(void 0!==e&&void 0!==e.shapes){const s=e.shapes;if(Array.isArray(s))for(let e=0,i=s.length;e0){i.children=[];for(let e=0;e0){i.animations=[];for(let e=0;e0&&(s.geometries=e),i.length>0&&(s.materials=i),r.length>0&&(s.textures=r),a.length>0&&(s.images=a),o.length>0&&(s.shapes=o),h.length>0&&(s.skeletons=h),l.length>0&&(s.animations=l),c.length>0&&(s.nodes=c)}return s.object=i,s;function n(t){const e=[];for(const s in t){const i=t[s];delete i.metadata,e.push(i)}return e}}clone(t){return(new this.constructor).copy(this,t)}copy(t,e=!0){if(this.name=t.name,this.up.copy(t.up),this.position.copy(t.position),this.rotation.order=t.rotation.order,this.quaternion.copy(t.quaternion),this.scale.copy(t.scale),this.pivot=null!==t.pivot?t.pivot.clone():null,this.matrix.copy(t.matrix),this.matrixWorld.copy(t.matrixWorld),this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrixWorldAutoUpdate=t.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=t.matrixWorldNeedsUpdate,this.layers.mask=t.layers.mask,this.visible=t.visible,this.castShadow=t.castShadow,this.receiveShadow=t.receiveShadow,this.frustumCulled=t.frustumCulled,this.renderOrder=t.renderOrder,this.static=t.static,this.animations=t.animations.slice(),this.userData=JSON.parse(JSON.stringify(t.userData)),!0===e)for(let e=0;eo+l?(h.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!h.inputState.pinching&&a<=o-l&&(h.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:t.handedness,target:this}))}else null!==o&&t.gripSpace&&(r=e.getPose(t.gripSpace,s),null!==r&&(o.matrix.fromArray(r.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,r.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(r.linearVelocity)):o.hasLinearVelocity=!1,r.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(r.angularVelocity)):o.hasAngularVelocity=!1,o.eventsEnabled&&o.dispatchEvent({type:"gripUpdated",data:t,target:this})));null!==a&&(i=e.getPose(t.targetRaySpace,s),null===i&&null!==r&&(i=r),null!==i&&(a.matrix.fromArray(i.transform.matrix),a.matrix.decompose(a.position,a.rotation,a.scale),a.matrixWorldNeedsUpdate=!0,i.linearVelocity?(a.hasLinearVelocity=!0,a.linearVelocity.copy(i.linearVelocity)):a.hasLinearVelocity=!1,i.angularVelocity?(a.hasAngularVelocity=!0,a.angularVelocity.copy(i.angularVelocity)):a.hasAngularVelocity=!1,this.dispatchEvent(zr)))}return null!==a&&(a.visible=null!==i),null!==o&&(o.visible=null!==r),null!==h&&(h.visible=null!==n),this}_getHandJoint(t,e){if(void 0===t.joints[e.jointName]){const s=new Tr;s.matrixAutoUpdate=!1,s.visible=!1,t.joints[e.jointName]=s,t.add(s)}return t.joints[e.jointName]}}const Ir={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},Br={h:0,s:0,l:0},kr={h:0,s:0,l:0};function Or(t,e,s){return s<0&&(s+=1),s>1&&(s-=1),s<1/6?t+6*(e-t)*s:s<.5?e:s<2/3?t+6*(e-t)*(2/3-s):t}class Pr{constructor(t,e,s){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(t,e,s)}set(t,e,s){if(void 0===e&&void 0===s){const e=t;e&&e.isColor?this.copy(e):"number"==typeof e?this.setHex(e):"string"==typeof e&&this.setStyle(e)}else this.setRGB(t,e,s);return this}setScalar(t){return this.r=t,this.g=t,this.b=t,this}setHex(t,e=es){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(255&t)/255,Ri.colorSpaceToWorking(this,e),this}setRGB(t,e,s,i=Ri.workingColorSpace){return this.r=t,this.g=e,this.b=s,Ri.colorSpaceToWorking(this,i),this}setHSL(t,e,s,i=Ri.workingColorSpace){if(t=bi(t,1),e=xi(e,0,1),s=xi(s,0,1),0===e)this.r=this.g=this.b=s;else{const i=s<=.5?s*(1+e):s+e-s*e,r=2*s-i;this.r=Or(r,i,t+1/3),this.g=Or(r,i,t),this.b=Or(r,i,t-1/3)}return Ri.colorSpaceToWorking(this,i),this}setStyle(t,e=es){function s(e){void 0!==e&&parseFloat(e)<1&&ai("Color: Alpha component of "+t+" will be ignored.")}let i;if(i=/^(\w+)\(([^\)]*)\)/.exec(t)){let r;const n=i[1],a=i[2];switch(n){case"rgb":case"rgba":if(r=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return s(r[4]),this.setRGB(Math.min(255,parseInt(r[1],10))/255,Math.min(255,parseInt(r[2],10))/255,Math.min(255,parseInt(r[3],10))/255,e);if(r=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return s(r[4]),this.setRGB(Math.min(100,parseInt(r[1],10))/100,Math.min(100,parseInt(r[2],10))/100,Math.min(100,parseInt(r[3],10))/100,e);break;case"hsl":case"hsla":if(r=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(a))return s(r[4]),this.setHSL(parseFloat(r[1])/360,parseFloat(r[2])/100,parseFloat(r[3])/100,e);break;default:ai("Color: Unknown color model "+t)}}else if(i=/^\#([A-Fa-f\d]+)$/.exec(t)){const s=i[1],r=s.length;if(3===r)return this.setRGB(parseInt(s.charAt(0),16)/15,parseInt(s.charAt(1),16)/15,parseInt(s.charAt(2),16)/15,e);if(6===r)return this.setHex(parseInt(s,16),e);ai("Color: Invalid hex color "+t)}else if(t&&t.length>0)return this.setColorName(t,e);return this}setColorName(t,e=es){const s=Ir[t.toLowerCase()];return void 0!==s?this.setHex(s,e):ai("Color: Unknown color "+t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(t){return this.r=t.r,this.g=t.g,this.b=t.b,this}copySRGBToLinear(t){return this.r=Ei(t.r),this.g=Ei(t.g),this.b=Ei(t.b),this}copyLinearToSRGB(t){return this.r=Ni(t.r),this.g=Ni(t.g),this.b=Ni(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(t=es){return Ri.workingToColorSpace(Rr.copy(this),t),65536*Math.round(xi(255*Rr.r,0,255))+256*Math.round(xi(255*Rr.g,0,255))+Math.round(xi(255*Rr.b,0,255))}getHexString(t=es){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=Ri.workingColorSpace){Ri.workingToColorSpace(Rr.copy(this),e);const s=Rr.r,i=Rr.g,r=Rr.b,n=Math.max(s,i,r),a=Math.min(s,i,r);let o,h;const l=(a+n)/2;if(a===n)o=0,h=0;else{const t=n-a;switch(h=l<=.5?t/(n+a):t/(2-n-a),n){case s:o=(i-r)/t+(i0&&(e.object.backgroundBlurriness=this.backgroundBlurriness),1!==this.backgroundIntensity&&(e.object.backgroundIntensity=this.backgroundIntensity),e.object.backgroundRotation=this.backgroundRotation.toArray(),1!==this.environmentIntensity&&(e.object.environmentIntensity=this.environmentIntensity),e.object.environmentRotation=this.environmentRotation.toArray(),e}}const Lr=new Ti,Fr=new Ti,Dr=new Ti,Ur=new Ti,jr=new Ti,Wr=new Ti,Jr=new Ti,qr=new Ti,Hr=new Ti,Xr=new Ti,Yr=new qi,Zr=new qi,Gr=new qi;class $r{constructor(t=new Ti,e=new Ti,s=new Ti){this.a=t,this.b=e,this.c=s}static getNormal(t,e,s,i){i.subVectors(s,e),Lr.subVectors(t,e),i.cross(Lr);const r=i.lengthSq();return r>0?i.multiplyScalar(1/Math.sqrt(r)):i.set(0,0,0)}static getBarycoord(t,e,s,i,r){Lr.subVectors(i,e),Fr.subVectors(s,e),Dr.subVectors(t,e);const n=Lr.dot(Lr),a=Lr.dot(Fr),o=Lr.dot(Dr),h=Fr.dot(Fr),l=Fr.dot(Dr),c=n*h-a*a;if(0===c)return r.set(0,0,0),null;const u=1/c,d=(h*o-a*l)*u,p=(n*l-a*o)*u;return r.set(1-d-p,p,d)}static containsPoint(t,e,s,i){return null!==this.getBarycoord(t,e,s,i,Ur)&&(Ur.x>=0&&Ur.y>=0&&Ur.x+Ur.y<=1)}static getInterpolation(t,e,s,i,r,n,a,o){return null===this.getBarycoord(t,e,s,i,Ur)?(o.x=0,o.y=0,"z"in o&&(o.z=0),"w"in o&&(o.w=0),null):(o.setScalar(0),o.addScaledVector(r,Ur.x),o.addScaledVector(n,Ur.y),o.addScaledVector(a,Ur.z),o)}static getInterpolatedAttribute(t,e,s,i,r,n){return Yr.setScalar(0),Zr.setScalar(0),Gr.setScalar(0),Yr.fromBufferAttribute(t,e),Zr.fromBufferAttribute(t,s),Gr.fromBufferAttribute(t,i),n.setScalar(0),n.addScaledVector(Yr,r.x),n.addScaledVector(Zr,r.y),n.addScaledVector(Gr,r.z),n}static isFrontFacing(t,e,s,i){return Lr.subVectors(s,e),Fr.subVectors(t,e),Lr.cross(Fr).dot(i)<0}set(t,e,s){return this.a.copy(t),this.b.copy(e),this.c.copy(s),this}setFromPointsAndIndices(t,e,s,i){return this.a.copy(t[e]),this.b.copy(t[s]),this.c.copy(t[i]),this}setFromAttributeAndIndices(t,e,s,i){return this.a.fromBufferAttribute(t,e),this.b.fromBufferAttribute(t,s),this.c.fromBufferAttribute(t,i),this}clone(){return(new this.constructor).copy(this)}copy(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this}getArea(){return Lr.subVectors(this.c,this.b),Fr.subVectors(this.a,this.b),.5*Lr.cross(Fr).length()}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return $r.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return $r.getBarycoord(t,this.a,this.b,this.c,e)}getInterpolation(t,e,s,i,r){return $r.getInterpolation(t,this.a,this.b,this.c,e,s,i,r)}containsPoint(t){return $r.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return $r.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(t){return t.intersectsTriangle(this)}closestPointToPoint(t,e){const s=this.a,i=this.b,r=this.c;let n,a;jr.subVectors(i,s),Wr.subVectors(r,s),qr.subVectors(t,s);const o=jr.dot(qr),h=Wr.dot(qr);if(o<=0&&h<=0)return e.copy(s);Hr.subVectors(t,i);const l=jr.dot(Hr),c=Wr.dot(Hr);if(l>=0&&c<=l)return e.copy(i);const u=o*c-l*h;if(u<=0&&o>=0&&l<=0)return n=o/(o-l),e.copy(s).addScaledVector(jr,n);Xr.subVectors(t,r);const d=jr.dot(Xr),p=Wr.dot(Xr);if(p>=0&&d<=p)return e.copy(r);const m=d*h-o*p;if(m<=0&&h>=0&&p<=0)return a=h/(h-p),e.copy(s).addScaledVector(Wr,a);const y=l*p-d*c;if(y<=0&&c-l>=0&&d-p>=0)return Jr.subVectors(r,i),a=(c-l)/(c-l+(d-p)),e.copy(i).addScaledVector(Jr,a);const g=1/(y+m+u);return n=m*g,a=u*g,e.copy(s).addScaledVector(jr,n).addScaledVector(Wr,a)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}class Qr{constructor(t=new Ti(1/0,1/0,1/0),e=new Ti(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=t,this.max=e}set(t,e){return this.min.copy(t),this.max.copy(e),this}setFromArray(t){this.makeEmpty();for(let e=0,s=t.length;e=this.min.x&&t.x<=this.max.x&&t.y>=this.min.y&&t.y<=this.max.y&&t.z>=this.min.z&&t.z<=this.max.z}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y&&this.min.z<=t.min.z&&t.max.z<=this.max.z}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y),(t.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(t){return t.max.x>=this.min.x&&t.min.x<=this.max.x&&t.max.y>=this.min.y&&t.min.y<=this.max.y&&t.max.z>=this.min.z&&t.min.z<=this.max.z}intersectsSphere(t){return this.clampPoint(t.center,tn),tn.distanceToSquared(t.center)<=t.radius*t.radius}intersectsPlane(t){let e,s;return t.normal.x>0?(e=t.normal.x*this.min.x,s=t.normal.x*this.max.x):(e=t.normal.x*this.max.x,s=t.normal.x*this.min.x),t.normal.y>0?(e+=t.normal.y*this.min.y,s+=t.normal.y*this.max.y):(e+=t.normal.y*this.max.y,s+=t.normal.y*this.min.y),t.normal.z>0?(e+=t.normal.z*this.min.z,s+=t.normal.z*this.max.z):(e+=t.normal.z*this.max.z,s+=t.normal.z*this.min.z),e<=-t.constant&&s>=-t.constant}intersectsTriangle(t){if(this.isEmpty())return!1;this.getCenter(ln),cn.subVectors(this.max,ln),sn.subVectors(t.a,ln),rn.subVectors(t.b,ln),nn.subVectors(t.c,ln),an.subVectors(rn,sn),on.subVectors(nn,rn),hn.subVectors(sn,nn);let e=[0,-an.z,an.y,0,-on.z,on.y,0,-hn.z,hn.y,an.z,0,-an.x,on.z,0,-on.x,hn.z,0,-hn.x,-an.y,an.x,0,-on.y,on.x,0,-hn.y,hn.x,0];return!!pn(e,sn,rn,nn,cn)&&(e=[1,0,0,0,1,0,0,0,1],!!pn(e,sn,rn,nn,cn)&&(un.crossVectors(an,on),e=[un.x,un.y,un.z],pn(e,sn,rn,nn,cn)))}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,tn).distanceTo(t)}getBoundingSphere(t){return this.isEmpty()?t.makeEmpty():(this.getCenter(t.center),t.radius=.5*this.getSize(tn).length()),t}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}applyMatrix4(t){return this.isEmpty()||(Kr[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),Kr[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),Kr[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),Kr[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),Kr[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),Kr[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),Kr[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),Kr[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(Kr)),this}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}toJSON(){return{min:this.min.toArray(),max:this.max.toArray()}}fromJSON(t){return this.min.fromArray(t.min),this.max.fromArray(t.max),this}}const Kr=[new Ti,new Ti,new Ti,new Ti,new Ti,new Ti,new Ti,new Ti],tn=new Ti,en=new Qr,sn=new Ti,rn=new Ti,nn=new Ti,an=new Ti,on=new Ti,hn=new Ti,ln=new Ti,cn=new Ti,un=new Ti,dn=new Ti;function pn(t,e,s,i,r){for(let n=0,a=t.length-3;n<=a;n+=3){dn.fromArray(t,n);const a=r.x*Math.abs(dn.x)+r.y*Math.abs(dn.y)+r.z*Math.abs(dn.z),o=e.dot(dn),h=s.dot(dn),l=i.dot(dn);if(Math.max(-Math.max(o,h,l),Math.min(o,h,l))>a)return!1}return!0}const mn=yn();function yn(){const t=new ArrayBuffer(4),e=new Float32Array(t),s=new Uint32Array(t),i=new Uint32Array(512),r=new Uint32Array(512);for(let t=0;t<256;++t){const e=t-127;e<-27?(i[t]=0,i[256|t]=32768,r[t]=24,r[256|t]=24):e<-14?(i[t]=1024>>-e-14,i[256|t]=1024>>-e-14|32768,r[t]=-e-1,r[256|t]=-e-1):e<=15?(i[t]=e+15<<10,i[256|t]=e+15<<10|32768,r[t]=13,r[256|t]=13):e<128?(i[t]=31744,i[256|t]=64512,r[t]=24,r[256|t]=24):(i[t]=31744,i[256|t]=64512,r[t]=13,r[256|t]=13)}const n=new Uint32Array(2048),a=new Uint32Array(64),o=new Uint32Array(64);for(let t=1;t<1024;++t){let e=t<<13,s=0;for(;!(8388608&e);)e<<=1,s-=8388608;e&=-8388609,s+=947912704,n[t]=e|s}for(let t=1024;t<2048;++t)n[t]=939524096+(t-1024<<13);for(let t=1;t<31;++t)a[t]=t<<23;a[31]=1199570944,a[32]=2147483648;for(let t=33;t<63;++t)a[t]=2147483648+(t-32<<23);a[63]=3347054592;for(let t=1;t<64;++t)32!==t&&(o[t]=1024);return{floatView:e,uint32View:s,baseTable:i,shiftTable:r,mantissaTable:n,exponentTable:a,offsetTable:o}}function gn(t){Math.abs(t)>65504&&ai("DataUtils.toHalfFloat(): Value out of range."),t=xi(t,-65504,65504),mn.floatView[0]=t;const e=mn.uint32View[0],s=e>>23&511;return mn.baseTable[s]+((8388607&e)>>mn.shiftTable[s])}function fn(t){const e=t>>10;return mn.uint32View[0]=mn.mantissaTable[mn.offsetTable[e]+(1023&t)]+mn.exponentTable[e],mn.floatView[0]}class xn{static toHalfFloat(t){return gn(t)}static fromHalfFloat(t){return fn(t)}}const bn=new Ti,vn=new _i;let wn=0;class Mn extends di{constructor(t,e,s=!1){if(super(),Array.isArray(t))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,Object.defineProperty(this,"id",{value:wn++}),this.name="",this.array=t,this.itemSize=e,this.count=void 0!==t?t.length/e:0,this.normalized=s,this.usage=Os,this.updateRanges=[],this.gpuType=Pt,this.version=0}onUploadCallback(){}set needsUpdate(t){!0===t&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.name=t.name,this.array=new t.array.constructor(t.array),this.itemSize=t.itemSize,this.count=t.count,this.normalized=t.normalized,this.usage=t.usage,this.gpuType=t.gpuType,this}copyAt(t,e,s){t*=this.itemSize,s*=e.itemSize;for(let i=0,r=this.itemSize;ithis.radius*this.radius&&(e.sub(this.center).normalize(),e.multiplyScalar(this.radius).add(this.center)),e}getBoundingBox(t){return this.isEmpty()?(t.makeEmpty(),t):(t.set(this.center,this.center),t.expandByScalar(this.radius),t)}applyMatrix4(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this}translate(t){return this.center.add(t),this}expandByPoint(t){if(this.isEmpty())return this.center.copy(t),this.radius=0,this;Pn.subVectors(t,this.center);const e=Pn.lengthSq();if(e>this.radius*this.radius){const t=Math.sqrt(e),s=.5*(t-this.radius);this.center.addScaledVector(Pn,s/t),this.radius+=s}return this}union(t){return t.isEmpty()?this:this.isEmpty()?(this.copy(t),this):(!0===this.center.equals(t.center)?this.radius=Math.max(this.radius,t.radius):(Rn.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint(Pn.copy(t.center).add(Rn)),this.expandByPoint(Pn.copy(t.center).sub(Rn))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return(new this.constructor).copy(this)}toJSON(){return{radius:this.radius,center:this.center.toArray()}}fromJSON(t){return this.radius=t.radius,this.center.fromArray(t.center),this}}let Nn=0;const Vn=new Qi,Ln=new Ar,Fn=new Ti,Dn=new Qr,Un=new Qr,jn=new Ti;class Wn extends di{constructor(){super(),this.isBufferGeometry=!0,Object.defineProperty(this,"id",{value:Nn++}),this.uuid=fi(),this.name="",this.type="BufferGeometry",this.index=null,this.indirect=null,this.indirectOffset=0,this.attributes={},this.morphAttributes={},this.morphTargetsRelative=!1,this.groups=[],this.boundingBox=null,this.boundingSphere=null,this.drawRange={start:0,count:1/0},this.userData={},this._transformed=!1}getIndex(){return this.index}setIndex(t){return Array.isArray(t)?this.index=new(function(t){for(let e=t.length-1;e>=0;--e)if(t[e]>=65535)return!0;return!1}(t)?In:zn)(t,1):this.index=t,this}setIndirect(t,e=0){return this.indirect=t,this.indirectOffset=e,this}getIndirect(){return this.indirect}getAttribute(t){return this.attributes[t]}setAttribute(t,e){return this.attributes[t]=e,this}deleteAttribute(t){return delete this.attributes[t],this}hasAttribute(t){return void 0!==this.attributes[t]}addGroup(t,e,s=0){this.groups.push({start:t,count:e,materialIndex:s})}clearGroups(){this.groups=[]}setDrawRange(t,e){this.drawRange.start=t,this.drawRange.count=e}applyMatrix4(t){const e=this.attributes.position;void 0!==e&&(e.applyMatrix4(t),e.needsUpdate=!0);const s=this.attributes.normal;if(void 0!==s){const e=(new Ii).getNormalMatrix(t);s.applyNormalMatrix(e),s.needsUpdate=!0}const i=this.attributes.tangent;return void 0!==i&&(i.transformDirection(t),i.needsUpdate=!0),null!==this.boundingBox&&this.computeBoundingBox(),null!==this.boundingSphere&&this.computeBoundingSphere(),this._transformed=!0,this}applyQuaternion(t){return Vn.makeRotationFromQuaternion(t),this.applyMatrix4(Vn),this}rotateX(t){return Vn.makeRotationX(t),this.applyMatrix4(Vn),this}rotateY(t){return Vn.makeRotationY(t),this.applyMatrix4(Vn),this}rotateZ(t){return Vn.makeRotationZ(t),this.applyMatrix4(Vn),this}translate(t,e,s){return Vn.makeTranslation(t,e,s),this.applyMatrix4(Vn),this}scale(t,e,s){return Vn.makeScale(t,e,s),this.applyMatrix4(Vn),this}lookAt(t){return Ln.lookAt(t),Ln.updateMatrix(),this.applyMatrix4(Ln.matrix),this}center(){return this.computeBoundingBox(),this.boundingBox.getCenter(Fn).negate(),this.translate(Fn.x,Fn.y,Fn.z),this}setFromPoints(t){const e=this.getAttribute("position");if(void 0===e){const e=[];for(let s=0,i=t.length;se.count&&ai("BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry."),e.needsUpdate=!0}return this}computeBoundingBox(){null===this.boundingBox&&(this.boundingBox=new Qr);const t=this.attributes.position,e=this.morphAttributes.position;if(t&&t.isGLBufferAttribute)return oi("BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.",this),void this.boundingBox.set(new Ti(-1/0,-1/0,-1/0),new Ti(1/0,1/0,1/0));if(void 0!==t){if(this.boundingBox.setFromBufferAttribute(t),e)for(let t=0,s=e.length;t0&&(t.userData=this.userData),void 0!==this.parameters&&!0!==this._transformed){const e=this.parameters;for(const s in e)void 0!==e[s]&&(t[s]=e[s]);return t}t.data={attributes:{}};const e=this.index;null!==e&&(t.data.index={type:e.array.constructor.name,array:Array.prototype.slice.call(e.array)});const s=this.attributes;for(const e in s){const i=s[e];t.data.attributes[e]=i.toJSON(t.data)}const i={};let r=!1;for(const e in this.morphAttributes){const s=this.morphAttributes[e],n=[];for(let e=0,i=s.length;e0&&(i[e]=n,r=!0)}r&&(t.data.morphAttributes=i,t.data.morphTargetsRelative=this.morphTargetsRelative);const n=this.groups;n.length>0&&(t.data.groups=JSON.parse(JSON.stringify(n)));const a=this.boundingSphere;return null!==a&&(t.data.boundingSphere=a.toJSON()),t}clone(){return(new this.constructor).copy(this)}copy(t){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;const e={};this.name=t.name;const s=t.index;null!==s&&this.setIndex(s.clone());const i=t.attributes;for(const t in i){const s=i[t];this.setAttribute(t,s.clone(e))}const r=t.morphAttributes;for(const t in r){const s=[],i=r[t];for(let t=0,r=i.length;t0!=t>0&&this.version++,this._alphaTest=t}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(t){if(void 0!==t)for(const e in t){const s=t[e];if(void 0===s){ai(`Material: parameter '${e}' has value of undefined.`);continue}const i=this[e];void 0!==i?i&&i.isColor?i.set(s):i&&i.isVector2&&s&&s.isVector2||i&&i.isEuler&&s&&s.isEuler||i&&i.isVector3&&s&&s.isVector3?i.copy(s):this[e]=s:ai(`Material: '${e}' is not a property of THREE.${this.type}.`)}}toJSON(t){const e=void 0===t||"string"==typeof t;e&&(t={textures:{},images:{}});const s={metadata:{version:4.7,type:"Material",generator:"Material.toJSON"}};function i(t){const e=[];for(const s in t){const i=t[s];delete i.metadata,e.push(i)}return e}if(s.uuid=this.uuid,s.type=this.type,""!==this.name&&(s.name=this.name),this.color&&this.color.isColor&&(s.color=this.color.getHex()),void 0!==this.roughness&&(s.roughness=this.roughness),void 0!==this.metalness&&(s.metalness=this.metalness),void 0!==this.sheen&&(s.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(s.sheenColor=this.sheenColor.getHex()),void 0!==this.sheenRoughness&&(s.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(s.emissive=this.emissive.getHex()),void 0!==this.emissiveIntensity&&1!==this.emissiveIntensity&&(s.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(s.specular=this.specular.getHex()),void 0!==this.specularIntensity&&(s.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(s.specularColor=this.specularColor.getHex()),void 0!==this.shininess&&(s.shininess=this.shininess),void 0!==this.clearcoat&&(s.clearcoat=this.clearcoat),void 0!==this.clearcoatRoughness&&(s.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(s.clearcoatMap=this.clearcoatMap.toJSON(t).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(s.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(t).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(s.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(t).uuid,s.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.sheenColorMap&&this.sheenColorMap.isTexture&&(s.sheenColorMap=this.sheenColorMap.toJSON(t).uuid),this.sheenRoughnessMap&&this.sheenRoughnessMap.isTexture&&(s.sheenRoughnessMap=this.sheenRoughnessMap.toJSON(t).uuid),void 0!==this.dispersion&&(s.dispersion=this.dispersion),void 0!==this.iridescence&&(s.iridescence=this.iridescence),void 0!==this.iridescenceIOR&&(s.iridescenceIOR=this.iridescenceIOR),void 0!==this.iridescenceThicknessRange&&(s.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(s.iridescenceMap=this.iridescenceMap.toJSON(t).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(s.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(t).uuid),void 0!==this.anisotropy&&(s.anisotropy=this.anisotropy),void 0!==this.anisotropyRotation&&(s.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(s.anisotropyMap=this.anisotropyMap.toJSON(t).uuid),this.map&&this.map.isTexture&&(s.map=this.map.toJSON(t).uuid),this.matcap&&this.matcap.isTexture&&(s.matcap=this.matcap.toJSON(t).uuid),this.alphaMap&&this.alphaMap.isTexture&&(s.alphaMap=this.alphaMap.toJSON(t).uuid),this.lightMap&&this.lightMap.isTexture&&(s.lightMap=this.lightMap.toJSON(t).uuid,s.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(s.aoMap=this.aoMap.toJSON(t).uuid,s.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(s.bumpMap=this.bumpMap.toJSON(t).uuid,s.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(s.normalMap=this.normalMap.toJSON(t).uuid,s.normalMapType=this.normalMapType,s.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(s.displacementMap=this.displacementMap.toJSON(t).uuid,s.displacementScale=this.displacementScale,s.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(s.roughnessMap=this.roughnessMap.toJSON(t).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(s.metalnessMap=this.metalnessMap.toJSON(t).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(s.emissiveMap=this.emissiveMap.toJSON(t).uuid),this.specularMap&&this.specularMap.isTexture&&(s.specularMap=this.specularMap.toJSON(t).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(s.specularIntensityMap=this.specularIntensityMap.toJSON(t).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(s.specularColorMap=this.specularColorMap.toJSON(t).uuid),this.envMap&&this.envMap.isTexture&&(s.envMap=this.envMap.toJSON(t).uuid,void 0!==this.combine&&(s.combine=this.combine)),void 0!==this.envMapRotation&&(s.envMapRotation=this.envMapRotation.toArray()),void 0!==this.envMapIntensity&&(s.envMapIntensity=this.envMapIntensity),void 0!==this.reflectivity&&(s.reflectivity=this.reflectivity),void 0!==this.refractionRatio&&(s.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(s.gradientMap=this.gradientMap.toJSON(t).uuid),void 0!==this.transmission&&(s.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(s.transmissionMap=this.transmissionMap.toJSON(t).uuid),void 0!==this.thickness&&(s.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(s.thicknessMap=this.thicknessMap.toJSON(t).uuid),void 0!==this.attenuationDistance&&this.attenuationDistance!==1/0&&(s.attenuationDistance=this.attenuationDistance),void 0!==this.attenuationColor&&(s.attenuationColor=this.attenuationColor.getHex()),void 0!==this.size&&(s.size=this.size),null!==this.shadowSide&&(s.shadowSide=this.shadowSide),void 0!==this.sizeAttenuation&&(s.sizeAttenuation=this.sizeAttenuation),1!==this.blending&&(s.blending=this.blending),0!==this.side&&(s.side=this.side),!0===this.vertexColors&&(s.vertexColors=!0),this.opacity<1&&(s.opacity=this.opacity),!0===this.transparent&&(s.transparent=!0),204!==this.blendSrc&&(s.blendSrc=this.blendSrc),205!==this.blendDst&&(s.blendDst=this.blendDst),100!==this.blendEquation&&(s.blendEquation=this.blendEquation),null!==this.blendSrcAlpha&&(s.blendSrcAlpha=this.blendSrcAlpha),null!==this.blendDstAlpha&&(s.blendDstAlpha=this.blendDstAlpha),null!==this.blendEquationAlpha&&(s.blendEquationAlpha=this.blendEquationAlpha),this.blendColor&&this.blendColor.isColor&&(s.blendColor=this.blendColor.getHex()),0!==this.blendAlpha&&(s.blendAlpha=this.blendAlpha),3!==this.depthFunc&&(s.depthFunc=this.depthFunc),!1===this.depthTest&&(s.depthTest=this.depthTest),!1===this.depthWrite&&(s.depthWrite=this.depthWrite),!1===this.colorWrite&&(s.colorWrite=this.colorWrite),255!==this.stencilWriteMask&&(s.stencilWriteMask=this.stencilWriteMask),519!==this.stencilFunc&&(s.stencilFunc=this.stencilFunc),0!==this.stencilRef&&(s.stencilRef=this.stencilRef),255!==this.stencilFuncMask&&(s.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==ls&&(s.stencilFail=this.stencilFail),this.stencilZFail!==ls&&(s.stencilZFail=this.stencilZFail),this.stencilZPass!==ls&&(s.stencilZPass=this.stencilZPass),!0===this.stencilWrite&&(s.stencilWrite=this.stencilWrite),void 0!==this.rotation&&0!==this.rotation&&(s.rotation=this.rotation),!0===this.polygonOffset&&(s.polygonOffset=!0),0!==this.polygonOffsetFactor&&(s.polygonOffsetFactor=this.polygonOffsetFactor),0!==this.polygonOffsetUnits&&(s.polygonOffsetUnits=this.polygonOffsetUnits),void 0!==this.linewidth&&1!==this.linewidth&&(s.linewidth=this.linewidth),void 0!==this.dashSize&&(s.dashSize=this.dashSize),void 0!==this.gapSize&&(s.gapSize=this.gapSize),void 0!==this.scale&&(s.scale=this.scale),!0===this.dithering&&(s.dithering=!0),this.alphaTest>0&&(s.alphaTest=this.alphaTest),!0===this.alphaHash&&(s.alphaHash=!0),!0===this.alphaToCoverage&&(s.alphaToCoverage=!0),!0===this.premultipliedAlpha&&(s.premultipliedAlpha=!0),!0===this.forceSinglePass&&(s.forceSinglePass=!0),!1===this.allowOverride&&(s.allowOverride=!1),!0===this.wireframe&&(s.wireframe=!0),this.wireframeLinewidth>1&&(s.wireframeLinewidth=this.wireframeLinewidth),"round"!==this.wireframeLinecap&&(s.wireframeLinecap=this.wireframeLinecap),"round"!==this.wireframeLinejoin&&(s.wireframeLinejoin=this.wireframeLinejoin),!0===this.flatShading&&(s.flatShading=!0),!1===this.visible&&(s.visible=!1),!1===this.toneMapped&&(s.toneMapped=!1),!1===this.fog&&(s.fog=!1),Object.keys(this.userData).length>0&&(s.userData=this.userData),e){const e=i(t.textures),r=i(t.images);e.length>0&&(s.textures=e),r.length>0&&(s.images=r)}return s}fromJSON(t,e){if(void 0!==t.uuid&&(this.uuid=t.uuid),void 0!==t.name&&(this.name=t.name),void 0!==t.color&&void 0!==this.color&&this.color.setHex(t.color),void 0!==t.roughness&&(this.roughness=t.roughness),void 0!==t.metalness&&(this.metalness=t.metalness),void 0!==t.sheen&&(this.sheen=t.sheen),void 0!==t.sheenColor&&(this.sheenColor=(new Pr).setHex(t.sheenColor)),void 0!==t.sheenRoughness&&(this.sheenRoughness=t.sheenRoughness),void 0!==t.emissive&&void 0!==this.emissive&&this.emissive.setHex(t.emissive),void 0!==t.specular&&void 0!==this.specular&&this.specular.setHex(t.specular),void 0!==t.specularIntensity&&(this.specularIntensity=t.specularIntensity),void 0!==t.specularColor&&void 0!==this.specularColor&&this.specularColor.setHex(t.specularColor),void 0!==t.shininess&&(this.shininess=t.shininess),void 0!==t.clearcoat&&(this.clearcoat=t.clearcoat),void 0!==t.clearcoatRoughness&&(this.clearcoatRoughness=t.clearcoatRoughness),void 0!==t.dispersion&&(this.dispersion=t.dispersion),void 0!==t.iridescence&&(this.iridescence=t.iridescence),void 0!==t.iridescenceIOR&&(this.iridescenceIOR=t.iridescenceIOR),void 0!==t.iridescenceThicknessRange&&(this.iridescenceThicknessRange=t.iridescenceThicknessRange),void 0!==t.transmission&&(this.transmission=t.transmission),void 0!==t.thickness&&(this.thickness=t.thickness),void 0!==t.attenuationDistance&&(this.attenuationDistance=t.attenuationDistance),void 0!==t.attenuationColor&&void 0!==this.attenuationColor&&this.attenuationColor.setHex(t.attenuationColor),void 0!==t.anisotropy&&(this.anisotropy=t.anisotropy),void 0!==t.anisotropyRotation&&(this.anisotropyRotation=t.anisotropyRotation),void 0!==t.fog&&(this.fog=t.fog),void 0!==t.flatShading&&(this.flatShading=t.flatShading),void 0!==t.blending&&(this.blending=t.blending),void 0!==t.combine&&(this.combine=t.combine),void 0!==t.side&&(this.side=t.side),void 0!==t.shadowSide&&(this.shadowSide=t.shadowSide),void 0!==t.opacity&&(this.opacity=t.opacity),void 0!==t.transparent&&(this.transparent=t.transparent),void 0!==t.alphaTest&&(this.alphaTest=t.alphaTest),void 0!==t.alphaHash&&(this.alphaHash=t.alphaHash),void 0!==t.depthFunc&&(this.depthFunc=t.depthFunc),void 0!==t.depthTest&&(this.depthTest=t.depthTest),void 0!==t.depthWrite&&(this.depthWrite=t.depthWrite),void 0!==t.colorWrite&&(this.colorWrite=t.colorWrite),void 0!==t.blendSrc&&(this.blendSrc=t.blendSrc),void 0!==t.blendDst&&(this.blendDst=t.blendDst),void 0!==t.blendEquation&&(this.blendEquation=t.blendEquation),void 0!==t.blendSrcAlpha&&(this.blendSrcAlpha=t.blendSrcAlpha),void 0!==t.blendDstAlpha&&(this.blendDstAlpha=t.blendDstAlpha),void 0!==t.blendEquationAlpha&&(this.blendEquationAlpha=t.blendEquationAlpha),void 0!==t.blendColor&&void 0!==this.blendColor&&this.blendColor.setHex(t.blendColor),void 0!==t.blendAlpha&&(this.blendAlpha=t.blendAlpha),void 0!==t.stencilWriteMask&&(this.stencilWriteMask=t.stencilWriteMask),void 0!==t.stencilFunc&&(this.stencilFunc=t.stencilFunc),void 0!==t.stencilRef&&(this.stencilRef=t.stencilRef),void 0!==t.stencilFuncMask&&(this.stencilFuncMask=t.stencilFuncMask),void 0!==t.stencilFail&&(this.stencilFail=t.stencilFail),void 0!==t.stencilZFail&&(this.stencilZFail=t.stencilZFail),void 0!==t.stencilZPass&&(this.stencilZPass=t.stencilZPass),void 0!==t.stencilWrite&&(this.stencilWrite=t.stencilWrite),void 0!==t.wireframe&&(this.wireframe=t.wireframe),void 0!==t.wireframeLinewidth&&(this.wireframeLinewidth=t.wireframeLinewidth),void 0!==t.wireframeLinecap&&(this.wireframeLinecap=t.wireframeLinecap),void 0!==t.wireframeLinejoin&&(this.wireframeLinejoin=t.wireframeLinejoin),void 0!==t.rotation&&(this.rotation=t.rotation),void 0!==t.linewidth&&(this.linewidth=t.linewidth),void 0!==t.dashSize&&(this.dashSize=t.dashSize),void 0!==t.gapSize&&(this.gapSize=t.gapSize),void 0!==t.scale&&(this.scale=t.scale),void 0!==t.polygonOffset&&(this.polygonOffset=t.polygonOffset),void 0!==t.polygonOffsetFactor&&(this.polygonOffsetFactor=t.polygonOffsetFactor),void 0!==t.polygonOffsetUnits&&(this.polygonOffsetUnits=t.polygonOffsetUnits),void 0!==t.dithering&&(this.dithering=t.dithering),void 0!==t.alphaToCoverage&&(this.alphaToCoverage=t.alphaToCoverage),void 0!==t.premultipliedAlpha&&(this.premultipliedAlpha=t.premultipliedAlpha),void 0!==t.forceSinglePass&&(this.forceSinglePass=t.forceSinglePass),void 0!==t.allowOverride&&(this.allowOverride=t.allowOverride),void 0!==t.visible&&(this.visible=t.visible),void 0!==t.toneMapped&&(this.toneMapped=t.toneMapped),void 0!==t.userData&&(this.userData=t.userData),void 0!==t.vertexColors&&("number"==typeof t.vertexColors?this.vertexColors=t.vertexColors>0:this.vertexColors=t.vertexColors),void 0!==t.size&&(this.size=t.size),void 0!==t.sizeAttenuation&&(this.sizeAttenuation=t.sizeAttenuation),void 0!==t.map&&(this.map=e[t.map]||null),void 0!==t.matcap&&(this.matcap=e[t.matcap]||null),void 0!==t.alphaMap&&(this.alphaMap=e[t.alphaMap]||null),void 0!==t.bumpMap&&(this.bumpMap=e[t.bumpMap]||null),void 0!==t.bumpScale&&(this.bumpScale=t.bumpScale),void 0!==t.normalMap&&(this.normalMap=e[t.normalMap]||null),void 0!==t.normalMapType&&(this.normalMapType=t.normalMapType),void 0!==t.normalScale){let e=t.normalScale;!1===Array.isArray(e)&&(e=[e,e]),this.normalScale=(new _i).fromArray(e)}return void 0!==t.displacementMap&&(this.displacementMap=e[t.displacementMap]||null),void 0!==t.displacementScale&&(this.displacementScale=t.displacementScale),void 0!==t.displacementBias&&(this.displacementBias=t.displacementBias),void 0!==t.roughnessMap&&(this.roughnessMap=e[t.roughnessMap]||null),void 0!==t.metalnessMap&&(this.metalnessMap=e[t.metalnessMap]||null),void 0!==t.emissiveMap&&(this.emissiveMap=e[t.emissiveMap]||null),void 0!==t.emissiveIntensity&&(this.emissiveIntensity=t.emissiveIntensity),void 0!==t.specularMap&&(this.specularMap=e[t.specularMap]||null),void 0!==t.specularIntensityMap&&(this.specularIntensityMap=e[t.specularIntensityMap]||null),void 0!==t.specularColorMap&&(this.specularColorMap=e[t.specularColorMap]||null),void 0!==t.envMap&&(this.envMap=e[t.envMap]||null),void 0!==t.envMapRotation&&this.envMapRotation.fromArray(t.envMapRotation),void 0!==t.envMapIntensity&&(this.envMapIntensity=t.envMapIntensity),void 0!==t.reflectivity&&(this.reflectivity=t.reflectivity),void 0!==t.refractionRatio&&(this.refractionRatio=t.refractionRatio),void 0!==t.lightMap&&(this.lightMap=e[t.lightMap]||null),void 0!==t.lightMapIntensity&&(this.lightMapIntensity=t.lightMapIntensity),void 0!==t.aoMap&&(this.aoMap=e[t.aoMap]||null),void 0!==t.aoMapIntensity&&(this.aoMapIntensity=t.aoMapIntensity),void 0!==t.gradientMap&&(this.gradientMap=e[t.gradientMap]||null),void 0!==t.clearcoatMap&&(this.clearcoatMap=e[t.clearcoatMap]||null),void 0!==t.clearcoatRoughnessMap&&(this.clearcoatRoughnessMap=e[t.clearcoatRoughnessMap]||null),void 0!==t.clearcoatNormalMap&&(this.clearcoatNormalMap=e[t.clearcoatNormalMap]||null),void 0!==t.clearcoatNormalScale&&(this.clearcoatNormalScale=(new _i).fromArray(t.clearcoatNormalScale)),void 0!==t.iridescenceMap&&(this.iridescenceMap=e[t.iridescenceMap]||null),void 0!==t.iridescenceThicknessMap&&(this.iridescenceThicknessMap=e[t.iridescenceThicknessMap]||null),void 0!==t.transmissionMap&&(this.transmissionMap=e[t.transmissionMap]||null),void 0!==t.thicknessMap&&(this.thicknessMap=e[t.thicknessMap]||null),void 0!==t.anisotropyMap&&(this.anisotropyMap=e[t.anisotropyMap]||null),void 0!==t.sheenColorMap&&(this.sheenColorMap=e[t.sheenColorMap]||null),void 0!==t.sheenRoughnessMap&&(this.sheenRoughnessMap=e[t.sheenRoughnessMap]||null),this}clone(){return(new this.constructor).copy(this)}copy(t){this.name=t.name,this.blending=t.blending,this.side=t.side,this.vertexColors=t.vertexColors,this.opacity=t.opacity,this.transparent=t.transparent,this.blendSrc=t.blendSrc,this.blendDst=t.blendDst,this.blendEquation=t.blendEquation,this.blendSrcAlpha=t.blendSrcAlpha,this.blendDstAlpha=t.blendDstAlpha,this.blendEquationAlpha=t.blendEquationAlpha,this.blendColor.copy(t.blendColor),this.blendAlpha=t.blendAlpha,this.depthFunc=t.depthFunc,this.depthTest=t.depthTest,this.depthWrite=t.depthWrite,this.stencilWriteMask=t.stencilWriteMask,this.stencilFunc=t.stencilFunc,this.stencilRef=t.stencilRef,this.stencilFuncMask=t.stencilFuncMask,this.stencilFail=t.stencilFail,this.stencilZFail=t.stencilZFail,this.stencilZPass=t.stencilZPass,this.stencilWrite=t.stencilWrite;const e=t.clippingPlanes;let s=null;if(null!==e){const t=e.length;s=new Array(t);for(let i=0;i!==t;++i)s[i]=e[i].clone()}return this.clippingPlanes=s,this.clipIntersection=t.clipIntersection,this.clipShadows=t.clipShadows,this.shadowSide=t.shadowSide,this.colorWrite=t.colorWrite,this.precision=t.precision,this.polygonOffset=t.polygonOffset,this.polygonOffsetFactor=t.polygonOffsetFactor,this.polygonOffsetUnits=t.polygonOffsetUnits,this.dithering=t.dithering,this.alphaTest=t.alphaTest,this.alphaHash=t.alphaHash,this.alphaToCoverage=t.alphaToCoverage,this.premultipliedAlpha=t.premultipliedAlpha,this.forceSinglePass=t.forceSinglePass,this.allowOverride=t.allowOverride,this.visible=t.visible,this.toneMapped=t.toneMapped,this.userData=JSON.parse(JSON.stringify(t.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(t){!0===t&&this.version++}}class Gn extends Zn{constructor(t){super(),this.isSpriteMaterial=!0,this.type="SpriteMaterial",this.color=new Pr(16777215),this.map=null,this.alphaMap=null,this.rotation=0,this.sizeAttenuation=!0,this.transparent=!0,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.alphaMap=t.alphaMap,this.rotation=t.rotation,this.sizeAttenuation=t.sizeAttenuation,this.fog=t.fog,this}}const $n=new Ti,Qn=new Ti,Kn=new Ti,ta=new _i,ea=new _i,sa=new Qi,ia=new Ti,ra=new Ti,na=new Ti,aa=new _i,oa=new _i,ha=new _i;class la extends Ar{constructor(t=new Gn){if(super(),this.isSprite=!0,this.type="Sprite",void 0===Xn){Xn=new Wn;const t=new Float32Array([-.5,-.5,0,0,0,.5,-.5,0,1,0,.5,.5,0,1,1,-.5,.5,0,0,1]),e=new Jn(t,5);Xn.setIndex([0,1,2,0,2,3]),Xn.setAttribute("position",new Hn(e,3,0,!1)),Xn.setAttribute("uv",new Hn(e,2,3,!1))}this.geometry=Xn,this.material=t,this.center=new _i(.5,.5),this.count=1}raycast(t,e){null===t.camera&&oi('Sprite: "Raycaster.camera" needs to be set in order to raycast against sprites.'),Qn.setFromMatrixScale(this.matrixWorld),sa.copy(t.camera.matrixWorld),this.modelViewMatrix.multiplyMatrices(t.camera.matrixWorldInverse,this.matrixWorld),Kn.setFromMatrixPosition(this.modelViewMatrix),t.camera.isPerspectiveCamera&&!1===this.material.sizeAttenuation&&Qn.multiplyScalar(-Kn.z);const s=this.material.rotation;let i,r;0!==s&&(r=Math.cos(s),i=Math.sin(s));const n=this.center;ca(ia.set(-.5,-.5,0),Kn,n,Qn,i,r),ca(ra.set(.5,-.5,0),Kn,n,Qn,i,r),ca(na.set(.5,.5,0),Kn,n,Qn,i,r),aa.set(0,0),oa.set(1,0),ha.set(1,1);let a=t.ray.intersectTriangle(ia,ra,na,!1,$n);if(null===a&&(ca(ra.set(-.5,.5,0),Kn,n,Qn,i,r),oa.set(0,1),a=t.ray.intersectTriangle(ia,na,ra,!1,$n),null===a))return;const o=t.ray.origin.distanceTo($n);ot.far||e.push({distance:o,point:$n.clone(),uv:$r.getInterpolation($n,ia,ra,na,aa,oa,ha,new _i),face:null,object:this})}copy(t,e){return super.copy(t,e),void 0!==t.center&&this.center.copy(t.center),this.material=t.material,this}}function ca(t,e,s,i,r,n){ta.subVectors(t,s).addScalar(.5).multiply(i),void 0!==r?(ea.x=n*ta.x-r*ta.y,ea.y=r*ta.x+n*ta.y):ea.copy(ta),t.copy(e),t.x+=ea.x,t.y+=ea.y,t.applyMatrix4(sa)}const ua=new Ti,da=new Ti;class pa extends Ar{constructor(){super(),this.isLOD=!0,this._currentLevel=0,this.type="LOD",Object.defineProperties(this,{levels:{enumerable:!0,value:[]}}),this.autoUpdate=!0}copy(t){super.copy(t,!1);const e=t.levels;for(let t=0,s=e.length;t0){let s,i;for(s=1,i=e.length;s0){ua.setFromMatrixPosition(this.matrixWorld);const s=t.ray.origin.distanceTo(ua);this.getObjectForDistance(s).raycast(t,e)}}update(t){const e=this.levels;if(e.length>1){ua.setFromMatrixPosition(t.matrixWorld),da.setFromMatrixPosition(this.matrixWorld);const s=ua.distanceTo(da)/t.zoom;let i,r;for(e[0].object.visible=!0,i=1,r=e.length;i=t))break;e[i-1].object.visible=!1,e[i].object.visible=!0}for(this._currentLevel=i-1;i0)if(c=n*o-a,u=n*a-o,p=r*l,c>=0)if(u>=-p)if(u<=p){const t=1/l;c*=t,u*=t,d=c*(c+n*u+2*a)+u*(n*c+u+2*o)+h}else u=r,c=Math.max(0,-(n*u+a)),d=-c*c+u*(u+2*o)+h;else u=-r,c=Math.max(0,-(n*u+a)),d=-c*c+u*(u+2*o)+h;else u<=-p?(c=Math.max(0,-(-n*r+a)),u=c>0?-r:Math.min(Math.max(-r,-o),r),d=-c*c+u*(u+2*o)+h):u<=p?(c=0,u=Math.min(Math.max(-r,-o),r),d=u*(u+2*o)+h):(c=Math.max(0,-(n*r+a)),u=c>0?r:Math.min(Math.max(-r,-o),r),d=-c*c+u*(u+2*o)+h);else u=n>0?-r:r,c=Math.max(0,-(n*u+a)),d=-c*c+u*(u+2*o)+h;return s&&s.copy(this.origin).addScaledVector(this.direction,c),i&&i.copy(ya).addScaledVector(ga,u),d}intersectSphere(t,e){ma.subVectors(t.center,this.origin);const s=ma.dot(this.direction),i=ma.dot(ma)-s*s,r=t.radius*t.radius;if(i>r)return null;const n=Math.sqrt(r-i),a=s-n,o=s+n;return o<0?null:a<0?this.at(o,e):this.at(a,e)}intersectsSphere(t){return!(t.radius<0)&&this.distanceSqToPoint(t.center)<=t.radius*t.radius}distanceToPlane(t){const e=t.normal.dot(this.direction);if(0===e)return 0===t.distanceToPoint(this.origin)?0:null;const s=-(this.origin.dot(t.normal)+t.constant)/e;return s>=0?s:null}intersectPlane(t,e){const s=this.distanceToPlane(t);return null===s?null:this.at(s,e)}intersectsPlane(t){const e=t.distanceToPoint(this.origin);if(0===e)return!0;return t.normal.dot(this.direction)*e<0}intersectBox(t,e){let s,i,r,n,a,o;const h=1/this.direction.x,l=1/this.direction.y,c=1/this.direction.z,u=this.origin;return h>=0?(s=(t.min.x-u.x)*h,i=(t.max.x-u.x)*h):(s=(t.max.x-u.x)*h,i=(t.min.x-u.x)*h),l>=0?(r=(t.min.y-u.y)*l,n=(t.max.y-u.y)*l):(r=(t.max.y-u.y)*l,n=(t.min.y-u.y)*l),s>n||r>i?null:((r>s||isNaN(s))&&(s=r),(n=0?(a=(t.min.z-u.z)*c,o=(t.max.z-u.z)*c):(a=(t.max.z-u.z)*c,o=(t.min.z-u.z)*c),s>o||a>i?null:((a>s||s!=s)&&(s=a),(o=0?s:i,e)))}intersectsBox(t){return null!==this.intersectBox(t,ma)}intersectTriangle(t,e,s,i,r){xa.subVectors(e,t),ba.subVectors(s,t),va.crossVectors(xa,ba);let n,a=this.direction.dot(va);if(a>0){if(i)return null;n=1}else{if(!(a<0))return null;n=-1,a=-a}fa.subVectors(this.origin,t);const o=n*this.direction.dot(ba.crossVectors(fa,ba));if(o<0)return null;const h=n*this.direction.dot(xa.cross(fa));if(h<0)return null;if(o+h>a)return null;const l=-n*fa.dot(va);return l<0?null:this.at(l/a,r)}applyMatrix4(t){return this.origin.applyMatrix4(t),this.direction.transformDirection(t),this}equals(t){return t.origin.equals(this.origin)&&t.direction.equals(this.direction)}clone(){return(new this.constructor).copy(this)}}class Ma extends Zn{constructor(t){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new Pr(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new hr,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}const Sa=new Qi,_a=new wa,Aa=new En,Ta=new Ti,za=new Ti,Ca=new Ti,Ia=new Ti,Ba=new Ti,ka=new Ti,Oa=new Ti,Pa=new Ti;class Ra extends Ar{constructor(t=new Wn,e=new Ma){super(),this.isMesh=!0,this.type="Mesh",this.geometry=t,this.material=e,this.morphTargetDictionary=void 0,this.morphTargetInfluences=void 0,this.count=1,this.updateMorphTargets()}copy(t,e){return super.copy(t,e),void 0!==t.morphTargetInfluences&&(this.morphTargetInfluences=t.morphTargetInfluences.slice()),void 0!==t.morphTargetDictionary&&(this.morphTargetDictionary=Object.assign({},t.morphTargetDictionary)),this.material=Array.isArray(t.material)?t.material.slice():t.material,this.geometry=t.geometry,this}updateMorphTargets(){const t=this.geometry.morphAttributes,e=Object.keys(t);if(e.length>0){const s=t[e[0]];if(void 0!==s){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=s.length;t(t.far-t.near)**2)return}Sa.copy(r).invert(),_a.copy(t.ray).applyMatrix4(Sa),null!==s.boundingBox&&!1===_a.intersectsBox(s.boundingBox)||this._computeIntersections(t,e,_a)}}_computeIntersections(t,e,s){let i;const r=this.geometry,n=this.material,a=r.index,o=r.attributes.position,h=r.attributes.uv,l=r.attributes.uv1,c=r.attributes.normal,u=r.groups,d=r.drawRange;if(null!==a)if(Array.isArray(n))for(let r=0,o=u.length;rs.far?null:{distance:l,point:Pa.clone(),object:t}}(t,e,s,i,za,Ca,Ia,Oa);if(c){const t=new Ti;$r.getBarycoord(Oa,za,Ca,Ia,t),r&&(c.uv=$r.getInterpolatedAttribute(r,o,h,l,t,new _i)),n&&(c.uv1=$r.getInterpolatedAttribute(n,o,h,l,t,new _i)),a&&(c.normal=$r.getInterpolatedAttribute(a,o,h,l,t,new Ti),c.normal.dot(i.direction)>0&&c.normal.multiplyScalar(-1));const e={a:o,b:h,c:l,normal:new Ti,materialIndex:0};$r.getNormal(za,Ca,Ia,e.normal),c.face=e,c.barycoord=t}return c}const Na=new qi,Va=new qi,La=new qi,Fa=new qi,Da=new Qi,Ua=new Ti,ja=new En,Wa=new Qi,Ja=new wa;class qa extends Ra{constructor(t,e){super(t,e),this.isSkinnedMesh=!0,this.type="SkinnedMesh",this.bindMode=at,this.bindMatrix=new Qi,this.bindMatrixInverse=new Qi,this.boundingBox=null,this.boundingSphere=null}computeBoundingBox(){const t=this.geometry;null===this.boundingBox&&(this.boundingBox=new Qr),this.boundingBox.makeEmpty();const e=t.getAttribute("position");for(let t=0;t1)?null:e.copy(t.start).addScaledVector(i,n)}intersectsLine(t){const e=this.distanceToPoint(t.start),s=this.distanceToPoint(t.end);return e<0&&s>0||s<0&&e>0}intersectsBox(t){return t.intersectsPlane(this)}intersectsSphere(t){return t.intersectsPlane(this)}coplanarPoint(t){return t.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(t,e){const s=e||ho.getNormalMatrix(t),i=this.coplanarPoint(ao).applyMatrix4(t),r=this.normal.applyMatrix3(s).normalize();return this.constant=-i.dot(r),this}translate(t){return this.constant-=t.dot(this.normal),this}equals(t){return t.normal.equals(this.normal)&&t.constant===this.constant}clone(){return(new this.constructor).copy(this)}}const co=new En,uo=new _i(.5,.5),po=new Ti;class mo{constructor(t=new lo,e=new lo,s=new lo,i=new lo,r=new lo,n=new lo){this.planes=[t,e,s,i,r,n]}set(t,e,s,i,r,n){const a=this.planes;return a[0].copy(t),a[1].copy(e),a[2].copy(s),a[3].copy(i),a[4].copy(r),a[5].copy(n),this}copy(t){const e=this.planes;for(let s=0;s<6;s++)e[s].copy(t.planes[s]);return this}setFromProjectionMatrix(t,e=2e3,s=!1){const i=this.planes,r=t.elements,n=r[0],a=r[1],o=r[2],h=r[3],l=r[4],c=r[5],u=r[6],d=r[7],p=r[8],m=r[9],y=r[10],g=r[11],f=r[12],x=r[13],b=r[14],v=r[15];if(i[0].setComponents(h-n,d-l,g-p,v-f).normalize(),i[1].setComponents(h+n,d+l,g+p,v+f).normalize(),i[2].setComponents(h+a,d+c,g+m,v+x).normalize(),i[3].setComponents(h-a,d-c,g-m,v-x).normalize(),s)i[4].setComponents(o,u,y,b).normalize(),i[5].setComponents(h-o,d-u,g-y,v-b).normalize();else if(i[4].setComponents(h-o,d-u,g-y,v-b).normalize(),e===Ws)i[5].setComponents(h+o,d+u,g+y,v+b).normalize();else{if(e!==Js)throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+e);i[5].setComponents(o,u,y,b).normalize()}return this}intersectsObject(t){if(void 0!==t.boundingSphere)null===t.boundingSphere&&t.computeBoundingSphere(),co.copy(t.boundingSphere).applyMatrix4(t.matrixWorld);else{const e=t.geometry;null===e.boundingSphere&&e.computeBoundingSphere(),co.copy(e.boundingSphere).applyMatrix4(t.matrixWorld)}return this.intersectsSphere(co)}intersectsSprite(t){co.center.set(0,0,0);const e=uo.distanceTo(t.center);return co.radius=.7071067811865476+e,co.applyMatrix4(t.matrixWorld),this.intersectsSphere(co)}intersectsSphere(t){const e=this.planes,s=t.center,i=-t.radius;for(let t=0;t<6;t++){if(e[t].distanceToPoint(s)0?t.max.x:t.min.x,po.y=i.normal.y>0?t.max.y:t.min.y,po.z=i.normal.z>0?t.max.z:t.min.z,i.distanceToPoint(po)<0)return!1}return!0}containsPoint(t){const e=this.planes;for(let s=0;s<6;s++)if(e[s].distanceToPoint(t)<0)return!1;return!0}clone(){return(new this.constructor).copy(this)}}const yo=new Qi;class go{constructor(){this.coordinateSystem=Ws,this._frustums=[],this._count=0}setFromArrayCamera(t){const e=t.cameras,s=this._frustums;for(let t=0;t=r.length&&r.push({start:-1,count:-1,z:-1,index:-1});const a=r[this.index];n.push(a),this.index++,a.start=t,a.count=e,a.z=s,a.index=i}reset(){this.list.length=0,this.index=0}}const wo=new Qi,Mo=new Pr(1,1,1),So=new mo,_o=new go,Ao=new Qr,To=new En,zo=new Ti,Co=new Ti,Io=new Ti,Bo=new vo,ko=new Ra,Oo=[];function Po(t,e,s=0){const i=e.itemSize;if(t.isInterleavedBufferAttribute||t.array.constructor!==e.array.constructor){const r=t.count;for(let n=0;n65535?new Uint32Array(i):new Uint16Array(i);e.setIndex(new Mn(t,1))}this._geometryInitialized=!0}}_validateGeometry(t){const e=this.geometry;if(Boolean(t.getIndex())!==Boolean(e.getIndex()))throw new Error('THREE.BatchedMesh: All geometries must consistently have "index".');for(const s in e.attributes){if(!t.hasAttribute(s))throw new Error(`THREE.BatchedMesh: Added geometry missing "${s}". All geometries must have consistent attributes.`);const i=t.getAttribute(s),r=e.getAttribute(s);if(i.itemSize!==r.itemSize||i.normalized!==r.normalized)throw new Error("THREE.BatchedMesh: All attributes must have a consistent itemSize and normalized value.")}}validateInstanceId(t){const e=this._instanceInfo;if(t<0||t>=e.length||!1===e[t].active)throw new Error(`THREE.BatchedMesh: Invalid instanceId ${t}. Instance is either out of range or has been deleted.`)}validateGeometryId(t){const e=this._geometryInfo;if(t<0||t>=e.length||!1===e[t].active)throw new Error(`THREE.BatchedMesh: Invalid geometryId ${t}. Geometry is either out of range or has been deleted.`)}setCustomSort(t){return this.customSort=t,this}computeBoundingBox(){null===this.boundingBox&&(this.boundingBox=new Qr);const t=this.boundingBox,e=this._instanceInfo;t.makeEmpty();for(let s=0,i=e.length;s=this.maxInstanceCount&&0===this._availableInstanceIds.length)throw new Error("THREE.BatchedMesh: Maximum item count reached.");const e={visible:!0,active:!0,geometryIndex:t};let s=null;this._availableInstanceIds.length>0?(this._availableInstanceIds.sort(fo),s=this._availableInstanceIds.shift(),this._instanceInfo[s]=e):(s=this._instanceInfo.length,this._instanceInfo.push(e));const i=this._matricesTexture;wo.identity().toArray(i.image.data,16*s),i.needsUpdate=!0;const r=this._colorsTexture;return r&&(Mo.toArray(r.image.data,4*s),r.needsUpdate=!0),this._visibilityChanged=!0,s}addGeometry(t,e=-1,s=-1){this._initializeGeometry(t),this._validateGeometry(t);const i={vertexStart:-1,vertexCount:-1,reservedVertexCount:-1,indexStart:-1,indexCount:-1,reservedIndexCount:-1,start:-1,count:-1,boundingBox:null,boundingSphere:null,active:!0},r=this._geometryInfo;i.vertexStart=this._nextVertexStart,i.reservedVertexCount=-1===e?t.getAttribute("position").count:e;const n=t.getIndex();if(null!==n&&(i.indexStart=this._nextIndexStart,i.reservedIndexCount=-1===s?n.count:s),-1!==i.indexStart&&i.indexStart+i.reservedIndexCount>this._maxIndexCount||i.vertexStart+i.reservedVertexCount>this._maxVertexCount)throw new Error("THREE.BatchedMesh: Reserved space request exceeds the maximum buffer size.");let a;return this._availableGeometryIds.length>0?(this._availableGeometryIds.sort(fo),a=this._availableGeometryIds.shift(),r[a]=i):(a=this._geometryCount,this._geometryCount++,r.push(i)),this.setGeometryAt(a,t),this._nextIndexStart=i.indexStart+i.reservedIndexCount,this._nextVertexStart=i.vertexStart+i.reservedVertexCount,a}setGeometryAt(t,e){if(t>=this._geometryCount)throw new Error("THREE.BatchedMesh: Maximum geometry count reached.");this._validateGeometry(e);const s=this.geometry,i=null!==s.getIndex(),r=s.getIndex(),n=e.getIndex(),a=this._geometryInfo[t];if(i&&n.count>a.reservedIndexCount||e.attributes.position.count>a.reservedVertexCount)throw new Error("THREE.BatchedMesh: Reserved space not large enough for provided geometry.");const o=a.vertexStart,h=a.reservedVertexCount;a.vertexCount=e.getAttribute("position").count;for(const t in s.attributes){const i=e.getAttribute(t),r=s.getAttribute(t);Po(i,r,o);const n=i.itemSize;for(let t=i.count,e=h;t=e.length||!1===e[t].active)return this;const s=this._instanceInfo;for(let e=0,i=s.length;ee).sort((t,e)=>s[t].vertexStart-s[e].vertexStart),r=this.geometry;for(let n=0,a=s.length;n=this._geometryCount)return null;const s=this.geometry,i=this._geometryInfo[t];if(null===i.boundingBox){const t=new Qr,e=s.index,r=s.attributes.position;for(let s=i.start,n=i.start+i.count;s=this._geometryCount)return null;const s=this.geometry,i=this._geometryInfo[t];if(null===i.boundingSphere){const e=new En;this.getBoundingBoxAt(t,Ao),Ao.getCenter(e.center);const r=s.index,n=s.attributes.position;let a=0;for(let t=i.start,s=i.start+i.count;tt.active);if(Math.max(...s.map(t=>t.vertexStart+t.reservedVertexCount))>t)throw new Error(`THREE.BatchedMesh: Geometry vertex values are being used outside the range ${e}. Cannot shrink further.`);if(this.geometry.index){if(Math.max(...s.map(t=>t.indexStart+t.reservedIndexCount))>e)throw new Error(`THREE.BatchedMesh: Geometry index values are being used outside the range ${e}. Cannot shrink further.`)}const i=this.geometry;i.dispose(),this._maxVertexCount=t,this._maxIndexCount=e,this._geometryInitialized&&(this._geometryInitialized=!1,this.geometry=new Wn,this._initializeGeometry(i));const r=this.geometry;i.index&&Ro(i.index.array,r.index.array);for(const t in i.attributes)Ro(i.attributes[t].array,r.attributes[t].array)}raycast(t,e){const s=this._instanceInfo,i=this._geometryInfo,r=this.matrixWorld,n=this.geometry;ko.material=this.material,ko.geometry.index=n.index,ko.geometry.attributes=n.attributes,null===ko.geometry.boundingBox&&(ko.geometry.boundingBox=new Qr),null===ko.geometry.boundingSphere&&(ko.geometry.boundingSphere=new En);for(let n=0,a=s.length;n({...t,boundingBox:null!==t.boundingBox?t.boundingBox.clone():null,boundingSphere:null!==t.boundingSphere?t.boundingSphere.clone():null})),this._instanceInfo=t._instanceInfo.map(t=>({...t})),this._availableInstanceIds=t._availableInstanceIds.slice(),this._availableGeometryIds=t._availableGeometryIds.slice(),this._nextIndexStart=t._nextIndexStart,this._nextVertexStart=t._nextVertexStart,this._geometryCount=t._geometryCount,this._maxInstanceCount=t._maxInstanceCount,this._maxVertexCount=t._maxVertexCount,this._maxIndexCount=t._maxIndexCount,this._geometryInitialized=t._geometryInitialized,this._multiDrawCounts=t._multiDrawCounts.slice(),this._multiDrawStarts=t._multiDrawStarts.slice(),this._indirectTexture=t._indirectTexture.clone(),this._indirectTexture.image.data=this._indirectTexture.image.data.slice(),this._matricesTexture=t._matricesTexture.clone(),this._matricesTexture.image.data=this._matricesTexture.image.data.slice(),null!==this._colorsTexture&&(this._colorsTexture=t._colorsTexture.clone(),this._colorsTexture.image.data=this._colorsTexture.image.data.slice()),this}dispose(){this.geometry.dispose(),this._matricesTexture.dispose(),this._matricesTexture=null,this._indirectTexture.dispose(),this._indirectTexture=null,null!==this._colorsTexture&&(this._colorsTexture.dispose(),this._colorsTexture=null)}onBeforeRender(t,e,s,i,r){if(!this._visibilityChanged&&!this.perObjectFrustumCulled&&!this.sortObjects)return;const n=i.getIndex();let a=null===n?1:n.array.BYTES_PER_ELEMENT,o=1;r.wireframe&&(o=2,a=i.attributes.position.count>65535?4:2);const h=this._instanceInfo,l=this._multiDrawStarts,c=this._multiDrawCounts,u=this._geometryInfo,d=this.perObjectFrustumCulled,p=this._indirectTexture,m=p.image.data,y=s.isArrayCamera?_o:So;d&&(s.isArrayCamera?y.setFromArrayCamera(s):(wo.multiplyMatrices(s.projectionMatrix,s.matrixWorldInverse).multiply(this.matrixWorld),y.setFromProjectionMatrix(wo,s.coordinateSystem,s.reversedDepth)));let g=0;if(this.sortObjects){wo.copy(this.matrixWorld).invert(),zo.setFromMatrixPosition(s.matrixWorld).applyMatrix4(wo),Co.set(0,0,-1).transformDirection(s.matrixWorld).transformDirection(wo);for(let t=0,e=h.length;t0){const s=t[e[0]];if(void 0!==s){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=s.length;ti)return;jo.applyMatrix4(t.matrixWorld);const h=e.ray.origin.distanceTo(jo);return he.far?void 0:{distance:h,point:Wo.clone().applyMatrix4(t.matrixWorld),index:a,face:null,faceIndex:null,barycoord:null,object:t}}const Ho=new Ti,Xo=new Ti;class Yo extends Jo{constructor(t,e){super(t,e),this.isLineSegments=!0,this.type="LineSegments"}computeLineDistances(){const t=this.geometry;if(null===t.index){const e=t.attributes.position,s=[];for(let t=0,i=e.count;t0){const s=t[e[0]];if(void 0!==s){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=s.length;tr.far)return;n.push({distance:h,distanceToRay:Math.sqrt(o),point:s,index:e,face:null,faceIndex:null,barycoord:null,object:a})}}class ih extends Ji{constructor(t,e,s,i,r=1006,n=1006,a,o,h){super(t,e,s,i,r,n,a,o,h),this.isVideoTexture=!0,this.generateMipmaps=!1,this._requestVideoFrameCallbackId=0;const l=this;"requestVideoFrameCallback"in t&&(this._requestVideoFrameCallbackId=t.requestVideoFrameCallback(function e(){l.needsUpdate=!0,l._requestVideoFrameCallbackId=t.requestVideoFrameCallback(e)}))}clone(){return new this.constructor(this.image).copy(this)}update(){const t=this.image;!1==="requestVideoFrameCallback"in t&&t.readyState>=t.HAVE_CURRENT_DATA&&(this.needsUpdate=!0)}dispose(){0!==this._requestVideoFrameCallbackId&&(this.source.data.cancelVideoFrameCallback(this._requestVideoFrameCallbackId),this._requestVideoFrameCallbackId=0),super.dispose()}}class rh extends ih{constructor(t,e,s,i,r,n,a,o){super({},t,e,s,i,r,n,a,o),this.isVideoFrameTexture=!0}update(){}clone(){return(new this.constructor).copy(this)}setFrame(t){this.image=t,this.needsUpdate=!0}}class nh extends Ji{constructor(t,e){super({width:t,height:e}),this.isFramebufferTexture=!0,this.magFilter=ft,this.minFilter=ft,this.generateMipmaps=!1,this.needsUpdate=!0}}class ah extends Ji{constructor(t,e,s,i,r,n,a,o,h,l,c,u){super(null,n,a,o,h,l,i,r,c,u),this.isCompressedTexture=!0,this.image={width:e,height:s},this.mipmaps=t,this.flipY=!1,this.generateMipmaps=!1}}class oh extends ah{constructor(t,e,s,i,r,n){super(t,e,s,r,n),this.isCompressedArrayTexture=!0,this.image.depth=i,this.wrapR=yt,this.layerUpdates=new Set}addLayerUpdate(t){this.layerUpdates.add(t)}clearLayerUpdates(){this.layerUpdates.clear()}}class hh extends ah{constructor(t,e,s){super(void 0,t[0].width,t[0].height,e,s,lt),this.isCompressedCubeTexture=!0,this.isCubeTexture=!0,this.image=t}}class lh extends Ji{constructor(t=[],e=301,s,i,r,n,a,o,h,l){super(t,e,s,i,r,n,a,o,h,l),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(t){this.image=t}}class ch extends Ji{constructor(t,e,s,i,r,n,a,o,h){super(t,e,s,i,r,n,a,o,h),this.isCanvasTexture=!0,this.needsUpdate=!0}}class uh extends Ji{constructor(t,e,s,i,r,n,a,o,h){super(t,e,s,i,r,n,a,o,h),this.isHTMLTexture=!0,this.generateMipmaps=!1,this.needsUpdate=!0;const l=t?t.parentNode:null;null!==l&&"requestPaint"in l&&(l.onpaint=()=>{this.needsUpdate=!0},l.requestPaint())}dispose(){const t=this.image?this.image.parentNode:null;null!==t&&"onpaint"in t&&(t.onpaint=null),super.dispose()}}class dh extends Ji{constructor(t,e,s=1014,i,r,n,a=1003,o=1003,h,l=1026,c=1){if(l!==Wt&&1027!==l)throw new Error("THREE.DepthTexture: format must be either THREE.DepthFormat or THREE.DepthStencilFormat");super({width:t,height:e,depth:c},i,r,n,a,o,l,s,h),this.isDepthTexture=!0,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(t){return super.copy(t),this.source=new Di(Object.assign({},t.image)),this.compareFunction=t.compareFunction,this}toJSON(t){const e=super.toJSON(t);return null!==this.compareFunction&&(e.compareFunction=this.compareFunction),e}}class ph extends dh{constructor(t,e=1014,s=301,i,r,n=1003,a=1003,o,h=1026){const l={width:t,height:t,depth:1},c=[l,l,l,l,l,l];super(t,t,e,s,i,r,n,a,o,h),this.image=c,this.isCubeDepthTexture=!0,this.isCubeTexture=!0}get images(){return this.image}set images(t){this.image=t}}class mh extends Ji{constructor(t=null){super(),this.sourceTexture=t,this.isExternalTexture=!0}copy(t){return super.copy(t),this.sourceTexture=t.sourceTexture,this}}class yh extends Wn{constructor(t=1,e=1,s=1,i=1,r=1,n=1){super(),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:s,widthSegments:i,heightSegments:r,depthSegments:n};const a=this;i=Math.floor(i),r=Math.floor(r),n=Math.floor(n);const o=[],h=[],l=[],c=[];let u=0,d=0;function p(t,e,s,i,r,n,p,m,y,g,f){const x=n/y,b=p/g,v=n/2,w=p/2,M=m/2,S=y+1,_=g+1;let A=0,T=0;const z=new Ti;for(let n=0;n<_;n++){const a=n*b-w;for(let o=0;o0?1:-1,l.push(z.x,z.y,z.z),c.push(o/y),c.push(1-n/g),A+=1}}for(let t=0;t0){const t=(f-1)*m;for(let e=0;e0||0!==i)&&(l.push(n,a,h),x+=3),(e>0||i!==r-1)&&(l.push(a,o,h),x+=3)}h.addGroup(g,x,0),g+=x}(),!1===n&&(t>0&&f(!0),e>0&&f(!1)),this.setIndex(l),this.setAttribute("position",new kn(c,3)),this.setAttribute("normal",new kn(u,3)),this.setAttribute("uv",new kn(d,2))}copy(t){return super.copy(t),this.parameters=Object.assign({},t.parameters),this}static fromJSON(t){return new xh(t.radiusTop,t.radiusBottom,t.height,t.radialSegments,t.heightSegments,t.openEnded,t.thetaStart,t.thetaLength)}}class bh extends xh{constructor(t=1,e=1,s=32,i=1,r=!1,n=0,a=2*Math.PI){super(0,t,e,s,i,r,n,a),this.type="ConeGeometry",this.parameters={radius:t,height:e,radialSegments:s,heightSegments:i,openEnded:r,thetaStart:n,thetaLength:a}}static fromJSON(t){return new bh(t.radius,t.height,t.radialSegments,t.heightSegments,t.openEnded,t.thetaStart,t.thetaLength)}}class vh extends Wn{constructor(t=[],e=[],s=1,i=0){super(),this.type="PolyhedronGeometry",this.parameters={vertices:t,indices:e,radius:s,detail:i};const r=[],n=[];function a(t,e,s,i){const r=i+1,n=[];for(let i=0;i<=r;i++){n[i]=[];const a=t.clone().lerp(s,i/r),o=e.clone().lerp(s,i/r),h=r-i;for(let t=0;t<=h;t++)n[i][t]=0===t&&i===r?a:a.clone().lerp(o,t/h)}for(let t=0;t.9&&a<.1&&(e<.2&&(n[t+0]+=1),s<.2&&(n[t+2]+=1),i<.2&&(n[t+4]+=1))}}()}(),this.setAttribute("position",new kn(r,3)),this.setAttribute("normal",new kn(r.slice(),3)),this.setAttribute("uv",new kn(n,2)),0===i?this.computeVertexNormals():this.normalizeNormals()}copy(t){return super.copy(t),this.parameters=Object.assign({},t.parameters),this}static fromJSON(t){return new vh(t.vertices,t.indices,t.radius,t.detail)}}class wh extends vh{constructor(t=1,e=0){const s=(1+Math.sqrt(5))/2,i=1/s;super([-1,-1,-1,-1,-1,1,-1,1,-1,-1,1,1,1,-1,-1,1,-1,1,1,1,-1,1,1,1,0,-i,-s,0,-i,s,0,i,-s,0,i,s,-i,-s,0,-i,s,0,i,-s,0,i,s,0,-s,0,-i,s,0,-i,-s,0,i,s,0,i],[3,11,7,3,7,15,3,15,13,7,19,17,7,17,6,7,6,15,17,4,8,17,8,10,17,10,6,8,0,16,8,16,2,8,2,10,0,12,1,0,1,18,0,18,16,6,10,2,6,2,13,6,13,15,2,16,18,2,18,3,2,3,13,18,1,9,18,9,11,18,11,3,4,14,12,4,12,0,4,0,8,11,9,5,11,5,19,11,19,7,19,5,14,19,14,4,19,4,17,1,12,14,1,14,5,1,5,9],t,e),this.type="DodecahedronGeometry",this.parameters={radius:t,detail:e}}static fromJSON(t){return new wh(t.radius,t.detail)}}const Mh=new Ti,Sh=new Ti,_h=new Ti,Ah=new $r;class Th extends Wn{constructor(t=null,e=1){if(super(),this.type="EdgesGeometry",this.parameters={geometry:t,thresholdAngle:e},null!==t){const s=4,i=Math.pow(10,s),r=Math.cos(yi*e),n=t.getIndex(),a=t.getAttribute("position"),o=n?n.count:a.count,h=[0,0,0],l=["a","b","c"],c=new Array(3),u={},d=[];for(let t=0;t0)){h=i;break}h=i-1}if(i=h,s[i]===n)return i/(r-1);const l=s[i];return(i+(n-l)/(s[i+1]-l))/(r-1)}getTangent(t,e){const s=1e-4;let i=t-s,r=t+s;i<0&&(i=0),r>1&&(r=1);const n=this.getPoint(i),a=this.getPoint(r),o=e||(n.isVector2?new _i:new Ti);return o.copy(a).sub(n).normalize(),o}getTangentAt(t,e){const s=this.getUtoTmapping(t);return this.getTangent(s,e)}computeFrenetFrames(t,e=!1){const s=new Ti,i=[],r=[],n=[],a=new Ti,o=new Qi;for(let e=0;e<=t;e++){const s=e/t;i[e]=this.getTangentAt(s,new Ti)}r[0]=new Ti,n[0]=new Ti;let h=Number.MAX_VALUE;const l=Math.abs(i[0].x),c=Math.abs(i[0].y),u=Math.abs(i[0].z);l<=h&&(h=l,s.set(1,0,0)),c<=h&&(h=c,s.set(0,1,0)),u<=h&&s.set(0,0,1),a.crossVectors(i[0],s).normalize(),r[0].crossVectors(i[0],a),n[0].crossVectors(i[0],r[0]);for(let e=1;e<=t;e++){if(r[e]=r[e-1].clone(),n[e]=n[e-1].clone(),a.crossVectors(i[e-1],i[e]),a.length()>Number.EPSILON){a.normalize();const t=Math.acos(xi(i[e-1].dot(i[e]),-1,1));r[e].applyMatrix4(o.makeRotationAxis(a,t))}n[e].crossVectors(i[e],r[e])}if(!0===e){let e=Math.acos(xi(r[0].dot(r[t]),-1,1));e/=t,i[0].dot(a.crossVectors(r[0],r[t]))>0&&(e=-e);for(let s=1;s<=t;s++)r[s].applyMatrix4(o.makeRotationAxis(i[s],e*s)),n[s].crossVectors(i[s],r[s])}return{tangents:i,normals:r,binormals:n}}clone(){return(new this.constructor).copy(this)}copy(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}toJSON(){const t={metadata:{version:4.7,type:"Curve",generator:"Curve.toJSON"}};return t.arcLengthDivisions=this.arcLengthDivisions,t.type=this.type,t}fromJSON(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}}class Ch extends zh{constructor(t=0,e=0,s=1,i=1,r=0,n=2*Math.PI,a=!1,o=0){super(),this.isEllipseCurve=!0,this.type="EllipseCurve",this.aX=t,this.aY=e,this.xRadius=s,this.yRadius=i,this.aStartAngle=r,this.aEndAngle=n,this.aClockwise=a,this.aRotation=o}getPoint(t,e=new _i){const s=e,i=2*Math.PI;let r=this.aEndAngle-this.aStartAngle;const n=Math.abs(r)i;)r-=i;r0?0:(Math.floor(Math.abs(h)/r)+1)*r:0===l&&h===r-1&&(h=r-2,l=1),this.closed||h>0?a=i[(h-1)%r]:(Oh.subVectors(i[0],i[1]).add(i[0]),a=Oh);const c=i[h%r],u=i[(h+1)%r];if(this.closed||h+2i.length-2?i.length-1:n+1],c=i[n>i.length-3?i.length-1:n+2];return s.set(Vh(a,o.x,h.x,l.x,c.x),Vh(a,o.y,h.y,l.y,c.y)),s}copy(t){super.copy(t),this.points=[];for(let e=0,s=t.points.length;e=s){const t=i[r]-s,n=this.curves[r],a=n.getLength(),o=0===a?0:1-t/a;return n.getPointAt(o,e)}r++}return null}getLength(){const t=this.getCurveLengths();return t[t.length-1]}updateArcLengths(){this.needsUpdate=!0,this.cacheLengths=null,this.getCurveLengths()}getCurveLengths(){if(this.cacheLengths&&this.cacheLengths.length===this.curves.length)return this.cacheLengths;const t=[];let e=0;for(let s=0,i=this.curves.length;s1&&!e[e.length-1].equals(e[0])&&e.push(e[0]),e}copy(t){super.copy(t),this.curves=[];for(let e=0,s=t.curves.length;e0){const t=h.getPoint(0);t.equals(this.currentPoint)||this.lineTo(t.x,t.y)}this.curves.push(h);const l=h.getPoint(1);return this.currentPoint.copy(l),this}copy(t){return super.copy(t),this.currentPoint.copy(t.currentPoint),this}toJSON(){const t=super.toJSON();return t.currentPoint=this.currentPoint.toArray(),t}fromJSON(t){return super.fromJSON(t),this.currentPoint.fromArray(t.currentPoint),this}}class Gh extends Zh{constructor(t){super(t),this.uuid=fi(),this.type="Shape",this.holes=[]}getPointsHoles(t){const e=[];for(let s=0,i=this.holes.length;s80*s){o=t[0],h=t[1];let e=o,i=h;for(let n=s;ne&&(e=s),r>i&&(i=r)}l=Math.max(e-o,i-h),l=0!==l?32767/l:0}return tl(n,a,s,o,h,l,0),a}function Qh(t,e,s,i,r){let n;if(r===function(t,e,s,i){let r=0;for(let n=e,a=s-i;n0)for(let r=e;r=e;r-=i)n=vl(r/i|0,t[r],t[r+1],n);return n&&ml(n,n.next)&&(wl(n),n=n.next),n}function Kh(t,e){if(!t)return t;e||(e=t);let s,i=t;do{if(s=!1,i.steiner||!ml(i,i.next)&&0!==pl(i.prev,i,i.next))i=i.next;else{if(wl(i),i=e=i.prev,i===i.next)break;s=!0}}while(s||i!==e);return e}function tl(t,e,s,i,r,n,a){if(!t)return;!a&&n&&function(t,e,s,i){let r=t;do{0===r.z&&(r.z=hl(r.x,r.y,e,s,i)),r.prevZ=r.prev,r.nextZ=r.next,r=r.next}while(r!==t);r.prevZ.nextZ=null,r.prevZ=null,function(t){let e,s=1;do{let i,r=t;t=null;let n=null;for(e=0;r;){e++;let a=r,o=0;for(let t=0;t0||h>0&&a;)0!==o&&(0===h||!a||r.z<=a.z)?(i=r,r=r.nextZ,o--):(i=a,a=a.nextZ,h--),n?n.nextZ=i:t=i,i.prevZ=n,n=i;r=a}n.nextZ=null,s*=2}while(e>1)}(r)}(t,i,r,n);let o=t;for(;t.prev!==t.next;){const h=t.prev,l=t.next;if(n?sl(t,i,r,n):el(t))e.push(h.i,t.i,l.i),wl(t),t=l.next,o=l.next;else if((t=l)===o){a?1===a?tl(t=il(Kh(t),e),e,s,i,r,n,2):2===a&&rl(t,e,s,i,r,n):tl(Kh(t),e,s,i,r,n,1);break}}}function el(t){const e=t.prev,s=t,i=t.next;if(pl(e,s,i)>=0)return!1;const r=e.x,n=s.x,a=i.x,o=e.y,h=s.y,l=i.y,c=Math.min(r,n,a),u=Math.min(o,h,l),d=Math.max(r,n,a),p=Math.max(o,h,l);let m=i.next;for(;m!==e;){if(m.x>=c&&m.x<=d&&m.y>=u&&m.y<=p&&ul(r,o,n,h,a,l,m.x,m.y)&&pl(m.prev,m,m.next)>=0)return!1;m=m.next}return!0}function sl(t,e,s,i){const r=t.prev,n=t,a=t.next;if(pl(r,n,a)>=0)return!1;const o=r.x,h=n.x,l=a.x,c=r.y,u=n.y,d=a.y,p=Math.min(o,h,l),m=Math.min(c,u,d),y=Math.max(o,h,l),g=Math.max(c,u,d),f=hl(p,m,e,s,i),x=hl(y,g,e,s,i);let b=t.prevZ,v=t.nextZ;for(;b&&b.z>=f&&v&&v.z<=x;){if(b.x>=p&&b.x<=y&&b.y>=m&&b.y<=g&&b!==r&&b!==a&&ul(o,c,h,u,l,d,b.x,b.y)&&pl(b.prev,b,b.next)>=0)return!1;if(b=b.prevZ,v.x>=p&&v.x<=y&&v.y>=m&&v.y<=g&&v!==r&&v!==a&&ul(o,c,h,u,l,d,v.x,v.y)&&pl(v.prev,v,v.next)>=0)return!1;v=v.nextZ}for(;b&&b.z>=f;){if(b.x>=p&&b.x<=y&&b.y>=m&&b.y<=g&&b!==r&&b!==a&&ul(o,c,h,u,l,d,b.x,b.y)&&pl(b.prev,b,b.next)>=0)return!1;b=b.prevZ}for(;v&&v.z<=x;){if(v.x>=p&&v.x<=y&&v.y>=m&&v.y<=g&&v!==r&&v!==a&&ul(o,c,h,u,l,d,v.x,v.y)&&pl(v.prev,v,v.next)>=0)return!1;v=v.nextZ}return!0}function il(t,e){let s=t;do{const i=s.prev,r=s.next.next;!ml(i,r)&&yl(i,s,s.next,r)&&xl(i,r)&&xl(r,i)&&(e.push(i.i,s.i,r.i),wl(s),wl(s.next),s=t=r),s=s.next}while(s!==t);return Kh(s)}function rl(t,e,s,i,r,n){let a=t;do{let t=a.next.next;for(;t!==a.prev;){if(a.i!==t.i&&dl(a,t)){let o=bl(a,t);return a=Kh(a,a.next),o=Kh(o,o.next),tl(a,e,s,i,r,n,0),void tl(o,e,s,i,r,n,0)}t=t.next}a=a.next}while(a!==t)}function nl(t,e){let s=t.x-e.x;if(0===s&&(s=t.y-e.y,0===s)){s=(t.next.y-t.y)/(t.next.x-t.x)-(e.next.y-e.y)/(e.next.x-e.x)}return s}function al(t,e){const s=function(t,e){let s=e;const i=t.x,r=t.y;let n,a=-1/0;if(ml(t,s))return s;do{if(ml(t,s.next))return s.next;if(r<=s.y&&r>=s.next.y&&s.next.y!==s.y){const t=s.x+(r-s.y)*(s.next.x-s.x)/(s.next.y-s.y);if(t<=i&&t>a&&(a=t,n=s.x=s.x&&s.x>=h&&i!==s.x&&cl(rn.x||s.x===n.x&&ol(n,s)))&&(n=s,c=e)}s=s.next}while(s!==o);return n}(t,e);if(!s)return e;const i=bl(s,t);return Kh(i,i.next),Kh(s,s.next)}function ol(t,e){return pl(t.prev,t,e.prev)<0&&pl(e.next,t,t.next)<0}function hl(t,e,s,i,r){return(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=(t-s)*r|0)|t<<8))|t<<4))|t<<2))|t<<1))|(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=(e-i)*r|0)|e<<8))|e<<4))|e<<2))|e<<1))<<1}function ll(t){let e=t,s=t;do{(e.x=(t-a)*(n-o)&&(t-a)*(i-o)>=(s-a)*(e-o)&&(s-a)*(n-o)>=(r-a)*(i-o)}function ul(t,e,s,i,r,n,a,o){return!(t===a&&e===o)&&cl(t,e,s,i,r,n,a,o)}function dl(t,e){return t.next.i!==e.i&&t.prev.i!==e.i&&!function(t,e){let s=t;do{if(s.i!==t.i&&s.next.i!==t.i&&s.i!==e.i&&s.next.i!==e.i&&yl(s,s.next,t,e))return!0;s=s.next}while(s!==t);return!1}(t,e)&&(xl(t,e)&&xl(e,t)&&function(t,e){let s=t,i=!1;const r=(t.x+e.x)/2,n=(t.y+e.y)/2;do{s.y>n!=s.next.y>n&&s.next.y!==s.y&&r<(s.next.x-s.x)*(n-s.y)/(s.next.y-s.y)+s.x&&(i=!i),s=s.next}while(s!==t);return i}(t,e)&&(pl(t.prev,t,e.prev)||pl(t,e.prev,e))||ml(t,e)&&pl(t.prev,t,t.next)>0&&pl(e.prev,e,e.next)>0)}function pl(t,e,s){return(e.y-t.y)*(s.x-e.x)-(e.x-t.x)*(s.y-e.y)}function ml(t,e){return t.x===e.x&&t.y===e.y}function yl(t,e,s,i){const r=fl(pl(t,e,s)),n=fl(pl(t,e,i)),a=fl(pl(s,i,t)),o=fl(pl(s,i,e));return r!==n&&a!==o||(!(0!==r||!gl(t,s,e))||(!(0!==n||!gl(t,i,e))||(!(0!==a||!gl(s,t,i))||!(0!==o||!gl(s,e,i)))))}function gl(t,e,s){return e.x<=Math.max(t.x,s.x)&&e.x>=Math.min(t.x,s.x)&&e.y<=Math.max(t.y,s.y)&&e.y>=Math.min(t.y,s.y)}function fl(t){return t>0?1:t<0?-1:0}function xl(t,e){return pl(t.prev,t,t.next)<0?pl(t,e,t.next)>=0&&pl(t,t.prev,e)>=0:pl(t,e,t.prev)<0||pl(t,t.next,e)<0}function bl(t,e){const s=Ml(t.i,t.x,t.y),i=Ml(e.i,e.x,e.y),r=t.next,n=e.prev;return t.next=e,e.prev=t,s.next=r,r.prev=s,i.next=s,s.prev=i,n.next=i,i.prev=n,i}function vl(t,e,s,i){const r=Ml(t,e,s);return i?(r.next=i.next,r.prev=i,i.next.prev=r,i.next=r):(r.prev=r,r.next=r),r}function wl(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ)}function Ml(t,e,s){return{i:t,x:e,y:s,prev:null,next:null,z:0,prevZ:null,nextZ:null,steiner:!1}}class Sl{static triangulate(t,e,s=2){return $h(t,e,s)}}class _l{static area(t){const e=t.length;let s=0;for(let i=e-1,r=0;r2&&t[e-1].equals(t[0])&&t.pop()}function Tl(t,e){for(let s=0;sNumber.EPSILON){const u=Math.sqrt(c),d=Math.sqrt(h*h+l*l),p=e.x-o/u,m=e.y+a/u,y=((s.x-l/d-p)*l-(s.y+h/d-m)*h)/(a*l-o*h);i=p+a*y-t.x,r=m+o*y-t.y;const g=i*i+r*r;if(g<=2)return new _i(i,r);n=Math.sqrt(g/2)}else{let t=!1;a>Number.EPSILON?h>Number.EPSILON&&(t=!0):a<-Number.EPSILON?h<-Number.EPSILON&&(t=!0):Math.sign(o)===Math.sign(l)&&(t=!0),t?(i=-o,r=a,n=Math.sqrt(c)):(i=a,r=o,n=Math.sqrt(c/2))}return new _i(i/n,r/n)}const k=[];for(let t=0,e=z.length,s=e-1,i=t+1;t=0;t--){const e=t/p,s=c*Math.cos(e*Math.PI/2),i=u*Math.sin(e*Math.PI/2)+d;for(let t=0,e=z.length;t=0;){const i=s;let r=s-1;r<0&&(r=t.length-1);for(let t=0,s=o+2*p;t0)&&d.push(e,r,h),(t!==s-1||o0&&(e.defines=this.defines),e.vertexShader=this.vertexShader,e.fragmentShader=this.fragmentShader,e.lights=this.lights,e.clipping=this.clipping;const s={};for(const t in this.extensions)!0===this.extensions[t]&&(s[t]=!0);return Object.keys(s).length>0&&(e.extensions=s),e}fromJSON(t,e){if(super.fromJSON(t,e),void 0!==t.uniforms)for(const s in t.uniforms){const i=t.uniforms[s];switch(this.uniforms[s]={},i.type){case"t":this.uniforms[s].value=e[i.value]||null;break;case"c":this.uniforms[s].value=(new Pr).setHex(i.value);break;case"v2":this.uniforms[s].value=(new _i).fromArray(i.value);break;case"v3":this.uniforms[s].value=(new Ti).fromArray(i.value);break;case"v4":this.uniforms[s].value=(new qi).fromArray(i.value);break;case"m3":this.uniforms[s].value=(new Ii).fromArray(i.value);break;case"m4":this.uniforms[s].value=(new Qi).fromArray(i.value);break;default:this.uniforms[s].value=i.value}}if(void 0!==t.defines&&(this.defines=t.defines),void 0!==t.vertexShader&&(this.vertexShader=t.vertexShader),void 0!==t.fragmentShader&&(this.fragmentShader=t.fragmentShader),void 0!==t.glslVersion&&(this.glslVersion=t.glslVersion),void 0!==t.extensions)for(const e in t.extensions)this.extensions[e]=t.extensions[e];return void 0!==t.lights&&(this.lights=t.lights),void 0!==t.clipping&&(this.clipping=t.clipping),this}}class Gl extends Zl{constructor(t){super(t),this.isRawShaderMaterial=!0,this.type="RawShaderMaterial"}}class $l extends Zn{constructor(t){super(),this.isMeshStandardMaterial=!0,this.type="MeshStandardMaterial",this.defines={STANDARD:""},this.color=new Pr(16777215),this.roughness=1,this.metalness=0,this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Pr(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new _i(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.roughnessMap=null,this.metalnessMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new hr,this.envMapIntensity=1,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.defines={STANDARD:""},this.color.copy(t.color),this.roughness=t.roughness,this.metalness=t.metalness,this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.roughnessMap=t.roughnessMap,this.metalnessMap=t.metalnessMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.envMapIntensity=t.envMapIntensity,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.flatShading=t.flatShading,this.fog=t.fog,this}}class Ql extends $l{constructor(t){super(),this.isMeshPhysicalMaterial=!0,this.defines={STANDARD:"",PHYSICAL:""},this.type="MeshPhysicalMaterial",this.anisotropyRotation=0,this.anisotropyMap=null,this.clearcoatMap=null,this.clearcoatRoughness=0,this.clearcoatRoughnessMap=null,this.clearcoatNormalScale=new _i(1,1),this.clearcoatNormalMap=null,this.ior=1.5,Object.defineProperty(this,"reflectivity",{get:function(){return xi(2.5*(this.ior-1)/(this.ior+1),0,1)},set:function(t){this.ior=(1+.4*t)/(1-.4*t)}}),this.iridescenceMap=null,this.iridescenceIOR=1.3,this.iridescenceThicknessRange=[100,400],this.iridescenceThicknessMap=null,this.sheenColor=new Pr(0),this.sheenColorMap=null,this.sheenRoughness=1,this.sheenRoughnessMap=null,this.transmissionMap=null,this.thickness=0,this.thicknessMap=null,this.attenuationDistance=1/0,this.attenuationColor=new Pr(1,1,1),this.specularIntensity=1,this.specularIntensityMap=null,this.specularColor=new Pr(1,1,1),this.specularColorMap=null,this._anisotropy=0,this._clearcoat=0,this._dispersion=0,this._iridescence=0,this._sheen=0,this._transmission=0,this.setValues(t)}get anisotropy(){return this._anisotropy}set anisotropy(t){this._anisotropy>0!=t>0&&this.version++,this._anisotropy=t}get clearcoat(){return this._clearcoat}set clearcoat(t){this._clearcoat>0!=t>0&&this.version++,this._clearcoat=t}get iridescence(){return this._iridescence}set iridescence(t){this._iridescence>0!=t>0&&this.version++,this._iridescence=t}get dispersion(){return this._dispersion}set dispersion(t){this._dispersion>0!=t>0&&this.version++,this._dispersion=t}get sheen(){return this._sheen}set sheen(t){this._sheen>0!=t>0&&this.version++,this._sheen=t}get transmission(){return this._transmission}set transmission(t){this._transmission>0!=t>0&&this.version++,this._transmission=t}copy(t){return super.copy(t),this.defines={STANDARD:"",PHYSICAL:""},this.anisotropy=t.anisotropy,this.anisotropyRotation=t.anisotropyRotation,this.anisotropyMap=t.anisotropyMap,this.clearcoat=t.clearcoat,this.clearcoatMap=t.clearcoatMap,this.clearcoatRoughness=t.clearcoatRoughness,this.clearcoatRoughnessMap=t.clearcoatRoughnessMap,this.clearcoatNormalMap=t.clearcoatNormalMap,this.clearcoatNormalScale.copy(t.clearcoatNormalScale),this.dispersion=t.dispersion,this.ior=t.ior,this.iridescence=t.iridescence,this.iridescenceMap=t.iridescenceMap,this.iridescenceIOR=t.iridescenceIOR,this.iridescenceThicknessRange=[...t.iridescenceThicknessRange],this.iridescenceThicknessMap=t.iridescenceThicknessMap,this.sheen=t.sheen,this.sheenColor.copy(t.sheenColor),this.sheenColorMap=t.sheenColorMap,this.sheenRoughness=t.sheenRoughness,this.sheenRoughnessMap=t.sheenRoughnessMap,this.transmission=t.transmission,this.transmissionMap=t.transmissionMap,this.thickness=t.thickness,this.thicknessMap=t.thicknessMap,this.attenuationDistance=t.attenuationDistance,this.attenuationColor.copy(t.attenuationColor),this.specularIntensity=t.specularIntensity,this.specularIntensityMap=t.specularIntensityMap,this.specularColor.copy(t.specularColor),this.specularColorMap=t.specularColorMap,this}}class Kl extends Zn{constructor(t){super(),this.isMeshPhongMaterial=!0,this.type="MeshPhongMaterial",this.color=new Pr(16777215),this.specular=new Pr(1118481),this.shininess=30,this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Pr(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new _i(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new hr,this.combine=0,this.reflectivity=1,this.envMapIntensity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.specular.copy(t.specular),this.shininess=t.shininess,this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.envMapIntensity=t.envMapIntensity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.flatShading=t.flatShading,this.fog=t.fog,this}}class tc extends Zn{constructor(t){super(),this.isMeshToonMaterial=!0,this.defines={TOON:""},this.type="MeshToonMaterial",this.color=new Pr(16777215),this.map=null,this.gradientMap=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Pr(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new _i(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.gradientMap=t.gradientMap,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.alphaMap=t.alphaMap,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}class ec extends Zn{constructor(t){super(),this.isMeshNormalMaterial=!0,this.type="MeshNormalMaterial",this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new _i(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.flatShading=!1,this.setValues(t)}copy(t){return super.copy(t),this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.flatShading=t.flatShading,this}}class sc extends Zn{constructor(t){super(),this.isMeshLambertMaterial=!0,this.type="MeshLambertMaterial",this.color=new Pr(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Pr(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new _i(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new hr,this.combine=0,this.reflectivity=1,this.envMapIntensity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.envMapIntensity=t.envMapIntensity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.flatShading=t.flatShading,this.fog=t.fog,this}}class ic extends Zn{constructor(t){super(),this.isMeshDepthMaterial=!0,this.type="MeshDepthMaterial",this.depthPacking=3200,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.setValues(t)}copy(t){return super.copy(t),this.depthPacking=t.depthPacking,this.map=t.map,this.alphaMap=t.alphaMap,this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this}}class rc extends Zn{constructor(t){super(),this.isMeshDistanceMaterial=!0,this.type="MeshDistanceMaterial",this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.setValues(t)}copy(t){return super.copy(t),this.map=t.map,this.alphaMap=t.alphaMap,this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this}}class nc extends Zn{constructor(t){super(),this.isMeshMatcapMaterial=!0,this.defines={MATCAP:""},this.type="MeshMatcapMaterial",this.color=new Pr(16777215),this.matcap=null,this.map=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new _i(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.wireframe=!1,this.wireframeLinewidth=1,this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.defines={MATCAP:""},this.color.copy(t.color),this.matcap=t.matcap,this.map=t.map,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.alphaMap=t.alphaMap,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.flatShading=t.flatShading,this.fog=t.fog,this}}class ac extends No{constructor(t){super(),this.isLineDashedMaterial=!0,this.type="LineDashedMaterial",this.scale=1,this.dashSize=3,this.gapSize=1,this.setValues(t)}copy(t){return super.copy(t),this.scale=t.scale,this.dashSize=t.dashSize,this.gapSize=t.gapSize,this}}function oc(t,e){return t&&t.constructor!==e?"number"==typeof e.BYTES_PER_ELEMENT?new e(t):Array.prototype.slice.call(t):t}function hc(t){const e=t.length,s=new Array(e);for(let t=0;t!==e;++t)s[t]=t;return s.sort(function(e,s){return t[e]-t[s]}),s}function lc(t,e,s){const i=t.length,r=new t.constructor(i);for(let n=0,a=0;a!==i;++n){const i=s[n]*e;for(let s=0;s!==e;++s)r[a++]=t[i+s]}return r}function cc(t,e,s,i){let r=1,n=t[0];for(;void 0!==n&&void 0===n[i];)n=t[r++];if(void 0===n)return;let a=n[i];if(void 0!==a)if(Array.isArray(a))do{a=n[i],void 0!==a&&(e.push(n.time),s.push(...a)),n=t[r++]}while(void 0!==n);else if(void 0!==a.toArray)do{a=n[i],void 0!==a&&(e.push(n.time),a.toArray(s,s.length)),n=t[r++]}while(void 0!==n);else do{a=n[i],void 0!==a&&(e.push(n.time),s.push(a)),n=t[r++]}while(void 0!==n)}class uc{static convertArray(t,e){return oc(t,e)}static isTypedArray(t){return $s(t)}static getKeyframeOrder(t){return hc(t)}static sortedArray(t,e,s){return lc(t,e,s)}static flattenJSON(t,e,s,i){cc(t,e,s,i)}static subclip(t,e,s,i,r=30){return function(t,e,s,i,r=30){const n=t.clone();n.name=e;const a=[];for(let t=0;t=i)){h.push(e.times[t]);for(let s=0;sn.tracks[t].times[0]&&(o=n.tracks[t].times[0]);for(let t=0;t=i.times[u]){const t=u*h+o,e=t+h-o;d=i.values.slice(t,e)}else{const t=i.createInterpolant(),e=o,s=h-o;t.evaluate(n),d=t.resultBuffer.slice(e,s)}"quaternion"===r&&(new Ai).fromArray(d).normalize().conjugate().toArray(d);const p=a.times.length;for(let t=0;t=r)){const a=e[1];t=r)break e}n=s,s=0;break s}break t}for(;s>>1;te;)--n;if(++n,0!==r||n!==i){r>=n&&(n=Math.max(n,1),r=n-1);const t=this.getValueSize();this.times=s.slice(r,n),this.values=this.values.slice(r*t,n*t)}return this}validate(){let t=!0;const e=this.getValueSize();e-Math.floor(e)!==0&&(oi("KeyframeTrack: Invalid value size in track.",this),t=!1);const s=this.times,i=this.values,r=s.length;0===r&&(oi("KeyframeTrack: Track is empty.",this),t=!1);let n=null;for(let e=0;e!==r;e++){const i=s[e];if("number"==typeof i&&isNaN(i)){oi("KeyframeTrack: Time is not a valid number.",this,e,i),t=!1;break}if(null!==n&&n>i){oi("KeyframeTrack: Out of order keys.",this,e,i,n),t=!1;break}n=i}if(void 0!==i&&$s(i))for(let e=0,s=i.length;e!==s;++e){const s=i[e];if(isNaN(s)){oi("KeyframeTrack: Value is not a valid number.",this,e,s),t=!1;break}}return t}optimize(){const t=this.times.slice(),e=this.values.slice(),s=this.getValueSize(),i=this.getInterpolation()===Le,r=t.length-1;let n=1;for(let a=1;a0){t[n]=t[r];for(let t=r*s,i=n*s,a=0;a!==s;++a)e[i+a]=e[t+a];++n}return n!==t.length?(this.times=t.slice(0,n),this.values=e.slice(0,n*s)):(this.times=t,this.values=e),this}clone(){const t=this.times.slice(),e=this.values.slice(),s=new(0,this.constructor)(this.name,t,e);return s.createInterpolant=this.createInterpolant,s}}fc.prototype.ValueTypeName="",fc.prototype.TimeBufferType=Float32Array,fc.prototype.ValueBufferType=Float32Array,fc.prototype.DefaultInterpolation=Ve;class xc extends fc{constructor(t,e,s){super(t,e,s)}}xc.prototype.ValueTypeName="bool",xc.prototype.ValueBufferType=Array,xc.prototype.DefaultInterpolation=Ne,xc.prototype.InterpolantFactoryMethodLinear=void 0,xc.prototype.InterpolantFactoryMethodSmooth=void 0;class bc extends fc{constructor(t,e,s,i){super(t,e,s,i)}}bc.prototype.ValueTypeName="color";class vc extends fc{constructor(t,e,s,i){super(t,e,s,i)}}vc.prototype.ValueTypeName="number";class wc extends dc{constructor(t,e,s,i){super(t,e,s,i)}interpolate_(t,e,s,i){const r=this.resultBuffer,n=this.sampleValues,a=this.valueSize,o=(s-e)/(i-e);let h=t*a;for(let t=h+a;h!==t;h+=4)Ai.slerpFlat(r,0,n,h-a,n,h,o);return r}}class Mc extends fc{constructor(t,e,s,i){super(t,e,s,i)}InterpolantFactoryMethodLinear(t){return new wc(this.times,this.values,this.getValueSize(),t)}}Mc.prototype.ValueTypeName="quaternion",Mc.prototype.InterpolantFactoryMethodSmooth=void 0;class Sc extends fc{constructor(t,e,s){super(t,e,s)}}Sc.prototype.ValueTypeName="string",Sc.prototype.ValueBufferType=Array,Sc.prototype.DefaultInterpolation=Ne,Sc.prototype.InterpolantFactoryMethodLinear=void 0,Sc.prototype.InterpolantFactoryMethodSmooth=void 0;class _c extends fc{constructor(t,e,s,i){super(t,e,s,i)}}_c.prototype.ValueTypeName="vector";class Ac{constructor(t="",e=-1,s=[],i=2500){this.name=t,this.tracks=s,this.duration=e,this.blendMode=i,this.uuid=fi(),this.userData={},this.duration<0&&this.resetDuration()}static parse(t){const e=[],s=t.tracks,i=1/(t.fps||1);for(let t=0,r=s.length;t!==r;++t)e.push(Tc(s[t]).scale(i));const r=new this(t.name,t.duration,e,t.blendMode);return r.uuid=t.uuid,r.userData=JSON.parse(t.userData||"{}"),r}static toJSON(t){const e=[],s=t.tracks,i={name:t.name,duration:t.duration,tracks:e,uuid:t.uuid,blendMode:t.blendMode,userData:JSON.stringify(t.userData)};for(let t=0,i=s.length;t!==i;++t)e.push(fc.toJSON(s[t]));return i}static CreateFromMorphTargetSequence(t,e,s,i){const r=e.length,n=[];for(let t=0;t1){const t=n[1];let e=i[t];e||(i[t]=e=[]),e.push(s)}}const n=[];for(const t in i)n.push(this.CreateFromMorphTargetSequence(t,i[t],e,s));return n}resetDuration(){let t=0;for(let e=0,s=this.tracks.length;e!==s;++e){const s=this.tracks[e];t=Math.max(t,s.times[s.times.length-1])}return this.duration=t,this}trim(){for(let t=0;t{e&&e(r),this.manager.itemEnd(t)},0);if(void 0!==Oc[t])return void Oc[t].push({onLoad:e,onProgress:s,onError:i});Oc[t]=[],Oc[t].push({onLoad:e,onProgress:s,onError:i});const n=new Request(t,{headers:new Headers(this.requestHeader),credentials:this.withCredentials?"include":"same-origin",signal:"function"==typeof AbortSignal.any?AbortSignal.any([this._abortController.signal,this.manager.abortController.signal]):this._abortController.signal}),a=this.mimeType,o=this.responseType;fetch(n).then(e=>{if(200===e.status||0===e.status){if(0===e.status&&ai("FileLoader: HTTP Status 0 received."),"undefined"==typeof ReadableStream||void 0===e.body||void 0===e.body.getReader)return e;const s=Oc[t],i=e.body.getReader(),r=e.headers.get("X-File-Size")||e.headers.get("Content-Length"),n=r?parseInt(r):0,a=0!==n;let o=0;const h=new ReadableStream({start(t){!function e(){i.read().then(({done:i,value:r})=>{if(i)t.close();else{o+=r.byteLength;const i=new ProgressEvent("progress",{lengthComputable:a,loaded:o,total:n});for(let t=0,e=s.length;t{t.error(e)})}()}});return new Response(h)}throw new Pc(`fetch for "${e.url}" responded with ${e.status}: ${e.statusText}`,e)}).then(t=>{switch(o){case"arraybuffer":return t.arrayBuffer();case"blob":return t.blob();case"document":return t.text().then(t=>(new DOMParser).parseFromString(t,a));case"json":return t.json();default:if(""===a)return t.text();{const e=/charset="?([^;"\s]*)"?/i.exec(a),s=e&&e[1]?e[1].toLowerCase():void 0,i=new TextDecoder(s);return t.arrayBuffer().then(t=>i.decode(t))}}}).then(e=>{zc.add(`file:${t}`,e);const s=Oc[t];delete Oc[t];for(let t=0,i=s.length;t{const s=Oc[t];if(void 0===s)throw this.manager.itemError(t),e;delete Oc[t];for(let t=0,i=s.length;t{this.manager.itemEnd(t)}),this.manager.itemStart(t)}setResponseType(t){return this.responseType=t,this}setMimeType(t){return this.mimeType=t,this}abort(){return this._abortController.abort(),this._abortController=new AbortController,this}}class Ec extends kc{constructor(t){super(t)}load(t,e,s,i){const r=this,n=new Rc(this.manager);n.setPath(this.path),n.setRequestHeader(this.requestHeader),n.setWithCredentials(this.withCredentials),n.load(t,function(s){try{e(r.parse(JSON.parse(s)))}catch(e){i?i(e):oi(e),r.manager.itemError(t)}},s,i)}parse(t){const e=[];for(let s=0;s0){const s=new Ic(e);r=new Lc(s),r.setCrossOrigin(this.crossOrigin);for(let e=0,s=t.length;e0){i=new Lc(this.manager),i.setCrossOrigin(this.crossOrigin);for(let e=0,i=t.length;e{let e=null,s=null;return void 0!==t.boundingBox&&(e=(new Qr).fromJSON(t.boundingBox)),void 0!==t.boundingSphere&&(s=(new En).fromJSON(t.boundingSphere)),{...t,boundingBox:e,boundingSphere:s}}),n._instanceInfo=t.instanceInfo,n._availableInstanceIds=t._availableInstanceIds,n._availableGeometryIds=t._availableGeometryIds,n._nextIndexStart=t.nextIndexStart,n._nextVertexStart=t.nextVertexStart,n._geometryCount=t.geometryCount,n._maxInstanceCount=t.maxInstanceCount,n._maxVertexCount=t.maxVertexCount,n._maxIndexCount=t.maxIndexCount,n._geometryInitialized=t.geometryInitialized,n._matricesTexture=c(t.matricesTexture.uuid),n._indirectTexture=c(t.indirectTexture.uuid),void 0!==t.colorsTexture&&(n._colorsTexture=c(t.colorsTexture.uuid)),void 0!==t.boundingSphere&&(n.boundingSphere=(new En).fromJSON(t.boundingSphere)),void 0!==t.boundingBox&&(n.boundingBox=(new Qr).fromJSON(t.boundingBox));break;case"LOD":n=new pa;break;case"Line":n=new Jo(h(t.geometry),l(t.material));break;case"LineLoop":n=new Zo(h(t.geometry),l(t.material));break;case"LineSegments":n=new Yo(h(t.geometry),l(t.material));break;case"PointCloud":case"Points":n=new eh(h(t.geometry),l(t.material));break;case"Sprite":n=new la(l(t.material));break;case"Group":n=new Tr;break;case"Bone":n=new Ha;break;default:n=new Ar}if(n.uuid=t.uuid,void 0!==t.name&&(n.name=t.name),void 0!==t.matrix?(n.matrix.fromArray(t.matrix),void 0!==t.matrixAutoUpdate&&(n.matrixAutoUpdate=t.matrixAutoUpdate),n.matrixAutoUpdate&&n.matrix.decompose(n.position,n.quaternion,n.scale)):(void 0!==t.position&&n.position.fromArray(t.position),void 0!==t.rotation&&n.rotation.fromArray(t.rotation),void 0!==t.quaternion&&n.quaternion.fromArray(t.quaternion),void 0!==t.scale&&n.scale.fromArray(t.scale)),void 0!==t.up&&n.up.fromArray(t.up),void 0!==t.pivot&&(n.pivot=(new Ti).fromArray(t.pivot)),void 0!==t.morphTargetDictionary&&(n.morphTargetDictionary=Object.assign({},t.morphTargetDictionary)),void 0!==t.morphTargetInfluences&&(n.morphTargetInfluences=t.morphTargetInfluences.slice()),void 0!==t.castShadow&&(n.castShadow=t.castShadow),void 0!==t.receiveShadow&&(n.receiveShadow=t.receiveShadow),t.shadow&&(void 0!==t.shadow.intensity&&(n.shadow.intensity=t.shadow.intensity),void 0!==t.shadow.bias&&(n.shadow.bias=t.shadow.bias),void 0!==t.shadow.normalBias&&(n.shadow.normalBias=t.shadow.normalBias),void 0!==t.shadow.radius&&(n.shadow.radius=t.shadow.radius),void 0!==t.shadow.mapSize&&n.shadow.mapSize.fromArray(t.shadow.mapSize),void 0!==t.shadow.camera&&(n.shadow.camera=this.parseObject(t.shadow.camera))),void 0!==t.visible&&(n.visible=t.visible),void 0!==t.frustumCulled&&(n.frustumCulled=t.frustumCulled),void 0!==t.renderOrder&&(n.renderOrder=t.renderOrder),void 0!==t.static&&(n.static=t.static),void 0!==t.userData&&(n.userData=t.userData),void 0!==t.layers&&(n.layers.mask=t.layers),void 0!==t.children){const a=t.children;for(let t=0;t{!0===Su.has(n)?(i&&i(Su.get(n)),r.manager.itemError(t),r.manager.itemEnd(t)):(e&&e(s),r.manager.itemEnd(t))}):void setTimeout(function(){e&&e(n),r.manager.itemEnd(t)},0);const a={};a.credentials="anonymous"===this.crossOrigin?"same-origin":"include",a.headers=this.requestHeader,a.signal="function"==typeof AbortSignal.any?AbortSignal.any([this._abortController.signal,this.manager.abortController.signal]):this._abortController.signal;const o=fetch(t,a).then(function(t){return t.blob()}).then(function(t){return createImageBitmap(t,Object.assign(r.options,{colorSpaceConversion:"none"}))}).then(function(s){zc.add(`image-bitmap:${t}`,s),e&&e(s),r.manager.itemEnd(t)}).catch(function(e){i&&i(e),Su.set(o,e),zc.remove(`image-bitmap:${t}`),r.manager.itemError(t),r.manager.itemEnd(t)});zc.add(`image-bitmap:${t}`,o),r.manager.itemStart(t)}abort(){return this._abortController.abort(),this._abortController=new AbortController,this}}let Au;class Tu{static getContext(){return void 0===Au&&(Au=new(window.AudioContext||window.webkitAudioContext)),Au}static setContext(t){Au=t}}class zu extends kc{constructor(t){super(t)}load(t,e,s,i){const r=this,n=new Rc(this.manager);function a(e){i?i(e):oi(e),r.manager.itemError(t)}n.setResponseType("arraybuffer"),n.setPath(this.path),n.setRequestHeader(this.requestHeader),n.setWithCredentials(this.withCredentials),n.load(t,function(s){try{const i=s.slice(0),n=Tu.getContext(),o=t+"#decode";r.manager.itemStart(o),n.decodeAudioData(i,function(t){e(t),r.manager.itemEnd(o)}).catch(function(t){a(t),r.manager.itemEnd(o)})}catch(t){a(t)}},s,i)}}const Cu=new Qi,Iu=new Qi,Bu=new Qi;class ku{constructor(){this.type="StereoCamera",this.aspect=1,this.eyeSep=.064,this.cameraL=new eu,this.cameraL.layers.enable(1),this.cameraL.matrixAutoUpdate=!1,this.cameraR=new eu,this.cameraR.layers.enable(2),this.cameraR.matrixAutoUpdate=!1,this._cache={focus:null,fov:null,aspect:null,near:null,far:null,zoom:null,eyeSep:null}}update(t){const e=this._cache;if(e.focus!==t.focus||e.fov!==t.fov||e.aspect!==t.aspect*this.aspect||e.near!==t.near||e.far!==t.far||e.zoom!==t.zoom||e.eyeSep!==this.eyeSep){e.focus=t.focus,e.fov=t.fov,e.aspect=t.aspect*this.aspect,e.near=t.near,e.far=t.far,e.zoom=t.zoom,e.eyeSep=this.eyeSep,Bu.copy(t.projectionMatrix);const s=e.eyeSep/2,i=s*e.near/e.focus,r=e.near*Math.tan(yi*e.fov*.5)/e.zoom;let n,a;Iu.elements[12]=-s,Cu.elements[12]=s,n=-r*e.aspect+i,a=r*e.aspect+i,Bu.elements[0]=2*e.near/(a-n),Bu.elements[8]=(a+n)/(a-n),this.cameraL.projectionMatrix.copy(Bu),n=-r*e.aspect-i,a=r*e.aspect-i,Bu.elements[0]=2*e.near/(a-n),Bu.elements[8]=(a+n)/(a-n),this.cameraR.projectionMatrix.copy(Bu)}this.cameraL.matrix.copy(t.matrixWorld).multiply(Iu),this.cameraL.matrixWorldNeedsUpdate=!0,this.cameraR.matrix.copy(t.matrixWorld).multiply(Cu),this.cameraR.matrixWorldNeedsUpdate=!0}}const Ou=-90;class Pu extends Ar{constructor(t,e,s){super(),this.type="CubeCamera",this.renderTarget=s,this.coordinateSystem=null,this.activeMipmapLevel=0;const i=new eu(Ou,1,t,e);i.layers=this.layers,this.add(i);const r=new eu(Ou,1,t,e);r.layers=this.layers,this.add(r);const n=new eu(Ou,1,t,e);n.layers=this.layers,this.add(n);const a=new eu(Ou,1,t,e);a.layers=this.layers,this.add(a);const o=new eu(Ou,1,t,e);o.layers=this.layers,this.add(o);const h=new eu(Ou,1,t,e);h.layers=this.layers,this.add(h)}updateCoordinateSystem(){const t=this.coordinateSystem,e=this.children.concat(),[s,i,r,n,a,o]=e;for(const t of e)this.remove(t);if(t===Ws)s.up.set(0,1,0),s.lookAt(1,0,0),i.up.set(0,1,0),i.lookAt(-1,0,0),r.up.set(0,0,-1),r.lookAt(0,1,0),n.up.set(0,0,1),n.lookAt(0,-1,0),a.up.set(0,1,0),a.lookAt(0,0,1),o.up.set(0,1,0),o.lookAt(0,0,-1);else{if(t!==Js)throw new Error("THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: "+t);s.up.set(0,-1,0),s.lookAt(-1,0,0),i.up.set(0,-1,0),i.lookAt(1,0,0),r.up.set(0,0,1),r.lookAt(0,1,0),n.up.set(0,0,-1),n.lookAt(0,-1,0),a.up.set(0,-1,0),a.lookAt(0,0,1),o.up.set(0,-1,0),o.lookAt(0,0,-1)}for(const t of e)this.add(t),t.updateMatrixWorld()}update(t,e){null===this.parent&&this.updateMatrixWorld();const{renderTarget:s,activeMipmapLevel:i}=this;this.coordinateSystem!==t.coordinateSystem&&(this.coordinateSystem=t.coordinateSystem,this.updateCoordinateSystem());const[r,n,a,o,h,l]=this.children,c=t.getRenderTarget(),u=t.getActiveCubeFace(),d=t.getActiveMipmapLevel(),p=t.xr.enabled;t.xr.enabled=!1;const m=s.texture.generateMipmaps;s.texture.generateMipmaps=!1;let y=!1;y=!0===t.isWebGLRenderer?t.state.buffers.depth.getReversed():t.reversedDepthBuffer,t.setRenderTarget(s,0,i),y&&!1===t.autoClear&&t.clearDepth(),t.render(e,r),t.setRenderTarget(s,1,i),y&&!1===t.autoClear&&t.clearDepth(),t.render(e,n),t.setRenderTarget(s,2,i),y&&!1===t.autoClear&&t.clearDepth(),t.render(e,a),t.setRenderTarget(s,3,i),y&&!1===t.autoClear&&t.clearDepth(),t.render(e,o),t.setRenderTarget(s,4,i),y&&!1===t.autoClear&&t.clearDepth(),t.render(e,h),s.texture.generateMipmaps=m,t.setRenderTarget(s,5,i),y&&!1===t.autoClear&&t.clearDepth(),t.render(e,l),t.setRenderTarget(c,u,d),t.xr.enabled=p,s.texture.needsPMREMUpdate=!0}}class Ru extends eu{constructor(t=[]){super(),this.isArrayCamera=!0,this.isMultiViewCamera=!1,this.cameras=t}}class Eu{constructor(){this._previousTime=0,this._currentTime=0,this._startTime=performance.now(),this._delta=0,this._elapsed=0,this._timescale=1,this._document=null,this._pageVisibilityHandler=null}connect(t){this._document=t,void 0!==t.hidden&&(this._pageVisibilityHandler=Nu.bind(this),t.addEventListener("visibilitychange",this._pageVisibilityHandler,!1))}disconnect(){null!==this._pageVisibilityHandler&&(this._document.removeEventListener("visibilitychange",this._pageVisibilityHandler),this._pageVisibilityHandler=null),this._document=null}getDelta(){return this._delta/1e3}getElapsed(){return this._elapsed/1e3}getTimescale(){return this._timescale}setTimescale(t){return this._timescale=t,this}reset(){return this._currentTime=performance.now()-this._startTime,this}dispose(){this.disconnect()}update(t){return null!==this._pageVisibilityHandler&&!0===this._document.hidden?this._delta=0:(this._previousTime=this._currentTime,this._currentTime=(void 0!==t?t:performance.now())-this._startTime,this._delta=(this._currentTime-this._previousTime)*this._timescale,this._elapsed+=this._delta),this}}function Nu(){!1===this._document.hidden&&this.reset()}const Vu=new Ti,Lu=new Ai,Fu=new Ti,Du=new Ti,Uu=new Ti;class ju extends Ar{constructor(){super(),this.type="AudioListener",this.context=Tu.getContext(),this.gain=this.context.createGain(),this.gain.connect(this.context.destination),this.filter=null,this.timeDelta=0,this._timer=new Eu}getInput(){return this.gain}removeFilter(){return null!==this.filter&&(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination),this.gain.connect(this.context.destination),this.filter=null),this}getFilter(){return this.filter}setFilter(t){return null!==this.filter?(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination)):this.gain.disconnect(this.context.destination),this.filter=t,this.gain.connect(this.filter),this.filter.connect(this.context.destination),this}getMasterVolume(){return this.gain.gain.value}setMasterVolume(t){return this.gain.gain.setTargetAtTime(t,this.context.currentTime,.01),this}updateMatrixWorld(t){super.updateMatrixWorld(t),this._timer.update();const e=this.context.listener;if(this.timeDelta=this._timer.getDelta(),this.matrixWorld.decompose(Vu,Lu,Fu),Du.set(0,0,-1).applyQuaternion(Lu),Uu.set(0,1,0).applyQuaternion(Lu),e.positionX){const t=this.context.currentTime+this.timeDelta;e.positionX.linearRampToValueAtTime(Vu.x,t),e.positionY.linearRampToValueAtTime(Vu.y,t),e.positionZ.linearRampToValueAtTime(Vu.z,t),e.forwardX.linearRampToValueAtTime(Du.x,t),e.forwardY.linearRampToValueAtTime(Du.y,t),e.forwardZ.linearRampToValueAtTime(Du.z,t),e.upX.linearRampToValueAtTime(Uu.x,t),e.upY.linearRampToValueAtTime(Uu.y,t),e.upZ.linearRampToValueAtTime(Uu.z,t)}else e.setPosition(Vu.x,Vu.y,Vu.z),e.setOrientation(Du.x,Du.y,Du.z,Uu.x,Uu.y,Uu.z)}}class Wu extends Ar{constructor(t){super(),this.type="Audio",this.listener=t,this.context=t.context,this.gain=this.context.createGain(),this.gain.connect(t.getInput()),this.autoplay=!1,this.buffer=null,this.detune=0,this.loop=!1,this.loopStart=0,this.loopEnd=0,this.offset=0,this.duration=void 0,this.playbackRate=1,this.isPlaying=!1,this.hasPlaybackControl=!0,this.source=null,this.sourceType="empty",this._startedAt=0,this._progress=0,this._connected=!1,this.filters=[]}getOutput(){return this.gain}setNodeSource(t){return this.hasPlaybackControl=!1,this.sourceType="audioNode",this.source=t,this.connect(),this}setMediaElementSource(t){return this.hasPlaybackControl=!1,this.sourceType="mediaNode",this.source=this.context.createMediaElementSource(t),this.connect(),this}setMediaStreamSource(t){return this.hasPlaybackControl=!1,this.sourceType="mediaStreamNode",this.source=this.context.createMediaStreamSource(t),this.connect(),this}setBuffer(t){return this.buffer=t,this.sourceType="buffer",this.autoplay&&this.play(),this}play(t=0){if(!0===this.isPlaying)return void ai("Audio: Audio is already playing.");if(!1===this.hasPlaybackControl)return void ai("Audio: this Audio has no playback control.");this._startedAt=this.context.currentTime+t;const e=this.context.createBufferSource();return e.buffer=this.buffer,e.loop=this.loop,e.loopStart=this.loopStart,e.loopEnd=this.loopEnd,e.onended=this.onEnded.bind(this),e.start(this._startedAt,this._progress+this.offset,this.duration),this.isPlaying=!0,this.source=e,this.setDetune(this.detune),this.setPlaybackRate(this.playbackRate),this.connect()}pause(){if(!1!==this.hasPlaybackControl)return!0===this.isPlaying&&(this._progress+=Math.max(this.context.currentTime-this._startedAt,0)*this.playbackRate,!0===this.loop&&(this._progress=this._progress%(this.duration||this.buffer.duration)),this.source.stop(),this.source.onended=null,this.isPlaying=!1),this;ai("Audio: this Audio has no playback control.")}stop(t=0){if(!1!==this.hasPlaybackControl)return this._progress=0,null!==this.source&&(this.source.stop(this.context.currentTime+t),this.source.onended=null),this.isPlaying=!1,this;ai("Audio: this Audio has no playback control.")}connect(){if(this.filters.length>0){this.source.connect(this.filters[0]);for(let t=1,e=this.filters.length;t0){this.source.disconnect(this.filters[0]);for(let t=1,e=this.filters.length;t0&&this._mixBufferRegionAdditive(s,i,this._addIndex*e,1,e);for(let t=e,r=e+e;t!==r;++t)if(s[t]!==s[t+e]){a.setValue(s,i);break}}saveOriginalState(){const t=this.binding,e=this.buffer,s=this.valueSize,i=s*this._origIndex;t.getValue(e,i);for(let t=s,r=i;t!==r;++t)e[t]=e[i+t%s];this._setIdentity(),this.cumulativeWeight=0,this.cumulativeWeightAdditive=0}restoreOriginalState(){const t=3*this.valueSize;this.binding.setValue(this.buffer,t)}_setAdditiveIdentityNumeric(){const t=this._addIndex*this.valueSize,e=t+this.valueSize;for(let s=t;s=.5)for(let i=0;i!==r;++i)t[e+i]=t[s+i]}_slerp(t,e,s,i){Ai.slerpFlat(t,e,t,e,t,s,i)}_slerpAdditive(t,e,s,i,r){const n=this._workIndex*r;Ai.multiplyQuaternionsFlat(t,n,t,e,t,s),Ai.slerpFlat(t,e,t,e,t,n,i)}_lerp(t,e,s,i,r){const n=1-i;for(let a=0;a!==r;++a){const r=e+a;t[r]=t[r]*n+t[s+a]*i}}_lerpAdditive(t,e,s,i,r){for(let n=0;n!==r;++n){const r=e+n;t[r]=t[r]+t[s+n]*i}}}const $u="\\[\\]\\.:\\/",Qu=new RegExp("["+$u+"]","g"),Ku="[^"+$u+"]",td="[^"+$u.replace("\\.","")+"]",ed=new RegExp("^"+/((?:WC+[\/:])*)/.source.replace("WC",Ku)+/(WCOD+)?/.source.replace("WCOD",td)+/(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace("WC",Ku)+/\.(WC+)(?:\[(.+)\])?/.source.replace("WC",Ku)+"$"),sd=["material","materials","bones","map"];class id{constructor(t,e,s){this.path=e,this.parsedPath=s||id.parseTrackName(e),this.node=id.findNode(t,this.parsedPath.nodeName),this.rootNode=t,this.getValue=this._getValue_unbound,this.setValue=this._setValue_unbound}static create(t,e,s){return t&&t.isAnimationObjectGroup?new id.Composite(t,e,s):new id(t,e,s)}static sanitizeNodeName(t){return t.replace(/\s/g,"_").replace(Qu,"")}static parseTrackName(t){const e=ed.exec(t);if(null===e)throw new Error("THREE.PropertyBinding: Cannot parse trackName: "+t);const s={nodeName:e[2],objectName:e[3],objectIndex:e[4],propertyName:e[5],propertyIndex:e[6]},i=s.nodeName&&s.nodeName.lastIndexOf(".");if(void 0!==i&&-1!==i){const t=s.nodeName.substring(i+1);-1!==sd.indexOf(t)&&(s.nodeName=s.nodeName.substring(0,i),s.objectName=t)}if(null===s.propertyName||0===s.propertyName.length)throw new Error("THREE.PropertyBinding: can not parse propertyName from trackName: "+t);return s}static findNode(t,e){if(void 0===e||""===e||"."===e||-1===e||e===t.name||e===t.uuid)return t;if(t.skeleton){const s=t.skeleton.getBoneByName(e);if(void 0!==s)return s}if(t.children){const s=function(t){for(let i=0;i=r){const n=r++,l=t[n];e[l.uuid]=h,t[h]=l,e[o]=n,t[n]=a;for(let t=0,e=i;t!==e;++t){const e=s[t],i=e[n],r=e[h];e[h]=i,e[n]=r}}}this.nCachedObjects_=r}uncache(){const t=this._objects,e=this._indicesByUUID,s=this._bindings,i=s.length;let r=this.nCachedObjects_,n=t.length;for(let a=0,o=arguments.length;a!==o;++a){const o=arguments[a].uuid,h=e[o];if(void 0!==h)if(delete e[o],h0&&(e[a.uuid]=h),t[h]=a,t.pop();for(let t=0,e=i;t!==e;++t){const e=s[t];e[h]=e[r],e.pop()}}}this.nCachedObjects_=r}subscribe_(t,e){const s=this._bindingsIndicesByPath;let i=s[t];const r=this._bindings;if(void 0!==i)return r[i];const n=this._paths,a=this._parsedPaths,o=this._objects,h=o.length,l=this.nCachedObjects_,c=new Array(h);i=r.length,s[t]=i,n.push(t),a.push(e),r.push(c);for(let s=l,i=o.length;s!==i;++s){const i=o[s];c[s]=new id(i,t,e)}return c}unsubscribe_(t){const e=this._bindingsIndicesByPath,s=e[t];if(void 0!==s){const i=this._paths,r=this._parsedPaths,n=this._bindings,a=n.length-1,o=n[a];e[t[a]]=s,n[s]=o,n.pop(),r[s]=r[a],r.pop(),i[s]=i[a],i.pop()}}}class nd{constructor(t,e,s=null,i=e.blendMode){this._mixer=t,this._clip=e,this._localRoot=s,this.blendMode=i;const r=e.tracks,n=r.length,a=new Array(n),o={endingStart:De,endingEnd:De};for(let t=0;t!==n;++t){const e=r[t].createInterpolant(null);a[t]=e,e.settings=o}this._interpolantSettings=o,this._interpolants=a,this._propertyBindings=new Array(n),this._cacheIndex=null,this._byClipCacheIndex=null,this._timeScaleInterpolant=null,this._restoreTimeScale=null,this._weightInterpolant=null,this.loop=2201,this._loopCount=-1,this._startTime=null,this.time=0,this.timeScale=1,this._effectiveTimeScale=1,this.weight=1,this._effectiveWeight=1,this.repetitions=1/0,this.paused=!1,this.enabled=!0,this.clampWhenFinished=!1,this.zeroSlopeAtStart=!0,this.zeroSlopeAtEnd=!0}play(){return this._mixer._activateAction(this),this}stop(){return this._mixer._deactivateAction(this),this.reset()}reset(){return this.paused=!1,this.enabled=!0,this.time=0,this._loopCount=-1,this._startTime=null,this.stopFading().stopWarping()}isRunning(){return this.enabled&&!this.paused&&0!==this.timeScale&&null===this._startTime&&this._mixer._isActiveAction(this)}isScheduled(){return this._mixer._isActiveAction(this)}startAt(t){return this._startTime=t,this}setLoop(t,e){return this.loop=t,this.repetitions=e,this}setEffectiveWeight(t){return this.weight=t,this._effectiveWeight=this.enabled?t:0,this.stopFading()}getEffectiveWeight(){return this._effectiveWeight}fadeIn(t){return this._scheduleFading(t,0,1)}fadeOut(t){return this._scheduleFading(t,1,0)}crossFadeFrom(t,e,s=!1){if(t.fadeOut(e),this.fadeIn(e),!0===s){const s=this._clip.duration,i=t._clip.duration,r=i/s,n=s/i;t._restoreTimeScale=t.timeScale,this._restoreTimeScale=this.timeScale,t.warp(1,r,e),this.warp(n,1,e)}return this}crossFadeTo(t,e,s=!1){return t.crossFadeFrom(this,e,s)}stopFading(){const t=this._weightInterpolant;return null!==t&&(this._weightInterpolant=null,this._mixer._takeBackControlInterpolant(t)),this}setEffectiveTimeScale(t){return this.timeScale=t,this._effectiveTimeScale=this.paused?0:t,this.stopWarping()}getEffectiveTimeScale(){return this._effectiveTimeScale}setDuration(t){return this.timeScale=this._clip.duration/t,this.stopWarping()}syncWith(t){return this.time=t.time,this.timeScale=t.timeScale,this.stopWarping()}halt(t){return this.warp(this._effectiveTimeScale,0,t)}warp(t,e,s){const i=this._mixer,r=i.time,n=this.timeScale;let a=this._timeScaleInterpolant;null===a&&(a=i._lendControlInterpolant(),this._timeScaleInterpolant=a);const o=a.parameterPositions,h=a.sampleValues;return o[0]=r,o[1]=r+s,h[0]=t/n,h[1]=e/n,this}stopWarping(){const t=this._timeScaleInterpolant;return null!==t&&(this._timeScaleInterpolant=null,this._mixer._takeBackControlInterpolant(t)),this._restoreTimeScale=null,this}getMixer(){return this._mixer}getClip(){return this._clip}getRoot(){return this._localRoot||this._mixer._root}_update(t,e,s,i){if(!this.enabled)return void this._updateWeight(t);const r=this._startTime;if(null!==r){const i=(t-r)*s;i<0||0===s?e=0:(this._startTime=null,e=s*i)}e*=this._updateTimeScale(t);const n=this._updateTime(e),a=this._updateWeight(t);if(a>0){const t=this._interpolants,e=this._propertyBindings;if(this.blendMode===Je)for(let s=0,i=t.length;s!==i;++s)t[s].evaluate(n),e[s].accumulateAdditive(a);else for(let s=0,r=t.length;s!==r;++s)t[s].evaluate(n),e[s].accumulate(i,a)}}_updateWeight(t){let e=0;if(this.enabled){e=this.weight;const s=this._weightInterpolant;if(null!==s){const i=s.evaluate(t)[0];e*=i,t>s.parameterPositions[1]&&(this.stopFading(),0===i&&(this.enabled=!1))}}return this._effectiveWeight=e,e}_updateTimeScale(t){let e=0;if(!this.paused){e=this.timeScale;const s=this._timeScaleInterpolant;if(null!==s){e*=s.evaluate(t)[0],t>s.parameterPositions[1]&&(0===e?this.paused=!0:(null!==this._restoreTimeScale&&(e=this._restoreTimeScale),this.timeScale=e),this.stopWarping())}}return this._effectiveTimeScale=e,e}_updateTime(t){const e=this._clip.duration,s=this.loop;let i=this.time+t,r=this._loopCount;const n=2202===s;if(0===t)return-1===r||!n||1&~r?i:e-i;if(2200===s){-1===r&&(this._loopCount=0,this._setEndings(!0,!0,!1));t:{if(i>=e)i=e;else{if(!(i<0)){this.time=i;break t}i=0}this.clampWhenFinished?this.paused=!0:this.enabled=!1,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:t<0?-1:1})}}else{if(-1===r&&(t>=0?(r=0,this._setEndings(!0,0===this.repetitions,n)):this._setEndings(0===this.repetitions,!0,n)),i>=e||i<0){const s=Math.floor(i/e);i-=e*s,r+=Math.abs(s);const a=this.repetitions-r;if(a<=0)this.clampWhenFinished?this.paused=!0:this.enabled=!1,i=t>0?e:0,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:t>0?1:-1});else{if(1===a){const e=t<0;this._setEndings(e,!e,n)}else this._setEndings(!1,!1,n);this._loopCount=r,this.time=i,this._mixer.dispatchEvent({type:"loop",action:this,loopDelta:s})}}else this._loopCount=r,this.time=i;if(n&&!(1&~r))return e-i}return i}_setEndings(t,e,s){const i=this._interpolantSettings;s?(i.endingStart=Ue,i.endingEnd=Ue):(i.endingStart=t?this.zeroSlopeAtStart?Ue:De:je,i.endingEnd=e?this.zeroSlopeAtEnd?Ue:De:je)}_scheduleFading(t,e,s){const i=this._mixer,r=i.time;let n=this._weightInterpolant;null===n&&(n=i._lendControlInterpolant(),this._weightInterpolant=n);const a=n.parameterPositions,o=n.sampleValues;return a[0]=r,o[0]=e,a[1]=r+t,o[1]=s,this}}const ad=new Float32Array(1);class od extends di{constructor(t){super(),this._root=t,this._initMemoryManager(),this._accuIndex=0,this.time=0,this.timeScale=1,"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}_bindAction(t,e){const s=t._localRoot||this._root,i=t._clip.tracks,r=i.length,n=t._propertyBindings,a=t._interpolants,o=s.uuid,h=this._bindingsByRootAndName;let l=h[o];void 0===l&&(l={},h[o]=l);for(let t=0;t!==r;++t){const r=i[t],h=r.name;let c=l[h];if(void 0!==c)++c.referenceCount,n[t]=c;else{if(c=n[t],void 0!==c){null===c._cacheIndex&&(++c.referenceCount,this._addInactiveBinding(c,o,h));continue}const i=e&&e._propertyBindings[t].binding.parsedPath;c=new Gu(id.create(s,h,i),r.ValueTypeName,r.getValueSize()),++c.referenceCount,this._addInactiveBinding(c,o,h),n[t]=c}a[t].resultBuffer=c.buffer}}_activateAction(t){if(!this._isActiveAction(t)){if(null===t._cacheIndex){const e=(t._localRoot||this._root).uuid,s=t._clip.uuid,i=this._actionsByClip[s];this._bindAction(t,i&&i.knownActions[0]),this._addInactiveAction(t,s,e)}const e=t._propertyBindings;for(let t=0,s=e.length;t!==s;++t){const s=e[t];0===s.useCount++&&(this._lendBinding(s),s.saveOriginalState())}this._lendAction(t)}}_deactivateAction(t){if(this._isActiveAction(t)){const e=t._propertyBindings;for(let t=0,s=e.length;t!==s;++t){const s=e[t];0===--s.useCount&&(s.restoreOriginalState(),this._takeBackBinding(s))}this._takeBackAction(t)}}_initMemoryManager(){this._actions=[],this._nActiveActions=0,this._actionsByClip={},this._bindings=[],this._nActiveBindings=0,this._bindingsByRootAndName={},this._controlInterpolants=[],this._nActiveControlInterpolants=0;const t=this;this.stats={actions:{get total(){return t._actions.length},get inUse(){return t._nActiveActions}},bindings:{get total(){return t._bindings.length},get inUse(){return t._nActiveBindings}},controlInterpolants:{get total(){return t._controlInterpolants.length},get inUse(){return t._nActiveControlInterpolants}}}}_isActiveAction(t){const e=t._cacheIndex;return null!==e&&e=0;--e)t[e].stop();return this}update(t){t*=this.timeScale;const e=this._actions,s=this._nActiveActions,i=this.time+=t,r=Math.sign(t),n=this._accuIndex^=1;for(let a=0;a!==s;++a){e[a]._update(i,t,r,n)}const a=this._bindings,o=this._nActiveBindings;for(let t=0;t!==o;++t)a[t].apply(n);return this}setTime(t){this.time=0;for(let t=0;t=this.min.x&&t.x<=this.max.x&&t.y>=this.min.y&&t.y<=this.max.y}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y))}intersectsBox(t){return t.max.x>=this.min.x&&t.min.x<=this.max.x&&t.max.y>=this.min.y&&t.min.y<=this.max.y}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,Md).distanceTo(t)}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}const _d=new Ti,Ad=new Ti,Td=new Ti,zd=new Ti,Cd=new Ti,Id=new Ti,Bd=new Ti;class kd{constructor(t=new Ti,e=new Ti){this.start=t,this.end=e}set(t,e){return this.start.copy(t),this.end.copy(e),this}copy(t){return this.start.copy(t.start),this.end.copy(t.end),this}getCenter(t){return t.addVectors(this.start,this.end).multiplyScalar(.5)}delta(t){return t.subVectors(this.end,this.start)}distanceSq(){return this.start.distanceToSquared(this.end)}distance(){return this.start.distanceTo(this.end)}at(t,e){return this.delta(e).multiplyScalar(t).add(this.start)}closestPointToPointParameter(t,e){_d.subVectors(t,this.start),Ad.subVectors(this.end,this.start);const s=Ad.dot(Ad);if(0===s)return 0;let i=Ad.dot(_d)/s;return e&&(i=xi(i,0,1)),i}closestPointToPoint(t,e,s){const i=this.closestPointToPointParameter(t,e);return this.delta(s).multiplyScalar(i).add(this.start)}distanceSqToLine3(t,e=Id,s=Bd){const i=1e-8*1e-8;let r,n;const a=this.start,o=t.start,h=this.end,l=t.end;Td.subVectors(h,a),zd.subVectors(l,o),Cd.subVectors(a,o);const c=Td.dot(Td),u=zd.dot(zd),d=zd.dot(Cd);if(c<=i&&u<=i)return e.copy(a),s.copy(o),e.sub(s),e.dot(e);if(c<=i)r=0,n=d/u,n=xi(n,0,1);else{const t=Td.dot(Cd);if(u<=i)n=0,r=xi(-t/c,0,1);else{const e=Td.dot(zd),s=c*u-e*e;r=0!==s?xi((e*d-t*u)/s,0,1):0,n=(e*r+d)/u,n<0?(n=0,r=xi(-t/c,0,1)):n>1&&(n=1,r=xi((e-t)/c,0,1))}}return e.copy(a).addScaledVector(Td,r),s.copy(o).addScaledVector(zd,n),e.distanceToSquared(s)}applyMatrix4(t){return this.start.applyMatrix4(t),this.end.applyMatrix4(t),this}equals(t){return t.start.equals(this.start)&&t.end.equals(this.end)}clone(){return(new this.constructor).copy(this)}}const Od=new Ti;class Pd extends Ar{constructor(t,e){super(),this.light=t,this.matrixAutoUpdate=!1,this.color=e,this.type="SpotLightHelper";const s=new Wn,i=[0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,-1,0,1,0,0,0,0,1,1,0,0,0,0,-1,1];for(let t=0,e=1,s=32;t1)for(let s=0;s.99999)this.quaternion.set(0,0,0,1);else if(t.y<-.99999)this.quaternion.set(1,0,0,0);else{rp.set(t.z,0,-t.x).normalize();const e=Math.acos(t.y);this.quaternion.setFromAxisAngle(rp,e)}}setLength(t,e=.2*t,s=.2*e){this.line.scale.set(1,Math.max(1e-4,t-e),1),this.line.updateMatrix(),this.cone.scale.set(s,e,s),this.cone.position.y=t,this.cone.updateMatrix()}setColor(t){this.line.material.color.set(t),this.cone.material.color.set(t)}copy(t){return super.copy(t,!1),this.line.copy(t.line),this.cone.copy(t.cone),this}dispose(){this.line.geometry.dispose(),this.line.material.dispose(),this.cone.geometry.dispose(),this.cone.material.dispose()}}class hp extends Yo{constructor(t=1){const e=[0,0,0,t,0,0,0,0,0,0,t,0,0,0,0,0,0,t],s=new Wn;s.setAttribute("position",new kn(e,3)),s.setAttribute("color",new kn([1,0,0,1,.6,0,0,1,0,.6,1,0,0,0,1,0,.6,1],3));super(s,new No({vertexColors:!0,toneMapped:!1})),this.type="AxesHelper"}setColors(t,e,s){const i=new Pr,r=this.geometry.attributes.color.array;return i.set(t),i.toArray(r,0),i.toArray(r,3),i.set(e),i.toArray(r,6),i.toArray(r,9),i.set(s),i.toArray(r,12),i.toArray(r,15),this.geometry.attributes.color.needsUpdate=!0,this}dispose(){this.geometry.dispose(),this.material.dispose()}}class lp{constructor(){this.type="ShapePath",this.color=new Pr,this.subPaths=[],this.currentPath=null,this.userData={}}moveTo(t,e){return this.currentPath=new Zh,this.subPaths.push(this.currentPath),this.currentPath.moveTo(t,e),this}lineTo(t,e){return this.currentPath.lineTo(t,e),this}quadraticCurveTo(t,e,s,i){return this.currentPath.quadraticCurveTo(t,e,s,i),this}bezierCurveTo(t,e,s,i,r,n){return this.currentPath.bezierCurveTo(t,e,s,i,r,n),this}splineThru(t){return this.currentPath.splineThru(t),this}toShapes(){function t(t,e){let s=!1;const i=e.length;for(let r=0,n=i-1;rt.y!=a.y>t.y&&t.x<(a.x-i.x)*(t.y-i.y)/(a.y-i.y)+i.x&&(s=!s)}return s}function e(e,s){const i=s.getCenter(new _i);if(t(i,e))return i;const r=i.y,n=[],a=e.length;for(let t=0;tr!=i.y>r){const t=s.x+(r-s.y)*(i.x-s.x)/(i.y-s.y);n.push(t)}}return n.length>1&&(n.sort((t,e)=>t-e),i.x=(n[0]+n[1])/2),i}let s=this.userData.style&&this.userData.style.fillRule||"nonzero";"nonzero"!==s&&"evenodd"!==s&&(ai('Fill-rule "'+s+'" is not supported, falling back to "nonzero".'),s="nonzero");const i="nonzero"===s?t=>0!==t:t=>!!(1&t),r=[];for(const t of this.subPaths){const s=t.getPoints();if(s.length<3)continue;const i=_l.area(s);if(0===i)continue;const n=new Sd;for(let t=0;te.absArea-t.absArea);for(let e=0;e=0;i--){const e=r[i];if(e.boundingBox.containsBox(s.boundingBox)&&t(s.interiorPoint,e.points)){s.container=e.exclude?e.container:e,n=e.winding,s.winding+=n;break}}i(s.winding)===i(n)&&(s.exclude=!0)}for(const t of r)t.exclude||(t.role=null===t.container||"hole"===t.container.role?"outer":"hole");const n=[],a=new Map;for(const t of r){if(t.exclude||"outer"!==t.role)continue;const e=new Gh;e.curves=t.subPath.curves,n.push(e),a.set(t,e)}for(const t of r){if(t.exclude||"hole"!==t.role)continue;const e=a.get(t.container);if(!e)continue;const s=new Zh;s.curves=t.subPath.curves,e.holes.push(s)}return n}}class cp extends di{constructor(t,e=null){super(),this.object=t,this.domElement=e,this.enabled=!0,this.state=-1,this.keys={},this.mouseButtons={LEFT:null,MIDDLE:null,RIGHT:null},this.touches={ONE:null,TWO:null}}connect(t){void 0!==t?(null!==this.domElement&&this.disconnect(),this.domElement=t):ai("Controls: connect() now requires an element.")}disconnect(){}dispose(){}update(){}}function up(t,e,s,i){const r=function(t){switch(t){case zt:case Ct:return{byteLength:1,components:1};case Bt:case It:case Rt:return{byteLength:2,components:1};case Et:case Nt:return{byteLength:2,components:4};case Ot:case kt:case Pt:return{byteLength:4,components:1};case Lt:case Ft:return{byteLength:4,components:3}}throw new Error(`THREE.TextureUtils: Unknown texture type ${t}.`)}(i);switch(s){case 1021:return t*e;case qt:case Ht:return t*e/r.components*r.byteLength;case 1030:case 1031:return t*e*2/r.components*r.byteLength;case 1022:return t*e*3/r.components*r.byteLength;case jt:case 1033:return t*e*4/r.components*r.byteLength;case 33776:case 33777:return Math.floor((t+3)/4)*Math.floor((e+3)/4)*8;case 33778:case 33779:return Math.floor((t+3)/4)*Math.floor((e+3)/4)*16;case 35841:case 35843:return Math.max(t,16)*Math.max(e,8)/4;case 35840:case 35842:return Math.max(t,8)*Math.max(e,8)/2;case 36196:case 37492:case 37488:case 37489:return Math.floor((t+3)/4)*Math.floor((e+3)/4)*8;case 37496:case 37490:case 37491:case 37808:return Math.floor((t+3)/4)*Math.floor((e+3)/4)*16;case 37809:return Math.floor((t+4)/5)*Math.floor((e+3)/4)*16;case 37810:return Math.floor((t+4)/5)*Math.floor((e+4)/5)*16;case 37811:return Math.floor((t+5)/6)*Math.floor((e+4)/5)*16;case 37812:return Math.floor((t+5)/6)*Math.floor((e+5)/6)*16;case 37813:return Math.floor((t+7)/8)*Math.floor((e+4)/5)*16;case 37814:return Math.floor((t+7)/8)*Math.floor((e+5)/6)*16;case 37815:return Math.floor((t+7)/8)*Math.floor((e+7)/8)*16;case 37816:return Math.floor((t+9)/10)*Math.floor((e+4)/5)*16;case 37817:return Math.floor((t+9)/10)*Math.floor((e+5)/6)*16;case 37818:return Math.floor((t+9)/10)*Math.floor((e+7)/8)*16;case 37819:return Math.floor((t+9)/10)*Math.floor((e+9)/10)*16;case 37820:return Math.floor((t+11)/12)*Math.floor((e+9)/10)*16;case 37821:return Math.floor((t+11)/12)*Math.floor((e+11)/12)*16;case 36492:case 36494:case 36495:return Math.ceil(t/4)*Math.ceil(e/4)*16;case 36283:case 36284:return Math.ceil(t/4)*Math.ceil(e/4)*8;case 36285:case 36286:return Math.ceil(t/4)*Math.ceil(e/4)*16}throw new Error(`Unable to determine texture byte length for ${s} format.`)}class dp{static contain(t,e){return function(t,e){const s=t.image&&t.image.width?t.image.width/t.image.height:1;return s>e?(t.repeat.x=1,t.repeat.y=s/e,t.offset.x=0,t.offset.y=(1-t.repeat.y)/2):(t.repeat.x=e/s,t.repeat.y=1,t.offset.x=(1-t.repeat.x)/2,t.offset.y=0),t}(t,e)}static cover(t,e){return function(t,e){const s=t.image&&t.image.width?t.image.width/t.image.height:1;return s>e?(t.repeat.x=e/s,t.repeat.y=1,t.offset.x=(1-t.repeat.x)/2,t.offset.y=0):(t.repeat.x=1,t.repeat.y=s/e,t.offset.x=0,t.offset.y=(1-t.repeat.y)/2),t}(t,e)}static fill(t){return function(t){return t.repeat.x=1,t.repeat.y=1,t.offset.x=0,t.offset.y=0,t}(t)}static getByteLength(t,e,s,i){return up(t,e,s,i)}}"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("register",{detail:{revision:t}})),"undefined"!=typeof window&&(window.__THREE__?ai("WARNING: Multiple instances of Three.js being imported."):window.__THREE__=t);export{st as ACESFilmicToneMapping,w as AddEquation,$ as AddOperation,Je as AdditiveAnimationBlendMode,g as AdditiveBlending,rt as AgXToneMapping,Dt as AlphaFormat,ks as AlwaysCompare,j as AlwaysDepth,Ss as AlwaysStencilFunc,lu as AmbientLight,nd as AnimationAction,Ac as AnimationClip,Ec as AnimationLoader,od as AnimationMixer,rd as AnimationObjectGroup,uc as AnimationUtils,Ih as ArcCurve,Ru as ArrayCamera,op as ArrowHelper,at as AttachedBindMode,Wu as Audio,Zu as AudioAnalyser,Tu as AudioContext,ju as AudioListener,zu as AudioLoader,hp as AxesHelper,d as BackSide,Ye as BasicDepthPacking,o as BasicShadowMap,Eo as BatchedMesh,gc as BezierInterpolant,Ha as Bone,xc as BooleanKeyframeTrack,Sd as Box2,Qr as Box3,sp as Box3Helper,yh as BoxGeometry,ep as BoxHelper,Mn as BufferAttribute,Wn as BufferGeometry,fu as BufferGeometryLoader,Ct as ByteType,zc as Cache,$c as Camera,Qd as CameraHelper,ch as CanvasTexture,gh as CapsuleGeometry,Nh as CatmullRomCurve3,et as CineonToneMapping,fh as CircleGeometry,yt as ClampToEdgeWrapping,xd as Clock,Pr as Color,bc as ColorKeyframeTrack,Ri as ColorManagement,Ys as Compatibility,oh as CompressedArrayTexture,hh as CompressedCubeTexture,ah as CompressedTexture,Nc as CompressedTextureLoader,bh as ConeGeometry,F as ConstantAlphaFactor,V as ConstantColorFactor,cp as Controls,Pu as CubeCamera,ph as CubeDepthTexture,lt as CubeReflectionMapping,ct as CubeRefractionMapping,lh as CubeTexture,Fc as CubeTextureLoader,pt as CubeUVReflectionMapping,Dh as CubicBezierCurve,Uh as CubicBezierCurve3,pc as CubicInterpolant,r as CullFaceBack,n as CullFaceFront,a as CullFaceFrontBack,i as CullFaceNone,zh as Curve,Yh as CurvePath,b as CustomBlending,it as CustomToneMapping,xh as CylinderGeometry,vd as Cylindrical,Gi as Data3DTexture,Yi as DataArrayTexture,Xa as DataTexture,Dc as DataTextureLoader,xn as DataUtils,ds as DecrementStencilOp,ms as DecrementWrapStencilOp,Bc as DefaultLoadingManager,Wt as DepthFormat,Jt as DepthStencilFormat,dh as DepthTexture,ot as DetachedBindMode,hu as DirectionalLight,Zd as DirectionalLightHelper,yc as DiscreteInterpolant,wh as DodecahedronGeometry,p as DoubleSide,O as DstAlphaFactor,R as DstColorFactor,Fs as DynamicCopyUsage,Ps as DynamicDrawUsage,Ns as DynamicReadUsage,Th as EdgesGeometry,Ch as EllipseCurve,Ts as EqualCompare,q as EqualDepth,xs as EqualStencilFunc,ut as EquirectangularReflectionMapping,dt as EquirectangularRefractionMapping,hr as Euler,di as EventDispatcher,mh as ExternalTexture,zl as ExtrudeGeometry,Rc as FileLoader,Bn as Float16BufferAttribute,kn as Float32BufferAttribute,Pt as FloatType,Nr as Fog,Er as FogExp2,nh as FramebufferTexture,u as FrontSide,mo as Frustum,go as FrustumArray,pd as GLBufferAttribute,Us as GLSL1,js as GLSL3,Cs as GreaterCompare,X as GreaterDepth,Bs as GreaterEqualCompare,H as GreaterEqualDepth,Ms as GreaterEqualStencilFunc,vs as GreaterStencilFunc,Jd as GridHelper,Tr as Group,uh as HTMLTexture,Rt as HalfFloatType,Wc as HemisphereLight,Wd as HemisphereLightHelper,Il as IcosahedronGeometry,_u as ImageBitmapLoader,Lc as ImageLoader,Li as ImageUtils,us as IncrementStencilOp,ps as IncrementWrapStencilOp,$a as InstancedBufferAttribute,gu as InstancedBufferGeometry,dd as InstancedInterleavedBuffer,no as InstancedMesh,Tn as Int16BufferAttribute,Cn as Int32BufferAttribute,Sn as Int8BufferAttribute,kt as IntType,Jn as InterleavedBuffer,Hn as InterleavedBufferAttribute,dc as Interpolant,Fe as InterpolateBezier,Ne as InterpolateDiscrete,Ve as InterpolateLinear,Le as InterpolateSmooth,Xs as InterpolationSamplingMode,Hs as InterpolationSamplingType,ys as InvertStencilOp,ls as KeepStencilOp,fc as KeyframeTrack,pa as LOD,Bl as LatheGeometry,lr as Layers,As as LessCompare,W as LessDepth,zs as LessEqualCompare,J as LessEqualDepth,bs as LessEqualStencilFunc,fs as LessStencilFunc,jc as Light,du as LightProbe,Jo as Line,kd as Line3,No as LineBasicMaterial,jh as LineCurve,Wh as LineCurve3,ac as LineDashedMaterial,Zo as LineLoop,Yo as LineSegments,Mt as LinearFilter,mc as LinearInterpolant,Tt as LinearMipMapLinearFilter,_t as LinearMipMapNearestFilter,At as LinearMipmapLinearFilter,St as LinearMipmapNearestFilter,ss as LinearSRGBColorSpace,K as LinearToneMapping,is as LinearTransfer,kc as Loader,yu as LoaderUtils,Ic as LoadingManager,Pe as LoopOnce,Ee as LoopPingPong,Re as LoopRepeat,e as MOUSE,Zn as Material,v as MaterialBlending,mu as MaterialLoader,Si as MathUtils,wd as Matrix2,Ii as Matrix3,Qi as Matrix4,A as MaxEquation,Ra as Mesh,Ma as MeshBasicMaterial,ic as MeshDepthMaterial,rc as MeshDistanceMaterial,sc as MeshLambertMaterial,nc as MeshMatcapMaterial,ec as MeshNormalMaterial,Kl as MeshPhongMaterial,Ql as MeshPhysicalMaterial,$l as MeshStandardMaterial,tc as MeshToonMaterial,_ as MinEquation,gt as MirroredRepeatWrapping,G as MixOperation,x as MultiplyBlending,Z as MultiplyOperation,ft as NearestFilter,wt as NearestMipMapLinearFilter,bt as NearestMipMapNearestFilter,vt as NearestMipmapLinearFilter,xt as NearestMipmapNearestFilter,nt as NeutralToneMapping,_s as NeverCompare,U as NeverDepth,gs as NeverStencilFunc,m as NoBlending,ts as NoColorSpace,ns as NoNormalPacking,Q as NoToneMapping,We as NormalAnimationBlendMode,y as NormalBlending,os as NormalGAPacking,as as NormalRGPacking,Is as NotEqualCompare,Y as NotEqualDepth,ws as NotEqualStencilFunc,vc as NumberKeyframeTrack,Ar as Object3D,bu as ObjectLoader,Ke as ObjectSpaceNormalMap,kl as OctahedronGeometry,z as OneFactor,D as OneMinusConstantAlphaFactor,L as OneMinusConstantColorFactor,P as OneMinusDstAlphaFactor,E as OneMinusDstColorFactor,k as OneMinusSrcAlphaFactor,I as OneMinusSrcColorFactor,au as OrthographicCamera,h as PCFShadowMap,l as PCFSoftShadowMap,Zh as Path,eu as PerspectiveCamera,lo as Plane,Ol as PlaneGeometry,ip as PlaneHelper,nu as PointLight,Fd as PointLightHelper,eh as Points,Go as PointsMaterial,qd as PolarGridHelper,vh as PolyhedronGeometry,Yu as PositionalAudio,id as PropertyBinding,Gu as PropertyMixer,Jh as QuadraticBezierCurve,qh as QuadraticBezierCurve3,Ai as Quaternion,Mc as QuaternionKeyframeTrack,wc as QuaternionLinearInterpolant,he as R11_EAC_Format,gi as RAD2DEG,ke as RED_GREEN_RGTC2_Format,Ie as RED_RGTC1_Format,t as REVISION,ce as RG11_EAC_Format,Ze as RGBADepthPacking,jt as RGBAFormat,Gt as RGBAIntegerFormat,Se as RGBA_ASTC_10x10_Format,ve as RGBA_ASTC_10x5_Format,we as RGBA_ASTC_10x6_Format,Me as RGBA_ASTC_10x8_Format,_e as RGBA_ASTC_12x10_Format,Ae as RGBA_ASTC_12x12_Format,de as RGBA_ASTC_4x4_Format,pe as RGBA_ASTC_5x4_Format,me as RGBA_ASTC_5x5_Format,ye as RGBA_ASTC_6x5_Format,ge as RGBA_ASTC_6x6_Format,fe as RGBA_ASTC_8x5_Format,xe as RGBA_ASTC_8x6_Format,be as RGBA_ASTC_8x8_Format,Te as RGBA_BPTC_Format,oe as RGBA_ETC2_EAC_Format,re as RGBA_PVRTC_2BPPV1_Format,ie as RGBA_PVRTC_4BPPV1_Format,Qt as RGBA_S3TC_DXT1_Format,Kt as RGBA_S3TC_DXT3_Format,te as RGBA_S3TC_DXT5_Format,Ge as RGBDepthPacking,Ut as RGBFormat,Zt as RGBIntegerFormat,ze as RGB_BPTC_SIGNED_Format,Ce as RGB_BPTC_UNSIGNED_Format,ne as RGB_ETC1_Format,ae as RGB_ETC2_Format,se as RGB_PVRTC_2BPPV1_Format,ee as RGB_PVRTC_4BPPV1_Format,$t as RGB_S3TC_DXT1_Format,$e as RGDepthPacking,Xt as RGFormat,Yt as RGIntegerFormat,Gl as RawShaderMaterial,wa as Ray,yd as Raycaster,cu as RectAreaLight,qt as RedFormat,Ht as RedIntegerFormat,tt as ReinhardToneMapping,Hi as RenderTarget,hd as RenderTarget3D,mt as RepeatWrapping,cs as ReplaceStencilOp,S as ReverseSubtractEquation,ui as ReversedDepthFuncs,Pl as RingGeometry,le as SIGNED_R11_EAC_Format,Oe as SIGNED_RED_GREEN_RGTC2_Format,Be as SIGNED_RED_RGTC1_Format,ue as SIGNED_RG11_EAC_Format,es as SRGBColorSpace,rs as SRGBTransfer,Vr as Scene,Zl as ShaderMaterial,Wl as ShadowMaterial,Gh as Shape,Rl as ShapeGeometry,lp as ShapePath,_l as ShapeUtils,It as ShortType,Ga as Skeleton,Vd as SkeletonHelper,qa as SkinnedMesh,Di as Source,En as Sphere,El as SphereGeometry,bd as Spherical,uu as SphericalHarmonics3,Hh as SplineCurve,iu as SpotLight,Pd as SpotLightHelper,la as Sprite,Gn as SpriteMaterial,B as SrcAlphaFactor,N as SrcAlphaSaturateFactor,C as SrcColorFactor,Ls as StaticCopyUsage,Os as StaticDrawUsage,Es as StaticReadUsage,ku as StereoCamera,Ds as StreamCopyUsage,Rs as StreamDrawUsage,Vs as StreamReadUsage,Sc as StringKeyframeTrack,M as SubtractEquation,f as SubtractiveBlending,s as TOUCH,Qe as TangentSpaceNormalMap,Nl as TetrahedronGeometry,Ji as Texture,Uc as TextureLoader,dp as TextureUtils,Eu as Timer,qs as TimestampQuery,Vl as TorusGeometry,Ll as TorusKnotGeometry,$r as Triangle,Xe as TriangleFanDrawMode,He as TriangleStripDrawMode,qe as TrianglesDrawMode,Fl as TubeGeometry,ht as UVMapping,zn as Uint16BufferAttribute,In as Uint32BufferAttribute,_n as Uint8BufferAttribute,An as Uint8ClampedBufferAttribute,ld as Uniform,ud as UniformsGroup,Yl as UniformsUtils,zt as UnsignedByteType,Ft as UnsignedInt101111Type,Vt as UnsignedInt248Type,Lt as UnsignedInt5999Type,Ot as UnsignedIntType,Et as UnsignedShort4444Type,Nt as UnsignedShort5551Type,Bt as UnsignedShortType,c as VSMShadowMap,_i as Vector2,Ti as Vector3,qi as Vector4,_c as VectorKeyframeTrack,rh as VideoFrameTexture,ih as VideoTexture,$i as WebGL3DRenderTarget,Zi as WebGLArrayRenderTarget,Ws as WebGLCoordinateSystem,Xi as WebGLRenderTarget,Js as WebGPUCoordinateSystem,Cr as WebXRController,Dl as WireframeGeometry,je as WrapAroundEnding,De as ZeroCurvatureEnding,T as ZeroFactor,Ue as ZeroSlopeEnding,hs as ZeroStencilOp,Jl as cloneUniforms,Ks as createCanvasElement,Qs as createElementNS,oi as error,up as getByteLength,ii as getConsoleFunction,Xl as getUnlitUniformColorSpace,$s as isTypedArray,ri as log,ql as mergeUniforms,ci as probeAsync,si as setConsoleFunction,ai as warn,hi as warnOnce,li as yieldToMain}; diff --git a/website/vendor/three.module.min.js b/website/vendor/three.module.min.js new file mode 100644 index 00000000..5ae623ca --- /dev/null +++ b/website/vendor/three.module.min.js @@ -0,0 +1,6 @@ +/** + * @license + * Copyright 2010-2026 Three.js Authors + * SPDX-License-Identifier: MIT + */ +import{Matrix3 as e,Vector2 as t,Color as n,Vector3 as i,mergeUniforms as r,CubeUVReflectionMapping as a,Mesh as o,BoxGeometry as s,ShaderMaterial as l,BackSide as c,cloneUniforms as d,Matrix4 as u,ColorManagement as f,SRGBTransfer as p,PlaneGeometry as m,FrontSide as h,getUnlitUniformColorSpace as _,IntType as g,warn as v,HalfFloatType as E,UnsignedByteType as S,FloatType as M,RGBAFormat as T,Plane as x,CubeReflectionMapping as R,CubeRefractionMapping as A,BufferGeometry as b,OrthographicCamera as C,PerspectiveCamera as P,NoToneMapping as L,MeshBasicMaterial as U,error as D,NoBlending as w,WebGLRenderTarget as I,BufferAttribute as N,LinearSRGBColorSpace as y,LinearFilter as O,CubeTexture as F,LinearMipmapLinearFilter as B,CubeCamera as G,EquirectangularReflectionMapping as H,EquirectangularRefractionMapping as V,warnOnce as W,Uint32BufferAttribute as z,Uint16BufferAttribute as k,DataArrayTexture as X,Vector4 as K,DepthTexture as Y,Float32BufferAttribute as q,RawShaderMaterial as j,CustomToneMapping as Z,NeutralToneMapping as $,AgXToneMapping as Q,ACESFilmicToneMapping as J,CineonToneMapping as ee,ReinhardToneMapping as te,LinearToneMapping as ne,Data3DTexture as ie,GreaterEqualCompare as re,LessEqualCompare as ae,Texture as oe,GLSL3 as se,VSMShadowMap as le,PCFShadowMap as ce,AddOperation as de,MixOperation as ue,MultiplyOperation as fe,LinearTransfer as pe,UniformsUtils as me,DoubleSide as he,NormalBlending as _e,TangentSpaceNormalMap as ge,ObjectSpaceNormalMap as ve,Layers as Ee,RGFormat as Se,RG11_EAC_Format as Me,RED_GREEN_RGTC2_Format as Te,MeshDepthMaterial as xe,MeshDistanceMaterial as Re,PCFSoftShadowMap as Ae,DepthFormat as be,NearestFilter as Ce,CubeDepthTexture as Pe,UnsignedIntType as Le,Frustum as Ue,LessEqualDepth as De,ReverseSubtractEquation as we,SubtractEquation as Ie,AddEquation as Ne,OneMinusConstantAlphaFactor as ye,ConstantAlphaFactor as Oe,OneMinusConstantColorFactor as Fe,ConstantColorFactor as Be,OneMinusDstAlphaFactor as Ge,OneMinusDstColorFactor as He,OneMinusSrcAlphaFactor as Ve,OneMinusSrcColorFactor as We,DstAlphaFactor as ze,DstColorFactor as ke,SrcAlphaSaturateFactor as Xe,SrcAlphaFactor as Ke,SrcColorFactor as Ye,OneFactor as qe,ZeroFactor as je,NotEqualDepth as Ze,GreaterDepth as $e,GreaterEqualDepth as Qe,EqualDepth as Je,LessDepth as et,AlwaysDepth as tt,NeverDepth as nt,CullFaceNone as it,CullFaceBack as rt,CullFaceFront as at,CustomBlending as ot,MultiplyBlending as st,SubtractiveBlending as lt,AdditiveBlending as ct,ReversedDepthFuncs as dt,MinEquation as ut,MaxEquation as ft,MirroredRepeatWrapping as pt,ClampToEdgeWrapping as mt,RepeatWrapping as ht,LinearMipmapNearestFilter as _t,NearestMipmapLinearFilter as gt,NearestMipmapNearestFilter as vt,NotEqualCompare as Et,GreaterCompare as St,EqualCompare as Mt,LessCompare as Tt,AlwaysCompare as xt,NeverCompare as Rt,NoColorSpace as At,DepthStencilFormat as bt,getByteLength as Ct,UnsignedInt248Type as Pt,UnsignedShortType as Lt,createElementNS as Ut,UnsignedShort4444Type as Dt,UnsignedShort5551Type as wt,UnsignedInt5999Type as It,UnsignedInt101111Type as Nt,ByteType as yt,ShortType as Ot,AlphaFormat as Ft,RGBFormat as Bt,RedFormat as Gt,RedIntegerFormat as Ht,RGIntegerFormat as Vt,RGBAIntegerFormat as Wt,RGB_S3TC_DXT1_Format as zt,RGBA_S3TC_DXT1_Format as kt,RGBA_S3TC_DXT3_Format as Xt,RGBA_S3TC_DXT5_Format as Kt,RGB_PVRTC_4BPPV1_Format as Yt,RGB_PVRTC_2BPPV1_Format as qt,RGBA_PVRTC_4BPPV1_Format as jt,RGBA_PVRTC_2BPPV1_Format as Zt,RGB_ETC1_Format as $t,RGB_ETC2_Format as Qt,RGBA_ETC2_EAC_Format as Jt,R11_EAC_Format as en,SIGNED_R11_EAC_Format as tn,SIGNED_RG11_EAC_Format as nn,RGBA_ASTC_4x4_Format as rn,RGBA_ASTC_5x4_Format as an,RGBA_ASTC_5x5_Format as on,RGBA_ASTC_6x5_Format as sn,RGBA_ASTC_6x6_Format as ln,RGBA_ASTC_8x5_Format as cn,RGBA_ASTC_8x6_Format as dn,RGBA_ASTC_8x8_Format as un,RGBA_ASTC_10x5_Format as fn,RGBA_ASTC_10x6_Format as pn,RGBA_ASTC_10x8_Format as mn,RGBA_ASTC_10x10_Format as hn,RGBA_ASTC_12x10_Format as _n,RGBA_ASTC_12x12_Format as gn,RGBA_BPTC_Format as vn,RGB_BPTC_SIGNED_Format as En,RGB_BPTC_UNSIGNED_Format as Sn,RED_RGTC1_Format as Mn,SIGNED_RED_RGTC1_Format as Tn,SIGNED_RED_GREEN_RGTC2_Format as xn,ExternalTexture as Rn,EventDispatcher as An,ArrayCamera as bn,WebXRController as Cn,RAD2DEG as Pn,DataTexture as Ln,createCanvasElement as Un,SRGBColorSpace as Dn,REVISION as wn,log as In,WebGLCoordinateSystem as Nn,probeAsync as yn}from"./three.core.min.js";export{AdditiveAnimationBlendMode,AlwaysStencilFunc,AmbientLight,AnimationAction,AnimationClip,AnimationLoader,AnimationMixer,AnimationObjectGroup,AnimationUtils,ArcCurve,ArrowHelper,AttachedBindMode,Audio,AudioAnalyser,AudioContext,AudioListener,AudioLoader,AxesHelper,BasicDepthPacking,BasicShadowMap,BatchedMesh,BezierInterpolant,Bone,BooleanKeyframeTrack,Box2,Box3,Box3Helper,BoxHelper,BufferGeometryLoader,Cache,Camera,CameraHelper,CanvasTexture,CapsuleGeometry,CatmullRomCurve3,CircleGeometry,Clock,ColorKeyframeTrack,Compatibility,CompressedArrayTexture,CompressedCubeTexture,CompressedTexture,CompressedTextureLoader,ConeGeometry,Controls,CubeTextureLoader,CubicBezierCurve,CubicBezierCurve3,CubicInterpolant,CullFaceFrontBack,Curve,CurvePath,CylinderGeometry,Cylindrical,DataTextureLoader,DataUtils,DecrementStencilOp,DecrementWrapStencilOp,DefaultLoadingManager,DetachedBindMode,DirectionalLight,DirectionalLightHelper,DiscreteInterpolant,DodecahedronGeometry,DynamicCopyUsage,DynamicDrawUsage,DynamicReadUsage,EdgesGeometry,EllipseCurve,EqualStencilFunc,Euler,ExtrudeGeometry,FileLoader,Float16BufferAttribute,Fog,FogExp2,FramebufferTexture,FrustumArray,GLBufferAttribute,GLSL1,GreaterEqualStencilFunc,GreaterStencilFunc,GridHelper,Group,HTMLTexture,HemisphereLight,HemisphereLightHelper,IcosahedronGeometry,ImageBitmapLoader,ImageLoader,ImageUtils,IncrementStencilOp,IncrementWrapStencilOp,InstancedBufferAttribute,InstancedBufferGeometry,InstancedInterleavedBuffer,InstancedMesh,Int16BufferAttribute,Int32BufferAttribute,Int8BufferAttribute,InterleavedBuffer,InterleavedBufferAttribute,Interpolant,InterpolateBezier,InterpolateDiscrete,InterpolateLinear,InterpolateSmooth,InterpolationSamplingMode,InterpolationSamplingType,InvertStencilOp,KeepStencilOp,KeyframeTrack,LOD,LatheGeometry,LessEqualStencilFunc,LessStencilFunc,Light,LightProbe,Line,Line3,LineBasicMaterial,LineCurve,LineCurve3,LineDashedMaterial,LineLoop,LineSegments,LinearInterpolant,LinearMipMapLinearFilter,LinearMipMapNearestFilter,Loader,LoaderUtils,LoadingManager,LoopOnce,LoopPingPong,LoopRepeat,MOUSE,Material,MaterialBlending,MaterialLoader,MathUtils,Matrix2,MeshLambertMaterial,MeshMatcapMaterial,MeshNormalMaterial,MeshPhongMaterial,MeshPhysicalMaterial,MeshStandardMaterial,MeshToonMaterial,NearestMipMapLinearFilter,NearestMipMapNearestFilter,NeverStencilFunc,NoNormalPacking,NormalAnimationBlendMode,NormalGAPacking,NormalRGPacking,NotEqualStencilFunc,NumberKeyframeTrack,Object3D,ObjectLoader,OctahedronGeometry,Path,PlaneHelper,PointLight,PointLightHelper,Points,PointsMaterial,PolarGridHelper,PolyhedronGeometry,PositionalAudio,PropertyBinding,PropertyMixer,QuadraticBezierCurve,QuadraticBezierCurve3,Quaternion,QuaternionKeyframeTrack,QuaternionLinearInterpolant,RGBADepthPacking,RGBDepthPacking,RGBIntegerFormat,RGDepthPacking,Ray,Raycaster,RectAreaLight,RenderTarget,RenderTarget3D,ReplaceStencilOp,RingGeometry,Scene,ShadowMaterial,Shape,ShapeGeometry,ShapePath,ShapeUtils,Skeleton,SkeletonHelper,SkinnedMesh,Source,Sphere,SphereGeometry,Spherical,SphericalHarmonics3,SplineCurve,SpotLight,SpotLightHelper,Sprite,SpriteMaterial,StaticCopyUsage,StaticDrawUsage,StaticReadUsage,StereoCamera,StreamCopyUsage,StreamDrawUsage,StreamReadUsage,StringKeyframeTrack,TOUCH,TetrahedronGeometry,TextureLoader,TextureUtils,Timer,TimestampQuery,TorusGeometry,TorusKnotGeometry,Triangle,TriangleFanDrawMode,TriangleStripDrawMode,TrianglesDrawMode,TubeGeometry,UVMapping,Uint8BufferAttribute,Uint8ClampedBufferAttribute,Uniform,UniformsGroup,VectorKeyframeTrack,VideoFrameTexture,VideoTexture,WebGL3DRenderTarget,WebGLArrayRenderTarget,WebGPUCoordinateSystem,WireframeGeometry,WrapAroundEnding,ZeroCurvatureEnding,ZeroSlopeEnding,ZeroStencilOp,getConsoleFunction,setConsoleFunction}from"./three.core.min.js";function On(){let e=null,t=!1,n=null,i=null;function r(t,a){n(t,a),i=e.requestAnimationFrame(r)}return{start:function(){!0!==t&&null!==n&&null!==e&&(i=e.requestAnimationFrame(r),t=!0)},stop:function(){null!==e&&e.cancelAnimationFrame(i),t=!1},setAnimationLoop:function(e){n=e},setContext:function(t){e=t}}}function Fn(e){const t=new WeakMap;return{get:function(e){return e.isInterleavedBufferAttribute&&(e=e.data),t.get(e)},remove:function(n){n.isInterleavedBufferAttribute&&(n=n.data);const i=t.get(n);i&&(e.deleteBuffer(i.buffer),t.delete(n))},update:function(n,i){if(n.isInterleavedBufferAttribute&&(n=n.data),n.isGLBufferAttribute){const e=t.get(n);return void((!e||e.versione.start-t.start);let t=0;for(let e=1;e 0\n\tvec4 plane;\n\t#ifdef ALPHA_TO_COVERAGE\n\t\tfloat distanceToPlane, distanceGradient;\n\t\tfloat clipOpacity = 1.0;\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\n\t\t\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\n\t\t\tclipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\n\t\t\tif ( clipOpacity == 0.0 ) discard;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\t\tfloat unionClipOpacity = 1.0;\n\t\t\t#pragma unroll_loop_start\n\t\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\t\tplane = clippingPlanes[ i ];\n\t\t\t\tdistanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w;\n\t\t\t\tdistanceGradient = fwidth( distanceToPlane ) / 2.0;\n\t\t\t\tunionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane );\n\t\t\t}\n\t\t\t#pragma unroll_loop_end\n\t\t\tclipOpacity *= 1.0 - unionClipOpacity;\n\t\t#endif\n\t\tdiffuseColor.a *= clipOpacity;\n\t\tif ( diffuseColor.a == 0.0 ) discard;\n\t#else\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tif ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\t\tbool clipped = true;\n\t\t\t#pragma unroll_loop_start\n\t\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\t\tplane = clippingPlanes[ i ];\n\t\t\t\tclipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;\n\t\t\t}\n\t\t\t#pragma unroll_loop_end\n\t\t\tif ( clipped ) discard;\n\t\t#endif\n\t#endif\n#endif",clipping_planes_pars_fragment:"#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\n#endif",clipping_planes_pars_vertex:"#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n#endif",clipping_planes_vertex:"#if NUM_CLIPPING_PLANES > 0\n\tvClipPosition = - mvPosition.xyz;\n#endif",color_fragment:"#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA )\n\tdiffuseColor *= vColor;\n#endif",color_pars_fragment:"#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#endif",color_pars_vertex:"#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\n\tvarying vec4 vColor;\n#endif",color_vertex:"#if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )\n\tvColor = vec4( 1.0 );\n#endif\n#ifdef USE_COLOR_ALPHA\n\tvColor *= color;\n#elif defined( USE_COLOR )\n\tvColor.rgb *= color;\n#endif\n#ifdef USE_INSTANCING_COLOR\n\tvColor.rgb *= instanceColor.rgb;\n#endif\n#ifdef USE_BATCHING_COLOR\n\tvColor *= getBatchingColor( getIndirectIndex( gl_DrawID ) );\n#endif",common:"#define PI 3.141592653589793\n#define PI2 6.283185307179586\n#define PI_HALF 1.5707963267948966\n#define RECIPROCAL_PI 0.3183098861837907\n#define RECIPROCAL_PI2 0.15915494309189535\n#define EPSILON 1e-6\n#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\n#define whiteComplement( a ) ( 1.0 - saturate( a ) )\nfloat pow2( const in float x ) { return x*x; }\nvec3 pow2( const in vec3 x ) { return x*x; }\nfloat pow3( const in float x ) { return x*x*x; }\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\nfloat max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); }\nfloat average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); }\nhighp float rand( const in vec2 uv ) {\n\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\n\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\n\treturn fract( sin( sn ) * c );\n}\n#ifdef HIGH_PRECISION\n\tfloat precisionSafeLength( vec3 v ) { return length( v ); }\n#else\n\tfloat precisionSafeLength( vec3 v ) {\n\t\tfloat maxComponent = max3( abs( v ) );\n\t\treturn length( v / maxComponent ) * maxComponent;\n\t}\n#endif\nstruct IncidentLight {\n\tvec3 color;\n\tvec3 direction;\n\tbool visible;\n};\nstruct ReflectedLight {\n\tvec3 directDiffuse;\n\tvec3 directSpecular;\n\tvec3 indirectDiffuse;\n\tvec3 indirectSpecular;\n};\n#ifdef USE_ALPHAHASH\n\tvarying vec3 vPosition;\n#endif\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n}\n#define inverseTransformDirection transformDirectionByInverseViewMatrix\nvec3 transformNormalByInverseViewMatrix( in vec3 normal, in mat4 viewMatrix ) {\n\treturn normalize( ( vec4( normal, 0.0 ) * viewMatrix ).xyz );\n}\nvec3 transformDirectionByInverseViewMatrix( in vec3 dir, in mat4 viewMatrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * viewMatrix ).xyz );\n}\nbool isPerspectiveMatrix( mat4 m ) {\n\treturn m[ 2 ][ 3 ] == - 1.0;\n}\nvec2 equirectUv( in vec3 dir ) {\n\tfloat u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5;\n\tfloat v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\treturn vec2( u, v );\n}\nvec3 BRDF_Lambert( const in vec3 diffuseColor ) {\n\treturn RECIPROCAL_PI * diffuseColor;\n}\nvec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) {\n\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\n\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\n}\nfloat F_Schlick( const in float f0, const in float f90, const in float dotVH ) {\n\tfloat fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );\n\treturn f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );\n} // validated",cube_uv_reflection_fragment:"#ifdef ENVMAP_TYPE_CUBE_UV\n\t#define cubeUV_minMipLevel 4.0\n\t#define cubeUV_minTileSize 16.0\n\tfloat getFace( vec3 direction ) {\n\t\tvec3 absDirection = abs( direction );\n\t\tfloat face = - 1.0;\n\t\tif ( absDirection.x > absDirection.z ) {\n\t\t\tif ( absDirection.x > absDirection.y )\n\t\t\t\tface = direction.x > 0.0 ? 0.0 : 3.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t} else {\n\t\t\tif ( absDirection.z > absDirection.y )\n\t\t\t\tface = direction.z > 0.0 ? 2.0 : 5.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t}\n\t\treturn face;\n\t}\n\tvec2 getUV( vec3 direction, float face ) {\n\t\tvec2 uv;\n\t\tif ( face == 0.0 ) {\n\t\t\tuv = vec2( direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 1.0 ) {\n\t\t\tuv = vec2( - direction.x, - direction.z ) / abs( direction.y );\n\t\t} else if ( face == 2.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.y ) / abs( direction.z );\n\t\t} else if ( face == 3.0 ) {\n\t\t\tuv = vec2( - direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 4.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.z ) / abs( direction.y );\n\t\t} else {\n\t\t\tuv = vec2( direction.x, direction.y ) / abs( direction.z );\n\t\t}\n\t\treturn 0.5 * ( uv + 1.0 );\n\t}\n\tvec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) {\n\t\tfloat face = getFace( direction );\n\t\tfloat filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 );\n\t\tmipInt = max( mipInt, cubeUV_minMipLevel );\n\t\tfloat faceSize = exp2( mipInt );\n\t\thighp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0;\n\t\tif ( face > 2.0 ) {\n\t\t\tuv.y += faceSize;\n\t\t\tface -= 3.0;\n\t\t}\n\t\tuv.x += face * faceSize;\n\t\tuv.x += filterInt * 3.0 * cubeUV_minTileSize;\n\t\tuv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize );\n\t\tuv.x *= CUBEUV_TEXEL_WIDTH;\n\t\tuv.y *= CUBEUV_TEXEL_HEIGHT;\n\t\t#ifdef texture2DGradEXT\n\t\t\treturn texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb;\n\t\t#else\n\t\t\treturn texture2D( envMap, uv ).rgb;\n\t\t#endif\n\t}\n\t#define cubeUV_r0 1.0\n\t#define cubeUV_m0 - 2.0\n\t#define cubeUV_r1 0.8\n\t#define cubeUV_m1 - 1.0\n\t#define cubeUV_r4 0.4\n\t#define cubeUV_m4 2.0\n\t#define cubeUV_r5 0.305\n\t#define cubeUV_m5 3.0\n\t#define cubeUV_r6 0.21\n\t#define cubeUV_m6 4.0\n\tfloat roughnessToMip( float roughness ) {\n\t\tfloat mip = 0.0;\n\t\tif ( roughness >= cubeUV_r1 ) {\n\t\t\tmip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0;\n\t\t} else if ( roughness >= cubeUV_r4 ) {\n\t\t\tmip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1;\n\t\t} else if ( roughness >= cubeUV_r5 ) {\n\t\t\tmip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4;\n\t\t} else if ( roughness >= cubeUV_r6 ) {\n\t\t\tmip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5;\n\t\t} else {\n\t\t\tmip = - 2.0 * log2( 1.16 * roughness );\t\t}\n\t\treturn mip;\n\t}\n\tvec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) {\n\t\tfloat mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP );\n\t\tfloat mipF = fract( mip );\n\t\tfloat mipInt = floor( mip );\n\t\tvec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt );\n\t\tif ( mipF == 0.0 ) {\n\t\t\treturn vec4( color0, 1.0 );\n\t\t} else {\n\t\t\tvec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 );\n\t\t\treturn vec4( mix( color0, color1, mipF ), 1.0 );\n\t\t}\n\t}\n#endif",defaultnormal_vertex:"vec3 transformedNormal = objectNormal;\n#ifdef USE_TANGENT\n\tvec3 transformedTangent = objectTangent;\n#endif\n#ifdef USE_BATCHING\n\tmat3 bm = mat3( batchingMatrix );\n\ttransformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) );\n\ttransformedNormal = bm * transformedNormal;\n\t#ifdef USE_TANGENT\n\t\ttransformedTangent = bm * transformedTangent;\n\t#endif\n#endif\n#ifdef USE_INSTANCING\n\tmat3 im = mat3( instanceMatrix );\n\ttransformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) );\n\ttransformedNormal = im * transformedNormal;\n\t#ifdef USE_TANGENT\n\t\ttransformedTangent = im * transformedTangent;\n\t#endif\n#endif\ntransformedNormal = normalMatrix * transformedNormal;\n#ifdef FLIP_SIDED\n\ttransformedNormal = - transformedNormal;\n#endif\n#ifdef USE_TANGENT\n\ttransformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz;\n#endif",displacementmap_pars_vertex:"#ifdef USE_DISPLACEMENTMAP\n\tuniform sampler2D displacementMap;\n\tuniform float displacementScale;\n\tuniform float displacementBias;\n#endif",displacementmap_vertex:"#ifdef USE_DISPLACEMENTMAP\n\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias );\n#endif",emissivemap_fragment:"#ifdef USE_EMISSIVEMAP\n\tvec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv );\n\t#ifdef DECODE_VIDEO_TEXTURE_EMISSIVE\n\t\temissiveColor = sRGBTransferEOTF( emissiveColor );\n\t#endif\n\ttotalEmissiveRadiance *= emissiveColor.rgb;\n#endif",emissivemap_pars_fragment:"#ifdef USE_EMISSIVEMAP\n\tuniform sampler2D emissiveMap;\n#endif",colorspace_fragment:"gl_FragColor = linearToOutputTexel( gl_FragColor );",colorspace_pars_fragment:"vec4 LinearTransferOETF( in vec4 value ) {\n\treturn value;\n}\nvec4 sRGBTransferEOTF( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );\n}\nvec4 sRGBTransferOETF( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );\n}",envmap_fragment:"#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvec3 cameraToFrag;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToFrag = normalize( vWorldPosition - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( cameraToFrag, worldNormal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio );\n\t\t#endif\n\t#else\n\t\tvec3 reflectVec = vReflect;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 envColor = textureCube( envMap, envMapRotation * reflectVec );\n\t\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t\t#endif\n\t#endif\n#endif",envmap_common_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float envMapIntensity;\n\tuniform mat3 envMapRotation;\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tuniform samplerCube envMap;\n\t#else\n\t\tuniform sampler2D envMap;\n\t#endif\n#endif",envmap_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float reflectivity;\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\tvarying vec3 vWorldPosition;\n\t\tuniform float refractionRatio;\n\t#else\n\t\tvarying vec3 vReflect;\n\t#endif\n#endif",envmap_pars_vertex:"#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\t\n\t\tvarying vec3 vWorldPosition;\n\t#else\n\t\tvarying vec3 vReflect;\n\t\tuniform float refractionRatio;\n\t#endif\n#endif",envmap_physical_pars_fragment:"#ifdef USE_ENVMAP\n\tvec3 getIBLIrradiance( const in vec3 normal ) {\n\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\tvec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 );\n\t\t\treturn PI * envMapColor.rgb * envMapIntensity;\n\t\t#else\n\t\t\treturn vec3( 0.0 );\n\t\t#endif\n\t}\n\tvec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) {\n\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\tvec3 reflectVec = reflect( - viewDir, normal );\n\t\t\treflectVec = normalize( mix( reflectVec, normal, pow4( roughness ) ) );\n\t\t\treflectVec = transformDirectionByInverseViewMatrix( reflectVec, viewMatrix );\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness );\n\t\t\treturn envMapColor.rgb * envMapIntensity;\n\t\t#else\n\t\t\treturn vec3( 0.0 );\n\t\t#endif\n\t}\n\t#ifdef USE_ANISOTROPY\n\t\tvec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) {\n\t\t\t#ifdef ENVMAP_TYPE_CUBE_UV\n\t\t\t\tvec3 bentNormal = cross( bitangent, viewDir );\n\t\t\t\tbentNormal = normalize( cross( bentNormal, bitangent ) );\n\t\t\t\tbentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) );\n\t\t\t\treturn getIBLRadiance( viewDir, bentNormal, roughness );\n\t\t\t#else\n\t\t\t\treturn vec3( 0.0 );\n\t\t\t#endif\n\t\t}\n\t#endif\n#endif",envmap_vertex:"#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvWorldPosition = worldPosition.xyz;\n\t#else\n\t\tvec3 cameraToVertex;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = transformNormalByInverseViewMatrix( transformedNormal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvReflect = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#endif\n#endif",fog_vertex:"#ifdef USE_FOG\n\tvFogDepth = - mvPosition.z;\n#endif",fog_pars_vertex:"#ifdef USE_FOG\n\tvarying float vFogDepth;\n#endif",fog_fragment:"#ifdef USE_FOG\n\t#ifdef FOG_EXP2\n\t\tfloat fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth );\n\t#else\n\t\tfloat fogFactor = smoothstep( fogNear, fogFar, vFogDepth );\n\t#endif\n\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n#endif",fog_pars_fragment:"#ifdef USE_FOG\n\tuniform vec3 fogColor;\n\tvarying float vFogDepth;\n\t#ifdef FOG_EXP2\n\t\tuniform float fogDensity;\n\t#else\n\t\tuniform float fogNear;\n\t\tuniform float fogFar;\n\t#endif\n#endif",gradientmap_pars_fragment:"#ifdef USE_GRADIENTMAP\n\tuniform sampler2D gradientMap;\n#endif\nvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\n\tfloat dotNL = dot( normal, lightDirection );\n\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\n\t#ifdef USE_GRADIENTMAP\n\t\treturn vec3( texture2D( gradientMap, coord ).r );\n\t#else\n\t\tvec2 fw = fwidth( coord ) * 0.5;\n\t\treturn mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) );\n\t#endif\n}",lightmap_pars_fragment:"#ifdef USE_LIGHTMAP\n\tuniform sampler2D lightMap;\n\tuniform float lightMapIntensity;\n#endif",lights_lambert_fragment:"LambertMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularStrength = specularStrength;",lights_lambert_pars_fragment:"varying vec3 vViewPosition;\nstruct LambertMaterial {\n\tvec3 diffuseColor;\n\tfloat specularStrength;\n};\nvoid RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Lambert\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Lambert",lights_pars_begin:"uniform bool receiveShadow;\nuniform vec3 ambientLightColor;\n#if defined( USE_LIGHT_PROBES )\n\tuniform vec3 lightProbe[ 9 ];\n#endif\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\n\tfloat x = normal.x, y = normal.y, z = normal.z;\n\tvec3 result = shCoefficients[ 0 ] * 0.886227;\n\tresult += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;\n\tresult += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;\n\tresult += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;\n\tresult += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;\n\tresult += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;\n\tresult += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );\n\tresult += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;\n\tresult += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );\n\treturn result;\n}\nvec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) {\n\tvec3 worldNormal = transformNormalByInverseViewMatrix( normal, viewMatrix );\n\tvec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );\n\treturn irradiance;\n}\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\n\tvec3 irradiance = ambientLightColor;\n\treturn irradiance;\n}\nfloat getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {\n\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\n\tif ( cutoffDistance > 0.0 ) {\n\t\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t}\n\treturn distanceFalloff;\n}\nfloat getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) {\n\treturn smoothstep( coneCosine, penumbraCosine, angleCosine );\n}\n#if NUM_DIR_LIGHTS > 0\n\tstruct DirectionalLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t};\n\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\n\tvoid getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) {\n\t\tlight.color = directionalLight.color;\n\t\tlight.direction = directionalLight.direction;\n\t\tlight.visible = true;\n\t}\n#endif\n#if NUM_POINT_LIGHTS > 0\n\tstruct PointLight {\n\t\tvec3 position;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t};\n\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\n\tvoid getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) {\n\t\tvec3 lVector = pointLight.position - geometryPosition;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tlight.color = pointLight.color;\n\t\tlight.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay );\n\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\tstruct SpotLight {\n\t\tvec3 position;\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tfloat coneCos;\n\t\tfloat penumbraCos;\n\t};\n\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\n\tvoid getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) {\n\t\tvec3 lVector = spotLight.position - geometryPosition;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat angleCos = dot( light.direction, spotLight.direction );\n\t\tfloat spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos );\n\t\tif ( spotAttenuation > 0.0 ) {\n\t\t\tfloat lightDistance = length( lVector );\n\t\t\tlight.color = spotLight.color * spotAttenuation;\n\t\t\tlight.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay );\n\t\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t\t} else {\n\t\t\tlight.color = vec3( 0.0 );\n\t\t\tlight.visible = false;\n\t\t}\n\t}\n#endif\n#if NUM_RECT_AREA_LIGHTS > 0\n\tstruct RectAreaLight {\n\t\tvec3 color;\n\t\tvec3 position;\n\t\tvec3 halfWidth;\n\t\tvec3 halfHeight;\n\t};\n\tuniform sampler2D ltc_1;\tuniform sampler2D ltc_2;\n\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\tstruct HemisphereLight {\n\t\tvec3 direction;\n\t\tvec3 skyColor;\n\t\tvec3 groundColor;\n\t};\n\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\n\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) {\n\t\tfloat dotNL = dot( normal, hemiLight.direction );\n\t\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\n\t\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\n\t\treturn irradiance;\n\t}\n#endif\n#include ",lights_toon_fragment:"ToonMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;",lights_toon_pars_fragment:"varying vec3 vViewPosition;\nstruct ToonMaterial {\n\tvec3 diffuseColor;\n};\nvoid RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\tvec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Toon\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Toon",lights_phong_fragment:"BlinnPhongMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularColor = specular;\nmaterial.specularShininess = shininess;\nmaterial.specularStrength = specularStrength;",lights_phong_pars_fragment:"varying vec3 vViewPosition;\nstruct BlinnPhongMaterial {\n\tvec3 diffuseColor;\n\tvec3 specularColor;\n\tfloat specularShininess;\n\tfloat specularStrength;\n};\nvoid RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n\treflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength;\n}\nvoid RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_BlinnPhong\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_BlinnPhong",lights_physical_fragment:"PhysicalMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.diffuseContribution = diffuseColor.rgb * ( 1.0 - metalnessFactor );\nmaterial.metalness = metalnessFactor;\nvec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) );\nfloat geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );\nmaterial.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness;\nmaterial.roughness = min( material.roughness, 1.0 );\n#ifdef IOR\n\tmaterial.ior = ior;\n\t#ifdef USE_SPECULAR\n\t\tfloat specularIntensityFactor = specularIntensity;\n\t\tvec3 specularColorFactor = specularColor;\n\t\t#ifdef USE_SPECULAR_COLORMAP\n\t\t\tspecularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb;\n\t\t#endif\n\t\t#ifdef USE_SPECULAR_INTENSITYMAP\n\t\t\tspecularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a;\n\t\t#endif\n\t\tmaterial.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor );\n\t#else\n\t\tfloat specularIntensityFactor = 1.0;\n\t\tvec3 specularColorFactor = vec3( 1.0 );\n\t\tmaterial.specularF90 = 1.0;\n\t#endif\n\tmaterial.specularColor = min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor;\n\tmaterial.specularColorBlended = mix( material.specularColor, diffuseColor.rgb, metalnessFactor );\n#else\n\tmaterial.specularColor = vec3( 0.04 );\n\tmaterial.specularColorBlended = mix( material.specularColor, diffuseColor.rgb, metalnessFactor );\n\tmaterial.specularF90 = 1.0;\n#endif\n#ifdef USE_CLEARCOAT\n\tmaterial.clearcoat = clearcoat;\n\tmaterial.clearcoatRoughness = clearcoatRoughness;\n\tmaterial.clearcoatF0 = vec3( 0.04 );\n\tmaterial.clearcoatF90 = 1.0;\n\t#ifdef USE_CLEARCOATMAP\n\t\tmaterial.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x;\n\t#endif\n\t#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\t\tmaterial.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y;\n\t#endif\n\tmaterial.clearcoat = saturate( material.clearcoat );\tmaterial.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 );\n\tmaterial.clearcoatRoughness += geometryRoughness;\n\tmaterial.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 );\n#endif\n#ifdef USE_DISPERSION\n\tmaterial.dispersion = dispersion;\n#endif\n#ifdef USE_IRIDESCENCE\n\tmaterial.iridescence = iridescence;\n\tmaterial.iridescenceIOR = iridescenceIOR;\n\t#ifdef USE_IRIDESCENCEMAP\n\t\tmaterial.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r;\n\t#endif\n\t#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\t\tmaterial.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum;\n\t#else\n\t\tmaterial.iridescenceThickness = iridescenceThicknessMaximum;\n\t#endif\n#endif\n#ifdef USE_SHEEN\n\tmaterial.sheenColor = sheenColor;\n\t#ifdef USE_SHEEN_COLORMAP\n\t\tmaterial.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb;\n\t#endif\n\tmaterial.sheenRoughness = clamp( sheenRoughness, 0.0001, 1.0 );\n\t#ifdef USE_SHEEN_ROUGHNESSMAP\n\t\tmaterial.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a;\n\t#endif\n#endif\n#ifdef USE_ANISOTROPY\n\t#ifdef USE_ANISOTROPYMAP\n\t\tmat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x );\n\t\tvec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb;\n\t\tvec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b;\n\t#else\n\t\tvec2 anisotropyV = anisotropyVector;\n\t#endif\n\tmaterial.anisotropy = length( anisotropyV );\n\tif( material.anisotropy == 0.0 ) {\n\t\tanisotropyV = vec2( 1.0, 0.0 );\n\t} else {\n\t\tanisotropyV /= material.anisotropy;\n\t\tmaterial.anisotropy = saturate( material.anisotropy );\n\t}\n\tmaterial.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) );\n\tmaterial.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y;\n\tmaterial.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y;\n#endif",lights_physical_pars_fragment:"uniform sampler2D dfgLUT;\nstruct PhysicalMaterial {\n\tvec3 diffuseColor;\n\tvec3 diffuseContribution;\n\tvec3 specularColor;\n\tvec3 specularColorBlended;\n\tfloat roughness;\n\tfloat metalness;\n\tfloat specularF90;\n\tfloat dispersion;\n\t#ifdef USE_CLEARCOAT\n\t\tfloat clearcoat;\n\t\tfloat clearcoatRoughness;\n\t\tvec3 clearcoatF0;\n\t\tfloat clearcoatF90;\n\t#endif\n\t#ifdef USE_IRIDESCENCE\n\t\tfloat iridescence;\n\t\tfloat iridescenceIOR;\n\t\tfloat iridescenceThickness;\n\t\tvec3 iridescenceFresnel;\n\t\tvec3 iridescenceF0;\n\t\tvec3 iridescenceFresnelDielectric;\n\t\tvec3 iridescenceFresnelMetallic;\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tvec3 sheenColor;\n\t\tfloat sheenRoughness;\n\t#endif\n\t#ifdef IOR\n\t\tfloat ior;\n\t#endif\n\t#ifdef USE_TRANSMISSION\n\t\tfloat transmission;\n\t\tfloat transmissionAlpha;\n\t\tfloat thickness;\n\t\tfloat attenuationDistance;\n\t\tvec3 attenuationColor;\n\t#endif\n\t#ifdef USE_ANISOTROPY\n\t\tfloat anisotropy;\n\t\tfloat alphaT;\n\t\tvec3 anisotropyT;\n\t\tvec3 anisotropyB;\n\t#endif\n};\nvec3 clearcoatSpecularDirect = vec3( 0.0 );\nvec3 clearcoatSpecularIndirect = vec3( 0.0 );\nvec3 sheenSpecularDirect = vec3( 0.0 );\nvec3 sheenSpecularIndirect = vec3(0.0 );\nvec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) {\n float x = clamp( 1.0 - dotVH, 0.0, 1.0 );\n float x2 = x * x;\n float x5 = clamp( x * x2 * x2, 0.0, 0.9999 );\n return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 );\n}\nfloat V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\tfloat gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\treturn 0.5 / max( gv + gl, EPSILON );\n}\nfloat D_GGX( const in float alpha, const in float dotNH ) {\n\tfloat a2 = pow2( alpha );\n\tfloat denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0;\n\treturn RECIPROCAL_PI * a2 / pow2( denom );\n}\n#ifdef USE_ANISOTROPY\n\tfloat V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) {\n\t\tfloat gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) );\n\t\tfloat gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) );\n\t\treturn 0.5 / max( gv + gl, EPSILON );\n\t}\n\tfloat D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) {\n\t\tfloat a2 = alphaT * alphaB;\n\t\thighp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH );\n\t\thighp float v2 = dot( v, v );\n\t\tfloat w2 = a2 / v2;\n\t\treturn RECIPROCAL_PI * a2 * pow2 ( w2 );\n\t}\n#endif\n#ifdef USE_CLEARCOAT\n\tvec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) {\n\t\tvec3 f0 = material.clearcoatF0;\n\t\tfloat f90 = material.clearcoatF90;\n\t\tfloat roughness = material.clearcoatRoughness;\n\t\tfloat alpha = pow2( roughness );\n\t\tvec3 halfDir = normalize( lightDir + viewDir );\n\t\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\t\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\t\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\t\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\t\tvec3 F = F_Schlick( f0, f90, dotVH );\n\t\tfloat V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\t\tfloat D = D_GGX( alpha, dotNH );\n\t\treturn F * ( V * D );\n\t}\n#endif\nvec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) {\n\tvec3 f0 = material.specularColorBlended;\n\tfloat f90 = material.specularF90;\n\tfloat roughness = material.roughness;\n\tfloat alpha = pow2( roughness );\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\tvec3 F = F_Schlick( f0, f90, dotVH );\n\t#ifdef USE_IRIDESCENCE\n\t\tF = mix( F, material.iridescenceFresnel, material.iridescence );\n\t#endif\n\t#ifdef USE_ANISOTROPY\n\t\tfloat dotTL = dot( material.anisotropyT, lightDir );\n\t\tfloat dotTV = dot( material.anisotropyT, viewDir );\n\t\tfloat dotTH = dot( material.anisotropyT, halfDir );\n\t\tfloat dotBL = dot( material.anisotropyB, lightDir );\n\t\tfloat dotBV = dot( material.anisotropyB, viewDir );\n\t\tfloat dotBH = dot( material.anisotropyB, halfDir );\n\t\tfloat V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL );\n\t\tfloat D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH );\n\t#else\n\t\tfloat V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\t\tfloat D = D_GGX( alpha, dotNH );\n\t#endif\n\treturn F * ( V * D );\n}\nvec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) {\n\tconst float LUT_SIZE = 64.0;\n\tconst float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;\n\tconst float LUT_BIAS = 0.5 / LUT_SIZE;\n\tfloat dotNV = saturate( dot( N, V ) );\n\tvec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) );\n\tuv = uv * LUT_SCALE + LUT_BIAS;\n\treturn uv;\n}\nfloat LTC_ClippedSphereFormFactor( const in vec3 f ) {\n\tfloat l = length( f );\n\treturn max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 );\n}\nvec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) {\n\tfloat x = dot( v1, v2 );\n\tfloat y = abs( x );\n\tfloat a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y;\n\tfloat b = 3.4175940 + ( 4.1616724 + y ) * y;\n\tfloat v = a / b;\n\tfloat theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v;\n\treturn cross( v1, v2 ) * theta_sintheta;\n}\nvec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) {\n\tvec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ];\n\tvec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ];\n\tvec3 lightNormal = cross( v1, v2 );\n\tif( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 );\n\tvec3 T1, T2;\n\tT1 = normalize( V - N * dot( V, N ) );\n\tT2 = - cross( N, T1 );\n\tmat3 mat = mInv * transpose( mat3( T1, T2, N ) );\n\tvec3 coords[ 4 ];\n\tcoords[ 0 ] = mat * ( rectCoords[ 0 ] - P );\n\tcoords[ 1 ] = mat * ( rectCoords[ 1 ] - P );\n\tcoords[ 2 ] = mat * ( rectCoords[ 2 ] - P );\n\tcoords[ 3 ] = mat * ( rectCoords[ 3 ] - P );\n\tcoords[ 0 ] = normalize( coords[ 0 ] );\n\tcoords[ 1 ] = normalize( coords[ 1 ] );\n\tcoords[ 2 ] = normalize( coords[ 2 ] );\n\tcoords[ 3 ] = normalize( coords[ 3 ] );\n\tvec3 vectorFormFactor = vec3( 0.0 );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] );\n\tfloat result = LTC_ClippedSphereFormFactor( vectorFormFactor );\n\treturn vec3( result );\n}\n#if defined( USE_SHEEN )\nfloat D_Charlie( float roughness, float dotNH ) {\n\tfloat alpha = pow2( roughness );\n\tfloat invAlpha = 1.0 / alpha;\n\tfloat cos2h = dotNH * dotNH;\n\tfloat sin2h = max( 1.0 - cos2h, 0.0078125 );\n\treturn ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI );\n}\nfloat V_Neubelt( float dotNV, float dotNL ) {\n\treturn saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) );\n}\nvec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) {\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat D = D_Charlie( sheenRoughness, dotNH );\n\tfloat V = V_Neubelt( dotNV, dotNL );\n\treturn sheenColor * ( D * V );\n}\n#endif\nfloat IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat r2 = roughness * roughness;\n\tfloat rInv = 1.0 / ( roughness + 0.1 );\n\tfloat a = -1.9362 + 1.0678 * roughness + 0.4573 * r2 - 0.8469 * rInv;\n\tfloat b = -0.6014 + 0.5538 * roughness - 0.4670 * r2 - 0.1255 * rInv;\n\tfloat DG = exp( a * dotNV + b );\n\treturn saturate( DG );\n}\nvec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tvec2 fab = texture2D( dfgLUT, vec2( roughness, dotNV ) ).rg;\n\treturn specularColor * fab.x + specularF90 * fab.y;\n}\n#ifdef USE_IRIDESCENCE\nvoid computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n#else\nvoid computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n#endif\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tvec2 fab = texture2D( dfgLUT, vec2( roughness, dotNV ) ).rg;\n\t#ifdef USE_IRIDESCENCE\n\t\tvec3 Fr = mix( specularColor, iridescenceF0, iridescence );\n\t#else\n\t\tvec3 Fr = specularColor;\n\t#endif\n\tvec3 FssEss = Fr * fab.x + specularF90 * fab.y;\n\tfloat Ess = fab.x + fab.y;\n\tfloat Ems = 1.0 - Ess;\n\tvec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619;\tvec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );\n\tsingleScatter += FssEss;\n\tmultiScatter += Fms * Ems;\n}\nvec3 BRDF_GGX_Multiscatter( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) {\n\tvec3 singleScatter = BRDF_GGX( lightDir, viewDir, normal, material );\n\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tvec2 dfgV = texture2D( dfgLUT, vec2( material.roughness, dotNV ) ).rg;\n\tvec2 dfgL = texture2D( dfgLUT, vec2( material.roughness, dotNL ) ).rg;\n\tvec3 FssEss_V = material.specularColorBlended * dfgV.x + material.specularF90 * dfgV.y;\n\tvec3 FssEss_L = material.specularColorBlended * dfgL.x + material.specularF90 * dfgL.y;\n\tfloat Ess_V = dfgV.x + dfgV.y;\n\tfloat Ess_L = dfgL.x + dfgL.y;\n\tfloat Ems_V = 1.0 - Ess_V;\n\tfloat Ems_L = 1.0 - Ess_L;\n\tvec3 Favg = material.specularColorBlended + ( 1.0 - material.specularColorBlended ) * 0.047619;\n\tvec3 Fms = FssEss_V * FssEss_L * Favg / ( 1.0 - Ems_V * Ems_L * Favg + EPSILON );\n\tfloat compensationFactor = Ems_V * Ems_L;\n\tvec3 multiScatter = Fms * compensationFactor;\n\treturn singleScatter + multiScatter;\n}\n#if NUM_RECT_AREA_LIGHTS > 0\n\tvoid RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\t\tvec3 normal = geometryNormal;\n\t\tvec3 viewDir = geometryViewDir;\n\t\tvec3 position = geometryPosition;\n\t\tvec3 lightPos = rectAreaLight.position;\n\t\tvec3 halfWidth = rectAreaLight.halfWidth;\n\t\tvec3 halfHeight = rectAreaLight.halfHeight;\n\t\tvec3 lightColor = rectAreaLight.color;\n\t\tfloat roughness = material.roughness;\n\t\tvec3 rectCoords[ 4 ];\n\t\trectCoords[ 0 ] = lightPos + halfWidth - halfHeight;\t\trectCoords[ 1 ] = lightPos - halfWidth - halfHeight;\n\t\trectCoords[ 2 ] = lightPos - halfWidth + halfHeight;\n\t\trectCoords[ 3 ] = lightPos + halfWidth + halfHeight;\n\t\tvec2 uv = LTC_Uv( normal, viewDir, roughness );\n\t\tvec4 t1 = texture2D( ltc_1, uv );\n\t\tvec4 t2 = texture2D( ltc_2, uv );\n\t\tmat3 mInv = mat3(\n\t\t\tvec3( t1.x, 0, t1.y ),\n\t\t\tvec3( 0, 1, 0 ),\n\t\t\tvec3( t1.z, 0, t1.w )\n\t\t);\n\t\tvec3 fresnel = ( material.specularColorBlended * t2.x + ( material.specularF90 - material.specularColorBlended ) * t2.y );\n\t\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\n\t\treflectedLight.directDiffuse += lightColor * material.diffuseContribution * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\n\t\t#ifdef USE_CLEARCOAT\n\t\t\tvec3 Ncc = geometryClearcoatNormal;\n\t\t\tvec2 uvClearcoat = LTC_Uv( Ncc, viewDir, material.clearcoatRoughness );\n\t\t\tvec4 t1Clearcoat = texture2D( ltc_1, uvClearcoat );\n\t\t\tvec4 t2Clearcoat = texture2D( ltc_2, uvClearcoat );\n\t\t\tmat3 mInvClearcoat = mat3(\n\t\t\t\tvec3( t1Clearcoat.x, 0, t1Clearcoat.y ),\n\t\t\t\tvec3( 0, 1, 0 ),\n\t\t\t\tvec3( t1Clearcoat.z, 0, t1Clearcoat.w )\n\t\t\t);\n\t\t\tvec3 fresnelClearcoat = material.clearcoatF0 * t2Clearcoat.x + ( material.clearcoatF90 - material.clearcoatF0 ) * t2Clearcoat.y;\n\t\t\tclearcoatSpecularDirect += lightColor * fresnelClearcoat * LTC_Evaluate( Ncc, viewDir, position, mInvClearcoat, rectCoords );\n\t\t#endif\n\t}\n#endif\nvoid RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometryNormal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifdef USE_CLEARCOAT\n\t\tfloat dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) );\n\t\tvec3 ccIrradiance = dotNLcc * directLight.color;\n\t\tclearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material );\n\t#endif\n\t#ifdef USE_SHEEN\n \n \t\tsheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness );\n \n \t\tfloat sheenAlbedoV = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );\n \t\tfloat sheenAlbedoL = IBLSheenBRDF( geometryNormal, directLight.direction, material.sheenRoughness );\n \n \t\tfloat sheenEnergyComp = 1.0 - max3( material.sheenColor ) * max( sheenAlbedoV, sheenAlbedoL );\n \n \t\tirradiance *= sheenEnergyComp;\n \n \t#endif\n\treflectedLight.directSpecular += irradiance * BRDF_GGX_Multiscatter( directLight.direction, geometryViewDir, geometryNormal, material );\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseContribution );\n}\nvoid RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\tvec3 diffuse = irradiance * BRDF_Lambert( material.diffuseContribution );\n\t#ifdef USE_SHEEN\n\t\tfloat sheenAlbedo = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );\n\t\tfloat sheenEnergyComp = 1.0 - max3( material.sheenColor ) * sheenAlbedo;\n\t\tdiffuse *= sheenEnergyComp;\n\t#endif\n\treflectedLight.indirectDiffuse += diffuse;\n}\nvoid RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {\n\t#ifdef USE_CLEARCOAT\n\t\tclearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness );\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tsheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ) * RECIPROCAL_PI;\n \t#endif\n\tvec3 singleScatteringDielectric = vec3( 0.0 );\n\tvec3 multiScatteringDielectric = vec3( 0.0 );\n\tvec3 singleScatteringMetallic = vec3( 0.0 );\n\tvec3 multiScatteringMetallic = vec3( 0.0 );\n\t#ifdef USE_IRIDESCENCE\n\t\tcomputeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnelDielectric, material.roughness, singleScatteringDielectric, multiScatteringDielectric );\n\t\tcomputeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.diffuseColor, material.specularF90, material.iridescence, material.iridescenceFresnelMetallic, material.roughness, singleScatteringMetallic, multiScatteringMetallic );\n\t#else\n\t\tcomputeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScatteringDielectric, multiScatteringDielectric );\n\t\tcomputeMultiscattering( geometryNormal, geometryViewDir, material.diffuseColor, material.specularF90, material.roughness, singleScatteringMetallic, multiScatteringMetallic );\n\t#endif\n\tvec3 singleScattering = mix( singleScatteringDielectric, singleScatteringMetallic, material.metalness );\n\tvec3 multiScattering = mix( multiScatteringDielectric, multiScatteringMetallic, material.metalness );\n\tvec3 totalScatteringDielectric = singleScatteringDielectric + multiScatteringDielectric;\n\tvec3 diffuse = material.diffuseContribution * ( 1.0 - totalScatteringDielectric );\n\tvec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;\n\tvec3 indirectSpecular = radiance * singleScattering;\n\tindirectSpecular += multiScattering * cosineWeightedIrradiance;\n\tvec3 indirectDiffuse = diffuse * cosineWeightedIrradiance;\n\t#ifdef USE_SHEEN\n\t\tfloat sheenAlbedo = IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness );\n\t\tfloat sheenEnergyComp = 1.0 - max3( material.sheenColor ) * sheenAlbedo;\n\t\tindirectSpecular *= sheenEnergyComp;\n\t\tindirectDiffuse *= sheenEnergyComp;\n\t#endif\n\treflectedLight.indirectSpecular += indirectSpecular;\n\treflectedLight.indirectDiffuse += indirectDiffuse;\n}\n#define RE_Direct\t\t\t\tRE_Direct_Physical\n#define RE_Direct_RectArea\t\tRE_Direct_RectArea_Physical\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Physical\n#define RE_IndirectSpecular\t\tRE_IndirectSpecular_Physical\nfloat computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) {\n\treturn saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion );\n}",lights_fragment_begin:"\nvec3 geometryPosition = - vViewPosition;\nvec3 geometryNormal = normal;\nvec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\nvec3 geometryClearcoatNormal = vec3( 0.0 );\n#ifdef USE_CLEARCOAT\n\tgeometryClearcoatNormal = clearcoatNormal;\n#endif\n#ifdef USE_IRIDESCENCE\n\tfloat dotNVi = saturate( dot( normal, geometryViewDir ) );\n\tif ( material.iridescenceThickness == 0.0 ) {\n\t\tmaterial.iridescence = 0.0;\n\t} else {\n\t\tmaterial.iridescence = saturate( material.iridescence );\n\t}\n\tif ( material.iridescence > 0.0 ) {\n\t\tmaterial.iridescenceFresnelDielectric = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );\n\t\tmaterial.iridescenceFresnelMetallic = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.diffuseColor );\n\t\tmaterial.iridescenceFresnel = mix( material.iridescenceFresnelDielectric, material.iridescenceFresnelMetallic, material.metalness );\n\t\tmaterial.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi );\n\t}\n#endif\nIncidentLight directLight;\n#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )\n\tPointLight pointLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tpointLight = pointLights[ i ];\n\t\tgetPointLightInfo( pointLight, geometryPosition, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) && ( defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_BASIC ) )\n\t\tpointLightShadow = pointLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )\n\tSpotLight spotLight;\n\tvec4 spotColor;\n\tvec3 spotLightCoord;\n\tbool inSpotLightMap;\n\t#if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tspotLight = spotLights[ i ];\n\t\tgetSpotLightInfo( spotLight, geometryPosition, directLight );\n\t\t#if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\n\t\t#define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX\n\t\t#elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\t#define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS\n\t\t#else\n\t\t#define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\n\t\t#endif\n\t\t#if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS )\n\t\t\tspotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w;\n\t\t\tinSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) );\n\t\t\tspotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy );\n\t\t\tdirectLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color;\n\t\t#endif\n\t\t#undef SPOT_LIGHT_MAP_INDEX\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\tspotLightShadow = spotLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )\n\tDirectionalLight directionalLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tdirectionalLight = directionalLights[ i ];\n\t\tgetDirectionalLightInfo( directionalLight, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )\n\t\tdirectionalLightShadow = directionalLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )\n\tRectAreaLight rectAreaLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {\n\t\trectAreaLight = rectAreaLights[ i ];\n\t\tRE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if defined( RE_IndirectDiffuse )\n\tvec3 iblIrradiance = vec3( 0.0 );\n\tvec3 irradiance = getAmbientLightIrradiance( ambientLightColor );\n\t#if defined( USE_LIGHT_PROBES )\n\t\tirradiance += getLightProbeIrradiance( lightProbe, geometryNormal );\n\t#endif\n\t#if ( NUM_HEMI_LIGHTS > 0 )\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\t\tirradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal );\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n\t#ifdef USE_LIGHT_PROBES_GRID\n\t\tvec3 probeWorldPos = ( ( vec4( geometryPosition, 1.0 ) - viewMatrix[ 3 ] ) * viewMatrix ).xyz;\n\t\tvec3 probeWorldNormal = transformNormalByInverseViewMatrix( geometryNormal, viewMatrix );\n\t\tirradiance += getLightProbeGridIrradiance( probeWorldPos, probeWorldNormal );\n\t#endif\n#endif\n#if defined( RE_IndirectSpecular )\n\tvec3 radiance = vec3( 0.0 );\n\tvec3 clearcoatRadiance = vec3( 0.0 );\n#endif",lights_fragment_maps:"#if defined( RE_IndirectDiffuse )\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\n\t\tvec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity;\n\t\tirradiance += lightMapIrradiance;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( ENVMAP_TYPE_CUBE_UV )\n\t\t#if defined( STANDARD ) || defined( LAMBERT ) || defined( PHONG )\n\t\t\tiblIrradiance += getIBLIrradiance( geometryNormal );\n\t\t#endif\n\t#endif\n#endif\n#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )\n\t#ifdef USE_ANISOTROPY\n\t\tradiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy );\n\t#else\n\t\tradiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness );\n\t#endif\n\t#ifdef USE_CLEARCOAT\n\t\tclearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness );\n\t#endif\n#endif",lights_fragment_end:"#if defined( RE_IndirectDiffuse )\n\t#if defined( LAMBERT ) || defined( PHONG )\n\t\tirradiance += iblIrradiance;\n\t#endif\n\tRE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n#endif\n#if defined( RE_IndirectSpecular )\n\tRE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );\n#endif",lightprobes_pars_fragment:"#ifdef USE_LIGHT_PROBES_GRID\nuniform highp sampler3D probesSH;\nuniform vec3 probesMin;\nuniform vec3 probesMax;\nuniform vec3 probesResolution;\nvec3 getLightProbeGridIrradiance( vec3 worldPos, vec3 worldNormal ) {\n\tvec3 res = probesResolution;\n\tvec3 gridRange = probesMax - probesMin;\n\tvec3 resMinusOne = res - 1.0;\n\tvec3 probeSpacing = gridRange / resMinusOne;\n\tvec3 samplePos = worldPos + worldNormal * probeSpacing * 0.5;\n\tvec3 uvw = clamp( ( samplePos - probesMin ) / gridRange, 0.0, 1.0 );\n\tuvw = uvw * resMinusOne / res + 0.5 / res;\n\tfloat nz = res.z;\n\tfloat paddedSlices = nz + 2.0;\n\tfloat atlasDepth = 7.0 * paddedSlices;\n\tfloat uvZBase = uvw.z * nz + 1.0;\n\tvec4 s0 = texture( probesSH, vec3( uvw.xy, ( uvZBase ) / atlasDepth ) );\n\tvec4 s1 = texture( probesSH, vec3( uvw.xy, ( uvZBase + paddedSlices ) / atlasDepth ) );\n\tvec4 s2 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 2.0 * paddedSlices ) / atlasDepth ) );\n\tvec4 s3 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 3.0 * paddedSlices ) / atlasDepth ) );\n\tvec4 s4 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 4.0 * paddedSlices ) / atlasDepth ) );\n\tvec4 s5 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 5.0 * paddedSlices ) / atlasDepth ) );\n\tvec4 s6 = texture( probesSH, vec3( uvw.xy, ( uvZBase + 6.0 * paddedSlices ) / atlasDepth ) );\n\tvec3 c0 = s0.xyz;\n\tvec3 c1 = vec3( s0.w, s1.xy );\n\tvec3 c2 = vec3( s1.zw, s2.x );\n\tvec3 c3 = s2.yzw;\n\tvec3 c4 = s3.xyz;\n\tvec3 c5 = vec3( s3.w, s4.xy );\n\tvec3 c6 = vec3( s4.zw, s5.x );\n\tvec3 c7 = s5.yzw;\n\tvec3 c8 = s6.xyz;\n\tfloat x = worldNormal.x, y = worldNormal.y, z = worldNormal.z;\n\tvec3 result = c0 * 0.886227;\n\tresult += c1 * 2.0 * 0.511664 * y;\n\tresult += c2 * 2.0 * 0.511664 * z;\n\tresult += c3 * 2.0 * 0.511664 * x;\n\tresult += c4 * 2.0 * 0.429043 * x * y;\n\tresult += c5 * 2.0 * 0.429043 * y * z;\n\tresult += c6 * ( 0.743125 * z * z - 0.247708 );\n\tresult += c7 * 2.0 * 0.429043 * x * z;\n\tresult += c8 * 0.429043 * ( x * x - y * y );\n\treturn max( result, vec3( 0.0 ) );\n}\n#endif",logdepthbuf_fragment:"#if defined( USE_LOGARITHMIC_DEPTH_BUFFER )\n\tgl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif",logdepthbuf_pars_fragment:"#if defined( USE_LOGARITHMIC_DEPTH_BUFFER )\n\tuniform float logDepthBufFC;\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif",logdepthbuf_pars_vertex:"#ifdef USE_LOGARITHMIC_DEPTH_BUFFER\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif",logdepthbuf_vertex:"#ifdef USE_LOGARITHMIC_DEPTH_BUFFER\n\tvFragDepth = 1.0 + gl_Position.w;\n\tvIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) );\n#endif",map_fragment:"#ifdef USE_MAP\n\tvec4 sampledDiffuseColor = texture2D( map, vMapUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\tsampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor );\n\t#endif\n\tdiffuseColor *= sampledDiffuseColor;\n#endif",map_pars_fragment:"#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif",map_particle_fragment:"#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\t#if defined( USE_POINTS_UV )\n\t\tvec2 uv = vUv;\n\t#else\n\t\tvec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy;\n\t#endif\n#endif\n#ifdef USE_MAP\n\tdiffuseColor *= texture2D( map, uv );\n#endif\n#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, uv ).g;\n#endif",map_particle_pars_fragment:"#if defined( USE_POINTS_UV )\n\tvarying vec2 vUv;\n#else\n\t#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\t\tuniform mat3 uvTransform;\n\t#endif\n#endif\n#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif",metalnessmap_fragment:"float metalnessFactor = metalness;\n#ifdef USE_METALNESSMAP\n\tvec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv );\n\tmetalnessFactor *= texelMetalness.b;\n#endif",metalnessmap_pars_fragment:"#ifdef USE_METALNESSMAP\n\tuniform sampler2D metalnessMap;\n#endif",morphinstance_vertex:"#ifdef USE_INSTANCING_MORPH\n\tfloat morphTargetInfluences[ MORPHTARGETS_COUNT ];\n\tfloat morphTargetBaseInfluence = texelFetch( morphTexture, ivec2( 0, gl_InstanceID ), 0 ).r;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tmorphTargetInfluences[i] = texelFetch( morphTexture, ivec2( i + 1, gl_InstanceID ), 0 ).r;\n\t}\n#endif",morphcolor_vertex:"#if defined( USE_MORPHCOLORS )\n\tvColor *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\t#if defined( USE_COLOR_ALPHA )\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ];\n\t\t#elif defined( USE_COLOR )\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ];\n\t\t#endif\n\t}\n#endif",morphnormal_vertex:"#ifdef USE_MORPHNORMALS\n\tobjectNormal *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tif ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ];\n\t}\n#endif",morphtarget_pars_vertex:"#ifdef USE_MORPHTARGETS\n\t#ifndef USE_INSTANCING_MORPH\n\t\tuniform float morphTargetBaseInfluence;\n\t\tuniform float morphTargetInfluences[ MORPHTARGETS_COUNT ];\n\t#endif\n\tuniform sampler2DArray morphTargetsTexture;\n\tuniform ivec2 morphTargetsTextureSize;\n\tvec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) {\n\t\tint texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset;\n\t\tint y = texelIndex / morphTargetsTextureSize.x;\n\t\tint x = texelIndex - y * morphTargetsTextureSize.x;\n\t\tivec3 morphUV = ivec3( x, y, morphTargetIndex );\n\t\treturn texelFetch( morphTargetsTexture, morphUV, 0 );\n\t}\n#endif",morphtarget_vertex:"#ifdef USE_MORPHTARGETS\n\ttransformed *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\tif ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ];\n\t}\n#endif",normal_fragment_begin:"float faceDirection = gl_FrontFacing ? 1.0 : - 1.0;\n#ifdef FLAT_SHADED\n\tvec3 fdx = dFdx( vViewPosition );\n\tvec3 fdy = dFdy( vViewPosition );\n\tvec3 normal = normalize( cross( fdx, fdy ) );\n#else\n\tvec3 normal = normalize( vNormal );\n\t#ifdef DOUBLE_SIDED\n\t\tnormal *= faceDirection;\n\t#endif\n#endif\n#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY )\n\t#ifdef USE_TANGENT\n\t\tmat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal );\n\t#else\n\t\tmat3 tbn = getTangentFrame( - vViewPosition, normal,\n\t\t#if defined( USE_NORMALMAP )\n\t\t\tvNormalMapUv\n\t\t#elif defined( USE_CLEARCOAT_NORMALMAP )\n\t\t\tvClearcoatNormalMapUv\n\t\t#else\n\t\t\tvUv\n\t\t#endif\n\t\t);\n\t#endif\n\t#ifdef DOUBLE_SIDED\n\t\ttbn[0] *= faceDirection;\n\t\ttbn[1] *= faceDirection;\n\t#endif\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\t#ifdef USE_TANGENT\n\t\tmat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal );\n\t#else\n\t\tmat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv );\n\t#endif\n\t#ifdef DOUBLE_SIDED\n\t\ttbn2[0] *= faceDirection;\n\t\ttbn2[1] *= faceDirection;\n\t#endif\n#endif\nvec3 nonPerturbedNormal = normal;",normal_fragment_maps:"#ifdef USE_NORMALMAP_OBJECTSPACE\n\tnormal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;\n\t#ifdef FLIP_SIDED\n\t\tnormal = - normal;\n\t#endif\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * faceDirection;\n\t#endif\n\tnormal = normalize( normalMatrix * normal );\n#elif defined( USE_NORMALMAP_TANGENTSPACE )\n\tvec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;\n\t#if defined( USE_PACKED_NORMALMAP )\n\t\tmapN = vec3( mapN.xy, sqrt( saturate( 1.0 - dot( mapN.xy, mapN.xy ) ) ) );\n\t#endif\n\tmapN.xy *= normalScale;\n\tnormal = normalize( tbn * mapN );\n#elif defined( USE_BUMPMAP )\n\tnormal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection );\n#endif",normal_pars_fragment:"#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif",normal_pars_vertex:"#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif",normal_vertex:"#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n\t#ifdef USE_TANGENT\n\t\tvTangent = normalize( transformedTangent );\n\t\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\n\t\t#ifdef FLIP_SIDED\n\t\t\tvBitangent = - vBitangent;\n\t\t#endif\n\t#endif\n#endif",normalmap_pars_fragment:"#ifdef USE_NORMALMAP\n\tuniform sampler2D normalMap;\n\tuniform vec2 normalScale;\n#endif\n#ifdef USE_NORMALMAP_OBJECTSPACE\n\tuniform mat3 normalMatrix;\n#endif\n#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) )\n\tmat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) {\n\t\tvec3 q0 = dFdx( eye_pos.xyz );\n\t\tvec3 q1 = dFdy( eye_pos.xyz );\n\t\tvec2 st0 = dFdx( uv.st );\n\t\tvec2 st1 = dFdy( uv.st );\n\t\tvec3 N = surf_norm;\n\t\tvec3 q1perp = cross( q1, N );\n\t\tvec3 q0perp = cross( N, q0 );\n\t\tvec3 T = q1perp * st0.x + q0perp * st1.x;\n\t\tvec3 B = q1perp * st0.y + q0perp * st1.y;\n\t\tfloat det = max( dot( T, T ), dot( B, B ) );\n\t\tfloat scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det );\n\t\treturn mat3( T * scale, B * scale, N );\n\t}\n#endif",clearcoat_normal_fragment_begin:"#ifdef USE_CLEARCOAT\n\tvec3 clearcoatNormal = nonPerturbedNormal;\n#endif",clearcoat_normal_fragment_maps:"#ifdef USE_CLEARCOAT_NORMALMAP\n\tvec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0;\n\tclearcoatMapN.xy *= clearcoatNormalScale;\n\tclearcoatNormal = normalize( tbn2 * clearcoatMapN );\n#endif",clearcoat_pars_fragment:"#ifdef USE_CLEARCOATMAP\n\tuniform sampler2D clearcoatMap;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform sampler2D clearcoatNormalMap;\n\tuniform vec2 clearcoatNormalScale;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform sampler2D clearcoatRoughnessMap;\n#endif",iridescence_pars_fragment:"#ifdef USE_IRIDESCENCEMAP\n\tuniform sampler2D iridescenceMap;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tuniform sampler2D iridescenceThicknessMap;\n#endif",opaque_fragment:"#ifdef OPAQUE\ndiffuseColor.a = 1.0;\n#endif\n#ifdef USE_TRANSMISSION\ndiffuseColor.a *= material.transmissionAlpha;\n#endif\ngl_FragColor = vec4( outgoingLight, diffuseColor.a );",packing:"vec3 packNormalToRGB( const in vec3 normal ) {\n\treturn normalize( normal ) * 0.5 + 0.5;\n}\nvec3 unpackRGBToNormal( const in vec3 rgb ) {\n\treturn 2.0 * rgb.xyz - 1.0;\n}\nconst float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.;\nconst float Inv255 = 1. / 255.;\nconst vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 );\nconst vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g );\nconst vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b );\nconst vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a );\nvec4 packDepthToRGBA( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec4( 0., 0., 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec4( 1., 1., 1., 1. );\n\tfloat vuf;\n\tfloat af = modf( v * PackFactors.a, vuf );\n\tfloat bf = modf( vuf * ShiftRight8, vuf );\n\tfloat gf = modf( vuf * ShiftRight8, vuf );\n\treturn vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af );\n}\nvec3 packDepthToRGB( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec3( 0., 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec3( 1., 1., 1. );\n\tfloat vuf;\n\tfloat bf = modf( v * PackFactors.b, vuf );\n\tfloat gf = modf( vuf * ShiftRight8, vuf );\n\treturn vec3( vuf * Inv255, gf * PackUpscale, bf );\n}\nvec2 packDepthToRG( const in float v ) {\n\tif( v <= 0.0 )\n\t\treturn vec2( 0., 0. );\n\tif( v >= 1.0 )\n\t\treturn vec2( 1., 1. );\n\tfloat vuf;\n\tfloat gf = modf( v * 256., vuf );\n\treturn vec2( vuf * Inv255, gf );\n}\nfloat unpackRGBAToDepth( const in vec4 v ) {\n\treturn dot( v, UnpackFactors4 );\n}\nfloat unpackRGBToDepth( const in vec3 v ) {\n\treturn dot( v, UnpackFactors3 );\n}\nfloat unpackRGToDepth( const in vec2 v ) {\n\treturn v.r * UnpackFactors2.r + v.g * UnpackFactors2.g;\n}\nvec4 pack2HalfToRGBA( const in vec2 v ) {\n\tvec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) );\n\treturn vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w );\n}\nvec2 unpackRGBATo2Half( const in vec4 v ) {\n\treturn vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) );\n}\nfloat viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( viewZ + near ) / ( near - far );\n}\nfloat orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) {\n\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\n\t\treturn depth * ( far - near ) - far;\n\t#else\n\t\treturn depth * ( near - far ) - near;\n\t#endif\n}\nfloat viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ );\n}\nfloat perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) {\n\t\n\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\treturn ( near * far ) / ( ( near - far ) * depth - near );\n\t#else\n\t\treturn ( near * far ) / ( ( far - near ) * depth - far );\n\t#endif\n}",premultiplied_alpha_fragment:"#ifdef PREMULTIPLIED_ALPHA\n\tgl_FragColor.rgb *= gl_FragColor.a;\n#endif",project_vertex:"vec4 mvPosition = vec4( transformed, 1.0 );\n#ifdef USE_BATCHING\n\tmvPosition = batchingMatrix * mvPosition;\n#endif\n#ifdef USE_INSTANCING\n\tmvPosition = instanceMatrix * mvPosition;\n#endif\nmvPosition = modelViewMatrix * mvPosition;\ngl_Position = projectionMatrix * mvPosition;",dithering_fragment:"#ifdef DITHERING\n\tgl_FragColor.rgb = dithering( gl_FragColor.rgb );\n#endif",dithering_pars_fragment:"#ifdef DITHERING\n\tvec3 dithering( vec3 color ) {\n\t\tfloat grid_position = rand( gl_FragCoord.xy );\n\t\tvec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );\n\t\tdither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );\n\t\treturn color + dither_shift_RGB;\n\t}\n#endif",roughnessmap_fragment:"float roughnessFactor = roughness;\n#ifdef USE_ROUGHNESSMAP\n\tvec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv );\n\troughnessFactor *= texelRoughness.g;\n#endif",roughnessmap_pars_fragment:"#ifdef USE_ROUGHNESSMAP\n\tuniform sampler2D roughnessMap;\n#endif",shadowmap_pars_fragment:"#if NUM_SPOT_LIGHT_COORDS > 0\n\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\n#endif\n#if NUM_SPOT_LIGHT_MAPS > 0\n\tuniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ];\n#endif\n#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tuniform sampler2DShadow directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\n\t\t#else\n\t\t\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\n\t\t#endif\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tuniform sampler2DShadow spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\t#else\n\t\t\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\t#endif\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tuniform samplerCubeShadow pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\n\t\t#elif defined( SHADOWMAP_TYPE_BASIC )\n\t\t\tuniform samplerCube pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\n\t\t#endif\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\tfloat interleavedGradientNoise( vec2 position ) {\n\t\t\treturn fract( 52.9829189 * fract( dot( position, vec2( 0.06711056, 0.00583715 ) ) ) );\n\t\t}\n\t\tvec2 vogelDiskSample( int sampleIndex, int samplesCount, float phi ) {\n\t\t\tconst float goldenAngle = 2.399963229728653;\n\t\t\tfloat r = sqrt( ( float( sampleIndex ) + 0.5 ) / float( samplesCount ) );\n\t\t\tfloat theta = float( sampleIndex ) * goldenAngle + phi;\n\t\t\treturn vec2( cos( theta ), sin( theta ) ) * r;\n\t\t}\n\t#endif\n\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\tfloat getShadow( sampler2DShadow shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\t\tfloat shadow = 1.0;\n\t\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\t\tshadowCoord.z += shadowBias;\n\t\t\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\n\t\t\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\n\t\t\tif ( frustumTest ) {\n\t\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\t\tfloat radius = shadowRadius * texelSize.x;\n\t\t\t\tfloat phi = interleavedGradientNoise( gl_FragCoord.xy ) * PI2;\n\t\t\t\tshadow = (\n\t\t\t\t\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 0, 5, phi ) * radius, shadowCoord.z ) ) +\n\t\t\t\t\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 1, 5, phi ) * radius, shadowCoord.z ) ) +\n\t\t\t\t\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 2, 5, phi ) * radius, shadowCoord.z ) ) +\n\t\t\t\t\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 3, 5, phi ) * radius, shadowCoord.z ) ) +\n\t\t\t\t\ttexture( shadowMap, vec3( shadowCoord.xy + vogelDiskSample( 4, 5, phi ) * radius, shadowCoord.z ) )\n\t\t\t\t) * 0.2;\n\t\t\t}\n\t\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t\t}\n\t#elif defined( SHADOWMAP_TYPE_VSM )\n\t\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\t\tfloat shadow = 1.0;\n\t\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\t\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\t\t\tshadowCoord.z -= shadowBias;\n\t\t\t#else\n\t\t\t\tshadowCoord.z += shadowBias;\n\t\t\t#endif\n\t\t\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\n\t\t\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\n\t\t\tif ( frustumTest ) {\n\t\t\t\tvec2 distribution = texture2D( shadowMap, shadowCoord.xy ).rg;\n\t\t\t\tfloat mean = distribution.x;\n\t\t\t\tfloat variance = distribution.y * distribution.y;\n\t\t\t\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\t\t\t\tfloat hard_shadow = step( mean, shadowCoord.z );\n\t\t\t\t#else\n\t\t\t\t\tfloat hard_shadow = step( shadowCoord.z, mean );\n\t\t\t\t#endif\n\t\t\t\t\n\t\t\t\tif ( hard_shadow == 1.0 ) {\n\t\t\t\t\tshadow = 1.0;\n\t\t\t\t} else {\n\t\t\t\t\tvariance = max( variance, 0.0000001 );\n\t\t\t\t\tfloat d = shadowCoord.z - mean;\n\t\t\t\t\tfloat p_max = variance / ( variance + d * d );\n\t\t\t\t\tp_max = clamp( ( p_max - 0.3 ) / 0.65, 0.0, 1.0 );\n\t\t\t\t\tshadow = max( hard_shadow, p_max );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t\t}\n\t#else\n\t\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\t\tfloat shadow = 1.0;\n\t\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\t\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\t\t\tshadowCoord.z -= shadowBias;\n\t\t\t#else\n\t\t\t\tshadowCoord.z += shadowBias;\n\t\t\t#endif\n\t\t\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\n\t\t\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\n\t\t\tif ( frustumTest ) {\n\t\t\t\tfloat depth = texture2D( shadowMap, shadowCoord.xy ).r;\n\t\t\t\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\t\t\t\tshadow = step( depth, shadowCoord.z );\n\t\t\t\t#else\n\t\t\t\t\tshadow = step( shadowCoord.z, depth );\n\t\t\t\t#endif\n\t\t\t}\n\t\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t\t}\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t#if defined( SHADOWMAP_TYPE_PCF )\n\tfloat getPointShadow( samplerCubeShadow shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tfloat shadow = 1.0;\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\tvec3 bd3D = normalize( lightToPosition );\n\t\tvec3 absVec = abs( lightToPosition );\n\t\tfloat viewSpaceZ = max( max( absVec.x, absVec.y ), absVec.z );\n\t\tif ( viewSpaceZ - shadowCameraFar <= 0.0 && viewSpaceZ - shadowCameraNear >= 0.0 ) {\n\t\t\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\t\t\tfloat dp = ( shadowCameraNear * ( shadowCameraFar - viewSpaceZ ) ) / ( viewSpaceZ * ( shadowCameraFar - shadowCameraNear ) );\n\t\t\t\tdp -= shadowBias;\n\t\t\t#else\n\t\t\t\tfloat dp = ( shadowCameraFar * ( viewSpaceZ - shadowCameraNear ) ) / ( viewSpaceZ * ( shadowCameraFar - shadowCameraNear ) );\n\t\t\t\tdp += shadowBias;\n\t\t\t#endif\n\t\t\tfloat texelSize = shadowRadius / shadowMapSize.x;\n\t\t\tvec3 absDir = abs( bd3D );\n\t\t\tvec3 tangent = absDir.x > absDir.z ? vec3( 0.0, 1.0, 0.0 ) : vec3( 1.0, 0.0, 0.0 );\n\t\t\ttangent = normalize( cross( bd3D, tangent ) );\n\t\t\tvec3 bitangent = cross( bd3D, tangent );\n\t\t\tfloat phi = interleavedGradientNoise( gl_FragCoord.xy ) * PI2;\n\t\t\tvec2 sample0 = vogelDiskSample( 0, 5, phi );\n\t\t\tvec2 sample1 = vogelDiskSample( 1, 5, phi );\n\t\t\tvec2 sample2 = vogelDiskSample( 2, 5, phi );\n\t\t\tvec2 sample3 = vogelDiskSample( 3, 5, phi );\n\t\t\tvec2 sample4 = vogelDiskSample( 4, 5, phi );\n\t\t\tshadow = (\n\t\t\t\ttexture( shadowMap, vec4( bd3D + ( tangent * sample0.x + bitangent * sample0.y ) * texelSize, dp ) ) +\n\t\t\t\ttexture( shadowMap, vec4( bd3D + ( tangent * sample1.x + bitangent * sample1.y ) * texelSize, dp ) ) +\n\t\t\t\ttexture( shadowMap, vec4( bd3D + ( tangent * sample2.x + bitangent * sample2.y ) * texelSize, dp ) ) +\n\t\t\t\ttexture( shadowMap, vec4( bd3D + ( tangent * sample3.x + bitangent * sample3.y ) * texelSize, dp ) ) +\n\t\t\t\ttexture( shadowMap, vec4( bd3D + ( tangent * sample4.x + bitangent * sample4.y ) * texelSize, dp ) )\n\t\t\t) * 0.2;\n\t\t}\n\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t}\n\t#elif defined( SHADOWMAP_TYPE_BASIC )\n\tfloat getPointShadow( samplerCube shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tfloat shadow = 1.0;\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\tvec3 absVec = abs( lightToPosition );\n\t\tfloat viewSpaceZ = max( max( absVec.x, absVec.y ), absVec.z );\n\t\tif ( viewSpaceZ - shadowCameraFar <= 0.0 && viewSpaceZ - shadowCameraNear >= 0.0 ) {\n\t\t\tfloat dp = ( shadowCameraFar * ( viewSpaceZ - shadowCameraNear ) ) / ( viewSpaceZ * ( shadowCameraFar - shadowCameraNear ) );\n\t\t\tdp += shadowBias;\n\t\t\tvec3 bd3D = normalize( lightToPosition );\n\t\t\tfloat depth = textureCube( shadowMap, bd3D ).r;\n\t\t\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\t\t\tdepth = 1.0 - depth;\n\t\t\t#endif\n\t\t\tshadow = step( dp, depth );\n\t\t}\n\t\treturn mix( 1.0, shadow, shadowIntensity );\n\t}\n\t#endif\n\t#endif\n#endif",shadowmap_pars_vertex:"#if NUM_SPOT_LIGHT_COORDS > 0\n\tuniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ];\n\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\n#endif\n#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowIntensity;\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n#endif",shadowmap_vertex:"#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 )\n\t#ifdef HAS_NORMAL\n\t\tvec3 shadowWorldNormal = transformNormalByInverseViewMatrix( transformedNormal, viewMatrix );\n\t#else\n\t\tvec3 shadowWorldNormal = vec3( 0.0 );\n\t#endif\n\tvec4 shadowWorldPosition;\n#endif\n#if defined( USE_SHADOWMAP )\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );\n\t\t\tvDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 );\n\t\t\tvPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n#endif\n#if NUM_SPOT_LIGHT_COORDS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition;\n\t\t#if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\t\tshadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias;\n\t\t#endif\n\t\tvSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n#endif",shadowmask_pars_fragment:"float getShadowMask() {\n\tfloat shadow = 1.0;\n\t#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tdirectionalLight = directionalLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\n\t\tspotLight = spotLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0 && ( defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_BASIC ) )\n\tPointLightShadow pointLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tpointLight = pointLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#endif\n\treturn shadow;\n}",skinbase_vertex:"#ifdef USE_SKINNING\n\tmat4 boneMatX = getBoneMatrix( skinIndex.x );\n\tmat4 boneMatY = getBoneMatrix( skinIndex.y );\n\tmat4 boneMatZ = getBoneMatrix( skinIndex.z );\n\tmat4 boneMatW = getBoneMatrix( skinIndex.w );\n#endif",skinning_pars_vertex:"#ifdef USE_SKINNING\n\tuniform mat4 bindMatrix;\n\tuniform mat4 bindMatrixInverse;\n\tuniform highp sampler2D boneTexture;\n\tmat4 getBoneMatrix( const in float i ) {\n\t\tint size = textureSize( boneTexture, 0 ).x;\n\t\tint j = int( i ) * 4;\n\t\tint x = j % size;\n\t\tint y = j / size;\n\t\tvec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 );\n\t\tvec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 );\n\t\tvec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 );\n\t\tvec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 );\n\t\treturn mat4( v1, v2, v3, v4 );\n\t}\n#endif",skinning_vertex:"#ifdef USE_SKINNING\n\tvec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );\n\tvec4 skinned = vec4( 0.0 );\n\tskinned += boneMatX * skinVertex * skinWeight.x;\n\tskinned += boneMatY * skinVertex * skinWeight.y;\n\tskinned += boneMatZ * skinVertex * skinWeight.z;\n\tskinned += boneMatW * skinVertex * skinWeight.w;\n\ttransformed = ( bindMatrixInverse * skinned ).xyz;\n#endif",skinnormal_vertex:"#ifdef USE_SKINNING\n\tmat4 skinMatrix = mat4( 0.0 );\n\tskinMatrix += skinWeight.x * boneMatX;\n\tskinMatrix += skinWeight.y * boneMatY;\n\tskinMatrix += skinWeight.z * boneMatZ;\n\tskinMatrix += skinWeight.w * boneMatW;\n\tskinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\n\tobjectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz;\n\t#ifdef USE_TANGENT\n\t\tobjectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#endif\n#endif",specularmap_fragment:"float specularStrength;\n#ifdef USE_SPECULARMAP\n\tvec4 texelSpecular = texture2D( specularMap, vSpecularMapUv );\n\tspecularStrength = texelSpecular.r;\n#else\n\tspecularStrength = 1.0;\n#endif",specularmap_pars_fragment:"#ifdef USE_SPECULARMAP\n\tuniform sampler2D specularMap;\n#endif",tonemapping_fragment:"#if defined( TONE_MAPPING )\n\tgl_FragColor.rgb = toneMapping( gl_FragColor.rgb );\n#endif",tonemapping_pars_fragment:"#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\nuniform float toneMappingExposure;\nvec3 LinearToneMapping( vec3 color ) {\n\treturn saturate( toneMappingExposure * color );\n}\nvec3 ReinhardToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\treturn saturate( color / ( vec3( 1.0 ) + color ) );\n}\nvec3 CineonToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\tcolor = max( vec3( 0.0 ), color - 0.004 );\n\treturn pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );\n}\nvec3 RRTAndODTFit( vec3 v ) {\n\tvec3 a = v * ( v + 0.0245786 ) - 0.000090537;\n\tvec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;\n\treturn a / b;\n}\nvec3 ACESFilmicToneMapping( vec3 color ) {\n\tconst mat3 ACESInputMat = mat3(\n\t\tvec3( 0.59719, 0.07600, 0.02840 ),\t\tvec3( 0.35458, 0.90834, 0.13383 ),\n\t\tvec3( 0.04823, 0.01566, 0.83777 )\n\t);\n\tconst mat3 ACESOutputMat = mat3(\n\t\tvec3( 1.60475, -0.10208, -0.00327 ),\t\tvec3( -0.53108, 1.10813, -0.07276 ),\n\t\tvec3( -0.07367, -0.00605, 1.07602 )\n\t);\n\tcolor *= toneMappingExposure / 0.6;\n\tcolor = ACESInputMat * color;\n\tcolor = RRTAndODTFit( color );\n\tcolor = ACESOutputMat * color;\n\treturn saturate( color );\n}\nconst mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3(\n\tvec3( 1.6605, - 0.1246, - 0.0182 ),\n\tvec3( - 0.5876, 1.1329, - 0.1006 ),\n\tvec3( - 0.0728, - 0.0083, 1.1187 )\n);\nconst mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3(\n\tvec3( 0.6274, 0.0691, 0.0164 ),\n\tvec3( 0.3293, 0.9195, 0.0880 ),\n\tvec3( 0.0433, 0.0113, 0.8956 )\n);\nvec3 agxDefaultContrastApprox( vec3 x ) {\n\tvec3 x2 = x * x;\n\tvec3 x4 = x2 * x2;\n\treturn + 15.5 * x4 * x2\n\t\t- 40.14 * x4 * x\n\t\t+ 31.96 * x4\n\t\t- 6.868 * x2 * x\n\t\t+ 0.4298 * x2\n\t\t+ 0.1191 * x\n\t\t- 0.00232;\n}\nvec3 AgXToneMapping( vec3 color ) {\n\tconst mat3 AgXInsetMatrix = mat3(\n\t\tvec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ),\n\t\tvec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ),\n\t\tvec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 )\n\t);\n\tconst mat3 AgXOutsetMatrix = mat3(\n\t\tvec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ),\n\t\tvec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ),\n\t\tvec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 )\n\t);\n\tconst float AgxMinEv = - 12.47393;\tconst float AgxMaxEv = 4.026069;\n\tcolor *= toneMappingExposure;\n\tcolor = LINEAR_SRGB_TO_LINEAR_REC2020 * color;\n\tcolor = AgXInsetMatrix * color;\n\tcolor = max( color, 1e-10 );\tcolor = log2( color );\n\tcolor = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv );\n\tcolor = clamp( color, 0.0, 1.0 );\n\tcolor = agxDefaultContrastApprox( color );\n\tcolor = AgXOutsetMatrix * color;\n\tcolor = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) );\n\tcolor = LINEAR_REC2020_TO_LINEAR_SRGB * color;\n\tcolor = clamp( color, 0.0, 1.0 );\n\treturn color;\n}\nvec3 NeutralToneMapping( vec3 color ) {\n\tconst float StartCompression = 0.8 - 0.04;\n\tconst float Desaturation = 0.15;\n\tcolor *= toneMappingExposure;\n\tfloat x = min( color.r, min( color.g, color.b ) );\n\tfloat offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\n\tcolor -= offset;\n\tfloat peak = max( color.r, max( color.g, color.b ) );\n\tif ( peak < StartCompression ) return color;\n\tfloat d = 1. - StartCompression;\n\tfloat newPeak = 1. - d * d / ( peak + d - StartCompression );\n\tcolor *= newPeak / peak;\n\tfloat g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. );\n\treturn mix( color, vec3( newPeak ), g );\n}\nvec3 CustomToneMapping( vec3 color ) { return color; }",transmission_fragment:"#ifdef USE_TRANSMISSION\n\tmaterial.transmission = transmission;\n\tmaterial.transmissionAlpha = 1.0;\n\tmaterial.thickness = thickness;\n\tmaterial.attenuationDistance = attenuationDistance;\n\tmaterial.attenuationColor = attenuationColor;\n\t#ifdef USE_TRANSMISSIONMAP\n\t\tmaterial.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r;\n\t#endif\n\t#ifdef USE_THICKNESSMAP\n\t\tmaterial.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g;\n\t#endif\n\tvec3 pos = vWorldPosition;\n\tvec3 v = normalize( cameraPosition - pos );\n\tvec3 n = transformNormalByInverseViewMatrix( normal, viewMatrix );\n\tvec4 transmitted = getIBLVolumeRefraction(\n\t\tn, v, material.roughness, material.diffuseContribution, material.specularColorBlended, material.specularF90,\n\t\tpos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness,\n\t\tmaterial.attenuationColor, material.attenuationDistance );\n\tmaterial.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission );\n\ttotalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission );\n#endif",transmission_pars_fragment:"#ifdef USE_TRANSMISSION\n\tuniform float transmission;\n\tuniform float thickness;\n\tuniform float attenuationDistance;\n\tuniform vec3 attenuationColor;\n\t#ifdef USE_TRANSMISSIONMAP\n\t\tuniform sampler2D transmissionMap;\n\t#endif\n\t#ifdef USE_THICKNESSMAP\n\t\tuniform sampler2D thicknessMap;\n\t#endif\n\tuniform vec2 transmissionSamplerSize;\n\tuniform sampler2D transmissionSamplerMap;\n\tuniform mat4 modelMatrix;\n\tuniform mat4 projectionMatrix;\n\tvarying vec3 vWorldPosition;\n\tfloat w0( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 );\n\t}\n\tfloat w1( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 );\n\t}\n\tfloat w2( float a ){\n\t\treturn ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 );\n\t}\n\tfloat w3( float a ) {\n\t\treturn ( 1.0 / 6.0 ) * ( a * a * a );\n\t}\n\tfloat g0( float a ) {\n\t\treturn w0( a ) + w1( a );\n\t}\n\tfloat g1( float a ) {\n\t\treturn w2( a ) + w3( a );\n\t}\n\tfloat h0( float a ) {\n\t\treturn - 1.0 + w1( a ) / ( w0( a ) + w1( a ) );\n\t}\n\tfloat h1( float a ) {\n\t\treturn 1.0 + w3( a ) / ( w2( a ) + w3( a ) );\n\t}\n\tvec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) {\n\t\tuv = uv * texelSize.zw + 0.5;\n\t\tvec2 iuv = floor( uv );\n\t\tvec2 fuv = fract( uv );\n\t\tfloat g0x = g0( fuv.x );\n\t\tfloat g1x = g1( fuv.x );\n\t\tfloat h0x = h0( fuv.x );\n\t\tfloat h1x = h1( fuv.x );\n\t\tfloat h0y = h0( fuv.y );\n\t\tfloat h1y = h1( fuv.y );\n\t\tvec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy;\n\t\tvec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy;\n\t\treturn g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) +\n\t\t\tg1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) );\n\t}\n\tvec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) {\n\t\tvec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) );\n\t\tvec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) );\n\t\tvec2 fLodSizeInv = 1.0 / fLodSize;\n\t\tvec2 cLodSizeInv = 1.0 / cLodSize;\n\t\tvec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) );\n\t\tvec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) );\n\t\treturn mix( fSample, cSample, fract( lod ) );\n\t}\n\tvec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) {\n\t\tvec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior );\n\t\tvec3 modelScale;\n\t\tmodelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) );\n\t\tmodelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) );\n\t\tmodelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) );\n\t\treturn normalize( refractionVector ) * thickness * modelScale;\n\t}\n\tfloat applyIorToRoughness( const in float roughness, const in float ior ) {\n\t\treturn roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 );\n\t}\n\tvec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) {\n\t\tfloat lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior );\n\t\treturn textureBicubic( transmissionSamplerMap, fragCoord.xy, lod );\n\t}\n\tvec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tif ( isinf( attenuationDistance ) ) {\n\t\t\treturn vec3( 1.0 );\n\t\t} else {\n\t\t\tvec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance;\n\t\t\tvec3 transmittance = exp( - attenuationCoefficient * transmissionDistance );\t\t\treturn transmittance;\n\t\t}\n\t}\n\tvec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor,\n\t\tconst in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix,\n\t\tconst in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness,\n\t\tconst in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tvec4 transmittedLight;\n\t\tvec3 transmittance;\n\t\t#ifdef USE_DISPERSION\n\t\t\tfloat halfSpread = ( ior - 1.0 ) * 0.025 * dispersion;\n\t\t\tvec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread );\n\t\t\tfor ( int i = 0; i < 3; i ++ ) {\n\t\t\t\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix );\n\t\t\t\tvec3 refractedRayExit = position + transmissionRay;\n\t\t\t\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\n\t\t\t\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\n\t\t\t\trefractionCoords += 1.0;\n\t\t\t\trefractionCoords /= 2.0;\n\t\t\t\tvec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] );\n\t\t\t\ttransmittedLight[ i ] = transmissionSample[ i ];\n\t\t\t\ttransmittedLight.a += transmissionSample.a;\n\t\t\t\ttransmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ];\n\t\t\t}\n\t\t\ttransmittedLight.a /= 3.0;\n\t\t#else\n\t\t\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix );\n\t\t\tvec3 refractedRayExit = position + transmissionRay;\n\t\t\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\n\t\t\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\n\t\t\trefractionCoords += 1.0;\n\t\t\trefractionCoords /= 2.0;\n\t\t\ttransmittedLight = getTransmissionSample( refractionCoords, roughness, ior );\n\t\t\ttransmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance );\n\t\t#endif\n\t\tvec3 attenuatedColor = transmittance * transmittedLight.rgb;\n\t\tvec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness );\n\t\tfloat transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0;\n\t\treturn vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor );\n\t}\n#endif",uv_pars_fragment:"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvarying vec2 vUv;\n#endif\n#ifdef USE_MAP\n\tvarying vec2 vMapUv;\n#endif\n#ifdef USE_ALPHAMAP\n\tvarying vec2 vAlphaMapUv;\n#endif\n#ifdef USE_LIGHTMAP\n\tvarying vec2 vLightMapUv;\n#endif\n#ifdef USE_AOMAP\n\tvarying vec2 vAoMapUv;\n#endif\n#ifdef USE_BUMPMAP\n\tvarying vec2 vBumpMapUv;\n#endif\n#ifdef USE_NORMALMAP\n\tvarying vec2 vNormalMapUv;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tvarying vec2 vEmissiveMapUv;\n#endif\n#ifdef USE_METALNESSMAP\n\tvarying vec2 vMetalnessMapUv;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tvarying vec2 vRoughnessMapUv;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tvarying vec2 vAnisotropyMapUv;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tvarying vec2 vClearcoatMapUv;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tvarying vec2 vClearcoatNormalMapUv;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tvarying vec2 vClearcoatRoughnessMapUv;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tvarying vec2 vIridescenceMapUv;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tvarying vec2 vIridescenceThicknessMapUv;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tvarying vec2 vSheenColorMapUv;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tvarying vec2 vSheenRoughnessMapUv;\n#endif\n#ifdef USE_SPECULARMAP\n\tvarying vec2 vSpecularMapUv;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tvarying vec2 vSpecularColorMapUv;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tvarying vec2 vSpecularIntensityMapUv;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tuniform mat3 transmissionMapTransform;\n\tvarying vec2 vTransmissionMapUv;\n#endif\n#ifdef USE_THICKNESSMAP\n\tuniform mat3 thicknessMapTransform;\n\tvarying vec2 vThicknessMapUv;\n#endif",uv_pars_vertex:"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvarying vec2 vUv;\n#endif\n#ifdef USE_MAP\n\tuniform mat3 mapTransform;\n\tvarying vec2 vMapUv;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform mat3 alphaMapTransform;\n\tvarying vec2 vAlphaMapUv;\n#endif\n#ifdef USE_LIGHTMAP\n\tuniform mat3 lightMapTransform;\n\tvarying vec2 vLightMapUv;\n#endif\n#ifdef USE_AOMAP\n\tuniform mat3 aoMapTransform;\n\tvarying vec2 vAoMapUv;\n#endif\n#ifdef USE_BUMPMAP\n\tuniform mat3 bumpMapTransform;\n\tvarying vec2 vBumpMapUv;\n#endif\n#ifdef USE_NORMALMAP\n\tuniform mat3 normalMapTransform;\n\tvarying vec2 vNormalMapUv;\n#endif\n#ifdef USE_DISPLACEMENTMAP\n\tuniform mat3 displacementMapTransform;\n\tvarying vec2 vDisplacementMapUv;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tuniform mat3 emissiveMapTransform;\n\tvarying vec2 vEmissiveMapUv;\n#endif\n#ifdef USE_METALNESSMAP\n\tuniform mat3 metalnessMapTransform;\n\tvarying vec2 vMetalnessMapUv;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tuniform mat3 roughnessMapTransform;\n\tvarying vec2 vRoughnessMapUv;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tuniform mat3 anisotropyMapTransform;\n\tvarying vec2 vAnisotropyMapUv;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tuniform mat3 clearcoatMapTransform;\n\tvarying vec2 vClearcoatMapUv;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform mat3 clearcoatNormalMapTransform;\n\tvarying vec2 vClearcoatNormalMapUv;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform mat3 clearcoatRoughnessMapTransform;\n\tvarying vec2 vClearcoatRoughnessMapUv;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tuniform mat3 sheenColorMapTransform;\n\tvarying vec2 vSheenColorMapUv;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tuniform mat3 sheenRoughnessMapTransform;\n\tvarying vec2 vSheenRoughnessMapUv;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tuniform mat3 iridescenceMapTransform;\n\tvarying vec2 vIridescenceMapUv;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tuniform mat3 iridescenceThicknessMapTransform;\n\tvarying vec2 vIridescenceThicknessMapUv;\n#endif\n#ifdef USE_SPECULARMAP\n\tuniform mat3 specularMapTransform;\n\tvarying vec2 vSpecularMapUv;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tuniform mat3 specularColorMapTransform;\n\tvarying vec2 vSpecularColorMapUv;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tuniform mat3 specularIntensityMapTransform;\n\tvarying vec2 vSpecularIntensityMapUv;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tuniform mat3 transmissionMapTransform;\n\tvarying vec2 vTransmissionMapUv;\n#endif\n#ifdef USE_THICKNESSMAP\n\tuniform mat3 thicknessMapTransform;\n\tvarying vec2 vThicknessMapUv;\n#endif",uv_vertex:"#if defined( USE_UV ) || defined( USE_ANISOTROPY )\n\tvUv = vec3( uv, 1 ).xy;\n#endif\n#ifdef USE_MAP\n\tvMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ALPHAMAP\n\tvAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_LIGHTMAP\n\tvLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_AOMAP\n\tvAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_BUMPMAP\n\tvBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_NORMALMAP\n\tvNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_DISPLACEMENTMAP\n\tvDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_EMISSIVEMAP\n\tvEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_METALNESSMAP\n\tvMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ROUGHNESSMAP\n\tvRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_ANISOTROPYMAP\n\tvAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOATMAP\n\tvClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tvClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tvClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_IRIDESCENCEMAP\n\tvIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tvIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SHEEN_COLORMAP\n\tvSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SHEEN_ROUGHNESSMAP\n\tvSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULARMAP\n\tvSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULAR_COLORMAP\n\tvSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_SPECULAR_INTENSITYMAP\n\tvSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_TRANSMISSIONMAP\n\tvTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy;\n#endif\n#ifdef USE_THICKNESSMAP\n\tvThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy;\n#endif",worldpos_vertex:"#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0\n\tvec4 worldPosition = vec4( transformed, 1.0 );\n\t#ifdef USE_BATCHING\n\t\tworldPosition = batchingMatrix * worldPosition;\n\t#endif\n\t#ifdef USE_INSTANCING\n\t\tworldPosition = instanceMatrix * worldPosition;\n\t#endif\n\tworldPosition = modelMatrix * worldPosition;\n#endif",background_vert:"varying vec2 vUv;\nuniform mat3 uvTransform;\nvoid main() {\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\tgl_Position = vec4( position.xy, 1.0, 1.0 );\n}",background_frag:"uniform sampler2D t2D;\nuniform float backgroundIntensity;\nvarying vec2 vUv;\nvoid main() {\n\tvec4 texColor = texture2D( t2D, vUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\ttexColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w );\n\t#endif\n\ttexColor.rgb *= backgroundIntensity;\n\tgl_FragColor = texColor;\n\t#include \n\t#include \n}",backgroundCube_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n\tgl_Position.z = gl_Position.w;\n}",backgroundCube_frag:"#ifdef ENVMAP_TYPE_CUBE\n\tuniform samplerCube envMap;\n#elif defined( ENVMAP_TYPE_CUBE_UV )\n\tuniform sampler2D envMap;\n#endif\nuniform float backgroundBlurriness;\nuniform float backgroundIntensity;\nuniform mat3 backgroundRotation;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 texColor = textureCube( envMap, backgroundRotation * vWorldDirection );\n\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\tvec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness );\n\t#else\n\t\tvec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t#endif\n\ttexColor.rgb *= backgroundIntensity;\n\tgl_FragColor = texColor;\n\t#include \n\t#include \n}",cube_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n\tgl_Position.z = gl_Position.w;\n}",cube_frag:"uniform samplerCube tCube;\nuniform float tFlip;\nuniform float opacity;\nvarying vec3 vWorldDirection;\nvoid main() {\n\tvec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) );\n\tgl_FragColor = texColor;\n\tgl_FragColor.a *= opacity;\n\t#include \n\t#include \n}",depth_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvHighPrecisionZW = gl_Position.zw;\n}",depth_frag:"#if DEPTH_PACKING == 3200\n\tuniform float opacity;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#if DEPTH_PACKING == 3200\n\t\tdiffuseColor.a = opacity;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_REVERSED_DEPTH_BUFFER\n\t\tfloat fragCoordZ = vHighPrecisionZW[ 0 ] / vHighPrecisionZW[ 1 ];\n\t#else\n\t\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[ 0 ] / vHighPrecisionZW[ 1 ] + 0.5;\n\t#endif\n\t#if DEPTH_PACKING == 3200\n\t\tgl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );\n\t#elif DEPTH_PACKING == 3201\n\t\tgl_FragColor = packDepthToRGBA( fragCoordZ );\n\t#elif DEPTH_PACKING == 3202\n\t\tgl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 );\n\t#elif DEPTH_PACKING == 3203\n\t\tgl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 );\n\t#endif\n}",distance_vert:"#define DISTANCE\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvWorldPosition = worldPosition.xyz;\n}",distance_frag:"#define DISTANCE\nuniform vec3 referencePosition;\nuniform float nearDistance;\nuniform float farDistance;\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tfloat dist = length( vWorldPosition - referencePosition );\n\tdist = ( dist - nearDistance ) / ( farDistance - nearDistance );\n\tdist = saturate( dist );\n\tgl_FragColor = vec4( dist, 0.0, 0.0, 1.0 );\n}",equirect_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n}",equirect_frag:"uniform sampler2D tEquirect;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvec3 direction = normalize( vWorldDirection );\n\tvec2 sampleUV = equirectUv( direction );\n\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\t#include \n\t#include \n}",linedashed_vert:"uniform float scale;\nattribute float lineDistance;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvLineDistance = scale * lineDistance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",linedashed_frag:"uniform vec3 diffuse;\nuniform float opacity;\nuniform float dashSize;\nuniform float totalSize;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\n\t\tdiscard;\n\t}\n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshbasic_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#if defined ( USE_ENVMAP ) || defined ( USE_SKINNING )\n\t\t#include \n\t\t#include \n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshbasic_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\n\t\treflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI;\n\t#else\n\t\treflectedLight.indirectDiffuse += vec3( 1.0 );\n\t#endif\n\t#include \n\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\n\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshlambert_vert:"#define LAMBERT\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}",meshlambert_frag:"#define LAMBERT\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshmatcap_vert:"#define MATCAP\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n}",meshmatcap_frag:"#define MATCAP\nuniform vec3 diffuse;\nuniform float opacity;\nuniform sampler2D matcap;\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 viewDir = normalize( vViewPosition );\n\tvec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );\n\tvec3 y = cross( viewDir, x );\n\tvec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5;\n\t#ifdef USE_MATCAP\n\t\tvec4 matcapColor = texture2D( matcap, uv );\n\t#else\n\t\tvec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 );\n\t#endif\n\tvec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshnormal_vert:"#define NORMAL\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvarying vec3 vViewPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvViewPosition = - mvPosition.xyz;\n#endif\n}",meshnormal_frag:"#define NORMAL\nuniform float opacity;\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE )\n\tvarying vec3 vViewPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\tgl_FragColor = vec4( normalize( normal ) * 0.5 + 0.5, diffuseColor.a );\n\t#ifdef OPAQUE\n\t\tgl_FragColor.a = 1.0;\n\t#endif\n}",meshphong_vert:"#define PHONG\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphong_frag:"#define PHONG\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform vec3 specular;\nuniform float shininess;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphysical_vert:"#define STANDARD\nvarying vec3 vViewPosition;\n#ifdef USE_TRANSMISSION\n\tvarying vec3 vWorldPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n#ifdef USE_TRANSMISSION\n\tvWorldPosition = worldPosition.xyz;\n#endif\n}",meshphysical_frag:"#define STANDARD\n#ifdef PHYSICAL\n\t#define IOR\n\t#define USE_SPECULAR\n#endif\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\n#ifdef IOR\n\tuniform float ior;\n#endif\n#ifdef USE_SPECULAR\n\tuniform float specularIntensity;\n\tuniform vec3 specularColor;\n\t#ifdef USE_SPECULAR_COLORMAP\n\t\tuniform sampler2D specularColorMap;\n\t#endif\n\t#ifdef USE_SPECULAR_INTENSITYMAP\n\t\tuniform sampler2D specularIntensityMap;\n\t#endif\n#endif\n#ifdef USE_CLEARCOAT\n\tuniform float clearcoat;\n\tuniform float clearcoatRoughness;\n#endif\n#ifdef USE_DISPERSION\n\tuniform float dispersion;\n#endif\n#ifdef USE_IRIDESCENCE\n\tuniform float iridescence;\n\tuniform float iridescenceIOR;\n\tuniform float iridescenceThicknessMinimum;\n\tuniform float iridescenceThicknessMaximum;\n#endif\n#ifdef USE_SHEEN\n\tuniform vec3 sheenColor;\n\tuniform float sheenRoughness;\n\t#ifdef USE_SHEEN_COLORMAP\n\t\tuniform sampler2D sheenColorMap;\n\t#endif\n\t#ifdef USE_SHEEN_ROUGHNESSMAP\n\t\tuniform sampler2D sheenRoughnessMap;\n\t#endif\n#endif\n#ifdef USE_ANISOTROPY\n\tuniform vec2 anisotropyVector;\n\t#ifdef USE_ANISOTROPYMAP\n\t\tuniform sampler2D anisotropyMap;\n\t#endif\n#endif\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;\n\tvec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;\n\t#include \n\tvec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;\n\t#ifdef USE_SHEEN\n \n\t\toutgoingLight = outgoingLight + sheenSpecularDirect + sheenSpecularIndirect;\n \n \t#endif\n\t#ifdef USE_CLEARCOAT\n\t\tfloat dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) );\n\t\tvec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc );\n\t\toutgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshtoon_vert:"#define TOON\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n}",meshtoon_frag:"#define TOON\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",points_vert:"uniform float size;\nuniform float scale;\n#include \n#include \n#include \n#include \n#include \n#include \n#ifdef USE_POINTS_UV\n\tvarying vec2 vUv;\n\tuniform mat3 uvTransform;\n#endif\nvoid main() {\n\t#ifdef USE_POINTS_UV\n\t\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tgl_PointSize = size;\n\t#ifdef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n}",points_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",shadow_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",shadow_frag:"uniform vec3 color;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\t#include \n\t#include \n\t#include \n\t#include \n}",sprite_vert:"uniform float rotation;\nuniform vec2 center;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 mvPosition = modelViewMatrix[ 3 ];\n\tvec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) );\n\t#ifndef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) scale *= - mvPosition.z;\n\t#endif\n\tvec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;\n\tvec2 rotatedPosition;\n\trotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;\n\trotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;\n\tmvPosition.xy += rotatedPosition;\n\tgl_Position = projectionMatrix * mvPosition;\n\t#include \n\t#include \n\t#include \n}",sprite_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n}"},Gn={common:{diffuse:{value:new n(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new e},alphaMap:{value:null},alphaMapTransform:{value:new e},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new e}},envmap:{envMap:{value:null},envMapRotation:{value:new e},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98},dfgLUT:{value:null}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new e}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new e}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new e},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new e},normalScale:{value:new t(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new e},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new e}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new e}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new e}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new n(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null},probesSH:{value:null},probesMin:{value:new i},probesMax:{value:new i},probesResolution:{value:new i}},points:{diffuse:{value:new n(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new e},alphaTest:{value:0},uvTransform:{value:new e}},sprite:{diffuse:{value:new n(16777215)},opacity:{value:1},center:{value:new t(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new e},alphaMap:{value:null},alphaMapTransform:{value:new e},alphaTest:{value:0}}},Hn={basic:{uniforms:r([Gn.common,Gn.specularmap,Gn.envmap,Gn.aomap,Gn.lightmap,Gn.fog]),vertexShader:Bn.meshbasic_vert,fragmentShader:Bn.meshbasic_frag},lambert:{uniforms:r([Gn.common,Gn.specularmap,Gn.envmap,Gn.aomap,Gn.lightmap,Gn.emissivemap,Gn.bumpmap,Gn.normalmap,Gn.displacementmap,Gn.fog,Gn.lights,{emissive:{value:new n(0)},envMapIntensity:{value:1}}]),vertexShader:Bn.meshlambert_vert,fragmentShader:Bn.meshlambert_frag},phong:{uniforms:r([Gn.common,Gn.specularmap,Gn.envmap,Gn.aomap,Gn.lightmap,Gn.emissivemap,Gn.bumpmap,Gn.normalmap,Gn.displacementmap,Gn.fog,Gn.lights,{emissive:{value:new n(0)},specular:{value:new n(1118481)},shininess:{value:30},envMapIntensity:{value:1}}]),vertexShader:Bn.meshphong_vert,fragmentShader:Bn.meshphong_frag},standard:{uniforms:r([Gn.common,Gn.envmap,Gn.aomap,Gn.lightmap,Gn.emissivemap,Gn.bumpmap,Gn.normalmap,Gn.displacementmap,Gn.roughnessmap,Gn.metalnessmap,Gn.fog,Gn.lights,{emissive:{value:new n(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:Bn.meshphysical_vert,fragmentShader:Bn.meshphysical_frag},toon:{uniforms:r([Gn.common,Gn.aomap,Gn.lightmap,Gn.emissivemap,Gn.bumpmap,Gn.normalmap,Gn.displacementmap,Gn.gradientmap,Gn.fog,Gn.lights,{emissive:{value:new n(0)}}]),vertexShader:Bn.meshtoon_vert,fragmentShader:Bn.meshtoon_frag},matcap:{uniforms:r([Gn.common,Gn.bumpmap,Gn.normalmap,Gn.displacementmap,Gn.fog,{matcap:{value:null}}]),vertexShader:Bn.meshmatcap_vert,fragmentShader:Bn.meshmatcap_frag},points:{uniforms:r([Gn.points,Gn.fog]),vertexShader:Bn.points_vert,fragmentShader:Bn.points_frag},dashed:{uniforms:r([Gn.common,Gn.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:Bn.linedashed_vert,fragmentShader:Bn.linedashed_frag},depth:{uniforms:r([Gn.common,Gn.displacementmap]),vertexShader:Bn.depth_vert,fragmentShader:Bn.depth_frag},normal:{uniforms:r([Gn.common,Gn.bumpmap,Gn.normalmap,Gn.displacementmap,{opacity:{value:1}}]),vertexShader:Bn.meshnormal_vert,fragmentShader:Bn.meshnormal_frag},sprite:{uniforms:r([Gn.sprite,Gn.fog]),vertexShader:Bn.sprite_vert,fragmentShader:Bn.sprite_frag},background:{uniforms:{uvTransform:{value:new e},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:Bn.background_vert,fragmentShader:Bn.background_frag},backgroundCube:{uniforms:{envMap:{value:null},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new e}},vertexShader:Bn.backgroundCube_vert,fragmentShader:Bn.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:Bn.cube_vert,fragmentShader:Bn.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:Bn.equirect_vert,fragmentShader:Bn.equirect_frag},distance:{uniforms:r([Gn.common,Gn.displacementmap,{referencePosition:{value:new i},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:Bn.distance_vert,fragmentShader:Bn.distance_frag},shadow:{uniforms:r([Gn.lights,Gn.fog,{color:{value:new n(0)},opacity:{value:1}}]),vertexShader:Bn.shadow_vert,fragmentShader:Bn.shadow_frag}};Hn.physical={uniforms:r([Hn.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new e},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new e},clearcoatNormalScale:{value:new t(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new e},dispersion:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new e},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new e},sheen:{value:0},sheenColor:{value:new n(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new e},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new e},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new e},transmissionSamplerSize:{value:new t},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new e},attenuationDistance:{value:0},attenuationColor:{value:new n(0)},specularColor:{value:new n(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new e},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new e},anisotropyVector:{value:new t},anisotropyMap:{value:null},anisotropyMapTransform:{value:new e}}]),vertexShader:Bn.meshphysical_vert,fragmentShader:Bn.meshphysical_frag};const Vn={r:0,b:0,g:0},Wn=new u,zn=new e;function kn(e,t,i,r,u,g){const v=new n(0);let E,S,M=!0===u?0:1,T=null,x=0,R=null;function A(e){let n=!0===e.isScene?e.background:null;if(n&&n.isTexture){const i=e.backgroundBlurriness>0;n=t.get(n,i)}return n}function b(t,n){t.getRGB(Vn,_(e)),i.buffers.color.setClear(Vn.r,Vn.g,Vn.b,n,g)}return{getClearColor:function(){return v},setClearColor:function(e,t=1){v.set(e),M=t,b(v,M)},getClearAlpha:function(){return M},setClearAlpha:function(e){M=e,b(v,M)},render:function(t){let n=!1;const r=A(t);null===r?b(v,M):r&&r.isColor&&(b(r,1),n=!0);const a=e.xr.getEnvironmentBlendMode();"additive"===a?i.buffers.color.setClear(0,0,0,1,g):"alpha-blend"===a&&i.buffers.color.setClear(0,0,0,0,g),(e.autoClear||n)&&(i.buffers.depth.setTest(!0),i.buffers.depth.setMask(!0),i.buffers.color.setMask(!0),e.clear(e.autoClearColor,e.autoClearDepth,e.autoClearStencil))},addToRenderList:function(t,n){const i=A(n);i&&(i.isCubeTexture||i.mapping===a)?(void 0===S&&(S=new o(new s(1,1,1),new l({name:"BackgroundCubeMaterial",uniforms:d(Hn.backgroundCube.uniforms),vertexShader:Hn.backgroundCube.vertexShader,fragmentShader:Hn.backgroundCube.fragmentShader,side:c,depthTest:!1,depthWrite:!1,fog:!1,allowOverride:!1})),S.geometry.deleteAttribute("normal"),S.geometry.deleteAttribute("uv"),S.onBeforeRender=function(e,t,n){this.matrixWorld.copyPosition(n.matrixWorld)},Object.defineProperty(S.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),r.update(S)),S.material.uniforms.envMap.value=i,S.material.uniforms.backgroundBlurriness.value=n.backgroundBlurriness,S.material.uniforms.backgroundIntensity.value=n.backgroundIntensity,S.material.uniforms.backgroundRotation.value.setFromMatrix4(Wn.makeRotationFromEuler(n.backgroundRotation)).transpose(),i.isCubeTexture&&!1===i.isRenderTargetTexture&&S.material.uniforms.backgroundRotation.value.premultiply(zn),S.material.toneMapped=f.getTransfer(i.colorSpace)!==p,T===i&&x===i.version&&R===e.toneMapping||(S.material.needsUpdate=!0,T=i,x=i.version,R=e.toneMapping),S.layers.enableAll(),t.unshift(S,S.geometry,S.material,0,0,null)):i&&i.isTexture&&(void 0===E&&(E=new o(new m(2,2),new l({name:"BackgroundMaterial",uniforms:d(Hn.background.uniforms),vertexShader:Hn.background.vertexShader,fragmentShader:Hn.background.fragmentShader,side:h,depthTest:!1,depthWrite:!1,fog:!1,allowOverride:!1})),E.geometry.deleteAttribute("normal"),Object.defineProperty(E.material,"map",{get:function(){return this.uniforms.t2D.value}}),r.update(E)),E.material.uniforms.t2D.value=i,E.material.uniforms.backgroundIntensity.value=n.backgroundIntensity,E.material.toneMapped=f.getTransfer(i.colorSpace)!==p,!0===i.matrixAutoUpdate&&i.updateMatrix(),E.material.uniforms.uvTransform.value.copy(i.matrix),T===i&&x===i.version&&R===e.toneMapping||(E.material.needsUpdate=!0,T=i,x=i.version,R=e.toneMapping),E.layers.enableAll(),t.unshift(E,E.geometry,E.material,0,0,null))},dispose:function(){void 0!==S&&(S.geometry.dispose(),S.material.dispose(),S=void 0),void 0!==E&&(E.geometry.dispose(),E.material.dispose(),E=void 0)}}}function Xn(e,t){const n=e.getParameter(e.MAX_VERTEX_ATTRIBS),i={},r=c(null);let a=r,o=!1;function s(t){return e.bindVertexArray(t)}function l(t){return e.deleteVertexArray(t)}function c(e){const t=[],i=[],r=[];for(let e=0;e=0){const n=r[t];let i=o[t];if(void 0===i&&("instanceMatrix"===t&&e.instanceMatrix&&(i=e.instanceMatrix),"instanceColor"===t&&e.instanceColor&&(i=e.instanceColor)),void 0===n)return!0;if(n.attribute!==i)return!0;if(i&&n.data!==i.data)return!0;s++}}return a.attributesNum!==s||a.index!==i}(n,h,l,_),v&&function(e,t,n,i){const r={},o=t.attributes;let s=0;const l=n.getAttributes();for(const t in l){if(l[t].location>=0){let n=o[t];void 0===n&&("instanceMatrix"===t&&e.instanceMatrix&&(n=e.instanceMatrix),"instanceColor"===t&&e.instanceColor&&(n=e.instanceColor));const i={};i.attribute=n,n&&n.data&&(i.data=n.data),r[t]=i,s++}}a.attributes=r,a.attributesNum=s,a.index=i}(n,h,l,_),null!==_&&t.update(_,e.ELEMENT_ARRAY_BUFFER),(v||o)&&(o=!1,function(n,i,r,a){d();const o=a.attributes,s=r.getAttributes(),l=i.defaultAttributeValues;for(const i in s){const r=s[i];if(r.location>=0){let s=o[i];if(void 0===s&&("instanceMatrix"===i&&n.instanceMatrix&&(s=n.instanceMatrix),"instanceColor"===i&&n.instanceColor&&(s=n.instanceColor)),void 0!==s){const i=s.normalized,o=s.itemSize,l=t.get(s);if(void 0===l)continue;const c=l.buffer,d=l.type,p=l.bytesPerElement,h=d===e.INT||d===e.UNSIGNED_INT||s.gpuType===g;if(s.isInterleavedBufferAttribute){const t=s.data,l=t.stride,_=s.offset;if(t.isInstancedInterleavedBuffer){for(let e=0;e0&&e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.HIGH_FLOAT).precision>0)return"highp";t="mediump"}return"mediump"===t&&e.getShaderPrecisionFormat(e.VERTEX_SHADER,e.MEDIUM_FLOAT).precision>0&&e.getShaderPrecisionFormat(e.FRAGMENT_SHADER,e.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}let o=void 0!==n.precision?n.precision:"highp";const s=a(o);s!==o&&(v("WebGLRenderer:",o,"not supported, using",s,"instead."),o=s);const l=!0===n.logarithmicDepthBuffer,c=!0===n.reversedDepthBuffer&&t.has("EXT_clip_control");!0===n.reversedDepthBuffer&&!1===c&&v("WebGLRenderer: Unable to use reversed depth buffer due to missing EXT_clip_control extension. Fallback to default depth buffer.");return{isWebGL2:!0,getMaxAnisotropy:function(){if(void 0!==r)return r;if(!0===t.has("EXT_texture_filter_anisotropic")){const n=t.get("EXT_texture_filter_anisotropic");r=e.getParameter(n.MAX_TEXTURE_MAX_ANISOTROPY_EXT)}else r=0;return r},getMaxPrecision:a,textureFormatReadable:function(t){return t===T||i.convert(t)===e.getParameter(e.IMPLEMENTATION_COLOR_READ_FORMAT)},textureTypeReadable:function(n){const r=n===E&&(t.has("EXT_color_buffer_half_float")||t.has("EXT_color_buffer_float"));return!(n!==S&&i.convert(n)!==e.getParameter(e.IMPLEMENTATION_COLOR_READ_TYPE)&&n!==M&&!r)},precision:o,logarithmicDepthBuffer:l,reversedDepthBuffer:c,maxTextures:e.getParameter(e.MAX_TEXTURE_IMAGE_UNITS),maxVertexTextures:e.getParameter(e.MAX_VERTEX_TEXTURE_IMAGE_UNITS),maxTextureSize:e.getParameter(e.MAX_TEXTURE_SIZE),maxCubemapSize:e.getParameter(e.MAX_CUBE_MAP_TEXTURE_SIZE),maxAttributes:e.getParameter(e.MAX_VERTEX_ATTRIBS),maxVertexUniforms:e.getParameter(e.MAX_VERTEX_UNIFORM_VECTORS),maxVaryings:e.getParameter(e.MAX_VARYING_VECTORS),maxFragmentUniforms:e.getParameter(e.MAX_FRAGMENT_UNIFORM_VECTORS),maxSamples:e.getParameter(e.MAX_SAMPLES),samples:e.getParameter(e.SAMPLES)}}function qn(t){const n=this;let i=null,r=0,a=!1,o=!1;const s=new x,l=new e,c={value:null,needsUpdate:!1};function d(e,t,i,r){const a=null!==e?e.length:0;let o=null;if(0!==a){if(o=c.value,!0!==r||null===o){const n=i+4*a,r=t.matrixWorldInverse;l.getNormalMatrix(r),(null===o||o.length0);n.numPlanes=r,n.numIntersection=0}();else{const e=o?0:r,t=4*e;let n=m.clippingState||null;c.value=n,n=d(u,s,t,l);for(let e=0;e!==t;++e)n[e]=i[e];m.clippingState=n,this.numIntersection=f?this.numPlanes:0,this.numPlanes+=e}}}zn.set(-1,0,0,0,1,0,0,0,1);const jn=[.125,.215,.35,.446,.526,.582],Zn=20,$n=new C,Qn=new n;let Jn=null,ei=0,ti=0,ni=!1;const ii=new i;class ri{constructor(e){this._renderer=e,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._sizeLods=[],this._sigmas=[],this._lodMeshes=[],this._backgroundBox=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._blurMaterial=null,this._ggxMaterial=null}fromScene(e,t=0,n=.1,i=100,r={}){const{size:a=256,position:o=ii}=r;Jn=this._renderer.getRenderTarget(),ei=this._renderer.getActiveCubeFace(),ti=this._renderer.getActiveMipmapLevel(),ni=this._renderer.xr.enabled,this._renderer.xr.enabled=!1,this._setSize(a);const s=this._allocateTargets();return s.depthBuffer=!0,this._sceneToCubeUV(e,n,i,s,o),t>0&&this._blur(s,0,0,t),this._applyPMREM(s),this._cleanup(s),s}fromEquirectangular(e,t=null){return this._fromTexture(e,t)}fromCubemap(e,t=null){return this._fromTexture(e,t)}compileCubemapShader(){null===this._cubemapMaterial&&(this._cubemapMaterial=li(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){null===this._equirectMaterial&&(this._equirectMaterial=si(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),null!==this._cubemapMaterial&&this._cubemapMaterial.dispose(),null!==this._equirectMaterial&&this._equirectMaterial.dispose(),null!==this._backgroundBox&&(this._backgroundBox.geometry.dispose(),this._backgroundBox.material.dispose())}_setSize(e){this._lodMax=Math.floor(Math.log2(e)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){null!==this._blurMaterial&&this._blurMaterial.dispose(),null!==this._ggxMaterial&&this._ggxMaterial.dispose(),null!==this._pingPongRenderTarget&&this._pingPongRenderTarget.dispose();for(let e=0;ee-4?l=jn[s-e+4-1]:0===s&&(l=0),n.push(l);const c=1/(a-2),d=-c,u=1+c,f=[d,d,u,d,u,u,d,d,u,u,d,u],p=6,m=6,h=3,_=2,g=1,v=new Float32Array(h*m*p),E=new Float32Array(_*m*p),S=new Float32Array(g*m*p);for(let e=0;e2?0:-1,i=[t,n,0,t+2/3,n,0,t+2/3,n+1,0,t,n,0,t+2/3,n+1,0,t,n+1,0];v.set(i,h*m*e),E.set(f,_*m*e);const r=[e,e,e,e,e,e];S.set(r,g*m*e)}const M=new b;M.setAttribute("position",new N(v,h)),M.setAttribute("uv",new N(E,_)),M.setAttribute("faceIndex",new N(S,g)),i.push(new o(M,null)),r>4&&r--}return{lodMeshes:i,sizeLods:t,sigmas:n}}(r)),this._blurMaterial=function(e,t,n){const r=new Float32Array(Zn),a=new i(0,1,0),o=new l({name:"SphericalGaussianBlur",defines:{n:Zn,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/n,CUBEUV_MAX_MIP:`${e}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:r},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:a}},vertexShader:ci(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\t\t\tuniform int samples;\n\t\t\tuniform float weights[ n ];\n\t\t\tuniform bool latitudinal;\n\t\t\tuniform float dTheta;\n\t\t\tuniform float mipInt;\n\t\t\tuniform vec3 poleAxis;\n\n\t\t\t#define ENVMAP_TYPE_CUBE_UV\n\t\t\t#include \n\n\t\t\tvec3 getSample( float theta, vec3 axis ) {\n\n\t\t\t\tfloat cosTheta = cos( theta );\n\t\t\t\t// Rodrigues' axis-angle rotation\n\t\t\t\tvec3 sampleDirection = vOutputDirection * cosTheta\n\t\t\t\t\t+ cross( axis, vOutputDirection ) * sin( theta )\n\t\t\t\t\t+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );\n\n\t\t\t\treturn bilinearCubeUV( envMap, sampleDirection, mipInt );\n\n\t\t\t}\n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );\n\n\t\t\t\tif ( all( equal( axis, vec3( 0.0 ) ) ) ) {\n\n\t\t\t\t\taxis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );\n\n\t\t\t\t}\n\n\t\t\t\taxis = normalize( axis );\n\n\t\t\t\tgl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t\t\t\tgl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );\n\n\t\t\t\tfor ( int i = 1; i < n; i++ ) {\n\n\t\t\t\t\tif ( i >= samples ) {\n\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfloat theta = dTheta * float( i );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( theta, axis );\n\n\t\t\t\t}\n\n\t\t\t}\n\t\t",blending:w,depthTest:!1,depthWrite:!1});return o}(r,e,t),this._ggxMaterial=function(e,t,n){const i=new l({name:"PMREMGGXConvolution",defines:{GGX_SAMPLES:256,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/n,CUBEUV_MAX_MIP:`${e}.0`},uniforms:{envMap:{value:null},roughness:{value:0},mipInt:{value:0}},vertexShader:ci(),fragmentShader:'\n\n\t\t\tprecision highp float;\n\t\t\tprecision highp int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\t\t\tuniform float roughness;\n\t\t\tuniform float mipInt;\n\n\t\t\t#define ENVMAP_TYPE_CUBE_UV\n\t\t\t#include \n\n\t\t\t#define PI 3.14159265359\n\n\t\t\t// Van der Corput radical inverse\n\t\t\tfloat radicalInverse_VdC(uint bits) {\n\t\t\t\tbits = (bits << 16u) | (bits >> 16u);\n\t\t\t\tbits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);\n\t\t\t\tbits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);\n\t\t\t\tbits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);\n\t\t\t\tbits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);\n\t\t\t\treturn float(bits) * 2.3283064365386963e-10; // / 0x100000000\n\t\t\t}\n\n\t\t\t// Hammersley sequence\n\t\t\tvec2 hammersley(uint i, uint N) {\n\t\t\t\treturn vec2(float(i) / float(N), radicalInverse_VdC(i));\n\t\t\t}\n\n\t\t\t// GGX VNDF importance sampling (Eric Heitz 2018)\n\t\t\t// "Sampling the GGX Distribution of Visible Normals"\n\t\t\t// https://jcgt.org/published/0007/04/01/\n\t\t\tvec3 importanceSampleGGX_VNDF(vec2 Xi, vec3 V, float roughness) {\n\t\t\t\tfloat alpha = roughness * roughness;\n\n\t\t\t\t// Section 4.1: Orthonormal basis\n\t\t\t\tvec3 T1 = vec3(1.0, 0.0, 0.0);\n\t\t\t\tvec3 T2 = cross(V, T1);\n\n\t\t\t\t// Section 4.2: Parameterization of projected area\n\t\t\t\tfloat r = sqrt(Xi.x);\n\t\t\t\tfloat phi = 2.0 * PI * Xi.y;\n\t\t\t\tfloat t1 = r * cos(phi);\n\t\t\t\tfloat t2 = r * sin(phi);\n\t\t\t\tfloat s = 0.5 * (1.0 + V.z);\n\t\t\t\tt2 = (1.0 - s) * sqrt(1.0 - t1 * t1) + s * t2;\n\n\t\t\t\t// Section 4.3: Reprojection onto hemisphere\n\t\t\t\tvec3 Nh = t1 * T1 + t2 * T2 + sqrt(max(0.0, 1.0 - t1 * t1 - t2 * t2)) * V;\n\n\t\t\t\t// Section 3.4: Transform back to ellipsoid configuration\n\t\t\t\treturn normalize(vec3(alpha * Nh.x, alpha * Nh.y, max(0.0, Nh.z)));\n\t\t\t}\n\n\t\t\tvoid main() {\n\t\t\t\tvec3 N = normalize(vOutputDirection);\n\t\t\t\tvec3 V = N; // Assume view direction equals normal for pre-filtering\n\n\t\t\t\tvec3 prefilteredColor = vec3(0.0);\n\t\t\t\tfloat totalWeight = 0.0;\n\n\t\t\t\t// For very low roughness, just sample the environment directly\n\t\t\t\tif (roughness < 0.001) {\n\t\t\t\t\tgl_FragColor = vec4(bilinearCubeUV(envMap, N, mipInt), 1.0);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Tangent space basis for VNDF sampling\n\t\t\t\tvec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);\n\t\t\t\tvec3 tangent = normalize(cross(up, N));\n\t\t\t\tvec3 bitangent = cross(N, tangent);\n\n\t\t\t\tfor(uint i = 0u; i < uint(GGX_SAMPLES); i++) {\n\t\t\t\t\tvec2 Xi = hammersley(i, uint(GGX_SAMPLES));\n\n\t\t\t\t\t// For PMREM, V = N, so in tangent space V is always (0, 0, 1)\n\t\t\t\t\tvec3 H_tangent = importanceSampleGGX_VNDF(Xi, vec3(0.0, 0.0, 1.0), roughness);\n\n\t\t\t\t\t// Transform H back to world space\n\t\t\t\t\tvec3 H = normalize(tangent * H_tangent.x + bitangent * H_tangent.y + N * H_tangent.z);\n\t\t\t\t\tvec3 L = normalize(2.0 * dot(V, H) * H - V);\n\n\t\t\t\t\tfloat NdotL = max(dot(N, L), 0.0);\n\n\t\t\t\t\tif(NdotL > 0.0) {\n\t\t\t\t\t\t// Sample environment at fixed mip level\n\t\t\t\t\t\t// VNDF importance sampling handles the distribution filtering\n\t\t\t\t\t\tvec3 sampleColor = bilinearCubeUV(envMap, L, mipInt);\n\n\t\t\t\t\t\t// Weight by NdotL for the split-sum approximation\n\t\t\t\t\t\t// VNDF PDF naturally accounts for the visible microfacet distribution\n\t\t\t\t\t\tprefilteredColor += sampleColor * NdotL;\n\t\t\t\t\t\ttotalWeight += NdotL;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (totalWeight > 0.0) {\n\t\t\t\t\tprefilteredColor = prefilteredColor / totalWeight;\n\t\t\t\t}\n\n\t\t\t\tgl_FragColor = vec4(prefilteredColor, 1.0);\n\t\t\t}\n\t\t',blending:w,depthTest:!1,depthWrite:!1});return i}(r,e,t)}return r}_compileMaterial(e){const t=new o(new b,e);this._renderer.compile(t,$n)}_sceneToCubeUV(e,t,n,i,r){const a=new P(90,1,t,n),l=[1,-1,1,1,1,1],d=[1,1,1,-1,-1,-1],u=this._renderer,f=u.autoClear,p=u.toneMapping;u.getClearColor(Qn),u.toneMapping=L,u.autoClear=!1;u.state.buffers.depth.getReversed()&&(u.setRenderTarget(i),u.clearDepth(),u.setRenderTarget(null)),null===this._backgroundBox&&(this._backgroundBox=new o(new s,new U({name:"PMREM.Background",side:c,depthWrite:!1,depthTest:!1})));const m=this._backgroundBox,h=m.material;let _=!1;const g=e.background;g?g.isColor&&(h.color.copy(g),e.background=null,_=!0):(h.color.copy(Qn),_=!0);for(let t=0;t<6;t++){const n=t%3;0===n?(a.up.set(0,l[t],0),a.position.set(r.x,r.y,r.z),a.lookAt(r.x+d[t],r.y,r.z)):1===n?(a.up.set(0,0,l[t]),a.position.set(r.x,r.y,r.z),a.lookAt(r.x,r.y+d[t],r.z)):(a.up.set(0,l[t],0),a.position.set(r.x,r.y,r.z),a.lookAt(r.x,r.y,r.z+d[t]));const o=this._cubeSize;oi(i,n*o,t>2?o:0,o,o),u.setRenderTarget(i),_&&u.render(m,a),u.render(e,a)}u.toneMapping=p,u.autoClear=f,e.background=g}_textureToCubeUV(e,t){const n=this._renderer,i=e.mapping===R||e.mapping===A;i?(null===this._cubemapMaterial&&(this._cubemapMaterial=li()),this._cubemapMaterial.uniforms.flipEnvMap.value=!1===e.isRenderTargetTexture?-1:1):null===this._equirectMaterial&&(this._equirectMaterial=si());const r=i?this._cubemapMaterial:this._equirectMaterial,a=this._lodMeshes[0];a.material=r;r.uniforms.envMap.value=e;const o=this._cubeSize;oi(t,0,0,3*o,2*o),n.setRenderTarget(t),n.render(a,$n)}_applyPMREM(e){const t=this._renderer,n=t.autoClear;t.autoClear=!1;const i=this._lodMeshes.length;for(let t=1;tu-4?n-u+4:0),m=4*(this._cubeSize-f);s.envMap.value=e.texture,s.roughness.value=d,s.mipInt.value=u-t,oi(r,p,m,3*f,2*f),i.setRenderTarget(r),i.render(o,$n),s.envMap.value=r.texture,s.roughness.value=0,s.mipInt.value=u-n,oi(e,p,m,3*f,2*f),i.setRenderTarget(e),i.render(o,$n)}_blur(e,t,n,i,r){const a=this._pingPongRenderTarget;this._halfBlur(e,a,t,n,i,"latitudinal",r),this._halfBlur(a,e,n,n,i,"longitudinal",r)}_halfBlur(e,t,n,i,r,a,o){const s=this._renderer,l=this._blurMaterial;"latitudinal"!==a&&"longitudinal"!==a&&D("blur direction must be either latitudinal or longitudinal!");const c=this._lodMeshes[i];c.material=l;const d=l.uniforms,u=this._sizeLods[n]-1,f=isFinite(r)?Math.PI/(2*u):2*Math.PI/39,p=r/f,m=isFinite(r)?1+Math.floor(3*p):Zn;m>Zn&&v(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to 20`);const h=[];let _=0;for(let e=0;eg-4?i-g+4:0),4*(this._cubeSize-E),3*E,2*E),s.setRenderTarget(t),s.render(c,$n)}}function ai(e,t,n){const i=new I(e,t,n);return i.texture.mapping=a,i.texture.name="PMREM.cubeUv",i.scissorTest=!0,i}function oi(e,t,n,i,r){e.viewport.set(t,n,i,r),e.scissor.set(t,n,i,r)}function si(){return new l({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:ci(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\n\t\t\t#include \n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 outputDirection = normalize( vOutputDirection );\n\t\t\t\tvec2 uv = equirectUv( outputDirection );\n\n\t\t\t\tgl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 );\n\n\t\t\t}\n\t\t",blending:w,depthTest:!1,depthWrite:!1})}function li(){return new l({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:ci(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tuniform float flipEnvMap;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform samplerCube envMap;\n\n\t\t\tvoid main() {\n\n\t\t\t\tgl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) );\n\n\t\t\t}\n\t\t",blending:w,depthTest:!1,depthWrite:!1})}function ci(){return"\n\n\t\tprecision mediump float;\n\t\tprecision mediump int;\n\n\t\tattribute float faceIndex;\n\n\t\tvarying vec3 vOutputDirection;\n\n\t\t// RH coordinate system; PMREM face-indexing convention\n\t\tvec3 getDirection( vec2 uv, float face ) {\n\n\t\t\tuv = 2.0 * uv - 1.0;\n\n\t\t\tvec3 direction = vec3( uv, 1.0 );\n\n\t\t\tif ( face == 0.0 ) {\n\n\t\t\t\tdirection = direction.zyx; // ( 1, v, u ) pos x\n\n\t\t\t} else if ( face == 1.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xz *= -1.0; // ( -u, 1, -v ) pos y\n\n\t\t\t} else if ( face == 2.0 ) {\n\n\t\t\t\tdirection.x *= -1.0; // ( -u, v, 1 ) pos z\n\n\t\t\t} else if ( face == 3.0 ) {\n\n\t\t\t\tdirection = direction.zyx;\n\t\t\t\tdirection.xz *= -1.0; // ( -1, v, -u ) neg x\n\n\t\t\t} else if ( face == 4.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xy *= -1.0; // ( -u, -1, v ) neg y\n\n\t\t\t} else if ( face == 5.0 ) {\n\n\t\t\t\tdirection.z *= -1.0; // ( u, v, -1 ) neg z\n\n\t\t\t}\n\n\t\t\treturn direction;\n\n\t\t}\n\n\t\tvoid main() {\n\n\t\t\tvOutputDirection = getDirection( uv, faceIndex );\n\t\t\tgl_Position = vec4( position, 1.0 );\n\n\t\t}\n\t"}class di extends I{constructor(e=1,t={}){super(e,e,t),this.isWebGLCubeRenderTarget=!0;const n={width:e,height:e,depth:1},i=[n,n,n,n,n,n];this.texture=new F(i),this._setTextureOptions(t),this.texture.isRenderTargetTexture=!0}fromEquirectangularTexture(e,t){this.texture.type=t.type,this.texture.colorSpace=t.colorSpace,this.texture.generateMipmaps=t.generateMipmaps,this.texture.minFilter=t.minFilter,this.texture.magFilter=t.magFilter;const n={uniforms:{tEquirect:{value:null}},vertexShader:"\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\tvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\n\t\t\t\t\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n\n\t\t\t\t}\n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvWorldDirection = transformDirection( position, modelMatrix );\n\n\t\t\t\t\t#include \n\t\t\t\t\t#include \n\n\t\t\t\t}\n\t\t\t",fragmentShader:"\n\n\t\t\t\tuniform sampler2D tEquirect;\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\t#include \n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvec3 direction = normalize( vWorldDirection );\n\n\t\t\t\t\tvec2 sampleUV = equirectUv( direction );\n\n\t\t\t\t\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\n\t\t\t\t}\n\t\t\t"},i=new s(5,5,5),r=new l({name:"CubemapFromEquirect",uniforms:d(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:c,blending:w});r.uniforms.tEquirect.value=t;const a=new o(i,r),u=t.minFilter;t.minFilter===B&&(t.minFilter=O);return new G(1,10,this).update(e,a),t.minFilter=u,a.geometry.dispose(),a.material.dispose(),this}clear(e,t=!0,n=!0,i=!0){const r=e.getRenderTarget();for(let r=0;r<6;r++)e.setRenderTarget(this,r),e.clear(t,n,i);e.setRenderTarget(r)}}function ui(e){let t=new WeakMap,n=new WeakMap,i=null;function r(e,t){return t===H?e.mapping=R:t===V&&(e.mapping=A),e}function a(e){const n=e.target;n.removeEventListener("dispose",a);const i=t.get(n);void 0!==i&&(t.delete(n),i.dispose())}function o(e){const t=e.target;t.removeEventListener("dispose",o);const i=n.get(t);void 0!==i&&(n.delete(t),i.dispose())}return{get:function(s,l=!1){return null==s?null:l?function(t){if(t&&t.isTexture){const r=t.mapping,a=r===H||r===V,s=r===R||r===A;if(a||s){let r=n.get(t);const l=void 0!==r?r.texture.pmremVersion:0;if(t.isRenderTargetTexture&&t.pmremVersion!==l)return null===i&&(i=new ri(e)),r=a?i.fromEquirectangular(t,r):i.fromCubemap(t,r),r.texture.pmremVersion=t.pmremVersion,n.set(t,r),r.texture;if(void 0!==r)return r.texture;{const l=t.image;return a&&l&&l.height>0||s&&l&&function(e){let t=0;const n=6;for(let i=0;i0){const o=new di(i.height);return o.fromEquirectangularTexture(e,n),t.set(n,o),n.addEventListener("dispose",a),r(o.texture,n.mapping)}return null}}}return n}(s)},dispose:function(){t=new WeakMap,n=new WeakMap,null!==i&&(i.dispose(),i=null)}}}function fi(e){const t={};function n(n){if(void 0!==t[n])return t[n];const i=e.getExtension(n);return t[n]=i,i}return{has:function(e){return null!==n(e)},init:function(){n("EXT_color_buffer_float"),n("WEBGL_clip_cull_distance"),n("OES_texture_float_linear"),n("EXT_color_buffer_half_float"),n("WEBGL_multisampled_render_to_texture"),n("WEBGL_render_shared_exponent")},get:function(e){const t=n(e);return null===t&&W("WebGLRenderer: "+e+" extension not supported."),t}}}function pi(e,t,n,i){const r={},a=new WeakMap;function o(e){const s=e.target;null!==s.index&&t.remove(s.index);for(const e in s.attributes)t.remove(s.attributes[e]);s.removeEventListener("dispose",o),delete r[s.id];const l=a.get(s);l&&(t.remove(l),a.delete(s)),i.releaseStatesOfGeometry(s),!0===s.isInstancedBufferGeometry&&delete s._maxInstanceCount,n.memory.geometries--}function s(e){const n=[],i=e.index,r=e.attributes.position;let o=0;if(void 0===r)return;if(null!==i){const e=i.array;o=i.version;for(let t=0,i=e.length;t=65535?z:k)(n,1);s.version=o;const l=a.get(e);l&&t.remove(l),a.set(e,s)}return{get:function(e,t){return!0===r[t.id]||(t.addEventListener("dispose",o),r[t.id]=!0,n.memory.geometries++),t},update:function(n){const i=n.attributes;for(const n in i)t.update(i[n],e.ARRAY_BUFFER)},getWireframeAttribute:function(e){const t=a.get(e);if(t){const n=e.index;null!==n&&t.versionn.maxTextureSize&&(T=Math.ceil(S/n.maxTextureSize),S=n.maxTextureSize);const x=new Float32Array(S*T*4*u),R=new X(x,S,T,u);R.type=M,R.needsUpdate=!0;const A=4*E;for(let C=0;C\n\t\t\t#include \n\n\t\t\tvoid main() {\n\t\t\t\tgl_FragColor = texture2D( tDiffuse, vUv );\n\n\t\t\t\t#ifdef LINEAR_TONE_MAPPING\n\t\t\t\t\tgl_FragColor.rgb = LinearToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( REINHARD_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = ReinhardToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( CINEON_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = CineonToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( ACES_FILMIC_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = ACESFilmicToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( AGX_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = AgXToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( NEUTRAL_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = NeutralToneMapping( gl_FragColor.rgb );\n\t\t\t\t#elif defined( CUSTOM_TONE_MAPPING )\n\t\t\t\t\tgl_FragColor.rgb = CustomToneMapping( gl_FragColor.rgb );\n\t\t\t\t#endif\n\n\t\t\t\t#ifdef SRGB_TRANSFER\n\t\t\t\t\tgl_FragColor = sRGBTransferOETF( gl_FragColor );\n\t\t\t\t#endif\n\t\t\t}",depthTest:!1,depthWrite:!1}),u=new o(c,d),m=new C(-1,1,1,-1,0,1);let h,_=null,g=null,v=!1,S=null,M=[],T=!1;this.setSize=function(e,t){s.setSize(e,t),l.setSize(e,t);for(let n=0;n0&&!0===M[0].isRenderPass;const t=s.width,n=s.height;for(let e=0;e0)return e;const r=t*n;let a=Ai[r];if(void 0===a&&(a=new Float32Array(r),Ai[r]=a),0!==t){i.toArray(a,0);for(let i=1,r=0;i!==t;++i)r+=n,e[i].toArray(a,r)}return a}function Di(e,t){if(e.length!==t.length)return!1;for(let n=0,i=e.length;n0&&(this.seq=i.concat(r))}setValue(e,t,n,i){const r=this.map[t];void 0!==r&&r.setValue(e,n,i)}setOptional(e,t,n){const i=t[n];void 0!==i&&this.setValue(e,n,i)}static upload(e,t,n,i){for(let r=0,a=t.length;r!==a;++r){const a=t[r],o=n[a.id];!1!==o.needsUpdate&&a.setValue(e,o.value,i)}}static seqWithValue(e,t){const n=[];for(let i=0,r=e.length;i!==r;++i){const r=e[i];r.id in t&&n.push(r)}return n}}function Ar(e,t,n){const i=e.createShader(t);return e.shaderSource(i,n),e.compileShader(i),i}let br=0;const Cr=new e;function Pr(e,t,n){const i=e.getShaderParameter(t,e.COMPILE_STATUS),r=(e.getShaderInfoLog(t)||"").trim();if(i&&""===r)return"";const a=/ERROR: 0:(\d+)/.exec(r);if(a){const i=parseInt(a[1]);return n.toUpperCase()+"\n\n"+r+"\n\n"+function(e,t){const n=e.split("\n"),i=[],r=Math.max(t-6,0),a=Math.min(t+6,n.length);for(let e=r;e":" "} ${r}: ${n[e]}`)}return i.join("\n")}(e.getShaderSource(t),i)}return r}function Lr(e,t){const n=function(e){f._getMatrix(Cr,f.workingColorSpace,e);const t=`mat3( ${Cr.elements.map(e=>e.toFixed(4))} )`;switch(f.getTransfer(e)){case pe:return[t,"LinearTransferOETF"];case p:return[t,"sRGBTransferOETF"];default:return v("WebGLProgram: Unsupported color space: ",e),[t,"LinearTransferOETF"]}}(t);return[`vec4 ${e}( vec4 value ) {`,`\treturn ${n[1]}( vec4( value.rgb * ${n[0]}, value.a ) );`,"}"].join("\n")}const Ur={[ne]:"Linear",[te]:"Reinhard",[ee]:"Cineon",[J]:"ACESFilmic",[Q]:"AgX",[$]:"Neutral",[Z]:"Custom"};function Dr(e,t){const n=Ur[t];return void 0===n?(v("WebGLProgram: Unsupported toneMapping:",t),"vec3 "+e+"( vec3 color ) { return LinearToneMapping( color ); }"):"vec3 "+e+"( vec3 color ) { return "+n+"ToneMapping( color ); }"}const wr=new i;function Ir(){f.getLuminanceCoefficients(wr);return["float luminance( const in vec3 rgb ) {",`\tconst vec3 weights = vec3( ${wr.x.toFixed(4)}, ${wr.y.toFixed(4)}, ${wr.z.toFixed(4)} );`,"\treturn dot( weights, rgb );","}"].join("\n")}function Nr(e){return""!==e}function yr(e,t){const n=t.numSpotLightShadows+t.numSpotLightMaps-t.numSpotLightShadowsWithMaps;return e.replace(/NUM_DIR_LIGHTS/g,t.numDirLights).replace(/NUM_SPOT_LIGHTS/g,t.numSpotLights).replace(/NUM_SPOT_LIGHT_MAPS/g,t.numSpotLightMaps).replace(/NUM_SPOT_LIGHT_COORDS/g,n).replace(/NUM_RECT_AREA_LIGHTS/g,t.numRectAreaLights).replace(/NUM_POINT_LIGHTS/g,t.numPointLights).replace(/NUM_HEMI_LIGHTS/g,t.numHemiLights).replace(/NUM_DIR_LIGHT_SHADOWS/g,t.numDirLightShadows).replace(/NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS/g,t.numSpotLightShadowsWithMaps).replace(/NUM_SPOT_LIGHT_SHADOWS/g,t.numSpotLightShadows).replace(/NUM_POINT_LIGHT_SHADOWS/g,t.numPointLightShadows)}function Or(e,t){return e.replace(/NUM_CLIPPING_PLANES/g,t.numClippingPlanes).replace(/UNION_CLIPPING_PLANES/g,t.numClippingPlanes-t.numClipIntersection)}const Fr=/^[ \t]*#include +<([\w\d./]+)>/gm;function Br(e){return e.replace(Fr,Hr)}const Gr=new Map;function Hr(e,t){let n=Bn[t];if(void 0===n){const e=Gr.get(t);if(void 0===e)throw new Error("THREE.WebGLProgram: Can not resolve #include <"+t+">");n=Bn[e],v('WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',t,e)}return Br(n)}const Vr=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function Wr(e){return e.replace(Vr,zr)}function zr(e,t,n,i){let r="";for(let e=parseInt(t);e0&&(_+="\n"),g=["#define SHADER_TYPE "+n.shaderType,"#define SHADER_NAME "+n.shaderName,m].filter(Nr).join("\n"),g.length>0&&(g+="\n")):(_=[kr(n),"#define SHADER_TYPE "+n.shaderType,"#define SHADER_NAME "+n.shaderName,m,n.extensionClipCullDistance?"#define USE_CLIP_DISTANCE":"",n.batching?"#define USE_BATCHING":"",n.batchingColor?"#define USE_BATCHING_COLOR":"",n.instancing?"#define USE_INSTANCING":"",n.instancingColor?"#define USE_INSTANCING_COLOR":"",n.instancingMorph?"#define USE_INSTANCING_MORPH":"",n.useFog&&n.fog?"#define USE_FOG":"",n.useFog&&n.fogExp2?"#define FOG_EXP2":"",n.map?"#define USE_MAP":"",n.envMap?"#define USE_ENVMAP":"",n.envMap?"#define "+d:"",n.lightMap?"#define USE_LIGHTMAP":"",n.aoMap?"#define USE_AOMAP":"",n.bumpMap?"#define USE_BUMPMAP":"",n.normalMap?"#define USE_NORMALMAP":"",n.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",n.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",n.displacementMap?"#define USE_DISPLACEMENTMAP":"",n.emissiveMap?"#define USE_EMISSIVEMAP":"",n.anisotropy?"#define USE_ANISOTROPY":"",n.anisotropyMap?"#define USE_ANISOTROPYMAP":"",n.clearcoatMap?"#define USE_CLEARCOATMAP":"",n.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",n.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",n.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",n.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",n.specularMap?"#define USE_SPECULARMAP":"",n.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",n.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",n.roughnessMap?"#define USE_ROUGHNESSMAP":"",n.metalnessMap?"#define USE_METALNESSMAP":"",n.alphaMap?"#define USE_ALPHAMAP":"",n.alphaHash?"#define USE_ALPHAHASH":"",n.transmission?"#define USE_TRANSMISSION":"",n.transmissionMap?"#define USE_TRANSMISSIONMAP":"",n.thicknessMap?"#define USE_THICKNESSMAP":"",n.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",n.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",n.mapUv?"#define MAP_UV "+n.mapUv:"",n.alphaMapUv?"#define ALPHAMAP_UV "+n.alphaMapUv:"",n.lightMapUv?"#define LIGHTMAP_UV "+n.lightMapUv:"",n.aoMapUv?"#define AOMAP_UV "+n.aoMapUv:"",n.emissiveMapUv?"#define EMISSIVEMAP_UV "+n.emissiveMapUv:"",n.bumpMapUv?"#define BUMPMAP_UV "+n.bumpMapUv:"",n.normalMapUv?"#define NORMALMAP_UV "+n.normalMapUv:"",n.displacementMapUv?"#define DISPLACEMENTMAP_UV "+n.displacementMapUv:"",n.metalnessMapUv?"#define METALNESSMAP_UV "+n.metalnessMapUv:"",n.roughnessMapUv?"#define ROUGHNESSMAP_UV "+n.roughnessMapUv:"",n.anisotropyMapUv?"#define ANISOTROPYMAP_UV "+n.anisotropyMapUv:"",n.clearcoatMapUv?"#define CLEARCOATMAP_UV "+n.clearcoatMapUv:"",n.clearcoatNormalMapUv?"#define CLEARCOAT_NORMALMAP_UV "+n.clearcoatNormalMapUv:"",n.clearcoatRoughnessMapUv?"#define CLEARCOAT_ROUGHNESSMAP_UV "+n.clearcoatRoughnessMapUv:"",n.iridescenceMapUv?"#define IRIDESCENCEMAP_UV "+n.iridescenceMapUv:"",n.iridescenceThicknessMapUv?"#define IRIDESCENCE_THICKNESSMAP_UV "+n.iridescenceThicknessMapUv:"",n.sheenColorMapUv?"#define SHEEN_COLORMAP_UV "+n.sheenColorMapUv:"",n.sheenRoughnessMapUv?"#define SHEEN_ROUGHNESSMAP_UV "+n.sheenRoughnessMapUv:"",n.specularMapUv?"#define SPECULARMAP_UV "+n.specularMapUv:"",n.specularColorMapUv?"#define SPECULAR_COLORMAP_UV "+n.specularColorMapUv:"",n.specularIntensityMapUv?"#define SPECULAR_INTENSITYMAP_UV "+n.specularIntensityMapUv:"",n.transmissionMapUv?"#define TRANSMISSIONMAP_UV "+n.transmissionMapUv:"",n.thicknessMapUv?"#define THICKNESSMAP_UV "+n.thicknessMapUv:"",n.vertexTangents&&!1===n.flatShading?"#define USE_TANGENT":"",n.vertexNormals?"#define HAS_NORMAL":"",n.vertexColors?"#define USE_COLOR":"",n.vertexAlphas?"#define USE_COLOR_ALPHA":"",n.vertexUv1s?"#define USE_UV1":"",n.vertexUv2s?"#define USE_UV2":"",n.vertexUv3s?"#define USE_UV3":"",n.pointsUvs?"#define USE_POINTS_UV":"",n.flatShading?"#define FLAT_SHADED":"",n.skinning?"#define USE_SKINNING":"",n.morphTargets?"#define USE_MORPHTARGETS":"",n.morphNormals&&!1===n.flatShading?"#define USE_MORPHNORMALS":"",n.morphColors?"#define USE_MORPHCOLORS":"",n.morphTargetsCount>0?"#define MORPHTARGETS_TEXTURE_STRIDE "+n.morphTextureStride:"",n.morphTargetsCount>0?"#define MORPHTARGETS_COUNT "+n.morphTargetsCount:"",n.doubleSided?"#define DOUBLE_SIDED":"",n.flipSided?"#define FLIP_SIDED":"",n.shadowMapEnabled?"#define USE_SHADOWMAP":"",n.shadowMapEnabled?"#define "+l:"",n.sizeAttenuation?"#define USE_SIZEATTENUATION":"",n.numLightProbes>0?"#define USE_LIGHT_PROBES":"",n.logarithmicDepthBuffer?"#define USE_LOGARITHMIC_DEPTH_BUFFER":"",n.reversedDepthBuffer?"#define USE_REVERSED_DEPTH_BUFFER":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING","\tattribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR","\tattribute vec3 instanceColor;","#endif","#ifdef USE_INSTANCING_MORPH","\tuniform sampler2D morphTexture;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_UV1","\tattribute vec2 uv1;","#endif","#ifdef USE_UV2","\tattribute vec2 uv2;","#endif","#ifdef USE_UV3","\tattribute vec2 uv3;","#endif","#ifdef USE_TANGENT","\tattribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )","\tattribute vec4 color;","#elif defined( USE_COLOR )","\tattribute vec3 color;","#endif","#ifdef USE_SKINNING","\tattribute vec4 skinIndex;","\tattribute vec4 skinWeight;","#endif","\n"].filter(Nr).join("\n"),g=[kr(n),"#define SHADER_TYPE "+n.shaderType,"#define SHADER_NAME "+n.shaderName,m,n.useFog&&n.fog?"#define USE_FOG":"",n.useFog&&n.fogExp2?"#define FOG_EXP2":"",n.alphaToCoverage?"#define ALPHA_TO_COVERAGE":"",n.map?"#define USE_MAP":"",n.matcap?"#define USE_MATCAP":"",n.envMap?"#define USE_ENVMAP":"",n.envMap?"#define "+c:"",n.envMap?"#define "+d:"",n.envMap?"#define "+u:"",f?"#define CUBEUV_TEXEL_WIDTH "+f.texelWidth:"",f?"#define CUBEUV_TEXEL_HEIGHT "+f.texelHeight:"",f?"#define CUBEUV_MAX_MIP "+f.maxMip+".0":"",n.lightMap?"#define USE_LIGHTMAP":"",n.aoMap?"#define USE_AOMAP":"",n.bumpMap?"#define USE_BUMPMAP":"",n.normalMap?"#define USE_NORMALMAP":"",n.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",n.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",n.packedNormalMap?"#define USE_PACKED_NORMALMAP":"",n.emissiveMap?"#define USE_EMISSIVEMAP":"",n.anisotropy?"#define USE_ANISOTROPY":"",n.anisotropyMap?"#define USE_ANISOTROPYMAP":"",n.clearcoat?"#define USE_CLEARCOAT":"",n.clearcoatMap?"#define USE_CLEARCOATMAP":"",n.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",n.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",n.dispersion?"#define USE_DISPERSION":"",n.iridescence?"#define USE_IRIDESCENCE":"",n.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",n.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",n.specularMap?"#define USE_SPECULARMAP":"",n.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",n.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",n.roughnessMap?"#define USE_ROUGHNESSMAP":"",n.metalnessMap?"#define USE_METALNESSMAP":"",n.alphaMap?"#define USE_ALPHAMAP":"",n.alphaTest?"#define USE_ALPHATEST":"",n.alphaHash?"#define USE_ALPHAHASH":"",n.sheen?"#define USE_SHEEN":"",n.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",n.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",n.transmission?"#define USE_TRANSMISSION":"",n.transmissionMap?"#define USE_TRANSMISSIONMAP":"",n.thicknessMap?"#define USE_THICKNESSMAP":"",n.vertexTangents&&!1===n.flatShading?"#define USE_TANGENT":"",n.vertexColors||n.instancingColor?"#define USE_COLOR":"",n.vertexAlphas||n.batchingColor?"#define USE_COLOR_ALPHA":"",n.vertexUv1s?"#define USE_UV1":"",n.vertexUv2s?"#define USE_UV2":"",n.vertexUv3s?"#define USE_UV3":"",n.pointsUvs?"#define USE_POINTS_UV":"",n.gradientMap?"#define USE_GRADIENTMAP":"",n.flatShading?"#define FLAT_SHADED":"",n.doubleSided?"#define DOUBLE_SIDED":"",n.flipSided?"#define FLIP_SIDED":"",n.shadowMapEnabled?"#define USE_SHADOWMAP":"",n.shadowMapEnabled?"#define "+l:"",n.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",n.numLightProbes>0?"#define USE_LIGHT_PROBES":"",n.numLightProbeGrids>0?"#define USE_LIGHT_PROBES_GRID":"",n.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",n.decodeVideoTextureEmissive?"#define DECODE_VIDEO_TEXTURE_EMISSIVE":"",n.logarithmicDepthBuffer?"#define USE_LOGARITHMIC_DEPTH_BUFFER":"",n.reversedDepthBuffer?"#define USE_REVERSED_DEPTH_BUFFER":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",n.toneMapping!==L?"#define TONE_MAPPING":"",n.toneMapping!==L?Bn.tonemapping_pars_fragment:"",n.toneMapping!==L?Dr("toneMapping",n.toneMapping):"",n.dithering?"#define DITHERING":"",n.opaque?"#define OPAQUE":"",Bn.colorspace_pars_fragment,Lr("linearToOutputTexel",n.outputColorSpace),Ir(),n.useDepthPacking?"#define DEPTH_PACKING "+n.depthPacking:"","\n"].filter(Nr).join("\n")),o=Br(o),o=yr(o,n),o=Or(o,n),s=Br(s),s=yr(s,n),s=Or(s,n),o=Wr(o),s=Wr(s),!0!==n.isRawShaderMaterial&&(E="#version 300 es\n",_=[p,"#define attribute in","#define varying out","#define texture2D texture"].join("\n")+"\n"+_,g=["#define varying in",n.glslVersion===se?"":"layout(location = 0) out highp vec4 pc_fragColor;",n.glslVersion===se?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join("\n")+"\n"+g);const S=E+_+o,M=E+g+s,T=Ar(r,r.VERTEX_SHADER,S),x=Ar(r,r.FRAGMENT_SHADER,M);function R(t){if(e.debug.checkShaderErrors){const n=r.getProgramInfoLog(h)||"",i=r.getShaderInfoLog(T)||"",a=r.getShaderInfoLog(x)||"",o=n.trim(),s=i.trim(),l=a.trim();let c=!0,d=!0;if(!1===r.getProgramParameter(h,r.LINK_STATUS))if(c=!1,"function"==typeof e.debug.onShaderError)e.debug.onShaderError(r,h,T,x);else{const e=Pr(r,T,"vertex"),n=Pr(r,x,"fragment");D("WebGLProgram: Shader Error "+r.getError()+" - VALIDATE_STATUS "+r.getProgramParameter(h,r.VALIDATE_STATUS)+"\n\nMaterial Name: "+t.name+"\nMaterial Type: "+t.type+"\n\nProgram Info Log: "+o+"\n"+e+"\n"+n)}else""!==o?v("WebGLProgram: Program Info Log:",o):""!==s&&""!==l||(d=!1);d&&(t.diagnostics={runnable:c,programLog:o,vertexShader:{log:s,prefix:_},fragmentShader:{log:l,prefix:g}})}r.deleteShader(T),r.deleteShader(x),A=new Rr(r,h),b=function(e,t){const n={},i=e.getProgramParameter(t,e.ACTIVE_ATTRIBUTES);for(let r=0;r0,J=r.clearcoat>0,ee=r.dispersion>0,te=r.iridescence>0,ne=r.sheen>0,ie=r.transmission>0,re=Q&&!!r.anisotropyMap,ae=J&&!!r.clearcoatMap,oe=J&&!!r.clearcoatNormalMap,se=J&&!!r.clearcoatRoughnessMap,le=te&&!!r.iridescenceMap,ce=te&&!!r.iridescenceThicknessMap,de=ne&&!!r.sheenColorMap,ue=ne&&!!r.sheenRoughnessMap,fe=!!r.specularMap,pe=!!r.specularColorMap,me=!!r.specularIntensityMap,Ee=ie&&!!r.transmissionMap,xe=ie&&!!r.thicknessMap,Re=!!r.gradientMap,Ae=!!r.alphaMap,be=r.alphaTest>0,Ce=!!r.alphaHash,Pe=!!r.extensions;let Le=L;r.toneMapped&&(null!==F&&!0!==F.isXRRenderTarget||(Le=e.toneMapping));const Ue={shaderID:P,shaderType:r.type,shaderName:r.name,vertexShader:w,fragmentShader:I,defines:r.defines,customVertexShaderID:N,customFragmentShaderID:y,isRawShaderMaterial:!0===r.isRawShaderMaterial,glslVersion:r.glslVersion,precision:_,batching:H,batchingColor:H&&null!==S._colorsTexture,instancing:G,instancingColor:G&&null!==S.instanceColor,instancingMorph:G&&null!==S.morphTexture,outputColorSpace:null===F?e.outputColorSpace:!0===F.isXRRenderTarget?F.texture.colorSpace:f.workingColorSpace,alphaToCoverage:!!r.alphaToCoverage,map:V,matcap:W,envMap:z,envMapMode:z&&b.mapping,envMapCubeUVHeight:C,aoMap:k,lightMap:X,bumpMap:K,normalMap:Y,displacementMap:q,emissiveMap:j,normalMapObjectSpace:Y&&r.normalMapType===ve,normalMapTangentSpace:Y&&r.normalMapType===ge,packedNormalMap:Y&&r.normalMapType===ge&&(De=r.normalMap.format,De===Se||De===Me||De===Te),metalnessMap:Z,roughnessMap:$,anisotropy:Q,anisotropyMap:re,clearcoat:J,clearcoatMap:ae,clearcoatNormalMap:oe,clearcoatRoughnessMap:se,dispersion:ee,iridescence:te,iridescenceMap:le,iridescenceThicknessMap:ce,sheen:ne,sheenColorMap:de,sheenRoughnessMap:ue,specularMap:fe,specularColorMap:pe,specularIntensityMap:me,transmission:ie,transmissionMap:Ee,thicknessMap:xe,gradientMap:Re,opaque:!1===r.transparent&&r.blending===_e&&!1===r.alphaToCoverage,alphaMap:Ae,alphaTest:be,alphaHash:Ce,combine:r.combine,mapUv:V&&E(r.map.channel),aoMapUv:k&&E(r.aoMap.channel),lightMapUv:X&&E(r.lightMap.channel),bumpMapUv:K&&E(r.bumpMap.channel),normalMapUv:Y&&E(r.normalMap.channel),displacementMapUv:q&&E(r.displacementMap.channel),emissiveMapUv:j&&E(r.emissiveMap.channel),metalnessMapUv:Z&&E(r.metalnessMap.channel),roughnessMapUv:$&&E(r.roughnessMap.channel),anisotropyMapUv:re&&E(r.anisotropyMap.channel),clearcoatMapUv:ae&&E(r.clearcoatMap.channel),clearcoatNormalMapUv:oe&&E(r.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:se&&E(r.clearcoatRoughnessMap.channel),iridescenceMapUv:le&&E(r.iridescenceMap.channel),iridescenceThicknessMapUv:ce&&E(r.iridescenceThicknessMap.channel),sheenColorMapUv:de&&E(r.sheenColorMap.channel),sheenRoughnessMapUv:ue&&E(r.sheenRoughnessMap.channel),specularMapUv:fe&&E(r.specularMap.channel),specularColorMapUv:pe&&E(r.specularColorMap.channel),specularIntensityMapUv:me&&E(r.specularIntensityMap.channel),transmissionMapUv:Ee&&E(r.transmissionMap.channel),thicknessMapUv:xe&&E(r.thicknessMap.channel),alphaMapUv:Ae&&E(r.alphaMap.channel),vertexTangents:!!x.attributes.tangent&&(Y||Q),vertexNormals:!!x.attributes.normal,vertexColors:r.vertexColors,vertexAlphas:!0===r.vertexColors&&!!x.attributes.color&&4===x.attributes.color.itemSize,pointsUvs:!0===S.isPoints&&!!x.attributes.uv&&(V||Ae),fog:!!T,useFog:!0===r.fog,fogExp2:!!T&&T.isFogExp2,flatShading:!1===r.wireframe&&(!0===r.flatShading||void 0===x.attributes.normal&&!1===Y&&(r.isMeshLambertMaterial||r.isMeshPhongMaterial||r.isMeshStandardMaterial||r.isMeshPhysicalMaterial)),sizeAttenuation:!0===r.sizeAttenuation,logarithmicDepthBuffer:h,reversedDepthBuffer:B,skinning:!0===S.isSkinnedMesh,hasPositionAttribute:void 0!==x.attributes.position,morphTargets:void 0!==x.morphAttributes.position,morphNormals:void 0!==x.morphAttributes.normal,morphColors:void 0!==x.morphAttributes.color,morphTargetsCount:D,morphTextureStride:O,numDirLights:s.directional.length,numPointLights:s.point.length,numSpotLights:s.spot.length,numSpotLightMaps:s.spotLightMap.length,numRectAreaLights:s.rectArea.length,numHemiLights:s.hemi.length,numDirLightShadows:s.directionalShadowMap.length,numPointLightShadows:s.pointShadowMap.length,numSpotLightShadows:s.spotShadowMap.length,numSpotLightShadowsWithMaps:s.numSpotLightShadowsWithMaps,numLightProbes:s.numLightProbes,numLightProbeGrids:M.length,numClippingPlanes:o.numPlanes,numClipIntersection:o.numIntersection,dithering:r.dithering,shadowMapEnabled:e.shadowMap.enabled&&u.length>0,shadowMapType:e.shadowMap.type,toneMapping:Le,decodeVideoTexture:V&&!0===r.map.isVideoTexture&&f.getTransfer(r.map.colorSpace)===p,decodeVideoTextureEmissive:j&&!0===r.emissiveMap.isVideoTexture&&f.getTransfer(r.emissiveMap.colorSpace)===p,premultipliedAlpha:r.premultipliedAlpha,doubleSided:r.side===he,flipSided:r.side===c,useDepthPacking:r.depthPacking>=0,depthPacking:r.depthPacking||0,index0AttributeName:r.index0AttributeName,extensionClipCullDistance:Pe&&!0===r.extensions.clipCullDistance&&n.has("WEBGL_clip_cull_distance"),extensionMultiDraw:(Pe&&!0===r.extensions.multiDraw||H)&&n.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:n.has("KHR_parallel_shader_compile"),customProgramCacheKey:r.customProgramCacheKey()};var De;return Ue.vertexUv1s=d.has(1),Ue.vertexUv2s=d.has(2),Ue.vertexUv3s=d.has(3),d.clear(),Ue},getProgramCacheKey:function(t){const n=[];if(t.shaderID?n.push(t.shaderID):(n.push(t.customVertexShaderID),n.push(t.customFragmentShaderID)),void 0!==t.defines)for(const e in t.defines)n.push(e),n.push(t.defines[e]);return!1===t.isRawShaderMaterial&&(!function(e,t){e.push(t.precision),e.push(t.outputColorSpace),e.push(t.envMapMode),e.push(t.envMapCubeUVHeight),e.push(t.mapUv),e.push(t.alphaMapUv),e.push(t.lightMapUv),e.push(t.aoMapUv),e.push(t.bumpMapUv),e.push(t.normalMapUv),e.push(t.displacementMapUv),e.push(t.emissiveMapUv),e.push(t.metalnessMapUv),e.push(t.roughnessMapUv),e.push(t.anisotropyMapUv),e.push(t.clearcoatMapUv),e.push(t.clearcoatNormalMapUv),e.push(t.clearcoatRoughnessMapUv),e.push(t.iridescenceMapUv),e.push(t.iridescenceThicknessMapUv),e.push(t.sheenColorMapUv),e.push(t.sheenRoughnessMapUv),e.push(t.specularMapUv),e.push(t.specularColorMapUv),e.push(t.specularIntensityMapUv),e.push(t.transmissionMapUv),e.push(t.thicknessMapUv),e.push(t.combine),e.push(t.fogExp2),e.push(t.sizeAttenuation),e.push(t.morphTargetsCount),e.push(t.morphAttributeCount),e.push(t.numDirLights),e.push(t.numPointLights),e.push(t.numSpotLights),e.push(t.numSpotLightMaps),e.push(t.numHemiLights),e.push(t.numRectAreaLights),e.push(t.numDirLightShadows),e.push(t.numPointLightShadows),e.push(t.numSpotLightShadows),e.push(t.numSpotLightShadowsWithMaps),e.push(t.numLightProbes),e.push(t.shadowMapType),e.push(t.toneMapping),e.push(t.numClippingPlanes),e.push(t.numClipIntersection),e.push(t.depthPacking)}(n,t),function(e,t){s.disableAll(),t.instancing&&s.enable(0);t.instancingColor&&s.enable(1);t.instancingMorph&&s.enable(2);t.matcap&&s.enable(3);t.envMap&&s.enable(4);t.normalMapObjectSpace&&s.enable(5);t.normalMapTangentSpace&&s.enable(6);t.clearcoat&&s.enable(7);t.iridescence&&s.enable(8);t.alphaTest&&s.enable(9);t.vertexColors&&s.enable(10);t.vertexAlphas&&s.enable(11);t.vertexUv1s&&s.enable(12);t.vertexUv2s&&s.enable(13);t.vertexUv3s&&s.enable(14);t.vertexTangents&&s.enable(15);t.anisotropy&&s.enable(16);t.alphaHash&&s.enable(17);t.batching&&s.enable(18);t.dispersion&&s.enable(19);t.batchingColor&&s.enable(20);t.gradientMap&&s.enable(21);t.packedNormalMap&&s.enable(22);t.vertexNormals&&s.enable(23);e.push(s.mask),s.disableAll(),t.fog&&s.enable(0);t.useFog&&s.enable(1);t.flatShading&&s.enable(2);t.logarithmicDepthBuffer&&s.enable(3);t.reversedDepthBuffer&&s.enable(4);t.skinning&&s.enable(5);t.morphTargets&&s.enable(6);t.morphNormals&&s.enable(7);t.morphColors&&s.enable(8);t.premultipliedAlpha&&s.enable(9);t.shadowMapEnabled&&s.enable(10);t.doubleSided&&s.enable(11);t.flipSided&&s.enable(12);t.useDepthPacking&&s.enable(13);t.dithering&&s.enable(14);t.transmission&&s.enable(15);t.sheen&&s.enable(16);t.opaque&&s.enable(17);t.pointsUvs&&s.enable(18);t.decodeVideoTexture&&s.enable(19);t.decodeVideoTextureEmissive&&s.enable(20);t.alphaToCoverage&&s.enable(21);t.numLightProbeGrids>0&&s.enable(22);t.hasPositionAttribute&&s.enable(23);e.push(s.mask)}(n,t),n.push(e.outputColorSpace)),n.push(t.customProgramCacheKey),n.join()},getUniforms:function(e){const t=g[e.type];let n;if(t){const e=Hn[t];n=me.clone(e.uniforms)}else n=e.uniforms;return n},acquireProgram:function(t,n){let i=m.get(n);return void 0!==i?++i.usedTimes:(i=new jr(e,n,t,r),u.push(i),m.set(n,i)),i},releaseProgram:function(e){if(0===--e.usedTimes){const t=u.indexOf(e);u[t]=u[u.length-1],u.pop(),m.delete(e.cacheKey),e.destroy()}},releaseShaderCache:function(e){l.remove(e)},programs:u,dispose:function(){l.dispose()}}}function ea(){let e=new WeakMap;return{has:function(t){return e.has(t)},get:function(t){let n=e.get(t);return void 0===n&&(n={},e.set(t,n)),n},remove:function(t){e.delete(t)},update:function(t,n,i){e.get(t)[n]=i},dispose:function(){e=new WeakMap}}}function ta(e,t){return e.groupOrder!==t.groupOrder?e.groupOrder-t.groupOrder:e.renderOrder!==t.renderOrder?e.renderOrder-t.renderOrder:e.material.id!==t.material.id?e.material.id-t.material.id:e.materialVariant!==t.materialVariant?e.materialVariant-t.materialVariant:e.z!==t.z?e.z-t.z:e.id-t.id}function na(e,t){return e.groupOrder!==t.groupOrder?e.groupOrder-t.groupOrder:e.renderOrder!==t.renderOrder?e.renderOrder-t.renderOrder:e.z!==t.z?t.z-e.z:e.id-t.id}function ia(){const e=[];let t=0;const n=[],i=[],r=[];function a(e){let t=0;return e.isInstancedMesh&&(t+=2),e.isSkinnedMesh&&(t+=1),t}function o(n,i,r,o,s,l){let c=e[t];return void 0===c?(c={id:n.id,object:n,geometry:i,material:r,materialVariant:a(n),groupOrder:o,renderOrder:n.renderOrder,z:s,group:l},e[t]=c):(c.id=n.id,c.object=n,c.geometry=i,c.material=r,c.materialVariant=a(n),c.groupOrder=o,c.renderOrder=n.renderOrder,c.z=s,c.group=l),t++,c}return{opaque:n,transmissive:i,transparent:r,init:function(){t=0,n.length=0,i.length=0,r.length=0},push:function(e,t,a,s,l,c){const d=o(e,t,a,s,l,c);a.transmission>0?i.push(d):!0===a.transparent?r.push(d):n.push(d)},unshift:function(e,t,a,s,l,c){const d=o(e,t,a,s,l,c);a.transmission>0?i.unshift(d):!0===a.transparent?r.unshift(d):n.unshift(d)},finish:function(){for(let n=t,i=e.length;n1&&n.sort(e||ta),i.length>1&&i.sort(t||na),r.length>1&&r.sort(t||na),a&&(n.reverse(),i.reverse(),r.reverse())}}}function ra(){let e=new WeakMap;return{get:function(t,n){const i=e.get(t);let r;return void 0===i?(r=new ia,e.set(t,[r])):n>=i.length?(r=new ia,i.push(r)):r=i[n],r},dispose:function(){e=new WeakMap}}}function aa(){const e={};return{get:function(t){if(void 0!==e[t.id])return e[t.id];let r;switch(t.type){case"DirectionalLight":r={direction:new i,color:new n};break;case"SpotLight":r={position:new i,direction:new i,color:new n,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":r={position:new i,color:new n,distance:0,decay:0};break;case"HemisphereLight":r={direction:new i,skyColor:new n,groundColor:new n};break;case"RectAreaLight":r={color:new n,position:new i,halfWidth:new i,halfHeight:new i}}return e[t.id]=r,r}}}let oa=0;function sa(e,t){return(t.castShadow?2:0)-(e.castShadow?2:0)+(t.map?1:0)-(e.map?1:0)}function la(e){const n=new aa,r=function(){const e={};return{get:function(n){if(void 0!==e[n.id])return e[n.id];let i;switch(n.type){case"DirectionalLight":case"SpotLight":i={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new t};break;case"PointLight":i={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new t,shadowCameraNear:1,shadowCameraFar:1e3}}return e[n.id]=i,i}}}(),a={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let e=0;e<9;e++)a.probe.push(new i);const o=new i,s=new u,l=new u;return{setup:function(t){let i=0,o=0,s=0;for(let e=0;e<9;e++)a.probe[e].set(0,0,0);let l=0,c=0,d=0,u=0,f=0,p=0,m=0,h=0,_=0,g=0,v=0;t.sort(sa);for(let e=0,E=t.length;e0&&(!0===e.has("OES_texture_float_linear")?(a.rectAreaLTC1=Gn.LTC_FLOAT_1,a.rectAreaLTC2=Gn.LTC_FLOAT_2):(a.rectAreaLTC1=Gn.LTC_HALF_1,a.rectAreaLTC2=Gn.LTC_HALF_2)),a.ambient[0]=i,a.ambient[1]=o,a.ambient[2]=s;const E=a.hash;E.directionalLength===l&&E.pointLength===c&&E.spotLength===d&&E.rectAreaLength===u&&E.hemiLength===f&&E.numDirectionalShadows===p&&E.numPointShadows===m&&E.numSpotShadows===h&&E.numSpotMaps===_&&E.numLightProbes===v||(a.directional.length=l,a.spot.length=d,a.rectArea.length=u,a.point.length=c,a.hemi.length=f,a.directionalShadow.length=p,a.directionalShadowMap.length=p,a.pointShadow.length=m,a.pointShadowMap.length=m,a.spotShadow.length=h,a.spotShadowMap.length=h,a.directionalShadowMatrix.length=p,a.pointShadowMatrix.length=m,a.spotLightMatrix.length=h+_-g,a.spotLightMap.length=_,a.numSpotLightShadowsWithMaps=g,a.numLightProbes=v,E.directionalLength=l,E.pointLength=c,E.spotLength=d,E.rectAreaLength=u,E.hemiLength=f,E.numDirectionalShadows=p,E.numPointShadows=m,E.numSpotShadows=h,E.numSpotMaps=_,E.numLightProbes=v,a.version=oa++)},setupView:function(e,t){let n=0,i=0,r=0,c=0,d=0;const u=t.matrixWorldInverse;for(let t=0,f=e.length;t=r.length?(a=new ca(e),r.push(a)):a=r[i],a},dispose:function(){t=new WeakMap}}}const ua=[new i(1,0,0),new i(-1,0,0),new i(0,1,0),new i(0,-1,0),new i(0,0,1),new i(0,0,-1)],fa=[new i(0,-1,0),new i(0,-1,0),new i(0,0,1),new i(0,0,-1),new i(0,-1,0),new i(0,-1,0)],pa=new u,ma=new i,ha=new i;function _a(e,n,i){let r=new Ue;const a=new t,s=new t,d=new K,u=new xe,f=new Re,p={},m=i.maxTextureSize,_={[h]:c,[c]:h,[he]:he},g=new l({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new t},radius:{value:4}},vertexShader:"void main() {\n\tgl_Position = vec4( position, 1.0 );\n}",fragmentShader:"uniform sampler2D shadow_pass;\nuniform vec2 resolution;\nuniform float radius;\nvoid main() {\n\tconst float samples = float( VSM_SAMPLES );\n\tfloat mean = 0.0;\n\tfloat squared_mean = 0.0;\n\tfloat uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 );\n\tfloat uvStart = samples <= 1.0 ? 0.0 : - 1.0;\n\tfor ( float i = 0.0; i < samples; i ++ ) {\n\t\tfloat uvOffset = uvStart + i * uvStride;\n\t\t#ifdef HORIZONTAL_PASS\n\t\t\tvec2 distribution = texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ).rg;\n\t\t\tmean += distribution.x;\n\t\t\tsquared_mean += distribution.y * distribution.y + distribution.x * distribution.x;\n\t\t#else\n\t\t\tfloat depth = texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ).r;\n\t\t\tmean += depth;\n\t\t\tsquared_mean += depth * depth;\n\t\t#endif\n\t}\n\tmean = mean / samples;\n\tsquared_mean = squared_mean / samples;\n\tfloat std_dev = sqrt( max( 0.0, squared_mean - mean * mean ) );\n\tgl_FragColor = vec4( mean, std_dev, 0.0, 1.0 );\n}"}),S=g.clone();S.defines.HORIZONTAL_PASS=1;const T=new b;T.setAttribute("position",new N(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const x=new o(T,g),R=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=ce;let A=this.type;function C(t,i){const r=n.update(x);g.defines.VSM_SAMPLES!==t.blurSamples&&(g.defines.VSM_SAMPLES=t.blurSamples,S.defines.VSM_SAMPLES=t.blurSamples,g.needsUpdate=!0,S.needsUpdate=!0),null===t.mapPass&&(t.mapPass=new I(a.x,a.y,{format:Se,type:E})),g.uniforms.shadow_pass.value=t.map.depthTexture,g.uniforms.resolution.value=t.mapSize,g.uniforms.radius.value=t.radius,e.setRenderTarget(t.mapPass),e.clear(),e.renderBufferDirect(i,null,r,g,x,null),S.uniforms.shadow_pass.value=t.mapPass.texture,S.uniforms.resolution.value=t.mapSize,S.uniforms.radius.value=t.radius,e.setRenderTarget(t.map),e.clear(),e.renderBufferDirect(i,null,r,S,x,null)}function P(t,n,i,r){let a=null;const o=!0===i.isPointLight?t.customDistanceMaterial:t.customDepthMaterial;if(void 0!==o)a=o;else if(a=!0===i.isPointLight?f:u,e.localClippingEnabled&&!0===n.clipShadows&&Array.isArray(n.clippingPlanes)&&0!==n.clippingPlanes.length||n.displacementMap&&0!==n.displacementScale||n.alphaMap&&n.alphaTest>0||n.map&&n.alphaTest>0||!0===n.alphaToCoverage){const e=a.uuid,t=n.uuid;let i=p[e];void 0===i&&(i={},p[e]=i);let r=i[t];void 0===r&&(r=a.clone(),i[t]=r,n.addEventListener("dispose",U)),a=r}if(a.visible=n.visible,a.wireframe=n.wireframe,a.side=r===le?null!==n.shadowSide?n.shadowSide:n.side:null!==n.shadowSide?n.shadowSide:_[n.side],a.alphaMap=n.alphaMap,a.alphaTest=!0===n.alphaToCoverage?.5:n.alphaTest,a.map=n.map,a.clipShadows=n.clipShadows,a.clippingPlanes=n.clippingPlanes,a.clipIntersection=n.clipIntersection,a.displacementMap=n.displacementMap,a.displacementScale=n.displacementScale,a.displacementBias=n.displacementBias,a.wireframeLinewidth=n.wireframeLinewidth,a.linewidth=n.linewidth,!0===i.isPointLight&&!0===a.isMeshDistanceMaterial){e.properties.get(a).light=i}return a}function L(t,i,a,o,s){if(!1===t.visible)return;if(t.layers.test(i.layers)&&(t.isMesh||t.isLine||t.isPoints)&&(t.castShadow||t.receiveShadow&&s===le)&&(!t.frustumCulled||r.intersectsObject(t))){t.modelViewMatrix.multiplyMatrices(a.matrixWorldInverse,t.matrixWorld);const r=n.update(t),l=t.material;if(Array.isArray(l)){const n=r.groups;for(let c=0,d=n.length;ce.needsUpdate=!0):e.material.needsUpdate=!0)});for(let o=0,l=t.length;om||a.y>m)&&(a.x>m&&(s.x=Math.floor(m/p.x),a.x=s.x*p.x,c.mapSize.x=s.x),a.y>m&&(s.y=Math.floor(m/p.y),a.y=s.y*p.y,c.mapSize.y=s.y));const h=e.state.buffers.depth.getReversed();if(c.camera._reversedDepth=h,null===c.map||!0===f){if(null!==c.map&&(null!==c.map.depthTexture&&(c.map.depthTexture.dispose(),c.map.depthTexture=null),c.map.dispose()),this.type===le){if(l.isPointLight){v("WebGLShadowMap: VSM shadow maps are not supported for PointLights. Use PCF or BasicShadowMap instead.");continue}c.map=new I(a.x,a.y,{format:Se,type:E,minFilter:O,magFilter:O,generateMipmaps:!1}),c.map.texture.name=l.name+".shadowMap",c.map.depthTexture=new Y(a.x,a.y,M),c.map.depthTexture.name=l.name+".shadowMapDepth",c.map.depthTexture.format=be,c.map.depthTexture.compareFunction=null,c.map.depthTexture.minFilter=Ce,c.map.depthTexture.magFilter=Ce}else l.isPointLight?(c.map=new di(a.x),c.map.depthTexture=new Pe(a.x,Le)):(c.map=new I(a.x,a.y),c.map.depthTexture=new Y(a.x,a.y,Le)),c.map.depthTexture.name=l.name+".shadowMap",c.map.depthTexture.format=be,this.type===ce?(c.map.depthTexture.compareFunction=h?re:ae,c.map.depthTexture.minFilter=O,c.map.depthTexture.magFilter=O):(c.map.depthTexture.compareFunction=null,c.map.depthTexture.minFilter=Ce,c.map.depthTexture.magFilter=Ce);c.camera.updateProjectionMatrix()}const _=c.map.isWebGLCubeRenderTarget?6:1;for(let t=0;t<_;t++){if(c.map.isWebGLCubeRenderTarget)e.setRenderTarget(c.map,t),e.clear();else{0===t&&(e.setRenderTarget(c.map),e.clear());const n=c.getViewport(t);d.set(s.x*n.x,s.y*n.y,s.x*n.z,s.y*n.w),u.viewport(d)}if(l.isPointLight){const e=c.camera,n=c.matrix,i=l.distance||e.far;i!==e.far&&(e.far=i,e.updateProjectionMatrix()),ma.setFromMatrixPosition(l.matrixWorld),e.position.copy(ma),ha.copy(e.position),ha.add(ua[t]),e.up.copy(fa[t]),e.lookAt(ha),e.updateMatrixWorld(),n.makeTranslation(-ma.x,-ma.y,-ma.z),pa.multiplyMatrices(e.projectionMatrix,e.matrixWorldInverse),c._frustum.setFromProjectionMatrix(pa,e.coordinateSystem,e.reversedDepth)}else c.updateMatrices(l);r=c.getFrustum(),L(n,i,c.camera,l,this.type)}!0!==c.isPointLightShadow&&this.type===le&&C(c,i),c.needsUpdate=!1}A=this.type,R.needsUpdate=!1,e.setRenderTarget(o,l,c)}}function ga(e,t){const i=new function(){let t=!1;const n=new K;let i=null;const r=new K(0,0,0,0);return{setMask:function(n){i===n||t||(e.colorMask(n,n,n,n),i=n)},setLocked:function(e){t=e},setClear:function(t,i,a,o,s){!0===s&&(t*=o,i*=o,a*=o),n.set(t,i,a,o),!1===r.equals(n)&&(e.clearColor(t,i,a,o),r.copy(n))},reset:function(){t=!1,i=null,r.set(-1,0,0,0)}}},r=new function(){let n=!1,i=!1,r=null,a=null,o=null;return{setReversed:function(e){if(i!==e){const n=t.get("EXT_clip_control");e?n.clipControlEXT(n.LOWER_LEFT_EXT,n.ZERO_TO_ONE_EXT):n.clipControlEXT(n.LOWER_LEFT_EXT,n.NEGATIVE_ONE_TO_ONE_EXT),i=e;const r=o;o=null,this.setClear(r)}},getReversed:function(){return i},setTest:function(t){t?X(e.DEPTH_TEST):Y(e.DEPTH_TEST)},setMask:function(t){r===t||n||(e.depthMask(t),r=t)},setFunc:function(t){if(i&&(t=dt[t]),a!==t){switch(t){case nt:e.depthFunc(e.NEVER);break;case tt:e.depthFunc(e.ALWAYS);break;case et:e.depthFunc(e.LESS);break;case De:e.depthFunc(e.LEQUAL);break;case Je:e.depthFunc(e.EQUAL);break;case Qe:e.depthFunc(e.GEQUAL);break;case $e:e.depthFunc(e.GREATER);break;case Ze:e.depthFunc(e.NOTEQUAL);break;default:e.depthFunc(e.LEQUAL)}a=t}},setLocked:function(e){n=e},setClear:function(t){o!==t&&(o=t,i&&(t=1-t),e.clearDepth(t))},reset:function(){n=!1,r=null,a=null,o=null,i=!1}}},a=new function(){let t=!1,n=null,i=null,r=null,a=null,o=null,s=null,l=null,c=null;return{setTest:function(n){t||(n?X(e.STENCIL_TEST):Y(e.STENCIL_TEST))},setMask:function(i){n===i||t||(e.stencilMask(i),n=i)},setFunc:function(t,n,o){i===t&&r===n&&a===o||(e.stencilFunc(t,n,o),i=t,r=n,a=o)},setOp:function(t,n,i){o===t&&s===n&&l===i||(e.stencilOp(t,n,i),o=t,s=n,l=i)},setLocked:function(e){t=e},setClear:function(t){c!==t&&(e.clearStencil(t),c=t)},reset:function(){t=!1,n=null,i=null,r=null,a=null,o=null,s=null,l=null,c=null}}},o=new WeakMap,s=new WeakMap;let l={},d={},u={},f=new WeakMap,p=[],m=null,h=!1,_=null,g=null,v=null,E=null,S=null,M=null,T=null,x=new n(0,0,0),R=0,A=!1,b=null,C=null,P=null,L=null,U=null;const I=e.getParameter(e.MAX_COMBINED_TEXTURE_IMAGE_UNITS);let N=!1,y=0;const O=e.getParameter(e.VERSION);-1!==O.indexOf("WebGL")?(y=parseFloat(/^WebGL (\d)/.exec(O)[1]),N=y>=1):-1!==O.indexOf("OpenGL ES")&&(y=parseFloat(/^OpenGL ES (\d)/.exec(O)[1]),N=y>=2);let F=null,B={};const G=e.getParameter(e.SCISSOR_BOX),H=e.getParameter(e.VIEWPORT),V=(new K).fromArray(G),W=(new K).fromArray(H);function z(t,n,i,r){const a=new Uint8Array(4),o=e.createTexture();e.bindTexture(t,o),e.texParameteri(t,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(t,e.TEXTURE_MAG_FILTER,e.NEAREST);for(let o=0;on||r.height>n)&&(i=n/Math.max(r.width,r.height)),i<1){if("undefined"!=typeof HTMLImageElement&&e instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&e instanceof ImageBitmap||"undefined"!=typeof VideoFrame&&e instanceof VideoFrame){const n=Math.floor(i*r.width),a=Math.floor(i*r.height);void 0===h&&(h=E(n,a));const o=t?E(n,a):h;o.width=n,o.height=a;return o.getContext("2d").drawImage(e,0,0,n,a),v("WebGLRenderer: Texture has been resized from ("+r.width+"x"+r.height+") to ("+n+"x"+a+")."),o}return"data"in e&&v("WebGLRenderer: Image in DataTexture is too big ("+r.width+"x"+r.height+")."),e}return e}function R(e){return e.generateMipmaps}function A(t){e.generateMipmap(t)}function b(t){return t.isWebGLCubeRenderTarget?e.TEXTURE_CUBE_MAP:t.isWebGL3DRenderTarget?e.TEXTURE_3D:t.isWebGLArrayRenderTarget||t.isCompressedArrayTexture?e.TEXTURE_2D_ARRAY:e.TEXTURE_2D}function C(t,i,r,a,o,s=!1){if(null!==t){if(void 0!==e[t])return e[t];v("WebGLRenderer: Attempt to use non-existing WebGL internal format '"+t+"'")}let l;a&&(l=n.get("EXT_texture_norm16"),l||v("WebGLRenderer: Unable to use normalized textures without EXT_texture_norm16 extension"));let c=i;if(i===e.RED&&(r===e.FLOAT&&(c=e.R32F),r===e.HALF_FLOAT&&(c=e.R16F),r===e.UNSIGNED_BYTE&&(c=e.R8),r===e.UNSIGNED_SHORT&&l&&(c=l.R16_EXT),r===e.SHORT&&l&&(c=l.R16_SNORM_EXT)),i===e.RED_INTEGER&&(r===e.UNSIGNED_BYTE&&(c=e.R8UI),r===e.UNSIGNED_SHORT&&(c=e.R16UI),r===e.UNSIGNED_INT&&(c=e.R32UI),r===e.BYTE&&(c=e.R8I),r===e.SHORT&&(c=e.R16I),r===e.INT&&(c=e.R32I)),i===e.RG&&(r===e.FLOAT&&(c=e.RG32F),r===e.HALF_FLOAT&&(c=e.RG16F),r===e.UNSIGNED_BYTE&&(c=e.RG8),r===e.UNSIGNED_SHORT&&l&&(c=l.RG16_EXT),r===e.SHORT&&l&&(c=l.RG16_SNORM_EXT)),i===e.RG_INTEGER&&(r===e.UNSIGNED_BYTE&&(c=e.RG8UI),r===e.UNSIGNED_SHORT&&(c=e.RG16UI),r===e.UNSIGNED_INT&&(c=e.RG32UI),r===e.BYTE&&(c=e.RG8I),r===e.SHORT&&(c=e.RG16I),r===e.INT&&(c=e.RG32I)),i===e.RGB_INTEGER&&(r===e.UNSIGNED_BYTE&&(c=e.RGB8UI),r===e.UNSIGNED_SHORT&&(c=e.RGB16UI),r===e.UNSIGNED_INT&&(c=e.RGB32UI),r===e.BYTE&&(c=e.RGB8I),r===e.SHORT&&(c=e.RGB16I),r===e.INT&&(c=e.RGB32I)),i===e.RGBA_INTEGER&&(r===e.UNSIGNED_BYTE&&(c=e.RGBA8UI),r===e.UNSIGNED_SHORT&&(c=e.RGBA16UI),r===e.UNSIGNED_INT&&(c=e.RGBA32UI),r===e.BYTE&&(c=e.RGBA8I),r===e.SHORT&&(c=e.RGBA16I),r===e.INT&&(c=e.RGBA32I)),i===e.RGB&&(r===e.UNSIGNED_SHORT&&l&&(c=l.RGB16_EXT),r===e.SHORT&&l&&(c=l.RGB16_SNORM_EXT),r===e.UNSIGNED_INT_5_9_9_9_REV&&(c=e.RGB9_E5),r===e.UNSIGNED_INT_10F_11F_11F_REV&&(c=e.R11F_G11F_B10F)),i===e.RGBA){const t=s?pe:f.getTransfer(o);r===e.FLOAT&&(c=e.RGBA32F),r===e.HALF_FLOAT&&(c=e.RGBA16F),r===e.UNSIGNED_BYTE&&(c=t===p?e.SRGB8_ALPHA8:e.RGBA8),r===e.UNSIGNED_SHORT&&l&&(c=l.RGBA16_EXT),r===e.SHORT&&l&&(c=l.RGBA16_SNORM_EXT),r===e.UNSIGNED_SHORT_4_4_4_4&&(c=e.RGBA4),r===e.UNSIGNED_SHORT_5_5_5_1&&(c=e.RGB5_A1)}return c!==e.R16F&&c!==e.R32F&&c!==e.RG16F&&c!==e.RG32F&&c!==e.RGBA16F&&c!==e.RGBA32F||n.get("EXT_color_buffer_float"),c}function P(t,n){let i;return t?null===n||n===Le||n===Pt?i=e.DEPTH24_STENCIL8:n===M?i=e.DEPTH32F_STENCIL8:n===Lt&&(i=e.DEPTH24_STENCIL8,v("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):null===n||n===Le||n===Pt?i=e.DEPTH_COMPONENT24:n===M?i=e.DEPTH_COMPONENT32F:n===Lt&&(i=e.DEPTH_COMPONENT16),i}function L(e,t){return!0===R(e)||e.isFramebufferTexture&&e.minFilter!==Ce&&e.minFilter!==O?Math.log2(Math.max(t.width,t.height))+1:void 0!==e.mipmaps&&e.mipmaps.length>0?e.mipmaps.length:e.isCompressedTexture&&Array.isArray(e.image)?t.mipmaps.length:1}function U(e){const t=e.target;t.removeEventListener("dispose",U),function(e){const t=r.get(e);if(void 0===t.__webglInit)return;const n=e.source,i=_.get(n);if(i){const r=i[t.__cacheKey];r.usedTimes--,0===r.usedTimes&&I(e),0===Object.keys(i).length&&_.delete(n)}r.remove(e)}(t),t.isVideoTexture&&u.delete(t),t.isHTMLTexture&&m.delete(t)}function w(t){const n=t.target;n.removeEventListener("dispose",w),function(t){const n=r.get(t);t.depthTexture&&(t.depthTexture.dispose(),r.remove(t.depthTexture));if(t.isWebGLCubeRenderTarget)for(let t=0;t<6;t++){if(Array.isArray(n.__webglFramebuffer[t]))for(let i=0;i0&&a.__version!==t.version){const e=t.image;if(null===e)v("WebGLRenderer: Texture marked for update but no image data found.");else{if(!1!==e.complete)return void X(a,t,n);v("WebGLRenderer: Texture marked for update but image is incomplete")}}else t.isExternalTexture&&(a.__webglTexture=t.sourceTexture?t.sourceTexture:null);i.bindTexture(e.TEXTURE_2D,a.__webglTexture,e.TEXTURE0+n)}const G={[ht]:e.REPEAT,[mt]:e.CLAMP_TO_EDGE,[pt]:e.MIRRORED_REPEAT},H={[Ce]:e.NEAREST,[vt]:e.NEAREST_MIPMAP_NEAREST,[gt]:e.NEAREST_MIPMAP_LINEAR,[O]:e.LINEAR,[_t]:e.LINEAR_MIPMAP_NEAREST,[B]:e.LINEAR_MIPMAP_LINEAR},V={[Rt]:e.NEVER,[xt]:e.ALWAYS,[Tt]:e.LESS,[ae]:e.LEQUAL,[Mt]:e.EQUAL,[re]:e.GEQUAL,[St]:e.GREATER,[Et]:e.NOTEQUAL};function W(t,i){if(i.type!==M||!1!==n.has("OES_texture_float_linear")||i.magFilter!==O&&i.magFilter!==_t&&i.magFilter!==gt&&i.magFilter!==B&&i.minFilter!==O&&i.minFilter!==_t&&i.minFilter!==gt&&i.minFilter!==B||v("WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device."),e.texParameteri(t,e.TEXTURE_WRAP_S,G[i.wrapS]),e.texParameteri(t,e.TEXTURE_WRAP_T,G[i.wrapT]),t!==e.TEXTURE_3D&&t!==e.TEXTURE_2D_ARRAY||e.texParameteri(t,e.TEXTURE_WRAP_R,G[i.wrapR]),e.texParameteri(t,e.TEXTURE_MAG_FILTER,H[i.magFilter]),e.texParameteri(t,e.TEXTURE_MIN_FILTER,H[i.minFilter]),i.compareFunction&&(e.texParameteri(t,e.TEXTURE_COMPARE_MODE,e.COMPARE_REF_TO_TEXTURE),e.texParameteri(t,e.TEXTURE_COMPARE_FUNC,V[i.compareFunction])),!0===n.has("EXT_texture_filter_anisotropic")){if(i.magFilter===Ce)return;if(i.minFilter!==gt&&i.minFilter!==B)return;if(i.type===M&&!1===n.has("OES_texture_float_linear"))return;if(i.anisotropy>1||r.get(i).__currentAnisotropy){const o=n.get("EXT_texture_filter_anisotropic");e.texParameterf(t,o.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(i.anisotropy,a.getMaxAnisotropy())),r.get(i).__currentAnisotropy=i.anisotropy}}}function z(t,n){let i=!1;void 0===t.__webglInit&&(t.__webglInit=!0,n.addEventListener("dispose",U));const r=n.source;let a=_.get(r);void 0===a&&(a={},_.set(r,a));const o=function(e){const t=[];return t.push(e.wrapS),t.push(e.wrapT),t.push(e.wrapR||0),t.push(e.magFilter),t.push(e.minFilter),t.push(e.anisotropy),t.push(e.internalFormat),t.push(e.format),t.push(e.type),t.push(e.generateMipmaps),t.push(e.premultiplyAlpha),t.push(e.flipY),t.push(e.unpackAlignment),t.push(e.colorSpace),t.join()}(n);if(o!==t.__cacheKey){void 0===a[o]&&(a[o]={texture:e.createTexture(),usedTimes:0},s.memory.textures++,i=!0),a[o].usedTimes++;const r=a[t.__cacheKey];void 0!==r&&(a[t.__cacheKey].usedTimes--,0===r.usedTimes&&I(n)),t.__cacheKey=o,t.__webglTexture=a[o].texture}return i}function k(e,t,n){return Math.floor(Math.floor(e/n)/t)}function X(t,n,s){let l=e.TEXTURE_2D;(n.isDataArrayTexture||n.isCompressedArrayTexture)&&(l=e.TEXTURE_2D_ARRAY),n.isData3DTexture&&(l=e.TEXTURE_3D);const c=z(t,n),d=n.source;i.bindTexture(l,t.__webglTexture,e.TEXTURE0+s);const u=r.get(d);if(d.version!==u.__version||!0===c){i.activeTexture(e.TEXTURE0+s);if(!1===("undefined"!=typeof ImageBitmap&&n.image instanceof ImageBitmap)){const t=f.getPrimaries(f.workingColorSpace),r=n.colorSpace===At?null:f.getPrimaries(n.colorSpace),a=n.colorSpace===At||t===r?e.NONE:e.BROWSER_DEFAULT_WEBGL;i.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,n.flipY),i.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,n.premultiplyAlpha),i.pixelStorei(e.UNPACK_COLORSPACE_CONVERSION_WEBGL,a)}i.pixelStorei(e.UNPACK_ALIGNMENT,n.unpackAlignment);let t=x(n.image,!1,a.maxTextureSize);t=ee(n,t);const r=o.convert(n.format,n.colorSpace),p=o.convert(n.type);let h,_=C(n.internalFormat,r,p,n.normalized,n.colorSpace,n.isVideoTexture);W(l,n);const g=n.mipmaps,E=!0!==n.isVideoTexture,S=void 0===u.__version||!0===c,M=d.dataReady,b=L(n,t);if(n.isDepthTexture)_=P(n.format===bt,n.type),S&&(E?i.texStorage2D(e.TEXTURE_2D,1,_,t.width,t.height):i.texImage2D(e.TEXTURE_2D,0,_,t.width,t.height,0,r,p,null));else if(n.isDataTexture)if(g.length>0){E&&S&&i.texStorage2D(e.TEXTURE_2D,b,_,g[0].width,g[0].height);for(let t=0,n=g.length;te.start-t.start);let s=0;for(let e=1;e0){const t=Ct(h.width,h.height,n.format,n.type);for(const o of n.layerUpdates){const n=h.data.subarray(o*t/h.data.BYTES_PER_ELEMENT,(o+1)*t/h.data.BYTES_PER_ELEMENT);i.compressedTexSubImage3D(e.TEXTURE_2D_ARRAY,a,0,0,o,h.width,h.height,1,r,n)}n.clearLayerUpdates()}else i.compressedTexSubImage3D(e.TEXTURE_2D_ARRAY,a,0,0,0,h.width,h.height,t.depth,r,h.data)}else i.compressedTexImage3D(e.TEXTURE_2D_ARRAY,a,_,h.width,h.height,t.depth,0,h.data,0,0);else v("WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()");else E?M&&i.texSubImage3D(e.TEXTURE_2D_ARRAY,a,0,0,0,h.width,h.height,t.depth,r,p,h.data):i.texImage3D(e.TEXTURE_2D_ARRAY,a,_,h.width,h.height,t.depth,0,r,p,h.data)}else{E&&S&&i.texStorage2D(e.TEXTURE_2D,b,_,g[0].width,g[0].height);for(let t=0,a=g.length;t0){const a=Ct(t.width,t.height,n.format,n.type);for(const o of n.layerUpdates){const n=t.data.subarray(o*a/t.data.BYTES_PER_ELEMENT,(o+1)*a/t.data.BYTES_PER_ELEMENT);i.texSubImage3D(e.TEXTURE_2D_ARRAY,0,0,0,o,t.width,t.height,1,r,p,n)}n.clearLayerUpdates()}else i.texSubImage3D(e.TEXTURE_2D_ARRAY,0,0,0,0,t.width,t.height,t.depth,r,p,t.data)}else i.texImage3D(e.TEXTURE_2D_ARRAY,0,_,t.width,t.height,t.depth,0,r,p,t.data);else if(n.isData3DTexture)E?(S&&i.texStorage3D(e.TEXTURE_3D,b,_,t.width,t.height,t.depth),M&&i.texSubImage3D(e.TEXTURE_3D,0,0,0,0,t.width,t.height,t.depth,r,p,t.data)):i.texImage3D(e.TEXTURE_3D,0,_,t.width,t.height,t.depth,0,r,p,t.data);else if(n.isFramebufferTexture){if(S)if(E)i.texStorage2D(e.TEXTURE_2D,b,_,t.width,t.height);else{let n=t.width,a=t.height;for(let t=0;t>=1,a>>=1}}else if(n.isHTMLTexture){if("texElementImage2D"in e){const i=e.canvas;if(i.hasAttribute("layoutsubtree")||i.setAttribute("layoutsubtree","true"),t.parentNode!==i)return i.appendChild(t),m.add(n),i.onpaint=e=>{const t=e.changedElements;for(const e of m)t.includes(e.image)&&(e.needsUpdate=!0)},void i.requestPaint();if(3===e.texElementImage2D.length)e.texElementImage2D(e.TEXTURE_2D,e.RGBA8,t);else{const n=0,i=e.RGBA,r=e.RGBA,a=e.UNSIGNED_BYTE;e.texElementImage2D(e.TEXTURE_2D,n,i,r,a,t)}e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE)}}else if(g.length>0){if(E&&S){const t=te(g[0]);i.texStorage2D(e.TEXTURE_2D,b,_,t.width,t.height)}for(let t=0,n=g.length;t>d),r=Math.max(1,n.height>>d);c===e.TEXTURE_3D||c===e.TEXTURE_2D_ARRAY?i.texImage3D(c,d,p,t,r,n.depth,0,u,f,null):i.texImage2D(c,d,p,t,r,0,u,f,null)}i.bindFramebuffer(e.FRAMEBUFFER,t),J(n)?l.framebufferTexture2DMultisampleEXT(e.FRAMEBUFFER,s,c,h.__webglTexture,0,Q(n)):(c===e.TEXTURE_2D||c>=e.TEXTURE_CUBE_MAP_POSITIVE_X&&c<=e.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&e.framebufferTexture2D(e.FRAMEBUFFER,s,c,h.__webglTexture,d),i.bindFramebuffer(e.FRAMEBUFFER,null)}function Y(t,n,i){if(e.bindRenderbuffer(e.RENDERBUFFER,t),n.depthBuffer){const r=n.depthTexture,a=r&&r.isDepthTexture?r.type:null,o=P(n.stencilBuffer,a),s=n.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT;J(n)?l.renderbufferStorageMultisampleEXT(e.RENDERBUFFER,Q(n),o,n.width,n.height):i?e.renderbufferStorageMultisample(e.RENDERBUFFER,Q(n),o,n.width,n.height):e.renderbufferStorage(e.RENDERBUFFER,o,n.width,n.height),e.framebufferRenderbuffer(e.FRAMEBUFFER,s,e.RENDERBUFFER,t)}else{const t=n.textures;for(let r=0;r{delete n.__boundDepthTexture,delete n.__depthDisposeCallback,e.removeEventListener("dispose",t)};e.addEventListener("dispose",t),n.__depthDisposeCallback=t}n.__boundDepthTexture=e}if(t.depthTexture&&!n.__autoAllocateDepthBuffer)if(a)for(let e=0;e<6;e++)q(n.__webglFramebuffer[e],t,e);else{const e=t.texture.mipmaps;e&&e.length>0?q(n.__webglFramebuffer[0],t,0):q(n.__webglFramebuffer,t,0)}else if(a){n.__webglDepthbuffer=[];for(let r=0;r<6;r++)if(i.bindFramebuffer(e.FRAMEBUFFER,n.__webglFramebuffer[r]),void 0===n.__webglDepthbuffer[r])n.__webglDepthbuffer[r]=e.createRenderbuffer(),Y(n.__webglDepthbuffer[r],t,!1);else{const i=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,a=n.__webglDepthbuffer[r];e.bindRenderbuffer(e.RENDERBUFFER,a),e.framebufferRenderbuffer(e.FRAMEBUFFER,i,e.RENDERBUFFER,a)}}else{const r=t.texture.mipmaps;if(r&&r.length>0?i.bindFramebuffer(e.FRAMEBUFFER,n.__webglFramebuffer[0]):i.bindFramebuffer(e.FRAMEBUFFER,n.__webglFramebuffer),void 0===n.__webglDepthbuffer)n.__webglDepthbuffer=e.createRenderbuffer(),Y(n.__webglDepthbuffer,t,!1);else{const i=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,r=n.__webglDepthbuffer;e.bindRenderbuffer(e.RENDERBUFFER,r),e.framebufferRenderbuffer(e.FRAMEBUFFER,i,e.RENDERBUFFER,r)}}i.bindFramebuffer(e.FRAMEBUFFER,null)}const Z=[],$=[];function Q(e){return Math.min(a.maxSamples,e.samples)}function J(e){const t=r.get(e);return e.samples>0&&!0===n.has("WEBGL_multisampled_render_to_texture")&&!1!==t.__useRenderToTexture}function ee(e,t){const n=e.colorSpace,i=e.format,r=e.type;return!0===e.isCompressedTexture||!0===e.isVideoTexture||n!==y&&n!==At&&(f.getTransfer(n)===p?i===T&&r===S||v("WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):D("WebGLTextures: Unsupported texture color space:",n)),t}function te(e){return"undefined"!=typeof HTMLImageElement&&e instanceof HTMLImageElement?(d.width=e.naturalWidth||e.width,d.height=e.naturalHeight||e.height):"undefined"!=typeof VideoFrame&&e instanceof VideoFrame?(d.width=e.displayWidth,d.height=e.displayHeight):(d.width=e.width,d.height=e.height),d}this.allocateTextureUnit=function(){const e=N;return e>=a.maxTextures&&v("WebGLTextures: Trying to use "+e+" texture units while this GPU supports only "+a.maxTextures),N+=1,e},this.resetTextureUnits=function(){N=0},this.getTextureUnits=function(){return N},this.setTextureUnits=function(e){N=e},this.setTexture2D=F,this.setTexture2DArray=function(t,n){const a=r.get(t);!1===t.isRenderTargetTexture&&t.version>0&&a.__version!==t.version?X(a,t,n):(t.isExternalTexture&&(a.__webglTexture=t.sourceTexture?t.sourceTexture:null),i.bindTexture(e.TEXTURE_2D_ARRAY,a.__webglTexture,e.TEXTURE0+n))},this.setTexture3D=function(t,n){const a=r.get(t);!1===t.isRenderTargetTexture&&t.version>0&&a.__version!==t.version?X(a,t,n):i.bindTexture(e.TEXTURE_3D,a.__webglTexture,e.TEXTURE0+n)},this.setTextureCube=function(t,n){const s=r.get(t);!0!==t.isCubeDepthTexture&&t.version>0&&s.__version!==t.version?function(t,n,s){if(6!==n.image.length)return;const l=z(t,n),c=n.source;i.bindTexture(e.TEXTURE_CUBE_MAP,t.__webglTexture,e.TEXTURE0+s);const d=r.get(c);if(c.version!==d.__version||!0===l){i.activeTexture(e.TEXTURE0+s);const t=f.getPrimaries(f.workingColorSpace),r=n.colorSpace===At?null:f.getPrimaries(n.colorSpace),u=n.colorSpace===At||t===r?e.NONE:e.BROWSER_DEFAULT_WEBGL;i.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,n.flipY),i.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,n.premultiplyAlpha),i.pixelStorei(e.UNPACK_ALIGNMENT,n.unpackAlignment),i.pixelStorei(e.UNPACK_COLORSPACE_CONVERSION_WEBGL,u);const p=n.isCompressedTexture||n.image[0].isCompressedTexture,m=n.image[0]&&n.image[0].isDataTexture,h=[];for(let e=0;e<6;e++)h[e]=p||m?m?n.image[e].image:n.image[e]:x(n.image[e],!0,a.maxCubemapSize),h[e]=ee(n,h[e]);const _=h[0],g=o.convert(n.format,n.colorSpace),E=o.convert(n.type),S=C(n.internalFormat,g,E,n.normalized,n.colorSpace),M=!0!==n.isVideoTexture,b=void 0===d.__version||!0===l,P=c.dataReady;let U,D=L(n,_);if(W(e.TEXTURE_CUBE_MAP,n),p){M&&b&&i.texStorage2D(e.TEXTURE_CUBE_MAP,D,S,_.width,_.height);for(let t=0;t<6;t++){U=h[t].mipmaps;for(let r=0;r0&&D++;const t=te(h[0]);i.texStorage2D(e.TEXTURE_CUBE_MAP,D,S,t.width,t.height)}for(let t=0;t<6;t++)if(m){M?P&&i.texSubImage2D(e.TEXTURE_CUBE_MAP_POSITIVE_X+t,0,0,0,h[t].width,h[t].height,g,E,h[t].data):i.texImage2D(e.TEXTURE_CUBE_MAP_POSITIVE_X+t,0,S,h[t].width,h[t].height,0,g,E,h[t].data);for(let n=0;n1;if(u||(void 0===l.__webglTexture&&(l.__webglTexture=e.createTexture()),l.__version=n.version,s.memory.textures++),d){a.__webglFramebuffer=[];for(let t=0;t<6;t++)if(n.mipmaps&&n.mipmaps.length>0){a.__webglFramebuffer[t]=[];for(let i=0;i0){a.__webglFramebuffer=[];for(let t=0;t0&&!1===J(t)){a.__webglMultisampledFramebuffer=e.createFramebuffer(),a.__webglColorRenderbuffer=[],i.bindFramebuffer(e.FRAMEBUFFER,a.__webglMultisampledFramebuffer);for(let n=0;n0)for(let r=0;r0)for(let i=0;i0)if(!1===J(t)){const n=t.textures,a=t.width,o=t.height;let s=e.COLOR_BUFFER_BIT;const l=t.stencilBuffer?e.DEPTH_STENCIL_ATTACHMENT:e.DEPTH_ATTACHMENT,d=r.get(t),u=n.length>1;if(u)for(let t=0;t0?i.bindFramebuffer(e.DRAW_FRAMEBUFFER,d.__webglFramebuffer[0]):i.bindFramebuffer(e.DRAW_FRAMEBUFFER,d.__webglFramebuffer);for(let i=0;i= 1.0 ) {\n\n\t\tgl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r;\n\n\t} else {\n\n\t\tgl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r;\n\n\t}\n\n}",uniforms:{depthColor:{value:this.texture},depthWidth:{value:t.z},depthHeight:{value:t.w}}});this.mesh=new o(new m(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}}class Ma extends An{constructor(e,n){super();const r=this;let a=null,o=1,s=null,l="local-floor",c=1,d=null,u=null,f=null,p=null,m=null,h=null;const _="undefined"!=typeof XRWebGLBinding,g=new Sa,E={},M=n.getContextAttributes();let x=null,R=null;const A=[],b=[],C=new t;let L=null;const U=new P;U.viewport=new K;const D=new P;D.viewport=new K;const w=[U,D],N=new bn;let y=null,O=null;function F(e){const t=b.indexOf(e.inputSource);if(-1===t)return;const n=A[t];void 0!==n&&(n.update(e.inputSource,e.frame,d||s),n.dispatchEvent({type:e.type,data:e.inputSource}))}function B(){a.removeEventListener("select",F),a.removeEventListener("selectstart",F),a.removeEventListener("selectend",F),a.removeEventListener("squeeze",F),a.removeEventListener("squeezestart",F),a.removeEventListener("squeezeend",F),a.removeEventListener("end",B),a.removeEventListener("inputsourceschange",G);for(let e=0;e=0&&(b[i]=null,A[i].disconnect(n))}for(let t=0;t=b.length){b.push(n),i=e;break}if(null===b[e]){b[e]=n,i=e;break}}if(-1===i)break}const r=A[i];r&&r.connect(n)}}this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(e){let t=A[e];return void 0===t&&(t=new Cn,A[e]=t),t.getTargetRaySpace()},this.getControllerGrip=function(e){let t=A[e];return void 0===t&&(t=new Cn,A[e]=t),t.getGripSpace()},this.getHand=function(e){let t=A[e];return void 0===t&&(t=new Cn,A[e]=t),t.getHandSpace()},this.setFramebufferScaleFactor=function(e){o=e,!0===r.isPresenting&&v("WebXRManager: Cannot change framebuffer scale while presenting.")},this.setReferenceSpaceType=function(e){l=e,!0===r.isPresenting&&v("WebXRManager: Cannot change reference space type while presenting.")},this.getReferenceSpace=function(){return d||s},this.setReferenceSpace=function(e){d=e},this.getBaseLayer=function(){return null!==p?p:m},this.getBinding=function(){return null===f&&_&&(f=new XRWebGLBinding(a,n)),f},this.getFrame=function(){return h},this.getSession=function(){return a},this.setSession=async function(t){if(a=t,null!==a){x=e.getRenderTarget(),a.addEventListener("select",F),a.addEventListener("selectstart",F),a.addEventListener("selectend",F),a.addEventListener("squeeze",F),a.addEventListener("squeezestart",F),a.addEventListener("squeezeend",F),a.addEventListener("end",B),a.addEventListener("inputsourceschange",G),!0!==M.xrCompatible&&await n.makeXRCompatible(),L=e.getPixelRatio(),e.getSize(C);if(_&&"createProjectionLayer"in XRWebGLBinding.prototype){let t=null,i=null,r=null;M.depth&&(r=M.stencil?n.DEPTH24_STENCIL8:n.DEPTH_COMPONENT24,t=M.stencil?bt:be,i=M.stencil?Pt:Le);const s={colorFormat:n.RGBA8,depthFormat:r,scaleFactor:o};f=this.getBinding(),p=f.createProjectionLayer(s),a.updateRenderState({layers:[p]}),e.setPixelRatio(1),e.setSize(p.textureWidth,p.textureHeight,!1),R=new I(p.textureWidth,p.textureHeight,{format:T,type:S,depthTexture:new Y(p.textureWidth,p.textureHeight,i,void 0,void 0,void 0,void 0,void 0,void 0,t),stencilBuffer:M.stencil,colorSpace:e.outputColorSpace,samples:M.antialias?4:0,resolveDepthBuffer:!1===p.ignoreDepthValues,resolveStencilBuffer:!1===p.ignoreDepthValues})}else{const t={antialias:M.antialias,alpha:!0,depth:M.depth,stencil:M.stencil,framebufferScaleFactor:o};m=new XRWebGLLayer(a,n,t),a.updateRenderState({baseLayer:m}),e.setPixelRatio(1),e.setSize(m.framebufferWidth,m.framebufferHeight,!1),R=new I(m.framebufferWidth,m.framebufferHeight,{format:T,type:S,colorSpace:e.outputColorSpace,stencilBuffer:M.stencil,resolveDepthBuffer:!1===m.ignoreDepthValues,resolveStencilBuffer:!1===m.ignoreDepthValues})}R.isXRRenderTarget=!0,this.setFoveation(c),d=null,s=await a.requestReferenceSpace(l),k.setContext(a),k.start(),r.isPresenting=!0,r.dispatchEvent({type:"sessionstart"})}},this.getEnvironmentBlendMode=function(){if(null!==a)return a.environmentBlendMode},this.getDepthTexture=function(){return g.getDepthTexture()};const H=new i,V=new i;function W(e,t){null===t?e.matrixWorld.copy(e.matrix):e.matrixWorld.multiplyMatrices(t.matrixWorld,e.matrix),e.matrixWorldInverse.copy(e.matrixWorld).invert()}this.updateCamera=function(e){if(null===a)return;let t=e.near,n=e.far;null!==g.texture&&(g.depthNear>0&&(t=g.depthNear),g.depthFar>0&&(n=g.depthFar)),N.near=D.near=U.near=t,N.far=D.far=U.far=n,y===N.near&&O===N.far||(a.updateRenderState({depthNear:N.near,depthFar:N.far}),y=N.near,O=N.far),N.layers.mask=6|e.layers.mask,U.layers.mask=-5&N.layers.mask,D.layers.mask=-3&N.layers.mask;const i=e.parent,r=N.cameras;W(N,i);for(let e=0;e0&&(e.alphaTest.value=i.alphaTest);const r=t.get(i),a=r.envMap,o=r.envMapRotation;a&&(e.envMap.value=a,e.envMapRotation.value.setFromMatrix4(Ta.makeRotationFromEuler(o)).transpose(),a.isCubeTexture&&!1===a.isRenderTargetTexture&&e.envMapRotation.value.premultiply(xa),e.reflectivity.value=i.reflectivity,e.ior.value=i.ior,e.refractionRatio.value=i.refractionRatio),i.lightMap&&(e.lightMap.value=i.lightMap,e.lightMapIntensity.value=i.lightMapIntensity,n(i.lightMap,e.lightMapTransform)),i.aoMap&&(e.aoMap.value=i.aoMap,e.aoMapIntensity.value=i.aoMapIntensity,n(i.aoMap,e.aoMapTransform))}return{refreshFogUniforms:function(t,n){n.color.getRGB(t.fogColor.value,_(e)),n.isFog?(t.fogNear.value=n.near,t.fogFar.value=n.far):n.isFogExp2&&(t.fogDensity.value=n.density)},refreshMaterialUniforms:function(e,r,a,o,s){r.isNodeMaterial?r.uniformsNeedUpdate=!1:r.isMeshBasicMaterial?i(e,r):r.isMeshLambertMaterial?(i(e,r),r.envMap&&(e.envMapIntensity.value=r.envMapIntensity)):r.isMeshToonMaterial?(i(e,r),function(e,t){t.gradientMap&&(e.gradientMap.value=t.gradientMap)}(e,r)):r.isMeshPhongMaterial?(i(e,r),function(e,t){e.specular.value.copy(t.specular),e.shininess.value=Math.max(t.shininess,1e-4)}(e,r),r.envMap&&(e.envMapIntensity.value=r.envMapIntensity)):r.isMeshStandardMaterial?(i(e,r),function(e,t){e.metalness.value=t.metalness,t.metalnessMap&&(e.metalnessMap.value=t.metalnessMap,n(t.metalnessMap,e.metalnessMapTransform));e.roughness.value=t.roughness,t.roughnessMap&&(e.roughnessMap.value=t.roughnessMap,n(t.roughnessMap,e.roughnessMapTransform));t.envMap&&(e.envMapIntensity.value=t.envMapIntensity)}(e,r),r.isMeshPhysicalMaterial&&function(e,t,i){e.ior.value=t.ior,t.sheen>0&&(e.sheenColor.value.copy(t.sheenColor).multiplyScalar(t.sheen),e.sheenRoughness.value=t.sheenRoughness,t.sheenColorMap&&(e.sheenColorMap.value=t.sheenColorMap,n(t.sheenColorMap,e.sheenColorMapTransform)),t.sheenRoughnessMap&&(e.sheenRoughnessMap.value=t.sheenRoughnessMap,n(t.sheenRoughnessMap,e.sheenRoughnessMapTransform)));t.clearcoat>0&&(e.clearcoat.value=t.clearcoat,e.clearcoatRoughness.value=t.clearcoatRoughness,t.clearcoatMap&&(e.clearcoatMap.value=t.clearcoatMap,n(t.clearcoatMap,e.clearcoatMapTransform)),t.clearcoatRoughnessMap&&(e.clearcoatRoughnessMap.value=t.clearcoatRoughnessMap,n(t.clearcoatRoughnessMap,e.clearcoatRoughnessMapTransform)),t.clearcoatNormalMap&&(e.clearcoatNormalMap.value=t.clearcoatNormalMap,n(t.clearcoatNormalMap,e.clearcoatNormalMapTransform),e.clearcoatNormalScale.value.copy(t.clearcoatNormalScale),t.side===c&&e.clearcoatNormalScale.value.negate()));t.dispersion>0&&(e.dispersion.value=t.dispersion);t.iridescence>0&&(e.iridescence.value=t.iridescence,e.iridescenceIOR.value=t.iridescenceIOR,e.iridescenceThicknessMinimum.value=t.iridescenceThicknessRange[0],e.iridescenceThicknessMaximum.value=t.iridescenceThicknessRange[1],t.iridescenceMap&&(e.iridescenceMap.value=t.iridescenceMap,n(t.iridescenceMap,e.iridescenceMapTransform)),t.iridescenceThicknessMap&&(e.iridescenceThicknessMap.value=t.iridescenceThicknessMap,n(t.iridescenceThicknessMap,e.iridescenceThicknessMapTransform)));t.transmission>0&&(e.transmission.value=t.transmission,e.transmissionSamplerMap.value=i.texture,e.transmissionSamplerSize.value.set(i.width,i.height),t.transmissionMap&&(e.transmissionMap.value=t.transmissionMap,n(t.transmissionMap,e.transmissionMapTransform)),e.thickness.value=t.thickness,t.thicknessMap&&(e.thicknessMap.value=t.thicknessMap,n(t.thicknessMap,e.thicknessMapTransform)),e.attenuationDistance.value=t.attenuationDistance,e.attenuationColor.value.copy(t.attenuationColor));t.anisotropy>0&&(e.anisotropyVector.value.set(t.anisotropy*Math.cos(t.anisotropyRotation),t.anisotropy*Math.sin(t.anisotropyRotation)),t.anisotropyMap&&(e.anisotropyMap.value=t.anisotropyMap,n(t.anisotropyMap,e.anisotropyMapTransform)));e.specularIntensity.value=t.specularIntensity,e.specularColor.value.copy(t.specularColor),t.specularColorMap&&(e.specularColorMap.value=t.specularColorMap,n(t.specularColorMap,e.specularColorMapTransform));t.specularIntensityMap&&(e.specularIntensityMap.value=t.specularIntensityMap,n(t.specularIntensityMap,e.specularIntensityMapTransform))}(e,r,s)):r.isMeshMatcapMaterial?(i(e,r),function(e,t){t.matcap&&(e.matcap.value=t.matcap)}(e,r)):r.isMeshDepthMaterial?i(e,r):r.isMeshDistanceMaterial?(i(e,r),function(e,n){const i=t.get(n).light;e.referencePosition.value.setFromMatrixPosition(i.matrixWorld),e.nearDistance.value=i.shadow.camera.near,e.farDistance.value=i.shadow.camera.far}(e,r)):r.isMeshNormalMaterial?i(e,r):r.isLineBasicMaterial?(function(e,t){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,t.map&&(e.map.value=t.map,n(t.map,e.mapTransform))}(e,r),r.isLineDashedMaterial&&function(e,t){e.dashSize.value=t.dashSize,e.totalSize.value=t.dashSize+t.gapSize,e.scale.value=t.scale}(e,r)):r.isPointsMaterial?function(e,t,i,r){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,e.size.value=t.size*i,e.scale.value=.5*r,t.map&&(e.map.value=t.map,n(t.map,e.uvTransform));t.alphaMap&&(e.alphaMap.value=t.alphaMap,n(t.alphaMap,e.alphaMapTransform));t.alphaTest>0&&(e.alphaTest.value=t.alphaTest)}(e,r,a,o):r.isSpriteMaterial?function(e,t){e.diffuse.value.copy(t.color),e.opacity.value=t.opacity,e.rotation.value=t.rotation,t.map&&(e.map.value=t.map,n(t.map,e.mapTransform));t.alphaMap&&(e.alphaMap.value=t.alphaMap,n(t.alphaMap,e.alphaMapTransform));t.alphaTest>0&&(e.alphaTest.value=t.alphaTest)}(e,r):r.isShadowMaterial?(e.color.value.copy(r.color),e.opacity.value=r.opacity):r.isShaderMaterial&&(r.uniformsNeedUpdate=!1)}}}function Aa(e,t,n,i){let r={},a={},o=[];const s=e.getParameter(e.MAX_UNIFORM_BUFFER_BINDINGS);function l(t,n,i,r){if(!0===function(e,t,n,i){const r=e.value,a=t+"_"+n;if(void 0===i[a])return"number"==typeof r||"boolean"==typeof r?i[a]=r:ArrayBuffer.isView(r)?i[a]=r.slice():i[a]=r.clone(),!0;{const e=i[a];if("number"==typeof r||"boolean"==typeof r){if(e!==r)return i[a]=r,!0}else{if(ArrayBuffer.isView(r))return!0;if(!1===e.equals(r))return e.copy(r),!0}}return!1}(t,n,i,r)){const n=t.__offset,i=t.value;if(Array.isArray(i)){let e=0;for(let n=0;n0&&(n+=i-r);e.__size=n,e.__cache={}}(n),f=function(t){const n=function(){for(let e=0;e0),p=!!n.morphAttributes.position,m=!!n.morphAttributes.normal,h=!!n.morphAttributes.color;let _=L;i.toneMapped&&(null!==q&&!0!==q.isXRRenderTarget||(_=G.toneMapping));const g=n.morphAttributes.position||n.morphAttributes.normal||n.morphAttributes.color,v=void 0!==g?g.length:0,S=Ae.get(i),M=w.state.lights;if(!0===ue&&(!0===fe||e!==Z)){const t=e===Z&&i.id===j;Fe.setState(i,e,t)}let T=!1;i.version===S.__version?S.needsLights&&S.lightsStateVersion!==M.state.version||S.outputColorSpace!==s||r.isBatchedMesh&&!1===S.batching?T=!0:r.isBatchedMesh||!0!==S.batching?r.isBatchedMesh&&!0===S.batchingColor&&null===r.colorTexture||r.isBatchedMesh&&!1===S.batchingColor&&null!==r.colorTexture||r.isInstancedMesh&&!1===S.instancing?T=!0:r.isInstancedMesh||!0!==S.instancing?r.isSkinnedMesh&&!1===S.skinning?T=!0:r.isSkinnedMesh||!0!==S.skinning?r.isInstancedMesh&&!0===S.instancingColor&&null===r.instanceColor||r.isInstancedMesh&&!1===S.instancingColor&&null!==r.instanceColor||r.isInstancedMesh&&!0===S.instancingMorph&&null===r.morphTexture||r.isInstancedMesh&&!1===S.instancingMorph&&null!==r.morphTexture||S.envMap!==c||!0===i.fog&&S.fog!==a?T=!0:void 0===S.numClippingPlanes||S.numClippingPlanes===Fe.numPlanes&&S.numIntersection===Fe.numIntersection?(S.vertexAlphas!==d||S.vertexTangents!==u||S.morphTargets!==p||S.morphNormals!==m||S.morphColors!==h||S.toneMapping!==_||S.morphTargetsCount!==v||!!S.lightProbeGrid!=w.state.lightProbeGridArray.length>0)&&(T=!0):T=!0:T=!0:T=!0:T=!0:(T=!0,S.__version=i.version);let x=S.currentProgram;!0===T&&(x=dt(i,t,r),V&&i.isNodeMaterial&&V.onUpdateProgram(i,x,S));let R=!1,A=!1,b=!1;const C=x.getUniforms(),U=S.uniforms;xe.useProgram(x.program)&&(R=!0,A=!0,b=!0);i.id!==j&&(j=i.id,A=!0);if(S.needsLights){const e=function(e,t){if(0===e.length)return null;if(1===e.length)return null!==e[0].texture?e[0]:null;P.setFromMatrixPosition(t.matrixWorld);for(let t=0,n=e.length;t0&&C.setValue(Ke,"directionalShadowMap",M.state.directionalShadowMap,be),M.state.spotShadowMap.length>0&&C.setValue(Ke,"spotShadowMap",M.state.spotShadowMap,be),M.state.pointShadowMap.length>0&&C.setValue(Ke,"pointShadowMap",M.state.pointShadowMap,be));if(r.isSkinnedMesh){C.setOptional(Ke,r,"bindMatrix"),C.setOptional(Ke,r,"bindMatrixInverse");const e=r.skeleton;e&&(null===e.boneTexture&&e.computeBoneTexture(),C.setValue(Ke,"boneTexture",e.boneTexture,be))}r.isBatchedMesh&&(C.setOptional(Ke,r,"batchingTexture"),C.setValue(Ke,"batchingTexture",r._matricesTexture,be),C.setOptional(Ke,r,"batchingIdTexture"),C.setValue(Ke,"batchingIdTexture",r._indirectTexture,be),C.setOptional(Ke,r,"batchingColorTexture"),null!==r._colorsTexture&&C.setValue(Ke,"batchingColorTexture",r._colorsTexture,be));const D=n.morphAttributes;void 0===D.position&&void 0===D.normal&&void 0===D.color||He.update(r,n,x);(A||S.receiveShadow!==r.receiveShadow)&&(S.receiveShadow=r.receiveShadow,C.setValue(Ke,"receiveShadow",r.receiveShadow));(i.isMeshStandardMaterial||i.isMeshLambertMaterial||i.isMeshPhongMaterial)&&null===i.envMap&&null!==t.environment&&(U.envMapIntensity.value=t.environmentIntensity);void 0!==U.dfgLUT&&(U.dfgLUT.value=(null===Ca&&(Ca=new Ln(ba,16,16,Se,E),Ca.name="DFG_LUT",Ca.minFilter=O,Ca.magFilter=O,Ca.wrapS=mt,Ca.wrapT=mt,Ca.generateMipmaps=!1,Ca.needsUpdate=!0),Ca));if(A){if(C.setValue(Ke,"toneMappingExposure",G.toneMappingExposure),S.needsLights&&(N=b,(I=U).ambientLightColor.needsUpdate=N,I.lightProbe.needsUpdate=N,I.directionalLights.needsUpdate=N,I.directionalLightShadows.needsUpdate=N,I.pointLights.needsUpdate=N,I.pointLightShadows.needsUpdate=N,I.spotLights.needsUpdate=N,I.spotLightShadows.needsUpdate=N,I.rectAreaLights.needsUpdate=N,I.hemisphereLights.needsUpdate=N),a&&!0===i.fog&&Ne.refreshFogUniforms(U,a),Ne.refreshMaterialUniforms(U,i,re,ie,w.state.transmissionRenderTarget[e.id]),S.needsLights&&S.lightProbeGrid){const e=S.lightProbeGrid;U.probesSH.value=e.texture,U.probesMin.value.copy(e.boundingBox.min),U.probesMax.value.copy(e.boundingBox.max),U.probesResolution.value.copy(e.resolution)}Rr.upload(Ke,ut(S),U,be)}var I,N;i.isShaderMaterial&&!0===i.uniformsNeedUpdate&&(Rr.upload(Ke,ut(S),U,be),i.uniformsNeedUpdate=!1);i.isSpriteMaterial&&C.setValue(Ke,"center",r.center);if(C.setValue(Ke,"modelViewMatrix",r.modelViewMatrix),C.setValue(Ke,"normalMatrix",r.normalMatrix),C.setValue(Ke,"modelMatrix",r.matrixWorld),void 0!==i.uniformsGroups){const e=i.uniformsGroups;for(let t=0,n=e.length;t{function n(){i.forEach(function(e){Ae.get(e).currentProgram.isReady()&&i.delete(e)}),0!==i.size?setTimeout(n,10):t(e)}null!==Me.get("KHR_parallel_shader_compile")?n():setTimeout(n,10)})};let tt=null;function nt(){rt.stop()}function it(){rt.start()}const rt=new On;function at(e,t,n,i){if(!1===e.visible)return;if(e.layers.test(t.layers))if(e.isGroup)n=e.renderOrder;else if(e.isLOD)!0===e.autoUpdate&&e.update(t);else if(e.isLightProbeGrid)w.pushLightProbeGrid(e);else if(e.isLight)w.pushLight(e),e.castShadow&&w.pushShadow(e);else if(e.isSprite){if(!e.frustumCulled||de.intersectsSprite(e)){i&&_e.setFromMatrixPosition(e.matrixWorld).applyMatrix4(pe);const t=we.update(e),r=e.material;r.visible&&U.push(e,t,r,n,_e.z,null)}}else if((e.isMesh||e.isLine||e.isPoints)&&(!e.frustumCulled||de.intersectsObject(e))){const t=we.update(e),r=e.material;if(i&&(void 0!==e.boundingSphere?(null===e.boundingSphere&&e.computeBoundingSphere(),_e.copy(e.boundingSphere.center)):(null===t.boundingSphere&&t.computeBoundingSphere(),_e.copy(t.boundingSphere.center)),_e.applyMatrix4(e.matrixWorld).applyMatrix4(pe)),Array.isArray(r)){const i=t.groups;for(let a=0,o=i.length;a0&<(r,t,n),a.length>0&<(a,t,n),o.length>0&<(o,t,n),xe.buffers.depth.setTest(!0),xe.buffers.depth.setMask(!0),xe.buffers.color.setMask(!0),xe.setPolygonOffset(!1)}function st(e,t,n,i){if(null!==(!0===n.isScene?n.overrideMaterial:null))return;if(void 0===w.state.transmissionRenderTarget[i.id]){const e=Me.has("EXT_color_buffer_half_float")||Me.has("EXT_color_buffer_float");w.state.transmissionRenderTarget[i.id]=new I(1,1,{generateMipmaps:!0,type:e?E:S,minFilter:B,samples:Math.max(4,Te.samples),stencilBuffer:o,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:f.workingColorSpace})}const r=w.state.transmissionRenderTarget[i.id],a=i.viewport||$;r.setSize(a.z*G.transmissionResolutionScale,a.w*G.transmissionResolutionScale);const s=G.getRenderTarget(),l=G.getActiveCubeFace(),d=G.getActiveMipmapLevel();G.setRenderTarget(r),G.getClearColor(ee),te=G.getClearAlpha(),te<1&&G.setClearColor(16777215,.5),G.clear(),ve&&Ge.render(n);const u=G.toneMapping;G.toneMapping=L;const p=i.viewport;if(void 0!==i.viewport&&(i.viewport=void 0),w.setupLightsView(i),!0===ue&&Fe.setGlobalState(G.clippingPlanes,i),lt(e,n,i),be.updateMultisampleRenderTarget(r),be.updateRenderTargetMipmap(r),!1===Me.has("WEBGL_multisampled_render_to_texture")){let e=!1;for(let r=0,a=t.length;r0,i.currentProgram=u,i.uniformsList=null,u}function ut(e){if(null===e.uniformsList){const t=e.currentProgram.getUniforms();e.uniformsList=Rr.seqWithValue(t.seq,e.uniforms)}return e.uniformsList}function ft(e,t){const n=Ae.get(e);n.outputColorSpace=t.outputColorSpace,n.batching=t.batching,n.batchingColor=t.batchingColor,n.instancing=t.instancing,n.instancingColor=t.instancingColor,n.instancingMorph=t.instancingMorph,n.skinning=t.skinning,n.morphTargets=t.morphTargets,n.morphNormals=t.morphNormals,n.morphColors=t.morphColors,n.morphTargetsCount=t.morphTargetsCount,n.numClippingPlanes=t.numClippingPlanes,n.numIntersection=t.numClipIntersection,n.vertexAlphas=t.vertexAlphas,n.vertexTangents=t.vertexTangents,n.toneMapping=t.toneMapping}rt.setAnimationLoop(function(e){tt&&tt(e)}),"undefined"!=typeof self&&rt.setContext(self),this.setAnimationLoop=function(e){tt=e,je.setAnimationLoop(e),null===e?rt.stop():rt.start()},je.addEventListener("sessionstart",nt),je.addEventListener("sessionend",it),this.render=function(e,t){if(void 0!==t&&!0!==t.isCamera)return void D("WebGLRenderer.render: camera is not an instance of THREE.Camera.");if(!0===H)return;null!==V&&V.renderStart(e,t);const n=!0===je.enabled&&!0===je.isPresenting,i=null!==F&&(null===q||n)&&F.begin(G,q);if(!0===e.matrixWorldAutoUpdate&&e.updateMatrixWorld(),null===t.parent&&!0===t.matrixWorldAutoUpdate&&t.updateMatrixWorld(),!0!==je.enabled||!0!==je.isPresenting||null!==F&&!1!==F.isCompositing()||(!0===je.cameraAutoUpdate&&je.updateCamera(t),t=je.getCamera()),!0===e.isScene&&e.onBeforeRender(G,e,t,q),w=Oe.get(e,y.length),w.init(t),w.state.textureUnits=be.getTextureUnits(),y.push(w),pe.multiplyMatrices(t.projectionMatrix,t.matrixWorldInverse),de.setFromProjectionMatrix(pe,Nn,t.reversedDepth),fe=this.localClippingEnabled,ue=Fe.init(this.clippingPlanes,fe),U=ye.get(e,N.length),U.init(),N.push(U),!0===je.enabled&&!0===je.isPresenting){const e=G.xr.getDepthSensingMesh();null!==e&&at(e,t,-1/0,G.sortObjects)}at(e,t,0,G.sortObjects),U.finish(),!0===G.sortObjects&&U.sort(ae,oe,t.reversedDepth),ve=!1===je.enabled||!1===je.isPresenting||!1===je.hasDepthSensing(),ve&&Ge.addToRenderList(U,e),this.info.render.frame++,!0===this.info.autoReset&&this.info.reset(),!0===ue&&Fe.beginShadows();const r=w.state.shadowsArray;Be.render(r,e,t),!0===ue&&Fe.endShadows();if(!1===(i&&F.hasRenderPass())){const n=U.opaque,i=U.transmissive;if(w.setupLights(),t.isArrayCamera){const r=t.cameras;if(i.length>0)for(let t=0,a=r.length;t0&&st(n,i,e,t),ve&&Ge.render(e),ot(U,e,t)}null!==q&&0===Y&&(be.updateMultisampleRenderTarget(q),be.updateRenderTargetMipmap(q)),i&&F.end(G),!0===e.isScene&&e.onAfterRender(G,e,t),ke.resetDefaultState(),j=-1,Z=null,y.pop(),y.length>0?(w=y[y.length-1],be.setTextureUnits(w.state.textureUnits),!0===ue&&Fe.setGlobalState(G.clippingPlanes,w.state.camera)):w=null,N.pop(),U=N.length>0?N[N.length-1]:null,null!==V&&V.renderEnd()},this.getActiveCubeFace=function(){return X},this.getActiveMipmapLevel=function(){return Y},this.getRenderTarget=function(){return q},this.setRenderTargetTextures=function(e,t,n){const i=Ae.get(e);i.__autoAllocateDepthBuffer=!1===e.resolveDepthBuffer,!1===i.__autoAllocateDepthBuffer&&(i.__useRenderToTexture=!1),Ae.get(e.texture).__webglTexture=t,Ae.get(e.depthTexture).__webglTexture=i.__autoAllocateDepthBuffer?void 0:n,i.__hasExternalTextures=!0},this.setRenderTargetFramebuffer=function(e,t){const n=Ae.get(e);n.__webglFramebuffer=t,n.__useDefaultFramebuffer=void 0===t},this.setRenderTarget=function(e,t=0,n=0){q=e,X=t,Y=n;let i=null,r=!1,a=!1;if(e){const o=Ae.get(e);if(void 0!==o.__useDefaultFramebuffer)return xe.bindFramebuffer(Ke.FRAMEBUFFER,o.__webglFramebuffer),$.copy(e.viewport),Q.copy(e.scissor),J=e.scissorTest,xe.viewport($),xe.scissor(Q),xe.setScissorTest(J),void(j=-1);if(void 0===o.__webglFramebuffer)be.setupRenderTarget(e);else if(o.__hasExternalTextures)be.rebindTextures(e,Ae.get(e.texture).__webglTexture,Ae.get(e.depthTexture).__webglTexture);else if(e.depthBuffer){const t=e.depthTexture;if(o.__boundDepthTexture!==t){if(null!==t&&Ae.has(t)&&(e.width!==t.image.width||e.height!==t.image.height))throw new Error("THREE.WebGLRenderer: Attached DepthTexture is initialized to the incorrect size.");be.setupDepthRenderbuffer(e)}}const s=e.texture;(s.isData3DTexture||s.isDataArrayTexture||s.isCompressedArrayTexture)&&(a=!0);const l=Ae.get(e).__webglFramebuffer;e.isWebGLCubeRenderTarget?(i=Array.isArray(l[t])?l[t][n]:l[t],r=!0):i=e.samples>0&&!1===be.useMultisampledRTT(e)?Ae.get(e).__webglMultisampledFramebuffer:Array.isArray(l)?l[n]:l,$.copy(e.viewport),Q.copy(e.scissor),J=e.scissorTest}else $.copy(se).multiplyScalar(re).floor(),Q.copy(le).multiplyScalar(re).floor(),J=ce;0!==n&&(i=W);if(xe.bindFramebuffer(Ke.FRAMEBUFFER,i)&&xe.drawBuffers(e,i),xe.viewport($),xe.scissor(Q),xe.setScissorTest(J),r){const i=Ae.get(e.texture);Ke.framebufferTexture2D(Ke.FRAMEBUFFER,Ke.COLOR_ATTACHMENT0,Ke.TEXTURE_CUBE_MAP_POSITIVE_X+t,i.__webglTexture,n)}else if(a){const i=t;for(let t=0;t1&&Ke.readBuffer(Ke.COLOR_ATTACHMENT0+s),!Te.textureFormatReadable(l))return void D("WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");if(!Te.textureTypeReadable(c))return void D("WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");t>=0&&t<=e.width-i&&n>=0&&n<=e.height-r&&Ke.readPixels(t,n,i,r,ze.convert(l),ze.convert(c),a)}finally{const e=null!==q?Ae.get(q).__webglFramebuffer:null;xe.bindFramebuffer(Ke.FRAMEBUFFER,e)}}},this.readRenderTargetPixelsAsync=async function(e,t,n,i,r,a,o,s=0){if(!e||!e.isWebGLRenderTarget)throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let l=Ae.get(e).__webglFramebuffer;if(e.isWebGLCubeRenderTarget&&void 0!==o&&(l=l[o]),l){if(t>=0&&t<=e.width-i&&n>=0&&n<=e.height-r){xe.bindFramebuffer(Ke.FRAMEBUFFER,l);const o=e.textures[s],c=o.format,d=o.type;if(e.textures.length>1&&Ke.readBuffer(Ke.COLOR_ATTACHMENT0+s),!Te.textureFormatReadable(c))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!Te.textureTypeReadable(d))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.");const u=Ke.createBuffer();Ke.bindBuffer(Ke.PIXEL_PACK_BUFFER,u),Ke.bufferData(Ke.PIXEL_PACK_BUFFER,a.byteLength,Ke.STREAM_READ),Ke.readPixels(t,n,i,r,ze.convert(c),ze.convert(d),0);const f=null!==q?Ae.get(q).__webglFramebuffer:null;xe.bindFramebuffer(Ke.FRAMEBUFFER,f);const p=Ke.fenceSync(Ke.SYNC_GPU_COMMANDS_COMPLETE,0);return Ke.flush(),await yn(Ke,p,4),Ke.bindBuffer(Ke.PIXEL_PACK_BUFFER,u),Ke.getBufferSubData(Ke.PIXEL_PACK_BUFFER,0,a),Ke.deleteBuffer(u),Ke.deleteSync(p),a}throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.")}},this.copyFramebufferToTexture=function(e,t=null,n=0){const i=Math.pow(2,-n),r=Math.floor(e.image.width*i),a=Math.floor(e.image.height*i),o=null!==t?t.x:0,s=null!==t?t.y:0;be.setTexture2D(e,0),Ke.copyTexSubImage2D(Ke.TEXTURE_2D,n,0,0,o,s,r,a),xe.unbindTexture()},this.copyTextureToTexture=function(e,t,n=null,i=null,r=0,a=0){let o,s,l,c,d,u,f,p,m;const h=e.isCompressedTexture?e.mipmaps[a]:e.image;if(null!==n)o=n.max.x-n.min.x,s=n.max.y-n.min.y,l=n.isBox3?n.max.z-n.min.z:1,c=n.min.x,d=n.min.y,u=n.isBox3?n.min.z:0;else{const t=Math.pow(2,-r);o=Math.floor(h.width*t),s=Math.floor(h.height*t),l=e.isDataArrayTexture?h.depth:e.isData3DTexture?Math.floor(h.depth*t):1,c=0,d=0,u=0}null!==i?(f=i.x,p=i.y,m=i.z):(f=0,p=0,m=0);const _=ze.convert(t.format),g=ze.convert(t.type);let v;t.isData3DTexture?(be.setTexture3D(t,0),v=Ke.TEXTURE_3D):t.isDataArrayTexture||t.isCompressedArrayTexture?(be.setTexture2DArray(t,0),v=Ke.TEXTURE_2D_ARRAY):(be.setTexture2D(t,0),v=Ke.TEXTURE_2D),xe.activeTexture(Ke.TEXTURE0),xe.pixelStorei(Ke.UNPACK_FLIP_Y_WEBGL,t.flipY),xe.pixelStorei(Ke.UNPACK_PREMULTIPLY_ALPHA_WEBGL,t.premultiplyAlpha),xe.pixelStorei(Ke.UNPACK_ALIGNMENT,t.unpackAlignment);const E=xe.getParameter(Ke.UNPACK_ROW_LENGTH),S=xe.getParameter(Ke.UNPACK_IMAGE_HEIGHT),M=xe.getParameter(Ke.UNPACK_SKIP_PIXELS),T=xe.getParameter(Ke.UNPACK_SKIP_ROWS),x=xe.getParameter(Ke.UNPACK_SKIP_IMAGES);xe.pixelStorei(Ke.UNPACK_ROW_LENGTH,h.width),xe.pixelStorei(Ke.UNPACK_IMAGE_HEIGHT,h.height),xe.pixelStorei(Ke.UNPACK_SKIP_PIXELS,c),xe.pixelStorei(Ke.UNPACK_SKIP_ROWS,d),xe.pixelStorei(Ke.UNPACK_SKIP_IMAGES,u);const R=e.isDataArrayTexture||e.isData3DTexture,A=t.isDataArrayTexture||t.isData3DTexture;if(e.isDepthTexture){const n=Ae.get(e),i=Ae.get(t),h=Ae.get(n.__renderTarget),_=Ae.get(i.__renderTarget);xe.bindFramebuffer(Ke.READ_FRAMEBUFFER,h.__webglFramebuffer),xe.bindFramebuffer(Ke.DRAW_FRAMEBUFFER,_.__webglFramebuffer);for(let n=0;n