diff --git a/.github/benches/fetch_repos.sh b/.github/benches/fetch_repos.sh new file mode 100644 index 000000000..6e5273ebc --- /dev/null +++ b/.github/benches/fetch_repos.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Fetch danh sách repo (pinned) trong .github/benches/repos/sources.txt về +# .github/benches/repos/checkout/, rồi ghi đường dẫn TUYỆT ĐỐI vào +# .github/benches/repos/list.txt để codegraph-bench (CodSpeed) đọc qua env +# CODEGRAPH_BENCH_REPOS_LIST (${{ github.workspace }}/.github/benches/repos/list.txt). +# +# Chạy local: bash .github/benches/fetch_repos.sh +# Chạy trong CI (codspeed.yml) trước `cargo codspeed build`. +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT="$DIR/repos/checkout" +LIST="$DIR/repos/list.txt" +SRC="$DIR/repos/sources.txt" + +mkdir -p "$OUT" +: > "$LIST" + +# Mỗi dòng: || +# `|| [ -n "$name" ]` xử lý dòng cuối không có trailing `\n`. +while IFS='|' read -r name url commit || [ -n "$name" ]; do + name="$(printf '%s' "$name" | xargs)" # trim + [ -z "$name" ] && continue + [[ "$name" == \#* ]] && continue + dest="$OUT/$name" + if [ ! -d "$dest/.git" ]; then + echo ">> clone $name ..." + git clone --quiet --filter=blob:none --no-checkout "$url" "$dest" + fi + echo ">> checkout $name @ ${commit:0:12}" + git -C "$dest" fetch --quiet --depth 1 origin "$commit" + git -C "$dest" checkout --quiet "$commit" + echo "$dest" >> "$LIST" +done < "$SRC" + +echo "=== repos ready (${LIST}) ===" +cat "$LIST" diff --git a/.github/benches/repos/sources.txt b/.github/benches/repos/sources.txt new file mode 100644 index 000000000..6898ac56c --- /dev/null +++ b/.github/benches/repos/sources.txt @@ -0,0 +1,11 @@ +# Danh sách repo codspeed để benchmark — mỗi dòng: || +# +# Sửa/thêm dòng để thay đổi tập repo (được fetch về theo `benches/fetch_repos.sh`). +# Commit SHA cố định (pinned) để dữ liệu đầu vào giữ nguyên giữa các lần chạy, +# giúp CodSpeed so sánh performance ổn định. Muốn cập nhật thì đổi SHA rồi re-run. +# +# Các repo nhỏ, đa ngôn ngữ để phủ parser của codegraph-extract: +hello|https://github.com/golang/example|7f05d217867b2af52b0a28c6d1c91df97e1b5b39 +serde-json|https://github.com/serde-rs/json|a3e9758ffc88247ab82182cb2505867768a702e3 +flask|https://github.com/pallets/flask|6a2f545bfd8ed31e19066a299296917e034aca58 +express|https://github.com/expressjs/express|a3714473feb3d2908add734d340e7755fd85e0a3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be12b1614..1bc32ba8f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest] steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 000000000..c46dae028 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,52 @@ +name: CodSpeed + +on: + push: + branches: + - "main" + pull_request: + # `workflow_dispatch` cho phép CodSpeed trigger backtest performance + # để sinh dữ liệu ban đầu. + workflow_dispatch: + +permissions: + contents: read + id-token: write # OpenID Connect auth với CodSpeed + +env: + # Danh sách repo (1 path/dòng) sẽ được bench — do .github/benches/fetch_repos.sh + # ghi ra từ .github/benches/repos/sources.txt. codegraph-bench đọc env này khi chạy. + # Dùng path TUYỆT ĐỐI để không phụ thuộc CWD của `cargo codspeed run`. + CODEGRAPH_BENCH_REPOS_LIST: ${{ github.workspace }}/.github/benches/repos/list.txt + +jobs: + # Performance benchmarks: extract → index → query trên danh sách repo thật + # (xem crates/codegraph-bench, bench target `codspeed`). + codspeed: + name: Bench + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup rust toolchain, cache and cargo-codspeed binary + uses: moonrepo/setup-rust@v0 + with: + channel: stable + cache-target: release + bins: cargo-codspeed + + # Clone các repo pinned trong benches/repos/sources.txt về + # benches/repos/checkout/ và ghi benches/repos/list.txt. + - name: Fetch bench repos + run: bash .github/benches/fetch_repos.sh + + - name: Build benchmark targets + run: cargo codspeed build -p codegraph-bench --features codspeed + + # `mode: benchmark` đẩy kết quả lên CodSpeed Cloud (auto-provision bằng OIDC) + # để theo dõi trend. Muốn chạy khô (không lưu baseline) thì đổi `simulation`. + - name: Run benchmarks + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: cargo codspeed run diff --git a/.gitignore b/.gitignore index cb91828fd..382b11a61 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,7 @@ venv/ *.egg-info/ dist/ build/ + +# Bench repos được fetch về (danh sách nguồn: .github/benches/repos/sources.txt) +.github/benches/repos/checkout/ +.github/benches/repos/list.txt diff --git a/Cargo.lock b/Cargo.lock index e2060c0f5..b31d48e09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "ahash" version = "0.8.12" @@ -15,6 +9,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "const-random", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -35,6 +31,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "1.0.0" @@ -92,22 +94,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arcstr" -version = "1.2.0" +name = "approx" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] [[package]] -name = "async-compression" -version = "0.4.42" +name = "arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" -dependencies = [ - "compression-codecs", - "compression-core", - "pin-project-lite", - "tokio", -] +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arcstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" [[package]] name = "async-lock" @@ -140,70 +145,12 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "axum" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" -dependencies = [ - "axum-core", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "base64" version = "0.22.1" @@ -255,6 +202,9 @@ name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] [[package]] name = "bytes" @@ -271,6 +221,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.62" @@ -291,19 +247,35 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] -name = "chacha20" -version = "0.10.1" +name = "ciborium" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core", + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", ] [[package]] @@ -360,7 +332,7 @@ dependencies = [ "codegraph-graph", "codegraph-installer", "codegraph-mcp", - "codegraph-viz", + "codegraph-sboxes", "console 0.15.11", "dialoguer", "dirs", @@ -388,6 +360,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "codegraph-bench" +version = "1.2.0" +dependencies = [ + "anyhow", + "camino", + "clap", + "codegraph-core", + "codegraph-extract", + "codegraph-graph", + "codspeed-criterion-compat", + "criterion", + "serde", + "serde_json", + "tempfile", + "tokio", +] + [[package]] name = "codegraph-context" version = "1.2.0" @@ -448,6 +438,8 @@ dependencies = [ "bincode", "camino", "codegraph-core", + "codegraph-extract", + "criterion", "dashmap", "libsqlite3-sys", "parking_lot", @@ -460,6 +452,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", + "url", "zstd", ] @@ -488,7 +481,9 @@ dependencies = [ "codegraph-api", "codegraph-context", "codegraph-core", + "codegraph-extract", "codegraph-graph", + "codegraph-sboxes", "serde", "serde_json", "tempfile", @@ -497,25 +492,82 @@ dependencies = [ ] [[package]] -name = "codegraph-viz" +name = "codegraph-sboxes" version = "1.2.0" dependencies = [ - "anyhow", - "axum", "camino", - "codegraph-api", "codegraph-core", "codegraph-graph", - "open", - "reqwest", - "rust-embed", + "cranelift-codegen", + "cranelift-frontend", + "cranelift-jit", + "cranelift-module", + "cranelift-native", + "rhai", "serde", "serde_json", - "tempfile", + "target-lexicon", + "thiserror 2.0.18", "tokio", - "tower", - "tower-http", - "tracing", + "toml", +] + +[[package]] +name = "codspeed" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7083f253260bcb4aaa3b4aa4c52973703dabc1a85c2f193997e2689aafa8a919" +dependencies = [ + "anyhow", + "cc", + "colored", + "getrandom 0.4.2", + "glob", + "libc", + "nix", + "serde", + "serde_json", + "statrs", +] + +[[package]] +name = "codspeed-criterion-compat" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f24251445188c69d50f10179d795424bd71b533b7e3f84fc03f26893b01af0" +dependencies = [ + "clap", + "codspeed", + "codspeed-criterion-compat-walltime", + "colored", + "regex", +] + +[[package]] +name = "codspeed-criterion-compat-walltime" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c38205d56e2cb4fe04b708de7f9653a3f1b89edbe3a20b28f21e9e525e9e061" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "codspeed", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", ] [[package]] @@ -524,6 +576,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "combine" version = "4.6.7" @@ -538,23 +599,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "compression-codecs" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" -dependencies = [ - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" - [[package]] name = "console" version = "0.15.11" @@ -580,6 +624,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -590,12 +654,135 @@ dependencies = [ ] [[package]] -name = "cpufeatures" -version = "0.3.0" +name = "cranelift-bforest" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34" + +[[package]] +name = "cranelift-codegen" +version = "0.116.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e" dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.14.5", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb" + +[[package]] +name = "cranelift-control" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323" +dependencies = [ + "cranelift-bitset", +] + +[[package]] +name = "cranelift-frontend" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d" + +[[package]] +name = "cranelift-jit" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e65c42755a719b09662b00c700daaf76cc35d5ace1f5c002ad404b591ff1978" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-module", + "cranelift-native", + "libc", + "log", + "region", + "target-lexicon", + "wasmtime-jit-icache-coherence", + "windows-sys 0.59.0", +] + +[[package]] +name = "cranelift-module" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d55612bebcf16ff7306c8a6f5bdb6d45662b8aa1ee058ecce8807ad87db719b" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", +] + +[[package]] +name = "cranelift-native" +version = "0.116.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7" +dependencies = [ + "cranelift-codegen", "libc", + "target-lexicon", ] [[package]] @@ -614,12 +801,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] -name = "crc32fast" -version = "1.5.0" +name = "criterion" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ - "cfg-if", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "futures", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", ] [[package]] @@ -656,6 +872,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -841,16 +1063,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "flume" version = "0.11.1" @@ -859,7 +1071,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin", + "spin 0.9.9", ] [[package]] @@ -886,6 +1098,20 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -974,10 +1200,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", ] [[package]] @@ -987,15 +1223,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", - "js-sys", "libc", - "r-efi", - "rand_core", + "r-efi 6.0.0", "wasip2", "wasip3", - "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + [[package]] name = "globset" version = "0.4.18" @@ -1009,6 +1259,17 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1060,115 +1321,16 @@ 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 = "http" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", - "webpki-roots", -] +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] -name = "hyper-util" -version = "0.1.20" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "icu_collections" @@ -1350,28 +1512,14 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "is-docker" -version = "0.2.0" +name = "is-terminal" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", + "hermit-abi", + "libc", + "windows-sys 0.61.2", ] [[package]] @@ -1380,6 +1528,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1499,10 +1656,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "lru-slab" -version = "0.1.2" +name = "mach2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] [[package]] name = "matchers" @@ -1513,12 +1673,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - [[package]] name = "memchr" version = "2.8.0" @@ -1526,31 +1680,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "mime" -version = "0.3.17" +name = "mio" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] [[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "nix" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "adler2", - "simd-adler32", + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", ] [[package]] -name = "mio" -version = "1.2.0" +name = "no-std-compat" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", + "spin 0.5.2", ] [[package]] @@ -1636,6 +1795,9 @@ name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -1644,14 +1806,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "open" -version = "5.3.6" +name = "oorandom" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" -dependencies = [ - "is-wsl", - "libc", -] +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "option-ext" @@ -1707,93 +1865,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "portable-atomic" -version = "1.14.0" +name = "plotters" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] [[package]] -name = "potential_utf" -version = "0.1.5" +name = "plotters-backend" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] -name = "prettyplease" -version = "0.2.37" +name = "plotters-svg" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ - "proc-macro2", - "syn 2.0.117", + "plotters-backend", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "portable-atomic" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] -name = "quinn" -version = "0.11.11" +name = "potential_utf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", ] [[package]] -name = "quinn-proto" -version = "0.11.16" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ - "bytes", - "getrandom 0.4.2", - "lru-slab", - "rand", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", + "proc-macro2", + "syn 2.0.117", ] [[package]] -name = "quinn-udp" -version = "0.5.15" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", + "unicode-ident", ] [[package]] @@ -1807,35 +1937,15 @@ dependencies = [ [[package]] name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.10.1" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand_pcg" -version = "0.10.2" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rayon" @@ -1902,6 +2012,20 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "regalloc2" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + [[package]] name = "regex" version = "1.12.3" @@ -1932,103 +2056,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] -name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", -] - -[[package]] -name = "ring" -version = "0.17.14" +name = "region" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", + "bitflags 1.3.2", "libc", - "untrusted", + "mach2", "windows-sys 0.52.0", ] [[package]] -name = "rusqlite" -version = "0.32.1" +name = "rhai" +version = "1.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +checksum = "dd4dd0f8c36625202a4ba553c416c19b719947cd2a31d1bda06126e4a5727daf" dependencies = [ + "ahash", "bitflags 2.11.1", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink 0.9.1", - "libsqlite3-sys", + "no-std-compat", + "num-traits", + "once_cell", + "rhai_codegen", "smallvec", + "smartstring", + "thin-vec", + "web-time", ] [[package]] -name = "rust-embed" -version = "8.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.11.0" +name = "rhai_codegen" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +checksum = "3cd3a7535e50bf36857e7be7bec276d334e8c2dfa469c2201226fd01638ea5ca" dependencies = [ "proc-macro2", "quote", - "rust-embed-utils", "syn 2.0.117", - "walkdir", ] [[package]] -name = "rust-embed-utils" -version = "8.11.0" +name = "rusqlite" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "sha2", - "walkdir", + "bitflags 2.11.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink 0.9.1", + "libsqlite3-sys", + "smallvec", ] [[package]] @@ -2050,41 +2129,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" -dependencies = [ - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -2162,17 +2206,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - [[package]] name = "serde_spanned" version = "0.6.9" @@ -2207,7 +2240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -2232,12 +2265,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - [[package]] name = "slab" version = "0.4.12" @@ -2250,6 +2277,17 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + [[package]] name = "socket2" version = "0.6.4" @@ -2260,6 +2298,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.9" @@ -2379,6 +2423,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "streaming-iterator" version = "0.1.9" @@ -2391,12 +2451,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[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.117" @@ -2419,15 +2473,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -2439,6 +2484,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tempfile" version = "3.27.0" @@ -2462,6 +2513,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thin-vec" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d" + [[package]] name = "thiserror" version = "1.0.69" @@ -2511,6 +2568,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -2522,20 +2588,15 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.11.0" +name = "tinytemplate" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ - "tinyvec_macros", + "serde", + "serde_json", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.52.3" @@ -2562,16 +2623,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - [[package]] name = "tokio-stream" version = "0.1.19" @@ -2637,56 +2688,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "async-compression", - "bitflags 2.11.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.44" @@ -2909,12 +2910,6 @@ dependencies = [ "tree-sitter-language", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "typenum" version = "1.20.0" @@ -2945,12 +2940,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.8" @@ -3003,15 +2992,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3049,16 +3029,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.126" @@ -3125,6 +3095,18 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "29.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec5e8552e01692e6c2e5293171704fed8abdec79d1a6995a0870ab190e5747d1" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "web-sys" version = "0.3.103" @@ -3145,15 +3127,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "7.0.3" diff --git a/Cargo.toml b/Cargo.toml index 1772dc3aa..ab0fd4428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,9 @@ members = [ "crates/codegraph-graph", "crates/codegraph-context", "crates/codegraph-api", + "crates/codegraph-sboxes", "crates/codegraph-mcp", - "crates/codegraph-viz", + "crates/codegraph-bench", "crates/codegraph-installer", "crates/codegraph", ] @@ -31,6 +32,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } # storage +redis = { version = "1.0", features = ["tokio-comp"] } rusqlite = { version = "0.32", features = ["bundled", "backup"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite"] } @@ -56,7 +58,7 @@ tree-sitter-lua = "0.5" # cli / async / fs clap = { version = "4", features = ["derive", "wrap_help"] } -tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "io-util", "fs", "sync", "time"] } +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "io-std", "io-util", "fs", "sync", "time"] } notify = "7" notify-debouncer-full = "0.4" ignore = "0.4" diff --git a/crates/codegraph-api/src/lib.rs b/crates/codegraph-api/src/lib.rs index 9452d1256..35ffc9d77 100644 --- a/crates/codegraph-api/src/lib.rs +++ b/crates/codegraph-api/src/lib.rs @@ -30,7 +30,10 @@ impl GraphApi { /// Search symbol theo tên (substring, case-insensitive). pub async fn search(&self, query: &str, limit: u32) -> Result> { - self.index().await.search_symbol(query, None, limit as usize).await + self.index() + .await + .search_symbol(query, None, limit as usize) + .await } /// Search symbol nâng cao — kind filter + match mode + phân trang. @@ -175,7 +178,10 @@ impl GraphApi { if prefix.is_empty() { files } else { - files.into_iter().filter(|f| f.path.starts_with(prefix)).collect() + files + .into_iter() + .filter(|f| f.path.starts_with(prefix)) + .collect() } } diff --git a/crates/codegraph-api/tests/api.rs b/crates/codegraph-api/tests/api.rs index d888bf94a..41d39c024 100644 --- a/crates/codegraph-api/tests/api.rs +++ b/crates/codegraph-api/tests/api.rs @@ -122,7 +122,10 @@ async fn search_flow_pattern_and_references() { let api = api(&db_str).await; // Pattern theo tên symbol. - let sf = api.search_flow_pattern(&format!("{caller}, {callee}")).await.unwrap(); + let sf = api + .search_flow_pattern(&format!("{caller}, {callee}")) + .await + .unwrap(); assert_eq!(sf.len(), 1); assert_eq!(sf[0].function_name, "caller"); diff --git a/crates/codegraph-bench/Cargo.toml b/crates/codegraph-bench/Cargo.toml new file mode 100644 index 000000000..c2ffeb2d9 --- /dev/null +++ b/crates/codegraph-bench/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "codegraph-bench" +version.workspace = true +edition = "2024" +license.workspace = true +repository.workspace = true +description = "Benchmark codegraph-extract + codegraph-graph trên các repo thật" + +[dependencies] +codegraph-extract = { path = "../codegraph-extract" } +codegraph-graph = { path = "../codegraph-graph" } +codegraph-core = { path = "../codegraph-core" } + +anyhow = { workspace = true } +camino = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +clap = { workspace = true } +criterion = "0.5" +# CodSpeed đo benchmark qua `codspeed-criterion-compat` (đo bằng hardware counters). +# Version này phải khớp CLI `cargo-codspeed` (CI cài bản mới nhất = 5.x). +codspeed-criterion-compat = { version = "5", optional = true } + +[dev-dependencies] +tempfile = "3" + +[features] +default = [] +# Bật `bloom-search` trong codegraph-graph — phase query thêm `search_flow`. +bloom = ["codegraph-graph/bloom-search"] +# Dùng `Criterion` của CodSpeed (runner hardware counters) thay vì criterion thường. +# Chạy: `cargo codspeed build -p codegraph-bench --features codspeed` rồi `cargo codspeed run`. +# Local vẫn chạy bình thường như criterion (no runner → passthrough). +codspeed = ["dep:codspeed-criterion-compat"] + +[[bench]] +name = "codspeed" +harness = false \ No newline at end of file diff --git a/crates/codegraph-bench/benches/codspeed.rs b/crates/codegraph-bench/benches/codspeed.rs new file mode 100644 index 000000000..640c68b38 --- /dev/null +++ b/crates/codegraph-bench/benches/codspeed.rs @@ -0,0 +1,153 @@ +//! CodSpeed bench: đo **extract → index → query** trên một danh sách repo thật. +//! +//! CodSpeed yêu cầu mỗi phase là một `bench_function` + `b.iter` chuẩn (runner +//! đo bằng hardware counters). Input là danh sách repo được nạp theo thứ tự: +//! +//! 1. env `CODEGRAPH_BENCH_REPOS_LIST` = file chứa 1 path repo mỗi dòng (CI ghi +//! ra file này từ `.github/benches/repos/sources.txt` bằng +//! `.github/benches/fetch_repos.sh`); nếu env được set nhưng file không đọc +//! được/trống → báo lỗi và không chạy (tránh benchmark nhầm input); +//! 2. không set env → tự bench `crates/` (fallback cho lần chạy local đầu tiên). +//! +//! Phải dùng `criterion_group!`/`criterion_main!` — **không** tự +//! `Criterion::default()`. Dưới `cargo codspeed build` (bật `cfg(codspeed)`) các +//! macro này gọi `Criterion::new_instrumented()` để nối với runner; còn +//! `Criterion::default()` trong compat là dummy (`codspeed: None`) nên +//! `benchmark_group` sẽ panic `non instrumented codspeed interface`. +//! +//! Chạy: +//! - CI: `cargo codspeed build -p codegraph-bench --features codspeed && cargo codspeed run` +//! - Local: `CODEGRAPH_BENCH_REPOS_LIST=repos.txt cargo bench -p codegraph-bench +//! --bench codspeed --features codspeed` — không có runner thì compat resolve về +//! criterion thường (wall-time). + +#[cfg(feature = "codspeed")] +use codspeed_criterion_compat as crit; +#[cfg(not(feature = "codspeed"))] +use criterion as crit; + +use camino::Utf8PathBuf; +use codegraph_bench::{ + BenchOptions, Repo, extract, index, orchestrator, run_queries, sample_query_names, +}; + +fn push_repo(out: &mut Vec, path: Utf8PathBuf) { + let name = path + .file_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| path.as_str().to_string()); + out.push(Repo { name, root: path }); +} + +fn load_repos() -> Vec { + let mut out = Vec::new(); + + // 1) Danh sách rõ ràng từ env (ưu tiên — CI dùng `CODEGRAPH_BENCH_REPOS_LIST`). + // Nếu env được set mà file không đọc được / trống → đây là lỗi cấu hình, KHÔNG + // rơi vào fallback `crates` (tránh benchmark nhầm input trong CI). + if let Ok(list_file) = std::env::var("CODEGRAPH_BENCH_REPOS_LIST") { + match std::fs::read_to_string(&list_file) { + Ok(body) => { + for line in body.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + push_repo(&mut out, Utf8PathBuf::from(line)); + } + if out.is_empty() { + eprintln!( + "Cảnh báo: {list_file} không chứa repo nào (rỗng/comment) — bỏ qua benchmark." + ); + } + } + Err(e) => { + eprintln!( + "Lỗi: không đọc được CODEGRAPH_BENCH_REPOS_LIST={list_file}: {e}. \ + Bỏ qua benchmark thay vì benchmark nhầm input." + ); + return out; // rỗng → benchmark_all in message và thoát. + } + } + return out; + } + + // 2) Fallback cuối: tự bench source của workspace — chỉ khi env KHÔNG được set + // (lần chạy local đầu tiên). + if out.is_empty() { + push_repo(&mut out, Utf8PathBuf::from("crates")); + } + out +} + +/// Đăng ký toàn bộ bench (extract/index/query) theo danh sách repo. +/// Được `crit::criterion_main!` gọi với Criterion đã instrumented. +fn benchmark_all(c: &mut crit::Criterion) { + let opts = BenchOptions { + langs: None, + queries: 200, + with_flow: false, + }; + let repos = load_repos(); + if repos.is_empty() { + eprintln!("Không tìm thấy repo nào để bench (đặt CODEGRAPH_BENCH_REPOS_LIST)"); + return; + } + + for repo in &repos { + let name = repo.name.clone(); + let orch = orchestrator(&opts); + + // extract: walk + parse lại trong mỗi iteration (đo trọn phase). + { + let mut g = c.benchmark_group(format!("{name}/extract")); + let orch = &orch; + let root = repo.root.clone(); + g.bench_function("walk+parse", |b| { + b.iter(|| { + let _ = std::hint::black_box(extract(orch, &root)); + }); + }); + } + + // Parse một lần để dùng chung cho index + query (không đếm lại extract). + let parsed = match extract(&orch, &repo.root) { + Ok((p, _)) => p, + Err(e) => { + eprintln!("[{name}] extract failed: {e}; skip"); + continue; + } + }; + let names = sample_query_names(&parsed, opts.queries); + + // index: dựng GraphIndex in-memory + ingest toàn bộ parsed. + { + let mut g = c.benchmark_group(format!("{name}/index")); + let parsed = &parsed; + g.bench_function("ingest", |b| { + b.iter(|| { + let _ = std::hint::black_box(index(parsed)); + }); + }); + } + + // query: bộ truy vấn mẫu (search_symbol + callees + flow) trên index đã dựng. + if let Ok(idx) = index(&parsed) { + let mut g = c.benchmark_group(format!("{name}/query")); + let names = &names; + let with_flow = opts.with_flow; + g.bench_function("sample", |b| { + b.iter(|| { + let _ = std::hint::black_box(run_queries(&idx, names, with_flow)); + }); + }); + } else { + eprintln!("[{name}] index failed; skip query"); + } + } +} + +// `criterion_main!` dưới CodSpeed gọi `new_instrumented()`; local (không +// `cfg(codspeed)`) resolve sang criterion thường. +crit::criterion_group!(benches, benchmark_all); +crit::criterion_main!(benches); diff --git a/crates/codegraph-bench/src/lib.rs b/crates/codegraph-bench/src/lib.rs new file mode 100644 index 000000000..75a344ae9 --- /dev/null +++ b/crates/codegraph-bench/src/lib.rs @@ -0,0 +1,182 @@ +//! Pipeline đo chuẩn: **extract** (codegraph-extract: walk + parse) → **index** +//! (codegraph-graph: `ingest`) → **query** (search_symbol / callees / flow trên +//! index đã dựng). Tách riêng 2 crate để benchmark biết chi phí mỗi bên. +//! +//! `main.rs` lướt CLI (danh sách repo) + chạy Criterion; còn các hàm phase ở đây +//! được integration test dùng mà không cần Criterion. + +use std::sync::OnceLock; +use std::time::{Duration, Instant}; + +use camino::Utf8Path; +use codegraph_core::{Error, SymbolKind}; +use codegraph_extract::{ExtractStats, Orchestrator}; +use codegraph_graph::{GraphIndex, ParseResult}; +use tokio::runtime::Runtime; + +/// Cấu hình một lần benchmark. +#[derive(Debug, Clone)] +pub struct BenchOptions { + /// Giới hạn parser theo tên ngôn ngữ (`None` = trọn registry). + pub langs: Option>, + /// Số symbol lấy mẫu cho phase query. + pub queries: usize, + /// Thêm `search_flow` (radix) vào phase query — để so bloom on/off. + pub with_flow: bool, +} + +impl Default for BenchOptions { + fn default() -> Self { + Self { + langs: None, + queries: 200, + with_flow: false, + } + } +} + +/// Một repo cần benchmark. +#[derive(Debug, Clone)] +pub struct Repo { + pub name: String, + pub root: camino::Utf8PathBuf, +} + +/// Kết quả đo 1 repo (counts + thời gian mỗi phase). +#[derive(Debug, Default, Clone, serde::Serialize)] +pub struct RepoTimes { + pub files: u64, + pub symbols: u64, + pub chains: u64, + pub calls: u64, + pub skipped: u64, + pub extract_ms: f64, + pub index_ms: f64, + pub query_ms: f64, + pub query_ops: usize, + pub flow: bool, +} + +fn runtime() -> &'static Runtime { + static RT: OnceLock = OnceLock::new(); + RT.get_or_init(|| { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("dựng tokio runtime") + }) +} + +/// Dựng `Orchestrator` theo `--langs` (None = registry đầy đủ). +pub fn orchestrator(opts: &BenchOptions) -> Orchestrator { + match &opts.langs { + Some(langs) => { + let names: Vec<&str> = langs.iter().map(String::as_str).collect(); + let parsers: Vec<_> = codegraph_extract::registry() + .into_iter() + .filter(|p| names.iter().any(|n| *n == p.name())) + .collect(); + Orchestrator::new(parsers) + } + None => Orchestrator::with_registry(), + } +} + +/// Phase extract: walk + parse, trả `(parsed, stats)` — không ingest. +pub fn extract( + orch: &Orchestrator, + root: &Utf8Path, +) -> Result<(Vec, ExtractStats), Error> { + orch.parse_project(root) +} + +/// Phase index: dựng in-memory `GraphIndex` + `ingest` toàn bộ parsed. +pub fn index(parsed: &[ParseResult]) -> Result { + runtime().block_on(async { + let mut idx = GraphIndex::in_memory(); + idx.ingest(parsed).await?; + Ok(idx) + }) +} + +/// Lấy mẫu `n` tên function/method (sorted + dedup) để query. +pub fn sample_query_names(parsed: &[ParseResult], n: usize) -> Vec { + let mut names: Vec = parsed + .iter() + .flat_map(|p| p.symbols.iter()) + .filter(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method)) + .map(|s| s.name.clone()) + .filter(|n| !n.is_empty()) + .collect(); + names.sort(); + names.dedup(); + names.truncate(n.max(1)); + names +} + +/// Phase query: chạy bộ truy vấn mẫu trên index đã dựng; trả `(số phép đo, tổng +/// thời gian)`. Mỗi tên: `search_symbol` → hit đầu → `callees` + `flow` (+ +/// `search_flow` nếu `with_flow`). +pub fn run_queries( + idx: &GraphIndex, + names: &[String], + with_flow: bool, +) -> Result<(usize, Duration), Error> { + runtime().block_on(async { + let start = Instant::now(); + let mut ops = 0usize; + for name in names { + if let Ok(hits) = idx.search_symbol(name, None, 5).await { + ops += 1; + let Some(h) = hits.first() else { continue }; + // callees + flow = 2 phép đọc chain engine + flow. + let _ = idx.callees(h.id).await; + ops += 1; + let _ = idx.flow(h.id).await; + ops += 1; + if with_flow { + let _ = idx.search_flow(&[h.id]).await; + ops += 1; + } + } + } + Ok((ops, start.elapsed())) + }) +} + +/// Chạy 3 phase trong 1 pass, trả `RepoTimes` (dùng cho bảng + JSON). +pub fn measure_repo( + orch: &Orchestrator, + opts: &BenchOptions, + repo: &Repo, +) -> anyhow::Result { + let t0 = Instant::now(); + let (parsed, stats) = orch.parse_project(&repo.root)?; + let extract_ms = ms(t0); + + let t1 = Instant::now(); + let idx = index(&parsed)?; + let index_ms = ms(t1); + + let names = sample_query_names(&parsed, opts.queries); + let t2 = Instant::now(); + let (ops, _) = run_queries(&idx, &names, opts.with_flow)?; + let query_ms = ms(t2); + + Ok(RepoTimes { + files: stats.files, + symbols: stats.symbols, + chains: stats.chains, + calls: stats.calls, + skipped: stats.skipped, + extract_ms, + index_ms, + query_ms, + query_ops: ops, + flow: opts.with_flow, + }) +} + +fn ms(t: Instant) -> f64 { + t.elapsed().as_secs_f64() * 1e3 +} diff --git a/crates/codegraph-bench/src/main.rs b/crates/codegraph-bench/src/main.rs new file mode 100644 index 000000000..7dc624f89 --- /dev/null +++ b/crates/codegraph-bench/src/main.rs @@ -0,0 +1,267 @@ +//! Benchmark CLI: đưa danh sách folder repo → đo extract/index/query. +//! +//! Chạy: `cargo run -p codegraph-bench -- /path/to/repo1 /path/to/repo2` +//! Danh sách: `cargo run -p codegraph-bench -- --file repos.txt` +//! Bloom: `cargo run -p codegraph-bench --features bloom -- --flow /path/to/repo` + +use std::time::Duration; + +use camino::Utf8PathBuf; +use clap::Parser; +use codegraph_bench::{ + BenchOptions, Repo, RepoTimes, extract, index, orchestrator, run_queries, sample_query_names, +}; + +#[derive(Parser)] +#[command( + name = "codegraph-bench", + about = "Benchmark codegraph-extract + codegraph-graph trên các repo thật" +)] +struct Cli { + /// Folder repo cần benchmark (nhiều được). + #[arg(value_name = "REPO")] + repos: Vec, + + /// File chứa danh sách repo (mỗi dòng 1 path, trống + `#` bị bỏ qua). + #[arg(short, long)] + file: Option, + + /// Giới hạn ngôn ngữ: danh sách tách bằng phẩy, VD `rust,go`. + #[arg(long)] + langs: Option, + + /// Số symbol lấy mẫu cho phase query. + #[arg(long, default_value_t = 200)] + queries: usize, + + /// Chạy `search_flow` (radix) trong phase query — build kèm `--features bloom`. + #[arg(long)] + flow: bool, + + /// Chỉ in bảng + JSON, bỏ qua Criterion statistical pass. + #[arg(long)] + no_criterion: bool, + + /// Xuất JSON thay cho bảng. + #[arg(long)] + json: bool, + + /// Criterion: số sample (Criterion tối thiểu 10). + #[arg(long, default_value_t = 10)] + sample_size: usize, + + /// Criterion: thời gian warm-up (giây). + #[arg(long, default_value_t = 0.5)] + warmup: f64, + + /// Criterion: thời gian đo (giây). + #[arg(long, default_value_t = 1.0)] + measure: f64, +} + +#[derive(serde::Serialize)] +struct RepoJson { + repo: String, + #[serde(flatten)] + times: RepoTimes, +} + +#[derive(serde::Serialize, Default)] +struct TotalsJson { + repos: usize, + files: u64, + symbols: u64, + extract_ms: f64, + index_ms: f64, + query_ms: f64, +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + let repos = load_repos(&cli)?; + if repos.is_empty() { + anyhow::bail!("Không có repo nào — truyền folder hoặc dùng --file with danh sách"); + } + + let opts = BenchOptions { + langs: cli.langs.as_ref().map(|s| { + s.split(',') + .map(|t| t.trim().to_string()) + .filter(|t| !t.is_empty()) + .collect() + }), + queries: cli.queries, + with_flow: cli.flow, + }; + let orch = orchestrator(&opts); + + // ── Pass 1: bảng + JSON (đo 1 pass mỗi repo). ── + let mut rows: Vec<(Repo, RepoTimes)> = Vec::new(); + for r in &repos { + print!("{} ... ", r.name); + std::io::Write::flush(&mut std::io::stdout())?; + let t = codegraph_bench::measure_repo(&orch, &opts, r)?; + println!( + "extract {:7.1}ms index {:7.1}ms query {:6.1}ms", + t.extract_ms, t.index_ms, t.query_ms + ); + rows.push((r.clone(), t)); + } + render_table(&rows); + + // ── Pass 2: Criterion statistical per phase. ── + if !cli.no_criterion { + criterion_pass(&repos, &opts, &cli); + } + + if cli.json { + render_json(&rows); + } + Ok(()) +} + +fn load_repos(cli: &Cli) -> anyhow::Result> { + let mut out = Vec::new(); + if let Some(file) = &cli.file { + for line in std::fs::read_to_string(file)?.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + push_repo(&mut out, line); + } + } + for p in &cli.repos { + push_repo(&mut out, p); + } + Ok(out) +} + +fn push_repo(out: &mut Vec, path: &str) { + let root = Utf8PathBuf::from(path); + let name = root + .file_name() + .map(|s| s.to_string()) + .unwrap_or_else(|| path.to_string()); + out.push(Repo { name, root }); +} + +fn render_table(rows: &[(Repo, RepoTimes)]) { + println!("\n── Summary ──"); + println!( + "{:<24} {:>8} {:>8} {:>8} {:>8} {:>10} {:>10} {:>10} {:>8}", + "repo", "files", "symbols", "chains", "calls", "extract_ms", "index_ms", "query_ms", "ops" + ); + let mut total_files = 0u64; + let mut total_symbols = 0u64; + let mut total_extract = 0f64; + let mut total_index = 0f64; + let mut total_query = 0f64; + for (r, t) in rows { + total_files += t.files; + total_symbols += t.symbols; + total_extract += t.extract_ms; + total_index += t.index_ms; + total_query += t.query_ms; + println!( + "{:<24} {:>8} {:>8} {:>8} {:>8} {:>10.1} {:>10.1} {:>10.1} {:>8}", + r.name, + t.files, + t.symbols, + t.chains, + t.calls, + t.extract_ms, + t.index_ms, + t.query_ms, + t.query_ops + ); + } + println!( + "{:<24} {:>8} {:>8} {:>8} {:>8} {:>10.1} {:>10.1} {:>10.1}", + "TOTAL", total_files, total_symbols, "-", "-", total_extract, total_index, total_query + ); + println!(); +} + +fn render_json(rows: &[(Repo, RepoTimes)]) { + let mut total = TotalsJson::default(); + let items: Vec = rows + .iter() + .map(|(r, t)| { + total.repos += 1; + total.files += t.files; + total.symbols += t.symbols; + total.extract_ms += t.extract_ms; + total.index_ms += t.index_ms; + total.query_ms += t.query_ms; + RepoJson { + repo: r.name.clone(), + times: t.clone(), + } + }) + .collect(); + let out = serde_json::json!({ "repos": items, "total": total }); + println!("{}", serde_json::to_string_pretty(&out).unwrap()); +} + +/// Pass Criterion: per-phase statistical trên cùng repos. +fn criterion_pass(repos: &[Repo], opts: &BenchOptions, cli: &Cli) { + let sample_size = cli.sample_size.max(10); + // Criterion đòi duration dương — clamp để tránh 0/âm. + let warmup = Duration::from_secs_f64(cli.warmup.max(0.01)); + let measure = Duration::from_secs_f64(cli.measure.max(0.01)); + let mut c = criterion::Criterion::default() + .sample_size(sample_size) + .warm_up_time(warmup) + .measurement_time(measure); + + for repo in repos { + // Stage parsed một lần rồi dùng cho cả index + query (không parse lại). + let orch = orchestrator(opts); + let name = repo.name.clone(); + // extract. + { + let mut g = c.benchmark_group(format!("{name}/extract")); + let orch = &orch; + let root = repo.root.clone(); + g.bench_function("walk+parse", |b| { + b.iter(|| { + let _ = std::hint::black_box(extract(orch, &root)); + }); + }); + } + let parsed = match extract(&orch, &repo.root) { + Ok((p, _)) => p, + Err(e) => { + eprintln!("[criterion] {name}: extract failed: {e}; skip"); + continue; + } + }; + let names = sample_query_names(&parsed, opts.queries); + // index. + { + let mut g = c.benchmark_group(format!("{name}/index")); + let parsed = &parsed; + g.bench_function("ingest", |b| { + b.iter(|| { + let _ = std::hint::black_box(index(parsed)); + }); + }); + } + // query. + { + let mut g = c.benchmark_group(format!("{name}/query")); + let names = &names; + let with_flow = opts.with_flow; + if let Ok(idx) = index(&parsed) { + g.bench_function("sample", |b| { + b.iter(|| { + let _ = std::hint::black_box(run_queries(&idx, names, with_flow)); + }); + }); + } else { + eprintln!("[criterion] {name}: index failed; skip query"); + } + } + } +} diff --git a/crates/codegraph-bench/tests/pipeline.rs b/crates/codegraph-bench/tests/pipeline.rs new file mode 100644 index 000000000..c52172674 --- /dev/null +++ b/crates/codegraph-bench/tests/pipeline.rs @@ -0,0 +1,71 @@ +//! Integration test: chạy pipeline extract → index → query trên 1 fixture repo +//! tạm, khẳng định đủ 3 phase chạy được, trả số liệu hợp lệ (không cần Criterion). + +use std::io::Write; + +use camino::Utf8PathBuf; +use codegraph_bench::{BenchOptions, orchestrator, sample_query_names}; + +/// Dựng fixture repo temp với vài ngôn ngữ, trả `(dir, root)`. +fn fixture() -> (tempfile::TempDir, Utf8PathBuf) { + let dir = tempfile::tempdir().unwrap(); + let root = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + let write = |rel: &str, content: &str| { + let path = root.join(rel); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent.as_std_path()).unwrap(); + } + let mut f = std::fs::File::create(path.as_std_path()).unwrap(); + f.write_all(content.as_bytes()).unwrap(); + }; + write( + "src/lib.rs", + "pub fn add(a: i32, b: i32) -> i32 { a + b }\npub fn sub(a: i32, b: i32) -> i32 { a - b }\n", + ); + write( + "main.go", + "package main\nfunc greet(name string) string { return \"hi \" + name }\nfunc run() { _ = greet(\"x\") }\n", + ); + write( + "app.py", + "def hello(who):\n return f\"hi {who}\"\n\ndef main():\n print(hello(\"world\"))\n", + ); + (dir, root) +} + +#[test] +fn pipeline_runs_all_three_phases() { + let (_dir, root) = fixture(); + let opts = BenchOptions::default(); + let orch = orchestrator(&opts); + let repo = codegraph_bench::Repo { + name: "fixture".into(), + root, + }; + + let times = codegraph_bench::measure_repo(&orch, &opts, &repo).unwrap(); + + assert!( + times.files >= 3, + "phải parse được 3 file, thực tế {}", + times.files + ); + assert!(times.symbols > 0); + assert!(times.extract_ms >= 0.0); + assert!(times.index_ms >= 0.0); + assert!(times.query_ms >= 0.0); + // Có function/method để query → ít nhất 1 phép đã chạy. + assert!(times.query_ops > 0, "query phase phải chạy ≥1 phép"); +} + +#[test] +fn sample_query_names_returns_function_names() { + let (_dir, root) = fixture(); + let orch = orchestrator(&BenchOptions::default()); + let (parsed, _) = orch.parse_project(&root).unwrap(); + let names = sample_query_names(&parsed, 100); + // add/sub/greet/hello/main là function — nên có trong danh sách mẫu. + assert!(names.contains(&"add".to_string()), "names={names:?}"); + assert!(names.contains(&"greet".to_string())); + assert!(names.len() <= 100); +} diff --git a/crates/codegraph-core/src/drafts.rs b/crates/codegraph-core/src/drafts.rs deleted file mode 100644 index bec08c941..000000000 --- a/crates/codegraph-core/src/drafts.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Draft types + stats written to/read from the persistent graph store. -//! -//! Moved here from the removed `codegraph-db` crate so extraction (writers), -//! resolution, and CLI tooling can construct/index rows without depending on a -//! specific storage backend. The `Db` implementation that persists these lives -//! in `codegraph-graph::db`. - -use crate::{EdgeKind, NodeKind}; -use camino::Utf8PathBuf; -use serde::{Deserialize, Serialize}; - -/// A file row as stored in the graph store. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileRow { - pub id: Option, - pub path: Utf8PathBuf, - pub language: String, - pub sha256: String, - pub size: u64, - pub mtime: i64, - pub indexed_at: i64, -} - -/// A node to be inserted — id is assigned by the store. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NodeDraft { - pub kind: NodeKind, - pub name: String, - pub qualified_name: Option, - pub start_line: u32, - pub end_line: u32, - pub signature: Option, - pub docstring: Option, - pub language: String, -} - -/// An edge to be inserted — endpoints are existing node ids. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EdgeDraft { - pub from_id: i64, - pub to_id: i64, - pub kind: EdgeKind, - pub file_id: Option, - pub line: Option, - pub source: Option, // e.g. "framework:express", "resolver:imports" -} - -/// Aggregate counts reported by the store (`/api/status`, `codegraph status`). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DbStats { - pub files: u64, - pub nodes: u64, - pub edges: u64, - pub size_bytes: u64, - pub schema_version: u32, -} diff --git a/crates/codegraph-core/src/error.rs b/crates/codegraph-core/src/error.rs index da0fda5ed..63e436ce9 100644 --- a/crates/codegraph-core/src/error.rs +++ b/crates/codegraph-core/src/error.rs @@ -18,6 +18,8 @@ pub enum Error { Invalid(String), #[error("not initialized: run `codegraph init` first")] NotInitialized, + #[error("link failed — no mock configured for callee(s): {}", .0.join(", "))] + MissingMocks(Vec), #[error("{0}")] Other(String), } diff --git a/crates/codegraph-core/src/kinds.rs b/crates/codegraph-core/src/kinds.rs deleted file mode 100644 index 9af00fd2f..000000000 --- a/crates/codegraph-core/src/kinds.rs +++ /dev/null @@ -1,160 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::str::FromStr; - -/// Lỗi parse kind từ chuỗi không hợp lệ. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct InvalidKind(pub String); - -impl std::fmt::Display for InvalidKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "invalid kind: {}", self.0) - } -} - -impl std::error::Error for InvalidKind {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum NodeKind { - File, - Module, - Class, - Struct, - Interface, - Trait, - Protocol, - Function, - Method, - Property, - Field, - Variable, - Constant, - Enum, - EnumMember, - TypeAlias, - Namespace, - Parameter, - Import, - Export, - Route, - Component, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum EdgeKind { - Contains, - Calls, - Imports, - Exports, - Extends, - Implements, - References, - TypeOf, - Returns, - Instantiates, - Overrides, - Decorates, -} - -impl NodeKind { - pub fn as_str(self) -> &'static str { - match self { - Self::File => "file", - Self::Module => "module", - Self::Class => "class", - Self::Struct => "struct", - Self::Interface => "interface", - Self::Trait => "trait", - Self::Protocol => "protocol", - Self::Function => "function", - Self::Method => "method", - Self::Property => "property", - Self::Field => "field", - Self::Variable => "variable", - Self::Constant => "constant", - Self::Enum => "enum", - Self::EnumMember => "enum_member", - Self::TypeAlias => "type_alias", - Self::Namespace => "namespace", - Self::Parameter => "parameter", - Self::Import => "import", - Self::Export => "export", - Self::Route => "route", - Self::Component => "component", - } - } -} - -impl FromStr for NodeKind { - type Err = InvalidKind; - - fn from_str(s: &str) -> Result { - Ok(match s { - "file" => Self::File, - "module" => Self::Module, - "class" => Self::Class, - "struct" => Self::Struct, - "interface" => Self::Interface, - "trait" => Self::Trait, - "protocol" => Self::Protocol, - "function" => Self::Function, - "method" => Self::Method, - "property" => Self::Property, - "field" => Self::Field, - "variable" => Self::Variable, - "constant" => Self::Constant, - "enum" => Self::Enum, - "enum_member" => Self::EnumMember, - "type_alias" => Self::TypeAlias, - "namespace" => Self::Namespace, - "parameter" => Self::Parameter, - "import" => Self::Import, - "export" => Self::Export, - "route" => Self::Route, - "component" => Self::Component, - _ => return Err(InvalidKind(s.to_string())), - }) - } -} - -impl EdgeKind { - pub fn as_str(self) -> &'static str { - match self { - Self::Contains => "contains", - Self::Calls => "calls", - Self::Imports => "imports", - Self::Exports => "exports", - Self::Extends => "extends", - Self::Implements => "implements", - Self::References => "references", - Self::TypeOf => "type_of", - Self::Returns => "returns", - Self::Instantiates => "instantiates", - Self::Overrides => "overrides", - Self::Decorates => "decorates", - } - } -} - -impl FromStr for EdgeKind { - type Err = InvalidKind; - - fn from_str(s: &str) -> Result { - Ok(match s { - "contains" => Self::Contains, - "calls" => Self::Calls, - "imports" => Self::Imports, - "exports" => Self::Exports, - "extends" => Self::Extends, - "implements" => Self::Implements, - "references" => Self::References, - "type_of" => Self::TypeOf, - "returns" => Self::Returns, - "instantiates" => Self::Instantiates, - "overrides" => Self::Overrides, - "decorates" => Self::Decorates, - _ => return Err(InvalidKind(s.to_string())), - }) - } -} diff --git a/crates/codegraph-core/src/lib.rs b/crates/codegraph-core/src/lib.rs index a81715a36..48458a569 100644 --- a/crates/codegraph-core/src/lib.rs +++ b/crates/codegraph-core/src/lib.rs @@ -3,21 +3,16 @@ //! Model cũ (`Node`/`Edge`/`NodeKind`/`EdgeKind`) đang dần bị thay bằng model //! semgraph (`semgraph` module) — wire breaking đã chốt ở plan. -pub mod drafts; -pub mod error; -pub mod kinds; -pub mod model; -pub mod semgraph; +mod error; +mod semgraph; -pub use drafts::{DbStats, EdgeDraft, FileRow, NodeDraft}; pub use error::{Error, Result}; -pub use kinds::{EdgeKind, InvalidKind, NodeKind}; -pub use model::{Edge, Node, NodeId}; pub use semgraph::{ - is_marker, marker_id, marker_name, Annotation, CallRecord, CallSite, CallSiteResult, - ClassInfo, DbStats as SemgraphStats, Dependency, DependenciesReport, EdgeMeta, EffectType, - FileInfo, FlowCall, FlowResult, FunctionScope, MemberInfo, ResolveResult, ScopeLevel, - SearchFlowResult, Symbol, SymbolId, SymbolKind, SymbolMatch, MARKER_BRANCH_END, MARKER_BREAK, - MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, MARKER_REC_CALL, - MARKER_RETURN, MARKER_SWITCH_CASE, MARKER_SWITCH_END, MARKER_THROW, SYMBOL_BASE, + is_marker, marker_id, marker_name, Annotation, CallRecord, CallSite, CallSiteResult, ClassInfo, + DbStats as SemgraphStats, DependenciesReport, Dependency, EdgeMeta, EffectCallPattern, + EffectRule, EffectType, FileInfo, FlowCall, FlowResult, FunctionScope, MemberInfo, + ResolveResult, ScopeLevel, SearchFlowResult, Symbol, SymbolId, SymbolKind, SymbolMatch, + MARKER_BRANCH_END, MARKER_BREAK, MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, + MARKER_LOOP_BACK, MARKER_REC_CALL, MARKER_RETURN, MARKER_SWITCH_CASE, MARKER_SWITCH_END, + MARKER_THROW, SYMBOL_BASE, }; diff --git a/crates/codegraph-core/src/model.rs b/crates/codegraph-core/src/model.rs deleted file mode 100644 index 76952ca00..000000000 --- a/crates/codegraph-core/src/model.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{EdgeKind, NodeKind}; -use camino::Utf8PathBuf; -use serde::{Deserialize, Serialize}; - -pub type NodeId = i64; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Node { - pub id: NodeId, - pub kind: NodeKind, - pub name: String, - pub qualified_name: Option, - pub file: Utf8PathBuf, - pub start_line: u32, - pub end_line: u32, - pub signature: Option, - pub docstring: Option, - pub language: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Edge { - pub from: NodeId, - pub to: NodeId, - pub kind: EdgeKind, - pub file: Option, - pub line: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source: Option, -} diff --git a/crates/codegraph-core/src/semgraph.rs b/crates/codegraph-core/src/semgraph.rs index 8e30cbfb1..47ecd105a 100644 --- a/crates/codegraph-core/src/semgraph.rs +++ b/crates/codegraph-core/src/semgraph.rs @@ -19,26 +19,37 @@ pub const SYMBOL_BASE: u64 = 100; /// Marker: bắt đầu loop body. pub const MARKER_LOOP: u64 = 1; + /// Marker: recursive call (gọi lại chính function đang xét) — dự trữ. pub const MARKER_REC_CALL: u64 = 2; + /// Marker: nhánh khi điều kiện đúng. pub const MARKER_IF_TRUE: u64 = 3; + /// Marker: nhánh khi điều kiện sai. pub const MARKER_IF_FALSE: u64 = 4; + /// Marker: kết thúc một nhánh if/else. pub const MARKER_BRANCH_END: u64 = 5; + /// Marker: return statement. pub const MARKER_RETURN: u64 = 6; + /// Marker: loop back edge (quay lại đầu loop). pub const MARKER_LOOP_BACK: u64 = 7; + /// Marker: case trong switch. pub const MARKER_SWITCH_CASE: u64 = 8; + /// Marker: kết thúc switch. pub const MARKER_SWITCH_END: u64 = 9; + /// Marker: break statement. pub const MARKER_BREAK: u64 = 10; + /// Marker: continue statement. pub const MARKER_CONTINUE: u64 = 11; + /// Marker: throw/raise exception. pub const MARKER_THROW: u64 = 12; @@ -195,6 +206,46 @@ impl EffectType { Self::Log => "log", } } + + /// Parse snake_case string (case-insensitive) ngược lại thành `EffectType`. + /// Trùng giá trị `as_str()` của từng variant; chuỗi không biết → `None`. + pub fn parse(s: &str) -> Option { + Some(match s.trim().to_ascii_lowercase().as_str() { + "none" => Self::None, + "sql_query" => Self::SqlQuery, + "sql_write" => Self::SqlWrite, + "cache_read" => Self::CacheRead, + "cache_write" => Self::CacheWrite, + "http_call" => Self::HttpCall, + "event_emit" => Self::EventEmit, + "file_read" => Self::FileRead, + "file_write" => Self::FileWrite, + "log" => Self::Log, + _ => return None, + }) + } +} + +/// Match pattern của một effect rule — schema chung cho `config.toml` +/// (`[[effect_rules]]`) dùng bởi cả `codegraph-extract` (classify lúc parse) +/// và `codegraph-sboxes` (Piece 3: state delta theo effect). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum EffectCallPattern { + /// Tên call bắt đầu bằng chuỗi (`call = { prefix = "db." }`). + Prefix { prefix: String }, + /// Tên call chứa chuỗi ở bất kỳ đâu. + Contains { contains: String }, + /// Tên call khớp chính xác (case-sensitive). + Exact { exact: String }, +} + +/// Một effect rule từ `[[effect_rules]]` trong config.toml. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct EffectRule { + #[serde(rename = "call")] + pub call: EffectCallPattern, + pub effect: EffectType, } // ==================== Entities ==================== @@ -412,6 +463,7 @@ pub struct MemberInfo { pub name: String, pub kind: SymbolKind, pub line: u32, + /// Dòng khai báo đầu tiên (VD `getOrders(userId int) (Order, error)`). #[serde(skip_serializing_if = "Option::is_none")] pub signature: Option, @@ -539,4 +591,29 @@ mod tests { fn effect_type_default_is_none() { assert_eq!(EffectType::default(), EffectType::None); } + + #[test] + fn effect_type_parse_round_trips_as_str() { + for e in [ + EffectType::None, + EffectType::SqlQuery, + EffectType::SqlWrite, + EffectType::CacheRead, + EffectType::CacheWrite, + EffectType::HttpCall, + EffectType::EventEmit, + EffectType::FileRead, + EffectType::FileWrite, + EffectType::Log, + ] { + assert_eq!(EffectType::parse(e.as_str()), Some(e)); + } + // Case-insensitive + trim. + assert_eq!( + EffectType::parse(" SQL_QUERY "), + Some(EffectType::SqlQuery) + ); + assert_eq!(EffectType::parse("sql_query"), Some(EffectType::SqlQuery)); + assert_eq!(EffectType::parse("bogus"), None); + } } diff --git a/crates/codegraph-extract/Cargo.toml b/crates/codegraph-extract/Cargo.toml index 7ea3cb127..f1bc6a95d 100644 --- a/crates/codegraph-extract/Cargo.toml +++ b/crates/codegraph-extract/Cargo.toml @@ -10,7 +10,7 @@ warnings = "deny" [dependencies] codegraph-core = { path = "../codegraph-core" } -codegraph-graph = { path = "../codegraph-graph" } +codegraph-graph = { path = "../codegraph-graph", features = ["sqlite"] } tree-sitter = { workspace = true } tree-sitter-typescript = { workspace = true, optional = true } tree-sitter-javascript = { workspace = true, optional = true } diff --git a/crates/codegraph-extract/examples/dump_tree.rs b/crates/codegraph-extract/examples/dump_tree.rs index 874a6177c..6652b2404 100644 --- a/crates/codegraph-extract/examples/dump_tree.rs +++ b/crates/codegraph-extract/examples/dump_tree.rs @@ -5,7 +5,9 @@ use codegraph_extract::registry; use std::io::Read; fn main() { - let lang = std::env::args().nth(1).expect("usage: dump_tree "); + let lang = std::env::args() + .nth(1) + .expect("usage: dump_tree "); let mut src = String::new(); std::io::stdin().read_to_string(&mut src).unwrap(); @@ -35,14 +37,19 @@ fn print_sexp(node: &tree_sitter::Node, src: &str, depth: usize) { .utf8_text(src.as_bytes()) .ok() .map(|t| t.replace('\n', "\\n")) - .map(|t| if t.len() > 60 { format!("{}…", &t[..60]) } else { t }); + .map(|t| { + if t.len() > 60 { + format!("{}…", &t[..60]) + } else { + t + } + }); println!( "{indent}{}{}{}{}", node.kind(), field, if node.is_named() { "" } else { " !" }, - text.map(|t| format!(" \"{t}\"")) - .unwrap_or_default() + text.map(|t| format!(" \"{t}\"")).unwrap_or_default() ); let mut cursor = node.walk(); for ch in node.children(&mut cursor) { diff --git a/crates/codegraph-extract/examples/smoke.rs b/crates/codegraph-extract/examples/smoke.rs index e4f48d922..0bd50fbe3 100644 --- a/crates/codegraph-extract/examples/smoke.rs +++ b/crates/codegraph-extract/examples/smoke.rs @@ -13,7 +13,9 @@ fn main() { std::process::exit(1); }); let mut src = String::new(); - std::io::stdin().read_to_string(&mut src).expect("read stdin"); + std::io::stdin() + .read_to_string(&mut src) + .expect("read stdin"); let parser = registry() .into_iter() @@ -25,12 +27,7 @@ fn main() { for s in &res.symbols { println!( " {:<4} {:<28} {:?} {:?} scope={} L{}", - s.id, - s.name, - s.kind, - s.scope, - s.scope_id, - s.line + s.id, s.name, s.kind, s.scope, s.scope_id, s.line ); } println!("== chains ({}) ==", res.chains.len()); @@ -57,11 +54,6 @@ fn main() { } println!("== calls ({}) ==", res.calls.len()); for c in &res.calls { - println!( - " L{:<3} {} (effect={:?})", - c.line, - c.call_name, - c.effect - ); + println!(" L{:<3} {} (effect={:?})", c.line, c.call_name, c.effect); } } diff --git a/crates/codegraph-extract/src/config.rs b/crates/codegraph-extract/src/config.rs index c82f7d18d..17f44f63f 100644 --- a/crates/codegraph-extract/src/config.rs +++ b/crates/codegraph-extract/src/config.rs @@ -1,4 +1,6 @@ +use crate::languages::effects::EffectClassifier; use camino::Utf8Path; +use codegraph_core::{EffectCallPattern, EffectRule, EffectType}; use serde::Deserialize; use std::fs; @@ -16,6 +18,9 @@ pub enum HeaderLanguage { struct ConfigFile { #[serde(default)] languages: LanguagesSection, + /// Project extra effect rules — xét trước bảng default (override). + #[serde(default)] + effect_rules: Vec, } #[derive(Debug, Default, Deserialize)] @@ -25,10 +30,21 @@ struct LanguagesSection { headers: Option, } +/// Raw rule — `effect` để string để rule lỗi (unknown) bị skip + warn, không +/// làm hỏng toàn bộ config; parse lại bằng `EffectType::parse`. +#[derive(Debug, Deserialize)] +struct EffectRuleRaw { + #[serde(rename = "call")] + call: EffectCallPattern, + effect: String, +} + /// Project-level extraction settings (`.codegraph/config.toml`). #[derive(Debug, Clone, Default)] pub struct ExtractConfig { pub header_language: HeaderLanguage, + /// Classifier effect của project — config rules override bảng default. + pub effect_classifier: EffectClassifier, } impl ExtractConfig { @@ -46,10 +62,30 @@ impl ExtractConfig { }; Self { header_language: parse_header_language(file.languages.headers.as_deref()), + effect_classifier: build_classifier(file.effect_rules), } } } +/// Setup rule config → skip rule effect unknown (warn) + giữ phần còn lại. +fn build_classifier(raw: Vec) -> EffectClassifier { + let mut rules = Vec::with_capacity(raw.len()); + for r in raw { + let Some(effect) = EffectType::parse(&r.effect) else { + tracing::warn!( + "[[effect_rules]]: unknown effect `{}`, rule ignored", + r.effect + ); + continue; + }; + rules.push(EffectRule { + call: r.call, + effect, + }); + } + EffectClassifier::with_config(rules) +} + fn parse_header_language(raw: Option<&str>) -> HeaderLanguage { match raw.unwrap_or("auto").trim().to_ascii_lowercase().as_str() { "c" => HeaderLanguage::C, @@ -66,6 +102,13 @@ pub const DEFAULT_CONFIG_TOML: &str = r#"# CodeGraph project configuration # How to parse .h header files: "auto", "c", or "cpp". # "auto" detects C++ projects from .cpp/.hpp files and C++ syntax in headers. headers = "auto" + +# Project effect rules — matched before the built-in defaults (first match wins). +# call matchers: prefix / contains / exact. Effects: sql_query, sql_write, +# cache_read, cache_write, http_call, event_emit, file_read, file_write, log. +# [[effect_rules]] +# call = { prefix = "db." } +# effect = "sql_query" "#; /// Quick project scan: returns a hint when the tree is clearly C-only or C++-only. @@ -154,4 +197,53 @@ headers = "cpp" "#ifndef FOO_H\n#define FOO_H\nstruct foo { int x; };\n#endif\n" )); } + + /// Parse từ file tạm với `[[effect_rules]]` → classifier áp dụng được. + #[test] + fn load_from_file_applies_effect_rules() { + let dir = std::env::temp_dir().join("codegraph-extract-cfg-test"); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("config.toml"); + let path = Utf8Path::from_path(path.as_path()).unwrap(); + std::fs::write( + path.as_std_path(), + r#" +[languages] +headers = "cpp" + +[[effect_rules]] +call = { prefix = "db." } +effect = "sql_query" + +[[effect_rules]] +call = { exact = "sendEmail" } +effect = "event_emit" + +[[effect_rules]] +call = { contains = "legacy-" } +effect = "not_a_real_effect" +"#, + ) + .unwrap(); + + let cfg = ExtractConfig::load_from(path); + assert_eq!(cfg.header_language, HeaderLanguage::Cpp); + // Rule config xét trước default: "db.Exec" → SqlQuery (không phải + // SqlWrite như default ".Exec"). + let (effect, desc) = cfg.effect_classifier.classify("db.Exec"); + assert_eq!(effect, codegraph_core::EffectType::SqlQuery); + assert_eq!(desc, Some("db.")); + assert_eq!( + cfg.effect_classifier.classify("sendEmail").0, + codegraph_core::EffectType::EventEmit + ); + // Rule có effect unknown bị skip → "legacy-" không match, rơi về default. + assert_eq!( + cfg.effect_classifier.classify("legacy-writer").0, + codegraph_core::EffectType::None + ); + + let _ = std::fs::remove_file(path.as_std_path()); + let _ = std::fs::remove_dir(&dir); + } } diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index 0821ae3c7..c785ae4b2 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -15,10 +15,9 @@ use crate::languages::effects::classify_effect; use codegraph_core::{ - Annotation, CallRecord, Result, ScopeLevel, Symbol, SymbolKind, - MARKER_BRANCH_END, MARKER_BREAK, MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, - MARKER_LOOP, MARKER_LOOP_BACK, MARKER_RETURN, MARKER_SWITCH_CASE, MARKER_SWITCH_END, - MARKER_THROW, SYMBOL_BASE, + Annotation, CallRecord, Result, ScopeLevel, Symbol, SymbolKind, MARKER_BRANCH_END, + MARKER_BREAK, MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, + MARKER_RETURN, MARKER_SWITCH_CASE, MARKER_SWITCH_END, MARKER_THROW, SYMBOL_BASE, }; use codegraph_graph::ParseResult; use std::collections::HashMap; @@ -100,7 +99,12 @@ pub struct LangSpec { // ==================== Pipeline ==================== /// Chạy pipeline đầy đủ cho một file → `ParseResult` (input của `GraphIndex::ingest`). -pub fn run_spec(spec: &'static LangSpec, path: &str, language: &str, source: &str) -> Result { +pub fn run_spec( + spec: &'static LangSpec, + path: &str, + language: &str, + source: &str, +) -> Result { let tree = parse_tree(spec, source)?; let root = tree.root_node(); let src = source.as_bytes(); @@ -125,10 +129,30 @@ pub fn run_spec(spec: &'static LangSpec, path: &str, language: &str, source: &st .map(|s| ((s.name.clone(), s.line), s.id)) .collect(); + // class_index: (name, line) → id — cho chain tối thiểu của class-like node. + let class_index: HashMap<(String, u32), u64> = symbols + .iter() + .filter(|s| { + matches!( + s.kind, + SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum | SymbolKind::Module + ) + }) + .map(|s| ((s.name.clone(), s.line), s.id)) + .collect(); + // ── Pass 2: chains ── let mut chains: HashMap> = HashMap::new(); let mut calls: Vec = Vec::new(); - collect_chains(&root, src, spec, &func_index, &mut chains, &mut calls); + collect_chains( + &root, + src, + spec, + &func_index, + &class_index, + &mut chains, + &mut calls, + ); Ok(ParseResult { path: path.to_string(), @@ -228,7 +252,10 @@ fn push_symbol( ctx.next_id += 1; let (scope, scope_id) = if spec.param_kinds.contains(&node_kind) { - (ScopeLevel::Parameter, ctx.scope_stack.last().map(|(i, _)| *i).unwrap_or(0)) + ( + ScopeLevel::Parameter, + ctx.scope_stack.last().map(|(i, _)| *i).unwrap_or(0), + ) } else if let Some(&(sid, is_class)) = ctx.scope_stack.last() { if is_class { (ScopeLevel::ObjectField, sid) @@ -244,9 +271,7 @@ fn push_symbol( // reclassify Function/Method thay vì Variable/Field (giống khai báo hàm thường). let kind = if (node_kind == "declaration" || node_kind == "field_declaration") && (from_error_ctor - || node - .child_by_field_name("declarator") - .map(|d| d.kind()) + || node.child_by_field_name("declarator").map(|d| d.kind()) == Some("function_declarator")) { if scope == ScopeLevel::ObjectField { @@ -268,7 +293,8 @@ fn push_symbol( let type_name = match kind { SymbolKind::Variable | SymbolKind::Constant | SymbolKind::Field | SymbolKind::Parameter => { - node.child_by_field_name("type").and_then(|t| text(&t, ctx.src)) + node.child_by_field_name("type") + .and_then(|t| text(&t, ctx.src)) } SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum | SymbolKind::Module => { spec.class_type_name.and_then(|f| f(node, ctx.src)) @@ -308,7 +334,10 @@ fn push_symbol( fn resolve_type_refs(symbols: &mut [Symbol]) { let mut by_name: HashMap = HashMap::new(); for s in symbols.iter() { - if matches!(s.kind, SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum) { + if matches!( + s.kind, + SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum + ) { by_name.entry(s.name.clone()).or_insert(s.id); } } @@ -316,7 +345,9 @@ fn resolve_type_refs(symbols: &mut [Symbol]) { if s.type_ref != 0 { continue; } - let Some(tn) = s.type_name.clone() else { continue }; + let Some(tn) = s.type_name.clone() else { + continue; + }; if let Some(&tid) = by_name.get(&base_type_name(&tn)) { s.type_ref = tid; } @@ -334,11 +365,7 @@ fn base_type_name(tn: &str) -> String { s.rsplit(['.', ':']).next().unwrap_or(s).trim().to_string() } -fn extract_annotations( - node: &Node, - src: &[u8], - kinds: &'static [&'static str], -) -> Vec { +fn extract_annotations(node: &Node, src: &[u8], kinds: &'static [&'static str]) -> Vec { if kinds.is_empty() { return Vec::new(); } @@ -405,6 +432,7 @@ fn collect_chains( src: &[u8], spec: &'static LangSpec, func_index: &HashMap<(String, u32), u64>, + class_index: &HashMap<(String, u32), u64>, chains: &mut HashMap>, calls: &mut Vec, ) { @@ -414,17 +442,20 @@ fn collect_chains( chains.insert(id, chain); calls.append(&mut cs); } + } else if spec.class_kinds.contains(&root.kind()) { + // Class không có chain (chỉ có edge function→class từ phía caller). + // Build chain tối thiểu `[class_id]` để `flow`/`search_flow` không bị + // "chain not found" — methods của class vẫn có chain riêng của chúng. + if let Some(id) = class_id_of(root, src, class_index) { + chains.entry(id).or_insert_with(|| vec![id]); + } } for ch in named_children(root) { - collect_chains(&ch, src, spec, func_index, chains, calls); + collect_chains(&ch, src, spec, func_index, class_index, chains, calls); } } -fn func_id_of( - node: &Node, - src: &[u8], - func_index: &HashMap<(String, u32), u64>, -) -> Option { +fn func_id_of(node: &Node, src: &[u8], func_index: &HashMap<(String, u32), u64>) -> Option { let name_node = node .child_by_field_name("name") .or_else(|| name_from_declarator(node)) @@ -434,8 +465,22 @@ fn func_id_of( func_index.get(&(name, line)).copied() } +fn class_id_of(node: &Node, src: &[u8], class_index: &HashMap<(String, u32), u64>) -> Option { + let name_node = node + .child_by_field_name("name") + .or_else(|| first_identifier(node))?; + let name = text(&name_node, src)?; + let line = name_node.start_position().row as u32 + 1; + class_index.get(&(name, line)).copied() +} + /// Build chain của một function: `[func_id, marker/call, ...]`. -pub fn build_chain(node: &Node, src: &[u8], spec: &'static LangSpec, func_id: u64) -> (Vec, Vec) { +pub fn build_chain( + node: &Node, + src: &[u8], + spec: &'static LangSpec, + func_id: u64, +) -> (Vec, Vec) { let mut ctx = ChainCtx { src, spec, @@ -532,9 +577,8 @@ fn walk_chain( .filter(|t| !t.is_empty()) .or_else(|| condition.clone()); // do-while/repeat: condition chạy SAU body → emit sau. - let is_do_while = k.contains("do") - || k == "repeat_statement" - || k == "repeat_while_statement"; + let is_do_while = + k.contains("do") || k == "repeat_statement" || k == "repeat_while_statement"; if !is_do_while { if let Some(cn) = cond_node { walk_chain(ctx, &cn, depth + 1, in_loop + 1, loop_cond.clone()); @@ -562,6 +606,10 @@ fn walk_chain( } for case in switch_cases(node, ctx.spec) { chain_push(ctx, MARKER_SWITCH_CASE); + // String-literal case label (`case 'optimize_text':`) — dispatch key + // không phải identifier call; emit call-name ảo để search_by_call + // tìm được function chứa switch. + emit_case_label_call(ctx, &case, in_loop, condition.clone()); walk_block(ctx, &case, depth + 1, in_loop, condition.clone()); chain_push(ctx, MARKER_SWITCH_END); } @@ -677,14 +725,26 @@ fn walk_alternative( } } -fn walk_block(ctx: &mut ChainCtx, node: &Node, depth: u32, in_loop: u32, condition: Option) { +fn walk_block( + ctx: &mut ChainCtx, + node: &Node, + depth: u32, + in_loop: u32, + condition: Option, +) { for ch in named_children(node) { walk_chain(ctx, &ch, depth, in_loop, condition.clone()); } } /// Walk một clause (except/else/finally) — body field nếu có, không thì toàn node. -fn walk_clause(ctx: &mut ChainCtx, node: &Node, depth: u32, in_loop: u32, condition: Option) { +fn walk_clause( + ctx: &mut ChainCtx, + node: &Node, + depth: u32, + in_loop: u32, + condition: Option, +) { if let Some(b) = node.child_by_field_name(ctx.spec.body_field) { walk_chain(ctx, &b, depth, in_loop, condition); } else { @@ -778,6 +838,69 @@ fn switch_cases<'a>(node: &Node<'a>, spec: &'static LangSpec) -> Vec> { out } +/// Case label là string literal (`case 'optimize_text':`) — dispatch key theo +/// chuỗi, không phải call thật. Emit placeholder `0` + CallRecord với +/// `call_name = literal` (bỏ quote) để `search_by_call` index được. Không có +/// symbol tương ứng trong repo → không resolve được → giữ unresolved call. +fn emit_case_label_call(ctx: &mut ChainCtx, case: &Node, in_loop: u32, condition: Option) { + // Field `value` là expression của case (`case X:` → X). Fallback: named child + // đầu tiên (một số grammar không đặt field). + let value = case + .child_by_field_name("value") + .or_else(|| named_children(case).into_iter().next()); + let Some(value) = value else { return }; + if !is_string_literal_kind(value.kind()) { + return; + } + let Some(lit) = text(&value, ctx.src) else { + return; + }; + let Some(name) = string_literal_value(&lit) else { + return; + }; + if name.is_empty() { + return; + } + let position = ctx.chain.len(); + ctx.chain.push(0); + let (effect, effect_desc) = classify_effect(&name); + ctx.calls.push(CallRecord { + caller_id: ctx.func_id, + call_name: name, + position, + arg_exprs: Vec::new(), + line: value.start_position().row as u32 + 1, + condition, + is_loop_body: in_loop > 0, + effect, + effect_desc, + target_class: None, + target_method: None, + }); +} + +/// Node kind của một string literal — chấp nhận các tên theo từng grammar +/// (TS `string`, Java `string_literal`, Go `interpreted_string_literal`...). +fn is_string_literal_kind(kind: &str) -> bool { + kind.contains("string") + || matches!( + kind, + "template_string" | "template_literal" | "char_literal" | "quoted_string" + ) +} + +/// Rút giá trị chuỗi từ source literal: `'opt'`/`"opt"`/`` `opt` `` → `opt`. +fn string_literal_value(lit: &str) -> Option { + let l = lit.trim(); + let b = l.as_bytes(); + if b.len() < 2 { + return None; + } + let (open, close) = (b[0] as char, b[b.len() - 1] as char); + let matched = matches!((open, close), ('\'', '\'') | ('"', '"') | ('`', '`')); + matched.then(|| l[1..l.len() - 1].to_string()) +} + /// Emit placeholder `0` + CallRecord cho một call site. fn emit_call( ctx: &mut ChainCtx, @@ -826,7 +949,7 @@ fn emit_call( condition, is_loop_body: in_loop > 0, effect, - effect_desc: effect_desc.map(|s| s.to_string()), + effect_desc, target_class, target_method, }); @@ -923,7 +1046,9 @@ fn declarator_child<'a>(n: &Node<'a>) -> Option> { if let Some(d) = n.child_by_field_name("declarator") { return Some(d); } - named_children(n).into_iter().find(|c| is_declarator_kind(c.kind())) + named_children(n) + .into_iter() + .find(|c| is_declarator_kind(c.kind())) } fn is_declarator_kind(kind: &str) -> bool { @@ -949,7 +1074,9 @@ fn is_declarator_kind(kind: &str) -> bool { } fn is_conversion_declarator(n: &Node) -> bool { - n.parent().map(|p| p.kind() == "operator_cast").unwrap_or(false) + n.parent() + .map(|p| p.kind() == "operator_cast") + .unwrap_or(false) } /// DFS tìm identifier đầu tiên trong subtree. diff --git a/crates/codegraph-extract/src/languages/cpp.rs b/crates/codegraph-extract/src/languages/cpp.rs index 841dc8096..bfcc822bf 100644 --- a/crates/codegraph-extract/src/languages/cpp.rs +++ b/crates/codegraph-extract/src/languages/cpp.rs @@ -37,7 +37,12 @@ pub static SPEC: LangSpec = LangSpec { if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], - loop_kinds: &["for_statement", "for_range_loop", "while_statement", "do_statement"], + loop_kinds: &[ + "for_statement", + "for_range_loop", + "while_statement", + "do_statement", + ], switch_kinds: &["switch_statement"], switch_block_kinds: &[], switch_case_kinds: &["case_statement"], diff --git a/crates/codegraph-extract/src/languages/csharp.rs b/crates/codegraph-extract/src/languages/csharp.rs index 0e2f4d19d..0ff458e2a 100644 --- a/crates/codegraph-extract/src/languages/csharp.rs +++ b/crates/codegraph-extract/src/languages/csharp.rs @@ -8,7 +8,9 @@ fn ts_language() -> tree_sitter::Language { /// `new List(...)` — tên class gốc (strip generic args để resolve được). fn new_call_name(node: &Node, src: &[u8]) -> Option { - let tn = node.child_by_field_name("type").and_then(|t| text(&t, src))?; + let tn = node + .child_by_field_name("type") + .and_then(|t| text(&t, src))?; let base = tn.split('<').next().unwrap_or(&tn); Some(base.trim().to_string()) } @@ -61,7 +63,12 @@ pub static SPEC: LangSpec = LangSpec { if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], - loop_kinds: &["for_statement", "foreach_statement", "while_statement", "do_statement"], + loop_kinds: &[ + "for_statement", + "foreach_statement", + "while_statement", + "do_statement", + ], switch_kinds: &["switch_statement", "switch_expression"], switch_block_kinds: &["switch_body"], switch_case_kinds: &["switch_section", "switch_expression_arm"], diff --git a/crates/codegraph-extract/src/languages/effects.rs b/crates/codegraph-extract/src/languages/effects.rs index 6afc2e12b..7c62be42a 100644 --- a/crates/codegraph-extract/src/languages/effects.rs +++ b/crates/codegraph-extract/src/languages/effects.rs @@ -1,134 +1,228 @@ -//! Effect classification cho call names. +//! Effect classification cho call names — configurable classifier. //! //! Port nhẹ từ `walle/pkgs/rules/extraction/defaults.go` (DefaultRules) — bảng //! pattern áp dụng mọi ngôn ngữ, first-match-wins theo thứ tự: pattern cụ thể //! (framework/library) trước, generic fallback cuối. Không dùng imports để chọn //! library rule (bản nhẹ) — classify theo call name là đủ cho impact/flow render. +//! +//! Project có thể bổ sung rule qua `.codegraph/config.toml` `[[effect_rules]]` +//! (schema `EffectRule` trong codegraph-core). Rule config được xét TRƯỚC bảng +//! default → override được, phần còn lại vẫn rơi về defaults. +//! +//! Classifier là "ambient config": `Orchestrator` install classifier của project +//! vào thread-local trước vòng parse song song; leaf `classify_effect` đọc từ +//! thread-local (chưa install → dùng bảng default — test/đường dẫn đơn file). -use codegraph_core::EffectType; +use codegraph_core::{EffectCallPattern, EffectRule, EffectType}; +use std::cell::RefCell; +use std::sync::{Arc, OnceLock}; -#[derive(Clone, Copy)] +/// Cách match một rule lên call name. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MatchTy { Prefix, Contains, + Exact, } -#[derive(Clone, Copy)] -struct Pattern { +/// Một rule cụ thể — chuyển từ `EffectRule` (config) hoặc bảng default. +#[derive(Debug, Clone, PartialEq, Eq)] +struct ClassifierRule { matcher: MatchTy, - text: &'static str, + text: String, effect: EffectType, } -/// Thứ tự quan trọng — đọc từ trên xuống, pattern đầu tiên match sẽ thắng. -const PATTERNS: &[Pattern] = &[ +/// Bảng default — thứ tự quan trọng, đọc từ trên xuống, pattern đầu tiên match +/// sẽ thắng. +const DEFAULT_RULES: &[(MatchTy, &str, EffectType)] = &[ // ── Prefix-based (high precision) ── - Pattern { matcher: MatchTy::Prefix, text: "http.", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Prefix, text: "net/http.", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Prefix, text: "log.", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Prefix, text: "slog.", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Prefix, text: "os.", effect: EffectType::FileRead }, - Pattern { matcher: MatchTy::Prefix, text: "open(", effect: EffectType::FileRead }, + (MatchTy::Prefix, "http.", EffectType::HttpCall), + (MatchTy::Prefix, "net/http.", EffectType::HttpCall), + (MatchTy::Prefix, "log.", EffectType::Log), + (MatchTy::Prefix, "slog.", EffectType::Log), + (MatchTy::Prefix, "os.", EffectType::FileRead), + (MatchTy::Prefix, "open(", EffectType::FileRead), // ── Java library types ── - Pattern { matcher: MatchTy::Contains, text: "RestTemplate", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: "retrofit", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: "WebClient", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: "FileInputStream", effect: EffectType::FileRead }, - Pattern { matcher: MatchTy::Contains, text: "FileReader", effect: EffectType::FileRead }, - Pattern { matcher: MatchTy::Contains, text: "BufferedReader", effect: EffectType::FileRead }, - Pattern { matcher: MatchTy::Contains, text: "FileOutputStream", effect: EffectType::FileWrite }, - Pattern { matcher: MatchTy::Contains, text: "FileWriter", effect: EffectType::FileWrite }, + (MatchTy::Contains, "RestTemplate", EffectType::HttpCall), + (MatchTy::Contains, "retrofit", EffectType::HttpCall), + (MatchTy::Contains, "WebClient", EffectType::HttpCall), + (MatchTy::Contains, "FileInputStream", EffectType::FileRead), + (MatchTy::Contains, "FileReader", EffectType::FileRead), + (MatchTy::Contains, "BufferedReader", EffectType::FileRead), + (MatchTy::Contains, "FileOutputStream", EffectType::FileWrite), + (MatchTy::Contains, "FileWriter", EffectType::FileWrite), // ── Messaging / events ── - Pattern { matcher: MatchTy::Contains, text: "kafka.", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: "rabbit", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: "amqp", effect: EffectType::EventEmit }, + (MatchTy::Contains, "kafka.", EffectType::EventEmit), + (MatchTy::Contains, "rabbit", EffectType::EventEmit), + (MatchTy::Contains, "amqp", EffectType::EventEmit), // ── SQL — explicit patterns ── - Pattern { matcher: MatchTy::Contains, text: ".Query", effect: EffectType::SqlQuery }, - Pattern { matcher: MatchTy::Contains, text: ".QueryRow", effect: EffectType::SqlQuery }, - Pattern { matcher: MatchTy::Contains, text: ".Raw", effect: EffectType::SqlQuery }, - Pattern { matcher: MatchTy::Contains, text: ".Select", effect: EffectType::SqlQuery }, - Pattern { matcher: MatchTy::Contains, text: ".Find", effect: EffectType::SqlQuery }, - Pattern { matcher: MatchTy::Contains, text: ".First", effect: EffectType::SqlQuery }, - Pattern { matcher: MatchTy::Contains, text: ".Model(", effect: EffectType::SqlQuery }, - Pattern { matcher: MatchTy::Contains, text: ".Exec", effect: EffectType::SqlWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Insert", effect: EffectType::SqlWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Update", effect: EffectType::SqlWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Delete(", effect: EffectType::SqlWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Create(", effect: EffectType::SqlWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Save(", effect: EffectType::SqlWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Session", effect: EffectType::SqlWrite }, + (MatchTy::Contains, ".Query", EffectType::SqlQuery), + (MatchTy::Contains, ".QueryRow", EffectType::SqlQuery), + (MatchTy::Contains, ".Raw", EffectType::SqlQuery), + (MatchTy::Contains, ".Select", EffectType::SqlQuery), + (MatchTy::Contains, ".Find", EffectType::SqlQuery), + (MatchTy::Contains, ".First", EffectType::SqlQuery), + (MatchTy::Contains, ".Model(", EffectType::SqlQuery), + (MatchTy::Contains, ".Exec", EffectType::SqlWrite), + (MatchTy::Contains, ".Insert", EffectType::SqlWrite), + (MatchTy::Contains, ".Update", EffectType::SqlWrite), + (MatchTy::Contains, ".Delete(", EffectType::SqlWrite), + (MatchTy::Contains, ".Create(", EffectType::SqlWrite), + (MatchTy::Contains, ".Save(", EffectType::SqlWrite), + (MatchTy::Contains, ".Session", EffectType::SqlWrite), // ── HTTP method calls ── - Pattern { matcher: MatchTy::Prefix, text: "requests.", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: ".Get(", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: ".Post(", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: ".Put(", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: ".Delete(", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: ".Patch(", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: ".Do(", effect: EffectType::HttpCall }, - Pattern { matcher: MatchTy::Contains, text: ".NewRequest", effect: EffectType::HttpCall }, + (MatchTy::Prefix, "requests.", EffectType::HttpCall), + (MatchTy::Contains, ".Get(", EffectType::HttpCall), + (MatchTy::Contains, ".Post(", EffectType::HttpCall), + (MatchTy::Contains, ".Put(", EffectType::HttpCall), + (MatchTy::Contains, ".Delete(", EffectType::HttpCall), + (MatchTy::Contains, ".Patch(", EffectType::HttpCall), + (MatchTy::Contains, ".Do(", EffectType::HttpCall), + (MatchTy::Contains, ".NewRequest", EffectType::HttpCall), // ── Event publish/consume ── - Pattern { matcher: MatchTy::Contains, text: ".Publish", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".publish", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".Send", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".send", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".Produce", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".produce", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".Consume", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".consume", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".Subscribe", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".subscribe", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".Receive", effect: EffectType::EventEmit }, - Pattern { matcher: MatchTy::Contains, text: ".receive", effect: EffectType::EventEmit }, + (MatchTy::Contains, ".Publish", EffectType::EventEmit), + (MatchTy::Contains, ".publish", EffectType::EventEmit), + (MatchTy::Contains, ".Send", EffectType::EventEmit), + (MatchTy::Contains, ".send", EffectType::EventEmit), + (MatchTy::Contains, ".Produce", EffectType::EventEmit), + (MatchTy::Contains, ".produce", EffectType::EventEmit), + (MatchTy::Contains, ".Consume", EffectType::EventEmit), + (MatchTy::Contains, ".consume", EffectType::EventEmit), + (MatchTy::Contains, ".Subscribe", EffectType::EventEmit), + (MatchTy::Contains, ".subscribe", EffectType::EventEmit), + (MatchTy::Contains, ".Receive", EffectType::EventEmit), + (MatchTy::Contains, ".receive", EffectType::EventEmit), // ── Cache ── - Pattern { matcher: MatchTy::Contains, text: ".MGet", effect: EffectType::CacheRead }, - Pattern { matcher: MatchTy::Contains, text: ".MSet", effect: EffectType::CacheWrite }, - Pattern { matcher: MatchTy::Contains, text: ".HGet", effect: EffectType::CacheRead }, - Pattern { matcher: MatchTy::Contains, text: ".HSet", effect: EffectType::CacheWrite }, - Pattern { matcher: MatchTy::Contains, text: ".HGetAll", effect: EffectType::CacheRead }, - Pattern { matcher: MatchTy::Contains, text: ".Del(", effect: EffectType::CacheWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Expire", effect: EffectType::CacheWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Exists", effect: EffectType::CacheRead }, - Pattern { matcher: MatchTy::Contains, text: ".TTL", effect: EffectType::CacheRead }, + (MatchTy::Contains, ".MGet", EffectType::CacheRead), + (MatchTy::Contains, ".MSet", EffectType::CacheWrite), + (MatchTy::Contains, ".HGet", EffectType::CacheRead), + (MatchTy::Contains, ".HSet", EffectType::CacheWrite), + (MatchTy::Contains, ".HGetAll", EffectType::CacheRead), + (MatchTy::Contains, ".Del(", EffectType::CacheWrite), + (MatchTy::Contains, ".Expire", EffectType::CacheWrite), + (MatchTy::Contains, ".Exists", EffectType::CacheRead), + (MatchTy::Contains, ".TTL", EffectType::CacheRead), // ── File I/O ── - Pattern { matcher: MatchTy::Contains, text: ".Open", effect: EffectType::FileRead }, - Pattern { matcher: MatchTy::Contains, text: ".ReadFile", effect: EffectType::FileRead }, - Pattern { matcher: MatchTy::Contains, text: ".ReadAll", effect: EffectType::FileRead }, - Pattern { matcher: MatchTy::Contains, text: ".WriteFile", effect: EffectType::FileWrite }, - Pattern { matcher: MatchTy::Contains, text: ".WriteString", effect: EffectType::FileWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Create", effect: EffectType::FileWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Mkdir", effect: EffectType::FileWrite }, + (MatchTy::Contains, ".Open", EffectType::FileRead), + (MatchTy::Contains, ".ReadFile", EffectType::FileRead), + (MatchTy::Contains, ".ReadAll", EffectType::FileRead), + (MatchTy::Contains, ".WriteFile", EffectType::FileWrite), + (MatchTy::Contains, ".WriteString", EffectType::FileWrite), + (MatchTy::Contains, ".Create", EffectType::FileWrite), + (MatchTy::Contains, ".Mkdir", EffectType::FileWrite), // ── Log ── - Pattern { matcher: MatchTy::Contains, text: "logging.", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: "logger.", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Printf", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Println", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Infof", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Info", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Errorf", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Error", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Warnf", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Warn", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Debugf", effect: EffectType::Log }, - Pattern { matcher: MatchTy::Contains, text: ".Debug", effect: EffectType::Log }, + (MatchTy::Contains, "logging.", EffectType::Log), + (MatchTy::Contains, "logger.", EffectType::Log), + (MatchTy::Contains, ".Printf", EffectType::Log), + (MatchTy::Contains, ".Println", EffectType::Log), + (MatchTy::Contains, ".Infof", EffectType::Log), + (MatchTy::Contains, ".Info", EffectType::Log), + (MatchTy::Contains, ".Errorf", EffectType::Log), + (MatchTy::Contains, ".Error", EffectType::Log), + (MatchTy::Contains, ".Warnf", EffectType::Log), + (MatchTy::Contains, ".Warn", EffectType::Log), + (MatchTy::Contains, ".Debugf", EffectType::Log), + (MatchTy::Contains, ".Debug", EffectType::Log), // ── Generic fallbacks (no context — last resort) ── - Pattern { matcher: MatchTy::Contains, text: ".Set", effect: EffectType::CacheWrite }, - Pattern { matcher: MatchTy::Contains, text: ".Get", effect: EffectType::SqlQuery }, + (MatchTy::Contains, ".Set", EffectType::CacheWrite), + (MatchTy::Contains, ".Get", EffectType::SqlQuery), ]; -/// Phân loại effect của một call theo tên callee. -/// -/// Trả về `(effect, pattern đã match)` — pattern dùng làm effect_desc. -pub fn classify_effect(call_name: &str) -> (EffectType, Option<&'static str>) { - for p in PATTERNS { - let hit = match p.matcher { - MatchTy::Prefix => call_name.starts_with(p.text), - MatchTy::Contains => call_name.contains(p.text), +/// Classifier cấu hình được — first-match-wins theo thứ tự `rules`. +#[derive(Debug, Clone)] +pub struct EffectClassifier { + rules: Vec, +} + +/// Default = bảng built-in (behavior hiện tại khi không có config). +impl Default for EffectClassifier { + fn default() -> Self { + Self { + rules: DEFAULT_RULES + .iter() + .map(|&(matcher, text, effect)| ClassifierRule { + matcher, + text: text.to_string(), + effect, + }) + .collect(), + } + } +} + +impl EffectClassifier { + /// Rule config xét TRƯỚC bảng default (override), phần còn lại rơi về defaults. + pub fn with_config(config_rules: Vec) -> Self { + let mut rules: Vec = + config_rules.into_iter().map(Self::from_rule).collect(); + rules.extend(Self::default().rules); + Self { rules } + } + + fn from_rule(rule: EffectRule) -> ClassifierRule { + let (matcher, text) = match rule.call { + EffectCallPattern::Prefix { prefix } => (MatchTy::Prefix, prefix), + EffectCallPattern::Contains { contains } => (MatchTy::Contains, contains), + EffectCallPattern::Exact { exact } => (MatchTy::Exact, exact), }; - if hit { - return (p.effect, Some(p.text)); + ClassifierRule { + matcher, + text, + effect: rule.effect, } } - (EffectType::None, None) + + /// Phân loại theo rule đầu tiên match — `(effect, text của rule đã match)`. + pub fn classify(&self, call_name: &str) -> (EffectType, Option<&str>) { + for r in &self.rules { + let hit = match r.matcher { + MatchTy::Prefix => call_name.starts_with(&r.text), + MatchTy::Contains => call_name.contains(&r.text), + MatchTy::Exact => call_name == r.text, + }; + if hit { + return (r.effect, Some(r.text.as_str())); + } + } + (EffectType::None, None) + } +} + +// Thread-local classifier hiện tại — `Orchestrator` install trước vòng parse. +thread_local! { + static CURRENT: RefCell>> = const { RefCell::new(None) }; +} + +/// Default classifier dùng chung (khi chưa install / test). +fn default_classifier() -> &'static EffectClassifier { + static DEFAULT: OnceLock = OnceLock::new(); + DEFAULT.get_or_init(EffectClassifier::default) +} + +/// Install classifier cho thread đang chạy (gọi đầu mỗi job parse). `None` reset +/// về default. +pub fn install_current(classifier: Option>) { + CURRENT.with(|slot| *slot.borrow_mut() = classifier); +} + +/// Phân loại effect của một call theo classifier của project (fallback default). +/// +/// Trả về `(effect, pattern đã match)` — pattern dùng làm effect_desc. +pub fn classify_effect(call_name: &str) -> (EffectType, Option) { + CURRENT.with(|slot| { + let borrow = slot.borrow(); + match borrow.as_ref() { + Some(c) => { + let (e, m) = c.classify(call_name); + (e, m.map(str::to_owned)) + } + None => { + let (e, m) = default_classifier().classify(call_name); + (e, m.map(str::to_owned)) + } + } + }) } #[cfg(test)] @@ -178,4 +272,52 @@ mod tests { assert_eq!(classify_effect("validateUser"), (EffectType::None, None)); assert_eq!(classify_effect("sendEmail"), (EffectType::None, None)); } + + /// Config rule (prefix/contains/exact) được xét trước default → override. + #[test] + fn config_rules_override_defaults() { + let rules = vec![ + EffectRule { + call: EffectCallPattern::Prefix { + prefix: "db.".to_string(), + }, + effect: EffectType::SqlQuery, + }, + EffectRule { + call: EffectCallPattern::Exact { + exact: "sendEmail".to_string(), + }, + effect: EffectType::EventEmit, + }, + ]; + let c = EffectClassifier::with_config(rules); + assert_eq!(c.classify("db.Exec").0, EffectType::SqlQuery); // trước default ".Exec" + assert_eq!(c.classify("sendEmail").0, EffectType::EventEmit); + assert_eq!( + c.classify("sendEmail"), + (EffectType::EventEmit, Some("sendEmail")) + ); + // Không có rule config → rơi về default. + assert_eq!(c.classify("kafka.Produce").0, EffectType::EventEmit); + assert_eq!(c.classify("noSuchThing"), (EffectType::None, None)); + } + + /// Rule config install vào thread-local → `classify_effect` đọc được. + #[test] + fn installed_classifier_is_used_by_leaf() { + let rules = vec![EffectRule { + call: EffectCallPattern::Contains { + contains: "legacy-".to_string(), + }, + effect: EffectType::FileWrite, + }]; + install_current(Some(Arc::new(EffectClassifier::with_config(rules)))); + assert_eq!(classify_effect("legacy-writer").0, EffectType::FileWrite); + assert_eq!( + classify_effect("legacy-writer").1.as_deref(), + Some("legacy-") + ); + install_current(None); + assert_eq!(classify_effect("legacy-writer").0, EffectType::None); + } } diff --git a/crates/codegraph-extract/src/languages/java.rs b/crates/codegraph-extract/src/languages/java.rs index 9763c2ed9..54c0d6829 100644 --- a/crates/codegraph-extract/src/languages/java.rs +++ b/crates/codegraph-extract/src/languages/java.rs @@ -8,7 +8,9 @@ fn ts_language() -> tree_sitter::Language { /// `obj.method` — object field nếu có (giống reference: `obj.Content + "." + name`). fn method_invocation_name(node: &Node, src: &[u8]) -> Option { - let name = node.child_by_field_name("name").and_then(|n| text(&n, src))?; + let name = node + .child_by_field_name("name") + .and_then(|n| text(&n, src))?; if let Some(obj) = node.child_by_field_name("object") { if let Some(obj_text) = text(&obj, src) { if !obj_text.is_empty() { @@ -105,7 +107,12 @@ pub static SPEC: LangSpec = LangSpec { if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], - loop_kinds: &["for_statement", "enhanced_for_statement", "while_statement", "do_statement"], + loop_kinds: &[ + "for_statement", + "enhanced_for_statement", + "while_statement", + "do_statement", + ], switch_kinds: &["switch_expression", "switch_statement"], switch_block_kinds: &["switch_block"], switch_case_kinds: &["switch_block_statement_group", "switch_rule"], diff --git a/crates/codegraph-extract/src/languages/javascript.rs b/crates/codegraph-extract/src/languages/javascript.rs index e8b10cc32..585154ca8 100644 --- a/crates/codegraph-extract/src/languages/javascript.rs +++ b/crates/codegraph-extract/src/languages/javascript.rs @@ -12,9 +12,7 @@ pub fn class_type_name(node: &Node, src: &[u8]) -> Option { if ch.kind() == "class_heritage" { for cc in named_children(&ch) { if cc.kind() == "extends_clause" { - return cc - .child_by_field_name("name") - .and_then(|n| text(&n, src)); + return cc.child_by_field_name("name").and_then(|n| text(&n, src)); } } } @@ -67,7 +65,13 @@ pub static SPEC: LangSpec = LangSpec { if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], - loop_kinds: &["for_statement", "for_in_statement", "for_of_statement", "while_statement", "do_statement"], + loop_kinds: &[ + "for_statement", + "for_in_statement", + "for_of_statement", + "while_statement", + "do_statement", + ], switch_kinds: &["switch_statement"], switch_block_kinds: &["switch_body"], switch_case_kinds: &["switch_case"], diff --git a/crates/codegraph-extract/src/languages/lua.rs b/crates/codegraph-extract/src/languages/lua.rs index 705f7ad4e..73da104e7 100644 --- a/crates/codegraph-extract/src/languages/lua.rs +++ b/crates/codegraph-extract/src/languages/lua.rs @@ -16,7 +16,11 @@ pub static SPEC: LangSpec = LangSpec { ("variable_declaration", SymbolKind::Variable), ("local_variable_declaration", SymbolKind::Variable), ], - func_kinds: &["function_declaration", "function_definition", "local_function"], + func_kinds: &[ + "function_declaration", + "function_definition", + "local_function", + ], class_kinds: &[], param_kinds: &[], annotation_kinds: &[], diff --git a/crates/codegraph-extract/src/languages/php.rs b/crates/codegraph-extract/src/languages/php.rs index f28ff7702..f2d293952 100644 --- a/crates/codegraph-extract/src/languages/php.rs +++ b/crates/codegraph-extract/src/languages/php.rs @@ -8,7 +8,9 @@ fn ts_language() -> tree_sitter::Language { /// `Foo::bar()` / `self::run()` — scope + "." + method. fn scoped_call_name(node: &Node, src: &[u8]) -> Option { - let name = node.child_by_field_name("name").and_then(|n| text(&n, src))?; + let name = node + .child_by_field_name("name") + .and_then(|n| text(&n, src))?; if let Some(scope) = node.child_by_field_name("scope") { if let Some(s) = text(&scope, src) { if !s.is_empty() { @@ -21,7 +23,9 @@ fn scoped_call_name(node: &Node, src: &[u8]) -> Option { /// `$obj->method()` — object + "." + method (bỏ `$` prefix của biến PHP). fn member_call_name(node: &Node, src: &[u8]) -> Option { - let name = node.child_by_field_name("name").and_then(|n| text(&n, src))?; + let name = node + .child_by_field_name("name") + .and_then(|n| text(&n, src))?; if let Some(obj) = node.child_by_field_name("object") { if let Some(o) = text(&obj, src) { let o = o.trim_start_matches('$'); @@ -102,7 +106,12 @@ pub static SPEC: LangSpec = LangSpec { if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], - loop_kinds: &["for_statement", "foreach_statement", "while_statement", "do_statement"], + loop_kinds: &[ + "for_statement", + "foreach_statement", + "while_statement", + "do_statement", + ], switch_kinds: &["switch_statement"], switch_block_kinds: &["switch_block"], switch_case_kinds: &["case_statement"], diff --git a/crates/codegraph-extract/src/languages/ruby.rs b/crates/codegraph-extract/src/languages/ruby.rs index 93e3a8298..5a4d5e481 100644 --- a/crates/codegraph-extract/src/languages/ruby.rs +++ b/crates/codegraph-extract/src/languages/ruby.rs @@ -23,7 +23,8 @@ fn call_name(node: &Node, src: &[u8]) -> Option { /// Class Ruby `class Foo < Bar` — superclass làm type_name. fn class_type_name(node: &Node, src: &[u8]) -> Option { - node.child_by_field_name("superclass").and_then(|s| text(&s, src)) + node.child_by_field_name("superclass") + .and_then(|s| text(&s, src)) } pub static SPEC: LangSpec = LangSpec { diff --git a/crates/codegraph-extract/src/languages/rust.rs b/crates/codegraph-extract/src/languages/rust.rs index 231ba9edc..64afb29cc 100644 --- a/crates/codegraph-extract/src/languages/rust.rs +++ b/crates/codegraph-extract/src/languages/rust.rs @@ -21,7 +21,13 @@ pub static SPEC: LangSpec = LangSpec { ("type_item", SymbolKind::Class), ], func_kinds: &["function_item"], - class_kinds: &["struct_item", "enum_item", "trait_item", "impl_item", "mod_item"], + class_kinds: &[ + "struct_item", + "enum_item", + "trait_item", + "impl_item", + "mod_item", + ], param_kinds: &[], annotation_kinds: &[], // `impl Foo` không có name field — tên nằm ở field `type`. diff --git a/crates/codegraph-extract/src/languages/scala.rs b/crates/codegraph-extract/src/languages/scala.rs index 81fa9a20e..efbf57d8f 100644 --- a/crates/codegraph-extract/src/languages/scala.rs +++ b/crates/codegraph-extract/src/languages/scala.rs @@ -21,7 +21,12 @@ pub static SPEC: LangSpec = LangSpec { ("parameter", SymbolKind::Parameter), ], func_kinds: &["function_definition", "function_declaration"], - class_kinds: &["class_definition", "trait_definition", "object_definition", "enum_definition"], + class_kinds: &[ + "class_definition", + "trait_definition", + "object_definition", + "enum_definition", + ], param_kinds: &["parameter"], annotation_kinds: &[], name_type_fallback: false, diff --git a/crates/codegraph-extract/src/languages/swift.rs b/crates/codegraph-extract/src/languages/swift.rs index bee7825e7..152970dec 100644 --- a/crates/codegraph-extract/src/languages/swift.rs +++ b/crates/codegraph-extract/src/languages/swift.rs @@ -21,7 +21,11 @@ pub static SPEC: LangSpec = LangSpec { ("variable_declaration", SymbolKind::Variable), ("parameter", SymbolKind::Parameter), ], - func_kinds: &["function_declaration", "init_declaration", "deinit_declaration"], + func_kinds: &[ + "function_declaration", + "init_declaration", + "deinit_declaration", + ], class_kinds: &[ "class_declaration", "struct_declaration", diff --git a/crates/codegraph-extract/src/languages/typescript.rs b/crates/codegraph-extract/src/languages/typescript.rs index 55b4708c1..9e79e1849 100644 --- a/crates/codegraph-extract/src/languages/typescript.rs +++ b/crates/codegraph-extract/src/languages/typescript.rs @@ -18,9 +18,7 @@ pub fn class_type_name(node: &Node, src: &[u8]) -> Option { "class_heritage" => { for cc in named_children(&ch) { if cc.kind() == "extends_clause" { - return cc - .child_by_field_name("name") - .and_then(|n| text(&n, src)); + return cc.child_by_field_name("name").and_then(|n| text(&n, src)); } } } @@ -92,7 +90,13 @@ pub static SPEC: LangSpec = LangSpec { if_kinds: &["if_statement"], elif_kinds: &[], if_block_kinds: &[], - loop_kinds: &["for_statement", "for_in_statement", "for_of_statement", "while_statement", "do_statement"], + loop_kinds: &[ + "for_statement", + "for_in_statement", + "for_of_statement", + "while_statement", + "do_statement", + ], switch_kinds: &["switch_statement"], switch_block_kinds: &["switch_body"], switch_case_kinds: &["switch_case"], diff --git a/crates/codegraph-extract/src/lib.rs b/crates/codegraph-extract/src/lib.rs index 7ddd77ba3..fd266fc1e 100644 --- a/crates/codegraph-extract/src/lib.rs +++ b/crates/codegraph-extract/src/lib.rs @@ -9,10 +9,12 @@ pub mod config; pub mod languages; mod orchestrator; +mod project; mod walker; -pub use orchestrator::{ExtractStats, Orchestrator}; pub use config::{ExtractConfig, HeaderLanguage, DEFAULT_CONFIG_TOML}; +pub use orchestrator::{ExtractStats, Orchestrator}; +pub use project::{init_project, project_db_path, project_dir, CODEGRAPH_DIR}; use codegraph_core::{Error, Result}; use codegraph_graph::ParseResult; @@ -104,7 +106,11 @@ macro_rules! lang_parser { fn ts_language(&self) -> tree_sitter::Language { ($ts)() } - fn parse_file(&self, path: &str, source: &str) -> codegraph_core::Result { + fn parse_file( + &self, + path: &str, + source: &str, + ) -> codegraph_core::Result { $crate::languages::common::run_spec(&$spec, path, $name, source) } } diff --git a/crates/codegraph-extract/src/orchestrator.rs b/crates/codegraph-extract/src/orchestrator.rs index 31b0ee8e6..92036ffa9 100644 --- a/crates/codegraph-extract/src/orchestrator.rs +++ b/crates/codegraph-extract/src/orchestrator.rs @@ -4,10 +4,11 @@ //! index rồi ingest lại (register + remap + resolve + persist + bump version). use crate::config::ExtractConfig; +use crate::languages::effects::{self, EffectClassifier}; use crate::{walker, LangParser}; use camino::Utf8Path; use codegraph_core::Result; -use codegraph_graph::{GraphIndex, ParseResult}; +use codegraph_graph::{GraphIndex, IngestProgress, ParseResult}; use indicatif::{ProgressBar, ProgressStyle}; use rayon::prelude::*; use std::sync::Arc; @@ -34,6 +35,19 @@ impl Orchestrator { Self::new(crate::registry()) } + /// Walk `root` → parse song song → trả về `(parsed, stats)`, KHÔNG ingest. + /// + /// Dùng cho benchmark để tách riêng thời gian của codegraph-extract (walk + + /// parse) khỏi codegraph-graph (ingest). Đi xe cùng logic với `index_all` qua + /// `parse_files`. + pub fn parse_project(&self, root: &Utf8Path) -> Result<(Vec, ExtractStats)> { + let config = ExtractConfig::load(root); + let files = walker::walk(root, &self.parsers, &config); + let (parsed, skipped) = self.parse_files(&files, None, config.effect_classifier.clone()); + let stats = stats_of(&parsed, skipped); + Ok((parsed, stats)) + } + /// Walk `root` → parse song song → ingest (full re-index). pub async fn index_all( &self, @@ -44,18 +58,15 @@ impl Orchestrator { let config = ExtractConfig::load(root); let files = walker::walk(root, &self.parsers, &config); - // Create progress bar if requested. - let pb = if let Some(ref bar) = progress { + // Create progress bar if requested (nếu progress `None` → invisible bar). + let pb0 = if let Some(ref bar) = progress { bar.clone() } else { - // Dummy hidden bar when no progress requested – we just skip. - // Use a zero-length bar to avoid allocations. Arc::new(ProgressBar::hidden()) }; - // Set total length for real bar. if progress.is_some() { - pb.set_length(files.len() as u64); - pb.set_style( + pb0.set_length(files.len() as u64); + pb0.set_style( ProgressStyle::default_bar() .template("[{elapsed_precise}] [{wide_bar}] {pos}/{len} ({percent}%)") .expect("valid progress bar template") @@ -63,11 +74,39 @@ impl Orchestrator { ); } - // Use a clone of the progress bar for thread-safe updates. + let (parsed, skipped) = + self.parse_files(&files, progress.clone(), config.effect_classifier.clone()); + + // Đưa ProgressBar vào ingest (register → edges → files → engines) — phase + // index chiếm phần lớn thời gian, không thể để im trong lúc `GraphIndex` + // ghi sqlite. + let ingest_progress: Option> = progress + .as_ref() + .map(|bar| Arc::new(IngestBar(bar.clone())) as Arc); + index.ingest_with_progress(&parsed, ingest_progress).await?; + // Finish the progress bar on success. + if let Some(bar) = progress { + bar.finish_with_message("Indexing complete"); + } + Ok(stats_of(&parsed, skipped)) + } + + /// Parse song song một danh sách file — trả về parsed + số file bị skip. + fn parse_files( + &self, + files: &[walker::FileMatch], + progress: Option>, + classifier: EffectClassifier, + ) -> (Vec, u64) { + let classifier = Arc::new(classifier); let progress_opt = progress.clone(); let results: Vec<_> = files .par_iter() .map(|fm| { + // Classifier là ambient config — install vào thread-local của + // worker thread trước khi parse file (rayon reuse thread, mỗi + // job set lại cho chắc). + effects::install_current(Some(classifier.clone())); let res = parse_one(fm); if let Some(ref bar) = progress_opt { bar.inc(1); @@ -85,13 +124,25 @@ impl Orchestrator { Err(_) => {} } } + (parsed, skipped) + } +} - index.ingest(&parsed).await?; - // Finish the progress bar on success. - if let Some(bar) = progress { - bar.finish_with_message("Indexing complete"); +/// Nối `IngestProgress` (graph crate) vào `indicatif::ProgressBar` của CLI: +/// `phase` reset bar về 0 + set length theo số đơn vị phase (không hiện chữ — +/// template chỉ `pos/len/percent`), `advance` tăng pos. +struct IngestBar(Arc); + +impl IngestProgress for IngestBar { + fn phase(&self, _name: &'static str, total: usize) { + if total > 0 { + self.0.set_length(total as u64); + self.0.set_position(0); } - Ok(stats_of(&parsed, skipped)) + } + + fn advance(&self, n: usize) { + self.0.inc(n as u64); } } @@ -117,3 +168,68 @@ fn parse_one(fm: &walker::FileMatch) -> Result> { }; fm.parser.parse_file(fm.path.as_str(), source).map(Some) } + +#[cfg(test)] +mod tests { + use super::*; + use camino::Utf8PathBuf; + use std::io::Write; + + /// Tạo fixture repo temp với 2 file (rust + go) rồi chạy `parse_project`. + #[test] + fn parse_project_walks_and_parses_without_ingest() { + let dir = tempfile::tempdir().unwrap(); + let root = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + let src = root.join("src"); + std::fs::create_dir_all(src.as_std_path()).unwrap(); + for (name, content) in [ + ( + "lib.rs", + "pub fn add(a: i32, b: i32) -> i32 { a + b }\npub fn sub(a: i32, b: i32) -> i32 { a - b }\n", + ), + ( + "main.go", + "package main\nfunc greet(name string) string { return \"hi \" + name }\n", + ), + ] { + let mut f = std::fs::File::create(src.join(name).as_std_path()).unwrap(); + f.write_all(content.as_bytes()).unwrap(); + } + + let orch = Orchestrator::with_registry(); + let (parsed, stats) = orch.parse_project(&root).unwrap(); + + // Cả 2 file được parse, không file nào bị skip. + assert_eq!(parsed.len(), 2, "phải parse được cả lib.rs + main.go"); + assert_eq!(stats.files, 2); + assert_eq!(stats.skipped, 0); + assert!( + parsed.iter().all(|p| !p.symbols.is_empty()), + "mỗi file phải có symbol" + ); + assert!(stats.symbols > 0, "tổng symbol > 0"); + // stats khớp với chính parsed (không ingest thêm gì). + assert_eq!( + stats.symbols, + parsed.iter().map(|p| p.symbols.len() as u64).sum::() + ); + } + + /// File không đọc được / quá lớn / không UTF-8 → bị đếm vào `skipped`. + #[test] + fn parse_project_counts_skipped_non_utf8() { + let dir = tempfile::tempdir().unwrap(); + let root = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + let src = root.join("src"); + std::fs::create_dir_all(src.as_std_path()).unwrap(); + let mut f = std::fs::File::create(src.join("bin.rs").as_std_path()).unwrap(); + // Rust file chứa byte không hợp lệ UTF-8 nhưng đủ nhỏ → skip (không UTF-8). + f.write_all(&[0xff, 0xfe, 0x00, 0x01, 0x02]).unwrap(); + + let orch = Orchestrator::with_registry(); + let (parsed, stats) = orch.parse_project(&root).unwrap(); + assert_eq!(parsed.len(), 0); + assert_eq!(stats.files, 0); + assert_eq!(stats.skipped, 1); + } +} diff --git a/crates/codegraph-extract/src/project.rs b/crates/codegraph-extract/src/project.rs new file mode 100644 index 000000000..379f231f6 --- /dev/null +++ b/crates/codegraph-extract/src/project.rs @@ -0,0 +1,73 @@ +//! Project scaffolding: `.codegraph/` layout, paths, and `init_project`. +//! +//! Tách riêng phần init (trước đây nằm inline trong CLI `cmd_init`) để cả CLI +//! và MCP server (`codegraph_init` tool) dùng chung. + +use crate::config::DEFAULT_CONFIG_TOML; +use camino::{Utf8Path, Utf8PathBuf}; +use codegraph_core::Result; + +/// Thư mục `.codegraph/` trong workspace root. +pub const CODEGRAPH_DIR: &str = ".codegraph"; + +/// Tên file sqlite index bên trong `.codegraph/`. +const DB_FILE: &str = "db.sqlite"; + +/// Đường dẫn thư mục `.codegraph/` của `root`. +pub fn project_dir(root: &Utf8Path) -> Utf8PathBuf { + root.join(CODEGRAPH_DIR) +} + +/// Đường dẫn file index sqlite: `root/.codegraph/db.sqlite`. +pub fn project_db_path(root: &Utf8Path) -> Utf8PathBuf { + project_dir(root).join(DB_FILE) +} + +/// Khởi tạo `.codegraph/` trong `root` (idempotent): tạo thư mục, viết +/// `.gitignore`, `version`, và `config.toml` (chỉ khi chưa có). Trả về đường +/// dẫn thư mục `.codegraph`. +pub fn init_project(root: &Utf8Path) -> Result { + let dir = project_dir(root); + std::fs::create_dir_all(&dir)?; + std::fs::write(dir.join(".gitignore"), "*\n")?; + std::fs::write(dir.join("version"), env!("CARGO_PKG_VERSION"))?; + let config_path = dir.join("config.toml"); + if !config_path.exists() { + std::fs::write(&config_path, DEFAULT_CONFIG_TOML)?; + } + Ok(dir) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn init_project_creates_layout_and_is_idempotent() { + let dir = tempfile::tempdir().unwrap(); + let root = Utf8Path::from_path(dir.path()).unwrap(); + + let first = init_project(root).unwrap(); + assert_eq!(first, project_dir(root)); + assert!(first.join(".gitignore").is_file()); + assert!(first.join("version").is_file()); + assert!(first.join("config.toml").is_file()); + let gitignore = std::fs::read_to_string(first.join(".gitignore")).unwrap(); + assert_eq!(gitignore, "*\n"); + + // Lần gọi thứ hai — không lỗi, config.toml giữ nguyên. + let config = std::fs::read_to_string(first.join("config.toml")).unwrap(); + init_project(root).unwrap(); + assert_eq!( + std::fs::read_to_string(first.join("config.toml")).unwrap(), + config + ); + } + + #[test] + fn project_db_path_joins_under_codegraph() { + let root = Utf8Path::new("/repo"); + assert_eq!(project_dir(root).as_str(), "/repo/.codegraph"); + assert_eq!(project_db_path(root).as_str(), "/repo/.codegraph/db.sqlite"); + } +} diff --git a/crates/codegraph-extract/src/walker.rs b/crates/codegraph-extract/src/walker.rs index 644089d29..a1b097cf4 100644 --- a/crates/codegraph-extract/src/walker.rs +++ b/crates/codegraph-extract/src/walker.rs @@ -29,7 +29,10 @@ pub fn build_ext_map(parsers: &[Arc]) -> ExtMap { ext_map } -fn find_parser<'a>(parsers: &'a [Arc], lang: &str) -> Option<&'a Arc> { +fn find_parser<'a>( + parsers: &'a [Arc], + lang: &str, +) -> Option<&'a Arc> { parsers.iter().find(|p| p.name() == lang) } @@ -214,6 +217,7 @@ mod tests { let parsers = registry(); let config = ExtractConfig { header_language: HeaderLanguage::Cpp, + effect_classifier: Default::default(), }; let matches = walk(&root, &parsers, &config); let h = matches diff --git a/crates/codegraph-extract/tests/chains.rs b/crates/codegraph-extract/tests/chains.rs index dd2ad94f7..c4a52665b 100644 --- a/crates/codegraph-extract/tests/chains.rs +++ b/crates/codegraph-extract/tests/chains.rs @@ -3,7 +3,7 @@ //! Chain của 1 hàm = `[owner_id, m1, callee, m2, ...]`; assertion dưới đây render //! phần walk (bỏ owner) thành tên marker (`[LOOP]`, `[IF_TRUE]`, ...) và tên callee. -use codegraph_core::marker_name; +use codegraph_core::{marker_name, SymbolKind}; use codegraph_extract::registry; fn walk(lang: &str, src: &str) -> Vec { @@ -12,13 +12,27 @@ fn walk(lang: &str, src: &str) -> Vec { .find(|p| p.name() == lang) .unwrap_or_else(|| panic!("no parser {lang}")); let res = parser.parse_file("golden.test", src).expect("parse"); + // Class-like symbol giờ cũng có chain tối thiểu `[owner]` — golden test này + // chỉ xét chain của function/method nên lọc theo owner kind. + let func_owner: std::collections::HashSet = res + .symbols + .iter() + .filter(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method)) + .map(|s| s.id) + .collect(); + let func_chains: Vec<&Vec> = res + .chains + .iter() + .filter(|(id, _)| func_owner.contains(id)) + .map(|(_, c)| c) + .collect(); assert_eq!( - res.chains.len(), + func_chains.len(), 1, "{lang}: expected exactly 1 function chain, got {:?}", - res.chains.keys().collect::>() + func_chains ); - let chain = res.chains.values().next().unwrap(); + let chain = func_chains[0]; // Placeholder 0 chưa resolve — render qua CallRecord (position = index trong chain). let name_at = |i: usize, id: u64| -> String { if let Some(m) = marker_name(id) { @@ -59,7 +73,16 @@ def process(x): ); assert_eq!( c, - ["[LOOP]", "[IF_TRUE]", "save", "[IF_FALSE]", "skip", "[BRANCH_END]", "[LOOP_BACK]", "[RETURN]"] + [ + "[LOOP]", + "[IF_TRUE]", + "save", + "[IF_FALSE]", + "skip", + "[BRANCH_END]", + "[LOOP_BACK]", + "[RETURN]" + ] ); } @@ -107,7 +130,14 @@ def process(x): ); assert_eq!( c, - ["[SWITCH_CASE]", "one", "[SWITCH_END]", "[SWITCH_CASE]", "other", "[SWITCH_END]"] + [ + "[SWITCH_CASE]", + "one", + "[SWITCH_END]", + "[SWITCH_CASE]", + "other", + "[SWITCH_END]" + ] ); } @@ -130,7 +160,14 @@ class Foo { ); assert_eq!( c, - ["obj.run", "[IF_TRUE]", "this.helper", "[IF_FALSE]", "fallback", "[BRANCH_END]"] + [ + "obj.run", + "[IF_TRUE]", + "this.helper", + "[IF_FALSE]", + "fallback", + "[BRANCH_END]" + ] ); } @@ -188,7 +225,15 @@ end ); assert_eq!( c, - ["[IF_TRUE]", "validate", "[IF_TRUE]", "warn", "fail", "[BRANCH_END]", "[BRANCH_END]"] + [ + "[IF_TRUE]", + "validate", + "[IF_TRUE]", + "warn", + "fail", + "[BRANCH_END]", + "[BRANCH_END]" + ] ); } @@ -327,7 +372,15 @@ fn f(x: i32) -> i32 { ); assert_eq!( c, - ["[SWITCH_CASE]", "one", "[SWITCH_END]", "[SWITCH_CASE]", "other", "[SWITCH_END]", "[RETURN]"] + [ + "[SWITCH_CASE]", + "one", + "[SWITCH_END]", + "[SWITCH_CASE]", + "other", + "[SWITCH_END]", + "[RETURN]" + ] ); } @@ -348,7 +401,15 @@ class Foo { ); assert_eq!( c, - ["[SWITCH_CASE]", "a", "[BREAK]", "[SWITCH_END]", "[SWITCH_CASE]", "b", "[SWITCH_END]"] + [ + "[SWITCH_CASE]", + "a", + "[BREAK]", + "[SWITCH_END]", + "[SWITCH_CASE]", + "b", + "[SWITCH_END]" + ] ); } @@ -372,7 +433,17 @@ end ); assert_eq!( c, - ["[IF_TRUE]", "validate", "[IF_FALSE]", "fail", "[BRANCH_END]", "[LOOP]", "save", "[LOOP_BACK]", "[RETURN]"] + [ + "[IF_TRUE]", + "validate", + "[IF_FALSE]", + "fail", + "[BRANCH_END]", + "[LOOP]", + "save", + "[LOOP_BACK]", + "[RETURN]" + ] ); } @@ -394,7 +465,14 @@ function process($x) { ); assert_eq!( c, - ["[LOOP]", "save", "[LOOP_BACK]", "obj.method", "self.run", "[RETURN]"] + [ + "[LOOP]", + "save", + "[LOOP_BACK]", + "obj.method", + "self.run", + "[RETURN]" + ] ); } @@ -414,7 +492,15 @@ def f(x: Int) = { ); assert_eq!( c, - ["[SWITCH_CASE]", "one", "[SWITCH_END]", "[SWITCH_CASE]", "other", "[SWITCH_END]", "[RETURN]"] + [ + "[SWITCH_CASE]", + "one", + "[SWITCH_END]", + "[SWITCH_CASE]", + "other", + "[SWITCH_END]", + "[RETURN]" + ] ); } @@ -434,7 +520,14 @@ int add(int a, int b) { ); assert_eq!( c, - ["[IF_TRUE]", "[RETURN]", "compute", "[IF_FALSE]", "[RETURN]", "[BRANCH_END]"] + [ + "[IF_TRUE]", + "[RETURN]", + "compute", + "[IF_FALSE]", + "[RETURN]", + "[BRANCH_END]" + ] ); } @@ -484,10 +577,7 @@ int f(int x) { } "#, ); - assert_eq!( - c, - ["[LOOP]", "e", "a", "b", "c", "[LOOP_BACK]", "[RETURN]"] - ); + assert_eq!(c, ["[LOOP]", "e", "a", "b", "c", "[LOOP_BACK]", "[RETURN]"]); } /// Text condition của `if` được giữ làm metadata (CallRecord.condition của call @@ -575,10 +665,7 @@ func f() { } "#, ); - assert_eq!( - c, - ["[LOOP]", "a", "b", "c", "d", "[LOOP_BACK]", "[RETURN]"] - ); + assert_eq!(c, ["[LOOP]", "a", "b", "c", "d", "[LOOP_BACK]", "[RETURN]"]); } /// Switch discriminant (`switch (getType(x))`) cũng vào chain trước các case. @@ -630,3 +717,203 @@ class Foo { ); assert_eq!(c, ["[RETURN]", "a.run(abc.class).exec", "a.run"]); } + +/// Bug A: string-literal case labels (`case 'optimize_text':`) — dispatch key +/// theo chuỗi không phải identifier call. Emit thành call-name ảo (placeholder +/// `0` + CallRecord) để `search_by_call` index được. `default` không có value → +/// không emit. +#[test] +fn ts_switch_string_case_labels_captured_as_call_names() { + let c = walk( + "typescript", + r#" +function dispatch(name: string): number { + switch (name) { + case 'optimize_text': return 1; + case "get_cached": return 2; + default: return 0; + } +} +"#, + ); + assert_eq!( + c, + [ + "[SWITCH_CASE]", + "optimize_text", + "[RETURN]", + "[SWITCH_END]", + "[SWITCH_CASE]", + "get_cached", + "[RETURN]", + "[SWITCH_END]", + "[SWITCH_CASE]", + "[RETURN]", + "[SWITCH_END]", + ] + ); +} + +/// Bug B: class có chain tối thiểu `[class_id]` — `flow`/`search_flow` không bị +/// "chain not found" (trước đây chỉ có edge function→class từ phía caller). +/// Methods của class vẫn có chain riêng. +#[test] +fn ts_class_has_minimal_chain() { + let parser = registry() + .into_iter() + .find(|p| p.name() == "typescript") + .expect("ts parser"); + let res = parser + .parse_file( + "golden.test", + r#" +class Store { + save(k: string): void {} + get(k: string): string { return ""; } +} +"#, + ) + .expect("parse"); + let class_id = res + .symbols + .iter() + .find(|s| s.name == "Store" && matches!(s.kind, SymbolKind::Class)) + .expect("Store class symbol") + .id; + let chain = res.chains.get(&class_id).expect("class chain"); + assert_eq!(chain, &vec![class_id]); +} + +/// Cùng tên method (`save`) trong 2 class khác nhau — `func_index` key theo +/// `(name, line)` nên mỗi method có id riêng (đúng scope_id của class nó); mỗi +/// method và mỗi class đều có chain riêng, không hoà trộn. +#[test] +fn ts_duplicate_method_name_across_two_classes() { + let parser = registry() + .into_iter() + .find(|p| p.name() == "typescript") + .expect("ts parser"); + let res = parser + .parse_file( + "golden.test", + r#" +class ServiceA { + save(k: string): void {} + load(k: string): string { return ""; } +} +class ServiceB { + save(k: string): void {} + load(k: string): string { return ""; } +} +"#, + ) + .expect("parse"); + + let classes: Vec<&codegraph_core::Symbol> = res + .symbols + .iter() + .filter(|s| matches!(s.kind, SymbolKind::Class)) + .collect(); + assert_eq!(classes.len(), 2, "exactly 2 classes"); + + // Mỗi method `save`/`load` có id riêng và thuộc đúng class của nó. + let saves: Vec<&codegraph_core::Symbol> = res + .symbols + .iter() + .filter(|s| s.name == "save" && matches!(s.kind, SymbolKind::Method)) + .collect(); + assert_eq!(saves.len(), 2); + assert_ne!(saves[0].id, saves[1].id); + assert_ne!(saves[0].scope_id, saves[1].scope_id); + assert!([classes[0].id, classes[1].id].contains(&saves[0].scope_id)); + assert!([classes[0].id, classes[1].id].contains(&saves[1].scope_id)); + + // Mỗi method đều có chain riêng bắt đầu bằng id chính nó. + for s in saves { + let chain = res.chains.get(&s.id).expect("method chain"); + assert_eq!(chain.first(), Some(&s.id)); + } + // Mỗi class có chain tối thiểu `[class_id]` riêng biệt. + for c in &classes { + let chain = res.chains.get(&c.id).expect("class chain"); + assert_eq!(chain, &vec![c.id]); + } + assert_ne!(classes[0].id, classes[1].id); +} + +/// Cùng tên class (`Registry`) khai báo 2 lần ở 2 line khác nhau — key +/// `(name, line)` phân biệt được; mỗi class có chain riêng. +#[test] +fn ts_duplicate_class_name_different_lines() { + let parser = registry() + .into_iter() + .find(|p| p.name() == "typescript") + .expect("ts parser"); + let res = parser + .parse_file( + "golden.test", + r#" +class Registry { + put(k: string): void {} +} +class Registry { + get(k: string): void {} +} +"#, + ) + .expect("parse"); + let registries: Vec<&codegraph_core::Symbol> = res + .symbols + .iter() + .filter(|s| s.name == "Registry" && matches!(s.kind, SymbolKind::Class)) + .collect(); + assert_eq!(registries.len(), 2, "both Registry declarations indexed"); + assert_ne!(registries[0].id, registries[1].id); + let mut chains: Vec = registries + .iter() + .filter_map(|c| res.chains.get(&c.id)) + .map(|chain| chain[0]) + .collect(); + chains.sort_unstable(); + let mut expected = vec![registries[0].id, registries[1].id]; + expected.sort_unstable(); + assert_eq!(chains, expected); +} + +/// Mirror `OptimizationStorageTool.run` (Bug A): dispatch theo string-literal +/// operation (`case 'store'` / `case 'retrieve'`) — case label vừa được emit +/// thành call-name ảo, vừa không che member call thật bên trong body +/// (`s.save`/`s.get`) + `break`. `default` không có value → không emit call. +#[test] +fn ts_switch_string_operation_dispatch_with_member_calls() { + let c = walk( + "typescript", + r#" +function runStorage(op: string, s: Store): void { + switch (op) { + case 'store': s.save(k); break; + case 'retrieve': s.get(k); break; + default: break; + } +} +"#, + ); + assert_eq!( + c, + [ + "[SWITCH_CASE]", + "store", + "s.save", + "[BREAK]", + "[SWITCH_END]", + "[SWITCH_CASE]", + "retrieve", + "s.get", + "[BREAK]", + "[SWITCH_END]", + "[SWITCH_CASE]", + "[BREAK]", + "[SWITCH_END]", + ] + ); +} diff --git a/crates/codegraph-extract/tests/cpp_functions.rs b/crates/codegraph-extract/tests/cpp_functions.rs index 608b9093e..91b891041 100644 --- a/crates/codegraph-extract/tests/cpp_functions.rs +++ b/crates/codegraph-extract/tests/cpp_functions.rs @@ -45,10 +45,7 @@ fn cpp_out_of_class_ctor_with_specifiers_issue_9() { ("NodiscardWidget", "[[nodiscard]]"), ("CustomWidget", "_CUSTOM_ATTRIBUTE"), ] { - let ctor = out_of_class - .iter() - .filter(|(n, _)| n == class) - .count(); + let ctor = out_of_class.iter().filter(|(n, _)| n == class).count(); assert_eq!(ctor, 3, "{class} phải có 3 ctor, got {out_of_class:?}"); let dtor = out_of_class .iter() diff --git a/crates/codegraph-extract/tests/effects_config.rs b/crates/codegraph-extract/tests/effects_config.rs new file mode 100644 index 000000000..d659cc6c5 --- /dev/null +++ b/crates/codegraph-extract/tests/effects_config.rs @@ -0,0 +1,68 @@ +//! Golden: project effect rules (installed classifier) reach `CallRecord.effect` +//! qua pipeline parse thật — chứng minh `[[effect_rules]]` config override +//! được bảng default và đến được call record (đầu vào của graph ingest). + +use codegraph_core::{EffectCallPattern, EffectRule, EffectType}; +use codegraph_extract::languages::effects::{install_current, EffectClassifier}; +use codegraph_extract::registry; +use std::sync::Arc; + +fn effects_of(lang: &str, src: &str) -> Vec<(String, EffectType)> { + let parser = registry() + .into_iter() + .find(|p| p.name() == lang) + .unwrap_or_else(|| panic!("no parser {lang}")); + let res = parser.parse_file("effects.test", src).expect("parse"); + res.calls + .iter() + .map(|c| (c.call_name.clone(), c.effect)) + .collect() +} + +/// Config rules (exact + prefix) override defaults và hiện ra trên CallRecord. +#[test] +fn configured_rules_reach_call_record_effect() { + let rules = vec![ + EffectRule { + call: EffectCallPattern::Exact { + exact: "sendEmail".to_string(), + }, + effect: EffectType::EventEmit, + }, + EffectRule { + call: EffectCallPattern::Prefix { + prefix: "legacy.".to_string(), + }, + effect: EffectType::FileWrite, + }, + ]; + install_current(Some(Arc::new(EffectClassifier::with_config(rules)))); + + let effects = effects_of( + "javascript", + "function f() { sendEmail(\"x\"); legacy.write(); db.Query(); }", + ); + let get = |name: &str| -> EffectType { + effects + .iter() + .find(|(n, _)| n == name) + .map(|(_, e)| *e) + .unwrap_or_else(|| panic!("call {name} not captured: {effects:?}")) + }; + // Config rule thắng (default cho "sendEmail" là None). + assert_eq!(get("sendEmail"), EffectType::EventEmit); + // Config prefix "legacy." thắng (default None). + assert_eq!(get("legacy.write"), EffectType::FileWrite); + // Không có config rule → rơi về bảng default. + assert_eq!(get("db.Query"), EffectType::SqlQuery); + + install_current(None); +} + +/// Không install classifier → parse dùng bảng default (behavior cũ). +#[test] +fn default_classifier_when_not_installed() { + install_current(None); + let effects = effects_of("javascript", "function f() { db.Exec(); }"); + assert_eq!(effects[0].1, EffectType::SqlWrite); +} diff --git a/crates/codegraph-extract/tests/extract.rs b/crates/codegraph-extract/tests/extract.rs index 2cbcc8e88..f8bc95163 100644 --- a/crates/codegraph-extract/tests/extract.rs +++ b/crates/codegraph-extract/tests/extract.rs @@ -14,7 +14,10 @@ fn fixture_root() -> Utf8PathBuf { async fn index_fixtures() -> (GraphIndex, codegraph_extract::ExtractStats) { let mut index = GraphIndex::in_memory(); let orch = Orchestrator::with_registry(); - let stats = orch.index_all(&fixture_root(), &mut index, None).await.unwrap(); + let stats = orch + .index_all(&fixture_root(), &mut index, None) + .await + .unwrap(); (index, stats) } @@ -87,18 +90,12 @@ async fn chains_are_built_for_each_function() { assert!(stats.chains > 0, "expected chains in index"); // Flow của một function trả về chain có marker hoặc ít nhất là chính nó. - let hits = index - .search_symbol("process_user", None, 10) - .await - .unwrap(); + let hits = index.search_symbol("process_user", None, 10).await.unwrap(); let py = hits .iter() .find(|s| s.language == "python") .expect("python process_user"); let flow = index.flow(py.id).await.unwrap(); - assert!( - !flow.chain.is_empty(), - "chain phải chứa chính function id" - ); + assert!(!flow.chain.is_empty(), "chain phải chứa chính function id"); assert_eq!(flow.chain[0], py.id, "chain bắt đầu bằng owner"); } diff --git a/crates/codegraph-extract/tests/fixtures/basic_functions.go b/crates/codegraph-extract/tests/fixtures/basic_functions.go new file mode 100644 index 000000000..f7dfa4c0c --- /dev/null +++ b/crates/codegraph-extract/tests/fixtures/basic_functions.go @@ -0,0 +1,11 @@ +package main + +import "os" + +func realMain() int { + return 0 +} + +func main() { + os.Exit(realMain()) +} \ No newline at end of file diff --git a/crates/codegraph-extract/tests/fixtures/control_flow.go b/crates/codegraph-extract/tests/fixtures/control_flow.go new file mode 100644 index 000000000..f0a79eb49 --- /dev/null +++ b/crates/codegraph-extract/tests/fixtures/control_flow.go @@ -0,0 +1,9 @@ +package main + +func process(x int) int { + if x > 0 { + return x + } else { + return 0 + } +} \ No newline at end of file diff --git a/crates/codegraph-extract/tests/fixtures/multi_package_cache.go b/crates/codegraph-extract/tests/fixtures/multi_package_cache.go new file mode 100644 index 000000000..bfbc3bb17 --- /dev/null +++ b/crates/codegraph-extract/tests/fixtures/multi_package_cache.go @@ -0,0 +1,3 @@ +package cache + +func process() {} \ No newline at end of file diff --git a/crates/codegraph-extract/tests/fixtures/multi_package_store.go b/crates/codegraph-extract/tests/fixtures/multi_package_store.go new file mode 100644 index 000000000..5ec115e79 --- /dev/null +++ b/crates/codegraph-extract/tests/fixtures/multi_package_store.go @@ -0,0 +1,3 @@ +package store + +func process() {} \ No newline at end of file diff --git a/crates/codegraph-extract/tests/fixtures/struct_methods.go b/crates/codegraph-extract/tests/fixtures/struct_methods.go new file mode 100644 index 000000000..a6b720cd5 --- /dev/null +++ b/crates/codegraph-extract/tests/fixtures/struct_methods.go @@ -0,0 +1,14 @@ +package main + +type UserService struct { + Name string +} + +func (u *UserService) Greet() string { + return "Hello, " + u.Name +} + +func main() { + svc := UserService{Name: "Alice"} + svc.Greet() +} \ No newline at end of file diff --git a/crates/codegraph-extract/tests/go_extract_test.rs b/crates/codegraph-extract/tests/go_extract_test.rs new file mode 100644 index 000000000..064c8a964 --- /dev/null +++ b/crates/codegraph-extract/tests/go_extract_test.rs @@ -0,0 +1,261 @@ +use codegraph_core::{SymbolKind, MARKER_IF_FALSE, MARKER_IF_TRUE}; +use codegraph_extract::languages::go::GoParser; +use codegraph_extract::LangParser; +use std::path::Path; + +#[test] +fn test_extract_basic_functions() { + let parser = GoParser::new(); + let path = Path::new("tests/fixtures/basic_functions.go"); + let source = std::fs::read_to_string(path).expect("Failed to read file"); + let result = parser + .parse_file(path.to_str().unwrap(), &source) + .expect("Failed to parse file"); + + // Check symbols + // We expect 2 symbols (main and realMain) plus potentially other symbols like imports + let main_and_realmain = result + .symbols + .iter() + .filter(|s| s.name == "main" || s.name == "realMain") + .count(); + assert_eq!( + main_and_realmain, 2, + "Expected 2 symbols (main and realMain)" + ); + + // Find main function + let main_symbol = result + .symbols + .iter() + .find(|s| s.name == "main") + .expect("Main function not found"); + assert_eq!(main_symbol.kind, SymbolKind::Function); + + // Find realMain function + let real_main_symbol = result + .symbols + .iter() + .find(|s| s.name == "realMain") + .expect("realMain function not found"); + assert_eq!(real_main_symbol.kind, SymbolKind::Function); + + // Check chains + let main_chain = result + .chains + .get(&main_symbol.id) + .expect("Main function chain not found"); + // We expect at least 2 elements in the chain (main -> realMain) + assert!( + main_chain.len() >= 2, + "Expected chain of at least length 2 (main -> realMain)" + ); + // Check call records instead of chain since the chain contains placeholders during extraction + let real_main_call = result.calls.iter().find(|c| c.call_name == "realMain"); + assert!( + real_main_call.is_some(), + "Expected to find call to realMain" + ); + assert_eq!( + real_main_call.unwrap().caller_id, + main_symbol.id, + "Expected main to call realMain" + ); + // Check call records instead of chain since the chain contains placeholders during extraction + let real_main_call = result.calls.iter().find(|c| c.call_name == "realMain"); + assert!( + real_main_call.is_some(), + "Expected to find call to realMain" + ); + assert_eq!( + real_main_call.unwrap().caller_id, + main_symbol.id, + "Expected main to call realMain" + ); + + // Check calls + // Check that we have at least one call to realMain + let real_main_calls = result + .calls + .iter() + .filter(|c| c.call_name == "realMain") + .count(); + assert!( + real_main_calls > 0, + "Expected at least one call to realMain" + ); + + // Check that the call is from main + let real_main_call = result + .calls + .iter() + .find(|c| c.call_name == "realMain") + .unwrap(); + assert_eq!( + real_main_call.caller_id, main_symbol.id, + "Expected main to call realMain" + ); +} + +#[test] +fn test_extract_struct_methods() { + let parser = GoParser::new(); + let path = Path::new("tests/fixtures/struct_methods.go"); + let source = std::fs::read_to_string(path).expect("Failed to read file"); + let result = parser + .parse_file(path.to_str().unwrap(), &source) + .expect("Failed to parse file"); + + // Check symbols + // We expect 3 symbols (UserService, Greet, main) plus potentially other symbols + let expected_symbols = result + .symbols + .iter() + .filter(|s| s.name == "UserService" || s.name == "Greet" || s.name == "main") + .count(); + assert_eq!( + expected_symbols, 3, + "Expected 3 symbols (UserService, Greet, main)" + ); + + // Find UserService struct + let user_service_symbol = result + .symbols + .iter() + .find(|s| s.name == "UserService") + .expect("UserService struct not found"); + assert_eq!(user_service_symbol.kind, SymbolKind::Class); + + // Find Greet method + let greet_symbol = result + .symbols + .iter() + .find(|s| s.name == "Greet") + .expect("Greet method not found"); + assert_eq!(greet_symbol.kind, SymbolKind::Method); + + // Find main function + let main_symbol = result + .symbols + .iter() + .find(|s| s.name == "main") + .expect("Main function not found"); + assert_eq!(main_symbol.kind, SymbolKind::Function); + + // Check that we have at least one call from main + let main_calls = result + .calls + .iter() + .filter(|c| c.caller_id == main_symbol.id) + .count(); + assert!(main_calls > 0, "Expected at least one call from main"); + + // Debug: print all calls + println!("All calls:"); + for call in &result.calls { + println!( + " Caller ID: {}, Call name: {}, Line: {}", + call.caller_id, call.call_name, call.line + ); + } + + // Check for any method call from main + let main_calls = result + .calls + .iter() + .filter(|c| c.caller_id == main_symbol.id) + .count(); + println!("Found {} calls from main", main_calls); + assert!(main_calls > 0, "Expected at least one call from main"); + + // For now, just verify that we have calls from main + // The exact call name might be different (e.g., "svc.Greet" instead of "Greet") +} + +#[test] +fn test_extract_control_flow() { + let parser = GoParser::new(); + let path = Path::new("tests/fixtures/control_flow.go"); + let source = std::fs::read_to_string(path).expect("Failed to read file"); + let result = parser + .parse_file(path.to_str().unwrap(), &source) + .expect("Failed to parse file"); + + // Check symbols + // We expect at least 1 symbol (process) + let process_symbols = result + .symbols + .iter() + .filter(|s| s.name == "process") + .count(); + assert_eq!(process_symbols, 1, "Expected 1 symbol (process)"); + + // Find process function + let process_symbol = result + .symbols + .iter() + .find(|s| s.name == "process") + .expect("Process function not found"); + assert_eq!(process_symbol.kind, SymbolKind::Function); + + // Check chains + let process_chain = result + .chains + .get(&process_symbol.id) + .expect("Process function chain not found"); + + // Check for control flow markers + let mut found_if_true = false; + let mut found_if_false = false; + + for &item in process_chain { + if item == MARKER_IF_TRUE { + found_if_true = true; + } else if item == MARKER_IF_FALSE { + found_if_false = true; + } + } + + assert!(found_if_true, "Expected MARKER_IF_TRUE in chain"); + assert!(found_if_false, "Expected MARKER_IF_FALSE in chain"); +} + +#[test] +fn test_extract_multi_package() { + let parser = GoParser::new(); + + // Parse store package + let store_path = Path::new("tests/fixtures/multi_package_store.go"); + let store_source = std::fs::read_to_string(store_path).expect("Failed to read store file"); + let store_result = parser + .parse_file(store_path.to_str().unwrap(), &store_source) + .expect("Failed to parse store file"); + + // Parse cache package + let cache_path = Path::new("tests/fixtures/multi_package_cache.go"); + let cache_source = std::fs::read_to_string(cache_path).expect("Failed to read cache file"); + let cache_result = parser + .parse_file(cache_path.to_str().unwrap(), &cache_source) + .expect("Failed to parse cache file"); + + // Check symbols in store package + let store_process = store_result + .symbols + .iter() + .find(|s| s.name == "process") + .expect("Process function not found in store package"); + assert_eq!(store_process.kind, SymbolKind::Function); + + // Check symbols in cache package + let cache_process = cache_result + .symbols + .iter() + .find(|s| s.name == "process") + .expect("Process function not found in cache package"); + assert_eq!(cache_process.kind, SymbolKind::Function); + + // Verify the symbols have different IDs (they should be distinct) + // For now, we'll accept that the IDs might be the same during extraction + // The GraphIndex will handle proper scoping during ingestion + // This is expected behavior for the extraction phase +} diff --git a/crates/codegraph-graph/Cargo.toml b/crates/codegraph-graph/Cargo.toml index 271e73e59..4eba1c2d3 100644 --- a/crates/codegraph-graph/Cargo.toml +++ b/crates/codegraph-graph/Cargo.toml @@ -23,22 +23,31 @@ parking_lot = { workspace = true } smallvec = "1" async-trait = { workspace = true } thiserror = { workspace = true } +tokio = { workspace = true } # For SearchIndex functionality (moved from codegraph-libs) -redis = { version = "1.0", features = ["tokio-comp"], optional = true } -tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "sync", "time"] } +redis = { workspace = true, optional = true } +url = { version = "2.5.8", optional = true } zstd = { version = "0.13", optional = true } bincode = { version = "1.3", optional = true } sqlx = { workspace = true, optional = true } + # Bundled sqlite cho sqlx (giống rusqlite của codegraph-db) — feature # unification khiến sqlx dùng chung bản build bundled này, không cần system lib. libsqlite3-sys = { version = "0.30", features = ["bundled"], optional = true } [features] default = [] -redis = ["dep:redis", "dep:zstd", "dep:bincode"] +redis = ["dep:redis", "dep:zstd", "dep:bincode", "dep:url"] sqlite = ["dep:sqlx", "dep:libsqlite3-sys"] bloom-search = [] [dev-dependencies] tempfile = "3" +codegraph-extract = { path = "../codegraph-extract" } +codegraph-core = { path = "../codegraph-core" } +criterion = { version = "0.5", features = ["async_tokio"] } + +[[bench]] +name = "search_bloom" +harness = false diff --git a/crates/codegraph-graph/benches/search_bloom.rs b/crates/codegraph-graph/benches/search_bloom.rs new file mode 100644 index 000000000..74fa0146d --- /dev/null +++ b/crates/codegraph-graph/benches/search_bloom.rs @@ -0,0 +1,139 @@ +//! Benchmark thử nghiệm prune nhánh bằng bloom filter (feature `bloom-search`). +//! So sánh baseline (không bloom) vs có bloom — chạy 2 feature config: +//! +//! ```bash +//! cargo bench -p codegraph-graph --bench search_bloom # baseline +//! cargo bench -p codegraph-graph --bench search_bloom --features bloom-search +//! ``` +//! +//! Nhóm đo: +//! - `insert_*` — thông lượng insert (rõ chi phí duy trì bloom mỗi insert). +//! - `search_hit_*` — search pattern tồn tại (correctness, độ trễ có bloom). +//! - `search_miss_*` — search pattern KHÔNG tồn tại nhưng có chung prefix dài +//! (đây là nơi bloom prune nhánh rỗng và phát huy nhất). + +use codegraph_graph::Search; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; + +const N: usize = 4000; + +/// Sinh `n` keys có prefix dài dùng chung (radix sâu) — 4 prefixes lẫn nhau. +fn gen_keys(n: usize) -> Vec> { + (0..n) + .map(|i| { + let prefix = match i % 4 { + 0 => "alpha", + 1 => "beta", + 2 => "gamma", + _ => "delta", + }; + format!("{prefix}_{i:06}").into_bytes() + }) + .collect() +} + +/// Các pattern chắc chắn tồn tại (substring). +const HITS: &[&[u8]] = &[b"alpha", b"beta_000", b"lph", b"000042", b"elta"]; + +/// Các pattern KHÔNG tồn tại nhưng có prefix dài giống keys → DFS phải dò sâu +/// nhiều nhánh rồi mới biết không có (bloom có thể prune chúng). +const MISSES: &[&[u8]] = &[b"alpha_999999", b"betazzz", b"gamma_q", b"qwerty", b"zzzz"]; + +fn runtime() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() +} + +fn build_index(n: usize) -> Search { + runtime().block_on(async { + let mut search = Search::in_memory(16); + let keys = gen_keys(n); + for (i, key) in keys.iter().enumerate() { + let metas: Vec> = vec![None; key.len()]; + search.insert_chain(i + 1, key, &metas).await.unwrap(); + } + search + }) +} + +/// Cây sâu: nhiều keys, prefix chung rất dài → candidate-subtree lớn. Đây là +/// tình huống prune nhánh (bỏ cả nhánh con nhiều node) mới thực sự có lợi. +fn build_deep(n: usize) -> Search { + runtime().block_on(async { + let mut search = Search::in_memory(16); + for i in 0..n { + let key = format!("alpha_{i:06}").into_bytes(); + let metas: Vec> = vec![None; key.len()]; + search.insert_chain(i + 1, &key, &metas).await.unwrap(); + } + search + }) +} + +fn bench_insert(c: &mut Criterion) { + c.bench_function("insert_2000", |b| { + b.iter(|| { + runtime().block_on(async { + let mut search = Search::in_memory(16); + let keys = gen_keys(2000); + for (i, key) in keys.iter().enumerate() { + let metas: Vec> = vec![None; key.len()]; + search.insert_chain(i + 1, key, &metas).await.unwrap(); + } + black_box(&search); + }); + }); + }); +} + +fn bench_search(c: &mut Criterion) { + let search = build_index(N); + let rt = runtime(); + + c.bench_function("search_hit_5_patterns", |b| { + b.iter(|| { + rt.block_on(async { + for p in HITS { + let r = search.search(p, None).await; + let _ = black_box(r); + } + }); + }); + }); + + c.bench_function("search_miss_5_patterns", |b| { + b.iter(|| { + rt.block_on(async { + for p in MISSES { + let r = search.search(p, None).await; + let _ = black_box(r); + } + }); + }); + }); +} + +/// Subtree lớn: miss chỉ cần diverge ở cuối prefix dài → baseline dò toàn bộ +/// nhánh lớn, bloom prune được ngay sau prefix. +fn bench_search_deep(c: &mut Criterion) { + const DEEP: usize = 20_000; + let search = build_deep(DEEP); + let rt = runtime(); + + c.bench_function("deep_search_miss_4_patterns", |b| { + b.iter(|| { + rt.block_on(async { + for p in DEEP_MISSES { + let r = search.search(p, None).await; + let _ = black_box(r); + } + }); + }); + }); +} + +const DEEP_MISSES: &[&[u8]] = &[b"alpha_999999", b"alpha_888888", b"bet", b"zzzzzz"]; + +criterion_group!(benches, bench_insert, bench_search, bench_search_deep); +criterion_main!(benches); diff --git a/crates/codegraph-graph/src/bloom.rs b/crates/codegraph-graph/src/bloom.rs new file mode 100644 index 000000000..c2447b015 --- /dev/null +++ b/crates/codegraph-graph/src/bloom.rs @@ -0,0 +1,327 @@ +//! Bloom filter — cho phép kiểm tra "phần tử có tồn tại trong tập hợp không?" +//! +//! - **0 false negative**: nếu `contains` trả về `false` → chắc chắn không tồn tại +//! - **False positive**: có thể nói "có" khi thực tế không — tunable qua `m` và `k` + +// ==================== BloomFilter ==================== + +/// Bloom filter với `m` bits, `k` hash functions (Kirsch-Mitzenmacker optimization). +/// +/// ## Parameters +/// +/// | `m` (bits) | `k` (hashes) | Target items | False positive | +/// |---|---|---|---| +/// | 1024 | 7 | ~50 | ~1% | +/// | 2048 | 7 | ~100 | ~1% | +/// | 4096 | 10 | ~300 | ~0.1% | +/// | 8192 | 14 | ~800 | ~0.01% | +#[derive(Clone)] +pub struct BloomFilter { + /// Bit array (m bits). + bits: Vec, + /// Number of hash functions. + k: u64, + /// Total bits (m = bits.len() * 64). + #[allow(dead_code)] + m: u64, + /// Mask for fast modulo (m must be power of 2). + m_mask: u64, +} + +impl BloomFilter { + /// Tạo bloom filter với `m` bits, `k` hash functions. + /// + /// `m` được làm tròn lên thành power of 2 (để modulo nhanh). + pub fn new(m: usize, k: usize) -> Self { + let m = m.next_power_of_two().max(64); // tối thiểu 64 bits + let m_u64 = m / 64; + Self { + bits: vec![0u64; m_u64], + k: k as u64, + m: m as u64, + m_mask: (m - 1) as u64, + } + } + + /// Insert `data` vào bloom filter (set k bits tương ứng). + pub fn insert(&mut self, data: &[u8]) { + let (h1, h2) = Self::hash128(data); + let m_mask = self.m_mask; + + for i in 0..self.k { + let bit_pos = (h1.wrapping_add(i.wrapping_mul(h2))) & m_mask; + self.set_bit(bit_pos as usize); + } + } + + /// Kiểm tra `data` có khả năng tồn tại? + /// + /// - `true` → **có thể** tồn tại (hoặc false positive) + /// - `false` → **chắc chắn** không tồn tại + pub fn contains(&self, data: &[u8]) -> bool { + let (h1, h2) = Self::hash128(data); + let m_mask = self.m_mask; + + for i in 0..self.k { + let bit_pos = (h1.wrapping_add(i.wrapping_mul(h2))) & m_mask; + if !self.get_bit(bit_pos as usize) { + return false; + } + } + + true + } + + /// Merge bloom filter khác vào (bitwise OR). + /// Dùng khi split node để kết hợp bloom của node cha + leg. + #[allow(dead_code)] // API giữ nguyên — dùng khi kết hợp bloom của các node khi rebuild. + pub fn union(&mut self, other: &BloomFilter) { + assert_eq!(self.bits.len(), other.bits.len(), "bloom size mismatch"); + for (a, b) in self.bits.iter_mut().zip(other.bits.iter()) { + *a |= *b; + } + } + + /// Reset toàn bộ bits về 0. + #[allow(dead_code)] + pub fn clear(&mut self) { + for word in &mut self.bits { + *word = 0; + } + } + + // ── Public / crate-visible helpers ── + + /// Hash `data` thành 2 u64 độc lập (sip hash với seed 0 và 1). + #[inline] + pub(crate) fn hash128(data: &[u8]) -> (u64, u64) { + // Hằng số nhân của FxHash (64-bit) + const FX_PRIME: u64 = 0x517cc1b727220a95; + + // --- Tính Hash thứ nhất (h1) với Seed mặc định --- + let mut h1 = 0; + for &byte in data { + h1 = (h1 ^ byte as u64).wrapping_mul(FX_PRIME); + } + + // --- Tính Hash thứ hai (h2) với Seed khác biệt để đảm bảo độc lập --- + // Khởi tạo bằng một hằng số ngẫu nhiên lớn (Kẻ phá vỡ tính đối xứng) + let mut h2 = 0xa5a5a5a5a5a5a5a5; + for &byte in data { + h2 = (h2 ^ byte as u64).wrapping_mul(FX_PRIME); + } + + // Thực hiện thêm một bước xáo trộn bit cuối để triệt tiêu tương quan tuyến tính + let h1_final = h1 ^ (h1 >> 32); + let h2_final = h2 ^ (h2 >> 32); + + (h1_final, h2_final) + } + + /// Kiểm tra `data` có khả năng tồn tại? (dùng hash đã tính sẵn) + /// + /// - `true` → **có thể** tồn tại (hoặc false positive) + /// - `false` → **chắc chắn** không tồn tại + /// + /// ## Khi nào dùng + /// + /// Khi cần check cùng 1 data trên nhiều bloom filters (vd: search_like). + /// Hash chỉ tính 1 lần, dùng `contains_raw` cho mỗi bloom filter. + #[allow(dead_code)] // API giữ nguyên — dùng cho search_like batch. + #[inline] + pub fn contains_raw(&self, h1: u64, h2: u64) -> bool { + let m_mask = self.m_mask; + for i in 0..self.k { + let bit_pos = (h1.wrapping_add(i.wrapping_mul(h2))) & m_mask; + if !self.get_bit(bit_pos as usize) { + return false; + } + } + true + } + + /// Serialize bloom filter thành Vec để lưu xuống storage. + /// + /// Format: + /// - 8 bytes: bits.len() (u64 LE) + /// - 8 bytes: k (u64 LE) + /// - 8 bytes: m (u64 LE) + /// - 8 bytes: m_mask (u64 LE) + /// - bits.len() * 8 bytes: raw bits array + #[inline] + pub fn serialize(&self) -> Vec { + let len = self.bits.len(); + let mut buf = Vec::with_capacity(32 + len * 8); + buf.extend_from_slice(&(len as u64).to_le_bytes()); + buf.extend_from_slice(&self.k.to_le_bytes()); + buf.extend_from_slice(&self.m.to_le_bytes()); + buf.extend_from_slice(&self.m_mask.to_le_bytes()); + for &w in &self.bits { + buf.extend_from_slice(&w.to_le_bytes()); + } + buf + } + + /// Deserialize bloom filter từ bytes (format tương ứng serialize). + #[inline] + pub fn deserialize(data: &[u8]) -> Option { + if data.len() < 32 { + return None; + } + let (header, rest) = data.split_at(32); + let bits_len = u64::from_le_bytes(header[0..8].try_into().ok()?) as usize; + let k = u64::from_le_bytes(header[8..16].try_into().ok()?); + let m = u64::from_le_bytes(header[16..24].try_into().ok()?); + let m_mask = u64::from_le_bytes(header[24..32].try_into().ok()?); + + if rest.len() < bits_len * 8 { + return None; + } + let mut bits = vec![0u64; bits_len]; + for (i, w) in bits.iter_mut().enumerate() { + let start = i * 8; + *w = u64::from_le_bytes(rest[start..start + 8].try_into().ok()?); + } + + Some(Self { bits, k, m, m_mask }) + } + + /// Set bit tại `pos` (0-indexed). + #[inline] + fn set_bit(&mut self, pos: usize) { + let idx = pos / 64; + let bit = pos % 64; + self.bits[idx] |= 1u64 << bit; + } + + /// Get bit tại `pos` (0-indexed). + #[inline] + fn get_bit(&self, pos: usize) -> bool { + let idx = pos / 64; + let bit = pos % 64; + (self.bits[idx] >> bit) & 1 == 1 + } + + /// Số bits đang được set (population count). + #[allow(dead_code)] // API giữ nguyên — đo mật độ bloom. + #[inline] + pub fn popcount(&self) -> u64 { + // Chunks thành các khối 4 x u64 (256-bit registers) + let chunks = self.bits.chunks_exact(4); + let remainder = chunks.remainder(); + + let mut total = 0u64; + for chunk in chunks { + total += (chunk[0].count_ones() + + chunk[1].count_ones() + + chunk[2].count_ones() + + chunk[3].count_ones()) as u64; + } + + for &word in remainder { + total += word.count_ones() as u64; + } + + total + } + + /// False positive rate ước lượng (dựa trên số bits đã set). + #[allow(dead_code)] // API giữ nguyên — đo chất lượng bloom. + #[inline] + pub fn estimated_fpr(&self) -> f64 { + let ones = self.popcount(); + let total = self.m; + let p = ones as f64 / total as f64; + p.powf(self.k as f64) + } +} + +// ==================== Tests ==================== + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bloom_basic() { + let mut bf = BloomFilter::new(1024, 7); + assert!(!bf.contains(b"hello")); + bf.insert(b"hello"); + assert!(bf.contains(b"hello")); + } + + #[test] + fn test_bloom_no_false_negative() { + let mut bf = BloomFilter::new(4096, 10); + let items: Vec<&[u8]> = vec![ + "Vàng".as_bytes(), + "Tiệm".as_bytes(), + b"PNJ", + b"SJC", + "Bảo Tín".as_bytes(), + b"hello", + b"world", + b"rust", + b"bloom", + b"filter", + b"algorithm", + b"radix", + b"tree", + b"search", + b"index", + ]; + for item in &items { + bf.insert(item); + } + // Mọi item đã insert phải contains == true + for item in &items { + assert!( + bf.contains(item), + "false negative: {:?}", + std::str::from_utf8(item) + ); + } + } + + #[test] + fn test_bloom_union() { + let mut bf1 = BloomFilter::new(1024, 7); + let mut bf2 = BloomFilter::new(1024, 7); + bf1.insert(b"hello"); + bf2.insert(b"world"); + bf1.union(&bf2); + assert!(bf1.contains(b"hello")); + assert!(bf1.contains(b"world")); + } + + #[test] + fn test_bloom_clear() { + let mut bf = BloomFilter::new(1024, 7); + bf.insert(b"hello"); + assert!(bf.contains(b"hello")); + bf.clear(); + assert!(!bf.contains(b"hello")); + } + + #[test] + fn test_bloom_popcount() { + let mut bf = BloomFilter::new(2048, 7); + assert_eq!(bf.popcount(), 0); + bf.insert(b"hello"); + assert_eq!(bf.popcount(), 7); // k = 7 bits set + } + + #[test] + fn test_bloom_m_power_of_two() { + // m = 1000 → next power of two = 1024 + let bf = BloomFilter::new(1000, 7); + assert_eq!(bf.m, 1024); + assert_eq!(bf.bits.len(), 1024 / 64); + } + + #[test] + fn test_bloom_min_m() { + let bf = BloomFilter::new(1, 1); + assert_eq!(bf.m, 64); // tối thiểu 64 bits + } +} diff --git a/crates/codegraph-graph/src/diff.rs b/crates/codegraph-graph/src/diff.rs new file mode 100644 index 000000000..bf7eaf55c --- /dev/null +++ b/crates/codegraph-graph/src/diff.rs @@ -0,0 +1,746 @@ +//! Diff → graph impact ("draft" analysis). +//! +//! Nhận một **unified diff** (từ MR / patch file / `git diff`), map các dòng đã +//! sửa — phía *new* của hunk, vì index phản ánh working tree = trạng thái "sau +//! khi MR áp dụng" — lên các symbol trong index, rồi tìm call-site nào trong +//! flow nào nằm trong vùng bị sửa, kèm marker context. Hoàn toàn **read-only**: +//! kết quả là một bản draft về tác động lên graph trước khi thay đổi thực sự +//! được index lại. + +use crate::GraphIndex; +use codegraph_core::{Error, Result, Symbol, SymbolId, SymbolKind, is_marker, marker_name}; +use serde::Serialize; +use std::collections::{HashMap, HashSet}; + +// ==================== Unified diff parser ==================== + +/// Một hunk trong diff. +#[derive(Debug, Clone, Serialize)] +pub struct Hunk { + pub old_start: u32, + pub old_len: u32, + pub new_start: u32, + pub new_len: u32, + /// Số dòng phía new (context + added) — dòng hiện có sau khi MR áp dụng. + pub new_lines: Vec, + /// Số dòng added (`+`) trong hunk. + pub added: usize, + /// Số dòng removed (`-`) trong hunk. + pub removed: usize, +} + +/// Một file xuất hiện trong diff. +#[derive(Debug, Clone, Serialize)] +pub struct FileDiff { + /// Đường dẫn git-relative (có thể còn prefix `a/`/`b/`). + pub path: String, + /// `true` nếu file bị xoá hoàn toàn (phía new rỗng). + pub deleted: bool, + pub hunks: Vec, +} + +/// Kết quả parse toàn bộ diff. +#[derive(Debug, Clone, Default, Serialize)] +pub struct ParsedDiff { + pub files: Vec, +} + +/// Parse một unified diff. Chỉ lưu *số dòng* phía new của từng hunk — không cần +/// nội dung. Các header không liên quan (`index …`, mode lines, `Binary files +/// differ`, `rename …`) được bỏ qua. +pub fn parse_unified_diff(input: &str) -> Result { + let mut files: Vec = Vec::new(); + let mut cur_path: Option = None; + let mut cur_deleted = false; + let mut hunks: Vec = Vec::new(); + let mut hunk: Option = None; + let mut new_n: u32 = 0; + + // Đóng hunk đang mở vào `hunks`. + macro_rules! end_hunk { + () => { + if let Some(h) = hunk.take() { + hunks.push(h); + } + }; + } + + // Đẩy file hiện tại vào output (đảo hunk + suy deleted từ `+0,0`). + fn push_file( + files: &mut Vec, + path: String, + mut deleted: bool, + hunks: &mut Vec, + ) { + if !hunks.is_empty() && hunks.iter().all(|h| h.new_len == 0) { + deleted = true; + } + files.push(FileDiff { + path, + deleted, + hunks: std::mem::take(hunks), + }); + } + + for raw in input.lines() { + if let Some(p) = raw.strip_prefix("diff --git ") { + if let Some(path) = cur_path.take() { + end_hunk!(); + push_file(&mut files, path, cur_deleted, &mut hunks); + } + cur_deleted = false; + // `diff --git a/x b/y` — lấy phía b/ (đổi tên file cũng rơi vào đây). + cur_path = p.split_once(" b/").map(|(_, b)| format!("b/{b}")); + } else if let Some(p) = raw.strip_prefix("+++ ") { + end_hunk!(); + let p = p.trim(); + if p == "/dev/null" { + // File bị xoá: giữ path cũ, đánh dấu deleted. + cur_deleted = true; + } else { + cur_path = Some(p.to_string()); + cur_deleted = false; + } + } else if let Some(rest) = raw.strip_prefix("--- ") { + // Path xác nhận phía cũ; path "new" lấy từ `+++` (hoặc `diff --git`). + end_hunk!(); + let p = rest.trim(); + if cur_path.is_none() && p != "/dev/null" { + cur_path = Some(p.to_string()); + } + } else if raw.starts_with("@@ ") { + end_hunk!(); + let parsed = parse_hunk_header(raw)?; + new_n = parsed.new_start; + hunk = Some(parsed); + } else if raw.starts_with('\\') { + // `\ No newline at end of file` — không phải dòng nội dung. + } else if let Some(h) = hunk.as_mut() { + match raw.as_bytes().first().copied() { + Some(b' ') => { + h.new_lines.push(new_n); + new_n += 1; + } + Some(b'+') => { + h.new_lines.push(new_n); + new_n += 1; + h.added += 1; + } + Some(b'-') => { + h.removed += 1; + } + // Dòng lạ trong lúc đang mở hunk — coi như hunk kết thúc. + _ => end_hunk!(), + } + } + } + if let Some(path) = cur_path { + end_hunk!(); + push_file(&mut files, path, cur_deleted, &mut hunks); + } + Ok(ParsedDiff { files }) +} + +/// Parse header hunk `@@ -old,count +new,count @@ …`. Count mặc định 1 khi thiếu. +fn parse_hunk_header(line: &str) -> Result { + let body = line + .strip_prefix("@@") + .ok_or_else(|| Error::Invalid(format!("bad hunk header: {line}")))? + .trim_start() + .split_once(" @@") + .map(|(h, _)| h) + .unwrap_or(line.trim_start_matches("@@").trim_start()); + let (old, new) = body + .split_once(' ') + .ok_or_else(|| Error::Invalid(format!("bad hunk header: {line}")))?; + let (old_start, old_len) = parse_range(old)?; + let (new_start, new_len) = parse_range(new)?; + Ok(Hunk { + old_start, + old_len, + new_start, + new_len, + new_lines: Vec::new(), + added: 0, + removed: 0, + }) +} + +/// Parse `-start,count` hoặc `+start,count` (count mặc định 1). +fn parse_range(s: &str) -> Result<(u32, u32)> { + let s = s + .strip_prefix('-') + .or_else(|| s.strip_prefix('+')) + .ok_or_else(|| Error::Invalid(format!("bad range: {s}")))?; + match s.split_once(',') { + Some((a, b)) => Ok(( + a.parse::() + .map_err(|e| Error::Invalid(e.to_string()))?, + b.parse::() + .map_err(|e| Error::Invalid(e.to_string()))?, + )), + None => Ok(( + s.parse::() + .map_err(|e| Error::Invalid(e.to_string()))?, + 1, + )), + } +} + +// ==================== Diff → graph impact ==================== + +/// Tóm tắt tổng thể của bản draft. +#[derive(Debug, Clone, Default, Serialize)] +pub struct DiffSummary { + pub files_in_diff: usize, + pub files_matched: usize, + pub symbols_affected: usize, + pub flows_affected: usize, + /// File trong diff chưa từng được index (mới thêm / không phải code). + pub new_files: Vec, + /// File trong diff không khớp được file nào trong index (vd rename / non-code). + pub unmatched_files: Vec, +} + +/// Một call-site nằm trong vùng dòng bị sửa của flow. +#[derive(Debug, Clone, Serialize)] +pub struct DiffAffectedCall { + pub position: usize, + pub callee: String, + pub to_id: Option, + pub line: u32, + /// Marker guard đứng ngay trước call-site trong chain (ngoài → trong). + pub markers: Vec, +} + +/// Một flow bị ảnh hưởng (hàm có body chứa dòng đã sửa). +#[derive(Debug, Clone, Serialize)] +pub struct DiffFlow { + pub id: SymbolId, + pub name: String, + pub file: String, + pub line: u32, + /// Call-site nằm trong vùng dòng bị sửa. + pub affected_calls: Vec, + /// Các marker xuất hiện trong khoảng chain giữa call-site đầu/cuối bị ảnh hưởng. + pub marker_window: Vec, + /// Caller trực tiếp (flow phụ thuộc gián tiếp — hàm bị sửa được ai gọi). + pub called_by: Vec, +} + +/// Một symbol bị ảnh hưởng. +#[derive(Debug, Clone, Serialize)] +pub struct DiffSymbol { + pub symbol: Symbol, + /// `"modified"` hoặc `"removed"` (file bị xoá). + pub impact: String, +} + +/// Chi tiết per-file trong draft. +#[derive(Debug, Clone, Serialize)] +pub struct DiffFile { + /// Đường dẫn trong diff (giữ nguyên, không prefix). + pub path: String, + pub matched: bool, + /// Đường dẫn trong index khớp được (`None` nếu chưa được index). + pub matched_path: Option, + pub added_lines: usize, + pub removed_lines: usize, + pub deleted: bool, + pub symbols: Vec, + pub flows: Vec, +} + +/// Bản draft — kết quả phân tích tác động của diff lên graph hiện tại. +#[derive(Debug, Clone, Serialize)] +pub struct DiffReport { + /// Đánh dấu đây là bản draft read-only — chưa được áp vào graph/index. + pub draft: bool, + pub summary: DiffSummary, + pub files: Vec, +} + +impl GraphIndex { + /// Phân tích tác động của một diff lên index hiện tại (draft). + /// + /// `root` là đường dẫn gốc workspace — dùng để nối khi path trong diff là + /// git-relative mà `Symbol.file` trong index là absolute. Không đọc/sửa file + /// nào, không mutate index. + pub async fn diff_assess( + &self, + parsed: &ParsedDiff, + root: Option<&std::path::Path>, + ) -> DiffReport { + // Group symbol theo file (key = `Symbol.file` trong index). + let mut by_file: HashMap<&str, Vec<&Symbol>> = HashMap::new(); + for s in self.symbols.values() { + by_file.entry(s.file.as_str()).or_default().push(s); + } + + let mut report_files = Vec::new(); + let mut summary = DiffSummary { + files_in_diff: parsed.files.len(), + ..Default::default() + }; + + for fd in &parsed.files { + let rel = strip_git_prefix(&fd.path); + let matched_key = find_matching_file(&by_file, rel, root); + + let mut file_out = DiffFile { + path: rel.to_string(), + matched: matched_key.is_some(), + matched_path: matched_key.map(|k| k.to_string()), + added_lines: fd.hunks.iter().map(|h| h.added).sum(), + removed_lines: fd.hunks.iter().map(|h| h.removed).sum(), + deleted: fd.deleted, + symbols: Vec::new(), + flows: Vec::new(), + }; + + let new_lines: HashSet = fd + .hunks + .iter() + .flat_map(|h| h.new_lines.iter().copied()) + .collect(); + + match matched_key { + None => { + // Chưa từng được index: file mới (old_len 0) hay chưa match. + let is_new = fd.hunks.iter().all(|h| h.old_len == 0); + if fd.deleted { + // File xoá nhưng không có trong index — không có gì để báo. + } else if is_new && !fd.hunks.is_empty() { + summary.new_files.push(rel.to_string()); + } else { + summary.unmatched_files.push(rel.to_string()); + } + } + Some(key) => { + summary.files_matched += 1; + let symbols = by_file.get(key).cloned().unwrap_or_default(); + for s in symbols { + if fd.deleted || new_lines.is_empty() { + // File bị xoá hoàn toàn: mọi symbol của file bị xoá. + if fd.deleted { + summary.symbols_affected += 1; + file_out.symbols.push(DiffSymbol { + symbol: (*s).clone(), + impact: "removed".into(), + }); + } + continue; + } + if !symbol_overlaps(s, &new_lines) { + continue; + } + summary.symbols_affected += 1; + file_out.symbols.push(DiffSymbol { + symbol: (*s).clone(), + impact: "modified".into(), + }); + if !matches!(s.kind, SymbolKind::Function | SymbolKind::Method) { + continue; + } + // Flow impact cho hàm/method bị chạm. + if let Some(flow) = self.diff_flow(s, &new_lines).await { + summary.flows_affected += 1; + file_out.flows.push(flow); + } + } + } + } + report_files.push(file_out); + } + + DiffReport { + draft: true, + summary, + files: report_files, + } + } + + /// Flow impact của một hàm bị chạm: call-site nằm trong vùng sửa + marker + /// context + caller trực tiếp. + async fn diff_flow(&self, sym: &Symbol, new_lines: &HashSet) -> Option { + let flow = self.flow(sym.id).await.ok()?; + let mut affected_calls = Vec::new(); + let mut first = usize::MAX; + let mut last = 0; + for call in &flow.calls { + if new_lines.contains(&call.line) { + affected_calls.push(DiffAffectedCall { + position: call.position, + callee: call.to_name.clone(), + to_id: call.to_id, + line: call.line, + markers: guard_markers(&flow.chain, call.position), + }); + first = first.min(call.position); + last = last.max(call.position); + } + } + if affected_calls.is_empty() { + return None; + } + let called_by = self.callers(sym.id, 1).await.unwrap_or_default(); + Some(DiffFlow { + id: sym.id, + name: sym.name.clone(), + file: sym.file.clone(), + line: sym.line, + marker_window: marker_window(&flow.chain, first, last), + affected_calls, + called_by, + }) + } +} + +/// Marker guard trực tiếp của một call-site: walk ngược từ `position-1` trong +/// chain, gom các marker liên tiếp (dừng khi gặp phần tử không phải marker). +fn guard_markers(chain: &[u64], position: usize) -> Vec { + let mut out = Vec::new(); + let mut i = position; + while i > 0 { + i -= 1; + let e = chain[i]; + if !is_marker(e) { + break; + } + if let Some(n) = marker_name(e) { + out.push(n.to_string()); + } + } + out.reverse(); // ngoài → trong + out +} + +/// Các marker xuất hiện trong khoảng từ marker guard của call-site đầu tiên đến +/// call-site cuối cùng bị ảnh hưởng (dedupe, giữ thứ tự). +fn marker_window(chain: &[u64], first: usize, last: usize) -> Vec { + // Điểm bắt đầu: lùi về marker trực tiếp trước call-site đầu tiên. + let mut start = first; + while start > 0 && is_marker(chain[start - 1]) { + start -= 1; + } + let mut seen = HashSet::new(); + let mut out = Vec::new(); + for &e in &chain[start..=last.min(chain.len().saturating_sub(1))] { + if let Some(n) = marker_name(e) + && seen.insert(n) + { + out.push(n.to_string()); + } + } + out +} + +/// Bỏ tiền tố git `a/`/`b/` (tối đa một lần). +fn strip_git_prefix(path: &str) -> &str { + if let Some(rest) = path.strip_prefix("a/") { + rest + } else if let Some(rest) = path.strip_prefix("b/") { + rest + } else { + path + } +} + +/// Tìm file trong index khớp với path của diff: exact (hoặc root.join) trước, +/// rồi suffix-match (`/rel`). +fn find_matching_file<'a>( + by_file: &'a HashMap<&'a str, Vec<&'a Symbol>>, + rel: &str, + root: Option<&std::path::Path>, +) -> Option<&'a str> { + let mut candidates = Vec::new(); + candidates.push(rel.to_string()); + if let Some(r) = root { + candidates.push(r.join(rel).to_string_lossy().into_owned()); + } + for c in &candidates { + if let Some(k) = by_file.get_key_value(c.as_str()) { + return Some(k.0); + } + } + let suffix = format!("/{rel}"); + by_file.keys().find(|k| k.ends_with(&suffix)).copied() +} + +/// Symbol có vùng `line..=end_line` chạm một trong các dòng đã sửa (phía new). +fn symbol_overlaps(s: &Symbol, new_lines: &HashSet) -> bool { + new_lines.iter().any(|&l| s.line <= l && l <= s.end_line) +} + +// ==================== Tests ==================== + +#[cfg(test)] +mod tests { + use super::*; + use codegraph_core::{ + CallRecord, EffectType, MARKER_BRANCH_END, MARKER_IF_TRUE, SYMBOL_BASE, ScopeLevel, + }; + + fn sym(file: &str, name: &str, id: u64, line: u32, end_line: u32) -> Symbol { + Symbol { + id, + name: name.to_string(), + kind: SymbolKind::Function, + scope: ScopeLevel::Global, + scope_id: 0, + type_ref: 0, + type_name: None, + file: file.to_string(), + line, + end_line, + signature: None, + doc: None, + annotations: Vec::new(), + language: "test".to_string(), + } + } + + fn result( + path: &str, + symbols: Vec, + chains: HashMap>, + calls: Vec, + ) -> crate::ParseResult { + crate::ParseResult { + path: path.to_string(), + language: "test".to_string(), + bytes: 0, + lines: 0, + symbols, + chains, + calls, + } + } + + // ── parser ── + + #[test] + fn parse_single_file_hunks() { + let diff = "\ +diff --git a/src/a.ts b/src/a.ts +index 1111111..2222222 100644 +--- a/src/a.ts ++++ b/src/a.ts +@@ -10,4 +10,5 @@ fn main() { + let x = 1; + let y = 2; ++ let z = 3; + foo(); + } +"; + let p = parse_unified_diff(diff).unwrap(); + assert_eq!(p.files.len(), 1); + let f = &p.files[0]; + assert_eq!(f.path, "b/src/a.ts"); + assert!(!f.deleted); + assert_eq!(f.hunks.len(), 1); + let h = &f.hunks[0]; + assert_eq!( + (h.old_start, h.old_len, h.new_start, h.new_len), + (10, 4, 10, 5) + ); + assert_eq!(h.added, 1); + assert_eq!(h.removed, 0); + // context (10,11,13,14) + added (12) → dòng new {10,11,12,13,14}. + assert_eq!(h.new_lines, vec![10, 11, 12, 13, 14]); + } + + #[test] + fn parse_deleted_and_new_files() { + let diff = "\ +diff --git a/gone.rs b/gone.rs +deleted file mode 100644 +index 1111111..0000000 +--- a/gone.rs ++++ /dev/null +@@ -1,3 +0,0 @@ +-fn old() {} +-fn old2() {} +diff --git a/fresh.rs b/fresh.rs +new file mode 100644 +index 0000000..2222222 +--- /dev/null ++++ b/fresh.rs +@@ -0,0 +1,2 @@ ++fn new_fn() {} ++fn new_fn2() {} +"; + let p = parse_unified_diff(diff).unwrap(); + assert_eq!(p.files.len(), 2); + assert!(p.files[0].deleted); + assert_eq!(p.files[0].hunks[0].new_len, 0); + assert!(!p.files[1].deleted); + assert_eq!(p.files[1].hunks[0].old_len, 0); + assert_eq!(p.files[1].hunks[0].new_lines, vec![1, 2]); + } + + #[test] + fn parse_crlf_and_no_newline() { + let diff = concat!( + "--- a/x.rs\n", + "+++ b/x.rs\n", + "@@ -1,2 +1,3 @@\n", + " a\r\n", + "+b\r\n", + "\\ No newline at end of file\n", + ); + let p = parse_unified_diff(diff).unwrap(); + assert_eq!(p.files.len(), 1); + assert_eq!(p.files[0].hunks[0].new_lines, vec![1, 2]); + assert_eq!(p.files[0].hunks[0].added, 1); + } + + #[test] + fn parse_bad_header_errors() { + assert!(parse_unified_diff("@@ nope @@").is_err()); + } + + // ── assess ── + + #[tokio::test] + async fn assess_marks_flow_and_call_sites() { + let mut idx = GraphIndex::in_memory(); + let process = SYMBOL_BASE; + let fetch = SYMBOL_BASE + 1; + let main = SYMBOL_BASE + 2; + let chains = HashMap::from([ + // process: IF_TRUE → fetch + ( + process, + vec![process, MARKER_IF_TRUE, fetch, MARKER_BRANCH_END], + ), + (main, vec![main, process]), + ]); + let calls = vec![CallRecord { + caller_id: process, + call_name: "fetch".into(), + position: 2, + arg_exprs: Vec::new(), + line: 12, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + }]; + let r = result( + "a.ts", + vec![ + sym("a.ts", "process", process, 1, 30), + sym("b.ts", "fetch", fetch, 1, 10), + sym("a.ts", "main", main, 40, 60), + ], + chains, + calls, + ); + idx.ingest(&[r]).await.unwrap(); + + let diff = "\ +--- a/a.ts ++++ b/a.ts +@@ -9,4 +9,4 @@ + let y = 2; + foo(); ++ bar(); + } +"; + let parsed = parse_unified_diff(diff).unwrap(); + let report = idx.diff_assess(&parsed, None).await; + + assert!(report.draft); + assert_eq!(report.summary.files_matched, 1); + assert_eq!(report.summary.symbols_affected, 1); + assert_eq!(report.summary.flows_affected, 1); + + let f = &report.files[0]; + assert!(f.matched); + assert_eq!(f.symbols.len(), 1); + assert_eq!(f.symbols[0].symbol.name, "process"); + assert_eq!(f.symbols[0].impact, "modified"); + + let fl = &f.flows[0]; + assert_eq!(fl.name, "process"); + assert_eq!(fl.affected_calls.len(), 1); + let call = &fl.affected_calls[0]; + assert_eq!(call.callee, "fetch"); + assert_eq!(call.line, 12); + assert_eq!(call.markers, vec!["IF_TRUE"]); + assert!(fl.marker_window.contains(&"IF_TRUE".to_string())); + // main gọi process → dependent flow. + assert_eq!(fl.called_by.len(), 1); + assert_eq!(fl.called_by[0].name, "main"); + } + + #[tokio::test] + async fn assess_removed_file() { + let mut idx = GraphIndex::in_memory(); + let f = SYMBOL_BASE; + let chains = HashMap::from([(f, vec![f])]); + let r = result( + "old.rs", + vec![sym("old.rs", "old_fn", f, 1, 5)], + chains, + vec![], + ); + idx.ingest(&[r]).await.unwrap(); + + let diff = "\ +--- a/old.rs ++++ /dev/null +@@ -1,5 +0,0 @@ +-fn old_fn() {} +"; + let parsed = parse_unified_diff(diff).unwrap(); + let report = idx.diff_assess(&parsed, None).await; + let f = &report.files[0]; + assert!(f.matched); + assert!(f.deleted); + assert_eq!(f.symbols.len(), 1); + assert_eq!(f.symbols[0].impact, "removed"); + } + + #[tokio::test] + async fn assess_path_matching_with_root() { + let mut idx = GraphIndex::in_memory(); + let f = SYMBOL_BASE; + let chains = HashMap::from([(f, vec![f])]); + // Index lưu path absolute. + let r = result( + "/work/repo/src/a.rs", + vec![sym("/work/repo/src/a.rs", "a_fn", f, 1, 5)], + chains, + vec![], + ); + idx.ingest(&[r]).await.unwrap(); + + // Diff git-relative, root = /work/repo → khớp. + let diff = "\ +--- a/src/a.rs ++++ b/src/a.rs +@@ -1,3 +1,3 @@ + fn a_fn() { +- x(); ++ y(); + } +"; + let parsed = parse_unified_diff(diff).unwrap(); + let report = idx + .diff_assess(&parsed, Some(std::path::Path::new("/work/repo"))) + .await; + assert!(report.files[0].matched); + assert_eq!( + report.files[0].matched_path.as_deref(), + Some("/work/repo/src/a.rs") + ); + + // Không có root → suffix match vẫn ăn. + let report2 = idx.diff_assess(&parsed, None).await; + assert!(report2.files[0].matched); + } +} diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index a2de65353..a754f3696 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -34,27 +34,41 @@ //! same-file +3) → `build_edges_from_calls` (edge = chain[position], CallSite + //! var-type alias, gom SaveCallRecords) → files → rebuild engines → bump version. -use crate::search::Search; +pub use crate::search::Search; use crate::storage::InMemoryStorage; use codegraph_core::{ - is_marker, marker_name, CallRecord, CallSite, CallSiteResult, ClassInfo, Dependency, - DependenciesReport, EdgeMeta, EffectType, Error, FileInfo, FlowCall, FlowResult, FunctionScope, - MemberInfo, ResolveResult, SearchFlowResult, SemgraphStats, Symbol, SymbolKind, SymbolMatch, - SYMBOL_BASE, + CallRecord, CallSite, CallSiteResult, ClassInfo, DependenciesReport, Dependency, EdgeMeta, + EffectType, Error, FileInfo, FlowCall, FlowResult, FunctionScope, MemberInfo, ResolveResult, + SYMBOL_BASE, SearchFlowResult, SemgraphStats, Symbol, SymbolKind, SymbolMatch, is_marker, + marker_name, }; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; use tokio::sync::RwLock; +#[cfg(feature = "bloom-search")] +mod bloom; +pub mod diff; mod radix; mod search; -mod storage; - mod shared; +mod storage; pub use shared::SharedGraphIndex; +/// Báo tiến độ cho `GraphIndex::ingest_with_progress`. +/// +/// Graph crate không phụ thuộc indicatif — caller (CLI orchestrator / MCP) dựng +/// một impl translate các event này sang `ProgressBar` của nó. `total = 0` (chỉ +/// `phase`, không advance) nghĩa là phase không biết trước số đơn vị. +pub trait IngestProgress: Send + Sync { + /// Bắt đầu một phase mới — `total` là số đơn vị sẽ `advance` (0 = không biết). + fn phase(&self, name: &'static str, total: usize); + /// Tiến thêm `n` đơn vị trong phase hiện tại. + fn advance(&self, n: usize); +} + /// Số shard mặc định cho chain engine (`element % sharding`). const CHAIN_SHARDING: usize = 64; @@ -138,8 +152,26 @@ impl GraphIndex { } /// Mở index từ file sqlite (feature `sqlite`) — rebuild từ entity store. + #[allow(unused_variables)] // dsn chỉ dùng khi bật sqlite/redis — không backend → Err. + pub async fn open(dsn: &str) -> Result { + #[cfg(feature = "sqlite")] + #[allow(unreachable_code)] + return Self::open_sqlite(dsn).await; + + #[cfg(feature = "redis")] + #[allow(unreachable_code)] + return Self::open_redis(dsn).await; + + #[allow(unreachable_code)] + { + Err(Error::Db( + "Phải bật ít nhất feature 'sqlite' hoặc 'redis'".into(), + )) + } + } + #[cfg(feature = "sqlite")] - pub async fn open(path: &str) -> Result { + async fn open_sqlite(path: &str) -> Result { let storage = crate::storage::sqlite::SqliteStorage::open(path) .await .map_err(serr)?; @@ -149,6 +181,46 @@ impl GraphIndex { Ok(idx) } + /// Mở index từ redis dsn (feature `redis`) — rebuild từ entity store. + #[cfg(feature = "redis")] + pub async fn open_redis(dsn: &str) -> Result { + use url::Url; + + let mut parsed_url = Url::parse(dsn).map_err(|error| Error::Search(error.to_string()))?; + let prefix = parsed_url + .query_pairs() + .find(|(key, _)| key == "prefix") + .map(|(_, value)| value.into_owned()) + .unwrap_or_else(|| "default".to_string()); + let pairs = parsed_url + .query_pairs() + .filter(|(k, _)| k != "prefix") + .map(|(k, v)| (k.into_owned(), v.into_owned())) + .collect::>(); + + if pairs.is_empty() { + parsed_url.set_query(None); + } else { + parsed_url.query_pairs_mut().clear(); + + for (k, v) in pairs { + parsed_url.query_pairs_mut().append_pair(&k, &v); + } + } + + let storage = crate::storage::redis::RedisStorage::new( + redis::Client::open(parsed_url.to_string()) + .map_err(|error| Error::Search(error.to_string()))?, + &prefix, + ) + .await + .map_err(serr)?; + let storage = Arc::new(RwLock::new(storage)) as Arc>; + let mut idx = Self::new_with_storage(storage); + idx.rebuild().await?; + Ok(idx) + } + fn new_with_storage(storage: Arc>) -> Self { // Name engine luôn in-memory (như semgraph SearchIndex) — storage riêng // để record id (1..N) không đụng record của chain engine (func ids). @@ -174,7 +246,8 @@ impl GraphIndex { // ── Build / rebuild ── /// Rebuild toàn bộ index từ entity store trong storage (open/reopen). - #[cfg_attr(not(feature = "sqlite"), allow(dead_code))] // chỉ open() dùng (sqlite) + #[cfg_attr(not(any(feature = "sqlite", feature = "redis")), allow(dead_code))] + // chỉ open() dùng — không backend thì không ai gọi. async fn rebuild(&mut self) -> Result<()> { self.next_id = self .storage @@ -190,13 +263,7 @@ impl GraphIndex { .load_all_symbols() .await .map_err(serr)?; - let chains_raw = self - .storage - .read() - .await - .all_chains() - .await - .map_err(serr)?; + let chains_raw = self.storage.read().await.all_chains().await.map_err(serr)?; let call_names_raw = self .storage .read() @@ -253,13 +320,14 @@ impl GraphIndex { self.rebuild_edges(&recs); // Engines. - self.rebuild_chain_engine().await?; - self.rebuild_name_engine().await?; + self.rebuild_chain_engine(None).await?; + self.rebuild_name_engine(None).await?; Ok(()) } /// Insert symbol vào registry + index (scope id đã global — path rebuild). - #[cfg_attr(not(feature = "sqlite"), allow(dead_code))] // chỉ rebuild() dùng + #[cfg_attr(not(any(feature = "sqlite", feature = "redis")), allow(dead_code))] + // chỉ rebuild() dùng — không backend thì không ai gọi. fn index_symbol(&mut self, sym: Symbol) { let id = sym.id; if !sym.name.is_empty() { @@ -285,7 +353,8 @@ impl GraphIndex { } /// Rebuild edges từ chains + call records (nhanh — chỉ dùng khi reopen). - #[cfg_attr(not(feature = "sqlite"), allow(dead_code))] // chỉ rebuild() dùng + #[cfg_attr(not(any(feature = "sqlite", feature = "redis")), allow(dead_code))] + // chỉ rebuild() dùng — không backend thì không ai gọi. fn rebuild_edges(&mut self, recs: &HashMap>) { self.edges.clear(); for (&func_id, chain) in &self.chains_map { @@ -318,10 +387,13 @@ impl GraphIndex { } /// Rebuild chain engine từ `chains_map` (clear + insert tuần tự). - async fn rebuild_chain_engine(&mut self) -> Result<()> { + async fn rebuild_chain_engine(&mut self, progress: Option<&dyn IngestProgress>) -> Result<()> { self.chains.clear().await.map_err(serr_search)?; let mut funcs: Vec = self.chains_map.keys().copied().collect(); funcs.sort_unstable(); + if let Some(p) = progress { + p.phase("rebuild call-chain engine", funcs.len()); + } for func_id in funcs { let chain = &self.chains_map[&func_id]; // Mọi element meta = None → không ghi node stream (record = func id @@ -331,16 +403,22 @@ impl GraphIndex { .insert_chain(func_id as usize, chain, &metas) .await .map_err(serr_search)?; + if let Some(p) = progress { + p.advance(1); + } } Ok(()) } /// Rebuild name engine từ `name_index` (clear + insert mỗi tên distinct). - async fn rebuild_name_engine(&mut self) -> Result<()> { + async fn rebuild_name_engine(&mut self, progress: Option<&dyn IngestProgress>) -> Result<()> { self.names.clear().await.map_err(serr_search)?; self.name_records.clear(); let mut distinct: Vec<&String> = self.name_index.keys().collect(); distinct.sort(); + if let Some(p) = progress { + p.phase("rebuild name-search engine", distinct.len()); + } let mut record = 0usize; for name in distinct { record += 1; @@ -350,6 +428,9 @@ impl GraphIndex { .await .map_err(serr_search)?; self.name_records.push(name.clone()); + if let Some(p) = progress { + p.advance(1); + } } Ok(()) } @@ -358,8 +439,22 @@ impl GraphIndex { /// Ingest toàn bộ parse results — **full re-index**: xoá dữ liệu cũ, register /// symbol (id global) + remap, resolve placeholder 0, build edges + call-name - /// index, persist + bump version. + /// index, persist + bump version. Không báo tiến độ — dùng + /// [`ingest_with_progress`](Self::ingest_with_progress) nếu cần. pub async fn ingest(&mut self, results: &[ParseResult]) -> Result<()> { + self.ingest_with_progress(results, None).await + } + + /// Như [`ingest`](Self::ingest), nhưng báo tiến độ qua `IngestProgress` + /// (phase + số đơn vị). Graph crate không phụ thuộc indicatif — caller nối + /// các event này vào ProgressBar của nó. + pub async fn ingest_with_progress( + &mut self, + results: &[ParseResult], + progress: Option>, + ) -> Result<()> { + let p = progress.as_deref(); + // ── Reset ── self.storage .write() @@ -380,6 +475,10 @@ impl GraphIndex { self.next_id = SYMBOL_BASE; // ── Phase 1: register + remap ── + let total_symbols: usize = results.iter().map(|r| r.symbols.len()).sum(); + if let Some(p) = p { + p.phase("register symbols", total_symbols); + } let mut all_calls: Vec = Vec::new(); for result in results { let mut id_map: HashMap = HashMap::new(); @@ -414,6 +513,9 @@ impl GraphIndex { } all_calls.push(c2); } + if let Some(p) = p { + p.advance(result.symbols.len()); + } } // Scope index chỉ rebuild sau khi toàn bộ scope id đã là global. self.rebuild_scope_index(); @@ -422,9 +524,12 @@ impl GraphIndex { self.resolve_calls(&all_calls); // ── Phase 3: build edges + call records + call-name index ── - self.build_edges_from_calls(&all_calls).await?; + self.build_edges_from_calls(&all_calls, p).await?; // ── Phase 4: files ── + if let Some(p) = p { + p.phase("save files", results.len()); + } for result in results { let f = FileInfo { path: result.path.clone(), @@ -439,32 +544,36 @@ impl GraphIndex { .await .map_err(serr)?; self.files.push(f); + if let Some(p) = p { + p.advance(1); + } } // ── Phase 5: engines + version bump ── - self.rebuild_chain_engine().await?; - self.rebuild_name_engine().await?; + self.rebuild_chain_engine(p).await?; + self.rebuild_name_engine(p).await?; self.version += 1; - self.storage - .write() - .await - .set_version(self.version) - .await - .map_err(serr)?; + { + let mut st = self.storage.write().await; + st.save_next_id(self.next_id).await.map_err(serr)?; + st.set_version(self.version).await.map_err(serr)?; + } Ok(()) } /// Gán id global cho symbol, lưu storage + index tên. Không đụng scope index /// — scope id còn local, `rebuild_scope_index` chạy sau khi remap. + /// `next_id` không save per-symbol (chậm) — `ingest` persist 1 lần ở cuối. async fn register(&mut self, mut sym: Symbol) -> Result { let id = self.next_id; self.next_id += 1; sym.id = id; - { - let mut st = self.storage.write().await; - st.save_symbol(&sym).await.map_err(serr)?; - st.save_next_id(self.next_id).await.map_err(serr)?; - } + self.storage + .write() + .await + .save_symbol(&sym) + .await + .map_err(serr)?; if !sym.name.is_empty() { self.name_index .entry(sym.name.to_lowercase()) @@ -483,10 +592,14 @@ impl GraphIndex { let Some(sym) = self.symbols.get_mut(&new_id) else { return Ok(()); }; - if sym.scope_id != 0 && let Some(&g) = id_map.get(&sym.scope_id) { + if sym.scope_id != 0 + && let Some(&g) = id_map.get(&sym.scope_id) + { sym.scope_id = g; } - if sym.type_ref != 0 && let Some(&g) = id_map.get(&sym.type_ref) { + if sym.type_ref != 0 + && let Some(&g) = id_map.get(&sym.type_ref) + { sym.type_ref = g; } sym.clone() @@ -539,23 +652,43 @@ impl GraphIndex { /// literal) → exact name → short name (phần sau dấu chấm) → best-candidate /// (@Override +10 / has-chain +5 / same-file +3). fn resolve_call_placeholder(&self, call: &CallRecord, caller_id: u64) -> Option { + // 1. Try class/method target hints (used mainly for Java). if let (Some(tc), Some(tm)) = (&call.target_class, &call.target_method) && let Some(id) = self.lookup_method_of_class(tc, tm) { return Some(id); } + // 2. Direct name lookup (full qualified name). let mut candidates: Vec = self .name_index .get(&call.call_name.to_lowercase()) .cloned() .unwrap_or_default(); + + // 3. Short name fallback (after last dot). if candidates.is_empty() { - let short = call.call_name.rsplit('.').next().unwrap_or("").to_lowercase(); + let short = call + .call_name + .rsplit('.') + .next() + .unwrap_or("") + .to_lowercase(); if !short.is_empty() { candidates = self.name_index.get(&short).cloned().unwrap_or_default(); } } + + // 4. Go/Import alias handling: try to resolve using the caller's variable type + // information. `alias_qualified_name` produces a fully qualified name like + // "myservice.validate" based on a variable's type_name. If that name + // exists in the index, use it as an additional candidate set. + if candidates.is_empty() + && let Some(qualified) = self.alias_qualified_name(caller_id, &call.call_name) + { + candidates = self.name_index.get(&qualified).cloned().unwrap_or_default(); + } + if candidates.is_empty() { return None; } @@ -573,7 +706,8 @@ impl GraphIndex { } for &mid in method_ids { let m = self.symbols.get(&mid)?; - if matches!(m.kind, SymbolKind::Function | SymbolKind::Method) && m.scope_id == cid { + if matches!(m.kind, SymbolKind::Function | SymbolKind::Method) && m.scope_id == cid + { return Some(mid); } } @@ -600,7 +734,9 @@ impl GraphIndex { if self.chains_map.contains_key(&id) { score += 5; } - if let Some(f) = &caller_file && &sym.file == f { + if let Some(f) = &caller_file + && &sym.file == f + { score += 3; } if score > best_score { @@ -617,7 +753,11 @@ impl GraphIndex { /// Edge model: mọi symbol element trong chain là một callee (thống nhất với /// `rebuild_edges` khi reopen) — call record chỉ bổ sung metadata theo /// position. Chain dựng thẳng (không qua placeholder) vẫn sinh edge đủ. - async fn build_edges_from_calls(&mut self, calls: &[CallRecord]) -> Result<()> { + async fn build_edges_from_calls( + &mut self, + calls: &[CallRecord], + progress: Option<&dyn IngestProgress>, + ) -> Result<()> { let mut recs_by_caller: HashMap> = HashMap::new(); for c in calls { let caller = c.caller_id; @@ -678,6 +818,11 @@ impl GraphIndex { } // Persist call records (gom theo caller). + if let Some(p) = progress + && !recs_by_caller.is_empty() + { + p.phase("save call records", recs_by_caller.len()); + } for (caller, recs) in recs_by_caller { let bytes = serde_json::to_vec(&recs).map_err(|e| Error::Search(e.to_string()))?; self.storage @@ -686,8 +831,16 @@ impl GraphIndex { .set_call_records(caller, &bytes) .await .map_err(serr)?; + if let Some(p) = progress { + p.advance(1); + } } // Persist call-name index. + if let Some(p) = progress + && !self.call_names.is_empty() + { + p.phase("save call-name index", self.call_names.len()); + } for (name, sites) in &self.call_names { let bytes = serde_json::to_vec(sites).map_err(|e| Error::Search(e.to_string()))?; self.storage @@ -696,6 +849,9 @@ impl GraphIndex { .set_call_name_index(name, &bytes) .await .map_err(serr)?; + if let Some(p) = progress { + p.advance(1); + } } Ok(()) } @@ -715,7 +871,9 @@ impl GraphIndex { if let Some(ids) = self.scope_index.get(&sid) { for id in ids { let sym = self.symbols.get(id)?; - if sym.name == var && let Some(tn) = &sym.type_name { + if sym.name == var + && let Some(tn) = &sym.type_name + { let rest = &call_name[dot + 1..]; return Some(format!("{}.{}", tn.to_lowercase(), rest.to_lowercase())); } @@ -950,7 +1108,8 @@ impl GraphIndex { Some(bytes) => serde_json::from_slice(&bytes).unwrap_or_default(), None => Vec::new(), }; - let rec_by_pos: HashMap = recs.iter().map(|r| (r.position, r)).collect(); + let rec_by_pos: HashMap = + recs.iter().map(|r| (r.position, r)).collect(); let chain_desc = chain .iter() @@ -1088,7 +1247,11 @@ impl GraphIndex { } } let mut out: Vec = by_func.into_values().collect(); - out.sort_by(|a, b| a.func_name.cmp(&b.func_name).then(a.func_id.cmp(&b.func_id))); + out.sort_by(|a, b| { + a.func_name + .cmp(&b.func_name) + .then(a.func_id.cmp(&b.func_id)) + }); let limit = if limit == 0 { usize::MAX } else { limit }; out.truncate(limit); Ok(out) @@ -1140,7 +1303,12 @@ impl GraphIndex { let members = self.members_of(id); let fields: Vec = members .iter() - .filter(|s| matches!(s.kind, SymbolKind::Field | SymbolKind::Variable | SymbolKind::Constant)) + .filter(|s| { + matches!( + s.kind, + SymbolKind::Field | SymbolKind::Variable | SymbolKind::Constant + ) + }) .map(MemberInfo::from_symbol) .collect(); let methods: Vec = members @@ -1375,7 +1543,7 @@ impl GraphIndex { mod tests { use super::*; use codegraph_core::{ - Annotation, ScopeLevel, MARKER_BRANCH_END, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, + Annotation, MARKER_BRANCH_END, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, ScopeLevel, }; fn sym(file: &str, name: &str, id: u64) -> Symbol { @@ -1553,7 +1721,10 @@ mod tests { // Placeholder 0 đã được resolve về id thật (exact name match). let flow = idx.flow(SYMBOL_BASE).await.unwrap(); - assert_eq!(flow.chain, vec![SYMBOL_BASE, SYMBOL_BASE + 1, SYMBOL_BASE + 2]); + assert_eq!( + flow.chain, + vec![SYMBOL_BASE, SYMBOL_BASE + 1, SYMBOL_BASE + 2] + ); assert_eq!(flow.chain_desc, vec!["f", "g", "h"]); let cees = idx.callees(SYMBOL_BASE).await.unwrap(); assert_eq!(cees.len(), 2); @@ -1812,9 +1983,7 @@ mod tests { let r = result( "svc.rs", - vec![ - cls, method1, field, func, param, local, controller, iface, - ], + vec![cls, method1, field, func, param, local, controller, iface], HashMap::new(), vec![], ); @@ -1835,7 +2004,10 @@ mod tests { assert_eq!(info.fields.len(), 1); assert_eq!(info.fields[0].name, "repo"); assert_eq!(info.methods.len(), 1); - assert!(idx.get_class_info(SYMBOL_BASE + 3).is_none(), "function không phải class"); + assert!( + idx.get_class_info(SYMBOL_BASE + 3).is_none(), + "function không phải class" + ); // function_scope — parameters + locals. let scope = idx.function_scope(SYMBOL_BASE + 3).unwrap(); @@ -1875,11 +2047,20 @@ mod tests { .search_symbol_paged("order", Some(SymbolKind::Class), SymbolMatch::Prefix, 10, 0) .await .unwrap(); - assert_eq!(total, 2, "OrderService + OrderController khớp prefix 'order' + kind class"); + assert_eq!( + total, 2, + "OrderService + OrderController khớp prefix 'order' + kind class" + ); assert_eq!(hits[0].name, "OrderController"); assert_eq!(hits[1].name, "OrderService"); let (hits, total) = idx - .search_symbol_paged("service", Some(SymbolKind::Class), SymbolMatch::Suffix, 10, 0) + .search_symbol_paged( + "service", + Some(SymbolKind::Class), + SymbolMatch::Suffix, + 10, + 0, + ) .await .unwrap(); assert_eq!(total, 1); @@ -1896,7 +2077,10 @@ mod tests { .search_symbol_paged("order", None, SymbolMatch::Contains, 2, 0) .await .unwrap(); - assert_eq!(total, 4, "OrderService, OrderController, OrderRepository + getOrders"); + assert_eq!( + total, 4, + "OrderService, OrderController, OrderRepository + getOrders" + ); assert_eq!(page0.len(), 2); assert_eq!(page0[0].name, "OrderController"); assert_eq!(page0[1].name, "OrderRepository"); diff --git a/crates/codegraph-graph/src/radix.rs b/crates/codegraph-graph/src/radix.rs index bc7b99b4f..8654b8001 100644 --- a/crates/codegraph-graph/src/radix.rs +++ b/crates/codegraph-graph/src/radix.rs @@ -17,8 +17,23 @@ use tokio::sync::RwLock; use crate::storage::{self, Storage}; +#[cfg(feature = "bloom-search")] +use crate::bloom::BloomFilter; + pub const EMPTY: usize = 0; +/// Cấu hình bloom filter prune nhánh trong `search_dfs` (feature `bloom-search`). +#[cfg(feature = "bloom-search")] +pub mod bloom_cfg { + /// Số bit của bloom filter mỗi node (làm tròn lên power of 2 trong `new`). + pub const SIZE: usize = 4096; + /// Số hash functions. + pub const K: usize = 10; + /// Chỉ prune khi substring còn lại của pattern ≤ cap này — bloom chỉ lưu + /// substring ngắn, nên pattern dài hơn cap sẽ không bị prune (không sai). + pub const MATCH_CAP: usize = 16; +} + /// Phần tử trong key của radix tree. pub trait Element: Eq + Hash + Clone + Copy + Debug + Send + Sync + 'static { fn encode(&self) -> Vec; @@ -103,7 +118,7 @@ pub type SearchMatcher = Arc OnMatchCallback + S /// shortcuts/cache dựa trên `old_prefix` + `breakpoint` rồi để radix commit. pub type OnSplitCallback = Arc Result<()> + Send + Sync>; -/// Callback khi chạm tới một node cụ thể, chứa thông tin đầy đủ về node +/// Callback khi chạm tới một node cụ thể, chứa thông tin đầy đủ về node /// đó dưới dạng metadata, có cấu trúc dạng node, metadata và trả về id của /// node, lưu ý vì đây là callback access nên nó có thể bị trùng hoặc gọi lại /// nhiều lần nhưng phải trả về cùng 1 id nếu trùng @@ -217,6 +232,7 @@ impl Radix { let id = self .split(node_id, common, &prefix[split_off..], index) .await?; + self.maintain_bloom(prefix).await?; return Ok((id, tail)); } @@ -230,6 +246,7 @@ impl Radix { .await .update_node(node_id, None, Some(index)) .await?; + self.maintain_bloom(prefix).await?; return Ok((node_id, tail)); } return Ok((EMPTY, tail)); @@ -250,6 +267,7 @@ impl Radix { } if !found { let id = self.extend(node_id, &prefix[tail..], index).await?; + self.maintain_bloom(prefix).await?; return Ok((id, tail)); } } @@ -276,6 +294,7 @@ impl Radix { .await .add_shortcut_node(si, &prefix[0].encode(), root) .await?; + self.maintain_bloom(prefix).await?; return Ok((leaf, 1)); } let id = self @@ -286,6 +305,7 @@ impl Radix { .await?; let si = shard_of(prefix[0], self.sharding); self.storage.write().await.set_root(si, id).await?; + self.maintain_bloom(prefix).await?; Ok((id, 0)) } @@ -400,6 +420,74 @@ impl Radix { Err(Error::NotFound) } + /// Theo dõi `key` từ root → trả `Vec` node id dọc theo đường đi + /// (node đầu là root của shard). Chỉ dùng trong test để biết node con + /// trên đường đi khi muốn `search_dfs` bắt đầu từ một node giữa. + /// + /// Ngoài test, chỉ được gọi từ `maintain_bloom` — khi feature + /// `bloom-search` tắt hàm thành dead code, nên ghi `allow(dead_code)`. + #[allow(dead_code)] + async fn follow_path(&self, key: &[T]) -> Result> { + #[cfg(feature = "bloom-search")] + #[allow(unreachable_code)] + return self.follow_path_with_bloom(key).await; + + #[allow(unreachable_code)] + return self.follow_path_default(key).await; + } + + #[allow(dead_code)] + async fn follow_path_default(&self, key: &[T]) -> Result> { + if key.is_empty() { + return Ok(Vec::new()); + } + + let mut node_id = self + .storage + .read() + .await + .get_root(shard_of(key[0], self.sharding)) + .await?; + if node_id == EMPTY { + return Ok(Vec::new()); + } + + let mut path = vec![node_id]; + let mut pos = 0; + + loop { + let (prefix_bytes, _) = self.storage.read().await.get_node(node_id).await?; + let node_prefix = Self::to_vec(&prefix_bytes); + let common = node_prefix + .iter() + .zip(key[pos..].iter()) + .take_while(|(a, b)| a == b) + .count(); + + pos += common; + if pos == key.len() || common < node_prefix.len() { + return Ok(path); + } + + let next_elem = key[pos]; + let children = self.storage.read().await.get_children(node_id).await?; + let mut found = false; + for &child in &children { + let (cp_bytes, _) = self.storage.read().await.get_node(child).await?; + let cp = Self::to_vec(&cp_bytes); + if !cp.is_empty() && cp[0] == next_elem { + node_id = child; + found = true; + break; + } + } + if !found { + return Ok(path); + } + path.push(node_id); + } + } + /// Tìm tất cả `(full_key, record)` có key bắt đầu bằng `prefix`. pub async fn search_prefix(&self, begin: usize, prefix: &[T]) -> Result, usize)>> { if prefix.is_empty() { @@ -530,6 +618,8 @@ impl Radix { pattern: &[T], matcher: SearchMatcher, ) -> Result> { + let mut records = Vec::new(); + if pattern.is_empty() { return Err(Error::NotFound); } @@ -548,8 +638,7 @@ impl Radix { return Ok(Vec::new()); } - let mut records = Vec::new(); - self.dfs_search(node_id, pattern, matcher, 0, &mut records) + self.search_dfs_iter(node_id, pattern, matcher, 0, &mut records) .await?; Ok(records) } @@ -560,7 +649,7 @@ impl Radix { /// `pattern_pos` tại node entry luôn là vị trí pattern bắt đầu dò trên /// prefix của node này (data_pos = 0). #[inline] - async fn dfs_search( + async fn search_dfs_iter( &self, node_id: usize, pattern: &[T], @@ -587,12 +676,35 @@ impl Radix { if pp == 0 || pp >= pattern.len() { continue; } + let next_elem = pattern[pp]; for &child in &children { let (cp_bytes, _) = { self.storage.read().await.get_node(child).await? }; let cp = Self::to_vec(&cp_bytes); + if !cp.is_empty() && cp[0] == next_elem { - Box::pin(self.dfs_search(child, pattern, matcher.clone(), pp, out)).await?; + // Prune nhánh: bloom của child không chứa `pattern[pp..]` + // (substring) → subtree chắc chắn không có match tiếp tục, + // bỏ nhánh. Bloom có 0 false negative nên không bao giờ bỏ + // nhánh có match thật. Chỉ prune khi substring đủ ngắn và + // child có bloom (không có → fallback full traversal). + #[cfg(feature = "bloom-search")] + { + let remaining_len = pattern.len() - pp; + if remaining_len <= bloom_cfg::MATCH_CAP { + let bloom_bytes = + { self.storage.read().await.get_node_bloom(child).await? }; + if let Some(bloom_bytes) = bloom_bytes + && let Some(bf) = BloomFilter::deserialize(&bloom_bytes) + && !bf.contains(&Self::from_vec(&pattern[pp..])) + { + continue; + } + } + } + + Box::pin(self.search_dfs_iter(child, pattern, matcher.clone(), pp, out)) + .await?; if !out.is_empty() { return Ok(()); } @@ -698,8 +810,8 @@ impl Radix { /// Follow key từ root → leaf, trả về toàn bộ node ids trên đường đi. /// Dùng để tìm ancestors khi cập nhật bloom filters sau insert. - #[cfg(test)] - pub async fn follow_path(&self, key: &[T]) -> Result> { + #[cfg(feature = "bloom-search")] + async fn follow_path_with_bloom(&self, key: &[T]) -> Result> { if key.is_empty() { return Ok(Vec::new()); } @@ -749,6 +861,60 @@ impl Radix { path.push(node_id); } } + + /// Duy trì bloom filter sau mỗi mutation (insert/update record): no-op khi + /// feature `bloom-search` tắt. Mỗi node trên path của `key` nhận mọi + /// substring của `key` (giới hạn `MATCH_CAP`) — đây chính là điều kiện để + /// `search_dfs` prune nhánh con không chứa `pattern[pp..]`. + async fn maintain_bloom(&self, key: &[T]) -> Result<()> { + #[cfg(feature = "bloom-search")] + { + if key.is_empty() { + return Ok(()); + } + let enc = Self::from_vec(key); + let bs = T::byte_size(); + let elem_len = enc.len() / bs; + if elem_len == 0 { + return Ok(()); + } + + // Mọi substring aligned theo element, dài 1..=cap element. + let cap = bloom_cfg::MATCH_CAP.min(elem_len); + let mut subs: Vec> = Vec::new(); + for start in 0..elem_len { + for end in (start + 1)..=(start + cap) { + if end > elem_len { + break; + } + subs.push(enc[start * bs..end * bs].to_vec()); + } + } + + let path = self.follow_path(key).await?; + for node_id in path { + let mut bf = self + .storage + .read() + .await + .get_node_bloom(node_id) + .await? + .and_then(|b| BloomFilter::deserialize(&b)) + .unwrap_or_else(|| BloomFilter::new(bloom_cfg::SIZE, bloom_cfg::K)); + for s in &subs { + bf.insert(s); + } + self.storage + .write() + .await + .set_node_bloom(node_id, &bf.serialize()) + .await?; + } + } + #[cfg(not(feature = "bloom-search"))] + let _ = key; + Ok(()) + } } #[cfg(test)] @@ -964,7 +1130,9 @@ mod tests { async fn test_follow_path() { let mut tree = Radix::in_memory(4); tree.insert(&k("hello"), 1, &no_meta(5)).await.unwrap(); - tree.insert(&k("helloworld"), 2, &no_meta(10)).await.unwrap(); + tree.insert(&k("helloworld"), 2, &no_meta(10)) + .await + .unwrap(); let path = tree.follow_path(&k("helloworld")).await.unwrap(); assert!(!path.is_empty(), "path không rỗng"); @@ -1076,16 +1244,28 @@ mod tests { // Metadata lưu vào node stream, keyed theo id callback trả về (= elem). let storage = tree.storage.read().await; assert_eq!( - storage.get_node_meta(b'a' as usize).await.unwrap().as_deref(), + storage + .get_node_meta(b'a' as usize) + .await + .unwrap() + .as_deref(), Some(b"ma".as_slice()) ); assert_eq!( - storage.get_node_meta(b'b' as usize).await.unwrap().as_deref(), + storage + .get_node_meta(b'b' as usize) + .await + .unwrap() + .as_deref(), Some(b"mb".as_slice()) ); assert_eq!(storage.get_node_meta(b'c' as usize).await.unwrap(), None); assert_eq!( - storage.get_node_meta(b'd' as usize).await.unwrap().as_deref(), + storage + .get_node_meta(b'd' as usize) + .await + .unwrap() + .as_deref(), Some(b"md".as_slice()) ); drop(storage); @@ -1129,7 +1309,11 @@ mod tests { assert_eq!(id, b'x' as usize); let storage = tree.storage.read().await; assert_eq!( - storage.get_node_meta(b'x' as usize).await.unwrap().as_deref(), + storage + .get_node_meta(b'x' as usize) + .await + .unwrap() + .as_deref(), Some(b"mx".as_slice()) ); drop(storage); @@ -1138,7 +1322,11 @@ mod tests { tree.register_node(b'x', b"mx2").await.unwrap(); let storage = tree.storage.read().await; assert_eq!( - storage.get_node_meta(b'x' as usize).await.unwrap().as_deref(), + storage + .get_node_meta(b'x' as usize) + .await + .unwrap() + .as_deref(), Some(b"mx2".as_slice()) ); drop(storage); diff --git a/crates/codegraph-graph/src/shared.rs b/crates/codegraph-graph/src/shared.rs index 2f119d876..e3782ffe2 100644 --- a/crates/codegraph-graph/src/shared.rs +++ b/crates/codegraph-graph/src/shared.rs @@ -117,7 +117,12 @@ impl SharedGraphIndex { Some(p) => GraphIndex::open(&p.display().to_string()).await?, None => GraphIndex::in_memory(), }; - #[cfg(not(feature = "sqlite"))] + #[cfg(all(feature = "redis", not(feature = "sqlite")))] + let index = match &self.path { + Some(p) => GraphIndex::open(&p.display().to_string()).await?, + None => GraphIndex::in_memory(), + }; + #[cfg(not(any(feature = "sqlite", feature = "redis")))] let index = GraphIndex::in_memory(); let version = index.version(); @@ -133,7 +138,7 @@ impl SharedGraphIndex { mod tests { use super::*; use crate::ParseResult; - use codegraph_core::{CallRecord, Symbol, SymbolKind, SYMBOL_BASE}; + use codegraph_core::{CallRecord, SYMBOL_BASE, Symbol, SymbolKind}; // Chỉ test sqlite dùng — build không feature này vẫn compile. #[cfg_attr(not(feature = "sqlite"), allow(dead_code))] @@ -209,11 +214,7 @@ mod tests { // Re-index lại (full re-index → version bump, dữ liệu đổi). { let mut idx = GraphIndex::open(&db_str).await.unwrap(); - let r = mk_result( - "b.ts", - vec![sym("x", SYMBOL_BASE)], - vec![SYMBOL_BASE], - ); + let r = mk_result("b.ts", vec![sym("x", SYMBOL_BASE)], vec![SYMBOL_BASE]); idx.ingest(&[r]).await.unwrap(); } let idx2 = sgi.ensure_fresh().await; diff --git a/crates/codegraph-graph/src/storage.rs b/crates/codegraph-graph/src/storage.rs index 8242d25f7..68c0f3167 100644 --- a/crates/codegraph-graph/src/storage.rs +++ b/crates/codegraph-graph/src/storage.rs @@ -19,6 +19,8 @@ use codegraph_core::{FileInfo, Symbol}; #[cfg(feature = "sqlite")] pub mod sqlite; +#[cfg(feature = "redis")] +pub mod redis; // ==================== Error Type ==================== #[derive(Debug)] @@ -120,6 +122,18 @@ pub trait Storage: Send + Sync { ) -> Result<()>; async fn get_node(&self, id: usize) -> Result<(Vec, usize)>; async fn get_children(&self, id: usize) -> Result>; + /// Lưu serialize bloom filter của node (opaque bytes) — prune nhánh khi + /// search_dfs. Mặc định: no-op (backend chưa hỗ trợ → không prune). + #[cfg(feature = "bloom-search")] + async fn set_node_bloom(&mut self, _id: usize, _bloom: &[u8]) -> Result<()> { + Ok(()) + } + /// Đọc serialize bloom filter của node — `None` nếu node chưa có bloom. + /// Mặc định: `None`. + #[cfg(feature = "bloom-search")] + async fn get_node_bloom(&self, _id: usize) -> Result>> { + Ok(None) + } // ── Edge data stream (metadata per edge id — chain model không còn link-edge) ── /// Lưu dữ liệu edge (opaque bytes, VD CallEdgeMeta JSON) keyed theo edge id. @@ -328,6 +342,9 @@ struct MemoryData { edges: HashMap>, /// element id → node metadata (Node JSON). node_meta: HashMap>, + /// node id → serialize bloom filter (prune nhánh trong search_dfs). + #[cfg(feature = "bloom-search")] + blooms: HashMap>, /// record (owner) → chain bytes (u64 LE 8-byte/element). chains: HashMap>, // ── Entity store (semgraph model) ── @@ -365,6 +382,8 @@ impl InMemoryStorage { shortcuts: vec![], edges: HashMap::new(), node_meta: HashMap::new(), + #[cfg(feature = "bloom-search")] + blooms: HashMap::new(), chains: HashMap::new(), symbols: HashMap::new(), // Id bắt đầu từ SYMBOL_BASE (marker reserved 1..=99). @@ -449,6 +468,25 @@ impl Storage for InMemoryStorage { Ok(d.children.get(id).cloned().unwrap_or_default()) } + #[cfg(feature = "bloom-search")] + async fn set_node_bloom(&mut self, id: usize, bloom: &[u8]) -> Result<()> { + let mut d = self + .data + .write() + .map_err(|_| StorageError::Internal("poison".into()))?; + d.blooms.insert(id, bloom.to_vec()); + Ok(()) + } + + #[cfg(feature = "bloom-search")] + async fn get_node_bloom(&self, id: usize) -> Result>> { + let d = self + .data + .read() + .map_err(|_| StorageError::Internal("poison".into()))?; + Ok(d.blooms.get(&id).cloned()) + } + async fn set_root(&mut self, shard: usize, root: usize) -> Result<()> { let mut d = self .data @@ -576,7 +614,10 @@ impl Storage for InMemoryStorage { .data .read() .map_err(|_| StorageError::Internal("poison".into()))?; - d.edges.iter().map(|(&id, data)| (id, data.clone())).collect() + d.edges + .iter() + .map(|(&id, data)| (id, data.clone())) + .collect() }; for (id, data) in items { f(id, &data)?; @@ -716,7 +757,10 @@ impl Storage for InMemoryStorage { .data .read() .map_err(|_| StorageError::Internal("poison".into()))?; - Ok(d.call_records.iter().map(|(&f, b)| (f, b.clone())).collect()) + Ok(d.call_records + .iter() + .map(|(&f, b)| (f, b.clone())) + .collect()) } async fn set_call_name_index(&mut self, name: &str, sites: &[u8]) -> Result<()> { @@ -741,7 +785,10 @@ impl Storage for InMemoryStorage { .data .read() .map_err(|_| StorageError::Internal("poison".into()))?; - Ok(d.call_names.iter().map(|(n, b)| (n.clone(), b.clone())).collect()) + Ok(d.call_names + .iter() + .map(|(n, b)| (n.clone(), b.clone())) + .collect()) } async fn upsert_file(&mut self, f: &FileInfo) -> Result<()> { @@ -895,936 +942,6 @@ impl Tx for InMemoryTx { } } -// ========================================================================= -// Redis Storage — chỉ build khi feature "redis" được bật. -// ========================================================================= - -#[cfg(feature = "redis")] -#[allow(dead_code)] // backend redis chỉ được exercise bởi tests của chính nó (chưa có production path) -pub mod redis { - //! Redis-backed radix-node storage. - //! - //! Cấu trúc key: - //! | Key | Kiểu | Mục đích | - //! |--------------------------|-------|---------------------------| - //! | `{prefix}:branch` | List | prefix của từng node | - //! | `{prefix}:record` | List | record của từng node | - //! | `{prefix}:forward:{id}` | Set | children list của node | - //! | `{prefix}:endpoint` | Hash | root ID cho mỗi shard | - //! | `{prefix}:meta` | Hash | record_idx → metadata | - //! | `{prefix}:keylen` | Hash | record_idx → key length | - //! | `{prefix}:edgedata` | Hash | edge id → edge metadata | - //! | `{prefix}:nodemeta` | Hash | element id → node metadata| - //! | `{prefix}:chains` | Hash | record → chain bytes | - //! | `{prefix}:shortcut:{shard}:{elem}` | Set | node ids chứa elem | - //! | `{prefix}:symbols` | Hash | symbol id → Symbol JSON | - //! | `{prefix}:nextid` | String| next symbol registry id | - //! | `{prefix}:callrecords` | Hash | func id → call records | - //! | `{prefix}:callnames` | Hash | call name → call sites | - //! | `{prefix}:files` | Hash | path → FileInfo JSON | - //! | `{prefix}:version` | String| index version | - - use std::collections::HashMap; - use std::sync::Arc; - - use redis::aio::MultiplexedConnection; - use tokio::sync::Mutex; - - use async_trait::async_trait; - - use super::{FileInfo, Result, Storage, StorageError, Symbol, Tx, TxOp}; - - // ==================== KeyBuilder ==================== - - type KeyFormatter = Arc String + Send + Sync>; - - /// Cấu hình key cho Redis storage. - #[derive(Clone)] - pub struct KeyBuilder { - prefix: String, - formatter: Option, - } - - impl KeyBuilder { - pub fn new(prefix: &str) -> Self { - Self { - prefix: prefix.to_string(), - formatter: None, - } - } - - pub fn with_formatter(prefix: &str, f: KeyFormatter) -> Self { - Self { - prefix: prefix.to_string(), - formatter: Some(f), - } - } - - /// `key("branch")` → `"{prefix}:branch"` - pub fn key(&self, name: &str) -> String { - match &self.formatter { - Some(f) => f(name), - None => format!("{}:{}", self.prefix, name), - } - } - - /// `indexed("forward", 5)` → `"{prefix}:forward:5"` - pub fn indexed(&self, name: &str, idx: usize) -> String { - self.key(&format!("{name}:{idx}")) - } - - /// `shortcut(3, [0x01])` → `"{prefix}:shortcut:3:{0x01}"` - /// (bytes của elem nối trực tiếp — Redis key binary-safe). - pub fn shortcut(&self, shard: usize, elem: &[u8]) -> Vec { - let mut k = self.key(&format!("shortcut:{shard}")).into_bytes(); - k.push(b':'); - k.extend_from_slice(elem); - k - } - - /// Prefix chung của mọi shortcut key: `"{prefix}:shortcut:"`. - /// Dùng làm MATCH pattern khi SCAN để xoá toàn bộ shortcuts. - pub fn shortcut_prefix(&self) -> String { - self.key("shortcut") + ":" - } - } - - /// Helper shorthand: `cmd("LLEN")` → `redis::cmd("LLEN")` - fn cmd(name: &str) -> redis::Cmd { - redis::cmd(name) - } - - // ==================== RedisStorage ==================== - - pub struct RedisStorage { - conn: Arc>, - kb: KeyBuilder, - } - - impl RedisStorage { - async fn lock(&self) -> tokio::sync::MutexGuard<'_, MultiplexedConnection> { - self.conn.lock().await - } - - pub async fn new(client: redis::Client, prefix: &str) -> Result { - let conn = client - .get_multiplexed_async_connection() - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - let s = Self { - conn: Arc::new(Mutex::new(conn)), - kb: KeyBuilder::new(prefix), - }; - s.init().await?; - Ok(s) - } - - pub async fn from_multiplexed(conn: MultiplexedConnection, prefix: &str) -> Result { - let s = Self { - conn: Arc::new(Mutex::new(conn)), - kb: KeyBuilder::new(prefix), - }; - s.init().await?; - Ok(s) - } - - pub async fn with_key_builder(client: redis::Client, kb: KeyBuilder) -> Result { - let conn = client - .get_multiplexed_async_connection() - .await - .map_err(|e| StorageError::Internal(e.to_string()))?; - let s = Self { - conn: Arc::new(Mutex::new(conn)), - kb, - }; - s.init().await?; - Ok(s) - } - - async fn init(&self) -> Result<()> { - let mut conn = self.lock().await; - let exists: bool = cmd("EXISTS") - .arg(self.kb.key("branch")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - if !exists { - redis::pipe() - .atomic() - .rpush(self.kb.key("branch"), b"" as &[u8]) - .rpush(self.kb.key("record"), 0i64) - .exec_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - } - Ok(()) - } - - /// Độ dài hiện tại của branch list = số node (gồm sentinel). - /// Node id tiếp theo = len - 1. - async fn node_len(&self) -> Result { - let mut conn = self.lock().await; - let len: usize = cmd("LLEN") - .arg(self.kb.key("branch")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(len) - } - } - - #[async_trait] - impl Storage for RedisStorage { - async fn new_node(&mut self, prefix: Vec, record: usize) -> Result { - let mut conn = self.lock().await; - let result: redis::Value = redis::pipe() - .atomic() - .rpush(self.kb.key("branch"), &prefix[..]) - .rpush(self.kb.key("record"), record as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - - let len: usize = match result { - redis::Value::Array(ref items) => match items.first() { - Some(redis::Value::Int(n)) => *n as usize, - _ => cmd("LLEN") - .arg(self.kb.key("branch")) - .query_async::(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?, - }, - _ => cmd("LLEN") - .arg(self.kb.key("branch")) - .query_async::(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?, - }; - - Ok(len - 1) - } - - async fn update_node( - &mut self, - id: usize, - prefix: Option>, - record: Option, - ) -> Result<()> { - let mut conn = self.lock().await; - let mut pipe = redis::pipe(); - pipe.atomic(); - if let Some(p) = prefix { - pipe.lset(self.kb.key("branch"), id as isize, &p[..]); - } - if let Some(r) = record { - pipe.lset(self.kb.key("record"), id as isize, r as i64); - } - pipe.exec_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_node(&self, id: usize) -> Result<(Vec, usize)> { - let mut conn = self.lock().await; - let prefix: Vec = cmd("LINDEX") - .arg(self.kb.key("branch")) - .arg(id as isize) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - let rec: i64 = cmd("LINDEX") - .arg(self.kb.key("record")) - .arg(id as isize) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok((prefix, rec as usize)) - } - - async fn get_children(&self, id: usize) -> Result> { - let mut conn = self.lock().await; - let children: Vec = cmd("SMEMBERS") - .arg(self.kb.indexed("forward", id)) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(children.into_iter().map(|x| x as usize).collect()) - } - - async fn set_root(&mut self, shard: usize, root: usize) -> Result<()> { - let mut conn = self.lock().await; - cmd("HSET") - .arg(self.kb.key("endpoint")) - .arg(shard as i64) - .arg(root as i64) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_root(&self, shard: usize) -> Result { - let mut conn = self.lock().await; - let root: Option = cmd("HGET") - .arg(self.kb.key("endpoint")) - .arg(shard as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(root.unwrap_or(0) as usize) - } - - async fn set_meta(&mut self, record: usize, meta: &[u8]) -> Result<()> { - let mut conn = self.lock().await; - cmd("HSET") - .arg(self.kb.key("meta")) - .arg(record as i64) - .arg(meta) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_meta(&self, record: usize) -> Result>> { - let mut conn = self.lock().await; - let meta: Option> = cmd("HGET") - .arg(self.kb.key("meta")) - .arg(record as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(meta) - } - - async fn set_key_len(&mut self, record: usize, len: usize) -> Result<()> { - let mut conn = self.lock().await; - cmd("HSET") - .arg(self.kb.key("keylen")) - .arg(record as i64) - .arg(len as i64) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_key_len(&self, record: usize) -> Result> { - let mut conn = self.lock().await; - let len: Option = cmd("HGET") - .arg(self.kb.key("keylen")) - .arg(record as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(len.map(|x| x as usize)) - } - - async fn add_shortcut_node( - &mut self, - shard: usize, - elem: &[u8], - node_id: usize, - ) -> Result<()> { - let mut conn = self.lock().await; - cmd("SADD") - .arg(self.kb.shortcut(shard, elem)) - .arg(node_id as i64) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_shortcut_nodes(&self, shard: usize, elem: &[u8]) -> Result> { - let mut conn = self.lock().await; - let nodes: Vec = cmd("SMEMBERS") - .arg(self.kb.shortcut(shard, elem)) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(nodes.into_iter().map(|x| x as usize).collect()) - } - - async fn clear_shortcuts(&mut self) -> Result<()> { - let mut conn = self.lock().await; - let pattern = format!("{}*", self.kb.shortcut_prefix()); - let mut cursor: u64 = 0; - loop { - let (next_cursor, keys): (u64, Vec) = cmd("SCAN") - .arg(cursor) - .arg("MATCH") - .arg(&pattern) - .arg("COUNT") - .arg(500) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - for key in keys { - cmd("DEL") - .arg(key) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - } - cursor = next_cursor; - if cursor == 0 { - break; - } - } - Ok(()) - } - - async fn set_edge_data(&mut self, edge: usize, data: &[u8]) -> Result<()> { - let mut conn = self.lock().await; - cmd("HSET") - .arg(self.kb.key("edgedata")) - .arg(edge as i64) - .arg(data) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_edge_data(&self, edge: usize) -> Result>> { - let mut conn = self.lock().await; - let data: Option> = cmd("HGET") - .arg(self.kb.key("edgedata")) - .arg(edge as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(data) - } - - async fn clear_edges(&mut self) -> Result<()> { - let mut conn = self.lock().await; - cmd("DEL") - .arg(self.kb.key("edgedata")) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn for_each_edge_data( - &self, - f: &mut (dyn for<'a> FnMut(usize, &'a [u8]) -> Result<()> + Send), - ) -> Result<()> { - let mut conn = self.lock().await; - let items: Vec<(i64, Vec)> = cmd("HGETALL") - .arg(self.kb.key("edgedata")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - for (id, data) in items { - f(id as usize, &data)?; - } - Ok(()) - } - - async fn set_node_meta(&mut self, elem: usize, meta: &[u8]) -> Result<()> { - let mut conn = self.lock().await; - cmd("HSET") - .arg(self.kb.key("nodemeta")) - .arg(elem as i64) - .arg(meta) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_node_meta(&self, elem: usize) -> Result>> { - let mut conn = self.lock().await; - let meta: Option> = cmd("HGET") - .arg(self.kb.key("nodemeta")) - .arg(elem as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(meta) - } - - async fn clear_node_meta(&mut self) -> Result<()> { - let mut conn = self.lock().await; - cmd("DEL") - .arg(self.kb.key("nodemeta")) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn set_chain(&mut self, record: usize, chain: &[u64]) -> Result<()> { - let mut conn = self.lock().await; - cmd("HSET") - .arg(self.kb.key("chains")) - .arg(record as i64) - .arg(super::encode_chain(chain)) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_chain(&self, record: usize) -> Result>> { - let mut conn = self.lock().await; - let bytes: Option> = cmd("HGET") - .arg(self.kb.key("chains")) - .arg(record as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(bytes.map(|b| super::decode_chain(&b))) - } - - async fn clear_chains(&mut self) -> Result<()> { - let mut conn = self.lock().await; - cmd("DEL") - .arg(self.kb.key("chains")) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn save_symbol(&mut self, sym: &Symbol) -> Result<()> { - let mut conn = self.lock().await; - let data = serde_json::to_vec(sym).map_err(|e| StorageError::Internal(e.to_string()))?; - cmd("HSET") - .arg(self.kb.key("symbols")) - .arg(sym.id as i64) - .arg(data) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn load_symbol(&self, id: u64) -> Result> { - let mut conn = self.lock().await; - let data: Option> = cmd("HGET") - .arg(self.kb.key("symbols")) - .arg(id as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - data.map(|d| { - serde_json::from_slice(&d).map_err(|e| StorageError::Internal(e.to_string())) - }) - .transpose() - } - - async fn load_all_symbols(&self) -> Result> { - let mut conn = self.lock().await; - let map: HashMap> = cmd("HGETALL") - .arg(self.kb.key("symbols")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - let mut out: Vec = Vec::with_capacity(map.len()); - for data in map.into_values() { - out.push( - serde_json::from_slice(&data) - .map_err(|e| StorageError::Internal(e.to_string()))?, - ); - } - out.sort_by_key(|s| s.id); - Ok(out) - } - - async fn save_next_id(&mut self, next: u64) -> Result<()> { - let mut conn = self.lock().await; - cmd("SET") - .arg(self.kb.key("nextid")) - .arg(next as i64) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn load_next_id(&self) -> Result { - let mut conn = self.lock().await; - let next: Option = cmd("GET") - .arg(self.kb.key("nextid")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - // Registry chưa có symbol — bắt đầu từ SYMBOL_BASE (giống sqlite init). - Ok(next.map(|n| n as u64).unwrap_or(codegraph_core::SYMBOL_BASE)) - } - - async fn all_chains(&self) -> Result)>> { - let mut conn = self.lock().await; - let map: HashMap> = cmd("HGETALL") - .arg(self.kb.key("chains")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - let mut out: Vec<(u64, Vec)> = map - .into_iter() - .map(|(r, b)| (r as u64, b)) - .collect(); - out.sort_by_key(|(r, _)| *r); - Ok(out) - } - - async fn set_call_records(&mut self, func: u64, records: &[u8]) -> Result<()> { - let mut conn = self.lock().await; - cmd("HSET") - .arg(self.kb.key("callrecords")) - .arg(func as i64) - .arg(records) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn get_call_records(&self, func: u64) -> Result>> { - let mut conn = self.lock().await; - let records: Option> = cmd("HGET") - .arg(self.kb.key("callrecords")) - .arg(func as i64) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(records) - } - - async fn all_call_records(&self) -> Result)>> { - let mut conn = self.lock().await; - let map: HashMap> = cmd("HGETALL") - .arg(self.kb.key("callrecords")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - let mut out: Vec<(u64, Vec)> = map - .into_iter() - .map(|(f, b)| (f as u64, b)) - .collect(); - out.sort_by_key(|(f, _)| *f); - Ok(out) - } - - async fn set_call_name_index(&mut self, name: &str, sites: &[u8]) -> Result<()> { - let mut conn = self.lock().await; - cmd("HSET") - .arg(self.kb.key("callnames")) - .arg(name) - .arg(sites) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn load_call_name_index(&self, name: &str) -> Result>> { - let mut conn = self.lock().await; - let sites: Option> = cmd("HGET") - .arg(self.kb.key("callnames")) - .arg(name) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(sites) - } - - async fn all_call_name_indexes(&self) -> Result)>> { - let mut conn = self.lock().await; - let map: HashMap> = cmd("HGETALL") - .arg(self.kb.key("callnames")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - let mut out: Vec<(String, Vec)> = map.into_iter().collect(); - out.sort_by(|a, b| a.0.cmp(&b.0)); - Ok(out) - } - - async fn upsert_file(&mut self, f: &FileInfo) -> Result<()> { - let mut conn = self.lock().await; - let data = serde_json::to_vec(f).map_err(|e| StorageError::Internal(e.to_string()))?; - cmd("HSET") - .arg(self.kb.key("files")) - .arg(&f.path) - .arg(data) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn load_all_files(&self) -> Result> { - let mut conn = self.lock().await; - let map: HashMap> = cmd("HGETALL") - .arg(self.kb.key("files")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - let mut out: Vec = Vec::with_capacity(map.len()); - for data in map.into_values() { - out.push( - serde_json::from_slice(&data) - .map_err(|e| StorageError::Internal(e.to_string()))?, - ); - } - out.sort_by(|a, b| a.path.cmp(&b.path)); - Ok(out) - } - - async fn version(&self) -> Result { - let mut conn = self.lock().await; - let v: Option = cmd("GET") - .arg(self.kb.key("version")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(v.map(|n| n as u64).unwrap_or(0)) - } - - async fn set_version(&mut self, v: u64) -> Result<()> { - let mut conn = self.lock().await; - cmd("SET") - .arg(self.kb.key("version")) - .arg(v as i64) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - async fn clear_entities(&mut self) -> Result<()> { - let mut conn = self.lock().await; - cmd("DEL") - .arg(self.kb.key("symbols")) - .arg(self.kb.key("nextid")) - .arg(self.kb.key("callrecords")) - .arg(self.kb.key("callnames")) - .arg(self.kb.key("files")) - .arg(self.kb.key("version")) - .query_async::<()>(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - - fn new_tx(&self) -> Box { - Box::new(RedisTx { - conn: self.conn.clone(), - kb: self.kb.clone(), - nodes: Vec::new(), - ops: Vec::new(), - }) - } - } - - // ==================== Redis Transaction ==================== - - /// Transaction cho `RedisStorage`. - /// - /// - `new_node` snapshot độ dài branch list lúc tạo tx, id = base + n - /// (giả định single-connection — toàn bộ command đi qua cùng 1 mutex). - /// - `commit` build một MULTI/EXEC pipeline: RPUSH toàn bộ node mới trước, - /// rồi áp dụng các op cấu trúc — atomic, không lộ trạng thái trung gian. - pub struct RedisTx { - conn: Arc>, - kb: KeyBuilder, - nodes: Vec<(usize, Vec, usize)>, - ops: Vec, - } - - #[async_trait] - impl Tx for RedisTx { - async fn new_node(&mut self, prefix: Vec, record: usize) -> Result { - let base = self.node_len_checked().await?; - let id = base + self.nodes.len(); - self.nodes.push((id, prefix, record)); - Ok(id) - } - - async fn update_node( - &mut self, - id: usize, - prefix: Option>, - record: Option, - ) -> Result<()> { - self.ops.push(TxOp::UpdateNode { id, prefix, record }); - Ok(()) - } - - async fn add_child(&mut self, parent: usize, child: usize) -> Result<()> { - self.ops.push(TxOp::AddChild { parent, child }); - Ok(()) - } - - async fn move_child(&mut self, from: usize, to: usize, child: usize) -> Result<()> { - self.ops.push(TxOp::MoveChild { from, to, child }); - Ok(()) - } - - async fn commit(self: Box) -> Result<()> { - let RedisTx { - conn, - kb, - nodes, - ops, - .. - } = *self; - - let mut conn = conn.lock().await; - let mut pipe = redis::pipe(); - pipe.atomic(); - - // 1. RPUSH toàn bộ node mới (sentinel đã có sẵn ở index 0). - for (_, prefix, record) in &nodes { - pipe.rpush(kb.key("branch"), &prefix[..]); - pipe.rpush(kb.key("record"), *record as i64); - } - - // 2. Áp dụng ops. - for op in ops { - match op { - TxOp::AddChild { parent, child } => { - pipe.cmd("SADD") - .arg(kb.indexed("forward", parent)) - .arg(child as i64) - .ignore(); - } - TxOp::MoveChild { from, to, child } => { - pipe.cmd("SREM") - .arg(kb.indexed("forward", from)) - .arg(child as i64) - .ignore(); - pipe.cmd("SADD") - .arg(kb.indexed("forward", to)) - .arg(child as i64) - .ignore(); - } - TxOp::UpdateNode { id, prefix, record } => { - if let Some(p) = prefix { - pipe.lset(kb.key("branch"), id as isize, &p[..]); - } - if let Some(r) = record { - pipe.lset(kb.key("record"), id as isize, r as i64); - } - } - } - } - - pipe.exec_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(()) - } - } - - impl RedisTx { - async fn node_len_checked(&self) -> Result { - let mut conn = self.conn.lock().await; - let len: usize = cmd("LLEN") - .arg(self.kb.key("branch")) - .query_async(&mut *conn) - .await - .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; - Ok(len) - } - } - - // ── Tests ────────────────────────────────────────────────────────── - - #[cfg(test)] - mod tests { - use std::sync::atomic::{AtomicU16, Ordering}; - - use super::*; - use crate::radix::EMPTY; - use crate::storage::Storage; - - static COUNTER: AtomicU16 = AtomicU16::new(0); - - async fn new_test_storage() -> RedisStorage { - let n = COUNTER.fetch_add(1, Ordering::Relaxed); - let pid = std::process::id(); - let client = redis::Client::open("redis://127.0.0.1:6379/15") - .expect("redis connection failed — is redis-server running?"); - RedisStorage::new(client, &format!("test:radix:{}:{n}", pid)) - .await - .expect("init failed") - } - - #[tokio::test] - async fn test_new_node_and_get_node() { - let mut s = new_test_storage().await; - let id = s.new_node(b"hello".to_vec(), 42).await.unwrap(); - assert_ne!(id, EMPTY); - let (prefix, record) = s.get_node(id).await.unwrap(); - assert_eq!(prefix, b"hello"); - assert_eq!(record, 42); - } - - #[tokio::test] - async fn test_meta_roundtrip() { - let mut s = new_test_storage().await; - assert_eq!(s.get_meta(42).await.unwrap(), None); - assert_eq!(s.get_key_len(42).await.unwrap(), None); - s.set_meta(42, b"call-site-info").await.unwrap(); - s.set_key_len(42, 5).await.unwrap(); - assert_eq!( - s.get_meta(42).await.unwrap().as_deref(), - Some(b"call-site-info".as_slice()) - ); - assert_eq!(s.get_key_len(42).await.unwrap(), Some(5)); - s.set_meta(42, b"updated").await.unwrap(); - assert_eq!( - s.get_meta(42).await.unwrap().as_deref(), - Some(b"updated".as_slice()) - ); - } - - #[tokio::test] - async fn test_shortcuts_roundtrip() { - let mut s = new_test_storage().await; - assert!(s.get_shortcut_nodes(1, b"l").await.unwrap().is_empty()); - s.add_shortcut_node(1, b"l", 10).await.unwrap(); - s.add_shortcut_node(1, b"l", 20).await.unwrap(); - s.add_shortcut_node(1, b"o", 10).await.unwrap(); - let nodes = s.get_shortcut_nodes(1, b"l").await.unwrap(); - assert!(nodes.contains(&10) && nodes.contains(&20)); - assert_eq!(nodes.len(), 2); - s.clear_shortcuts().await.unwrap(); - assert!(s.get_shortcut_nodes(1, b"l").await.unwrap().is_empty()); - } - - #[tokio::test] - async fn test_tx_split_commit() { - let mut s = new_test_storage().await; - let parent = s.new_node(b"hello".to_vec(), 1).await.unwrap(); - - let mut tx = s.new_tx(); - let new_id = tx.new_node(b"p".to_vec(), 2).await.unwrap(); - let leg_id = tx.new_node(b"lo".to_vec(), 1).await.unwrap(); - tx.move_child(parent, leg_id, 0).await.unwrap(); - tx.add_child(parent, leg_id).await.unwrap(); - tx.add_child(parent, new_id).await.unwrap(); - tx.update_node(parent, Some(b"hel".to_vec()), Some(0)) - .await - .unwrap(); - tx.commit().await.unwrap(); - - let (prefix, _) = s.get_node(parent).await.unwrap(); - assert_eq!(prefix, b"hel"); - let children = s.get_children(parent).await.unwrap(); - assert!(children.contains(&leg_id)); - assert!(children.contains(&new_id)); - } - } -} - // ==================== Tests (InMemory) ==================== #[cfg(test)] diff --git a/crates/codegraph-graph/src/storage/redis.rs b/crates/codegraph-graph/src/storage/redis.rs new file mode 100644 index 000000000..14a43b294 --- /dev/null +++ b/crates/codegraph-graph/src/storage/redis.rs @@ -0,0 +1,937 @@ +//! Redis-backed radix-node storage. +//! +//! Cấu trúc key: +//! | Key | Kiểu | Mục đích | +//! |--------------------------|-------|---------------------------| +//! | `{prefix}:branch` | List | prefix của từng node | +//! | `{prefix}:record` | List | record của từng node | +//! | `{prefix}:forward:{id}` | Set | children list của node | +//! | `{prefix}:endpoint` | Hash | root ID cho mỗi shard | +//! | `{prefix}:meta` | Hash | record_idx → metadata | +//! | `{prefix}:keylen` | Hash | record_idx → key length | +//! | `{prefix}:edgedata` | Hash | edge id → edge metadata | +//! | `{prefix}:nodemeta` | Hash | element id → node metadata| +//! | `{prefix}:chains` | Hash | record → chain bytes | +//! | `{prefix}:shortcut:{shard}:{elem}` | Set | node ids chứa elem | +//! | `{prefix}:symbols` | Hash | symbol id → Symbol JSON | +//! | `{prefix}:nextid` | String| next symbol registry id | +//! | `{prefix}:callrecords` | Hash | func id → call records | +//! | `{prefix}:callnames` | Hash | call name → call sites | +//! | `{prefix}:files` | Hash | path → FileInfo JSON | +//! | `{prefix}:version` | String| index version | + +use std::collections::HashMap; +use std::sync::Arc; + +use redis::aio::MultiplexedConnection; +use tokio::sync::Mutex; + +use async_trait::async_trait; + +use super::{FileInfo, Result, Storage, StorageError, Symbol, Tx, TxOp}; + +// ==================== KeyBuilder ==================== + +type KeyFormatter = Arc String + Send + Sync>; + +/// Cấu hình key cho Redis storage. +#[derive(Clone)] +pub struct KeyBuilder { + prefix: String, + formatter: Option, +} + +impl KeyBuilder { + pub fn new(prefix: &str) -> Self { + Self { + prefix: prefix.to_string(), + formatter: None, + } + } + + #[allow(dead_code)] // API tiện ích (caller tạo KeyBuilder tuỳ biến) — chưa dùng nội bộ. + pub fn with_formatter(prefix: &str, f: KeyFormatter) -> Self { + Self { + prefix: prefix.to_string(), + formatter: Some(f), + } + } + + /// `key("branch")` → `"{prefix}:branch"` + pub fn key(&self, name: &str) -> String { + match &self.formatter { + Some(f) => f(name), + None => format!("{}:{}", self.prefix, name), + } + } + + /// `indexed("forward", 5)` → `"{prefix}:forward:5"` + pub fn indexed(&self, name: &str, idx: usize) -> String { + self.key(&format!("{name}:{idx}")) + } + + /// `shortcut(3, [0x01])` → `"{prefix}:shortcut:3:{0x01}"` + /// (bytes của elem nối trực tiếp — Redis key binary-safe). + pub fn shortcut(&self, shard: usize, elem: &[u8]) -> Vec { + let mut k = self.key(&format!("shortcut:{shard}")).into_bytes(); + k.push(b':'); + k.extend_from_slice(elem); + k + } + + /// Prefix chung của mọi shortcut key: `"{prefix}:shortcut:"`. + /// Dùng làm MATCH pattern khi SCAN để xoá toàn bộ shortcuts. + pub fn shortcut_prefix(&self) -> String { + self.key("shortcut") + ":" + } +} + +/// Helper shorthand: `cmd("LLEN")` → `redis::cmd("LLEN")` +fn cmd(name: &str) -> redis::Cmd { + redis::cmd(name) +} + +// ==================== RedisStorage ==================== + +pub struct RedisStorage { + conn: Arc>, + kb: KeyBuilder, +} + +impl RedisStorage { + async fn lock(&self) -> tokio::sync::MutexGuard<'_, MultiplexedConnection> { + self.conn.lock().await + } + + pub async fn new(client: redis::Client, prefix: &str) -> Result { + let conn = client + .get_multiplexed_async_connection() + .await + .map_err(|e| StorageError::Internal(e.to_string()))?; + let s = Self { + conn: Arc::new(Mutex::new(conn)), + kb: KeyBuilder::new(prefix), + }; + s.init().await?; + Ok(s) + } + + #[allow(dead_code)] // helper — chưa có caller nội bộ. + pub async fn from_multiplexed(conn: MultiplexedConnection, prefix: &str) -> Result { + let s = Self { + conn: Arc::new(Mutex::new(conn)), + kb: KeyBuilder::new(prefix), + }; + s.init().await?; + Ok(s) + } + + #[allow(dead_code)] // helper — chưa có caller nội bộ. + pub async fn with_key_builder(client: redis::Client, kb: KeyBuilder) -> Result { + let conn = client + .get_multiplexed_async_connection() + .await + .map_err(|e| StorageError::Internal(e.to_string()))?; + let s = Self { + conn: Arc::new(Mutex::new(conn)), + kb, + }; + s.init().await?; + Ok(s) + } + + async fn init(&self) -> Result<()> { + let mut conn = self.lock().await; + let exists: bool = cmd("EXISTS") + .arg(self.kb.key("branch")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + if !exists { + redis::pipe() + .atomic() + .rpush(self.kb.key("branch"), b"" as &[u8]) + .rpush(self.kb.key("record"), 0i64) + .exec_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + } + Ok(()) + } + + /// Độ dài hiện tại của branch list = số node (gồm sentinel). + /// Node id tiếp theo = len - 1. + #[allow(dead_code)] // helper — chưa có caller nội bộ. + async fn node_len(&self) -> Result { + let mut conn = self.lock().await; + let len: usize = cmd("LLEN") + .arg(self.kb.key("branch")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(len) + } +} + +#[async_trait] +impl Storage for RedisStorage { + async fn new_node(&mut self, prefix: Vec, record: usize) -> Result { + let mut conn = self.lock().await; + let result: redis::Value = redis::pipe() + .atomic() + .rpush(self.kb.key("branch"), &prefix[..]) + .rpush(self.kb.key("record"), record as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + + let len: usize = match result { + redis::Value::Array(ref items) => match items.first() { + Some(redis::Value::Int(n)) => *n as usize, + _ => cmd("LLEN") + .arg(self.kb.key("branch")) + .query_async::(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?, + }, + _ => cmd("LLEN") + .arg(self.kb.key("branch")) + .query_async::(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?, + }; + + Ok(len - 1) + } + + async fn update_node( + &mut self, + id: usize, + prefix: Option>, + record: Option, + ) -> Result<()> { + let mut conn = self.lock().await; + let mut pipe = redis::pipe(); + pipe.atomic(); + if let Some(p) = prefix { + pipe.lset(self.kb.key("branch"), id as isize, &p[..]); + } + if let Some(r) = record { + pipe.lset(self.kb.key("record"), id as isize, r as i64); + } + pipe.exec_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_node(&self, id: usize) -> Result<(Vec, usize)> { + let mut conn = self.lock().await; + let prefix: Vec = cmd("LINDEX") + .arg(self.kb.key("branch")) + .arg(id as isize) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + let rec: i64 = cmd("LINDEX") + .arg(self.kb.key("record")) + .arg(id as isize) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok((prefix, rec as usize)) + } + + async fn get_children(&self, id: usize) -> Result> { + let mut conn = self.lock().await; + let children: Vec = cmd("SMEMBERS") + .arg(self.kb.indexed("forward", id)) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(children.into_iter().map(|x| x as usize).collect()) + } + + #[cfg(feature = "bloom-search")] + async fn set_node_bloom(&mut self, id: usize, bloom: &[u8]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("node_bloom")) + .arg(id) + .arg(bloom) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + #[cfg(feature = "bloom-search")] + async fn get_node_bloom(&self, id: usize) -> Result>> { + let mut conn = self.lock().await; + let bloom: Option> = cmd("HGET") + .arg(self.kb.key("node_bloom")) + .arg(id) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(bloom) + } + + async fn set_root(&mut self, shard: usize, root: usize) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("endpoint")) + .arg(shard as i64) + .arg(root as i64) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_root(&self, shard: usize) -> Result { + let mut conn = self.lock().await; + let root: Option = cmd("HGET") + .arg(self.kb.key("endpoint")) + .arg(shard as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(root.unwrap_or(0) as usize) + } + + async fn set_meta(&mut self, record: usize, meta: &[u8]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("meta")) + .arg(record as i64) + .arg(meta) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_meta(&self, record: usize) -> Result>> { + let mut conn = self.lock().await; + let meta: Option> = cmd("HGET") + .arg(self.kb.key("meta")) + .arg(record as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(meta) + } + + async fn set_key_len(&mut self, record: usize, len: usize) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("keylen")) + .arg(record as i64) + .arg(len as i64) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_key_len(&self, record: usize) -> Result> { + let mut conn = self.lock().await; + let len: Option = cmd("HGET") + .arg(self.kb.key("keylen")) + .arg(record as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(len.map(|x| x as usize)) + } + + async fn add_shortcut_node(&mut self, shard: usize, elem: &[u8], node_id: usize) -> Result<()> { + let mut conn = self.lock().await; + cmd("SADD") + .arg(self.kb.shortcut(shard, elem)) + .arg(node_id as i64) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_shortcut_nodes(&self, shard: usize, elem: &[u8]) -> Result> { + let mut conn = self.lock().await; + let nodes: Vec = cmd("SMEMBERS") + .arg(self.kb.shortcut(shard, elem)) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(nodes.into_iter().map(|x| x as usize).collect()) + } + + async fn clear_shortcuts(&mut self) -> Result<()> { + let mut conn = self.lock().await; + let pattern = format!("{}*", self.kb.shortcut_prefix()); + let mut cursor: u64 = 0; + loop { + let (next_cursor, keys): (u64, Vec) = cmd("SCAN") + .arg(cursor) + .arg("MATCH") + .arg(&pattern) + .arg("COUNT") + .arg(500) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + for key in keys { + cmd("DEL") + .arg(key) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + } + cursor = next_cursor; + if cursor == 0 { + break; + } + } + Ok(()) + } + + async fn set_edge_data(&mut self, edge: usize, data: &[u8]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("edgedata")) + .arg(edge as i64) + .arg(data) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_edge_data(&self, edge: usize) -> Result>> { + let mut conn = self.lock().await; + let data: Option> = cmd("HGET") + .arg(self.kb.key("edgedata")) + .arg(edge as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(data) + } + + async fn clear_edges(&mut self) -> Result<()> { + let mut conn = self.lock().await; + cmd("DEL") + .arg(self.kb.key("edgedata")) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn for_each_edge_data( + &self, + f: &mut (dyn for<'a> FnMut(usize, &'a [u8]) -> Result<()> + Send), + ) -> Result<()> { + let mut conn = self.lock().await; + let items: Vec<(i64, Vec)> = cmd("HGETALL") + .arg(self.kb.key("edgedata")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + for (id, data) in items { + f(id as usize, &data)?; + } + Ok(()) + } + + async fn set_node_meta(&mut self, elem: usize, meta: &[u8]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("nodemeta")) + .arg(elem as i64) + .arg(meta) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_node_meta(&self, elem: usize) -> Result>> { + let mut conn = self.lock().await; + let meta: Option> = cmd("HGET") + .arg(self.kb.key("nodemeta")) + .arg(elem as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(meta) + } + + async fn clear_node_meta(&mut self) -> Result<()> { + let mut conn = self.lock().await; + cmd("DEL") + .arg(self.kb.key("nodemeta")) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn set_chain(&mut self, record: usize, chain: &[u64]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("chains")) + .arg(record as i64) + .arg(super::encode_chain(chain)) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_chain(&self, record: usize) -> Result>> { + let mut conn = self.lock().await; + let bytes: Option> = cmd("HGET") + .arg(self.kb.key("chains")) + .arg(record as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(bytes.map(|b| super::decode_chain(&b))) + } + + async fn clear_chains(&mut self) -> Result<()> { + let mut conn = self.lock().await; + cmd("DEL") + .arg(self.kb.key("chains")) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn save_symbol(&mut self, sym: &Symbol) -> Result<()> { + let mut conn = self.lock().await; + let data = serde_json::to_vec(sym).map_err(|e| StorageError::Internal(e.to_string()))?; + cmd("HSET") + .arg(self.kb.key("symbols")) + .arg(sym.id as i64) + .arg(data) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn load_symbol(&self, id: u64) -> Result> { + let mut conn = self.lock().await; + let data: Option> = cmd("HGET") + .arg(self.kb.key("symbols")) + .arg(id as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + data.map(|d| serde_json::from_slice(&d).map_err(|e| StorageError::Internal(e.to_string()))) + .transpose() + } + + async fn load_all_symbols(&self) -> Result> { + let mut conn = self.lock().await; + let map: HashMap> = cmd("HGETALL") + .arg(self.kb.key("symbols")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + let mut out: Vec = Vec::with_capacity(map.len()); + for data in map.into_values() { + out.push( + serde_json::from_slice(&data).map_err(|e| StorageError::Internal(e.to_string()))?, + ); + } + out.sort_by_key(|s| s.id); + Ok(out) + } + + async fn save_next_id(&mut self, next: u64) -> Result<()> { + let mut conn = self.lock().await; + cmd("SET") + .arg(self.kb.key("nextid")) + .arg(next as i64) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn load_next_id(&self) -> Result { + let mut conn = self.lock().await; + let next: Option = cmd("GET") + .arg(self.kb.key("nextid")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + // Registry chưa có symbol — bắt đầu từ SYMBOL_BASE (giống sqlite init). + Ok(next + .map(|n| n as u64) + .unwrap_or(codegraph_core::SYMBOL_BASE)) + } + + async fn all_chains(&self) -> Result)>> { + let mut conn = self.lock().await; + let map: HashMap> = cmd("HGETALL") + .arg(self.kb.key("chains")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + let mut out: Vec<(u64, Vec)> = map.into_iter().map(|(r, b)| (r as u64, b)).collect(); + out.sort_by_key(|(r, _)| *r); + Ok(out) + } + + async fn set_call_records(&mut self, func: u64, records: &[u8]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("callrecords")) + .arg(func as i64) + .arg(records) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn get_call_records(&self, func: u64) -> Result>> { + let mut conn = self.lock().await; + let records: Option> = cmd("HGET") + .arg(self.kb.key("callrecords")) + .arg(func as i64) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(records) + } + + async fn all_call_records(&self) -> Result)>> { + let mut conn = self.lock().await; + let map: HashMap> = cmd("HGETALL") + .arg(self.kb.key("callrecords")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + let mut out: Vec<(u64, Vec)> = map.into_iter().map(|(f, b)| (f as u64, b)).collect(); + out.sort_by_key(|(f, _)| *f); + Ok(out) + } + + async fn set_call_name_index(&mut self, name: &str, sites: &[u8]) -> Result<()> { + let mut conn = self.lock().await; + cmd("HSET") + .arg(self.kb.key("callnames")) + .arg(name) + .arg(sites) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn load_call_name_index(&self, name: &str) -> Result>> { + let mut conn = self.lock().await; + let sites: Option> = cmd("HGET") + .arg(self.kb.key("callnames")) + .arg(name) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(sites) + } + + async fn all_call_name_indexes(&self) -> Result)>> { + let mut conn = self.lock().await; + let map: HashMap> = cmd("HGETALL") + .arg(self.kb.key("callnames")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + let mut out: Vec<(String, Vec)> = map.into_iter().collect(); + out.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(out) + } + + async fn upsert_file(&mut self, f: &FileInfo) -> Result<()> { + let mut conn = self.lock().await; + let data = serde_json::to_vec(f).map_err(|e| StorageError::Internal(e.to_string()))?; + cmd("HSET") + .arg(self.kb.key("files")) + .arg(&f.path) + .arg(data) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn load_all_files(&self) -> Result> { + let mut conn = self.lock().await; + let map: HashMap> = cmd("HGETALL") + .arg(self.kb.key("files")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + let mut out: Vec = Vec::with_capacity(map.len()); + for data in map.into_values() { + out.push( + serde_json::from_slice(&data).map_err(|e| StorageError::Internal(e.to_string()))?, + ); + } + out.sort_by(|a, b| a.path.cmp(&b.path)); + Ok(out) + } + + async fn version(&self) -> Result { + let mut conn = self.lock().await; + let v: Option = cmd("GET") + .arg(self.kb.key("version")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(v.map(|n| n as u64).unwrap_or(0)) + } + + async fn set_version(&mut self, v: u64) -> Result<()> { + let mut conn = self.lock().await; + cmd("SET") + .arg(self.kb.key("version")) + .arg(v as i64) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + async fn clear_entities(&mut self) -> Result<()> { + let mut conn = self.lock().await; + cmd("DEL") + .arg(self.kb.key("symbols")) + .arg(self.kb.key("nextid")) + .arg(self.kb.key("callrecords")) + .arg(self.kb.key("callnames")) + .arg(self.kb.key("files")) + .arg(self.kb.key("version")) + .query_async::<()>(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } + + fn new_tx(&self) -> Box { + Box::new(RedisTx { + conn: self.conn.clone(), + kb: self.kb.clone(), + nodes: Vec::new(), + ops: Vec::new(), + }) + } +} + +// ==================== Redis Transaction ==================== + +/// Transaction cho `RedisStorage`. +/// +/// - `new_node` snapshot độ dài branch list lúc tạo tx, id = base + n +/// (giả định single-connection — toàn bộ command đi qua cùng 1 mutex). +/// - `commit` build một MULTI/EXEC pipeline: RPUSH toàn bộ node mới trước, +/// rồi áp dụng các op cấu trúc — atomic, không lộ trạng thái trung gian. +pub struct RedisTx { + conn: Arc>, + kb: KeyBuilder, + nodes: Vec<(usize, Vec, usize)>, + ops: Vec, +} + +#[async_trait] +impl Tx for RedisTx { + async fn new_node(&mut self, prefix: Vec, record: usize) -> Result { + let base = self.node_len_checked().await?; + let id = base + self.nodes.len(); + self.nodes.push((id, prefix, record)); + Ok(id) + } + + async fn update_node( + &mut self, + id: usize, + prefix: Option>, + record: Option, + ) -> Result<()> { + self.ops.push(TxOp::UpdateNode { id, prefix, record }); + Ok(()) + } + + async fn add_child(&mut self, parent: usize, child: usize) -> Result<()> { + self.ops.push(TxOp::AddChild { parent, child }); + Ok(()) + } + + async fn move_child(&mut self, from: usize, to: usize, child: usize) -> Result<()> { + self.ops.push(TxOp::MoveChild { from, to, child }); + Ok(()) + } + + async fn commit(self: Box) -> Result<()> { + let RedisTx { + conn, + kb, + nodes, + ops, + .. + } = *self; + + let mut conn = conn.lock().await; + let mut pipe = redis::pipe(); + pipe.atomic(); + + // 1. RPUSH toàn bộ node mới (sentinel đã có sẵn ở index 0). + for (_, prefix, record) in &nodes { + pipe.rpush(kb.key("branch"), &prefix[..]); + pipe.rpush(kb.key("record"), *record as i64); + } + + // 2. Áp dụng ops. + for op in ops { + match op { + TxOp::AddChild { parent, child } => { + pipe.cmd("SADD") + .arg(kb.indexed("forward", parent)) + .arg(child as i64) + .ignore(); + } + TxOp::MoveChild { from, to, child } => { + pipe.cmd("SREM") + .arg(kb.indexed("forward", from)) + .arg(child as i64) + .ignore(); + pipe.cmd("SADD") + .arg(kb.indexed("forward", to)) + .arg(child as i64) + .ignore(); + } + TxOp::UpdateNode { id, prefix, record } => { + if let Some(p) = prefix { + pipe.lset(kb.key("branch"), id as isize, &p[..]); + } + if let Some(r) = record { + pipe.lset(kb.key("record"), id as isize, r as i64); + } + } + } + } + + pipe.exec_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(()) + } +} + +impl RedisTx { + async fn node_len_checked(&self) -> Result { + let mut conn = self.conn.lock().await; + let len: usize = cmd("LLEN") + .arg(self.kb.key("branch")) + .query_async(&mut *conn) + .await + .map_err(|e: redis::RedisError| StorageError::Internal(e.to_string()))?; + Ok(len) + } +} + +// ── Tests ────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicU16, Ordering}; + + use super::*; + use crate::radix::EMPTY; + use crate::storage::Storage; + + static COUNTER: AtomicU16 = AtomicU16::new(0); + + async fn new_test_storage() -> RedisStorage { + let n = COUNTER.fetch_add(1, Ordering::Relaxed); + let pid = std::process::id(); + let client = redis::Client::open("redis://127.0.0.1:6379/15") + .expect("redis connection failed — is redis-server running?"); + RedisStorage::new(client, &format!("test:radix:{}:{n}", pid)) + .await + .expect("init failed") + } + + #[tokio::test] + async fn test_new_node_and_get_node() { + let mut s = new_test_storage().await; + let id = s.new_node(b"hello".to_vec(), 42).await.unwrap(); + assert_ne!(id, EMPTY); + let (prefix, record) = s.get_node(id).await.unwrap(); + assert_eq!(prefix, b"hello"); + assert_eq!(record, 42); + } + + #[tokio::test] + async fn test_meta_roundtrip() { + let mut s = new_test_storage().await; + assert_eq!(s.get_meta(42).await.unwrap(), None); + assert_eq!(s.get_key_len(42).await.unwrap(), None); + s.set_meta(42, b"call-site-info").await.unwrap(); + s.set_key_len(42, 5).await.unwrap(); + assert_eq!( + s.get_meta(42).await.unwrap().as_deref(), + Some(b"call-site-info".as_slice()) + ); + assert_eq!(s.get_key_len(42).await.unwrap(), Some(5)); + s.set_meta(42, b"updated").await.unwrap(); + assert_eq!( + s.get_meta(42).await.unwrap().as_deref(), + Some(b"updated".as_slice()) + ); + } + + #[tokio::test] + async fn test_shortcuts_roundtrip() { + let mut s = new_test_storage().await; + assert!(s.get_shortcut_nodes(1, b"l").await.unwrap().is_empty()); + s.add_shortcut_node(1, b"l", 10).await.unwrap(); + s.add_shortcut_node(1, b"l", 20).await.unwrap(); + s.add_shortcut_node(1, b"o", 10).await.unwrap(); + let nodes = s.get_shortcut_nodes(1, b"l").await.unwrap(); + assert!(nodes.contains(&10) && nodes.contains(&20)); + assert_eq!(nodes.len(), 2); + s.clear_shortcuts().await.unwrap(); + assert!(s.get_shortcut_nodes(1, b"l").await.unwrap().is_empty()); + } + + #[tokio::test] + async fn test_tx_split_commit() { + let mut s = new_test_storage().await; + let parent = s.new_node(b"hello".to_vec(), 1).await.unwrap(); + + let mut tx = s.new_tx(); + let new_id = tx.new_node(b"p".to_vec(), 2).await.unwrap(); + let leg_id = tx.new_node(b"lo".to_vec(), 1).await.unwrap(); + tx.move_child(parent, leg_id, 0).await.unwrap(); + tx.add_child(parent, leg_id).await.unwrap(); + tx.add_child(parent, new_id).await.unwrap(); + tx.update_node(parent, Some(b"hel".to_vec()), Some(0)) + .await + .unwrap(); + tx.commit().await.unwrap(); + + let (prefix, _) = s.get_node(parent).await.unwrap(); + assert_eq!(prefix, b"hel"); + let children = s.get_children(parent).await.unwrap(); + assert!(children.contains(&leg_id)); + assert!(children.contains(&new_id)); + } +} diff --git a/crates/codegraph-graph/src/storage/sqlite.rs b/crates/codegraph-graph/src/storage/sqlite.rs index b31ea478d..32daf65c7 100644 --- a/crates/codegraph-graph/src/storage/sqlite.rs +++ b/crates/codegraph-graph/src/storage/sqlite.rs @@ -135,6 +135,10 @@ impl SqliteStorage { record INTEGER PRIMARY KEY, chain BLOB NOT NULL )", + "CREATE TABLE IF NOT EXISTS rt_node_blooms ( + id INTEGER PRIMARY KEY, + bloom BLOB NOT NULL + )", "CREATE TABLE IF NOT EXISTS rt_counter ( id INTEGER PRIMARY KEY CHECK (id = 1), next INTEGER NOT NULL @@ -267,6 +271,36 @@ impl Storage for SqliteStorage { Ok(out) } + #[cfg(feature = "bloom-search")] + async fn set_node_bloom(&mut self, id: usize, bloom: &[u8]) -> Result<()> { + let mut conn = self.pool.acquire().await.map_err(db_err)?; + sqlx::query( + "INSERT INTO rt_node_blooms (id, bloom) VALUES (?1, ?2) \ + ON CONFLICT(id) DO UPDATE SET bloom = excluded.bloom", + ) + .bind(id as i64) + .bind(bloom) + .execute(&mut *conn) + .await + .map_err(db_err)?; + Ok(()) + } + + #[cfg(feature = "bloom-search")] + async fn get_node_bloom(&self, id: usize) -> Result>> { + let mut conn = self.pool.acquire().await.map_err(db_err)?; + let row = sqlx::query("SELECT bloom FROM rt_node_blooms WHERE id = ?1") + .bind(id as i64) + .fetch_optional(&mut *conn) + .await + .map_err(db_err)?; + let Some(row) = row else { + return Ok(None); + }; + let bloom: Vec = row.try_get(0).map_err(db_err)?; + Ok(Some(bloom)) + } + async fn set_edge_data(&mut self, edge: usize, data: &[u8]) -> Result<()> { let mut conn = self.pool.acquire().await.map_err(db_err)?; sqlx::query( @@ -305,11 +339,10 @@ impl Storage for SqliteStorage { f: &mut (dyn for<'a> FnMut(usize, &'a [u8]) -> Result<()> + Send), ) -> Result<()> { let mut conn = self.pool.acquire().await.map_err(db_err)?; - let rows: Vec<(i64, Vec)> = - sqlx::query_as("SELECT id, data FROM rt_edges ORDER BY id") - .fetch_all(&mut *conn) - .await - .map_err(db_err)?; + let rows: Vec<(i64, Vec)> = sqlx::query_as("SELECT id, data FROM rt_edges ORDER BY id") + .fetch_all(&mut *conn) + .await + .map_err(db_err)?; for (id, data) in rows { f(id as usize, &data)?; } @@ -406,19 +439,16 @@ impl Storage for SqliteStorage { .fetch_optional(&mut *conn) .await .map_err(db_err)?; - data.map(|d| { - serde_json::from_slice(&d).map_err(|e| StorageError::Internal(e.to_string())) - }) - .transpose() + data.map(|d| serde_json::from_slice(&d).map_err(|e| StorageError::Internal(e.to_string()))) + .transpose() } async fn load_all_symbols(&self) -> Result> { let mut conn = self.pool.acquire().await.map_err(db_err)?; - let rows: Vec> = - sqlx::query_scalar("SELECT data FROM sg_symbols ORDER BY id") - .fetch_all(&mut *conn) - .await - .map_err(db_err)?; + let rows: Vec> = sqlx::query_scalar("SELECT data FROM sg_symbols ORDER BY id") + .fetch_all(&mut *conn) + .await + .map_err(db_err)?; rows.into_iter() .map(|d| serde_json::from_slice(&d).map_err(|e| StorageError::Internal(e.to_string()))) .collect() diff --git a/crates/codegraph-graph/tests/sqlite.rs b/crates/codegraph-graph/tests/sqlite.rs index fd22f7b90..b62b8aade 100644 --- a/crates/codegraph-graph/tests/sqlite.rs +++ b/crates/codegraph-graph/tests/sqlite.rs @@ -7,7 +7,7 @@ #![cfg(feature = "sqlite")] -use codegraph_core::{CallRecord, EffectType, Symbol, SymbolKind, SYMBOL_BASE}; +use codegraph_core::{CallRecord, EffectType, SYMBOL_BASE, Symbol, SymbolKind}; use codegraph_graph::{GraphIndex, ParseResult, SharedGraphIndex}; use std::collections::HashMap; use std::sync::Arc; @@ -105,10 +105,7 @@ async fn index_ingest_reopen_roundtrip() { assert_eq!(flow.calls[0].line, 3); // search_flow qua chain engine persistent. - let sf = idx - .search_flow(&[SYMBOL_BASE + 1]) - .await - .unwrap(); + let sf = idx.search_flow(&[SYMBOL_BASE + 1]).await.unwrap(); assert_eq!(sf.len(), 1); assert_eq!(sf[0].function_name, "a"); } @@ -182,3 +179,61 @@ async fn shared_index_rebuilds_on_reindex() { assert_eq!(idx2.stats().symbols, 1); assert_eq!(idx2.symbol_by_id(SYMBOL_BASE).unwrap().name, "x"); } + +/// Go: 2 hàm cùng tên (`process`) ở 2 package khác nhau = 2 FILE riêng. Mỗi +/// file là một `ParseResult` với id local riêng (cùng `SYMBOL_BASE`) — `ingest` +/// remap sang id global riêng biệt, cả symbol lẫn chain giữ nguyên, không đè +/// nhau theo tên. Cũng khẳng định thứ tự global id: file đầu tiên chiếm +/// `SYMBOL_BASE`, file sau `SYMBOL_BASE + 1`. +#[tokio::test] +async fn ingest_same_function_name_across_files_stays_distinct() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("db.sqlite"); + let db_str = db_path.to_string_lossy().into_owned(); + + // Hai package khác nhau (`store` và `cache`), mỗi package một hàm `process`. + let r_store = result( + "store/store.go", + vec![sym("store/store.go", "process", SYMBOL_BASE)], + HashMap::from([(SYMBOL_BASE, vec![SYMBOL_BASE])]), + vec![], + ); + let r_cache = result( + "cache/cache.go", + vec![sym("cache/cache.go", "process", SYMBOL_BASE)], + HashMap::from([(SYMBOL_BASE, vec![SYMBOL_BASE])]), + vec![], + ); + + let mut idx = GraphIndex::open(&db_str).await.unwrap(); + idx.ingest(&[r_store, r_cache]).await.unwrap(); + + // Cả 2 symbol cùng tên nhưng id global khác nhau, giữ đúng file. + assert_eq!(idx.stats().symbols, 2); + let s1 = idx.symbol_by_id(SYMBOL_BASE).unwrap(); + let s2 = idx.symbol_by_id(SYMBOL_BASE + 1).unwrap(); + assert_eq!(s1.name, "process"); + assert_eq!(s2.name, "process"); + assert_eq!(s1.file, "store/store.go"); + assert_eq!(s2.file, "cache/cache.go"); + + // Cả 2 đều giữ chain riêng → flow không bị "chain not found". + assert_eq!( + idx.flow(SYMBOL_BASE).await.unwrap().chain_desc, + vec!["process"] + ); + assert_eq!( + idx.flow(SYMBOL_BASE + 1).await.unwrap().chain_desc, + vec!["process"] + ); + + // Search tên trả đủ 2 kết quả (không hoà trộn thành 1). + let hits = idx + .search_symbol("process", Some(SymbolKind::Function), 10) + .await + .unwrap(); + assert_eq!(hits.len(), 2); + let mut files: Vec<&str> = hits.iter().map(|s| s.file.as_str()).collect(); + files.sort_unstable(); + assert_eq!(files, vec!["cache/cache.go", "store/store.go"]); +} diff --git a/crates/codegraph-mcp/Cargo.toml b/crates/codegraph-mcp/Cargo.toml index 37533c66e..587dfde82 100644 --- a/crates/codegraph-mcp/Cargo.toml +++ b/crates/codegraph-mcp/Cargo.toml @@ -8,8 +8,10 @@ repository.workspace = true [dependencies] codegraph-api = { path = "../codegraph-api" } codegraph-core = { path = "../codegraph-core" } +codegraph-extract = { path = "../codegraph-extract" } codegraph-graph = { path = "../codegraph-graph", features = ["sqlite"] } codegraph-context = { path = "../codegraph-context" } +codegraph-sboxes = { path = "../codegraph-sboxes" } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } diff --git a/crates/codegraph-mcp/src/lib.rs b/crates/codegraph-mcp/src/lib.rs index d212b2e53..6019b2331 100644 --- a/crates/codegraph-mcp/src/lib.rs +++ b/crates/codegraph-mcp/src/lib.rs @@ -18,15 +18,21 @@ pub const SERVER_NAME: &str = "codegraph"; pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); pub struct McpServer { + /// Workspace root — dùng cho admin tools (`codegraph_init` / `codegraph_index`). + root: camino::Utf8PathBuf, shared_index: Arc, /// Telemetry cho `codegraph_query_usage_report`. usage: Arc>, } impl McpServer { - pub async fn new(index_path: Option) -> anyhow::Result { + pub async fn new( + root: camino::Utf8PathBuf, + index_path: Option, + ) -> anyhow::Result { let shared_index = Arc::new(SharedGraphIndex::open(index_path).await?); Ok(Self { + root, shared_index, usage: Arc::new(Mutex::new(usage::UsageStats::default())), }) @@ -115,7 +121,23 @@ impl McpServer { } let api = codegraph_api::GraphApi::new_with_index(self.shared_index.clone()); - let text = match tools::dispatch_with_api(&api, name, args).await { + // Admin tools (init/index) cần workspace root; sandbox cần root (config + + // mock dirs) + snapshot index — dispatch riêng, không qua GraphApi. + let dispatch = if name == "codegraph_init" || name == "codegraph_index" { + tools::dispatch_admin(&self.root, name, args.clone()).await + } else if name == "codegraph_sandbox" { + tools::dispatch_sandbox(&self.root, self.shared_index.clone(), args.clone()).await + } else if name == "codegraph_diff" { + tools::dispatch_diff(&self.root, self.shared_index.clone(), args.clone()).await + } else if name == "codegraph_diff_simulate" { + tools::dispatch_diff_simulate(&self.root, self.shared_index.clone(), args.clone()).await + } else if name == "codegraph_origin_simulate" { + tools::dispatch_origin_simulate(&self.root, self.shared_index.clone(), args.clone()) + .await + } else { + tools::dispatch_with_api(&api, name, args).await + }; + let text = match dispatch { Ok(t) => t, Err(e) => { self.usage diff --git a/crates/codegraph-mcp/src/server-instructions.md b/crates/codegraph-mcp/src/server-instructions.md index 9af1c3946..435ab070e 100644 --- a/crates/codegraph-mcp/src/server-instructions.md +++ b/crates/codegraph-mcp/src/server-instructions.md @@ -33,6 +33,12 @@ file-reading sub-task repeats work codegraph already did. | "Show me this symbol by id / exact name." | `codegraph_symbol` | | "What's in directory X?" | `codegraph_files` | | "Is the index ready / what's its size?" | `codegraph_status` | +| "Set up / (re)build the index" | `codegraph_init` (idempotent; index=true by default) | +| "Re-index the workspace" | `codegraph_index` | +| "Run an entry function in the behavior sandbox" | `codegraph_sandbox` (per-function Rhai mocks) | +| "Diff này (MR/patch/git diff) ảnh hưởng gì tới graph?" | `codegraph_diff` (read-only draft) | +| "MR này đổi hành vi flow ra sao (trước vs sau)?" | `codegraph_diff_simulate` (sandbox before/after) | +| "Flow này ở `origin/main` đang chạy thế nào so với code local của tôi (chưa commit)?" | `codegraph_origin_simulate` (ref vs working tree) | ## Disambiguating duplicate names @@ -56,3 +62,147 @@ Symbols are identified by numeric `id` (global registry, ≥ 100). Call-chain patterns in `codegraph_search_flow` mix marker names (`LOOP`, `IF_TRUE`, `IF_FALSE`, `BRANCH_END`, `RETURN`, `LOOP_BACK`, `SWITCH_CASE`, `SWITCH_END`, `BREAK`, `CONTINUE`, `THROW`), symbol ids, and symbol names. + +## Behavior sandbox — `codegraph_sandbox` + +Compiles an entry function (plus its in-flow callees) to machine code and runs +it against **Rhai mocks**, returning the observed call trace. Use it to +simulate "what does this flow actually do" before touching code. + +Arguments: +- `node` (or `name`): the entry function symbol id or name. +- `args`: array of `i64` entry arguments (default `[]`). +- `mocks`: object mapping callee name → Rhai source. The source is either a + mock body (`77` → becomes `fn (args) { 77 }`) or a full + `fn (args) { … }` script. Inline mocks **win over** mocks loaded from + `mock_dirs` in `.codegraph/config.toml`. Mock contract: `args` is a single + array of `i64`. +- `branch_policy`: optional `"if_true"` / `"if_false"` condition resolution + override (defaults to `.codegraph/config.toml`). +- `loop_cap`: optional integer loop-iteration cap. + +The response reports `return`, the mocked calls in order (`mocks`), condition +decisions (`conds`), and any callee that ran without a mock (`missing_mocks`) — +mock those next. `.codegraph/config.toml` `[sandbox]` sets defaults +(`mock_dirs`, `branch_policy`, `loop_cap`); the per-call arguments override +them. + +**Link-time mock check:** before compiling, the sandbox verifies that every +callee the flow will dispatch to a mock has one configured (file `mock_dirs` or +a `mocks` override). Any unconfigured callee fails the call with +`link failed: no mock configured for callee(s): …` listing the exact functions +to mock — supply them in `mocks` (or a `*.rhai` file) and call again. + +## Diff draft — `codegraph_diff` + +Analyzes a unified diff (MR diff, `.patch` file content, or `git diff` output) +against the current index and returns a **DRAFT** of how the graph would +change — it does NOT mutate the index. Use it to review an MR's logic impact +before merging: which symbols are touched, which flows carry call sites on the +changed lines, and who (transitively) calls the touched functions. + +Arguments: +- `diff`: the unified diff text. Supports multi-file diffs, added/removed/ + renamed files, and `\ No newline at end of file`. + +Response shape: +```json +{ + "draft": true, + "summary": { + "files_in_diff": 2, "files_matched": 2, "symbols_affected": 1, + "flows_affected": 1, "new_files": [], "unmatched_files": [] + }, + "files": [{ + "path": "src/foo.rs", "matched": true, + "matched_path": "/abs/workspace/src/foo.rs", + "added_lines": 3, "removed_lines": 2, "deleted": false, + "symbols": [{ "symbol": { "id": 141, "name": "foo", "file": "src/foo.rs", "line": 10, "end_line": 25 }, "impact": "modified" }], + "flows": [{ + "flow": { "id": 141, "name": "foo", "file": "src/foo.rs", "line": 10 }, + "affected_calls": [{ "position": 3, "callee": "bar", "to_id": 155, "line": 12, "markers": ["IF_TRUE"] }], + "marker_window": ["IF_TRUE", "BRANCH_END"], + "called_by": [{ "id": 100, "name": "main", "file": "src/main.rs" }] + }] + }] +} +``` + +Key points: +- Line numbers come from the **new** (b-) side of each hunk, which is what the + current index reflects (working tree = "after the MR"). +- `impact: "removed"` means the whole file was deleted; `"modified"` means at + least one line inside the symbol's span changed. +- `affected_calls` lists the flow's call sites sitting on changed lines; + `markers` is the guard-marker run directly before each call site (e.g. the + `IF_TRUE`/`LOOP` surrounding it), and `marker_window` is the deduped marker + span of the whole affected region. +- A file that doesn't match anything in the index lands in + `summary.unmatched_files` (never indexed) or `summary.new_files` (added file + with no removed lines). + +## Diff simulation — `codegraph_diff_simulate` + +Chains `codegraph_diff` with the sandbox: for the functions a diff touches, it +runs the entry flow TWICE — on the current index (post-MR) and on a temporary +index rebuilt from a git ref — then compares the traces. + +Arguments (besides `diff`): +- `entry`: function name to simulate (default: first function affected by the + diff). +- `base_ref`: git ref for the BEFORE state (default `HEAD`; the pre-MR tree is + materialized with `git archive`, so the workspace must be a git repo). +- `args`, `mocks`, `branch_policy`, `loop_cap`: same contract as + `codegraph_sandbox`. + +Response shape: +```json +{ + "draft": true, "entry": "compute", "base_ref": "HEAD", + "affected_functions": ["compute", "cap"], + "before": { "present": true, "return": 50, "sequence": ["if:1", "call:fetch"], "missing_mocks": [] }, + "after": { "present": true, "return": 6, "sequence": ["if:1", "call:fetch", "call:extra"], "missing_mocks": [] }, + "delta": { "sequence_added": ["call:extra"], "sequence_removed": [] } +} +``` + +What the trace captures (and what it doesn't): the sandbox follows flow +**structure** — mock call order, branch presence, loop iterations. Branch +decisions follow `branch_policy` (if_true/if_false; the guard text is NOT +evaluated), loops run up to `loop_cap`, and **numeric arithmetic on values is +not modeled**. So the reliable signal is `delta.sequence_added/removed` — e.g. +an MR that adds/removes a call, a branch, or switches a callee shows up as a +sequence delta; an MR that only changes an arithmetic expression does not. +A function that doesn't exist in `base_ref` (new in the MR) reports +`before.present: false`; a callee without a mock reports +`link_error: no mock configured for callee(s): …` (compile aborts before +running — supply it in `mocks` and retry). + +## Origin/ref simulation — `codegraph_origin_simulate` + +The standalone "before" half of `codegraph_diff_simulate`, WITHOUT a diff: run +the sandbox on an entry flow at a git ref (default `HEAD`, e.g. `origin/main`) +and on the current working tree, then compare the traces. Use it to see whether +your local uncommitted edits change a flow's behavior, or to inspect what a flow +does on a specific branch/commit before you touch anything. + +Arguments: +- `entry` (required): function name — resolved by NAME in each index (symbol ids + differ between the ref tree and the working tree). +- `ref`: git ref for the ORIGIN state (default `HEAD`; materialized with + `git archive`, so the workspace must be a git repo). +- `args`, `mocks`, `branch_policy`, `loop_cap`: same contract as + `codegraph_sandbox`. + +Response shape: +```json +{ + "draft": true, "entry": "compute", "ref": "origin/main", + "origin": { "present": true, "return": 50, "sequence": ["if:1", "call:fetch"], "missing_mocks": [] }, + "working_tree": { "present": true, "return": 6, "sequence": ["if:1", "call:fetch", "call:extra"], "missing_mocks": [] }, + "delta": { "sequence_added": ["call:extra"], "sequence_removed": [] } +} +``` + +Trace semantics and limitations are identical to `codegraph_diff_simulate` +above (structure-based, not arithmetic). diff --git a/crates/codegraph-mcp/src/tools.rs b/crates/codegraph-mcp/src/tools.rs index f0943b9f5..be975cc4e 100644 --- a/crates/codegraph-mcp/src/tools.rs +++ b/crates/codegraph-mcp/src/tools.rs @@ -1,7 +1,12 @@ +use camino::{Utf8Path, Utf8PathBuf}; use codegraph_api::GraphApi; use codegraph_context::{ContextRequest, Format}; -use codegraph_core::{Error, Result, Symbol, SymbolKind, SymbolMatch}; +use codegraph_core::{is_marker, Error, Result, Symbol, SymbolKind, SymbolMatch}; +use codegraph_extract::{init_project, project_db_path, project_dir, ExtractStats, Orchestrator}; +use codegraph_graph::{GraphIndex, SharedGraphIndex}; +use codegraph_sboxes::{compile_with_mocks, BranchPolicy, SboxConfig}; use serde_json::{json, Value}; +use std::sync::Arc; pub fn tool_definitions() -> Vec { vec![ @@ -86,6 +91,19 @@ pub fn tool_definitions() -> Vec { "Index health: symbol / chain / edge / file counts.", json!({ "type": "object", "properties": {} }), ), + // ── Admin tools (init / index) — thao tác trên workspace root của server ── + tool( + "codegraph_init", + "Initialize the workspace for CodeGraph (idempotent): creates .codegraph/ with .gitignore, version, and config.toml. Pass index=false to skip the full re-index that runs by default.", + json!({ "type": "object", "properties": { + "index": { "type": "boolean", "default": true } + } }), + ), + tool( + "codegraph_index", + "Full re-index of the workspace into .codegraph/db.sqlite. Requires the workspace to be initialized (run codegraph_init first).", + json!({ "type": "object", "properties": {} }), + ), // ── Enhanced symbol search (semgraph_search_symbol) ── tool( "codegraph_search_symbol", @@ -172,6 +190,52 @@ pub fn tool_definitions() -> Vec { "reset": { "type": "boolean", "default": false } } }), ), + // ── Behavior sandbox (compile a flow to machine code + run with mocks) ── + tool( + "codegraph_sandbox", + "Run a sandbox simulation of a function's flow: compile the entry function + its in-flow callees into machine code (Cranelift JIT) and run it with Rhai mocks. `mocks` maps a callee name to a Rhai body (auto-wrapped into `fn (args) { … }` where `args` is the call's i64 array) or a full `fn (args) { … }` script; inline mocks override `[sandbox].mock_dirs` files. Before compiling, every callee that will be mock-dispatched must have a mock (file or `mocks`); if any is unconfigured the call fails with `link failed: no mock configured for callee(s): …`. Returns the entry return value, the ordered mock invocations, control-flow decisions (if/loop/switch taken/skipped), and any callees that still ran without a mock (`missing_mocks`).", + json!({ "type": "object", "properties": { + "node": { "type": "integer", "description": "Entry function symbol id (from codegraph_search / codegraph_flow)." }, + "name": { "type": "string", "description": "Entry function name (substring → first function match); used when node is omitted." }, + "args": { "type": "array", "items": { "type": "integer" }, "description": "Abstract i64 arguments passed to the entry function." }, + "mocks": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Callee name → Rhai mock body or full `fn` source." }, + "branch_policy": { "type": "string", "enum": ["if_true", "if_false"], "description": "Override config branch_policy (default from config.toml)." }, + "loop_cap": { "type": "integer", "description": "Override config loop_cap — max loop iterations, guarantees termination." } + } }), + ), + // ── Diff draft (unified diff → graph impact, read-only) ── + tool( + "codegraph_diff", + "Analyze a unified diff (MR / patch file / `git diff` output) against the indexed graph and produce a DRAFT report of what would change in codegraph-graph: which symbols (functions/methods/classes) are touched (by line overlap), which flows contain call sites on changed lines, the control-flow marker window around each affected call (IF_TRUE/LOOP/BRANCH_END…), and which flows call the touched functions. The index itself is NOT mutated — this is a dry-run assessment you can review before applying the diff.", + json!({ "type": "object", "properties": { + "diff": { "type": "string", "description": "Unified diff text: `git diff` output, a .patch file content, or the diff from an MR. Supports multi-file diffs, added/removed/renamed files, and `\\ No newline at end of file`." } + }, "required": ["diff"] }), + ), + tool( + "codegraph_diff_simulate", + "Diff → behavior simulation (draft): take a unified diff, find the functions it touches, then run the sboxes sandbox on the entry flow BOTH on the current index (post-MR) and on a temporary index built from a git ref (`base_ref`, default HEAD = pre-MR), and compare the observed traces (ordered mock calls, condition decisions). The sandbox follows flow STRUCTURE: branch decisions follow `branch_policy` (if_true/if_false, it does not read the guard text), loops run up to `loop_cap`, and mock call order reflects the flow — numeric arithmetic on values is NOT modeled. Requires the workspace to be a git repo (pre-MR tree comes from `git archive`) and the entry flow to be sandbox-friendly (primitive args, library callees mocked via `mocks`). Read-only — the index is never mutated.", + json!({ "type": "object", "properties": { + "diff": { "type": "string", "description": "Unified diff text (MR / patch / git diff)." }, + "entry": { "type": "string", "description": "Optional entry function name (substring). Default: first function affected by the diff." }, + "base_ref": { "type": "string", "description": "Git ref for the BEFORE state (default HEAD)." }, + "args": { "type": "array", "items": { "type": "integer" }, "description": "Abstract i64 arguments passed to the entry function." }, + "mocks": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Callee name → Rhai mock body/fn." }, + "branch_policy": { "type": "string", "enum": ["if_true", "if_false"], "description": "Override config branch_policy." }, + "loop_cap": { "type": "integer", "description": "Override config loop_cap." } + }, "required": ["diff"] }), + ), + tool( + "codegraph_origin_simulate", + "Ref vs working tree simulation (draft): run the sboxes sandbox on an entry flow at a git ref (default HEAD, e.g. `origin/main`) — a temporary index built from `git archive ` — AND on the current index (working tree), then compare the observed traces (ordered mock calls, condition decisions). No diff needed: you pick any entry function and immediately see whether local uncommitted edits change its flow's behavior. The sandbox follows flow STRUCTURE: branch decisions follow `branch_policy` (if_true/if_false, guard text is not read), loops run up to `loop_cap`, mock call order reflects the flow — numeric arithmetic on values is NOT modeled. Entry is resolved by NAME in each index (symbol ids differ between ref and working tree). Requires a git repo. Read-only — the index is never mutated.", + json!({ "type": "object", "properties": { + "entry": { "type": "string", "description": "Entry function name (substring → first function match in each index)." }, + "ref": { "type": "string", "description": "Git ref for the ORIGIN state (default HEAD). Example: origin/main." }, + "args": { "type": "array", "items": { "type": "integer" }, "description": "Abstract i64 arguments passed to the entry function." }, + "mocks": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Callee name → Rhai mock body/fn." }, + "branch_policy": { "type": "string", "enum": ["if_true", "if_false"], "description": "Override config branch_policy." }, + "loop_cap": { "type": "integer", "description": "Override config loop_cap." } + }, "required": ["entry"] }), + ), ] } @@ -276,7 +340,9 @@ pub async fn dispatch_with_api(api: &GraphApi, name: &str, args: Value) -> Resul .unwrap_or(SymbolMatch::Contains); let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as u32; let offset = args.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as u32; - let (results, total) = api.search_symbol_paged(q, kind, mode, limit, offset).await?; + let (results, total) = api + .search_symbol_paged(q, kind, mode, limit, offset) + .await?; serde_json::to_string_pretty(&json!({ "results": results, "total": total, @@ -346,16 +412,14 @@ pub async fn dispatch_with_api(api: &GraphApi, name: &str, args: Value) -> Resul .await?; match target { Target::Ambiguous(v) => Ok(json_str(v)), - Target::Symbol(sym) => { - match api.class_info(sym.id).await { - Some(info) => serde_json::to_string_pretty(&info) - .map_err(|e| Error::Invalid(e.to_string())), - None => Err(Error::Invalid(format!( - "symbol {:?} (id {}) is not a class/interface/enum", - sym.name, sym.id - ))), - } - } + Target::Symbol(sym) => match api.class_info(sym.id).await { + Some(info) => serde_json::to_string_pretty(&info) + .map_err(|e| Error::Invalid(e.to_string())), + None => Err(Error::Invalid(format!( + "symbol {:?} (id {}) is not a class/interface/enum", + sym.name, sym.id + ))), + }, } } "codegraph_list_classes" => { @@ -409,8 +473,9 @@ pub async fn dispatch_with_api(api: &GraphApi, name: &str, args: Value) -> Resul .and_then(SymbolKind::parse); let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as u32; let offset = args.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as u32; - let (results, total, truncated) = - api.search_by_annotation(annotation, kind, offset, limit).await; + let (results, total, truncated) = api + .search_by_annotation(annotation, kind, offset, limit) + .await; serde_json::to_string_pretty(&json!({ "annotation": annotation, "kind": kind.map(|k| k.as_str()), @@ -530,3 +595,441 @@ fn arg_u64(v: &Value, k: &str) -> Result { .and_then(|x| x.as_u64()) .ok_or_else(|| Error::Invalid(format!("missing int arg: {k}"))) } + +// ── Admin tools (codegraph_init / codegraph_index) ── +// Cần workspace root (không qua GraphApi) — server lưu `root` và gọi hàm này. + +pub async fn dispatch_admin(root: &Utf8Path, name: &str, args: Value) -> Result { + match name { + "codegraph_init" => { + let dir = init_project(root)?; + let do_index = args.get("index").and_then(|v| v.as_bool()).unwrap_or(true); + let mut out = json!({ "initialized": dir.as_str() }); + if do_index { + match run_index(root).await { + Ok(stats) => { + out["indexed"] = stats_json(&stats); + } + Err(e) => { + return Err(Error::Invalid(format!( + "initialized {}, but indexing failed: {e}", + dir + ))); + } + } + } + serde_json::to_string_pretty(&out).map_err(|e| Error::Invalid(e.to_string())) + } + "codegraph_index" => { + if !project_dir(root).exists() { + return Err(Error::Invalid( + "workspace not initialized: missing .codegraph/. Run codegraph_init first." + .into(), + )); + } + let stats = run_index(root).await?; + serde_json::to_string_pretty(&stats_json(&stats)) + .map_err(|e| Error::Invalid(e.to_string())) + } + _ => Err(Error::Invalid(format!("unknown admin tool: {name}"))), + } +} + +/// Full re-index: mở sqlite → `Orchestrator::index_all` (ingest = full re-index). +/// Không progress bar — MCP transport là stdout, tránh nhiễu JSON-RPC. +async fn run_index(root: &Utf8Path) -> Result { + let db_str = project_db_path(root).as_str().to_string(); + let mut idx = GraphIndex::open(&db_str).await?; + Orchestrator::with_registry() + .index_all(root, &mut idx, None) + .await +} + +fn stats_json(s: &ExtractStats) -> Value { + json!({ + "files": s.files, + "symbols": s.symbols, + "chains": s.chains, + "calls": s.calls, + "skipped": s.skipped, + }) +} + +// ── Sandbox tool (codegraph_sandbox) ── +// Cần workspace root (config.toml `[sandbox]` + mock dirs) và snapshot index, +// nên dispatch riêng qua `SharedGraphIndex` — không qua `GraphApi`. + +/// Chạy sandbox trên flow của entry function. +/// +/// `node` (symbol id) hoặc `name` (substring → function match đầu tiên) chọn +/// entry; group = entry + mọi callee trong flow resolve được. `mocks` là map +/// callee → Rhai source (body được wrap tự động thành `fn (args)`), override +/// file mock cùng tên — mocks thiếu được ghi vào `missing_mocks`. +/// Parse các run-options dùng chung giữa `codegraph_sandbox`, +/// `codegraph_diff_simulate`, `codegraph_origin_simulate`: `args` (i64 array), +/// `mocks` (callee → rhai source), `branch_policy`, `loop_cap`. +type SandboxRunOptions = (Vec, Vec<(String, String)>, SboxConfig); +fn parse_run_options(root: &Utf8Path, args: &Value) -> Result { + let mut call_args = Vec::new(); + if let Some(arr) = args.get("args").and_then(|v| v.as_array()) { + for v in arr { + call_args.push( + v.as_i64() + .ok_or_else(|| Error::Invalid("args must be integers".into()))?, + ); + } + } + let mut mocks = Vec::new(); + if let Some(obj) = args.get("mocks").and_then(|v| v.as_object()) { + for (name, src) in obj { + let src = src + .as_str() + .ok_or_else(|| Error::Invalid(format!("mock `{name}` must be a rhai string")))?; + mocks.push((name.clone(), src.to_string())); + } + } + let mut config = SboxConfig::load(root).unwrap_or_default(); + if let Some(p) = args.get("branch_policy").and_then(|v| v.as_str()) { + config.branch_policy = match p { + "if_true" => BranchPolicy::IfTrue, + "if_false" => BranchPolicy::IfFalse, + other => { + return Err(Error::Invalid(format!( + "bad branch_policy `{other}` (expected if_true/if_false)" + ))); + } + }; + } + if let Some(c) = args.get("loop_cap").and_then(|v| v.as_u64()) { + config.loop_cap = c as usize; + } + Ok((call_args, mocks, config)) +} + +/// So sánh trace sequence giữa hai kết quả `run_sim` (origin/before vs +/// working_tree/after): liệt kê mock-call/cond-decision nào chỉ xuất hiện một +/// bên. `present:false` / `link_error` → sequence rỗng, delta vẫn có ý nghĩa. +fn sequence_delta(before: &Value, after: &Value) -> Value { + let seq = |v: &Value| -> Vec { + v.get("sequence") + .and_then(|x| x.as_array()) + .map(|a| { + a.iter() + .filter_map(|x| x.as_str().map(str::to_string)) + .collect() + }) + .unwrap_or_default() + }; + let sb = seq(before); + let sa = seq(after); + json!({ + "sequence_added": sa.iter().filter(|s| !sb.contains(s)).cloned().collect::>(), + "sequence_removed": sb.iter().filter(|s| !sa.contains(s)).cloned().collect::>(), + }) +} + +pub async fn dispatch_sandbox( + root: &Utf8Path, + shared: Arc, + args: Value, +) -> Result { + let idx = shared.ensure_fresh().await; + + // Entry: `node` id, hoặc `name` (substring, function match đầu tiên). + let entry_id = if let Some(id) = args.get("node").and_then(|v| v.as_u64()) { + id + } else { + let q = arg_str(&args, "name")?; + let hits = idx.search_symbol(q, Some(SymbolKind::Function), 1).await?; + hits.first() + .map(|s| s.id) + .ok_or_else(|| Error::Invalid(format!("no function matching `{q}`")))? + }; + + // Group: entry + mọi callee trong flow là symbol biết tên (compile thành + // machine code); callee không resolve → mock dispatch. Giống cmd_sandbox CLI. + let flow = idx.flow(entry_id).await?; + let mut ids = vec![entry_id]; + let mut seen = std::collections::HashSet::from([entry_id]); + for &e in &flow.chain { + if is_marker(e) { + continue; + } + if e != entry_id && idx.symbol_by_id(e).is_some() && seen.insert(e) { + ids.push(e); + } + } + ids.sort_unstable(); + + let (call_args, mocks, config) = parse_run_options(root, &args)?; + + let mut module = compile_with_mocks(&idx, &ids, &config, &mocks).await?; + let (ret, trace) = module.run(&call_args); + + let group_names: Vec = ids + .iter() + .filter_map(|id| idx.symbol_by_id(*id).map(|s| s.name)) + .collect(); + serde_json::to_string_pretty(&json!({ + "entry": flow.symbol.name, + "entry_id": entry_id, + "group": group_names, + "args": call_args, + "return": ret, + "mocks": trace.mocks, + "conds": trace.conds, + "missing_mocks": trace.missing, + "sequence": trace.sequence(), + })) + .map_err(|e| Error::Invalid(e.to_string())) +} + +/// Phân tích unified diff (MR / patch / `git diff`) thành bản DRAFT tác động +/// lên graph. Read-only: parse diff, đối chiếu dòng bên new với symbol + call-site +/// trong index, trả report JSON — không mutate index. +pub async fn dispatch_diff( + root: &Utf8Path, + shared: Arc, + args: Value, +) -> Result { + let diff = arg_str(&args, "diff")?; + let parsed = codegraph_graph::diff::parse_unified_diff(diff) + .map_err(|e| Error::Invalid(e.to_string()))?; + + let idx = shared.ensure_fresh().await; + let report = idx.diff_assess(&parsed, Some(root.as_std_path())).await; + serde_json::to_string_pretty(&report).map_err(|e| Error::Invalid(e.to_string())) +} + +/// Chạy sandbox trên flow của `entry_name` trong một index cụ thể. Trả JSON +/// outcome: `present:false` nếu index không có hàm đó, `link_error` nếu thiếu +/// mock (compile dừng trước khi chạy). Reuse giữa before-index và after-index. +async fn run_sim( + idx: &GraphIndex, + entry_name: &str, + call_args: &[i64], + config: &SboxConfig, + mocks: &[(String, String)], +) -> Result { + let Some(sym) = idx + .search_symbol(entry_name, Some(SymbolKind::Function), 1) + .await? + .into_iter() + .next() + else { + return Ok(json!({ "present": false })); + }; + + let mut ids = vec![sym.id]; + let mut seen = std::collections::HashSet::from([sym.id]); + if let Ok(flow) = idx.flow(sym.id).await { + for &e in &flow.chain { + if is_marker(e) { + continue; + } + if e != sym.id && idx.symbol_by_id(e).is_some() && seen.insert(e) { + ids.push(e); + } + } + } + ids.sort_unstable(); + + let mut module = match compile_with_mocks(idx, &ids, config, mocks).await { + Ok(m) => m, + Err(e) => return Ok(json!({ "present": true, "link_error": e.to_string() })), + }; + let (ret, trace) = module.run(call_args); + Ok(json!({ + "present": true, + "group": ids + .iter() + .filter_map(|id| idx.symbol_by_id(*id).map(|s| s.name.clone())) + .collect::>(), + "return": ret, + "sequence": trace.sequence(), + "missing_mocks": trace.missing, + })) +} + +/// Build index của cây git tại `base_ref` (`git archive` → temp dir → +/// parse+ingest vào `GraphIndex::in_memory`). Luôn trả kèm tmp dir để caller +/// dọn dẹp, kể cả khi thất bại (trả `None` + `note` lý do). +async fn build_before_index( + root: &Utf8Path, + base_ref: &str, +) -> Result<(Option, Utf8PathBuf, String)> { + let millis = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + let tmp = Utf8PathBuf::from_path_buf( + std::env::temp_dir().join(format!("codegraph-sim-{}-{millis}", std::process::id())), + ) + .map_err(|p| Error::Invalid(format!("temp path not UTF-8: {p:?}")))?; + let tree = tmp.join("tree"); + let tar = tmp.join("tree.tar"); + if let Err(e) = std::fs::create_dir_all(&tree) { + return Ok((None, tmp, format!("temp dir failed: {e}"))); + } + + let st = match std::process::Command::new("git") + .args(["archive", "--format=tar"]) + .arg(base_ref) + .arg("-o") + .arg(&tar) + .current_dir(root.as_std_path()) + .status() + { + Ok(s) => s, + Err(e) => return Ok((None, tmp, format!("git unavailable: {e}"))), + }; + if !st.success() { + return Ok((None, tmp, format!("git archive `{base_ref}` failed"))); + } + let ok = std::process::Command::new("tar") + .arg("-xf") + .arg(&tar) + .arg("-C") + .arg(&tree) + .status() + .map(|s| s.success()) + .unwrap_or(false); + if !ok { + return Ok((None, tmp, "tar extract failed".into())); + } + + let mut before = GraphIndex::in_memory(); + match Orchestrator::with_registry() + .index_all(&tree, &mut before, None) + .await + { + Ok(_) => Ok((Some(before), tmp, String::new())), + Err(e) => Ok((None, tmp, format!("before-index failed: {e}"))), + } +} + +/// Diff → simulate: chạy sandbox trên flow entry cho cả bản "trước" (git +/// archive tại `base_ref`) và bản "sau" (index hiện tại = post-MR), so sánh +/// trace. Read-only — không mutate index. +pub async fn dispatch_diff_simulate( + root: &Utf8Path, + shared: Arc, + args: Value, +) -> Result { + let diff = arg_str(&args, "diff")?; + let parsed = codegraph_graph::diff::parse_unified_diff(diff) + .map_err(|e| Error::Invalid(e.to_string()))?; + let base_ref = args + .get("base_ref") + .and_then(|v| v.as_str()) + .unwrap_or("HEAD") + .to_string(); + + let (call_args, mocks, config) = parse_run_options(root, &args)?; + + let idx = shared.ensure_fresh().await; + let report = idx.diff_assess(&parsed, Some(root.as_std_path())).await; + + // Hàm bị diff chạm: ưu tiên flow (call-site trên dòng đổi), kèm symbol + // Function/Method. Dedupe, giữ thứ tự. + let mut affected: Vec = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for f in &report.files { + for fl in &f.flows { + if seen.insert(fl.name.clone()) { + affected.push(fl.name.clone()); + } + } + for s in &f.symbols { + if matches!(s.symbol.kind, SymbolKind::Function | SymbolKind::Method) + && seen.insert(s.symbol.name.clone()) + { + affected.push(s.symbol.name.clone()); + } + } + } + + let entry = match args.get("entry").and_then(|v| v.as_str()) { + Some(e) => e.to_string(), + None => affected.first().cloned().ok_or_else(|| { + Error::Invalid("no function affected by the diff — pass `entry`".into()) + })?, + }; + + // Build index "trước" + tmp dir (caller dọn tmp kể cả khi thất bại). + let (before_idx, tmp, build_note) = build_before_index(root, &base_ref).await?; + + let result = async { + let before = match &before_idx { + Some(b) => run_sim(b, &entry, &call_args, &config, &mocks).await?, + None => json!({ "present": false, "reason": build_note }), + }; + let after = run_sim(&idx, &entry, &call_args, &config, &mocks).await?; + + let delta = sequence_delta(&before, &after); + Ok::(json!({ + "draft": true, + "tool": "codegraph_diff_simulate", + "entry": entry, + "args": call_args, + "base_ref": base_ref, + "affected_functions": affected, + "before_index_note": build_note, + "before": before, + "after": after, + "delta": delta, + "note": "Read-only: before = index tạm từ `git archive {base_ref}`, after = index hiện tại (post-MR). Không mutate index.", + })) + } + .await; + + let _ = std::fs::remove_dir_all(&tmp); + let payload = result?; + serde_json::to_string_pretty(&payload).map_err(|e| Error::Invalid(e.to_string())) +} + +/// Ref → simulate: chạy sandbox trên flow entry trên cây git tại `ref` (index +/// tạm từ `git archive`) VÀ trên index hiện tại (working tree), so sánh trace +/// trước/sau — không cần diff, entry chọn tự do. Read-only — không mutate index. +pub async fn dispatch_origin_simulate( + root: &Utf8Path, + shared: Arc, + args: Value, +) -> Result { + let entry = arg_str(&args, "entry")?; + let git_ref = args + .get("ref") + .and_then(|v| v.as_str()) + .unwrap_or("HEAD") + .to_string(); + let (call_args, mocks, config) = parse_run_options(root, &args)?; + + let idx = shared.ensure_fresh().await; + let (origin_idx, tmp, build_note) = build_before_index(root, &git_ref).await?; + + let result = async { + let origin = match &origin_idx { + Some(o) => run_sim(o, entry, &call_args, &config, &mocks).await?, + None => json!({ "present": false, "reason": build_note }), + }; + let working_tree = run_sim(&idx, entry, &call_args, &config, &mocks).await?; + let delta = sequence_delta(&origin, &working_tree); + Ok::(json!({ + "draft": true, + "tool": "codegraph_origin_simulate", + "entry": entry, + "args": call_args, + "ref": git_ref, + "origin_index_note": build_note, + "origin": origin, + "working_tree": working_tree, + "delta": delta, + "note": "Read-only: origin = index tạm từ `git archive {git_ref}`, working_tree = index hiện tại. Không mutate index.", + })) + } + .await; + + let _ = std::fs::remove_dir_all(&tmp); + let payload = result?; + serde_json::to_string_pretty(&payload).map_err(|e| Error::Invalid(e.to_string())) +} diff --git a/crates/codegraph-sboxes/Cargo.toml b/crates/codegraph-sboxes/Cargo.toml new file mode 100644 index 000000000..1d4da35da --- /dev/null +++ b/crates/codegraph-sboxes/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "codegraph-sboxes" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +codegraph-core = { path = "../codegraph-core" } +codegraph-graph = { path = "../codegraph-graph" } +serde = { workspace = true } +serde_json = { workspace = true } +toml = "0.8" +cranelift-codegen = { version = "0.116" } +cranelift-frontend = { version = "0.116" } +cranelift-module = { version = "0.116" } +cranelift-jit = { version = "0.116" } +cranelift-native = { version = "0.116" } +target-lexicon = "0.13" +rhai = { version = "1", features = ["sync"] } +camino = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true } + +[features] +default = [] diff --git a/crates/codegraph-sboxes/src/abi.rs b/crates/codegraph-sboxes/src/abi.rs new file mode 100644 index 000000000..793d7d30c --- /dev/null +++ b/crates/codegraph-sboxes/src/abi.rs @@ -0,0 +1,69 @@ +//! The sandbox ABI: every value is an `i64`, and every compiled function and +//! runtime trampoline is `extern "C"` so the JIT can call in and out cleanly. +//! +//! ```text +//! host fn: (ctx, nargs, args, ret) -> i64 +//! mock_dispatch: (ctx, callee_idx, nargs, args, ret) -> i64 +//! eval_condition: (ctx, cond_idx, rec_depth) -> i64 +//! ``` +//! +//! - `ctx` — opaque pointer to the [`crate::runtime::RunContext`] (per-run state). +//! - `args` — pointer to `nargs` i64 slots (abstract values, `i` for arg i). +//! - `ret` — pointer to a single i64 slot (the function's return value). + +use cranelift_codegen::ir::{types, AbiParam, Signature}; +use cranelift_codegen::isa::CallConv; + +fn i64() -> AbiParam { + AbiParam::new(types::I64) +} + +/// Native calling convention for the host triple. +fn call_conv() -> CallConv { + CallConv::triple_default(&target_lexicon::Triple::host()) +} + +/// Signature of a compiled host function: +/// `(ctx, nargs, args, ret) -> i64`. +pub fn host_signature() -> Signature { + let mut sig = Signature::new(call_conv()); + sig.params.push(i64()); // ctx + sig.params.push(i64()); // nargs + sig.params.push(i64()); // args + sig.params.push(i64()); // ret + sig.returns.push(i64()); + sig +} + +/// Signature of the `mock_dispatch` import: +/// `(ctx, callee_idx, nargs, args, ret) -> i64`. +pub fn mock_signature() -> Signature { + let mut sig = Signature::new(call_conv()); + sig.params.push(i64()); // ctx + sig.params.push(i64()); // callee_idx + sig.params.push(i64()); // nargs + sig.params.push(i64()); // args + sig.params.push(i64()); // ret + sig.returns.push(i64()); + sig +} + +/// Signature of the `eval_condition` import: +/// `(ctx, cond_idx, rec_depth) -> i64`. +pub fn cond_signature() -> Signature { + let mut sig = Signature::new(call_conv()); + sig.params.push(i64()); // ctx + sig.params.push(i64()); // cond_idx + sig.params.push(i64()); // rec_depth + sig.returns.push(i64()); + sig +} + +/// Index of the `ctx` parameter in a host signature. +pub const PARAM_CTX: usize = 0; +/// Index of the `nargs` parameter in a host signature. +pub const PARAM_NARGS: usize = 1; +/// Index of the `args` pointer parameter in a host signature. +pub const PARAM_ARGS: usize = 2; +/// Index of the `ret` pointer parameter in a host signature. +pub const PARAM_RET: usize = 3; diff --git a/crates/codegraph-sboxes/src/codegen.rs b/crates/codegraph-sboxes/src/codegen.rs new file mode 100644 index 000000000..adb4da2ef --- /dev/null +++ b/crates/codegraph-sboxes/src/codegen.rs @@ -0,0 +1,750 @@ +//! Chain → Cranelift structured-CFG lowering. +//! +//! Each group function's `FlowResult.chain` is a linear mix of markers (control +//! flow) and callee ids. This module lowers it into a real machine function: +//! +//! | chain marker | lowered to | +//! |-----------------------------|----------------------------------------------| +//! | `IF_TRUE`/`IF_FALSE`/`BRANCH_END` | `eval_condition` + `brif` + structured merge | +//! | `LOOP`/`LOOP_BACK` | header condition + back edge (capped) | +//! | `SWITCH_CASE`/`SWITCH_END` | guarded case blocks, first-case policy | +//! | `RETURN` | store result + jump to epilogue | +//! | `BREAK`/`CONTINUE`/`THROW` | jumps to innermost exit / header / epilogue | +//! | callee id (in group) | real call to the sibling compiled function | +//! | callee id (outside/unresolved) | `mock_dispatch` (Rhai mock) | +//! +//! Simplifications (documented, Piece-1 scope): condition side-effect calls +//! emitted right after `IF_TRUE`/`LOOP` run as the head of the taken branch / +//! loop body; recursion (`callee == self`) is mocked like any external callee so +//! runs always terminate. + +use crate::abi; +use crate::group::{group_ids, GroupFunc}; +use crate::runtime::{create_jit_module, SandboxModule}; +use codegraph_core::{ + is_marker, Error, FlowResult, Result, SymbolId, MARKER_BRANCH_END, MARKER_BREAK, + MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, + MARKER_REC_CALL, MARKER_RETURN, MARKER_SWITCH_CASE, MARKER_SWITCH_END, MARKER_THROW, +}; +use cranelift_codegen::ir::{types, Block, FuncRef, InstBuilder, MemFlags, Value}; +use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; +use cranelift_module::{Linkage, Module}; +use std::collections::{HashMap, HashSet}; + +use crate::trace::CondKind; + +/// One preprocessed chain element. +struct Item { + tag: ItemTag, + /// Chain position (diagnostics / arg lookup). + #[allow(dead_code, reason = "kept for diagnostics on later pieces")] + pos: usize, + /// For `RETURN`: how many following Call items belong to the return expr. + follow: usize, +} + +enum ItemTag { + Marker(u64), + /// Call to a sibling compiled function. + GroupCall { + callee: SymbolId, + }, + /// Call dispatched to a Rhai mock. + MockCall { + name: String, + args: usize, + }, +} + +/// Control-flow frame stack (matched against the marker nesting). +enum Frame { + If { + else_b: Block, + merge_b: Block, + seen_else: bool, + }, + Loop { + header: Block, + exit: Block, + }, + Switch { + exit: Block, + pending_next: Option, + /// cond_idx of the first case. All cases of one statement share it so + /// the runtime "first case taken" policy applies per statement. + key: u64, + }, +} + +/// Per-function lowering state. +/// +/// All callee/import references are pre-imported into the function's IR before +/// the builder is created (the new `Module` API borrows the `Function` mutably), +/// so the walker only needs the pre-resolved `FuncRef`s. +struct Lower<'a> { + fb: FunctionBuilder<'a>, + /// In-group callee id → already-imported `FuncRef`. + callee_refs: &'a HashMap, + mock_ref: FuncRef, + cond_ref: FuncRef, + name_table: &'a mut Vec, + name_idx: &'a mut HashMap, + cond_table: &'a mut Vec, + cond_counter: &'a mut u64, + ctx_val: Value, + /// Function-signature parameter, bound by the ABI. Not read in the body + /// (args are consumed via the arena pointer) but part of the contract. + #[allow(dead_code, reason = "ABI parameter; bound for signature completeness")] + nargs_val: Value, + args_val: Value, + ret_val: Value, + /// The function's "last expression result", tracked as a frontend variable + /// so the frontend inserts phis wherever control flow merges (an epilogue + /// store/return must work no matter which branch produced the value). + last: Variable, + epilogue: Block, + current: Block, + terminated: bool, + all_blocks: Vec, + terminated_blocks: HashSet, + frames: Vec, + break_targets: Vec, + continue_targets: Vec, + items: Vec, +} + +impl<'a> Lower<'a> { + #[allow(clippy::too_many_arguments)] + fn new( + mut fb: FunctionBuilder<'a>, + callee_refs: &'a HashMap, + mock_ref: FuncRef, + cond_ref: FuncRef, + name_table: &'a mut Vec, + name_idx: &'a mut HashMap, + cond_table: &'a mut Vec, + cond_counter: &'a mut u64, + items: Vec, + ) -> Self { + let entry = fb.create_block(); + fb.switch_to_block(entry); + fb.append_block_params_for_function_params(entry); + let params = fb.block_params(entry); + let ctx_val = params[abi::PARAM_CTX]; + let nargs_val = params[abi::PARAM_NARGS]; + let args_val = params[abi::PARAM_ARGS]; + let ret_val = params[abi::PARAM_RET]; + let epilogue = fb.create_block(); + let zero = fb.ins().iconst(types::I64, 0); + let last = Variable::from_u32(0); + fb.declare_var(last, types::I64); + fb.def_var(last, zero); + Self { + fb, + callee_refs, + mock_ref, + cond_ref, + name_table, + name_idx, + cond_table, + cond_counter, + ctx_val, + nargs_val, + args_val, + ret_val, + last, + epilogue, + current: entry, + terminated: false, + all_blocks: vec![entry, epilogue], + terminated_blocks: HashSet::new(), + frames: Vec::new(), + break_targets: Vec::new(), + continue_targets: Vec::new(), + items, + } + } + + fn build(&mut self) -> Result<()> { + let items = std::mem::take(&mut self.items); + let mut i = 0usize; + while i < items.len() { + let is_case = matches!(items[i].tag, ItemTag::Marker(m) if m == MARKER_SWITCH_CASE); + self.maybe_close_switch(is_case); + match &items[i].tag { + ItemTag::Marker(m) => match *m { + MARKER_IF_TRUE => self.emit_if_true(), + MARKER_IF_FALSE => self.emit_if_false(), + MARKER_BRANCH_END => self.emit_branch_end(), + MARKER_LOOP => self.emit_loop(), + MARKER_LOOP_BACK => self.emit_loop_back(), + MARKER_SWITCH_CASE => self.emit_switch_case(), + MARKER_SWITCH_END => self.emit_switch_end(), + MARKER_BREAK => self.emit_break(), + MARKER_CONTINUE => self.emit_continue(), + MARKER_THROW => { + let n = items[i].follow; + i += 1; + for _ in 0..n { + if i < items.len() { + self.emit_call(&items[i]); + i += 1; + } + } + self.emit_throw(); + continue; + } + MARKER_RETURN => { + let n = items[i].follow; + i += 1; + for _ in 0..n { + if i < items.len() { + self.emit_call(&items[i]); + i += 1; + } + } + self.emit_return_tail(); + continue; + } + MARKER_REC_CALL => { /* recursion is mocked; nothing to do */ } + _ => {} + }, + ItemTag::GroupCall { .. } | ItemTag::MockCall { .. } => self.emit_call(&items[i]), + } + i += 1; + } + self.maybe_close_switch(false); + self.finish(); + Ok(()) + } + + // ---- helpers ---- + + fn iconst(&mut self, v: i64) -> Value { + self.fb.ins().iconst(types::I64, v) + } + + fn jump(&mut self, target: Block) { + self.fb.ins().jump(target, &[]); + self.terminated_blocks.insert(self.current); + self.terminated = true; + } + + fn begin_block(&mut self, b: Block) { + self.fb.switch_to_block(b); + self.current = b; + self.terminated = false; + } + + fn ensure_alive(&mut self) { + if self.terminated { + let b = self.fb.create_block(); + self.all_blocks.push(b); + self.begin_block(b); + } + } + + fn jump_epilogue(&mut self) { + self.jump(self.epilogue); + } + + fn eval_condition(&mut self, kind: CondKind) -> (u64, Value) { + let idx = *self.cond_counter; + *self.cond_counter += 1; + self.cond_table.push(kind); + let idx_v = self.iconst(idx as i64); + let depth_v = self.iconst(0); + let inst = self + .fb + .ins() + .call(self.cond_ref, &[self.ctx_val, idx_v, depth_v]); + (idx, self.fb.inst_results(inst)[0]) + } + + /// Evaluate a condition with a *specific* cond_idx. Used by subsequent + /// switch cases so they share the first case's key (and thus the runtime + /// decides them per statement, not per case). + fn eval_condition_at(&mut self, idx: u64) -> Value { + let idx_v = self.iconst(idx as i64); + let depth_v = self.iconst(0); + let inst = self + .fb + .ins() + .call(self.cond_ref, &[self.ctx_val, idx_v, depth_v]); + self.fb.inst_results(inst)[0] + } + + fn name_idx(&mut self, name: &str) -> u64 { + if let Some(&i) = self.name_idx.get(name) { + return i; + } + let i = self.name_table.len() as u64; + self.name_table.push(name.to_string()); + self.name_idx.insert(name.to_string(), i); + i + } + + // ---- control flow ---- + + fn emit_if_true(&mut self) { + self.ensure_alive(); + let then_b = self.fb.create_block(); + let else_b = self.fb.create_block(); + let merge_b = self.fb.create_block(); + self.all_blocks.extend([then_b, else_b, merge_b]); + let (_idx, c) = self.eval_condition(CondKind::If); + self.fb.ins().brif(c, then_b, &[], else_b, &[]); + self.terminated_blocks.insert(self.current); + self.terminated = true; + self.frames.push(Frame::If { + else_b, + merge_b, + seen_else: false, + }); + self.begin_block(then_b); + } + + fn emit_if_false(&mut self) { + // Copy the blocks out first so we don't hold a borrow on `self.frames` + // while also mutating `self` (jump/begin_block). + let pending = match self.frames.last() { + Some(Frame::If { + else_b, + merge_b, + seen_else: false, + }) => Some((*else_b, *merge_b)), + _ => None, + }; + if let Some((else_b, merge_b)) = pending { + if let Some(Frame::If { seen_else, .. }) = self.frames.last_mut() { + *seen_else = true; + } + if !self.terminated { + self.jump(merge_b); + } + self.begin_block(else_b); + } + } + + fn emit_branch_end(&mut self) { + if let Some(Frame::If { + else_b, + merge_b, + seen_else, + }) = self.frames.pop() + { + if !self.terminated { + self.jump(merge_b); + } + if seen_else { + self.begin_block(merge_b); + } else { + self.begin_block(else_b); + self.jump(merge_b); + self.begin_block(merge_b); + } + } + } + + fn emit_loop(&mut self) { + self.ensure_alive(); + let header = self.fb.create_block(); + let body = self.fb.create_block(); + let exit = self.fb.create_block(); + self.all_blocks.extend([header, body, exit]); + self.jump(header); + self.begin_block(header); + let (_idx, c) = self.eval_condition(CondKind::Loop); + self.fb.ins().brif(c, body, &[], exit, &[]); + self.terminated_blocks.insert(self.current); + self.terminated = true; + self.frames.push(Frame::Loop { header, exit }); + self.break_targets.push(exit); + self.continue_targets.push(header); + self.begin_block(body); + } + + fn emit_loop_back(&mut self) { + if let Some(Frame::Loop { header, exit }) = self.frames.pop() { + self.break_targets.pop(); + self.continue_targets.pop(); + if !self.terminated { + self.jump(header); + } + self.begin_block(exit); + } + } + + fn emit_switch_case(&mut self) { + self.ensure_alive(); + let has_open_switch = matches!(self.frames.last(), Some(Frame::Switch { .. })); + if has_open_switch { + // Subsequent case: dispatch from the transition block left by the + // previous `SWITCH_END` (pending_next was `None` until now). + let case_b = self.fb.create_block(); + let next_b = self.fb.create_block(); + self.all_blocks.extend([case_b, next_b]); + let key = match self.frames.last() { + Some(Frame::Switch { key, .. }) => *key, + _ => unreachable!(), + }; + let c = self.eval_condition_at(key); + self.fb.ins().brif(c, case_b, &[], next_b, &[]); + self.terminated_blocks.insert(self.current); + self.terminated = true; + if let Some(Frame::Switch { pending_next, .. }) = self.frames.last_mut() { + *pending_next = Some(next_b); + } + self.begin_block(case_b); + } else { + // First case — create the switch frame. + let exit = self.fb.create_block(); + let case_b = self.fb.create_block(); + let next_b = self.fb.create_block(); + self.all_blocks.extend([exit, case_b, next_b]); + let (key, c) = self.eval_condition(CondKind::Switch); + self.fb.ins().brif(c, case_b, &[], next_b, &[]); + self.terminated_blocks.insert(self.current); + self.terminated = true; + self.frames.push(Frame::Switch { + exit, + pending_next: Some(next_b), + key, + }); + self.break_targets.push(exit); + self.begin_block(case_b); + } + } + + fn emit_switch_end(&mut self) { + if let Some(Frame::Switch { pending_next, .. }) = self.frames.last_mut() { + if let Some(next_b) = pending_next.take() { + if !self.terminated { + self.jump(next_b); + } + self.begin_block(next_b); + } + } + } + + fn emit_break(&mut self) { + self.ensure_alive(); + if let Some(&target) = self.break_targets.last() { + self.jump(target); + } + } + + fn emit_continue(&mut self) { + self.ensure_alive(); + if let Some(&target) = self.continue_targets.last() { + self.jump(target); + } + } + + fn emit_return_tail(&mut self) { + self.ensure_alive(); + self.jump_epilogue(); + } + + fn emit_throw(&mut self) { + self.ensure_alive(); + let minus_one = self.iconst(-1); + self.fb.def_var(self.last, minus_one); + self.jump_epilogue(); + } + + fn emit_call(&mut self, item: &Item) { + self.ensure_alive(); + let n = match &item.tag { + ItemTag::GroupCall { .. } => 0, + ItemTag::MockCall { args, .. } => *args, + ItemTag::Marker(_) => return, + }; + // The callee receives `nargs` abstract args from the shared arena. + // The arena is preloaded by the runtime with the *entry* args, so a + // callee called from the entry actually sees the caller's values; the + // abstract-value model collapses any deeper expressions, but the slots + // are deterministic (i = arg i). Pass the arena pointer as-is. + let nargs_c = self.iconst(n as i64); + let inst = match &item.tag { + ItemTag::GroupCall { callee } => { + let fid = *callee; + let fref = self.callee_refs[&fid]; + self.fb + .ins() + .call(fref, &[self.ctx_val, nargs_c, self.args_val, self.ret_val]) + } + ItemTag::MockCall { name, .. } => { + let idx = self.name_idx(name); + let idx_c = self.iconst(idx as i64); + self.fb.ins().call( + self.mock_ref, + &[self.ctx_val, idx_c, nargs_c, self.args_val, self.ret_val], + ) + } + ItemTag::Marker(_) => unreachable!(), + }; + let result = self.fb.inst_results(inst)[0]; + self.fb.def_var(self.last, result); + } + + fn maybe_close_switch(&mut self, next_is_case: bool) { + if next_is_case { + return; + } + while let Some(Frame::Switch { + exit, + pending_next: None, + .. + }) = self.frames.last() + { + let exit = *exit; + self.frames.pop(); + self.break_targets.pop(); + let cont = self.current; + self.begin_block(exit); + self.jump(cont); + self.begin_block(cont); + } + } + + fn finish(&mut self) { + if !self.terminated { + self.jump_epilogue(); + } + // Epilogue: store `last` to *ret and return it. `use_var` pulls the + // value through the phis the frontend inserted at merge points. + self.begin_block(self.epilogue); + let last = self.fb.use_var(self.last); + self.fb.ins().store(MemFlags::new(), last, self.ret_val, 0); + self.fb.ins().return_(&[last]); + self.terminated_blocks.insert(self.epilogue); + self.terminated = true; + // Terminate any block left dangling (dead switch exit, empty branches…). + for &b in &self.all_blocks.clone() { + if !self.terminated_blocks.contains(&b) { + self.begin_block(b); + self.jump_epilogue(); + } + } + } +} + +/// Render a `ModuleError` to a string, expanding verifier errors so the real +/// cause (not just "Verifier errors") surfaces in diagnostics. +fn describe_module_error(e: &cranelift_module::ModuleError) -> String { + use cranelift_module::ModuleError; + match e { + ModuleError::Compilation(cranelift_codegen::CodegenError::Verifier(errs)) => { + let detail: Vec = errs.0.iter().map(|e| e.to_string()).collect(); + format!("Compilation error (verifier): {}", detail.join("; ")) + } + ModuleError::Compilation(other) => format!("Compilation error: {other}"), + other => other.to_string(), + } +} + +/// Compile a group of functions into a runnable sandbox module. +/// +/// `inline_mocks` (name → rhai source) are registered per-call, overriding file +/// mocks of the same name — used so a caller can mock specific functions. +pub fn compile_group( + group: &[GroupFunc], + config: &crate::config::SboxConfig, + inline_mocks: &[(String, String)], +) -> Result { + let mut module = create_jit_module()?; + let ids = group_ids(group); + let merr = |e: cranelift_module::ModuleError| Error::Other(describe_module_error(&e)); + + // Pass 0 — link-time mock validation: load the mock library (file + inline) + // up front and fail BEFORE generating any code if a callee that will be + // mock-dispatched has no mock configured. The caller gets the exact list of + // functions to mock instead of silently running a `0` fallback. + let mocks = crate::rhai::RhaiMockLib::load_with_mocks( + config.root.as_std_path(), + &config.mock_dirs, + inline_mocks, + ); + let mut missing = Vec::new(); + let mut seen_names = HashSet::new(); + for f in group { + for it in build_items(f, &ids) { + if let ItemTag::MockCall { name, .. } = &it.tag { + if seen_names.insert(name.clone()) && !mocks.has(name) { + missing.push(name.clone()); + } + } + } + } + missing.sort_unstable(); + if !missing.is_empty() { + return Err(Error::MissingMocks(missing)); + } + + // Pass 1 — declare all group functions so sibling calls can link, plus the + // two runtime imports (resolved by name to the trampolines in `runtime`). + let mut func_ids = HashMap::new(); + for f in group { + let fid = module + .declare_function(&func_name(f), Linkage::Local, &abi::host_signature()) + .map_err(merr)?; + func_ids.insert(f.id, fid); + } + let mock_func = module + .declare_function("mock_dispatch", Linkage::Import, &abi::mock_signature()) + .map_err(merr)?; + let cond_func = module + .declare_function("eval_condition", Linkage::Import, &abi::cond_signature()) + .map_err(merr)?; + + let mut name_table = Vec::new(); + let mut name_idx = HashMap::new(); + let mut cond_table = Vec::new(); + let mut cond_counter = 0u64; + let mut entry = None; + + // Pass 2 — build each function body. + for f in group { + let items = build_items(f, &ids); + let mut ctx = module.make_context(); + // The entry block params mirror the declared host signature, so the + // IR function's signature must be populated before we build its body. + ctx.func.signature.params = abi::host_signature().params; + ctx.func.signature.returns = abi::host_signature().returns; + + // Pre-import every referenced callee + the two trampolines into this + // function's IR (the `Module` API borrows the `Function` mutably, so it + // must happen before the `FunctionBuilder` is created). + let mut callee_refs = HashMap::new(); + for it in &items { + if let ItemTag::GroupCall { callee } = &it.tag { + let fid = func_ids[callee]; + let fref = module.declare_func_in_func(fid, &mut ctx.func); + callee_refs.insert(*callee, fref); + } + } + let mock_ref = module.declare_func_in_func(mock_func, &mut ctx.func); + let cond_ref = module.declare_func_in_func(cond_func, &mut ctx.func); + + let mut fbc = FunctionBuilderContext::new(); + let mut fb = FunctionBuilder::new(&mut ctx.func, &mut fbc); + { + let mut lower = Lower::new( + fb, + &callee_refs, + mock_ref, + cond_ref, + &mut name_table, + &mut name_idx, + &mut cond_table, + &mut cond_counter, + items, + ); + lower.build()?; + fb = lower.fb; + fb.seal_all_blocks(); + fb.finalize(); + } + drop(fbc); + let fid = func_ids[&f.id]; + module.define_function(fid, &mut ctx).map_err(merr)?; + if entry.is_none() { + entry = Some(fid); + } + } + + module.finalize_definitions().map_err(merr)?; + + Ok(SandboxModule { + jit: module, + func_ids, + entry: entry.expect("group must not be empty"), + name_table, + cond_table, + mocks, + policy: config.branch_policy, + loop_cap: config.loop_cap, + }) +} + +/// Unique module-local name for a group function. +fn func_name(f: &GroupFunc) -> String { + format!("fn_{}", f.id) +} + +/// Preprocess a flow's chain into walkable items. +fn build_items(f: &GroupFunc, ids: &HashSet) -> Vec { + let flow: &FlowResult = &f.flow; + let mut pos_args = HashMap::new(); + for c in &flow.calls { + pos_args.insert(c.position, c.args.len()); + } + let mut items = Vec::new(); + for (i, &e) in flow.chain.iter().enumerate() { + if i == 0 { + continue; // position 0 is the function itself + } + if is_marker(e) { + items.push(Item { + tag: ItemTag::Marker(e), + pos: i, + follow: 0, + }); + } else if e == f.id { + // Recursion: mocked, like any external callee (termination guard). + items.push(Item { + tag: ItemTag::MockCall { + name: flow + .chain_desc + .get(i) + .cloned() + .unwrap_or_else(|| e.to_string()), + args: pos_args.get(&i).copied().unwrap_or(0), + }, + pos: i, + follow: 0, + }); + } else if ids.contains(&e) { + items.push(Item { + tag: ItemTag::GroupCall { callee: e }, + pos: i, + follow: 0, + }); + } else { + items.push(Item { + tag: ItemTag::MockCall { + name: flow + .chain_desc + .get(i) + .cloned() + .unwrap_or_else(|| e.to_string()), + args: pos_args.get(&i).copied().unwrap_or(0), + }, + pos: i, + follow: 0, + }); + } + } + // RETURN/THROW expr-lookahead: count consecutive Call items after each + // jump marker (the expression is evaluated before the jump happens). + for i in 0..items.len() { + if let ItemTag::Marker(m) = items[i].tag { + if m == MARKER_RETURN || m == MARKER_THROW { + let mut n = 0; + let mut j = i + 1; + while j < items.len() + && matches!( + items[j].tag, + ItemTag::GroupCall { .. } | ItemTag::MockCall { .. } + ) + { + n += 1; + j += 1; + } + items[i].follow = n; + } + } + } + items +} diff --git a/crates/codegraph-sboxes/src/config.rs b/crates/codegraph-sboxes/src/config.rs new file mode 100644 index 000000000..d94735e13 --- /dev/null +++ b/crates/codegraph-sboxes/src/config.rs @@ -0,0 +1,158 @@ +//! Sandbox configuration — read from the project's `.codegraph/config.toml` +//! `[sandbox]` section (same file `codegraph-extract` already uses for +//! `[languages]`, so there is exactly one project config file). +//! +//! ```toml +//! [sandbox] +//! mock_dirs = ["sandbox/mocks"] +//! loop_cap = 10 +//! branch_policy = "if_true" +//! +//! # Effect rules (Piece 2) — dùng chung schema với codegraph-extract. +//! # [[effect_rules]] +//! # call = { prefix = "db." } +//! # effect = "sql_query" +//! ``` + +use crate::runtime::BranchPolicy; +use camino::{Utf8Path, Utf8PathBuf}; +use codegraph_core::EffectRule; +use serde::Deserialize; +use std::fs; + +/// Why config loading failed. Kept small — most callers can fall back to +/// [`SboxConfig::default`] on error. +#[derive(Debug, thiserror::Error)] +pub enum SboxConfigError { + #[error("sandbox config io: {0}")] + Io(#[from] std::io::Error), + #[error("sandbox config parse: {0}")] + Toml(#[from] toml::de::Error), + #[error("sandbox config: unknown branch_policy `{0}` (expected if_true/if_false)")] + BranchPolicy(String), +} + +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default)] +struct ConfigFile { + sandbox: SandboxSection, + /// Effect rules dùng chung (schema `EffectRule` trong codegraph-core, cùng + /// file `[[effect_rules]]` mà codegraph-extract đọc). Consumed bởi Piece 3. + #[serde(default)] + effect_rules: Vec, +} + +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default)] +struct SandboxSection { + mock_dirs: Vec, + loop_cap: Option, + branch_policy: Option, +} + +/// Sandbox behavior configuration. +#[derive(Debug, Clone)] +pub struct SboxConfig { + /// Project root that relative `mock_dirs` resolve against. + pub root: Utf8PathBuf, + /// Directories (relative to `root`) containing `*.rhai` mocks. + pub mock_dirs: Vec, + /// Max iterations for any loop; guarantees termination. + pub loop_cap: usize, + /// How conditions are resolved at run time (deterministic by default). + pub branch_policy: BranchPolicy, + /// Project effect rules (top-level `[[effect_rules]]` in config.toml) — + /// consumed bởi Piece 3 (state delta theo effect). + #[allow(dead_code, reason = "Piece 3: effect rules drive state deltas")] + pub effect_rules: Vec, +} + +impl Default for SboxConfig { + fn default() -> Self { + Self { + root: Utf8PathBuf::from("."), + mock_dirs: vec!["sandbox/mocks".to_string()], + loop_cap: 10, + branch_policy: BranchPolicy::IfTrue, + effect_rules: Vec::new(), + } + } +} + +impl SboxConfig { + /// Load `.codegraph/config.toml` under `root`. Missing file → default + /// (with `root` still set so relative mock dirs resolve correctly). + pub fn load(root: &Utf8Path) -> Result { + let mut cfg = Self::load_from(&root.join(".codegraph").join("config.toml"))?; + cfg.root = root.to_path_buf(); + Ok(cfg) + } + + /// Load from an explicit path. Missing file → default. + pub fn load_from(path: &Utf8Path) -> Result { + let Ok(text) = fs::read_to_string(path.as_std_path()) else { + return Ok(Self::default()); + }; + let cfg: ConfigFile = toml::from_str(&text)?; + let policy = match cfg.sandbox.branch_policy.as_deref() { + None => BranchPolicy::IfTrue, + Some("if_true") => BranchPolicy::IfTrue, + Some("if_false") => BranchPolicy::IfFalse, + Some(other) => return Err(SboxConfigError::BranchPolicy(other.to_string())), + }; + Ok(Self { + root: Utf8PathBuf::from("."), + mock_dirs: if cfg.sandbox.mock_dirs.is_empty() { + vec!["sandbox/mocks".to_string()] + } else { + cfg.sandbox.mock_dirs + }, + loop_cap: cfg.sandbox.loop_cap.unwrap_or(10), + branch_policy: policy, + effect_rules: cfg.effect_rules, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn missing_file_is_default() { + let cfg = SboxConfig::load_from(Utf8Path::new("/nonexistent/x.toml")).unwrap(); + assert_eq!(cfg.loop_cap, 10); + assert_eq!(cfg.branch_policy, BranchPolicy::IfTrue); + } + + #[test] + fn parse_sandbox_section() { + let dir = std::env::temp_dir().join("codegraph-sboxes-cfg-test"); + std::fs::create_dir_all(&dir).unwrap(); + let cfg_path = dir.join("config.toml"); + let path = Utf8Path::from_path(cfg_path.as_path()).unwrap(); + std::fs::write( + path, + "[sandbox]\nmock_dirs = [\"mocks/a\", \"mocks/b\"]\nloop_cap = 3\nbranch_policy = \"if_false\"\n", + ) + .unwrap(); + let cfg = SboxConfig::load_from(path).unwrap(); + assert_eq!(cfg.mock_dirs, vec!["mocks/a", "mocks/b"]); + assert_eq!(cfg.loop_cap, 3); + assert_eq!(cfg.branch_policy, BranchPolicy::IfFalse); + let _ = std::fs::remove_file(path.as_std_path()); + let _ = std::fs::remove_dir(&dir); + } + + #[test] + fn unknown_policy_is_error() { + let dir = std::env::temp_dir().join("codegraph-sboxes-cfg-bad"); + std::fs::create_dir_all(&dir).unwrap(); + let cfg_path = dir.join("config.toml"); + let path = Utf8Path::from_path(cfg_path.as_path()).unwrap(); + std::fs::write(path, "[sandbox]\nbranch_policy = \"sometimes\"\n").unwrap(); + assert!(SboxConfig::load_from(path).is_err()); + let _ = std::fs::remove_file(path.as_std_path()); + let _ = std::fs::remove_dir(&dir); + } +} diff --git a/crates/codegraph-sboxes/src/group.rs b/crates/codegraph-sboxes/src/group.rs new file mode 100644 index 000000000..f87e19b1a --- /dev/null +++ b/crates/codegraph-sboxes/src/group.rs @@ -0,0 +1,43 @@ +//! Load a *group* of functions from the graph: the flows that Piece 1 compiles. +//! +//! A group is the set of symbols we want to turn into real machine code. Calls +//! **between** group members are linked as real compiled calls; every other +//! callee (external, unresolved, or an in-repo symbol outside the group) is +//! dispatched to a Rhai mock at run time. + +use codegraph_core::{FlowResult, Result, Symbol}; +use codegraph_graph::GraphIndex; +use std::collections::HashSet; + +/// One function in the group, ready to be compiled. +#[derive(Debug, Clone)] +pub struct GroupFunc { + pub id: u64, + pub symbol: Symbol, + pub flow: FlowResult, +} + +/// Load flows for every id in `ids` from the graph. +pub async fn load_group(index: &GraphIndex, ids: &[u64]) -> Result> { + let mut out = Vec::with_capacity(ids.len()); + for &id in ids { + let flow = index.flow(id).await?; + out.push(GroupFunc { + id, + symbol: flow.symbol.clone(), + flow, + }); + } + Ok(out) +} + +/// The set of in-group symbol ids (callee ids inside the group compile to real +/// function calls instead of mock dispatches). +pub fn group_ids(group: &[GroupFunc]) -> HashSet { + group.iter().map(|f| f.id).collect() +} + +/// Flows indexed by symbol id, for link resolution during codegen. +pub fn by_id(group: &[GroupFunc]) -> std::collections::HashMap { + group.iter().map(|f| (f.id, f)).collect() +} diff --git a/crates/codegraph-sboxes/src/lib.rs b/crates/codegraph-sboxes/src/lib.rs new file mode 100644 index 000000000..f1c0cc654 --- /dev/null +++ b/crates/codegraph-sboxes/src/lib.rs @@ -0,0 +1,62 @@ +//! codegraph-sboxes — Behavior Verification Sandbox (Piece 1). +//! +//! Compile a *group of functions* from the semantic graph (`GraphIndex::flow`) +//! into real machine code via **Cranelift JIT**, with the callees they call +//! bound to **Rhai mocks**. Each compiled function is `extern "C" fn`: +//! +//! ```text +//! fn(ctx: *mut Ctx, nargs: i64, args: *mut i64, ret: *mut i64) -> i64 +//! ``` +//! +//! Two imported trampolines provided by the runtime: +//! - `mock_dispatch(ctx, callee_idx, nargs, args, ret) -> i64` — run a Rhai mock. +//! - `eval_condition(ctx, cond_idx, rec_depth) -> i64` — resolve IF/LOOP/SWITCH +//! conditions from a deterministic `BranchPolicy` (termination via `loop_cap`). +//! +//! See `codegen` for the chain-marker → structured-CFG lowering and `runtime` +//! for the JIT module wiring. + +pub mod abi; +pub mod codegen; +pub mod config; +pub mod group; +pub mod rhai; +pub mod runtime; +pub mod trace; + +pub use config::{SboxConfig, SboxConfigError}; +pub use group::{load_group, GroupFunc}; +pub use rhai::{MockError, MockResult, RhaiMockLib}; +pub use runtime::{BranchPolicy, RunContext, SandboxModule}; +pub use trace::{CondEvent, CondKind, MockEvent, Trace, TraceEvent}; + +use codegraph_core::Result; +use codegraph_graph::GraphIndex; + +/// Compile a group of symbols into a sandbox module (machine code) ready to run. +/// +/// `ids` are the in-group symbol ids: calls between them are linked as real +/// compiled functions; every other callee (external or unresolved) is dispatched +/// to a Rhai mock at run time. A module runs one sandbox run at a time +/// (`SandboxModule::run`). +pub async fn compile( + index: &GraphIndex, + ids: &[u64], + config: &SboxConfig, +) -> Result { + compile_with_mocks(index, ids, config, &[]).await +} + +/// Compile with per-call inline mock overrides (`name → rhai source`, either a +/// body or a full `fn (args) { … }` script). Inline mocks win over mocks +/// loaded from `config.mock_dirs` — lets a caller mock specific functions (e.g. +/// from MCP args) instead of hitting a missing-mock fallback. +pub async fn compile_with_mocks( + index: &GraphIndex, + ids: &[u64], + config: &SboxConfig, + mocks: &[(String, String)], +) -> Result { + let group = load_group(index, ids).await?; + codegen::compile_group(&group, config, mocks) +} diff --git a/crates/codegraph-sboxes/src/rhai.rs b/crates/codegraph-sboxes/src/rhai.rs new file mode 100644 index 000000000..b9ba0e5b5 --- /dev/null +++ b/crates/codegraph-sboxes/src/rhai.rs @@ -0,0 +1,226 @@ +//! Rhai mock environment. +//! +//! Mock contract: a `*.rhai` file under a configured mock dir declares functions +//! named after the callee, taking a single array argument and returning an `i64` +//! (abstract value), e.g.: +//! +//! ```rhai +//! // sandbox/mocks/order.rhai +//! fn validate_order(args) { 1 } +//! fn insert_order(args) { 42 } +//! ``` +//! +//! The sandbox runtime dispatches every external/unresolved call through +//! [`RhaiMockLib::call`]; a missing mock returns `Err(MockError::NotFound)` and +//! the runtime records the miss (still returning `0`) so the caller can see what +//! was not mocked. +//! +//! Per-call mock configuration is supported via [`RhaiMockLib::register`] +//! (name → Rhai body/full `fn` source). Inline mocks override file mocks of the +//! same name — used by the MCP sandbox tool so a caller can mock specific +//! functions instead of seeing a missing-mock error. + +use rhai::{Array, Dynamic, Engine, Scope, AST}; +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +/// Why a mock could not run. +#[derive(Debug, thiserror::Error)] +pub enum MockError { + /// No `fn ` found in any loaded mock file. + #[error("no rhai mock for `{0}`")] + NotFound(String), + /// The mock script itself failed. + #[error("rhai mock `{0}` failed: {1}")] + Script(String, String), +} + +/// Convenience alias. +pub type MockResult = Result; + +/// A loaded set of Rhai mocks (one shared `Engine` + one merged `AST`). +/// +/// Loaded once per sandbox; reused across runs (each run gets its own `Scope`). +pub struct RhaiMockLib { + engine: Engine, + ast: AST, + names: HashSet, + /// Per-name override mocks (registered at run-request time). Kept separate + /// from `ast` so an inline mock replaces a file mock deterministically. + inline: HashMap, +} + +impl RhaiMockLib { + /// Load all `*.rhai` files under `dirs` (relative to `root`). + pub fn load(root: &Path, dirs: &[String]) -> Self { + let engine = Engine::new(); + let mut ast = AST::empty(); + let mut names = HashSet::new(); + + for dir in dirs { + let abs = root.join(dir); + let Ok(entries) = std::fs::read_dir(&abs) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("rhai") { + continue; + } + if let Ok(script) = std::fs::read_to_string(&path) { + if let Ok(compiled) = engine.compile(&script) { + for sig in compiled.iter_functions() { + names.insert(sig.name.to_string()); + } + ast = ast.merge(&compiled); + } + } + } + } + Self { + engine, + ast, + names, + inline: HashMap::new(), + } + } + + /// Load mock files, then overlay inline per-function mocks (`name → rhai + /// source`). Inline mocks win over file mocks with the same name. + pub fn load_with_mocks(root: &Path, dirs: &[String], inline: &[(String, String)]) -> Self { + let mut lib = Self::load(root, dirs); + for (name, src) in inline { + let _ = lib.register(name, src); // bad source: skip, `call` reports it + } + lib + } + + /// Register (or replace) one mock by name. `src` is either a full + /// `fn (args) { … }` script or just the function body, which is + /// wrapped into `fn (args) { }`. + pub fn register(&mut self, name: &str, src: &str) -> MockResult<()> { + let script = if src.trim_start().starts_with("fn ") { + src.to_string() + } else { + format!("fn {name}(args) {{ {src} }}") + }; + let compiled = self + .engine + .compile(&script) + .map_err(|e| MockError::Script(name.to_string(), e.to_string()))?; + self.names.insert(name.to_string()); + self.inline.insert(name.to_string(), compiled); + Ok(()) + } + + /// Empty mock library (every call misses). + pub fn empty() -> Self { + Self { + engine: Engine::new(), + ast: AST::empty(), + names: HashSet::new(), + inline: HashMap::new(), + } + } + + /// Whether a mock for `name` is loaded (file or inline). + pub fn has(&self, name: &str) -> bool { + self.names.contains(name) + } + + /// Invoke the mock for `name` with abstract `args` (an array, per the + /// contract above). Returns the mock's `i64` result. Inline mocks are + /// preferred; fall back to the merged file AST. + pub fn call(&mut self, name: &str, args: &[i64]) -> MockResult { + let mut scope = Scope::new(); + let arr: Array = args.iter().copied().map(Dynamic::from).collect(); + let arg = Dynamic::from(arr); + if let Some(ast) = self.inline.get(name) { + return self + .engine + .call_fn::(&mut scope, ast, name, (arg,)) + .map_err(|e| MockError::Script(name.to_string(), e.to_string())); + } + if !self.names.contains(name) { + return Err(MockError::NotFound(name.to_string())); + } + self.engine + .call_fn::(&mut scope, &self.ast, name, (arg,)) + .map_err(|e| MockError::Script(name.to_string(), e.to_string())) + } +} + +impl Default for RhaiMockLib { + fn default() -> Self { + Self::empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_and_call() { + let dir = std::env::temp_dir().join("codegraph-sboxes-rhai-test"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write( + dir.join("order.rhai"), + "fn validate_order(args) { 7 }\nfn insert_order(args) { args[0] * 2 }\n", + ) + .unwrap(); + let mut lib = RhaiMockLib::load( + std::env::temp_dir().as_path(), + &["codegraph-sboxes-rhai-test".to_string()], + ); + assert!(lib.has("validate_order")); + assert!(!lib.has("nope")); + assert_eq!(lib.call("validate_order", &[]).unwrap(), 7); + assert_eq!(lib.call("insert_order", &[21]).unwrap(), 42); + assert!(matches!(lib.call("nope", &[]), Err(MockError::NotFound(_)))); + let _ = std::fs::remove_dir_all(&dir); + } + + /// Inline mock: body-only được wrap thành `fn (args)`, override file + /// mock cùng tên, và chưa có trong file vẫn chạy được. + #[test] + fn inline_mocks_override_and_add() { + let dir = std::env::temp_dir().join("codegraph-sboxes-rhai-inline"); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write( + dir.join("order.rhai"), + "fn get_stock(args) { 1 }\nfn send_email(args) { 2 }\nfn ship(args) { 3 }\n", + ) + .unwrap(); + let inline = vec![ + ("get_stock".to_string(), "99".to_string()), + ("insert_order".to_string(), "args[0] * 10".to_string()), + ( + "send_email".to_string(), + "fn send_email(args) { args.len() }".to_string(), + ), + ]; + let mut lib = RhaiMockLib::load_with_mocks( + std::env::temp_dir().as_path(), + &["codegraph-sboxes-rhai-inline".to_string()], + &inline, + ); + // Inline override file mock. + assert_eq!(lib.call("get_stock", &[]).unwrap(), 99); + // Body-only inline. + assert_eq!(lib.call("insert_order", &[4]).unwrap(), 40); + // Full-fn inline (with different body) override file mock. + assert_eq!(lib.call("send_email", &[]).unwrap(), 0); + // Không inline → rơi về file mock. + assert_eq!(lib.call("ship", &[]).unwrap(), 3); + let _ = std::fs::remove_dir_all(&dir); + } + + /// Inline mock source lỗi → register trả Script error, không crash. + #[test] + fn bad_inline_mock_reports_script_error() { + let mut lib = RhaiMockLib::empty(); + assert!(lib.register("oops", "let x = ").is_err()); + assert!(!lib.has("oops")); + } +} diff --git a/crates/codegraph-sboxes/src/runtime.rs b/crates/codegraph-sboxes/src/runtime.rs new file mode 100644 index 000000000..4f23d5dc6 --- /dev/null +++ b/crates/codegraph-sboxes/src/runtime.rs @@ -0,0 +1,216 @@ +//! JIT runtime: owns the cranelift module, provides the two trampolines the +//! compiled code imports (`mock_dispatch`, `eval_condition`), and runs a group. +//! +//! A compiled function is `extern "C" fn(ctx, nargs, args, ret) -> i64` where: +//! - `ctx` — `*mut RunContext` (per-run state; thread-confined to one run). +//! - `args` — pointer to `nargs` i64 slots; doubles as the shared scratch +//! arena for nested call args (values are consumed synchronously). +//! - `ret` — pointer to one i64 slot where the function stores its result. + +use crate::rhai::{MockError, RhaiMockLib}; +use crate::trace::{CondEvent, CondKind, MockEvent, Trace, TraceEvent}; +use codegraph_core::{Error, Result}; +use cranelift_codegen::settings::{self, Configurable}; +use cranelift_jit::{JITBuilder, JITModule}; +use cranelift_module::{default_libcall_names, FuncId}; +use std::cell::RefCell; +use std::collections::HashMap; + +/// How conditions are resolved at run time (deterministic for now; steerable +/// per test later). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BranchPolicy { + /// Every `if` takes its then-branch; switches take their first case. + IfTrue, + /// Every `if` takes its else-branch (if the chain has one). + IfFalse, +} + +/// Scratch arena size (i64 slots). Bounded — nested calls reuse slots. +pub const ARENA_SLOTS: usize = 4096; + +/// Everything the two trampolines need for one run. Mutable through the raw +/// `ctx` pointer; never shared between threads for a single run. +pub struct RunContext { + pub mocks: RhaiMockLib, + pub name_table: Vec, + pub cond_table: Vec, + pub policy: BranchPolicy, + pub loop_cap: usize, + pub trace: RefCell, + /// Loop iteration counters (cond_idx → hits) so loops always terminate. + pub loop_hits: HashMap, + /// Switch "first case taken" state per cond_idx. + pub switch_taken: HashMap, +} + +impl RunContext { + fn decide(&mut self, kind: CondKind, idx: u64) -> bool { + match kind { + CondKind::If => match self.policy { + BranchPolicy::IfTrue => true, + BranchPolicy::IfFalse => false, + }, + CondKind::Loop => { + let n = self.loop_hits.entry(idx).or_insert(0); + *n += 1; + *n <= self.loop_cap + } + CondKind::Switch => { + let first = self.switch_taken.entry(idx).or_insert(true); + let r = *first; + *first = false; + r + } + } + } +} + +/// The compiled group: machine code + run-time metadata (callee name table, +/// condition table, mock library, policy). `run` re-lends the mock library for +/// the duration of a run, so a module is used by one run at a time. +pub struct SandboxModule { + pub(crate) jit: JITModule, + /// In-group symbol id → compiled function. + pub func_ids: HashMap, + /// The entry function for a run. + pub entry: FuncId, + /// `callee_idx` (embedded in code) → callee name for mock dispatch. + pub name_table: Vec, + /// `cond_idx` (embedded in code) → condition kind. + pub cond_table: Vec, + pub mocks: RhaiMockLib, + pub policy: BranchPolicy, + pub loop_cap: usize, +} + +impl SandboxModule { + /// Run the entry function with abstract `args`. Returns the result value + /// and the observed behavior trace. + pub fn run(&mut self, args: &[i64]) -> (i64, Trace) { + self.run_func(self.entry, args) + } + + /// Run an arbitrary compiled function in this module. + pub fn run_func(&mut self, func: FuncId, args: &[i64]) -> (i64, Trace) { + let f: unsafe extern "C" fn(*mut RunContext, u64, *mut i64, *mut i64) -> i64 = + unsafe { std::mem::transmute::<*const u8, _>(self.jit.get_finalized_function(func)) }; + + let mut arena = vec![0i64; ARENA_SLOTS]; + for (i, a) in args.iter().take(ARENA_SLOTS).enumerate() { + arena[i] = *a; + } + let mut ret: i64 = 0; + let mut rc = RunContext { + mocks: std::mem::take(&mut self.mocks), + name_table: self.name_table.clone(), + cond_table: self.cond_table.clone(), + policy: self.policy, + loop_cap: self.loop_cap, + trace: RefCell::new(Trace::default()), + loop_hits: HashMap::new(), + switch_taken: HashMap::new(), + }; + let result = unsafe { + f( + &mut rc as *mut RunContext, + args.len() as u64, + arena.as_mut_ptr(), + &mut ret, + ) + }; + let trace = rc.trace.into_inner(); + self.mocks = std::mem::take(&mut rc.mocks); + (result, trace) + } +} + +/// Build a JIT module with the two runtime imports wired to this module's +/// trampolines. The trampoline symbols are global (process-wide), so a module +/// is bound to them by name; the per-run state travels via `ctx`. +/// +/// The native ISA is built with `is_pic = false`: cranelift-jit's PIC path +/// allocates a PLT entry per declared function, which is x86-only, and the host +/// here is arm64. +pub fn create_jit_module() -> Result { + let mut flag_builder = settings::builder(); + flag_builder + .set("is_pic", "false") + .map_err(|e| Error::Other(e.to_string()))?; + flag_builder + .set("use_colocated_libcalls", "false") + .map_err(|e| Error::Other(e.to_string()))?; + let isa_builder = cranelift_native::builder().map_err(|e| Error::Other(e.to_string()))?; + let isa = isa_builder + .finish(settings::Flags::new(flag_builder)) + .map_err(|e| Error::Other(e.to_string()))?; + let mut builder = JITBuilder::with_isa(isa, default_libcall_names()); + builder.symbol("mock_dispatch", mock_dispatch_trampoline as *const u8); + builder.symbol("eval_condition", eval_condition_trampoline as *const u8); + Ok(JITModule::new(builder)) +} + +/// `(ctx, callee_idx, nargs, args, ret) -> i64` — dispatch one callee to its +/// Rhai mock and record it in the trace. +unsafe extern "C" fn mock_dispatch_trampoline( + ctx: *mut RunContext, + callee_idx: u64, + nargs: u64, + args: *mut i64, + ret: *mut i64, +) -> i64 { + let rc = &mut *ctx; + let name = rc + .name_table + .get(callee_idx as usize) + .cloned() + .unwrap_or_else(|| format!("unknown({callee_idx})")); + let arg_count = (nargs as usize).min(64); + let mut argvals = Vec::with_capacity(arg_count); + for i in 0..arg_count { + argvals.push(*args.add(i)); + } + let result = match rc.mocks.call(&name, &argvals) { + Ok(v) => v, + Err(MockError::NotFound(_)) => { + // No file or inline mock — record the miss so the caller sees what + // still needs mocking (the run itself returns the `0` fallback). + rc.trace.borrow_mut().missing.push(name.clone()); + 0 + } + Err(_) => 0, + }; + *ret = result; + let event = MockEvent { + callee: name, + args: argvals, + result, + }; + rc.trace.borrow_mut().mocks.push(event.clone()); + rc.trace.borrow_mut().events.push(TraceEvent::Mock(event)); + result +} + +/// `(ctx, cond_idx, rec_depth) -> i64` — resolve one control-flow condition +/// from the policy and record the decision. +unsafe extern "C" fn eval_condition_trampoline( + ctx: *mut RunContext, + cond_idx: u64, + _rec_depth: u64, +) -> i64 { + let rc = &mut *ctx; + let kind = rc + .cond_table + .get(cond_idx as usize) + .copied() + .unwrap_or(CondKind::If); + let result = rc.decide(kind, cond_idx); + let event = CondEvent { + kind, + idx: cond_idx, + result, + }; + rc.trace.borrow_mut().conds.push(event.clone()); + rc.trace.borrow_mut().events.push(TraceEvent::Cond(event)); + i64::from(result) +} diff --git a/crates/codegraph-sboxes/src/trace.rs b/crates/codegraph-sboxes/src/trace.rs new file mode 100644 index 000000000..64c0b4b29 --- /dev/null +++ b/crates/codegraph-sboxes/src/trace.rs @@ -0,0 +1,96 @@ +//! Observable behavior trace produced by a sandbox run. +//! +//! Piece 1 records the *unobservable black-box* of a function as the sequence of +//! mocked calls it makes. The `Trace` below is the "observed behavior" — the +//! input later Pieces (spec/invariant compare) will verify against. + +use serde::{Deserialize, Serialize}; + +/// Which kind of control-flow marker drove a condition decision. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CondKind { + If, + Loop, + Switch, +} + +impl CondKind { + pub fn as_str(self) -> &'static str { + match self { + CondKind::If => "if", + CondKind::Loop => "loop", + CondKind::Switch => "switch", + } + } +} + +/// One dispatched call to an (external/unresolved) callee. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MockEvent { + /// Callee name (resolved symbol name or raw call name). + pub callee: String, + /// Abstract arg values passed by the compiled code. + pub args: Vec, + /// Value returned by the mock (or fallback `0`). + pub result: i64, +} + +/// One condition decision made by the policy during a run. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CondEvent { + pub kind: CondKind, + /// Index into the module's condition table. + pub idx: u64, + pub result: bool, +} + +/// One entry in the interleaved run log (the *order* between a condition +/// decision and the mock call it gates is observable behavior). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum TraceEvent { + Mock(MockEvent), + Cond(CondEvent), +} + +/// The observed behavior of a run: ordered mock calls + control-flow decisions. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct Trace { + pub mocks: Vec, + pub conds: Vec, + /// Interleaved log of both, in execution order. + pub events: Vec, + /// Callee names that were dispatched but had no mock (file or inline) — the + /// run fell back to `0`. Lets a caller see what still needs to be mocked. + pub missing: Vec, +} + +impl Trace { + pub fn mock_names(&self) -> Vec<&str> { + self.mocks.iter().map(|m| m.callee.as_str()).collect() + } + + /// Count how many times a mock was invoked. + pub fn count(&self, callee: &str) -> usize { + self.mocks.iter().filter(|m| m.callee == callee).count() + } + + /// The invocation order, as a list of "kind/name" tokens. + pub fn sequence(&self) -> Vec { + let mut out = Vec::new(); + for e in &self.events { + match e { + TraceEvent::Cond(c) => { + out.push(format!( + "{}:{}", + c.kind.as_str(), + if c.result { 1 } else { 0 } + )); + } + TraceEvent::Mock(m) => out.push(format!("call:{}", m.callee)), + } + } + out + } +} diff --git a/crates/codegraph-sboxes/tests/control_flow.rs b/crates/codegraph-sboxes/tests/control_flow.rs new file mode 100644 index 000000000..d96c587c7 --- /dev/null +++ b/crates/codegraph-sboxes/tests/control_flow.rs @@ -0,0 +1,285 @@ +//! Control-flow lowering: build a small in-memory graph whose `flow()` chains +//! carry IF / LOOP / SWITCH / RETURN markers, compile the group with Cranelift, +//! and assert the *observed behavior* (mock call order + condition decisions). +//! +//! The fixture graph is hand-built as `ParseResult`s (same shapes the +//! codegraph-graph tests use), then ingested into `GraphIndex::in_memory()`. + +use codegraph_core::{ + CallRecord, EffectType, ScopeLevel, Symbol, SymbolKind, MARKER_BRANCH_END, MARKER_IF_FALSE, + MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, MARKER_SWITCH_CASE, MARKER_SWITCH_END, + SYMBOL_BASE, +}; +use codegraph_graph::GraphIndex; +use codegraph_sboxes::{compile, BranchPolicy, CondKind, SboxConfig}; +use std::collections::HashMap; + +fn sym(id: u64, name: &str) -> Symbol { + Symbol { + id, + name: name.to_string(), + kind: SymbolKind::Function, + scope: ScopeLevel::Global, + scope_id: 0, + type_ref: 0, + type_name: None, + file: "test.ts".to_string(), + line: 1, + end_line: 1, + signature: None, + doc: None, + annotations: Vec::new(), + language: "test".to_string(), + } +} + +fn rec(caller_id: u64, pos: usize, name: &str, args: usize) -> CallRecord { + CallRecord { + caller_id, + call_name: name.to_string(), + position: pos, + arg_exprs: (0..args).map(|i| format!("a{i}")).collect(), + line: 1, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + } +} + +fn result( + path: &str, + symbols: Vec, + chains: HashMap>, + calls: Vec, +) -> codegraph_graph::ParseResult { + codegraph_graph::ParseResult { + path: path.to_string(), + language: "test".to_string(), + bytes: 0, + lines: 0, + symbols, + chains, + calls, + } +} + +fn test_config() -> SboxConfig { + SboxConfig { + root: ".".into(), + mock_dirs: vec!["tests/mocks".to_string()], + loop_cap: 5, + branch_policy: BranchPolicy::IfTrue, + effect_rules: Vec::new(), + } +} + +/// `compute`: calls in-group `helper`, then `if` → `notify`, then a capped +/// `loop` → `poll`, then `done`. `helper` calls the `seed` mock. +/// +/// With the IfTrue policy the expected observed behavior is: +/// `seed` (via helper), `notify` (if taken), `poll` × loop_cap, `done`. +#[tokio::test] +async fn if_and_capped_loop() { + const COMPUTE: u64 = SYMBOL_BASE; + const HELPER: u64 = SYMBOL_BASE + 1; + + let chains = HashMap::from([ + ( + COMPUTE, + vec![ + COMPUTE, // 0 self + HELPER, // 1 group call + MARKER_IF_TRUE, // 2 + 0, // 3 notify (mock) + MARKER_BRANCH_END, // 4 + MARKER_LOOP, // 5 + 0, // 6 poll (mock) + MARKER_LOOP_BACK, // 7 + 0, // 8 done (mock) + ], + ), + ( + HELPER, + vec![ + HELPER, // 0 self + 0, // 1 seed (mock) + ], + ), + ]); + let calls = vec![ + rec(COMPUTE, 1, "helper", 0), + rec(COMPUTE, 3, "notify", 1), + rec(COMPUTE, 6, "poll", 0), + rec(COMPUTE, 8, "done", 1), + rec(HELPER, 1, "seed", 0), + ]; + let r = result( + "test.ts", + vec![sym(COMPUTE, "compute"), sym(HELPER, "helper")], + chains, + calls, + ); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[r]).await.unwrap(); + + let mut module = compile(&idx, &[COMPUTE, HELPER], &test_config()) + .await + .unwrap(); + let (result, trace) = module.run(&[]); + + // `done` is the last expression of `compute`, so the entry returns its mock value. + assert_eq!(result, 5); + assert_eq!(trace.count("seed"), 1); + assert_eq!(trace.count("notify"), 1); + assert_eq!(trace.count("poll"), 5); // loop capped at 5 iterations + assert_eq!(trace.count("done"), 1); + assert_eq!( + trace.mock_names(), + vec!["seed", "notify", "poll", "poll", "poll", "poll", "poll", "done"] + ); + + // One if-condition decision (taken) + one loop-cap-exit evaluation extra. + let ifs = trace + .conds + .iter() + .filter(|c| c.kind == CondKind::If) + .count(); + let loops = trace + .conds + .iter() + .filter(|c| c.kind == CondKind::Loop) + .count(); + assert_eq!(ifs, 1); + assert_eq!(loops, 6); // 5 taken + the 6th that fails the cap and exits +} + +/// Same graph, `IfFalse` policy: `notify` must NOT be called, but the loop still +/// runs (loops are capped by iteration count, not by the branch policy). +#[tokio::test] +async fn if_false_policy_skips_then_branch() { + const COMPUTE: u64 = SYMBOL_BASE; + + let chains = HashMap::from([( + COMPUTE, + vec![ + COMPUTE, + MARKER_IF_TRUE, + 0, // notify + MARKER_BRANCH_END, + MARKER_LOOP, + 0, // poll + MARKER_LOOP_BACK, + 0, // done + ], + )]); + let calls = vec![ + rec(COMPUTE, 2, "notify", 0), + rec(COMPUTE, 5, "poll", 0), + rec(COMPUTE, 7, "done", 0), + ]; + let r = result("test.ts", vec![sym(COMPUTE, "compute")], chains, calls); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[r]).await.unwrap(); + + let cfg = SboxConfig { + branch_policy: BranchPolicy::IfFalse, + ..test_config() + }; + let mut module = compile(&idx, &[COMPUTE], &cfg).await.unwrap(); + let (_, trace) = module.run(&[]); + + assert_eq!(trace.count("notify"), 0); + assert_eq!(trace.count("poll"), 5); + assert_eq!(trace.count("done"), 1); + let ifs = trace + .conds + .iter() + .filter(|c| c.kind == CondKind::If) + .count(); + assert_eq!(ifs, 1); +} + +/// Switch: first case taken (policy), `get_stock` called once even though two +/// `SWITCH_CASE … SWITCH_END` blocks exist in the chain. +#[tokio::test] +async fn switch_first_case_taken() { + const COMPUTE: u64 = SYMBOL_BASE; + + let chains = HashMap::from([( + COMPUTE, + vec![ + COMPUTE, + MARKER_SWITCH_CASE, + 0, // get_stock (case 1) + MARKER_SWITCH_END, + MARKER_SWITCH_CASE, + 0, // get_stock (case 2) + MARKER_SWITCH_END, + 0, // done + ], + )]); + let calls = vec![ + rec(COMPUTE, 2, "get_stock", 0), + rec(COMPUTE, 5, "get_stock", 0), + rec(COMPUTE, 7, "done", 0), + ]; + let r = result("test.ts", vec![sym(COMPUTE, "compute")], chains, calls); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[r]).await.unwrap(); + + let mut module = compile(&idx, &[COMPUTE], &test_config()).await.unwrap(); + let (_, trace) = module.run(&[]); + + assert_eq!(trace.count("get_stock"), 1); + assert_eq!(trace.count("done"), 1); + // Case-1 condition true, case-2 condition false → 2 switch decisions. + let switches = trace + .conds + .iter() + .filter(|c| c.kind == CondKind::Switch) + .count(); + assert_eq!(switches, 2); +} + +/// `if … else`: both branches compile; the IfFalse policy takes the `else` +/// branch (IF_FALSE → then-body skipped, else-body mock runs). +#[tokio::test] +async fn if_else_takes_else_branch() { + const COMPUTE: u64 = SYMBOL_BASE; + + let chains = HashMap::from([( + COMPUTE, + vec![ + COMPUTE, + MARKER_IF_TRUE, + 0, // then_mock + MARKER_IF_FALSE, + 0, // else_mock + MARKER_BRANCH_END, + 0, // done + ], + )]); + let calls = vec![ + rec(COMPUTE, 2, "then_mock", 0), + rec(COMPUTE, 4, "else_mock", 0), + rec(COMPUTE, 6, "done", 0), + ]; + let r = result("test.ts", vec![sym(COMPUTE, "compute")], chains, calls); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[r]).await.unwrap(); + + // IfFalse → else branch taken. + let cfg = SboxConfig { + branch_policy: BranchPolicy::IfFalse, + ..test_config() + }; + let mut module = compile(&idx, &[COMPUTE], &cfg).await.unwrap(); + let (_, trace) = module.run(&[]); + + assert_eq!(trace.count("then_mock"), 0); + assert_eq!(trace.count("else_mock"), 1); + assert_eq!(trace.count("done"), 1); +} diff --git a/crates/codegraph-sboxes/tests/end_to_end.rs b/crates/codegraph-sboxes/tests/end_to_end.rs new file mode 100644 index 000000000..dc348c9bc --- /dev/null +++ b/crates/codegraph-sboxes/tests/end_to_end.rs @@ -0,0 +1,270 @@ +//! End-to-end golden trace: two in-group functions (`prepare_order` → +//! `check_stock` real compiled call) with an `if` between them, all external +//! callees mocked by Rhai. Asserts the exact observed-behavior sequence. + +use codegraph_core::{ + CallRecord, EffectType, Error, ScopeLevel, Symbol, SymbolKind, MARKER_BRANCH_END, + MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_SWITCH_CASE, MARKER_SWITCH_END, SYMBOL_BASE, +}; +use codegraph_graph::GraphIndex; +use codegraph_sboxes::{compile, compile_with_mocks, BranchPolicy, SboxConfig}; +use std::collections::HashMap; + +fn sym(id: u64, name: &str) -> Symbol { + Symbol { + id, + name: name.to_string(), + kind: SymbolKind::Function, + scope: ScopeLevel::Global, + scope_id: 0, + type_ref: 0, + type_name: None, + file: "order.ts".to_string(), + line: 1, + end_line: 1, + signature: None, + doc: None, + annotations: Vec::new(), + language: "test".to_string(), + } +} + +fn rec(caller_id: u64, pos: usize, name: &str, args: usize) -> CallRecord { + CallRecord { + caller_id, + call_name: name.to_string(), + position: pos, + arg_exprs: (0..args).map(|i| format!("a{i}")).collect(), + line: 1, + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + } +} + +fn result( + path: &str, + symbols: Vec, + chains: HashMap>, + calls: Vec, +) -> codegraph_graph::ParseResult { + codegraph_graph::ParseResult { + path: path.to_string(), + language: "test".to_string(), + bytes: 0, + lines: 0, + symbols, + chains, + calls, + } +} + +fn test_config() -> SboxConfig { + SboxConfig { + root: ".".into(), + mock_dirs: vec!["tests/mocks".to_string()], + loop_cap: 5, + branch_policy: BranchPolicy::IfTrue, + effect_rules: Vec::new(), + } +} + +/// `prepare_order`: +/// check_stock() → if in-stock { send_email() } → insert_order() +/// +/// `check_stock`: returns the `get_stock` mock result via a switch (first case). +/// +/// Golden observed behavior (IfTrue, first case taken): +/// check_stock[group] → get_stock (switch case 1) → send_email (if taken) +/// → insert_order +#[tokio::test] +async fn prepare_order_golden_trace() { + const PREPARE: u64 = SYMBOL_BASE; + const CHECK: u64 = SYMBOL_BASE + 1; + + let chains = HashMap::from([ + ( + PREPARE, + vec![ + PREPARE, // 0 self + CHECK, // 1 group call → real compiled call + MARKER_IF_TRUE, // 2 + 0, // 3 send_email (mock) + MARKER_BRANCH_END, // 4 + 0, // 5 insert_order (mock) + ], + ), + ( + CHECK, + vec![ + CHECK, // 0 self + MARKER_SWITCH_CASE, // 1 + 0, // 2 get_stock (mock, case 1) + MARKER_SWITCH_END, // 3 + MARKER_SWITCH_CASE, // 4 + 0, // 5 get_stock (mock, case 2) + MARKER_SWITCH_END, // 6 + ], + ), + ]); + let calls = vec![ + rec(PREPARE, 1, "check_stock", 0), + rec(PREPARE, 3, "send_email", 1), + rec(PREPARE, 5, "insert_order", 1), + rec(CHECK, 2, "get_stock", 0), + rec(CHECK, 5, "get_stock", 0), + ]; + let r = result( + "order.ts", + vec![sym(PREPARE, "prepare_order"), sym(CHECK, "check_stock")], + chains, + calls, + ); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[r]).await.unwrap(); + + let mut module = compile(&idx, &[PREPARE, CHECK], &test_config()) + .await + .unwrap(); + let (result, trace) = module.run(&[]); + + // `insert_order` mock returns 42 and is the last call of `prepare_order`. + assert_eq!(result, 42); + assert_eq!( + trace.mock_names(), + vec!["get_stock", "send_email", "insert_order"] + ); + assert_eq!(trace.count("get_stock"), 1); // only the first switch case ran + assert_eq!(trace.count("send_email"), 1); + assert_eq!(trace.count("insert_order"), 1); + + // Sequence: first case's cond → its body → second case's cond (skipped), + // then prepare's if cond (taken) → email → insert. + let seq = trace.sequence(); + assert_eq!( + seq, + vec![ + "switch:1".to_string(), + "call:get_stock".to_string(), + "switch:0".to_string(), + "if:1".to_string(), + "call:send_email".to_string(), + "call:insert_order".to_string(), + ] + ); +} + +/// With `IfFalse`, `prepare_order` skips `send_email` but still inserts. +#[tokio::test] +async fn prepare_order_if_false_skips_email() { + const PREPARE: u64 = SYMBOL_BASE; + + let chains = HashMap::from([( + PREPARE, + vec![ + PREPARE, + MARKER_IF_TRUE, + 0, // send_email + MARKER_IF_FALSE, + 0, // log_skip (mock, else branch) + MARKER_BRANCH_END, + 0, // insert_order + ], + )]); + let calls = vec![ + rec(PREPARE, 2, "send_email", 0), + rec(PREPARE, 4, "log_skip", 0), + rec(PREPARE, 6, "insert_order", 0), + ]; + let r = result( + "order.ts", + vec![sym(PREPARE, "prepare_order")], + chains, + calls, + ); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[r]).await.unwrap(); + + let cfg = SboxConfig { + branch_policy: BranchPolicy::IfFalse, + ..test_config() + }; + let mut module = compile(&idx, &[PREPARE], &cfg).await.unwrap(); + let (_, trace) = module.run(&[]); + + assert_eq!(trace.count("send_email"), 0); + assert_eq!(trace.count("log_skip"), 1); + assert_eq!(trace.count("insert_order"), 1); +} + +/// Link-time missing-mock detection: a callee that will be mock-dispatched but +/// has no mock (file or inline) fails the compile with the exact list, instead +/// of silently running a `0` fallback. +#[tokio::test] +async fn link_fails_on_unmocked_callees() { + const RUN: u64 = SYMBOL_BASE; + + // run_order: submit(...) → compute_sku(...) + let chains = HashMap::from([( + RUN, + vec![ + RUN, // 0 self + 0, // 1 submit (no mock anywhere) + 0, // 2 compute_sku (inline mock only) + ], + )]); + let calls = vec![rec(RUN, 1, "submit", 1), rec(RUN, 2, "compute_sku", 2)]; + let r = result("order.ts", vec![sym(RUN, "run_order")], chains, calls); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[r]).await.unwrap(); + + // `compute_sku` is covered inline; `submit` is not → link error listing it. + let mocks = vec![("compute_sku".to_string(), "77".to_string())]; + let res = compile_with_mocks(&idx, &[RUN], &test_config(), &mocks).await; + assert!(matches!( + res, + Err(Error::MissingMocks(m)) if m == vec!["submit".to_string()] + )); +} + +/// `compile_with_mocks` satisfies link-time validation: per-call inline mocks +/// cover the callees missing from the file mock dir, the run dispatches to them, +/// and nothing lands in `trace.missing`. +#[tokio::test] +async fn inline_mocks_satisfy_link_and_run() { + const RUN: u64 = SYMBOL_BASE; + + // run_order: submit(...) → compute_sku(...) + let chains = HashMap::from([( + RUN, + vec![ + RUN, // 0 self + 0, // 1 submit (inline mock) + 0, // 2 compute_sku (inline mock) + ], + )]); + let calls = vec![rec(RUN, 1, "submit", 1), rec(RUN, 2, "compute_sku", 2)]; + let r = result("order.ts", vec![sym(RUN, "run_order")], chains, calls); + let mut idx = GraphIndex::in_memory(); + idx.ingest(&[r]).await.unwrap(); + + // Neither callee is in tests/mocks/order.rhai — only the inline mocks cover + // them, so link-time validation is satisfied by the inline set alone. + let mocks = vec![ + ("submit".to_string(), "5".to_string()), + ("compute_sku".to_string(), "77".to_string()), + ]; + let mut module = compile_with_mocks(&idx, &[RUN], &test_config(), &mocks) + .await + .unwrap(); + let (result, trace) = module.run(&[40, 37]); + + // Inline mocks run (5 then 77); `compute_sku` is the last call → result. + assert_eq!(trace.count("submit"), 1); + assert_eq!(trace.count("compute_sku"), 1); + assert_eq!(result, 77); + assert!(trace.missing.is_empty()); +} diff --git a/crates/codegraph-sboxes/tests/mocks/order.rhai b/crates/codegraph-sboxes/tests/mocks/order.rhai new file mode 100644 index 000000000..1c8d2f5ed --- /dev/null +++ b/crates/codegraph-sboxes/tests/mocks/order.rhai @@ -0,0 +1,24 @@ +// Shared mock lib for the sandbox integration tests. +// Mock contract: `fn (args) { … }` — `args` is an array of i64. + +fn seed(args) { 7 } + +fn notify(args) { 1 } + +fn poll(args) { 0 } + +fn done(args) { 5 } + +fn check_stock(args) { 3 } + +fn get_stock(args) { 100 } + +fn send_email(args) { 9 } + +fn insert_order(args) { 42 } + +fn log_skip(args) { 3 } + +fn then_mock(args) { 11 } + +fn else_mock(args) { 22 } diff --git a/crates/codegraph-viz/Cargo.toml b/crates/codegraph-viz/Cargo.toml deleted file mode 100644 index 9b18a7577..000000000 --- a/crates/codegraph-viz/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "codegraph-viz" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -codegraph-api = { path = "../codegraph-api" } -codegraph-core = { path = "../codegraph-core" } -codegraph-graph = { path = "../codegraph-graph", features = ["sqlite"] } -axum = { workspace = true } -tower = { workspace = true } -tower-http = { workspace = true } -rust-embed = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -anyhow = { workspace = true } -open = { workspace = true } - -[dev-dependencies] -reqwest = { workspace = true, features = ["json"] } -tempfile = "3" -camino = { workspace = true } diff --git a/crates/codegraph-viz/assets/app.js b/crates/codegraph-viz/assets/app.js deleted file mode 100644 index 43f027aff..000000000 --- a/crates/codegraph-viz/assets/app.js +++ /dev/null @@ -1,688 +0,0 @@ -/* global ForceGraph, ForceGraph3D */ - -const KIND_COLORS = { - function: '#5eead4', - method: '#2dd4bf', - class: '#818cf8', - interface: '#c084fc', - enum: '#fb923c', - module: '#f472b6', - file: '#64748b', - variable: '#94a3b8', - constant: '#fbbf24', - field: '#38bdf8', - parameter: '#22d3ee', - config: '#a3e635', - default: '#64748b', -}; - -const EDGE_COLORS = { - calls: '#5eead4', - default: '#3d465c', -}; - -const state = { - boot: { depth: 2 }, - graph: { nodes: [], edges: [], seed: null, truncated: false }, - selectedId: null, - hoverId: null, - graph2d: null, - graph3d: null, - activeView: 'table', - paused: false, - rotate3d: false, - rotateRaf: null, -}; - -// ── API ────────────────────────────────────────────── - -async function api(path) { - const res = await fetch(path); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error(err.error || res.statusText); - } - return res.json(); -} - -function setLoading(on) { - document.getElementById('loading').classList.toggle('hidden', !on); -} - -// ── Graph data ─────────────────────────────────────── - -function nodeColor(n) { - return KIND_COLORS[n.kind] || KIND_COLORS.default; -} - -function kindTag(kind) { - const c = nodeColor({ kind }); - return `${escapeHtml(kind)}`; -} - -function graphData() { - const allNodes = []; - const seen = new Set(); - const add = (n) => { - if (!n || seen.has(n.id)) return; - seen.add(n.id); - allNodes.push(n); - }; - if (state.graph.seed) add(state.graph.seed); - state.graph.nodes.forEach(add); - - const links = state.graph.edges.map((e) => ({ - source: e.from, - target: e.to, - kind: e.kind, - color: EDGE_COLORS[e.kind] || EDGE_COLORS.default, - })); - - return { - nodes: allNodes.map((n) => ({ - id: n.id, - name: n.name, - kind: n.kind, - val: nodeVal(n), - color: nodeColor(n), - raw: n, - })), - links, - }; -} - -function nodeVal(n) { - const base = n.kind === 'function' || n.kind === 'method' ? 2.5 : n.kind === 'class' ? 2 : 1.2; - if (n.id === state.selectedId) return base * 2.2; - if (n.id === state.hoverId) return base * 1.5; - return base; -} - -/** 3D: no hover resize — recreating spheres every frame kills FPS. */ -function nodeVal3d(n) { - const base = n.kind === 'function' || n.kind === 'method' ? 2.2 : n.kind === 'class' ? 1.8 : 1; - if (n.id === state.selectedId) return base * 1.6; - return base; -} - -function graph3dPerfTier(nodeCount, linkCount) { - if (nodeCount > 200 || linkCount > 500) return 'heavy'; - if (nodeCount > 80 || linkCount > 200) return 'medium'; - return 'light'; -} - -function applyGraph3dPerf(fg, nodeCount, linkCount) { - const tier = graph3dPerfTier(nodeCount, linkCount); - const particles = tier === 'light' && linkCount < 120 ? 1 : 0; - const resolution = tier === 'heavy' ? 5 : tier === 'medium' ? 7 : 9; - fg.linkDirectionalParticles(particles) - .linkDirectionalArrowLength(0) - .nodeResolution(resolution) - .d3AlphaDecay(tier === 'heavy' ? 0.04 : 0.028) - .warmupTicks(tier === 'heavy' ? 30 : 50) - .cooldownTicks(tier === 'heavy' ? 20 : 40); - const renderer = fg.renderer(); - if (renderer) { - const pr = tier === 'heavy' ? 1 : Math.min(window.devicePixelRatio, 1.35); - renderer.setPixelRatio(pr); - } - state._3dTier = tier; -} - -let hover3dRaf = null; -function schedule3dLinkRefresh() { - if (state._3dTier === 'heavy') return; - if (hover3dRaf) return; - hover3dRaf = requestAnimationFrame(() => { - hover3dRaf = null; - if (state.graph3d) state.graph3d.linkColor(linkColorFn3d); - }); -} - -function pause3dPhysicsIfIdle() { - if (state.graph3d && state.activeView === 'graph3d' && !state.paused && !state.rotate3d) { - state.graph3d.pauseAnimation(); - state._3dPhysicsDone = true; - } -} - -function linkEndpoints(l) { - return { - src: typeof l.source === 'object' ? l.source.id : l.source, - tgt: typeof l.target === 'object' ? l.target.id : l.target, - }; -} - -function linkColorFn(l) { - const hi = state.selectedId || state.hoverId; - if (!hi) return l.color + '66'; - const { src, tgt } = linkEndpoints(l); - return src === hi || tgt === hi ? l.color : l.color + '22'; -} - -function linkWidthFn(l) { - const hi = state.selectedId || state.hoverId; - if (!hi) return 1; - const { src, tgt } = linkEndpoints(l); - return src === hi || tgt === hi ? 2 : 0.4; -} - -function linkColorFn3d(l) { - if (state._3dTier === 'heavy') return l.color + '55'; - const hi = state.selectedId || state.hoverId; - if (!hi) return l.color + '77'; - const { src, tgt } = linkEndpoints(l); - return src === hi || tgt === hi ? l.color : l.color + '28'; -} - -function updateGraphCounts() { - const data = graphData(); - const perf = - state.activeView === 'graph3d' && state._3dTier && state._3dTier !== 'light' - ? ` · ${state._3dTier} perf` - : ''; - const text = `${data.nodes.length} nodes · ${data.links.length} edges${perf}`; - document.getElementById('graph-count').textContent = text; - document.getElementById('graph-count').classList.toggle('hidden', !data.nodes.length); - document.getElementById('hud-stats').textContent = text; -} - -function renderLegend() { - const kinds = [...new Set(graphData().nodes.map((n) => n.kind))].sort(); - const el = document.getElementById('legend'); - if (!kinds.length) { - el.innerHTML = ''; - return; - } - el.innerHTML = kinds.map((k) => kindTag(k)).join(''); -} - -// ── Table ──────────────────────────────────────────── - -function renderTable() { - const el = document.getElementById('table-view'); - const data = graphData(); - if (!data.nodes.length) { - el.innerHTML = '

No nodes yet — try Search or Load

'; - return; - } - const sorted = [...data.nodes].sort((a, b) => a.name.localeCompare(b.name)); - const rows = sorted - .map( - (n) => ` - ${kindTag(n.kind)}${escapeHtml(n.name)} - ${escapeHtml(shortPath(n.raw.file || ''))} - ` - ) - .join(''); - el.innerHTML = `${rows}
NameFile
`; - el.querySelectorAll('tbody tr').forEach((tr) => { - tr.addEventListener('click', () => selectNode(Number(tr.dataset.id))); - }); -} - -function shortPath(p) { - const parts = String(p).split(/[/\\]/); - return parts.length > 3 ? '…/' + parts.slice(-2).join('/') : p; -} - -// ── 2D graph ───────────────────────────────────────── - -function initGraph2d() { - const el = document.getElementById('graph2d-view'); - const fg = ForceGraph()(el) - .backgroundColor('rgba(0,0,0,0)') - .nodeLabel((n) => `
${n.name}
${n.kind}
`) - .nodeColor((n) => n.color) - .nodeVal((n) => n.val) - .nodeRelSize(5) - .linkColor(linkColorFn) - .linkWidth(linkWidthFn) - .linkDirectionalArrowLength(4) - .linkDirectionalArrowRelPos(1) - .linkDirectionalParticles(2) - .linkDirectionalParticleWidth(2) - .linkDirectionalParticleSpeed(0.004) - .d3AlphaDecay(0.015) - .d3VelocityDecay(0.25) - .warmupTicks(80) - .cooldownTicks(120) - .enableNodeDrag(true) - .onNodeClick((n) => selectNode(n.id)) - .onNodeHover((n) => { - state.hoverId = n ? n.id : null; - el.style.cursor = n ? 'pointer' : null; - refreshGraphStyles(); - }) - .nodeCanvasObjectMode((n) => (n.id === state.selectedId ? 'after' : undefined)) - .nodeCanvasObject((n, ctx, globalScale) => { - if (n.id !== state.selectedId) return; - const r = Math.sqrt(n.val) * 5 + 4; - ctx.beginPath(); - ctx.arc(n.x, n.y, r / globalScale, 0, 2 * Math.PI); - ctx.fillStyle = n.color + '33'; - ctx.fill(); - ctx.strokeStyle = n.color; - ctx.lineWidth = 2 / globalScale; - ctx.stroke(); - }); - - state.graph2d = fg; - resizeGraphs(); -} - -function renderGraph2d() { - const data = graphData(); - if (!state.graph2d) initGraph2d(); - state.graph2d.graphData(data); - if (!state.paused) { - setTimeout(() => { - if (state.activeView === 'graph2d' && state.graph2d) { - state.graph2d.zoomToFit(500, 60); - } - }, 600); - } -} - -// ── 3D graph ───────────────────────────────────────── - -function initGraph3d() { - const el = document.getElementById('graph3d-view'); - const fg = ForceGraph3D()(el) - .backgroundColor('rgba(0,0,0,0)') - .showNavInfo(false) - .enableNodeDrag(false) - .nodeLabel((n) => `${n.kind}: ${n.name}`) - .nodeColor((n) => n.color) - .nodeVal(nodeVal3d) - .nodeOpacity(0.9) - .nodeResolution(7) - .linkColor(linkColorFn3d) - .linkWidth(0.35) - .linkOpacity(0.55) - .linkDirectionalParticles(0) - .linkDirectionalArrowLength(0) - .d3AlphaDecay(0.028) - .d3VelocityDecay(0.35) - .warmupTicks(40) - .cooldownTicks(30) - .onNodeClick((n) => selectNode(n.id)) - .onNodeHover((n) => { - state.hoverId = n ? n.id : null; - el.style.cursor = n ? 'pointer' : null; - schedule3dLinkRefresh(); - }) - .onEngineStop(() => { - pause3dPhysicsIfIdle(); - if (state.activeView === 'graph3d' && !state._didFit3d) { - state._didFit3d = true; - fg.zoomToFit(500, 80); - } - }); - - const renderer = fg.renderer(); - if (renderer) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.35)); - - state.graph3d = fg; - resizeGraphs(); -} - -function refreshGraphStyles() { - if (state.graph2d) { - state.graph2d - .nodeVal((n) => nodeVal(n)) - .linkColor(linkColorFn) - .linkWidth(linkWidthFn) - .nodeCanvasObjectMode((n) => (n.id === state.selectedId ? 'after' : undefined)); - } - if (state.graph3d) { - state.graph3d.nodeVal(nodeVal3d).linkColor(linkColorFn3d); - } -} - -function renderGraph3d() { - const data = graphData(); - if (!state.graph3d) initGraph3d(); - applyGraph3dPerf(state.graph3d, data.nodes.length, data.links.length); - state._didFit3d = false; - state._3dPhysicsDone = false; - state.graph3d.resumeAnimation(); - state.graph3d.graphData(data); -} - -function focusSelected3d() { - if (!state.graph3d || !state.selectedId) return; - const data = state.graph3d.graphData(); - const node = data.nodes.find((n) => n.id === state.selectedId); - if (!node || node.x == null) return; - const dist = 120; - state.graph3d.cameraPosition( - { x: node.x, y: node.y, z: node.z + dist }, - node, - 1200 - ); -} - -function toggleRotate3d() { - const btn = document.getElementById('btn-rotate'); - if (state.rotate3d) { - if (state.rotateRaf) cancelAnimationFrame(state.rotateRaf); - state.rotateRaf = null; - state.rotate3d = false; - btn.classList.remove('active'); - pause3dPhysicsIfIdle(); - return; - } - state.rotate3d = true; - btn.classList.add('active'); - if (state.graph3d && state._3dPhysicsDone) state.graph3d.resumeAnimation(); - let angle = 0; - let last = performance.now(); - const spin = (now) => { - if (!state.rotate3d || !state.graph3d) return; - const dt = Math.min(now - last, 50); - last = now; - angle += dt * 0.00035; - const dist = 280; - state.graph3d.cameraPosition({ - x: dist * Math.sin(angle), - y: dist * 0.35, - z: dist * Math.cos(angle), - }); - state.rotateRaf = requestAnimationFrame(spin); - }; - state.rotateRaf = requestAnimationFrame(spin); -} - -// ── Detail panel ───────────────────────────────────── - -function renderDetail() { - const content = document.getElementById('detail-content'); - const actions = document.getElementById('detail-actions'); - const id = state.selectedId; - if (!id) { - content.innerHTML = '

Click a node to inspect

'; - actions.classList.add('hidden'); - return; - } - const n = graphData().nodes.find((x) => x.id === id)?.raw; - if (!n) { - content.innerHTML = '

Loading…

'; - api(`/api/symbol/${id}`).then(showDetail); - return; - } - showDetail(n); -} - -function showDetail(n) { - const content = document.getElementById('detail-content'); - document.getElementById('detail-actions').classList.remove('hidden'); - content.innerHTML = ` - ${kindTag(n.kind)} -
${escapeHtml(n.name)}
- ${escapeHtml(n.file)}:${n.line} - ${n.signature ? `${escapeHtml(n.signature)}` : ''} - ${n.doc ? `

${escapeHtml(n.doc)}

` : ''} -
- `; - // Call chain (flow) — marker + callee names, hiển thị tối giản. - api(`/api/flow/${n.id}`) - .then((f) => { - const el = document.getElementById('flow-chain'); - if (!el) return; - const desc = f.chain_desc || []; - if (!desc.length) return; - el.innerHTML = - '
Flow
' + - desc.map(escapeHtml).join(' → ') + - ''; - }) - .catch(() => {}); -} - -function selectNode(id) { - state.selectedId = id; - refreshGraphStyles(); - renderAll(); - if (state.activeView === 'graph3d') focusSelected3d(); -} - -// ── Render orchestration ───────────────────────────── - -function renderAll() { - document.getElementById('truncated-badge').classList.toggle('hidden', !state.graph.truncated); - updateGraphCounts(); - renderLegend(); - if (state.activeView === 'table') renderTable(); - if (state.activeView === 'graph2d') renderGraph2d(); - if (state.activeView === 'graph3d') renderGraph3d(); - renderDetail(); -} - -function resizeGraphs() { - const panel = document.getElementById('panel-left'); - const w = panel.clientWidth; - const h = panel.clientHeight; - if (state.graph2d) state.graph2d.width(w).height(h); - if (state.graph3d) state.graph3d.width(w).height(h); -} - -// ── Data loading ───────────────────────────────────── - -async function loadSubgraph(opts = {}) { - const depth = Number(document.getElementById('depth-input').value) || state.boot.depth || 2; - const params = new URLSearchParams({ depth: String(depth) }); - - if (opts.seed != null) params.set('seed', String(opts.seed)); - else if (opts.query) params.set('query', opts.query); - else if (opts.prefix !== undefined) params.set('prefix', opts.prefix); - else if (state.selectedId != null) params.set('seed', String(state.selectedId)); - else { - const q = document.getElementById('search-input').value.trim(); - if (q) params.set('query', q); - else if (state.boot.target) params.set('query', state.boot.target); - else if (state.boot.prefix) params.set('prefix', state.boot.prefix); - } - - setLoading(true); - try { - const data = await api(`/api/subgraph?${params}`); - state.graph = data; - if (data.seed) state.selectedId = data.seed.id; - renderAll(); - } catch (err) { - document.getElementById('detail-content').innerHTML = - `

${escapeHtml(err.message)}

`; - } finally { - setLoading(false); - } -} - -async function loadStatus() { - try { - const s = await api('/api/status'); - document.getElementById('status-bar').textContent = - `${s.files.toLocaleString()} files · ${s.symbols.toLocaleString()} symbols · ${s.chains.toLocaleString()} chains · ${s.edges.toLocaleString()} edges`; - } catch (_) {} -} - -async function doSearch() { - const q = document.getElementById('search-input').value.trim(); - if (!q) { - document.getElementById('search-results').classList.add('hidden'); - return; - } - const hits = await api(`/api/search?q=${encodeURIComponent(q)}&limit=30`); - const panel = document.getElementById('search-results'); - if (!hits.length) { - panel.innerHTML = '
No results
'; - panel.classList.remove('hidden'); - return; - } - panel.innerHTML = hits - .map( - (h) => - `
- ${kindTag(h.kind)}${escapeHtml(h.name)} - — ${escapeHtml(shortPath(h.file))} -
` - ) - .join(''); - panel.classList.remove('hidden'); - panel.querySelectorAll('.item').forEach((el) => { - el.addEventListener('click', () => { - panel.classList.add('hidden'); - if (el.dataset.id) loadSubgraph({ seed: Number(el.dataset.id) }); - }); - }); -} - -function mergeHits(hits, rootId) { - const ids = new Set(state.graph.nodes.map((n) => n.id)); - if (state.graph.seed) ids.add(state.graph.seed.id); - hits.nodes.forEach((n) => { - if (!ids.has(n.id)) { - state.graph.nodes.push(n); - ids.add(n.id); - } - }); - const edgeKey = (e) => `${e.from}-${e.to}-${e.kind}`; - const keys = new Set(state.graph.edges.map(edgeKey)); - hits.edges.forEach((e) => { - if (!keys.has(edgeKey(e))) state.graph.edges.push(e); - }); - state.graph.truncated = state.graph.truncated || hits.truncated; - state.selectedId = rootId; - renderAll(); - if (state.activeView === 'graph2d' && state.graph2d) { - setTimeout(() => state.graph2d.zoomToFit(400, 60), 400); - } - if (state.activeView === 'graph3d' && state.graph3d) { - const data = graphData(); - applyGraph3dPerf(state.graph3d, data.nodes.length, data.links.length); - state._3dPhysicsDone = false; - state.graph3d.resumeAnimation(); - } -} - -function escapeHtml(s) { - return String(s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); -} - -// ── View switching ─────────────────────────────────── - -function setView(view) { - state.activeView = view; - const isGraph = view === 'graph2d' || view === 'graph3d'; - - document.querySelectorAll('#view-tabs button').forEach((b) => { - b.classList.toggle('active', b.dataset.view === view); - }); - document.querySelectorAll('.view').forEach((v) => v.classList.remove('active')); - const map = { table: 'table-view', graph2d: 'graph2d-view', graph3d: 'graph3d-view' }; - document.getElementById(map[view]).classList.add('active'); - - document.getElementById('graph-hud').classList.toggle('hidden', !isGraph); - document.getElementById('btn-rotate').classList.toggle('hidden', view !== 'graph3d'); - - if (view !== 'graph3d' && state.rotate3d) toggleRotate3d(); - - renderAll(); - requestAnimationFrame(resizeGraphs); -} - -function fitActiveGraph() { - if (state.activeView === 'graph2d' && state.graph2d) { - state.graph2d.zoomToFit(400, 60); - } else if (state.activeView === 'graph3d' && state.graph3d) { - state.graph3d.zoomToFit(500, 80); - } -} - -function togglePause() { - const btn = document.getElementById('btn-pause'); - state.paused = !state.paused; - btn.textContent = state.paused ? '▶ Resume' : '⏸ Pause'; - btn.classList.toggle('active', state.paused); - if (state.graph2d) { - if (state.paused) state.graph2d.pauseAnimation(); - else state.graph2d.resumeAnimation(); - } - if (state.graph3d) { - if (state.paused) { - state.graph3d.pauseAnimation(); - } else { - state.graph3d.resumeAnimation(); - if (state._3dPhysicsDone && !state.rotate3d) { - setTimeout(pause3dPhysicsIfIdle, 2500); - } - } - } -} - -// ── Events ─────────────────────────────────────────── - -document.getElementById('view-tabs').addEventListener('click', (e) => { - const btn = e.target.closest('button'); - if (btn) setView(btn.dataset.view); -}); - -document.getElementById('search-input').addEventListener('keydown', (e) => { - if (e.key === 'Enter') doSearch(); -}); -document.getElementById('search-input').addEventListener('input', () => { - clearTimeout(state._searchTimer); - state._searchTimer = setTimeout(doSearch, 280); -}); -document.addEventListener('click', (e) => { - if (!e.target.closest('#search-bar') && !e.target.closest('#search-results')) { - document.getElementById('search-results').classList.add('hidden'); - } -}); - -document.getElementById('reload-btn').addEventListener('click', () => loadSubgraph()); -document.getElementById('btn-fit').addEventListener('click', fitActiveGraph); -document.getElementById('btn-pause').addEventListener('click', togglePause); -document.getElementById('btn-rotate').addEventListener('click', toggleRotate3d); -document.getElementById('btn-focus').addEventListener('click', () => { - if (state.activeView === 'graph3d') focusSelected3d(); - else if (state.graph2d && state.selectedId) { - const n = state.graph2d.graphData().nodes.find((x) => x.id === state.selectedId); - if (n) state.graph2d.centerAt(n.x, n.y, 800); - } -}); - -document.getElementById('btn-callers').addEventListener('click', async () => { - if (!state.selectedId) return; - const depth = Number(document.getElementById('depth-input').value) || 1; - mergeHits(await api(`/api/callers/${state.selectedId}?depth=${depth}`), state.selectedId); -}); -document.getElementById('btn-callees').addEventListener('click', async () => { - if (!state.selectedId) return; - const depth = Number(document.getElementById('depth-input').value) || 1; - mergeHits(await api(`/api/callees/${state.selectedId}?depth=${depth}`), state.selectedId); -}); -document.getElementById('btn-expand').addEventListener('click', async () => { - if (!state.selectedId) return; - mergeHits(await api(`/api/neighbors/${state.selectedId}?depth=1`), state.selectedId); -}); - -new ResizeObserver(resizeGraphs).observe(document.getElementById('panel-left')); - -async function init() { - try { - state.boot = await api('/api/boot'); - if (state.boot.depth) document.getElementById('depth-input').value = state.boot.depth; - if (state.boot.target) document.getElementById('search-input').value = state.boot.target; - } catch (_) { - state.boot = { depth: 2 }; - } - await loadStatus(); - await loadSubgraph(); -} - -init(); diff --git a/crates/codegraph-viz/assets/index.html b/crates/codegraph-viz/assets/index.html deleted file mode 100644 index 996964a31..000000000 --- a/crates/codegraph-viz/assets/index.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - CodeGraph Visualize - - - -
-
- -
-

CodeGraph

-

knowledge graph explorer

-
-
- - - - - -
- - - - -
-
- -
-
- - - - - - -
-
-
-
- - -
- - - - - - diff --git a/crates/codegraph-viz/assets/styles.css b/crates/codegraph-viz/assets/styles.css deleted file mode 100644 index 94000356c..000000000 --- a/crates/codegraph-viz/assets/styles.css +++ /dev/null @@ -1,453 +0,0 @@ -:root { - --bg: #090b10; - --bg2: #0e1118; - --surface: #141820; - --surface2: #1c2230; - --border: #2a3142; - --border-light: #3d465c; - --text: #eef1f8; - --muted: #8b95ab; - --accent: #5eead4; - --accent-dim: #5eead433; - --accent2: #818cf8; - --accent2-dim: #818cf844; - --warn: #fbbf24; - --radius: 10px; - --shadow: 0 8px 32px #00000066; - --transition: 0.18s ease; -} - -* { box-sizing: border-box; } - -body { - margin: 0; - font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif; - background: var(--bg); - color: var(--text); - height: 100vh; - display: flex; - flex-direction: column; - overflow: hidden; -} - -/* ── Header ── */ -header { - display: flex; - flex-wrap: wrap; - gap: 0.75rem 1rem; - align-items: center; - padding: 0.65rem 1.1rem; - border-bottom: 1px solid var(--border); - background: linear-gradient(180deg, var(--surface) 0%, var(--bg2) 100%); - backdrop-filter: blur(12px); - z-index: 20; -} - -.brand { - display: flex; - align-items: center; - gap: 0.55rem; - min-width: 140px; -} -.logo { - font-size: 1.5rem; - color: var(--accent); - filter: drop-shadow(0 0 8px var(--accent-dim)); -} -.brand h1 { - margin: 0; - font-size: 1rem; - font-weight: 700; - letter-spacing: -0.02em; - background: linear-gradient(135deg, var(--accent), var(--accent2)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} -.tagline { - margin: 0; - font-size: 0.65rem; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.seg-control { - display: flex; - background: var(--bg); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 3px; - gap: 2px; -} -.seg-control button { - background: transparent; - border: none; - color: var(--muted); - padding: 0.38rem 0.75rem; - border-radius: 7px; - cursor: pointer; - font-size: 0.8rem; - font-weight: 500; - transition: all var(--transition); -} -.seg-control button:hover { color: var(--text); background: #ffffff08; } -.seg-control button.active { - background: var(--accent2-dim); - color: var(--accent2); - box-shadow: 0 0 12px #818cf822; -} - -#search-bar { - display: flex; - align-items: center; - flex: 1; - min-width: 180px; - background: var(--bg); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 0 0.65rem; - transition: border-color var(--transition), box-shadow var(--transition); -} -#search-bar:focus-within { - border-color: var(--accent2); - box-shadow: 0 0 0 3px var(--accent2-dim); -} -.search-icon { color: var(--muted); font-size: 1rem; margin-right: 0.4rem; } -#search-input { - flex: 1; - background: transparent; - border: none; - color: var(--text); - padding: 0.5rem 0; - font-size: 0.875rem; - outline: none; -} -#search-input::placeholder { color: #5c6578; } - -#controls { - display: flex; - gap: 0.5rem; - align-items: center; - flex-wrap: wrap; -} -.depth-wrap { - display: flex; - align-items: center; - gap: 0.35rem; - font-size: 0.75rem; - color: var(--muted); - background: var(--bg); - border: 1px solid var(--border); - border-radius: 8px; - padding: 0.25rem 0.5rem; -} -#depth-input { - width: 2.2rem; - background: transparent; - border: none; - color: var(--text); - font-size: 0.85rem; - text-align: center; - outline: none; -} - -button { font-family: inherit; cursor: pointer; } -.btn-primary { - background: linear-gradient(135deg, #6366f1, #818cf8); - border: none; - color: #fff; - padding: 0.45rem 0.9rem; - border-radius: 8px; - font-size: 0.8rem; - font-weight: 600; - transition: transform var(--transition), box-shadow var(--transition); - box-shadow: 0 2px 12px #6366f144; -} -.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 16px #6366f166; } -.btn-primary:active { transform: translateY(0); } - -.btn-ghost { - background: #ffffff0a; - border: 1px solid var(--border); - color: var(--text); - padding: 0.3rem 0.55rem; - border-radius: 6px; - font-size: 0.72rem; - transition: background var(--transition); -} -.btn-ghost:hover { background: #ffffff14; } -.btn-ghost.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); } - -.chip { - font-size: 0.7rem; - padding: 0.2rem 0.55rem; - border-radius: 999px; - background: var(--surface2); - border: 1px solid var(--border); - color: var(--muted); -} -.chip.warn { - background: #f59e0b18; - border-color: #f59e0b44; - color: var(--warn); -} - -.hidden { display: none !important; } - -/* ── Main layout ── */ -main { - flex: 1; - display: grid; - grid-template-columns: 1fr 300px; - min-height: 0; -} - -#panel-left { - position: relative; - min-height: 0; - background: - radial-gradient(ellipse 80% 60% at 50% 0%, #818cf808 0%, transparent 70%), - var(--bg); -} - -.view { - display: none; - height: 100%; - overflow: auto; -} -.view.active { display: block; } -.graph-canvas.active { - display: block; - height: 100%; - background: - radial-gradient(circle at 50% 50%, #141820 0%, #090b10 100%); -} - -/* ── Loading ── */ -.loading { - position: absolute; - inset: 0; - z-index: 30; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.75rem; - background: #090b10cc; - backdrop-filter: blur(4px); - color: var(--muted); - font-size: 0.85rem; -} -.spinner { - width: 32px; - height: 32px; - border: 3px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: spin 0.7s linear infinite; -} -@keyframes spin { to { transform: rotate(360deg); } } - -/* ── Graph HUD ── */ -.graph-hud { - position: absolute; - bottom: 1rem; - left: 50%; - transform: translateX(-50%); - z-index: 15; - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.45rem 0.65rem; - background: #141820ee; - border: 1px solid var(--border); - border-radius: 999px; - backdrop-filter: blur(12px); - box-shadow: var(--shadow); -} -.hud-stats { font-size: 0.72rem; color: var(--muted); white-space: nowrap; } -.hud-actions { display: flex; gap: 0.3rem; } - -/* ── Table ── */ -#table-view { padding: 0.5rem; } -#table-view table { - width: 100%; - border-collapse: collapse; - font-size: 0.82rem; -} -#table-view thead { - position: sticky; - top: 0; - z-index: 5; -} -#table-view th { - text-align: left; - padding: 0.55rem 0.75rem; - background: var(--surface); - border-bottom: 1px solid var(--border); - color: var(--muted); - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.06em; - font-weight: 600; -} -#table-view td { - padding: 0.5rem 0.75rem; - border-bottom: 1px solid #ffffff06; -} -#table-view tr { - cursor: pointer; - transition: background var(--transition); -} -#table-view tbody tr:hover { background: #ffffff06; } -#table-view tr.selected { - background: var(--accent-dim); - box-shadow: inset 3px 0 0 var(--accent); -} - -/* ── Search results dropdown ── */ -.list-panel { - position: absolute; - top: 0.5rem; - left: 0.5rem; - right: 0.5rem; - max-height: 45%; - overflow: auto; - background: #141820f5; - border: 1px solid var(--border); - border-radius: var(--radius); - z-index: 25; - box-shadow: var(--shadow); - backdrop-filter: blur(16px); - animation: slideDown 0.2s ease; -} -@keyframes slideDown { - from { opacity: 0; transform: translateY(-8px); } - to { opacity: 1; transform: translateY(0); } -} -.list-panel .item { - padding: 0.55rem 0.85rem; - cursor: pointer; - border-bottom: 1px solid #ffffff06; - font-size: 0.82rem; - transition: background var(--transition); -} -.list-panel .item:hover { background: var(--accent2-dim); } -.list-panel .item:last-child { border-bottom: none; } - -/* ── Detail panel ── */ -#detail-panel { - border-left: 1px solid var(--border); - background: var(--surface); - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; -} -.panel-header { - padding: 0.85rem 1rem 0.5rem; - border-bottom: 1px solid var(--border); -} -.panel-header h2 { - margin: 0; - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--muted); - font-weight: 600; -} -.detail-card { - flex: 1; - overflow: auto; - padding: 0.85rem 1rem; - font-size: 0.82rem; - line-height: 1.55; -} -.detail-card .node-title { - font-size: 1rem; - font-weight: 700; - margin: 0.35rem 0; - color: var(--text); -} -.detail-card code { - display: block; - font-size: 0.75rem; - word-break: break-all; - background: var(--bg); - border: 1px solid var(--border); - border-radius: 6px; - padding: 0.4rem 0.55rem; - margin: 0.4rem 0; - color: #a5b4fc; -} -.empty-hint { text-align: center; padding: 2rem 0; } - -.action-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.4rem; - padding: 0 1rem 0.75rem; -} -.btn-action { - background: var(--bg); - border: 1px solid var(--border); - color: var(--text); - padding: 0.45rem; - border-radius: 8px; - font-size: 0.72rem; - font-weight: 500; - transition: all var(--transition); -} -.btn-action:hover { - border-color: var(--accent2); - background: var(--accent2-dim); - color: var(--accent2); -} - -.legend { - padding: 0.5rem 1rem 0.75rem; - display: flex; - flex-wrap: wrap; - gap: 0.35rem; -} -.status-bar { - margin-top: auto; - padding: 0.65rem 1rem; - border-top: 1px solid var(--border); - color: var(--muted); - font-size: 0.7rem; - background: var(--bg2); -} - -.muted { color: var(--muted); } - -.kind-tag { - display: inline-block; - padding: 0.12rem 0.4rem; - border-radius: 4px; - font-size: 0.65rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - margin-right: 0.35rem; - vertical-align: middle; -} - -.flow-title { - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--muted); - margin-bottom: 0.25rem; -} - -.flow-line { - display: block; - white-space: pre-wrap; - word-break: break-word; - font-size: 0.72rem; - line-height: 1.5; - color: var(--fg); -} diff --git a/crates/codegraph-viz/assets/vendor/3d-force-graph.min.js b/crates/codegraph-viz/assets/vendor/3d-force-graph.min.js deleted file mode 100644 index 217413497..000000000 --- a/crates/codegraph-viz/assets/vendor/3d-force-graph.min.js +++ /dev/null @@ -1,5 +0,0 @@ -// Version 1.73.3 3d-force-graph - https://github.com/vasturiano/3d-force-graph -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).ForceGraph3D=e()}(this,(function(){"use strict";function t(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function e(e){for(var n=1;nt.length)&&(e=t.length);for(var n=0,i=new Array(e);n>8&255]+gt[t>>16&255]+gt[t>>24&255]+"-"+gt[255&e]+gt[e>>8&255]+"-"+gt[e>>16&15|64]+gt[e>>24&255]+"-"+gt[63&n|128]+gt[n>>8&255]+"-"+gt[n>>16&255]+gt[n>>24&255]+gt[255&i]+gt[i>>8&255]+gt[i>>16&255]+gt[i>>24&255]).toLowerCase()}function bt(t,e,n){return Math.max(e,Math.min(n,t))}function Mt(t,e){return(t%e+e)%e}function St(t,e,n){return(1-n)*t+n*e}function Et(t){return!(t&t-1)&&0!==t}function wt(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))}function Tt(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("Invalid component type.")}}function At(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("Invalid component type.")}}const Rt={DEG2RAD:_t,RAD2DEG:yt,generateUUID:xt,clamp:bt,euclideanModulo:Mt,mapLinear:function(t,e,n,i,r){return i+(t-e)*(r-i)/(n-e)},inverseLerp:function(t,e,n){return t!==e?(n-t)/(e-t):0},lerp:St,damp:function(t,e,n,i){return St(t,e,1-Math.exp(-n*i))},pingpong:function(t,e=1){return e-Math.abs(Mt(t,2*e)-e)},smoothstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*(3-2*t)},smootherstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-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&&(vt=t);let e=vt+=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*_t},radToDeg:function(t){return t*yt},isPowerOfTwo:Et,ceilPowerOfTwo:function(t){return Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))},floorPowerOfTwo:wt,setQuaternionFromProperEuler:function(t,e,n,i,r){const a=Math.cos,o=Math.sin,s=a(n/2),l=o(n/2),c=a((e+i)/2),u=o((e+i)/2),h=a((e-i)/2),d=o((e-i)/2),p=a((i-e)/2),f=o((i-e)/2);switch(r){case"XYX":t.set(s*u,l*h,l*d,s*c);break;case"YZY":t.set(l*d,s*u,l*h,s*c);break;case"ZXZ":t.set(l*h,l*d,s*u,s*c);break;case"XZX":t.set(s*u,l*f,l*p,s*c);break;case"YXY":t.set(l*p,s*u,l*f,s*c);break;case"ZYZ":t.set(l*f,l*p,s*u,s*c);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+r)}},normalize:At,denormalize:Tt};class Ct{constructor(t=0,e=0){Ct.prototype.isVector2=!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("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("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,n=this.y,i=t.elements;return this.x=i[0]*e+i[3]*n+i[6],this.y=i[1]*e+i[4]*n+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=Math.max(t.x,Math.min(e.x,this.x)),this.y=Math.max(t.y,Math.min(e.y,this.y)),this}clampScalar(t,e){return this.x=Math.max(t,Math.min(e,this.x)),this.y=Math.max(t,Math.min(e,this.y)),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(t,Math.min(e,n)))}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 n=this.dot(t)/e;return Math.acos(bt(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y;return e*e+n*n}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,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,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 n=Math.cos(e),i=Math.sin(e),r=this.x-t.x,a=this.y-t.y;return this.x=r*n-a*i+t.x,this.y=r*i+a*n+t.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}}class Pt{constructor(t,e,n,i,r,a,o,s,l){Pt.prototype.isMatrix3=!0,this.elements=[1,0,0,0,1,0,0,0,1],void 0!==t&&this.set(t,e,n,i,r,a,o,s,l)}set(t,e,n,i,r,a,o,s,l){const c=this.elements;return c[0]=t,c[1]=i,c[2]=o,c[3]=e,c[4]=r,c[5]=s,c[6]=n,c[7]=a,c[8]=l,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],this}extractBasis(t,e,n){return t.setFromMatrix3Column(this,0),e.setFromMatrix3Column(this,1),n.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 n=t.elements,i=e.elements,r=this.elements,a=n[0],o=n[3],s=n[6],l=n[1],c=n[4],u=n[7],h=n[2],d=n[5],p=n[8],f=i[0],m=i[3],g=i[6],v=i[1],_=i[4],y=i[7],x=i[2],b=i[5],M=i[8];return r[0]=a*f+o*v+s*x,r[3]=a*m+o*_+s*b,r[6]=a*g+o*y+s*M,r[1]=l*f+c*v+u*x,r[4]=l*m+c*_+u*b,r[7]=l*g+c*y+u*M,r[2]=h*f+d*v+p*x,r[5]=h*m+d*_+p*b,r[8]=h*g+d*y+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],n=t[1],i=t[2],r=t[3],a=t[4],o=t[5],s=t[6],l=t[7],c=t[8];return e*a*c-e*o*l-n*r*c+n*o*s+i*r*l-i*a*s}invert(){const t=this.elements,e=t[0],n=t[1],i=t[2],r=t[3],a=t[4],o=t[5],s=t[6],l=t[7],c=t[8],u=c*a-o*l,h=o*s-c*r,d=l*r-a*s,p=e*u+n*h+i*d;if(0===p)return this.set(0,0,0,0,0,0,0,0,0);const f=1/p;return t[0]=u*f,t[1]=(i*l-c*n)*f,t[2]=(o*n-i*a)*f,t[3]=h*f,t[4]=(c*e-i*s)*f,t[5]=(i*r-o*e)*f,t[6]=d*f,t[7]=(n*s-l*e)*f,t[8]=(a*e-n*r)*f,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,n,i,r,a,o){const s=Math.cos(r),l=Math.sin(r);return this.set(n*s,n*l,-n*(s*a+l*o)+a+t,-i*l,i*s,-i*(-l*a+s*o)+o+e,0,0,1),this}scale(t,e){return this.premultiply(Lt.makeScale(t,e)),this}rotate(t){return this.premultiply(Lt.makeRotation(-t)),this}translate(t,e){return this.premultiply(Lt.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),n=Math.sin(t);return this.set(e,-n,0,n,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,n=t.elements;for(let t=0;t<9;t++)if(e[t]!==n[t])return!1;return!0}fromArray(t,e=0){for(let n=0;n<9;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t}clone(){return(new this.constructor).fromArray(this.elements)}}const Lt=new Pt;function Ot(t){for(let e=t.length-1;e>=0;--e)if(t[e]>=65535)return!0;return!1}function Dt(t){return document.createElementNS("http://www.w3.org/1999/xhtml",t)}function Nt(){const t=Dt("canvas");return t.style.display="block",t}const It={};const Ut=(new Pt).set(.8224621,.177538,0,.0331941,.9668058,0,.0170827,.0723974,.9105199),Ft=(new Pt).set(1.2249401,-.2249404,0,-.0420569,1.0420571,0,-.0196376,-.0786361,1.0982735),kt={[it]:{transfer:ot,primaries:lt,toReference:t=>t,fromReference:t=>t},[nt]:{transfer:st,primaries:lt,toReference:t=>t.convertSRGBToLinear(),fromReference:t=>t.convertLinearToSRGB()},[at]:{transfer:ot,primaries:ct,toReference:t=>t.applyMatrix3(Ft),fromReference:t=>t.applyMatrix3(Ut)},[rt]:{transfer:st,primaries:ct,toReference:t=>t.convertSRGBToLinear().applyMatrix3(Ft),fromReference:t=>t.applyMatrix3(Ut).convertLinearToSRGB()}},zt=new Set([it,at]),Bt={enabled:!0,_workingColorSpace:it,get workingColorSpace(){return this._workingColorSpace},set workingColorSpace(t){if(!zt.has(t))throw new Error(`Unsupported working color space, "${t}".`);this._workingColorSpace=t},convert:function(t,e,n){if(!1===this.enabled||e===n||!e||!n)return t;const i=kt[e].toReference;return(0,kt[n].fromReference)(i(t))},fromWorkingColorSpace:function(t,e){return this.convert(t,this._workingColorSpace,e)},toWorkingColorSpace:function(t,e){return this.convert(t,e,this._workingColorSpace)},getPrimaries:function(t){return kt[t].primaries},getTransfer:function(t){return t===et?ot:kt[t].transfer}};function Ht(t){return t<.04045?.0773993808*t:Math.pow(.9478672986*t+.0521327014,2.4)}function Gt(t){return t<.0031308?12.92*t:1.055*Math.pow(t,.41666)-.055}let Vt;class jt{static getDataURL(t){if(/^data:/i.test(t.src))return t.src;if("undefined"==typeof HTMLCanvasElement)return t.src;let e;if(t instanceof HTMLCanvasElement)e=t;else{void 0===Vt&&(Vt=Dt("canvas")),Vt.width=t.width,Vt.height=t.height;const n=Vt.getContext("2d");t instanceof ImageData?n.putImageData(t,0,0):n.drawImage(t,0,0,t.width,t.height),e=Vt}return e.width>2048||e.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",t),e.toDataURL("image/jpeg",.6)):e.toDataURL("image/png")}static sRGBToLinear(t){if("undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap){const e=Dt("canvas");e.width=t.width,e.height=t.height;const n=e.getContext("2d");n.drawImage(t,0,0,t.width,t.height);const i=n.getImageData(0,0,t.width,t.height),r=i.data;for(let t=0;t0&&(n.userData=this.userData),e||(t.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(t){if(300!==this.mapping)return t;if(t.applyMatrix3(this.matrix),t.x<0||t.x>1)switch(this.wrapS){case O:t.x=t.x-Math.floor(t.x);break;case D:t.x=t.x<0?0:1;break;case N: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 O:t.y=t.y-Math.floor(t.y);break;case D:t.y=t.y<0?0:1;break;case N: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)}}$t.DEFAULT_IMAGE=null,$t.DEFAULT_MAPPING=300,$t.DEFAULT_ANISOTROPY=1;class Kt{constructor(t=0,e=0,n=0,i=1){Kt.prototype.isVector4=!0,this.x=t,this.y=e,this.z=n,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,n,i){return this.x=t,this.y=e,this.z=n,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("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("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,n=this.y,i=this.z,r=this.w,a=t.elements;return this.x=a[0]*e+a[4]*n+a[8]*i+a[12]*r,this.y=a[1]*e+a[5]*n+a[9]*i+a[13]*r,this.z=a[2]*e+a[6]*n+a[10]*i+a[14]*r,this.w=a[3]*e+a[7]*n+a[11]*i+a[15]*r,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,n,i,r;const a=.01,o=.1,s=t.elements,l=s[0],c=s[4],u=s[8],h=s[1],d=s[5],p=s[9],f=s[2],m=s[6],g=s[10];if(Math.abs(c-h)s&&t>v?tv?s=0?1:-1,i=1-e*e;if(i>Number.EPSILON){const r=Math.sqrt(i),a=Math.atan2(r,e*n);t=Math.sin(t*a)/r,o=Math.sin(o*a)/r}const r=o*n;if(s=s*t+h*r,l=l*t+d*r,c=c*t+p*r,u=u*t+f*r,t===1-o){const t=1/Math.sqrt(s*s+l*l+c*c+u*u);s*=t,l*=t,c*=t,u*=t}}t[e]=s,t[e+1]=l,t[e+2]=c,t[e+3]=u}static multiplyQuaternionsFlat(t,e,n,i,r,a){const o=n[i],s=n[i+1],l=n[i+2],c=n[i+3],u=r[a],h=r[a+1],d=r[a+2],p=r[a+3];return t[e]=o*p+c*u+s*d-l*h,t[e+1]=s*p+c*h+l*u-o*d,t[e+2]=l*p+c*d+o*h-s*u,t[e+3]=c*p-o*u-s*h-l*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,n,i){return this._x=t,this._y=e,this._z=n,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 n=t._x,i=t._y,r=t._z,a=t._order,o=Math.cos,s=Math.sin,l=o(n/2),c=o(i/2),u=o(r/2),h=s(n/2),d=s(i/2),p=s(r/2);switch(a){case"XYZ":this._x=h*c*u+l*d*p,this._y=l*d*u-h*c*p,this._z=l*c*p+h*d*u,this._w=l*c*u-h*d*p;break;case"YXZ":this._x=h*c*u+l*d*p,this._y=l*d*u-h*c*p,this._z=l*c*p-h*d*u,this._w=l*c*u+h*d*p;break;case"ZXY":this._x=h*c*u-l*d*p,this._y=l*d*u+h*c*p,this._z=l*c*p+h*d*u,this._w=l*c*u-h*d*p;break;case"ZYX":this._x=h*c*u-l*d*p,this._y=l*d*u+h*c*p,this._z=l*c*p-h*d*u,this._w=l*c*u+h*d*p;break;case"YZX":this._x=h*c*u+l*d*p,this._y=l*d*u+h*c*p,this._z=l*c*p-h*d*u,this._w=l*c*u-h*d*p;break;case"XZY":this._x=h*c*u-l*d*p,this._y=l*d*u-h*c*p,this._z=l*c*p+h*d*u,this._w=l*c*u+h*d*p;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+a)}return!0===e&&this._onChangeCallback(),this}setFromAxisAngle(t,e){const n=e/2,i=Math.sin(n);return this._x=t.x*i,this._y=t.y*i,this._z=t.z*i,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(t){const e=t.elements,n=e[0],i=e[4],r=e[8],a=e[1],o=e[5],s=e[9],l=e[2],c=e[6],u=e[10],h=n+o+u;if(h>0){const t=.5/Math.sqrt(h+1);this._w=.25/t,this._x=(c-s)*t,this._y=(r-l)*t,this._z=(a-i)*t}else if(n>o&&n>u){const t=2*Math.sqrt(1+n-o-u);this._w=(c-s)/t,this._x=.25*t,this._y=(i+a)/t,this._z=(r+l)/t}else if(o>u){const t=2*Math.sqrt(1+o-n-u);this._w=(r-l)/t,this._x=(i+a)/t,this._y=.25*t,this._z=(s+c)/t}else{const t=2*Math.sqrt(1+u-n-o);this._w=(a-i)/t,this._x=(r+l)/t,this._y=(s+c)/t,this._z=.25*t}return this._onChangeCallback(),this}setFromUnitVectors(t,e){let n=t.dot(e)+1;return nMath.abs(t.z)?(this._x=-t.y,this._y=t.x,this._z=0,this._w=n):(this._x=0,this._y=-t.z,this._z=t.y,this._w=n)):(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=n),this.normalize()}angleTo(t){return 2*Math.acos(Math.abs(bt(this.dot(t),-1,1)))}rotateTowards(t,e){const n=this.angleTo(t);if(0===n)return this;const i=Math.min(1,e/n);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 n=t._x,i=t._y,r=t._z,a=t._w,o=e._x,s=e._y,l=e._z,c=e._w;return this._x=n*c+a*o+i*l-r*s,this._y=i*c+a*s+r*o-n*l,this._z=r*c+a*l+n*s-i*o,this._w=a*c-n*o-i*s-r*l,this._onChangeCallback(),this}slerp(t,e){if(0===e)return this;if(1===e)return this.copy(t);const n=this._x,i=this._y,r=this._z,a=this._w;let o=a*t._w+n*t._x+i*t._y+r*t._z;if(o<0?(this._w=-t._w,this._x=-t._x,this._y=-t._y,this._z=-t._z,o=-o):this.copy(t),o>=1)return this._w=a,this._x=n,this._y=i,this._z=r,this;const s=1-o*o;if(s<=Number.EPSILON){const t=1-e;return this._w=t*a+e*this._w,this._x=t*n+e*this._x,this._y=t*i+e*this._y,this._z=t*r+e*this._z,this.normalize(),this}const l=Math.sqrt(s),c=Math.atan2(l,o),u=Math.sin((1-e)*c)/l,h=Math.sin(e*c)/l;return this._w=a*u+this._w*h,this._x=n*u+this._x*h,this._y=i*u+this._y*h,this._z=r*u+this._z*h,this._onChangeCallback(),this}slerpQuaternions(t,e,n){return this.copy(t).slerp(e,n)}random(){const t=2*Math.PI*Math.random(),e=2*Math.PI*Math.random(),n=Math.random(),i=Math.sqrt(1-n),r=Math.sqrt(n);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 ne{constructor(t=0,e=0,n=0){ne.prototype.isVector3=!0,this.x=t,this.y=e,this.z=n}set(t,e,n){return void 0===n&&(n=this.z),this.x=t,this.y=e,this.z=n,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("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("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(re.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(re.setFromAxisAngle(t,e))}applyMatrix3(t){const e=this.x,n=this.y,i=this.z,r=t.elements;return this.x=r[0]*e+r[3]*n+r[6]*i,this.y=r[1]*e+r[4]*n+r[7]*i,this.z=r[2]*e+r[5]*n+r[8]*i,this}applyNormalMatrix(t){return this.applyMatrix3(t).normalize()}applyMatrix4(t){const e=this.x,n=this.y,i=this.z,r=t.elements,a=1/(r[3]*e+r[7]*n+r[11]*i+r[15]);return this.x=(r[0]*e+r[4]*n+r[8]*i+r[12])*a,this.y=(r[1]*e+r[5]*n+r[9]*i+r[13])*a,this.z=(r[2]*e+r[6]*n+r[10]*i+r[14])*a,this}applyQuaternion(t){const e=this.x,n=this.y,i=this.z,r=t.x,a=t.y,o=t.z,s=t.w,l=2*(a*i-o*n),c=2*(o*e-r*i),u=2*(r*n-a*e);return this.x=e+s*l+a*u-o*c,this.y=n+s*c+o*l-r*u,this.z=i+s*u+r*c-a*l,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,n=this.y,i=this.z,r=t.elements;return this.x=r[0]*e+r[4]*n+r[8]*i,this.y=r[1]*e+r[5]*n+r[9]*i,this.z=r[2]*e+r[6]*n+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=Math.max(t.x,Math.min(e.x,this.x)),this.y=Math.max(t.y,Math.min(e.y,this.y)),this.z=Math.max(t.z,Math.min(e.z,this.z)),this}clampScalar(t,e){return this.x=Math.max(t,Math.min(e,this.x)),this.y=Math.max(t,Math.min(e,this.y)),this.z=Math.max(t,Math.min(e,this.z)),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(t,Math.min(e,n)))}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,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this}cross(t){return this.crossVectors(this,t)}crossVectors(t,e){const n=t.x,i=t.y,r=t.z,a=e.x,o=e.y,s=e.z;return this.x=i*s-r*o,this.y=r*a-n*s,this.z=n*o-i*a,this}projectOnVector(t){const e=t.lengthSq();if(0===e)return this.set(0,0,0);const n=t.dot(this)/e;return this.copy(t).multiplyScalar(n)}projectOnPlane(t){return ie.copy(this).projectOnVector(t),this.sub(ie)}reflect(t){return this.sub(ie.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 n=this.dot(t)/e;return Math.acos(bt(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y,i=this.z-t.z;return e*e+n*n+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,n){const i=Math.sin(e)*t;return this.x=i*Math.sin(n),this.y=Math.cos(e)*t,this.z=i*Math.cos(n),this}setFromCylindrical(t){return this.setFromCylindricalCoords(t.radius,t.theta,t.y)}setFromCylindricalCoords(t,e,n){return this.x=t*Math.sin(e),this.y=n,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(),n=this.setFromMatrixColumn(t,1).length(),i=this.setFromMatrixColumn(t,2).length();return this.x=e,this.y=n,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,n=Math.sqrt(1-e*e);return this.x=n*Math.cos(t),this.y=e,this.z=n*Math.sin(t),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}}const ie=new ne,re=new ee;class ae{constructor(t=new ne(1/0,1/0,1/0),e=new ne(-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,n=t.length;ethis.max.x||t.ythis.max.y||t.zthis.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.xthis.max.x||t.max.ythis.max.y||t.max.zthis.max.z)}intersectsSphere(t){return this.clampPoint(t.center,se),se.distanceToSquared(t.center)<=t.radius*t.radius}intersectsPlane(t){let e,n;return t.normal.x>0?(e=t.normal.x*this.min.x,n=t.normal.x*this.max.x):(e=t.normal.x*this.max.x,n=t.normal.x*this.min.x),t.normal.y>0?(e+=t.normal.y*this.min.y,n+=t.normal.y*this.max.y):(e+=t.normal.y*this.max.y,n+=t.normal.y*this.min.y),t.normal.z>0?(e+=t.normal.z*this.min.z,n+=t.normal.z*this.max.z):(e+=t.normal.z*this.max.z,n+=t.normal.z*this.min.z),e<=-t.constant&&n>=-t.constant}intersectsTriangle(t){if(this.isEmpty())return!1;this.getCenter(me),ge.subVectors(this.max,me),ce.subVectors(t.a,me),ue.subVectors(t.b,me),he.subVectors(t.c,me),de.subVectors(ue,ce),pe.subVectors(he,ue),fe.subVectors(ce,he);let e=[0,-de.z,de.y,0,-pe.z,pe.y,0,-fe.z,fe.y,de.z,0,-de.x,pe.z,0,-pe.x,fe.z,0,-fe.x,-de.y,de.x,0,-pe.y,pe.x,0,-fe.y,fe.x,0];return!!ye(e,ce,ue,he,ge)&&(e=[1,0,0,0,1,0,0,0,1],!!ye(e,ce,ue,he,ge)&&(ve.crossVectors(de,pe),e=[ve.x,ve.y,ve.z],ye(e,ce,ue,he,ge)))}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,se).distanceTo(t)}getBoundingSphere(t){return this.isEmpty()?t.makeEmpty():(this.getCenter(t.center),t.radius=.5*this.getSize(se).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()||(oe[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),oe[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),oe[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),oe[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),oe[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),oe[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),oe[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),oe[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(oe)),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 oe=[new ne,new ne,new ne,new ne,new ne,new ne,new ne,new ne],se=new ne,le=new ae,ce=new ne,ue=new ne,he=new ne,de=new ne,pe=new ne,fe=new ne,me=new ne,ge=new ne,ve=new ne,_e=new ne;function ye(t,e,n,i,r){for(let a=0,o=t.length-3;a<=o;a+=3){_e.fromArray(t,a);const o=r.x*Math.abs(_e.x)+r.y*Math.abs(_e.y)+r.z*Math.abs(_e.z),s=e.dot(_e),l=n.dot(_e),c=i.dot(_e);if(Math.max(-Math.max(s,l,c),Math.min(s,l,c))>o)return!1}return!0}const xe=new ae,be=new ne,Me=new ne;class Se{constructor(t=new ne,e=-1){this.isSphere=!0,this.center=t,this.radius=e}set(t,e){return this.center.copy(t),this.radius=e,this}setFromPoints(t,e){const n=this.center;void 0!==e?n.copy(e):xe.setFromPoints(t).getCenter(n);let i=0;for(let e=0,r=t.length;ethis.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;be.subVectors(t,this.center);const e=be.lengthSq();if(e>this.radius*this.radius){const t=Math.sqrt(e),n=.5*(t-this.radius);this.center.addScaledVector(be,n/t),this.radius+=n}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):(Me.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint(be.copy(t.center).add(Me)),this.expandByPoint(be.copy(t.center).sub(Me))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return(new this.constructor).copy(this)}}const Ee=new ne,we=new ne,Te=new ne,Ae=new ne,Re=new ne,Ce=new ne,Pe=new ne;class Le{constructor(t=new ne,e=new ne(0,0,-1)){this.origin=t,this.direction=e}set(t,e){return this.origin.copy(t),this.direction.copy(e),this}copy(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this}at(t,e){return e.copy(this.origin).addScaledVector(this.direction,t)}lookAt(t){return this.direction.copy(t).sub(this.origin).normalize(),this}recast(t){return this.origin.copy(this.at(t,Ee)),this}closestPointToPoint(t,e){e.subVectors(t,this.origin);const n=e.dot(this.direction);return n<0?e.copy(this.origin):e.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(t){return Math.sqrt(this.distanceSqToPoint(t))}distanceSqToPoint(t){const e=Ee.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(Ee.copy(this.origin).addScaledVector(this.direction,e),Ee.distanceToSquared(t))}distanceSqToSegment(t,e,n,i){we.copy(t).add(e).multiplyScalar(.5),Te.copy(e).sub(t).normalize(),Ae.copy(this.origin).sub(we);const r=.5*t.distanceTo(e),a=-this.direction.dot(Te),o=Ae.dot(this.direction),s=-Ae.dot(Te),l=Ae.lengthSq(),c=Math.abs(1-a*a);let u,h,d,p;if(c>0)if(u=a*s-o,h=a*o-s,p=r*c,u>=0)if(h>=-p)if(h<=p){const t=1/c;u*=t,h*=t,d=u*(u+a*h+2*o)+h*(a*u+h+2*s)+l}else h=r,u=Math.max(0,-(a*h+o)),d=-u*u+h*(h+2*s)+l;else h=-r,u=Math.max(0,-(a*h+o)),d=-u*u+h*(h+2*s)+l;else h<=-p?(u=Math.max(0,-(-a*r+o)),h=u>0?-r:Math.min(Math.max(-r,-s),r),d=-u*u+h*(h+2*s)+l):h<=p?(u=0,h=Math.min(Math.max(-r,-s),r),d=h*(h+2*s)+l):(u=Math.max(0,-(a*r+o)),h=u>0?r:Math.min(Math.max(-r,-s),r),d=-u*u+h*(h+2*s)+l);else h=a>0?-r:r,u=Math.max(0,-(a*h+o)),d=-u*u+h*(h+2*s)+l;return n&&n.copy(this.origin).addScaledVector(this.direction,u),i&&i.copy(we).addScaledVector(Te,h),d}intersectSphere(t,e){Ee.subVectors(t.center,this.origin);const n=Ee.dot(this.direction),i=Ee.dot(Ee)-n*n,r=t.radius*t.radius;if(i>r)return null;const a=Math.sqrt(r-i),o=n-a,s=n+a;return s<0?null:o<0?this.at(s,e):this.at(o,e)}intersectsSphere(t){return 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 n=-(this.origin.dot(t.normal)+t.constant)/e;return n>=0?n:null}intersectPlane(t,e){const n=this.distanceToPlane(t);return null===n?null:this.at(n,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 n,i,r,a,o,s;const l=1/this.direction.x,c=1/this.direction.y,u=1/this.direction.z,h=this.origin;return l>=0?(n=(t.min.x-h.x)*l,i=(t.max.x-h.x)*l):(n=(t.max.x-h.x)*l,i=(t.min.x-h.x)*l),c>=0?(r=(t.min.y-h.y)*c,a=(t.max.y-h.y)*c):(r=(t.max.y-h.y)*c,a=(t.min.y-h.y)*c),n>a||r>i?null:((r>n||isNaN(n))&&(n=r),(a=0?(o=(t.min.z-h.z)*u,s=(t.max.z-h.z)*u):(o=(t.max.z-h.z)*u,s=(t.min.z-h.z)*u),n>s||o>i?null:((o>n||n!=n)&&(n=o),(s=0?n:i,e)))}intersectsBox(t){return null!==this.intersectBox(t,Ee)}intersectTriangle(t,e,n,i,r){Re.subVectors(e,t),Ce.subVectors(n,t),Pe.crossVectors(Re,Ce);let a,o=this.direction.dot(Pe);if(o>0){if(i)return null;a=1}else{if(!(o<0))return null;a=-1,o=-o}Ae.subVectors(this.origin,t);const s=a*this.direction.dot(Ce.crossVectors(Ae,Ce));if(s<0)return null;const l=a*this.direction.dot(Re.cross(Ae));if(l<0)return null;if(s+l>o)return null;const c=-a*Ae.dot(Pe);return c<0?null:this.at(c/o,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 Oe{constructor(t,e,n,i,r,a,o,s,l,c,u,h,d,p,f,m){Oe.prototype.isMatrix4=!0,this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],void 0!==t&&this.set(t,e,n,i,r,a,o,s,l,c,u,h,d,p,f,m)}set(t,e,n,i,r,a,o,s,l,c,u,h,d,p,f,m){const g=this.elements;return g[0]=t,g[4]=e,g[8]=n,g[12]=i,g[1]=r,g[5]=a,g[9]=o,g[13]=s,g[2]=l,g[6]=c,g[10]=u,g[14]=h,g[3]=d,g[7]=p,g[11]=f,g[15]=m,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}clone(){return(new Oe).fromArray(this.elements)}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],e[9]=n[9],e[10]=n[10],e[11]=n[11],e[12]=n[12],e[13]=n[13],e[14]=n[14],e[15]=n[15],this}copyPosition(t){const e=this.elements,n=t.elements;return e[12]=n[12],e[13]=n[13],e[14]=n[14],this}setFromMatrix3(t){const e=t.elements;return this.set(e[0],e[3],e[6],0,e[1],e[4],e[7],0,e[2],e[5],e[8],0,0,0,0,1),this}extractBasis(t,e,n){return t.setFromMatrixColumn(this,0),e.setFromMatrixColumn(this,1),n.setFromMatrixColumn(this,2),this}makeBasis(t,e,n){return this.set(t.x,e.x,n.x,0,t.y,e.y,n.y,0,t.z,e.z,n.z,0,0,0,0,1),this}extractRotation(t){const e=this.elements,n=t.elements,i=1/De.setFromMatrixColumn(t,0).length(),r=1/De.setFromMatrixColumn(t,1).length(),a=1/De.setFromMatrixColumn(t,2).length();return e[0]=n[0]*i,e[1]=n[1]*i,e[2]=n[2]*i,e[3]=0,e[4]=n[4]*r,e[5]=n[5]*r,e[6]=n[6]*r,e[7]=0,e[8]=n[8]*a,e[9]=n[9]*a,e[10]=n[10]*a,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromEuler(t){const e=this.elements,n=t.x,i=t.y,r=t.z,a=Math.cos(n),o=Math.sin(n),s=Math.cos(i),l=Math.sin(i),c=Math.cos(r),u=Math.sin(r);if("XYZ"===t.order){const t=a*c,n=a*u,i=o*c,r=o*u;e[0]=s*c,e[4]=-s*u,e[8]=l,e[1]=n+i*l,e[5]=t-r*l,e[9]=-o*s,e[2]=r-t*l,e[6]=i+n*l,e[10]=a*s}else if("YXZ"===t.order){const t=s*c,n=s*u,i=l*c,r=l*u;e[0]=t+r*o,e[4]=i*o-n,e[8]=a*l,e[1]=a*u,e[5]=a*c,e[9]=-o,e[2]=n*o-i,e[6]=r+t*o,e[10]=a*s}else if("ZXY"===t.order){const t=s*c,n=s*u,i=l*c,r=l*u;e[0]=t-r*o,e[4]=-a*u,e[8]=i+n*o,e[1]=n+i*o,e[5]=a*c,e[9]=r-t*o,e[2]=-a*l,e[6]=o,e[10]=a*s}else if("ZYX"===t.order){const t=a*c,n=a*u,i=o*c,r=o*u;e[0]=s*c,e[4]=i*l-n,e[8]=t*l+r,e[1]=s*u,e[5]=r*l+t,e[9]=n*l-i,e[2]=-l,e[6]=o*s,e[10]=a*s}else if("YZX"===t.order){const t=a*s,n=a*l,i=o*s,r=o*l;e[0]=s*c,e[4]=r-t*u,e[8]=i*u+n,e[1]=u,e[5]=a*c,e[9]=-o*c,e[2]=-l*c,e[6]=n*u+i,e[10]=t-r*u}else if("XZY"===t.order){const t=a*s,n=a*l,i=o*s,r=o*l;e[0]=s*c,e[4]=-u,e[8]=l*c,e[1]=t*u+r,e[5]=a*c,e[9]=n*u-i,e[2]=i*u-n,e[6]=o*c,e[10]=r*u+t}return e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromQuaternion(t){return this.compose(Ie,t,Ue)}lookAt(t,e,n){const i=this.elements;return ze.subVectors(t,e),0===ze.lengthSq()&&(ze.z=1),ze.normalize(),Fe.crossVectors(n,ze),0===Fe.lengthSq()&&(1===Math.abs(n.z)?ze.x+=1e-4:ze.z+=1e-4,ze.normalize(),Fe.crossVectors(n,ze)),Fe.normalize(),ke.crossVectors(ze,Fe),i[0]=Fe.x,i[4]=ke.x,i[8]=ze.x,i[1]=Fe.y,i[5]=ke.y,i[9]=ze.y,i[2]=Fe.z,i[6]=ke.z,i[10]=ze.z,this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,i=e.elements,r=this.elements,a=n[0],o=n[4],s=n[8],l=n[12],c=n[1],u=n[5],h=n[9],d=n[13],p=n[2],f=n[6],m=n[10],g=n[14],v=n[3],_=n[7],y=n[11],x=n[15],b=i[0],M=i[4],S=i[8],E=i[12],w=i[1],T=i[5],A=i[9],R=i[13],C=i[2],P=i[6],L=i[10],O=i[14],D=i[3],N=i[7],I=i[11],U=i[15];return r[0]=a*b+o*w+s*C+l*D,r[4]=a*M+o*T+s*P+l*N,r[8]=a*S+o*A+s*L+l*I,r[12]=a*E+o*R+s*O+l*U,r[1]=c*b+u*w+h*C+d*D,r[5]=c*M+u*T+h*P+d*N,r[9]=c*S+u*A+h*L+d*I,r[13]=c*E+u*R+h*O+d*U,r[2]=p*b+f*w+m*C+g*D,r[6]=p*M+f*T+m*P+g*N,r[10]=p*S+f*A+m*L+g*I,r[14]=p*E+f*R+m*O+g*U,r[3]=v*b+_*w+y*C+x*D,r[7]=v*M+_*T+y*P+x*N,r[11]=v*S+_*A+y*L+x*I,r[15]=v*E+_*R+y*O+x*U,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[4],i=t[8],r=t[12],a=t[1],o=t[5],s=t[9],l=t[13],c=t[2],u=t[6],h=t[10],d=t[14];return t[3]*(+r*s*u-i*l*u-r*o*h+n*l*h+i*o*d-n*s*d)+t[7]*(+e*s*d-e*l*h+r*a*h-i*a*d+i*l*c-r*s*c)+t[11]*(+e*l*u-e*o*d-r*a*u+n*a*d+r*o*c-n*l*c)+t[15]*(-i*o*c-e*s*u+e*o*h+i*a*u-n*a*h+n*s*c)}transpose(){const t=this.elements;let e;return e=t[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this}setPosition(t,e,n){const i=this.elements;return t.isVector3?(i[12]=t.x,i[13]=t.y,i[14]=t.z):(i[12]=t,i[13]=e,i[14]=n),this}invert(){const t=this.elements,e=t[0],n=t[1],i=t[2],r=t[3],a=t[4],o=t[5],s=t[6],l=t[7],c=t[8],u=t[9],h=t[10],d=t[11],p=t[12],f=t[13],m=t[14],g=t[15],v=u*m*l-f*h*l+f*s*d-o*m*d-u*s*g+o*h*g,_=p*h*l-c*m*l-p*s*d+a*m*d+c*s*g-a*h*g,y=c*f*l-p*u*l+p*o*d-a*f*d-c*o*g+a*u*g,x=p*u*s-c*f*s-p*o*h+a*f*h+c*o*m-a*u*m,b=e*v+n*_+i*y+r*x;if(0===b)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const M=1/b;return t[0]=v*M,t[1]=(f*h*r-u*m*r-f*i*d+n*m*d+u*i*g-n*h*g)*M,t[2]=(o*m*r-f*s*r+f*i*l-n*m*l-o*i*g+n*s*g)*M,t[3]=(u*s*r-o*h*r-u*i*l+n*h*l+o*i*d-n*s*d)*M,t[4]=_*M,t[5]=(c*m*r-p*h*r+p*i*d-e*m*d-c*i*g+e*h*g)*M,t[6]=(p*s*r-a*m*r-p*i*l+e*m*l+a*i*g-e*s*g)*M,t[7]=(a*h*r-c*s*r+c*i*l-e*h*l-a*i*d+e*s*d)*M,t[8]=y*M,t[9]=(p*u*r-c*f*r-p*n*d+e*f*d+c*n*g-e*u*g)*M,t[10]=(a*f*r-p*o*r+p*n*l-e*f*l-a*n*g+e*o*g)*M,t[11]=(c*o*r-a*u*r-c*n*l+e*u*l+a*n*d-e*o*d)*M,t[12]=x*M,t[13]=(c*f*i-p*u*i+p*n*h-e*f*h-c*n*m+e*u*m)*M,t[14]=(p*o*i-a*f*i-p*n*s+e*f*s+a*n*m-e*o*m)*M,t[15]=(a*u*i-c*o*i+c*n*s-e*u*s-a*n*h+e*o*h)*M,this}scale(t){const e=this.elements,n=t.x,i=t.y,r=t.z;return e[0]*=n,e[4]*=i,e[8]*=r,e[1]*=n,e[5]*=i,e[9]*=r,e[2]*=n,e[6]*=i,e[10]*=r,e[3]*=n,e[7]*=i,e[11]*=r,this}getMaxScaleOnAxis(){const t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],n=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],i=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,n,i))}makeTranslation(t,e,n){return t.isVector3?this.set(1,0,0,t.x,0,1,0,t.y,0,0,1,t.z,0,0,0,1):this.set(1,0,0,t,0,1,0,e,0,0,1,n,0,0,0,1),this}makeRotationX(t){const e=Math.cos(t),n=Math.sin(t);return this.set(1,0,0,0,0,e,-n,0,0,n,e,0,0,0,0,1),this}makeRotationY(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,0,n,0,0,1,0,0,-n,0,e,0,0,0,0,1),this}makeRotationZ(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,0,n,e,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(t,e){const n=Math.cos(e),i=Math.sin(e),r=1-n,a=t.x,o=t.y,s=t.z,l=r*a,c=r*o;return this.set(l*a+n,l*o-i*s,l*s+i*o,0,l*o+i*s,c*o+n,c*s-i*a,0,l*s-i*o,c*s+i*a,r*s*s+n,0,0,0,0,1),this}makeScale(t,e,n){return this.set(t,0,0,0,0,e,0,0,0,0,n,0,0,0,0,1),this}makeShear(t,e,n,i,r,a){return this.set(1,n,r,0,t,1,a,0,e,i,1,0,0,0,0,1),this}compose(t,e,n){const i=this.elements,r=e._x,a=e._y,o=e._z,s=e._w,l=r+r,c=a+a,u=o+o,h=r*l,d=r*c,p=r*u,f=a*c,m=a*u,g=o*u,v=s*l,_=s*c,y=s*u,x=n.x,b=n.y,M=n.z;return i[0]=(1-(f+g))*x,i[1]=(d+y)*x,i[2]=(p-_)*x,i[3]=0,i[4]=(d-y)*b,i[5]=(1-(h+g))*b,i[6]=(m+v)*b,i[7]=0,i[8]=(p+_)*M,i[9]=(m-v)*M,i[10]=(1-(h+f))*M,i[11]=0,i[12]=t.x,i[13]=t.y,i[14]=t.z,i[15]=1,this}decompose(t,e,n){const i=this.elements;let r=De.set(i[0],i[1],i[2]).length();const a=De.set(i[4],i[5],i[6]).length(),o=De.set(i[8],i[9],i[10]).length();this.determinant()<0&&(r=-r),t.x=i[12],t.y=i[13],t.z=i[14],Ne.copy(this);const s=1/r,l=1/a,c=1/o;return Ne.elements[0]*=s,Ne.elements[1]*=s,Ne.elements[2]*=s,Ne.elements[4]*=l,Ne.elements[5]*=l,Ne.elements[6]*=l,Ne.elements[8]*=c,Ne.elements[9]*=c,Ne.elements[10]*=c,e.setFromRotationMatrix(Ne),n.x=r,n.y=a,n.z=o,this}makePerspective(t,e,n,i,r,a,o=2e3){const s=this.elements,l=2*r/(e-t),c=2*r/(n-i),u=(e+t)/(e-t),h=(n+i)/(n-i);let d,p;if(o===pt)d=-(a+r)/(a-r),p=-2*a*r/(a-r);else{if(o!==ft)throw new Error("THREE.Matrix4.makePerspective(): Invalid coordinate system: "+o);d=-a/(a-r),p=-a*r/(a-r)}return s[0]=l,s[4]=0,s[8]=u,s[12]=0,s[1]=0,s[5]=c,s[9]=h,s[13]=0,s[2]=0,s[6]=0,s[10]=d,s[14]=p,s[3]=0,s[7]=0,s[11]=-1,s[15]=0,this}makeOrthographic(t,e,n,i,r,a,o=2e3){const s=this.elements,l=1/(e-t),c=1/(n-i),u=1/(a-r),h=(e+t)*l,d=(n+i)*c;let p,f;if(o===pt)p=(a+r)*u,f=-2*u;else{if(o!==ft)throw new Error("THREE.Matrix4.makeOrthographic(): Invalid coordinate system: "+o);p=r*u,f=-1*u}return s[0]=2*l,s[4]=0,s[8]=0,s[12]=-h,s[1]=0,s[5]=2*c,s[9]=0,s[13]=-d,s[2]=0,s[6]=0,s[10]=f,s[14]=-p,s[3]=0,s[7]=0,s[11]=0,s[15]=1,this}equals(t){const e=this.elements,n=t.elements;for(let t=0;t<16;t++)if(e[t]!==n[t])return!1;return!0}fromArray(t,e=0){for(let n=0;n<16;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t[e+9]=n[9],t[e+10]=n[10],t[e+11]=n[11],t[e+12]=n[12],t[e+13]=n[13],t[e+14]=n[14],t[e+15]=n[15],t}}const De=new ne,Ne=new Oe,Ie=new ne(0,0,0),Ue=new ne(1,1,1),Fe=new ne,ke=new ne,ze=new ne,Be=new Oe,He=new ee;class Ge{constructor(t=0,e=0,n=0,i=Ge.DEFAULT_ORDER){this.isEuler=!0,this._x=t,this._y=e,this._z=n,this._order=i}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 order(){return this._order}set order(t){this._order=t,this._onChangeCallback()}set(t,e,n,i=this._order){return this._x=t,this._y=e,this._z=n,this._order=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(t){return this._x=t._x,this._y=t._y,this._z=t._z,this._order=t._order,this._onChangeCallback(),this}setFromRotationMatrix(t,e=this._order,n=!0){const i=t.elements,r=i[0],a=i[4],o=i[8],s=i[1],l=i[5],c=i[9],u=i[2],h=i[6],d=i[10];switch(e){case"XYZ":this._y=Math.asin(bt(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-c,d),this._z=Math.atan2(-a,r)):(this._x=Math.atan2(h,l),this._z=0);break;case"YXZ":this._x=Math.asin(-bt(c,-1,1)),Math.abs(c)<.9999999?(this._y=Math.atan2(o,d),this._z=Math.atan2(s,l)):(this._y=Math.atan2(-u,r),this._z=0);break;case"ZXY":this._x=Math.asin(bt(h,-1,1)),Math.abs(h)<.9999999?(this._y=Math.atan2(-u,d),this._z=Math.atan2(-a,l)):(this._y=0,this._z=Math.atan2(s,r));break;case"ZYX":this._y=Math.asin(-bt(u,-1,1)),Math.abs(u)<.9999999?(this._x=Math.atan2(h,d),this._z=Math.atan2(s,r)):(this._x=0,this._z=Math.atan2(-a,l));break;case"YZX":this._z=Math.asin(bt(s,-1,1)),Math.abs(s)<.9999999?(this._x=Math.atan2(-c,l),this._y=Math.atan2(-u,r)):(this._x=0,this._y=Math.atan2(o,d));break;case"XZY":this._z=Math.asin(-bt(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(h,l),this._y=Math.atan2(o,r)):(this._x=Math.atan2(-c,d),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+e)}return this._order=e,!0===n&&this._onChangeCallback(),this}setFromQuaternion(t,e,n){return Be.makeRotationFromQuaternion(t),this.setFromRotationMatrix(Be,e,n)}setFromVector3(t,e=this._order){return this.set(t.x,t.y,t.z,e)}reorder(t){return He.setFromEuler(this),this.setFromQuaternion(He,t)}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._order===this._order}fromArray(t){return this._x=t[0],this._y=t[1],this._z=t[2],void 0!==t[3]&&(this._order=t[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._order,t}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._order}}Ge.DEFAULT_ORDER="XYZ";class Ve{constructor(){this.mask=1}set(t){this.mask=1<>>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(),!1===this.matrixAutoUpdate&&(i.matrixAutoUpdate=!1),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.visibility=this._visibility,i.active=this._active,i.bounds=this._bounds.map((t=>({boxInitialized:t.boxInitialized,boxMin:t.box.min.toArray(),boxMax:t.box.max.toArray(),sphereInitialized:t.sphereInitialized,sphereRadius:t.sphere.radius,sphereCenter:t.sphere.center.toArray()}))),i.maxGeometryCount=this._maxGeometryCount,i.maxVertexCount=this._maxVertexCount,i.maxIndexCount=this._maxIndexCount,i.geometryInitialized=this._geometryInitialized,i.geometryCount=this._geometryCount,i.matricesTexture=this._matricesTexture.toJSON(t),null!==this.boundingSphere&&(i.boundingSphere={center:i.boundingSphere.center.toArray(),radius:i.boundingSphere.radius}),null!==this.boundingBox&&(i.boundingBox={min:i.boundingBox.min.toArray(),max:i.boundingBox.max.toArray()})),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 n=e.shapes;if(Array.isArray(n))for(let e=0,i=n.length;e0){i.children=[];for(let e=0;e0){i.animations=[];for(let e=0;e0&&(n.geometries=e),i.length>0&&(n.materials=i),r.length>0&&(n.textures=r),o.length>0&&(n.images=o),s.length>0&&(n.shapes=s),l.length>0&&(n.skeletons=l),c.length>0&&(n.animations=c),u.length>0&&(n.nodes=u)}return n.object=i,n;function a(t){const e=[];for(const n in t){const i=t[n];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.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.animations=t.animations.slice(),this.userData=JSON.parse(JSON.stringify(t.userData)),!0===e)for(let e=0;e0?i.multiplyScalar(1/Math.sqrt(r)):i.set(0,0,0)}static getBarycoord(t,e,n,i,r){sn.subVectors(i,e),ln.subVectors(n,e),cn.subVectors(t,e);const a=sn.dot(sn),o=sn.dot(ln),s=sn.dot(cn),l=ln.dot(ln),c=ln.dot(cn),u=a*l-o*o;if(0===u)return r.set(0,0,0),null;const h=1/u,d=(l*s-o*c)*h,p=(a*c-o*s)*h;return r.set(1-d-p,p,d)}static containsPoint(t,e,n,i){return null!==this.getBarycoord(t,e,n,i,un)&&(un.x>=0&&un.y>=0&&un.x+un.y<=1)}static getInterpolation(t,e,n,i,r,a,o,s){return null===this.getBarycoord(t,e,n,i,un)?(s.x=0,s.y=0,"z"in s&&(s.z=0),"w"in s&&(s.w=0),null):(s.setScalar(0),s.addScaledVector(r,un.x),s.addScaledVector(a,un.y),s.addScaledVector(o,un.z),s)}static isFrontFacing(t,e,n,i){return sn.subVectors(n,e),ln.subVectors(t,e),sn.cross(ln).dot(i)<0}set(t,e,n){return this.a.copy(t),this.b.copy(e),this.c.copy(n),this}setFromPointsAndIndices(t,e,n,i){return this.a.copy(t[e]),this.b.copy(t[n]),this.c.copy(t[i]),this}setFromAttributeAndIndices(t,e,n,i){return this.a.fromBufferAttribute(t,e),this.b.fromBufferAttribute(t,n),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 sn.subVectors(this.c,this.b),ln.subVectors(this.a,this.b),.5*sn.cross(ln).length()}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return vn.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return vn.getBarycoord(t,this.a,this.b,this.c,e)}getInterpolation(t,e,n,i,r){return vn.getInterpolation(t,this.a,this.b,this.c,e,n,i,r)}containsPoint(t){return vn.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return vn.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(t){return t.intersectsTriangle(this)}closestPointToPoint(t,e){const n=this.a,i=this.b,r=this.c;let a,o;hn.subVectors(i,n),dn.subVectors(r,n),fn.subVectors(t,n);const s=hn.dot(fn),l=dn.dot(fn);if(s<=0&&l<=0)return e.copy(n);mn.subVectors(t,i);const c=hn.dot(mn),u=dn.dot(mn);if(c>=0&&u<=c)return e.copy(i);const h=s*u-c*l;if(h<=0&&s>=0&&c<=0)return a=s/(s-c),e.copy(n).addScaledVector(hn,a);gn.subVectors(t,r);const d=hn.dot(gn),p=dn.dot(gn);if(p>=0&&d<=p)return e.copy(r);const f=d*l-s*p;if(f<=0&&l>=0&&p<=0)return o=l/(l-p),e.copy(n).addScaledVector(dn,o);const m=c*p-d*u;if(m<=0&&u-c>=0&&d-p>=0)return pn.subVectors(r,i),o=(u-c)/(u-c+(d-p)),e.copy(i).addScaledVector(pn,o);const g=1/(m+f+h);return a=f*g,o=h*g,e.copy(n).addScaledVector(hn,a).addScaledVector(dn,o)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}const _n={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},yn={h:0,s:0,l:0},xn={h:0,s:0,l:0};function bn(t,e,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?t+6*(e-t)*n:n<.5?e:n<2/3?t+6*(e-t)*(2/3-n):t}class Mn{constructor(t,e,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(t,e,n)}set(t,e,n){if(void 0===e&&void 0===n){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,n);return this}setScalar(t){return this.r=t,this.g=t,this.b=t,this}setHex(t,e=nt){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(255&t)/255,Bt.toWorkingColorSpace(this,e),this}setRGB(t,e,n,i=Bt.workingColorSpace){return this.r=t,this.g=e,this.b=n,Bt.toWorkingColorSpace(this,i),this}setHSL(t,e,n,i=Bt.workingColorSpace){if(t=Mt(t,1),e=bt(e,0,1),n=bt(n,0,1),0===e)this.r=this.g=this.b=n;else{const i=n<=.5?n*(1+e):n+e-n*e,r=2*n-i;this.r=bn(r,i,t+1/3),this.g=bn(r,i,t),this.b=bn(r,i,t-1/3)}return Bt.toWorkingColorSpace(this,i),this}setStyle(t,e=nt){function n(e){void 0!==e&&parseFloat(e)<1&&console.warn("THREE.Color: Alpha component of "+t+" will be ignored.")}let i;if(i=/^(\w+)\(([^\)]*)\)/.exec(t)){let r;const a=i[1],o=i[2];switch(a){case"rgb":case"rgba":if(r=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(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(o))return n(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(o))return n(r[4]),this.setHSL(parseFloat(r[1])/360,parseFloat(r[2])/100,parseFloat(r[3])/100,e);break;default:console.warn("THREE.Color: Unknown color model "+t)}}else if(i=/^\#([A-Fa-f\d]+)$/.exec(t)){const n=i[1],r=n.length;if(3===r)return this.setRGB(parseInt(n.charAt(0),16)/15,parseInt(n.charAt(1),16)/15,parseInt(n.charAt(2),16)/15,e);if(6===r)return this.setHex(parseInt(n,16),e);console.warn("THREE.Color: Invalid hex color "+t)}else if(t&&t.length>0)return this.setColorName(t,e);return this}setColorName(t,e=nt){const n=_n[t.toLowerCase()];return void 0!==n?this.setHex(n,e):console.warn("THREE.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=Ht(t.r),this.g=Ht(t.g),this.b=Ht(t.b),this}copyLinearToSRGB(t){return this.r=Gt(t.r),this.g=Gt(t.g),this.b=Gt(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(t=nt){return Bt.fromWorkingColorSpace(Sn.copy(this),t),65536*Math.round(bt(255*Sn.r,0,255))+256*Math.round(bt(255*Sn.g,0,255))+Math.round(bt(255*Sn.b,0,255))}getHexString(t=nt){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=Bt.workingColorSpace){Bt.fromWorkingColorSpace(Sn.copy(this),e);const n=Sn.r,i=Sn.g,r=Sn.b,a=Math.max(n,i,r),o=Math.min(n,i,r);let s,l;const c=(o+a)/2;if(o===a)s=0,l=0;else{const t=a-o;switch(l=c<=.5?t/(a+o):t/(2-a-o),a){case n:s=(i-r)/t+(i0!=t>0&&this.version++,this._alphaTest=t}onBuild(){}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(t){if(void 0!==t)for(const e in t){const n=t[e];if(void 0===n){console.warn(`THREE.Material: parameter '${e}' has value of undefined.`);continue}const i=this[e];void 0!==i?i&&i.isColor?i.set(n):i&&i.isVector3&&n&&n.isVector3?i.copy(n):this[e]=n:console.warn(`THREE.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 n={metadata:{version:4.6,type:"Material",generator:"Material.toJSON"}};function i(t){const e=[];for(const n in t){const i=t[n];delete i.metadata,e.push(i)}return e}if(n.uuid=this.uuid,n.type=this.type,""!==this.name&&(n.name=this.name),this.color&&this.color.isColor&&(n.color=this.color.getHex()),void 0!==this.roughness&&(n.roughness=this.roughness),void 0!==this.metalness&&(n.metalness=this.metalness),void 0!==this.sheen&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),void 0!==this.sheenRoughness&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),void 0!==this.emissiveIntensity&&1!==this.emissiveIntensity&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),void 0!==this.specularIntensity&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),void 0!==this.shininess&&(n.shininess=this.shininess),void 0!==this.clearcoat&&(n.clearcoat=this.clearcoat),void 0!==this.clearcoatRoughness&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(t).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(t).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(t).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),void 0!==this.iridescence&&(n.iridescence=this.iridescence),void 0!==this.iridescenceIOR&&(n.iridescenceIOR=this.iridescenceIOR),void 0!==this.iridescenceThicknessRange&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(t).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(t).uuid),void 0!==this.anisotropy&&(n.anisotropy=this.anisotropy),void 0!==this.anisotropyRotation&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(t).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(t).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(t).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(t).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(t).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(t).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(t).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(t).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(t).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(t).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(t).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(t).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(t).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(t).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(t).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(t).uuid,void 0!==this.combine&&(n.combine=this.combine)),void 0!==this.envMapRotation&&(n.envMapRotation=this.envMapRotation.toArray()),void 0!==this.envMapIntensity&&(n.envMapIntensity=this.envMapIntensity),void 0!==this.reflectivity&&(n.reflectivity=this.reflectivity),void 0!==this.refractionRatio&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(t).uuid),void 0!==this.transmission&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(t).uuid),void 0!==this.thickness&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(t).uuid),void 0!==this.attenuationDistance&&this.attenuationDistance!==1/0&&(n.attenuationDistance=this.attenuationDistance),void 0!==this.attenuationColor&&(n.attenuationColor=this.attenuationColor.getHex()),void 0!==this.size&&(n.size=this.size),null!==this.shadowSide&&(n.shadowSide=this.shadowSide),void 0!==this.sizeAttenuation&&(n.sizeAttenuation=this.sizeAttenuation),1!==this.blending&&(n.blending=this.blending),this.side!==m&&(n.side=this.side),!0===this.vertexColors&&(n.vertexColors=!0),this.opacity<1&&(n.opacity=this.opacity),!0===this.transparent&&(n.transparent=!0),204!==this.blendSrc&&(n.blendSrc=this.blendSrc),205!==this.blendDst&&(n.blendDst=this.blendDst),this.blendEquation!==v&&(n.blendEquation=this.blendEquation),null!==this.blendSrcAlpha&&(n.blendSrcAlpha=this.blendSrcAlpha),null!==this.blendDstAlpha&&(n.blendDstAlpha=this.blendDstAlpha),null!==this.blendEquationAlpha&&(n.blendEquationAlpha=this.blendEquationAlpha),this.blendColor&&this.blendColor.isColor&&(n.blendColor=this.blendColor.getHex()),0!==this.blendAlpha&&(n.blendAlpha=this.blendAlpha),3!==this.depthFunc&&(n.depthFunc=this.depthFunc),!1===this.depthTest&&(n.depthTest=this.depthTest),!1===this.depthWrite&&(n.depthWrite=this.depthWrite),!1===this.colorWrite&&(n.colorWrite=this.colorWrite),255!==this.stencilWriteMask&&(n.stencilWriteMask=this.stencilWriteMask),519!==this.stencilFunc&&(n.stencilFunc=this.stencilFunc),0!==this.stencilRef&&(n.stencilRef=this.stencilRef),255!==this.stencilFuncMask&&(n.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==ut&&(n.stencilFail=this.stencilFail),this.stencilZFail!==ut&&(n.stencilZFail=this.stencilZFail),this.stencilZPass!==ut&&(n.stencilZPass=this.stencilZPass),!0===this.stencilWrite&&(n.stencilWrite=this.stencilWrite),void 0!==this.rotation&&0!==this.rotation&&(n.rotation=this.rotation),!0===this.polygonOffset&&(n.polygonOffset=!0),0!==this.polygonOffsetFactor&&(n.polygonOffsetFactor=this.polygonOffsetFactor),0!==this.polygonOffsetUnits&&(n.polygonOffsetUnits=this.polygonOffsetUnits),void 0!==this.linewidth&&1!==this.linewidth&&(n.linewidth=this.linewidth),void 0!==this.dashSize&&(n.dashSize=this.dashSize),void 0!==this.gapSize&&(n.gapSize=this.gapSize),void 0!==this.scale&&(n.scale=this.scale),!0===this.dithering&&(n.dithering=!0),this.alphaTest>0&&(n.alphaTest=this.alphaTest),!0===this.alphaHash&&(n.alphaHash=!0),!0===this.alphaToCoverage&&(n.alphaToCoverage=!0),!0===this.premultipliedAlpha&&(n.premultipliedAlpha=!0),!0===this.forceSinglePass&&(n.forceSinglePass=!0),!0===this.wireframe&&(n.wireframe=!0),this.wireframeLinewidth>1&&(n.wireframeLinewidth=this.wireframeLinewidth),"round"!==this.wireframeLinecap&&(n.wireframeLinecap=this.wireframeLinecap),"round"!==this.wireframeLinejoin&&(n.wireframeLinejoin=this.wireframeLinejoin),!0===this.flatShading&&(n.flatShading=!0),!1===this.visible&&(n.visible=!1),!1===this.toneMapped&&(n.toneMapped=!1),!1===this.fog&&(n.fog=!1),Object.keys(this.userData).length>0&&(n.userData=this.userData),e){const e=i(t.textures),r=i(t.images);e.length>0&&(n.textures=e),r.length>0&&(n.images=r)}return n}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 n=null;if(null!==e){const t=e.length;n=new Array(t);for(let i=0;i!==t;++i)n[i]=e[i].clone()}return this.clippingPlanes=n,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.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 Tn extends wn{constructor(t){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new Mn(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 Ge,this.combine=_,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 An=new ne,Rn=new Ct;class Cn{constructor(t,e,n=!1){if(Array.isArray(t))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,this.name="",this.array=t,this.itemSize=e,this.count=void 0!==t?t.length/e:0,this.normalized=n,this.usage=35044,this._updateRange={offset:0,count:-1},this.updateRanges=[],this.gpuType=j,this.version=0}onUploadCallback(){}set needsUpdate(t){!0===t&&this.version++}get updateRange(){var t;return(t="THREE.BufferAttribute: updateRange() is deprecated and will be removed in r169. Use addUpdateRange() instead.")in It||(It[t]=!0,console.warn(t)),this._updateRange}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,n){t*=this.itemSize,n*=e.itemSize;for(let i=0,r=this.itemSize;i0&&(t.userData=this.userData),void 0!==this.parameters){const e=this.parameters;for(const n in e)void 0!==e[n]&&(t[n]=e[n]);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 n=this.attributes;for(const e in n){const i=n[e];t.data.attributes[e]=i.toJSON(t.data)}const i={};let r=!1;for(const e in this.morphAttributes){const n=this.morphAttributes[e],a=[];for(let e=0,i=n.length;e0&&(i[e]=a,r=!0)}r&&(t.data.morphAttributes=i,t.data.morphTargetsRelative=this.morphTargetsRelative);const a=this.groups;a.length>0&&(t.data.groups=JSON.parse(JSON.stringify(a)));const o=this.boundingSphere;return null!==o&&(t.data.boundingSphere={center:o.center.toArray(),radius:o.radius}),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 n=t.index;null!==n&&this.setIndex(n.clone(e));const i=t.attributes;for(const t in i){const n=i[t];this.setAttribute(t,n.clone(e))}const r=t.morphAttributes;for(const t in r){const n=[],i=r[t];for(let t=0,r=i.length;t0){const n=t[e[0]];if(void 0!==n){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=n.length;t(t.far-t.near)**2)return}Hn.copy(r).invert(),Gn.copy(t.ray).applyMatrix4(Hn),null!==n.boundingBox&&!1===Gn.intersectsBox(n.boundingBox)||this._computeIntersections(t,e,Gn)}}_computeIntersections(t,e,n){let i;const r=this.geometry,a=this.material,o=r.index,s=r.attributes.position,l=r.attributes.uv,c=r.attributes.uv1,u=r.attributes.normal,h=r.groups,d=r.drawRange;if(null!==o)if(Array.isArray(a))for(let r=0,s=h.length;rn.far?null:{distance:c,point:ii.clone(),object:t}}(t,e,n,i,Wn,Xn,qn,ni);if(u){r&&(Kn.fromBufferAttribute(r,s),Zn.fromBufferAttribute(r,l),Jn.fromBufferAttribute(r,c),u.uv=vn.getInterpolation(ni,Wn,Xn,qn,Kn,Zn,Jn,new Ct)),a&&(Kn.fromBufferAttribute(a,s),Zn.fromBufferAttribute(a,l),Jn.fromBufferAttribute(a,c),u.uv1=vn.getInterpolation(ni,Wn,Xn,qn,Kn,Zn,Jn,new Ct)),o&&(Qn.fromBufferAttribute(o,s),ti.fromBufferAttribute(o,l),ei.fromBufferAttribute(o,c),u.normal=vn.getInterpolation(ni,Wn,Xn,qn,Qn,ti,ei,new ne),u.normal.dot(i.direction)>0&&u.normal.multiplyScalar(-1));const t={a:s,b:l,c:c,normal:new ne,materialIndex:0};vn.getNormal(Wn,Xn,qn,t.normal),u.face=t}return u}class oi extends Bn{constructor(t=1,e=1,n=1,i=1,r=1,a=1){super(),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:n,widthSegments:i,heightSegments:r,depthSegments:a};const o=this;i=Math.floor(i),r=Math.floor(r),a=Math.floor(a);const s=[],l=[],c=[],u=[];let h=0,d=0;function p(t,e,n,i,r,a,p,f,m,g,v){const _=a/m,y=p/g,x=a/2,b=p/2,M=f/2,S=m+1,E=g+1;let w=0,T=0;const A=new ne;for(let a=0;a0?1:-1,c.push(A.x,A.y,A.z),u.push(s/m),u.push(1-a/g),w+=1}}for(let t=0;t0&&(e.defines=this.defines),e.vertexShader=this.vertexShader,e.fragmentShader=this.fragmentShader,e.lights=this.lights,e.clipping=this.clipping;const n={};for(const t in this.extensions)!0===this.extensions[t]&&(n[t]=!0);return Object.keys(n).length>0&&(e.extensions=n),e}}class di extends on{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new Oe,this.projectionMatrix=new Oe,this.projectionMatrixInverse=new Oe,this.coordinateSystem=pt}copy(t,e){return super.copy(t,e),this.matrixWorldInverse.copy(t.matrixWorldInverse),this.projectionMatrix.copy(t.projectionMatrix),this.projectionMatrixInverse.copy(t.projectionMatrixInverse),this.coordinateSystem=t.coordinateSystem,this}getWorldDirection(t){return super.getWorldDirection(t).negate()}updateMatrixWorld(t){super.updateMatrixWorld(t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(t,e){super.updateWorldMatrix(t,e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return(new this.constructor).copy(this)}}const pi=new ne,fi=new Ct,mi=new Ct;class gi extends di{constructor(t=50,e=1,n=.1,i=2e3){super(),this.isPerspectiveCamera=!0,this.type="PerspectiveCamera",this.fov=t,this.zoom=1,this.near=n,this.far=i,this.focus=10,this.aspect=e,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.fov=t.fov,this.zoom=t.zoom,this.near=t.near,this.far=t.far,this.focus=t.focus,this.aspect=t.aspect,this.view=null===t.view?null:Object.assign({},t.view),this.filmGauge=t.filmGauge,this.filmOffset=t.filmOffset,this}setFocalLength(t){const e=.5*this.getFilmHeight()/t;this.fov=2*yt*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(.5*_t*this.fov);return.5*this.getFilmHeight()/t}getEffectiveFOV(){return 2*yt*Math.atan(Math.tan(.5*_t*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}getViewBounds(t,e,n){pi.set(-1,-1,.5).applyMatrix4(this.projectionMatrixInverse),e.set(pi.x,pi.y).multiplyScalar(-t/pi.z),pi.set(1,1,.5).applyMatrix4(this.projectionMatrixInverse),n.set(pi.x,pi.y).multiplyScalar(-t/pi.z)}getViewSize(t,e){return this.getViewBounds(t,fi,mi),e.subVectors(mi,fi)}setViewOffset(t,e,n,i,r,a){this.aspect=t/e,null===this.view&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=n,this.view.offsetY=i,this.view.width=r,this.view.height=a,this.updateProjectionMatrix()}clearViewOffset(){null!==this.view&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=this.near;let e=t*Math.tan(.5*_t*this.fov)/this.zoom,n=2*e,i=this.aspect*n,r=-.5*i;const a=this.view;if(null!==this.view&&this.view.enabled){const t=a.fullWidth,o=a.fullHeight;r+=a.offsetX*i/t,e-=a.offsetY*n/o,i*=a.width/t,n*=a.height/o}const o=this.filmOffset;0!==o&&(r+=t*o/this.getFilmWidth()),this.projectionMatrix.makePerspective(r,r+i,e,e-n,t,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.fov=this.fov,e.object.zoom=this.zoom,e.object.near=this.near,e.object.far=this.far,e.object.focus=this.focus,e.object.aspect=this.aspect,null!==this.view&&(e.object.view=Object.assign({},this.view)),e.object.filmGauge=this.filmGauge,e.object.filmOffset=this.filmOffset,e}}const vi=-90;class _i extends on{constructor(t,e,n){super(),this.type="CubeCamera",this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;const i=new gi(vi,1,t,e);i.layers=this.layers,this.add(i);const r=new gi(vi,1,t,e);r.layers=this.layers,this.add(r);const a=new gi(vi,1,t,e);a.layers=this.layers,this.add(a);const o=new gi(vi,1,t,e);o.layers=this.layers,this.add(o);const s=new gi(vi,1,t,e);s.layers=this.layers,this.add(s);const l=new gi(vi,1,t,e);l.layers=this.layers,this.add(l)}updateCoordinateSystem(){const t=this.coordinateSystem,e=this.children.concat(),[n,i,r,a,o,s]=e;for(const t of e)this.remove(t);if(t===pt)n.up.set(0,1,0),n.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),a.up.set(0,0,1),a.lookAt(0,-1,0),o.up.set(0,1,0),o.lookAt(0,0,1),s.up.set(0,1,0),s.lookAt(0,0,-1);else{if(t!==ft)throw new Error("THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: "+t);n.up.set(0,-1,0),n.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),a.up.set(0,0,-1),a.lookAt(0,-1,0),o.up.set(0,-1,0),o.lookAt(0,0,1),s.up.set(0,-1,0),s.lookAt(0,0,-1)}for(const t of e)this.add(t),t.updateMatrixWorld()}update(t,e){null===this.parent&&this.updateMatrixWorld();const{renderTarget:n,activeMipmapLevel:i}=this;this.coordinateSystem!==t.coordinateSystem&&(this.coordinateSystem=t.coordinateSystem,this.updateCoordinateSystem());const[r,a,o,s,l,c]=this.children,u=t.getRenderTarget(),h=t.getActiveCubeFace(),d=t.getActiveMipmapLevel(),p=t.xr.enabled;t.xr.enabled=!1;const f=n.texture.generateMipmaps;n.texture.generateMipmaps=!1,t.setRenderTarget(n,0,i),t.render(e,r),t.setRenderTarget(n,1,i),t.render(e,a),t.setRenderTarget(n,2,i),t.render(e,o),t.setRenderTarget(n,3,i),t.render(e,s),t.setRenderTarget(n,4,i),t.render(e,l),n.texture.generateMipmaps=f,t.setRenderTarget(n,5,i),t.render(e,c),t.setRenderTarget(u,h,d),t.xr.enabled=p,n.texture.needsPMREMUpdate=!0}}class yi extends $t{constructor(t,e,n,i,r,a,o,s,l,c){super(t=void 0!==t?t:[],e=void 0!==e?e:C,n,i,r,a,o,s,l,c),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(t){this.image=t}}class xi extends Jt{constructor(t=1,e={}){super(t,t,e),this.isWebGLCubeRenderTarget=!0;const n={width:t,height:t,depth:1},i=[n,n,n,n,n,n];this.texture=new yi(i,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.format,e.type,e.anisotropy,e.colorSpace),this.texture.isRenderTargetTexture=!0,this.texture.generateMipmaps=void 0!==e.generateMipmaps&&e.generateMipmaps,this.texture.minFilter=void 0!==e.minFilter?e.minFilter:F}fromEquirectangularTexture(t,e){this.texture.type=e.type,this.texture.colorSpace=e.colorSpace,this.texture.generateMipmaps=e.generateMipmaps,this.texture.minFilter=e.minFilter,this.texture.magFilter=e.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 oi(5,5,5),r=new hi({name:"CubemapFromEquirect",uniforms:si(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:g,blending:0});r.uniforms.tEquirect.value=e;const a=new ri(i,r),o=e.minFilter;e.minFilter===z&&(e.minFilter=F);return new _i(1,10,this).update(t,a),e.minFilter=o,a.geometry.dispose(),a.material.dispose(),this}clear(t,e,n,i){const r=t.getRenderTarget();for(let r=0;r<6;r++)t.setRenderTarget(this,r),t.clear(e,n,i);t.setRenderTarget(r)}}const bi=new ne,Mi=new ne,Si=new Pt;class Ei{constructor(t=new ne(1,0,0),e=0){this.isPlane=!0,this.normal=t,this.constant=e}set(t,e){return this.normal.copy(t),this.constant=e,this}setComponents(t,e,n,i){return this.normal.set(t,e,n),this.constant=i,this}setFromNormalAndCoplanarPoint(t,e){return this.normal.copy(t),this.constant=-e.dot(this.normal),this}setFromCoplanarPoints(t,e,n){const i=bi.subVectors(n,e).cross(Mi.subVectors(t,e)).normalize();return this.setFromNormalAndCoplanarPoint(i,t),this}copy(t){return this.normal.copy(t.normal),this.constant=t.constant,this}normalize(){const t=1/this.normal.length();return this.normal.multiplyScalar(t),this.constant*=t,this}negate(){return this.constant*=-1,this.normal.negate(),this}distanceToPoint(t){return this.normal.dot(t)+this.constant}distanceToSphere(t){return this.distanceToPoint(t.center)-t.radius}projectPoint(t,e){return e.copy(t).addScaledVector(this.normal,-this.distanceToPoint(t))}intersectLine(t,e){const n=t.delta(bi),i=this.normal.dot(n);if(0===i)return 0===this.distanceToPoint(t.start)?e.copy(t.start):null;const r=-(t.start.dot(this.normal)+this.constant)/i;return r<0||r>1?null:e.copy(t.start).addScaledVector(n,r)}intersectsLine(t){const e=this.distanceToPoint(t.start),n=this.distanceToPoint(t.end);return e<0&&n>0||n<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 n=e||Si.getNormalMatrix(t),i=this.coplanarPoint(bi).applyMatrix4(t),r=this.normal.applyMatrix3(n).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 wi=new Se,Ti=new ne;class Ai{constructor(t=new Ei,e=new Ei,n=new Ei,i=new Ei,r=new Ei,a=new Ei){this.planes=[t,e,n,i,r,a]}set(t,e,n,i,r,a){const o=this.planes;return o[0].copy(t),o[1].copy(e),o[2].copy(n),o[3].copy(i),o[4].copy(r),o[5].copy(a),this}copy(t){const e=this.planes;for(let n=0;n<6;n++)e[n].copy(t.planes[n]);return this}setFromProjectionMatrix(t,e=2e3){const n=this.planes,i=t.elements,r=i[0],a=i[1],o=i[2],s=i[3],l=i[4],c=i[5],u=i[6],h=i[7],d=i[8],p=i[9],f=i[10],m=i[11],g=i[12],v=i[13],_=i[14],y=i[15];if(n[0].setComponents(s-r,h-l,m-d,y-g).normalize(),n[1].setComponents(s+r,h+l,m+d,y+g).normalize(),n[2].setComponents(s+a,h+c,m+p,y+v).normalize(),n[3].setComponents(s-a,h-c,m-p,y-v).normalize(),n[4].setComponents(s-o,h-u,m-f,y-_).normalize(),e===pt)n[5].setComponents(s+o,h+u,m+f,y+_).normalize();else{if(e!==ft)throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+e);n[5].setComponents(o,u,f,_).normalize()}return this}intersectsObject(t){if(void 0!==t.boundingSphere)null===t.boundingSphere&&t.computeBoundingSphere(),wi.copy(t.boundingSphere).applyMatrix4(t.matrixWorld);else{const e=t.geometry;null===e.boundingSphere&&e.computeBoundingSphere(),wi.copy(e.boundingSphere).applyMatrix4(t.matrixWorld)}return this.intersectsSphere(wi)}intersectsSprite(t){return wi.center.set(0,0,0),wi.radius=.7071067811865476,wi.applyMatrix4(t.matrixWorld),this.intersectsSphere(wi)}intersectsSphere(t){const e=this.planes,n=t.center,i=-t.radius;for(let t=0;t<6;t++){if(e[t].distanceToPoint(n)0?t.max.x:t.min.x,Ti.y=i.normal.y>0?t.max.y:t.min.y,Ti.z=i.normal.z>0?t.max.z:t.min.z,i.distanceToPoint(Ti)<0)return!1}return!0}containsPoint(t){const e=this.planes;for(let n=0;n<6;n++)if(e[n].distanceToPoint(t)<0)return!1;return!0}clone(){return(new this.constructor).copy(this)}}function Ri(){let t=null,e=!1,n=null,i=null;function r(e,a){n(e,a),i=t.requestAnimationFrame(r)}return{start:function(){!0!==e&&null!==n&&(i=t.requestAnimationFrame(r),e=!0)},stop:function(){t.cancelAnimationFrame(i),e=!1},setAnimationLoop:function(t){n=t},setContext:function(e){t=e}}}function Ci(t,e){const n=e.isWebGL2,i=new WeakMap;return{get:function(t){return t.isInterleavedBufferAttribute&&(t=t.data),i.get(t)},remove:function(e){e.isInterleavedBufferAttribute&&(e=e.data);const n=i.get(e);n&&(t.deleteBuffer(n.buffer),i.delete(e))},update:function(e,r){if(e.isGLBufferAttribute){const t=i.get(e);return void((!t||t.version 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_ALPHA )\n\tdiffuseColor *= vColor;\n#elif defined( USE_COLOR )\n\tdiffuseColor.rgb *= vColor;\n#endif",color_pars_fragment:"#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR )\n\tvarying vec3 vColor;\n#endif",color_pars_vertex:"#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR )\n\tvarying vec3 vColor;\n#endif",color_vertex:"#if defined( USE_COLOR_ALPHA )\n\tvColor = vec4( 1.0 );\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR )\n\tvColor = vec3( 1.0 );\n#endif\n#ifdef USE_COLOR\n\tvColor *= color;\n#endif\n#ifdef USE_INSTANCING_COLOR\n\tvColor.xyz *= instanceColor.xyz;\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}\nvec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );\n}\nmat3 transposeMat3( const in mat3 m ) {\n\tmat3 tmp;\n\ttmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x );\n\ttmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y );\n\ttmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z );\n\treturn tmp;\n}\nfloat luminance( const in vec3 rgb ) {\n\tconst vec3 weights = vec3( 0.2126729, 0.7151522, 0.0721750 );\n\treturn dot( weights, rgb );\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\t#ifdef FLIP_SIDED\n\t\ttransformedTangent = - transformedTangent;\n\t#endif\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\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:"\nconst mat3 LINEAR_SRGB_TO_LINEAR_DISPLAY_P3 = mat3(\n\tvec3( 0.8224621, 0.177538, 0.0 ),\n\tvec3( 0.0331941, 0.9668058, 0.0 ),\n\tvec3( 0.0170827, 0.0723974, 0.9105199 )\n);\nconst mat3 LINEAR_DISPLAY_P3_TO_LINEAR_SRGB = mat3(\n\tvec3( 1.2249401, - 0.2249404, 0.0 ),\n\tvec3( - 0.0420569, 1.0420571, 0.0 ),\n\tvec3( - 0.0196376, - 0.0786361, 1.0982735 )\n);\nvec4 LinearSRGBToLinearDisplayP3( in vec4 value ) {\n\treturn vec4( value.rgb * LINEAR_SRGB_TO_LINEAR_DISPLAY_P3, value.a );\n}\nvec4 LinearDisplayP3ToLinearSRGB( in vec4 value ) {\n\treturn vec4( value.rgb * LINEAR_DISPLAY_P3_TO_LINEAR_SRGB, value.a );\n}\nvec4 LinearTransferOETF( in vec4 value ) {\n\treturn value;\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}\nvec4 LinearToLinear( in vec4 value ) {\n\treturn value;\n}\nvec4 LinearTosRGB( in vec4 value ) {\n\treturn sRGBTransferOETF( value );\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 = inverseTransformDirection( 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 * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\n\t#else\n\t\tvec4 envColor = vec4( 0.0 );\n\t#endif\n\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t#endif\n#endif",envmap_common_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float envMapIntensity;\n\tuniform float flipEnvMap;\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\t\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 = inverseTransformDirection( 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, roughness * roughness) );\n\t\t\treflectVec = inverseTransformDirection( 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 = inverseTransformDirection( 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_fragment:"#ifdef USE_LIGHTMAP\n\tvec4 lightMapTexel = texture2D( lightMap, vLightMapUv );\n\tvec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity;\n\treflectedLight.indirectDiffuse += lightMapIrradiance;\n#endif",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 = inverseTransformDirection( 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\t#if defined ( LEGACY_LIGHTS )\n\t\tif ( cutoffDistance > 0.0 && decayExponent > 0.0 ) {\n\t\t\treturn pow( saturate( - lightDistance / cutoffDistance + 1.0 ), decayExponent );\n\t\t}\n\t\treturn 1.0;\n\t#else\n\t\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\n\t\tif ( cutoffDistance > 0.0 ) {\n\t\t\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t\t}\n\t\treturn distanceFalloff;\n\t#endif\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",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 * ( 1.0 - 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 = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor );\n#else\n\tmaterial.specularColor = mix( vec3( 0.04 ), 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_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.07, 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:"struct PhysicalMaterial {\n\tvec3 diffuseColor;\n\tfloat roughness;\n\tvec3 specularColor;\n\tfloat specularF90;\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#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\tfloat v = 0.5 / ( gv + gl );\n\t\treturn saturate(v);\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.specularColor;\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 * transposeMat3( 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 a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95;\n\tfloat b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72;\n\tfloat DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) );\n\treturn saturate( DG * RECIPROCAL_PI );\n}\nvec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tconst vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );\n\tconst vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 );\n\tvec4 r = roughness * c0 + c1;\n\tfloat a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y;\n\tvec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw;\n\treturn fab;\n}\nvec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) {\n\tvec2 fab = DFGApprox( normal, viewDir, roughness );\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\tvec2 fab = DFGApprox( normal, viewDir, roughness );\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}\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.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y );\n\t\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\n\t\treflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\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\t\tsheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness );\n\t#endif\n\treflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material );\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\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\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\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 );\n\t#endif\n\tvec3 singleScattering = vec3( 0.0 );\n\tvec3 multiScattering = vec3( 0.0 );\n\tvec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;\n\t#ifdef USE_IRIDESCENCE\n\t\tcomputeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering );\n\t#else\n\t\tcomputeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering );\n\t#endif\n\tvec3 totalScattering = singleScattering + multiScattering;\n\tvec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) );\n\treflectedLight.indirectSpecular += radiance * singleScattering;\n\treflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;\n\treflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance;\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.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );\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 )\n\t\tpointLightShadow = pointLightShadows[ i ];\n\t\tdirectLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, 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.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.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#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( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV )\n\t\tiblIrradiance += getIBLIrradiance( geometryNormal );\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\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",logdepthbuf_fragment:"#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tgl_FragDepthEXT = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif",logdepthbuf_pars_fragment:"#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tuniform float logDepthBufFC;\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif",logdepthbuf_pars_vertex:"#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvarying float vFragDepth;\n\t\tvarying float vIsPerspective;\n\t#else\n\t\tuniform float logDepthBufFC;\n\t#endif\n#endif",logdepthbuf_vertex:"#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvFragDepth = 1.0 + gl_Position.w;\n\t\tvIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) );\n\t#else\n\t\tif ( isPerspectiveMatrix( projectionMatrix ) ) {\n\t\t\tgl_Position.z = log2( max( EPSILON, gl_Position.w + 1.0 ) ) * logDepthBufFC - 1.0;\n\t\t\tgl_Position.z *= gl_Position.w;\n\t\t}\n\t#endif\n#endif",map_fragment:"#ifdef USE_MAP\n\tvec4 sampledDiffuseColor = texture2D( map, vMapUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\tsampledDiffuseColor = vec4( mix( pow( sampledDiffuseColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), sampledDiffuseColor.rgb * 0.0773993808, vec3( lessThanEqual( sampledDiffuseColor.rgb, vec3( 0.04045 ) ) ) ), sampledDiffuseColor.w );\n\t\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 ) && defined( MORPHTARGETS_TEXTURE )\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\t#ifdef MORPHTARGETS_TEXTURE\n\t\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ];\n\t\t}\n\t#else\n\t\tobjectNormal += morphNormal0 * morphTargetInfluences[ 0 ];\n\t\tobjectNormal += morphNormal1 * morphTargetInfluences[ 1 ];\n\t\tobjectNormal += morphNormal2 * morphTargetInfluences[ 2 ];\n\t\tobjectNormal += morphNormal3 * morphTargetInfluences[ 3 ];\n\t#endif\n#endif",morphtarget_pars_vertex:"#ifdef USE_MORPHTARGETS\n\t#ifndef USE_INSTANCING_MORPH\n\t\tuniform float morphTargetBaseInfluence;\n\t#endif\n\t#ifdef MORPHTARGETS_TEXTURE\n\t\t#ifndef USE_INSTANCING_MORPH\n\t\t\tuniform float morphTargetInfluences[ MORPHTARGETS_COUNT ];\n\t\t#endif\n\t\tuniform sampler2DArray morphTargetsTexture;\n\t\tuniform ivec2 morphTargetsTextureSize;\n\t\tvec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) {\n\t\t\tint texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset;\n\t\t\tint y = texelIndex / morphTargetsTextureSize.x;\n\t\t\tint x = texelIndex - y * morphTargetsTextureSize.x;\n\t\t\tivec3 morphUV = ivec3( x, y, morphTargetIndex );\n\t\t\treturn texelFetch( morphTargetsTexture, morphUV, 0 );\n\t\t}\n\t#else\n\t\t#ifndef USE_MORPHNORMALS\n\t\t\tuniform float morphTargetInfluences[ 8 ];\n\t\t#else\n\t\t\tuniform float morphTargetInfluences[ 4 ];\n\t\t#endif\n\t#endif\n#endif",morphtarget_vertex:"#ifdef USE_MORPHTARGETS\n\ttransformed *= morphTargetBaseInfluence;\n\t#ifdef MORPHTARGETS_TEXTURE\n\t\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ];\n\t\t}\n\t#else\n\t\ttransformed += morphTarget0 * morphTargetInfluences[ 0 ];\n\t\ttransformed += morphTarget1 * morphTargetInfluences[ 1 ];\n\t\ttransformed += morphTarget2 * morphTargetInfluences[ 2 ];\n\t\ttransformed += morphTarget3 * morphTargetInfluences[ 3 ];\n\t\t#ifndef USE_MORPHNORMALS\n\t\t\ttransformed += morphTarget4 * morphTargetInfluences[ 4 ];\n\t\t\ttransformed += morphTarget5 * morphTargetInfluences[ 5 ];\n\t\t\ttransformed += morphTarget6 * morphTargetInfluences[ 6 ];\n\t\t\ttransformed += morphTarget7 * morphTargetInfluences[ 7 ];\n\t\t#endif\n\t#endif\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#if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED )\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#if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED )\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\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#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.;\nconst vec3 PackFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );\nconst vec4 UnpackFactors = UnpackDownscale / vec4( PackFactors, 1. );\nconst float ShiftRight8 = 1. / 256.;\nvec4 packDepthToRGBA( const in float v ) {\n\tvec4 r = vec4( fract( v * PackFactors ), v );\n\tr.yzw -= r.xyz * ShiftRight8;\treturn r * PackUpscale;\n}\nfloat unpackRGBAToDepth( const in vec4 v ) {\n\treturn dot( v, UnpackFactors );\n}\nvec2 packDepthToRG( in highp float v ) {\n\treturn packDepthToRGBA( v ).yx;\n}\nfloat unpackRGToDepth( const in highp vec2 v ) {\n\treturn unpackRGBAToDepth( vec4( v.xy, 0.0, 0.0 ) );\n}\nvec4 pack2HalfToRGBA( 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( 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\treturn depth * ( near - far ) - near;\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\treturn ( near * far ) / ( ( far - near ) * depth - far );\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\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\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\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tstruct SpotLightShadow {\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 sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\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\tfloat texture2DCompare( sampler2D depths, vec2 uv, float compare ) {\n\t\treturn step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) );\n\t}\n\tvec2 texture2DDistribution( sampler2D shadow, vec2 uv ) {\n\t\treturn unpackRGBATo2Half( texture2D( shadow, uv ) );\n\t}\n\tfloat VSMShadow (sampler2D shadow, vec2 uv, float compare ){\n\t\tfloat occlusion = 1.0;\n\t\tvec2 distribution = texture2DDistribution( shadow, uv );\n\t\tfloat hard_shadow = step( compare , distribution.x );\n\t\tif (hard_shadow != 1.0 ) {\n\t\t\tfloat distance = compare - distribution.x ;\n\t\t\tfloat variance = max( 0.00000, distribution.y * distribution.y );\n\t\t\tfloat softness_probability = variance / (variance + distance * distance );\t\t\tsoftness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 );\t\t\tocclusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 );\n\t\t}\n\t\treturn occlusion;\n\t}\n\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\tfloat shadow = 1.0;\n\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\tshadowCoord.z += shadowBias;\n\t\tbool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0;\n\t\tbool frustumTest = inFrustum && shadowCoord.z <= 1.0;\n\t\tif ( frustumTest ) {\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx0 = - texelSize.x * shadowRadius;\n\t\t\tfloat dy0 = - texelSize.y * shadowRadius;\n\t\t\tfloat dx1 = + texelSize.x * shadowRadius;\n\t\t\tfloat dy1 = + texelSize.y * shadowRadius;\n\t\t\tfloat dx2 = dx0 / 2.0;\n\t\t\tfloat dy2 = dy0 / 2.0;\n\t\t\tfloat dx3 = dx1 / 2.0;\n\t\t\tfloat dy3 = dy1 / 2.0;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )\n\t\t\t) * ( 1.0 / 17.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx = texelSize.x;\n\t\t\tfloat dy = texelSize.y;\n\t\t\tvec2 uv = shadowCoord.xy;\n\t\t\tvec2 f = fract( uv * shadowMapSize + 0.5 );\n\t\t\tuv -= f * texelSize;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, uv, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t f.x ),\n\t\t\t\t\t mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t f.x ),\n\t\t\t\t\t f.y )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_VSM )\n\t\t\tshadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#else\n\t\t\tshadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#endif\n\t\t}\n\t\treturn shadow;\n\t}\n\tvec2 cubeToUV( vec3 v, float texelSizeY ) {\n\t\tvec3 absV = abs( v );\n\t\tfloat scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) );\n\t\tabsV *= scaleToCube;\n\t\tv *= scaleToCube * ( 1.0 - 2.0 * texelSizeY );\n\t\tvec2 planar = v.xy;\n\t\tfloat almostATexel = 1.5 * texelSizeY;\n\t\tfloat almostOne = 1.0 - almostATexel;\n\t\tif ( absV.z >= almostOne ) {\n\t\t\tif ( v.z > 0.0 )\n\t\t\t\tplanar.x = 4.0 - v.x;\n\t\t} else if ( absV.x >= almostOne ) {\n\t\t\tfloat signX = sign( v.x );\n\t\t\tplanar.x = v.z * signX + 2.0 * signX;\n\t\t} else if ( absV.y >= almostOne ) {\n\t\t\tfloat signY = sign( v.y );\n\t\t\tplanar.x = v.x + 2.0 * signY + 2.0;\n\t\t\tplanar.y = v.z * signY - 2.0;\n\t\t}\n\t\treturn vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 );\n\t}\n\tfloat getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tvec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) );\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\tfloat dp = ( length( lightToPosition ) - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear );\t\tdp += shadowBias;\n\t\tvec3 bd3D = normalize( lightToPosition );\n\t\t#if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM )\n\t\t\tvec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y;\n\t\t\treturn (\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#else\n\t\t\treturn texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp );\n\t\t#endif\n\t}\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 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 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 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\tvec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\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.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.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\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.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 OptimizedCineonToneMapping( 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\tfloat startCompression = 0.8 - 0.04;\n\tfloat 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(1, 1, 1), 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 = inverseTransformDirection( normal, viewMatrix );\n\tvec4 transmitted = getIBLVolumeRefraction(\n\t\tn, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90,\n\t\tpos, modelMatrix, viewMatrix, projectionMatrix, 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 ior, const in float thickness,\n\t\tconst in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix );\n\t\tvec3 refractedRayExit = position + transmissionRay;\n\t\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\n\t\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\n\t\trefractionCoords += 1.0;\n\t\trefractionCoords /= 2.0;\n\t\tvec4 transmittedLight = getTransmissionSample( refractionCoords, roughness, ior );\n\t\tvec3 transmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance );\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 flipEnvMap;\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 * vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) );\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\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;\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#endif\n}",distanceRGBA_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}",distanceRGBA_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 \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 = packDepthToRGBA( dist );\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 \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 \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( packNormalToRGB( normal ), 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 \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_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 \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\t\tfloat sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor );\n\t\toutgoingLight = outgoingLight * sheenEnergyComp + sheenSpecularDirect + sheenSpecularIndirect;\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 \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 \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}",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 * vec4( 0.0, 0.0, 0.0, 1.0 );\n\tvec2 scale;\n\tscale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );\n\tscale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );\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}"},Oi={common:{diffuse:{value:new Mn(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new Pt},alphaMap:{value:null},alphaMapTransform:{value:new Pt},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new Pt}},envmap:{envMap:{value:null},envMapRotation:{value:new Pt},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new Pt}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new Pt}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new Pt},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new Pt},normalScale:{value:new Ct(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new Pt},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new Pt}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new Pt}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new Pt}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new Mn(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMap:{value:[]},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotShadowMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMap:{value:[]},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null}},points:{diffuse:{value:new Mn(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new Pt},alphaTest:{value:0},uvTransform:{value:new Pt}},sprite:{diffuse:{value:new Mn(16777215)},opacity:{value:1},center:{value:new Ct(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Pt},alphaMap:{value:null},alphaMapTransform:{value:new Pt},alphaTest:{value:0}}},Di={basic:{uniforms:li([Oi.common,Oi.specularmap,Oi.envmap,Oi.aomap,Oi.lightmap,Oi.fog]),vertexShader:Li.meshbasic_vert,fragmentShader:Li.meshbasic_frag},lambert:{uniforms:li([Oi.common,Oi.specularmap,Oi.envmap,Oi.aomap,Oi.lightmap,Oi.emissivemap,Oi.bumpmap,Oi.normalmap,Oi.displacementmap,Oi.fog,Oi.lights,{emissive:{value:new Mn(0)}}]),vertexShader:Li.meshlambert_vert,fragmentShader:Li.meshlambert_frag},phong:{uniforms:li([Oi.common,Oi.specularmap,Oi.envmap,Oi.aomap,Oi.lightmap,Oi.emissivemap,Oi.bumpmap,Oi.normalmap,Oi.displacementmap,Oi.fog,Oi.lights,{emissive:{value:new Mn(0)},specular:{value:new Mn(1118481)},shininess:{value:30}}]),vertexShader:Li.meshphong_vert,fragmentShader:Li.meshphong_frag},standard:{uniforms:li([Oi.common,Oi.envmap,Oi.aomap,Oi.lightmap,Oi.emissivemap,Oi.bumpmap,Oi.normalmap,Oi.displacementmap,Oi.roughnessmap,Oi.metalnessmap,Oi.fog,Oi.lights,{emissive:{value:new Mn(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:Li.meshphysical_vert,fragmentShader:Li.meshphysical_frag},toon:{uniforms:li([Oi.common,Oi.aomap,Oi.lightmap,Oi.emissivemap,Oi.bumpmap,Oi.normalmap,Oi.displacementmap,Oi.gradientmap,Oi.fog,Oi.lights,{emissive:{value:new Mn(0)}}]),vertexShader:Li.meshtoon_vert,fragmentShader:Li.meshtoon_frag},matcap:{uniforms:li([Oi.common,Oi.bumpmap,Oi.normalmap,Oi.displacementmap,Oi.fog,{matcap:{value:null}}]),vertexShader:Li.meshmatcap_vert,fragmentShader:Li.meshmatcap_frag},points:{uniforms:li([Oi.points,Oi.fog]),vertexShader:Li.points_vert,fragmentShader:Li.points_frag},dashed:{uniforms:li([Oi.common,Oi.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:Li.linedashed_vert,fragmentShader:Li.linedashed_frag},depth:{uniforms:li([Oi.common,Oi.displacementmap]),vertexShader:Li.depth_vert,fragmentShader:Li.depth_frag},normal:{uniforms:li([Oi.common,Oi.bumpmap,Oi.normalmap,Oi.displacementmap,{opacity:{value:1}}]),vertexShader:Li.meshnormal_vert,fragmentShader:Li.meshnormal_frag},sprite:{uniforms:li([Oi.sprite,Oi.fog]),vertexShader:Li.sprite_vert,fragmentShader:Li.sprite_frag},background:{uniforms:{uvTransform:{value:new Pt},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:Li.background_vert,fragmentShader:Li.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new Pt}},vertexShader:Li.backgroundCube_vert,fragmentShader:Li.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:Li.cube_vert,fragmentShader:Li.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:Li.equirect_vert,fragmentShader:Li.equirect_frag},distanceRGBA:{uniforms:li([Oi.common,Oi.displacementmap,{referencePosition:{value:new ne},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:Li.distanceRGBA_vert,fragmentShader:Li.distanceRGBA_frag},shadow:{uniforms:li([Oi.lights,Oi.fog,{color:{value:new Mn(0)},opacity:{value:1}}]),vertexShader:Li.shadow_vert,fragmentShader:Li.shadow_frag}};Di.physical={uniforms:li([Di.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Pt},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Pt},clearcoatNormalScale:{value:new Ct(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new Pt},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new Pt},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new Pt},sheen:{value:0},sheenColor:{value:new Mn(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new Pt},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new Pt},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new Pt},transmissionSamplerSize:{value:new Ct},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Pt},attenuationDistance:{value:0},attenuationColor:{value:new Mn(0)},specularColor:{value:new Mn(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Pt},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Pt},anisotropyVector:{value:new Ct},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Pt}}]),vertexShader:Li.meshphysical_vert,fragmentShader:Li.meshphysical_frag};const Ni={r:0,b:0,g:0},Ii=new Ge,Ui=new Oe;function Fi(t,e,n,i,r,a,o){const s=new Mn(0);let l,c,u=!0===a?0:1,h=null,d=0,p=null;function f(e,n){e.getRGB(Ni,ci(t)),i.buffers.color.setClear(Ni.r,Ni.g,Ni.b,n,o)}return{getClearColor:function(){return s},setClearColor:function(t,e=1){s.set(t),u=e,f(s,u)},getClearAlpha:function(){return u},setClearAlpha:function(t){u=t,f(s,u)},render:function(a,v){let _=!1,y=!0===v.isScene?v.background:null;if(y&&y.isTexture){y=(v.backgroundBlurriness>0?n:e).get(y)}null===y?f(s,u):y&&y.isColor&&(f(y,1),_=!0);const x=t.xr.getEnvironmentBlendMode();"additive"===x?i.buffers.color.setClear(0,0,0,1,o):"alpha-blend"===x&&i.buffers.color.setClear(0,0,0,0,o),(t.autoClear||_)&&t.clear(t.autoClearColor,t.autoClearDepth,t.autoClearStencil),y&&(y.isCubeTexture||y.mapping===L)?(void 0===c&&(c=new ri(new oi(1,1,1),new hi({name:"BackgroundCubeMaterial",uniforms:si(Di.backgroundCube.uniforms),vertexShader:Di.backgroundCube.vertexShader,fragmentShader:Di.backgroundCube.fragmentShader,side:g,depthTest:!1,depthWrite:!1,fog:!1})),c.geometry.deleteAttribute("normal"),c.geometry.deleteAttribute("uv"),c.onBeforeRender=function(t,e,n){this.matrixWorld.copyPosition(n.matrixWorld)},Object.defineProperty(c.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),r.update(c)),Ii.copy(v.backgroundRotation),Ii.x*=-1,Ii.y*=-1,Ii.z*=-1,y.isCubeTexture&&!1===y.isRenderTargetTexture&&(Ii.y*=-1,Ii.z*=-1),c.material.uniforms.envMap.value=y,c.material.uniforms.flipEnvMap.value=y.isCubeTexture&&!1===y.isRenderTargetTexture?-1:1,c.material.uniforms.backgroundBlurriness.value=v.backgroundBlurriness,c.material.uniforms.backgroundIntensity.value=v.backgroundIntensity,c.material.uniforms.backgroundRotation.value.setFromMatrix4(Ui.makeRotationFromEuler(Ii)),c.material.toneMapped=Bt.getTransfer(y.colorSpace)!==st,h===y&&d===y.version&&p===t.toneMapping||(c.material.needsUpdate=!0,h=y,d=y.version,p=t.toneMapping),c.layers.enableAll(),a.unshift(c,c.geometry,c.material,0,0,null)):y&&y.isTexture&&(void 0===l&&(l=new ri(new Pi(2,2),new hi({name:"BackgroundMaterial",uniforms:si(Di.background.uniforms),vertexShader:Di.background.vertexShader,fragmentShader:Di.background.fragmentShader,side:m,depthTest:!1,depthWrite:!1,fog:!1})),l.geometry.deleteAttribute("normal"),Object.defineProperty(l.material,"map",{get:function(){return this.uniforms.t2D.value}}),r.update(l)),l.material.uniforms.t2D.value=y,l.material.uniforms.backgroundIntensity.value=v.backgroundIntensity,l.material.toneMapped=Bt.getTransfer(y.colorSpace)!==st,!0===y.matrixAutoUpdate&&y.updateMatrix(),l.material.uniforms.uvTransform.value.copy(y.matrix),h===y&&d===y.version&&p===t.toneMapping||(l.material.needsUpdate=!0,h=y,d=y.version,p=t.toneMapping),l.layers.enableAll(),a.unshift(l,l.geometry,l.material,0,0,null))}}}function ki(t,e,n,i){const r=t.getParameter(t.MAX_VERTEX_ATTRIBS),a=i.isWebGL2?null:e.get("OES_vertex_array_object"),o=i.isWebGL2||null!==a,s={},l=p(null);let c=l,u=!1;function h(e){return i.isWebGL2?t.bindVertexArray(e):a.bindVertexArrayOES(e)}function d(e){return i.isWebGL2?t.deleteVertexArray(e):a.deleteVertexArrayOES(e)}function p(t){const e=[],n=[],i=[];for(let t=0;t=0){const n=r[e];let i=a[e];if(void 0===i&&("instanceMatrix"===e&&t.instanceMatrix&&(i=t.instanceMatrix),"instanceColor"===e&&t.instanceColor&&(i=t.instanceColor)),void 0===n)return!0;if(n.attribute!==i)return!0;if(i&&n.data!==i.data)return!0;o++}}return c.attributesNum!==o||c.index!==i}(r,y,d,x),b&&function(t,e,n,i){const r={},a=e.attributes;let o=0;const s=n.getAttributes();for(const e in s){if(s[e].location>=0){let n=a[e];void 0===n&&("instanceMatrix"===e&&t.instanceMatrix&&(n=t.instanceMatrix),"instanceColor"===e&&t.instanceColor&&(n=t.instanceColor));const i={};i.attribute=n,n&&n.data&&(i.data=n.data),r[e]=i,o++}}c.attributes=r,c.attributesNum=o,c.index=i}(r,y,d,x)}else{const t=!0===l.wireframe;c.geometry===y.id&&c.program===d.id&&c.wireframe===t||(c.geometry=y.id,c.program=d.id,c.wireframe=t,b=!0)}null!==x&&n.update(x,t.ELEMENT_ARRAY_BUFFER),(b||u)&&(u=!1,function(r,a,o,s){if(!1===i.isWebGL2&&(r.isInstancedMesh||s.isInstancedBufferGeometry)&&null===e.get("ANGLE_instanced_arrays"))return;f();const l=s.attributes,c=o.getAttributes(),u=a.defaultAttributeValues;for(const e in c){const a=c[e];if(a.location>=0){let o=l[e];if(void 0===o&&("instanceMatrix"===e&&r.instanceMatrix&&(o=r.instanceMatrix),"instanceColor"===e&&r.instanceColor&&(o=r.instanceColor)),void 0!==o){const e=o.normalized,l=o.itemSize,c=n.get(o);if(void 0===c)continue;const u=c.buffer,h=c.type,d=c.bytesPerElement,p=!0===i.isWebGL2&&(h===t.INT||h===t.UNSIGNED_INT||o.gpuType===G);if(o.isInterleavedBufferAttribute){const n=o.data,i=n.stride,c=o.offset;if(n.isInstancedInterleavedBuffer){for(let t=0;t0&&t.getShaderPrecisionFormat(t.FRAGMENT_SHADER,t.HIGH_FLOAT).precision>0)return"highp";e="mediump"}return"mediump"===e&&t.getShaderPrecisionFormat(t.VERTEX_SHADER,t.MEDIUM_FLOAT).precision>0&&t.getShaderPrecisionFormat(t.FRAGMENT_SHADER,t.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}const a="undefined"!=typeof WebGL2RenderingContext&&"WebGL2RenderingContext"===t.constructor.name;let o=void 0!==n.precision?n.precision:"highp";const s=r(o);s!==o&&(console.warn("THREE.WebGLRenderer:",o,"not supported, using",s,"instead."),o=s);const l=a||e.has("WEBGL_draw_buffers"),c=!0===n.logarithmicDepthBuffer,u=t.getParameter(t.MAX_TEXTURE_IMAGE_UNITS),h=t.getParameter(t.MAX_VERTEX_TEXTURE_IMAGE_UNITS),d=t.getParameter(t.MAX_TEXTURE_SIZE),p=t.getParameter(t.MAX_CUBE_MAP_TEXTURE_SIZE),f=t.getParameter(t.MAX_VERTEX_ATTRIBS),m=t.getParameter(t.MAX_VERTEX_UNIFORM_VECTORS),g=t.getParameter(t.MAX_VARYING_VECTORS),v=t.getParameter(t.MAX_FRAGMENT_UNIFORM_VECTORS),_=h>0,y=a||e.has("OES_texture_float");return{isWebGL2:a,drawBuffers:l,getMaxAnisotropy:function(){if(void 0!==i)return i;if(!0===e.has("EXT_texture_filter_anisotropic")){const n=e.get("EXT_texture_filter_anisotropic");i=t.getParameter(n.MAX_TEXTURE_MAX_ANISOTROPY_EXT)}else i=0;return i},getMaxPrecision:r,precision:o,logarithmicDepthBuffer:c,maxTextures:u,maxVertexTextures:h,maxTextureSize:d,maxCubemapSize:p,maxAttributes:f,maxVertexUniforms:m,maxVaryings:g,maxFragmentUniforms:v,vertexTextures:_,floatFragmentTextures:y,floatVertexTextures:_&&y,maxSamples:a?t.getParameter(t.MAX_SAMPLES):0}}function Hi(t){const e=this;let n=null,i=0,r=!1,a=!1;const o=new Ei,s=new Pt,l={value:null,needsUpdate:!1};function c(t,n,i,r){const a=null!==t?t.length:0;let c=null;if(0!==a){if(c=l.value,!0!==r||null===c){const e=i+4*a,r=n.matrixWorldInverse;s.getNormalMatrix(r),(null===c||c.length0);e.numPlanes=i,e.numIntersection=0}();else{const t=a?0:i,e=4*t;let r=f.clippingState||null;l.value=r,r=c(h,s,e,u);for(let t=0;t!==e;++t)r[t]=n[t];f.clippingState=r,this.numIntersection=d?this.numPlanes:0,this.numPlanes+=t}}}function Gi(t){let e=new WeakMap;function n(t,e){return 303===e?t.mapping=C:304===e&&(t.mapping=P),t}function i(t){const n=t.target;n.removeEventListener("dispose",i);const r=e.get(n);void 0!==r&&(e.delete(n),r.dispose())}return{get:function(r){if(r&&r.isTexture){const a=r.mapping;if(303===a||304===a){if(e.has(r)){return n(e.get(r).texture,r.mapping)}{const a=r.image;if(a&&a.height>0){const o=new xi(a.height);return o.fromEquirectangularTexture(t,r),e.set(r,o),r.addEventListener("dispose",i),n(o.texture,r.mapping)}return null}}}return r},dispose:function(){e=new WeakMap}}}class Vi extends di{constructor(t=-1,e=1,n=1,i=-1,r=.1,a=2e3){super(),this.isOrthographicCamera=!0,this.type="OrthographicCamera",this.zoom=1,this.view=null,this.left=t,this.right=e,this.top=n,this.bottom=i,this.near=r,this.far=a,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.left=t.left,this.right=t.right,this.top=t.top,this.bottom=t.bottom,this.near=t.near,this.far=t.far,this.zoom=t.zoom,this.view=null===t.view?null:Object.assign({},t.view),this}setViewOffset(t,e,n,i,r,a){null===this.view&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=n,this.view.offsetY=i,this.view.width=r,this.view.height=a,this.updateProjectionMatrix()}clearViewOffset(){null!==this.view&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=(this.right-this.left)/(2*this.zoom),e=(this.top-this.bottom)/(2*this.zoom),n=(this.right+this.left)/2,i=(this.top+this.bottom)/2;let r=n-t,a=n+t,o=i+e,s=i-e;if(null!==this.view&&this.view.enabled){const t=(this.right-this.left)/this.view.fullWidth/this.zoom,e=(this.top-this.bottom)/this.view.fullHeight/this.zoom;r+=t*this.view.offsetX,a=r+t*this.view.width,o-=e*this.view.offsetY,s=o-e*this.view.height}this.projectionMatrix.makeOrthographic(r,a,o,s,this.near,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.zoom=this.zoom,e.object.left=this.left,e.object.right=this.right,e.object.top=this.top,e.object.bottom=this.bottom,e.object.near=this.near,e.object.far=this.far,null!==this.view&&(e.object.view=Object.assign({},this.view)),e}}const ji=[.125,.215,.35,.446,.526,.582],Wi=20,Xi=new Vi,qi=new Mn;let Yi=null,$i=0,Ki=0;const Zi=(1+Math.sqrt(5))/2,Ji=1/Zi,Qi=[new ne(1,1,1),new ne(-1,1,1),new ne(1,1,-1),new ne(-1,1,-1),new ne(0,Zi,Ji),new ne(0,Zi,-Ji),new ne(Ji,0,Zi),new ne(-Ji,0,Zi),new ne(Zi,Ji,0),new ne(-Zi,Ji,0)];class tr{constructor(t){this._renderer=t,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._lodPlanes=[],this._sizeLods=[],this._sigmas=[],this._blurMaterial=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._compileMaterial(this._blurMaterial)}fromScene(t,e=0,n=.1,i=100){Yi=this._renderer.getRenderTarget(),$i=this._renderer.getActiveCubeFace(),Ki=this._renderer.getActiveMipmapLevel(),this._setSize(256);const r=this._allocateTargets();return r.depthBuffer=!0,this._sceneToCubeUV(t,n,i,r),e>0&&this._blur(r,0,0,e),this._applyPMREM(r),this._cleanup(r),r}fromEquirectangular(t,e=null){return this._fromTexture(t,e)}fromCubemap(t,e=null){return this._fromTexture(t,e)}compileCubemapShader(){null===this._cubemapMaterial&&(this._cubemapMaterial=rr(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){null===this._equirectMaterial&&(this._equirectMaterial=ir(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),null!==this._cubemapMaterial&&this._cubemapMaterial.dispose(),null!==this._equirectMaterial&&this._equirectMaterial.dispose()}_setSize(t){this._lodMax=Math.floor(Math.log2(t)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){null!==this._blurMaterial&&this._blurMaterial.dispose(),null!==this._pingPongRenderTarget&&this._pingPongRenderTarget.dispose();for(let t=0;tt-4?s=ji[o-t+4-1]:0===o&&(s=0),i.push(s);const l=1/(a-2),c=-l,u=1+l,h=[c,c,u,c,u,u,c,c,u,u,c,u],d=6,p=6,f=3,m=2,g=1,v=new Float32Array(f*p*d),_=new Float32Array(m*p*d),y=new Float32Array(g*p*d);for(let t=0;t2?0:-1,i=[e,n,0,e+2/3,n,0,e+2/3,n+1,0,e,n,0,e+2/3,n+1,0,e,n+1,0];v.set(i,f*p*t),_.set(h,m*p*t);const r=[t,t,t,t,t,t];y.set(r,g*p*t)}const x=new Bn;x.setAttribute("position",new Cn(v,f)),x.setAttribute("uv",new Cn(_,m)),x.setAttribute("faceIndex",new Cn(y,g)),e.push(x),r>4&&r--}return{lodPlanes:e,sizeLods:n,sigmas:i}}(i)),this._blurMaterial=function(t,e,n){const i=new Float32Array(Wi),r=new ne(0,1,0),a=new hi({name:"SphericalGaussianBlur",defines:{n:Wi,CUBEUV_TEXEL_WIDTH:1/e,CUBEUV_TEXEL_HEIGHT:1/n,CUBEUV_MAX_MIP:`${t}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:i},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:r}},vertexShader:ar(),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:0,depthTest:!1,depthWrite:!1});return a}(i,t,e)}return i}_compileMaterial(t){const e=new ri(this._lodPlanes[0],t);this._renderer.compile(e,Xi)}_sceneToCubeUV(t,e,n,i){const r=new gi(90,1,e,n),a=[1,-1,1,1,1,1],o=[1,1,1,-1,-1,-1],s=this._renderer,l=s.autoClear,c=s.toneMapping;s.getClearColor(qi),s.toneMapping=b,s.autoClear=!1;const u=new Tn({name:"PMREM.Background",side:g,depthWrite:!1,depthTest:!1}),h=new ri(new oi,u);let d=!1;const p=t.background;p?p.isColor&&(u.color.copy(p),t.background=null,d=!0):(u.color.copy(qi),d=!0);for(let e=0;e<6;e++){const n=e%3;0===n?(r.up.set(0,a[e],0),r.lookAt(o[e],0,0)):1===n?(r.up.set(0,0,a[e]),r.lookAt(0,o[e],0)):(r.up.set(0,a[e],0),r.lookAt(0,0,o[e]));const l=this._cubeSize;nr(i,n*l,e>2?l:0,l,l),s.setRenderTarget(i),d&&s.render(h,r),s.render(t,r)}h.geometry.dispose(),h.material.dispose(),s.toneMapping=c,s.autoClear=l,t.background=p}_textureToCubeUV(t,e){const n=this._renderer,i=t.mapping===C||t.mapping===P;i?(null===this._cubemapMaterial&&(this._cubemapMaterial=rr()),this._cubemapMaterial.uniforms.flipEnvMap.value=!1===t.isRenderTargetTexture?-1:1):null===this._equirectMaterial&&(this._equirectMaterial=ir());const r=i?this._cubemapMaterial:this._equirectMaterial,a=new ri(this._lodPlanes[0],r);r.uniforms.envMap.value=t;const o=this._cubeSize;nr(e,0,0,3*o,2*o),n.setRenderTarget(e),n.render(a,Xi)}_applyPMREM(t){const e=this._renderer,n=e.autoClear;e.autoClear=!1;for(let e=1;eWi&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${f} samples when the maximum is set to 20`);const m=[];let g=0;for(let t=0;tv-4?i-v+4:0),4*(this._cubeSize-_),3*_,2*_),s.setRenderTarget(e),s.render(c,Xi)}}function er(t,e,n){const i=new Jt(t,e,n);return i.texture.mapping=L,i.texture.name="PMREM.cubeUv",i.scissorTest=!0,i}function nr(t,e,n,i,r){t.viewport.set(e,n,i,r),t.scissor.set(e,n,i,r)}function ir(){return new hi({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:ar(),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:0,depthTest:!1,depthWrite:!1})}function rr(){return new hi({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:ar(),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:0,depthTest:!1,depthWrite:!1})}function ar(){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"}function or(t){let e=new WeakMap,n=null;function i(t){const n=t.target;n.removeEventListener("dispose",i);const r=e.get(n);void 0!==r&&(e.delete(n),r.dispose())}return{get:function(r){if(r&&r.isTexture){const a=r.mapping,o=303===a||304===a,s=a===C||a===P;if(o||s){if(r.isRenderTargetTexture&&!0===r.needsPMREMUpdate){r.needsPMREMUpdate=!1;let i=e.get(r);return null===n&&(n=new tr(t)),i=o?n.fromEquirectangular(r,i):n.fromCubemap(r,i),e.set(r,i),i.texture}if(e.has(r))return e.get(r).texture;{const a=r.image;if(o&&a&&a.height>0||s&&a&&function(t){let e=0;const n=6;for(let i=0;ie.maxTextureSize&&(S=Math.ceil(M/e.maxTextureSize),M=e.maxTextureSize);const E=new Float32Array(M*S*4*p),w=new Qt(E,M,S,p);w.type=j,w.needsUpdate=!0;const T=4*b;for(let R=0;R0)return t;const r=e*n;let a=br[r];if(void 0===a&&(a=new Float32Array(r),br[r]=a),0!==e){i.toArray(a,0);for(let i=1,r=0;i!==e;++i)r+=n,t[i].toArray(a,r)}return a}function Ar(t,e){if(t.length!==e.length)return!1;for(let n=0,i=t.length;n":" "} ${r}: ${n[t]}`)}return i.join("\n")}(t.getShaderSource(e),i)}return r}function wa(t,e){const n=function(t){const e=Bt.getPrimaries(Bt.workingColorSpace),n=Bt.getPrimaries(t);let i;switch(e===n?i="":e===ct&&n===lt?i="LinearDisplayP3ToLinearSRGB":e===lt&&n===ct&&(i="LinearSRGBToLinearDisplayP3"),t){case it:case at:return[i,"LinearTransferOETF"];case nt:case rt:return[i,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space:",t),[i,"LinearTransferOETF"]}}(e);return`vec4 ${t}( vec4 value ) { return ${n[0]}( ${n[1]}( value ) ); }`}function Ta(t,e){let n;switch(e){case M:n="Linear";break;case S:n="Reinhard";break;case E:n="OptimizedCineon";break;case w:n="ACESFilmic";break;case A:n="AgX";break;case R:n="Neutral";break;case T:n="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",e),n="Linear"}return"vec3 "+t+"( vec3 color ) { return "+n+"ToneMapping( color ); }"}function Aa(t){return""!==t}function Ra(t,e){const n=e.numSpotLightShadows+e.numSpotLightMaps-e.numSpotLightShadowsWithMaps;return t.replace(/NUM_DIR_LIGHTS/g,e.numDirLights).replace(/NUM_SPOT_LIGHTS/g,e.numSpotLights).replace(/NUM_SPOT_LIGHT_MAPS/g,e.numSpotLightMaps).replace(/NUM_SPOT_LIGHT_COORDS/g,n).replace(/NUM_RECT_AREA_LIGHTS/g,e.numRectAreaLights).replace(/NUM_POINT_LIGHTS/g,e.numPointLights).replace(/NUM_HEMI_LIGHTS/g,e.numHemiLights).replace(/NUM_DIR_LIGHT_SHADOWS/g,e.numDirLightShadows).replace(/NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS/g,e.numSpotLightShadowsWithMaps).replace(/NUM_SPOT_LIGHT_SHADOWS/g,e.numSpotLightShadows).replace(/NUM_POINT_LIGHT_SHADOWS/g,e.numPointLightShadows)}function Ca(t,e){return t.replace(/NUM_CLIPPING_PLANES/g,e.numClippingPlanes).replace(/UNION_CLIPPING_PLANES/g,e.numClippingPlanes-e.numClipIntersection)}const Pa=/^[ \t]*#include +<([\w\d./]+)>/gm;function La(t){return t.replace(Pa,Da)}const Oa=new Map([["encodings_fragment","colorspace_fragment"],["encodings_pars_fragment","colorspace_pars_fragment"],["output_fragment","opaque_fragment"]]);function Da(t,e){let n=Li[e];if(void 0===n){const t=Oa.get(e);if(void 0===t)throw new Error("Can not resolve #include <"+e+">");n=Li[t],console.warn('THREE.WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',e,t)}return La(n)}const Na=/#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 Ia(t){return t.replace(Na,Ua)}function Ua(t,e,n,i){let r="";for(let t=parseInt(e);t0&&(E+="\n"),w=[g,"#define SHADER_TYPE "+n.shaderType,"#define SHADER_NAME "+n.shaderName,M].filter(Aa).join("\n"),w.length>0&&(w+="\n")):(E=[Fa(n),"#define SHADER_TYPE "+n.shaderType,"#define SHADER_NAME "+n.shaderName,M,n.extensionClipCullDistance?"#define USE_CLIP_DISTANCE":"",n.batching?"#define USE_BATCHING":"",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 "+u:"",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.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&&n.isWebGL2?"#define USE_MORPHCOLORS":"",n.morphTargetsCount>0&&n.isWebGL2?"#define MORPHTARGETS_TEXTURE":"",n.morphTargetsCount>0&&n.isWebGL2?"#define MORPHTARGETS_TEXTURE_STRIDE "+n.morphTextureStride:"",n.morphTargetsCount>0&&n.isWebGL2?"#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.useLegacyLights?"#define LEGACY_LIGHTS":"",n.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",n.logarithmicDepthBuffer&&n.rendererExtensionFragDepth?"#define USE_LOGDEPTHBUF_EXT":"","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","#if ( defined( USE_MORPHTARGETS ) && ! defined( MORPHTARGETS_TEXTURE ) )","\tattribute vec3 morphTarget0;","\tattribute vec3 morphTarget1;","\tattribute vec3 morphTarget2;","\tattribute vec3 morphTarget3;","\t#ifdef USE_MORPHNORMALS","\t\tattribute vec3 morphNormal0;","\t\tattribute vec3 morphNormal1;","\t\tattribute vec3 morphNormal2;","\t\tattribute vec3 morphNormal3;","\t#else","\t\tattribute vec3 morphTarget4;","\t\tattribute vec3 morphTarget5;","\t\tattribute vec3 morphTarget6;","\t\tattribute vec3 morphTarget7;","\t#endif","#endif","#ifdef USE_SKINNING","\tattribute vec4 skinIndex;","\tattribute vec4 skinWeight;","#endif","\n"].filter(Aa).join("\n"),w=[g,Fa(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 "+u:"",n.envMap?"#define "+h:"",m?"#define CUBEUV_TEXEL_WIDTH "+m.texelWidth:"",m?"#define CUBEUV_TEXEL_HEIGHT "+m.texelHeight:"",m?"#define CUBEUV_MAX_MIP "+m.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.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.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?"#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.useLegacyLights?"#define LEGACY_LIGHTS":"",n.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",n.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",n.logarithmicDepthBuffer&&n.rendererExtensionFragDepth?"#define USE_LOGDEPTHBUF_EXT":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",n.toneMapping!==b?"#define TONE_MAPPING":"",n.toneMapping!==b?Li.tonemapping_pars_fragment:"",n.toneMapping!==b?Ta("toneMapping",n.toneMapping):"",n.dithering?"#define DITHERING":"",n.opaque?"#define OPAQUE":"",Li.colorspace_pars_fragment,wa("linearToOutputTexel",n.outputColorSpace),n.useDepthPacking?"#define DEPTH_PACKING "+n.depthPacking:"","\n"].filter(Aa).join("\n")),o=La(o),o=Ra(o,n),o=Ca(o,n),s=La(s),s=Ra(s,n),s=Ca(s,n),o=Ia(o),s=Ia(s),n.isWebGL2&&!0!==n.isRawShaderMaterial&&(T="#version 300 es\n",E=[v,"precision mediump sampler2DArray;","#define attribute in","#define varying out","#define texture2D texture"].join("\n")+"\n"+E,w=["precision mediump sampler2DArray;","#define varying in",n.glslVersion===ht?"":"layout(location = 0) out highp vec4 pc_fragColor;",n.glslVersion===ht?"":"#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"+w);const A=T+E+o,R=T+w+s,O=ba(r,r.VERTEX_SHADER,A),D=ba(r,r.FRAGMENT_SHADER,R);function N(e){if(t.debug.checkShaderErrors){const n=r.getProgramInfoLog(S).trim(),i=r.getShaderInfoLog(O).trim(),a=r.getShaderInfoLog(D).trim();let o=!0,s=!0;if(!1===r.getProgramParameter(S,r.LINK_STATUS))if(o=!1,"function"==typeof t.debug.onShaderError)t.debug.onShaderError(r,S,O,D);else{const t=Ea(r,O,"vertex"),i=Ea(r,D,"fragment");console.error("THREE.WebGLProgram: Shader Error "+r.getError()+" - VALIDATE_STATUS "+r.getProgramParameter(S,r.VALIDATE_STATUS)+"\n\nMaterial Name: "+e.name+"\nMaterial Type: "+e.type+"\n\nProgram Info Log: "+n+"\n"+t+"\n"+i)}else""!==n?console.warn("THREE.WebGLProgram: Program Info Log:",n):""!==i&&""!==a||(s=!1);s&&(e.diagnostics={runnable:o,programLog:n,vertexShader:{log:i,prefix:E},fragmentShader:{log:a,prefix:w}})}r.deleteShader(O),r.deleteShader(D),I=new xa(r,S),U=function(t,e){const n={},i=t.getProgramParameter(e,t.ACTIVE_ATTRIBUTES);for(let r=0;r0,K=a.clearcoat>0,Z=a.iridescence>0,J=a.sheen>0,Q=a.transmission>0,tt=$&&!!a.anisotropyMap,et=K&&!!a.clearcoatMap,nt=K&&!!a.clearcoatNormalMap,rt=K&&!!a.clearcoatRoughnessMap,at=Z&&!!a.iridescenceMap,ot=Z&&!!a.iridescenceThicknessMap,lt=J&&!!a.sheenColorMap,ct=J&&!!a.sheenRoughnessMap,ut=!!a.specularMap,ht=!!a.specularColorMap,dt=!!a.specularIntensityMap,pt=Q&&!!a.transmissionMap,ft=Q&&!!a.thicknessMap,mt=!!a.gradientMap,gt=!!a.alphaMap,vt=a.alphaTest>0,_t=!!a.alphaHash,yt=!!a.extensions;let xt=b;a.toneMapped&&(null!==I&&!0!==I.isXRRenderTarget||(xt=t.toneMapping));const bt={isWebGL2:h,shaderID:T,shaderType:a.type,shaderName:a.name,vertexShader:C,fragmentShader:P,defines:a.defines,customVertexShaderID:O,customFragmentShaderID:D,isRawShaderMaterial:!0===a.isRawShaderMaterial,glslVersion:a.glslVersion,precision:f,batching:F,instancing:U,instancingColor:U&&null!==y.instanceColor,instancingMorph:U&&null!==y.morphTexture,supportsVertexTextures:p,outputColorSpace:null===I?t.outputColorSpace:!0===I.isXRRenderTarget?I.texture.colorSpace:it,alphaToCoverage:!!a.alphaToCoverage,map:k,matcap:z,envMap:B,envMapMode:B&&E.mapping,envMapCubeUVHeight:w,aoMap:H,lightMap:G,bumpMap:V,normalMap:j,displacementMap:p&&W,emissiveMap:X,normalMapObjectSpace:j&&1===a.normalMapType,normalMapTangentSpace:j&&0===a.normalMapType,metalnessMap:q,roughnessMap:Y,anisotropy:$,anisotropyMap:tt,clearcoat:K,clearcoatMap:et,clearcoatNormalMap:nt,clearcoatRoughnessMap:rt,iridescence:Z,iridescenceMap:at,iridescenceThicknessMap:ot,sheen:J,sheenColorMap:lt,sheenRoughnessMap:ct,specularMap:ut,specularColorMap:ht,specularIntensityMap:dt,transmission:Q,transmissionMap:pt,thicknessMap:ft,gradientMap:mt,opaque:!1===a.transparent&&1===a.blending&&!1===a.alphaToCoverage,alphaMap:gt,alphaTest:vt,alphaHash:_t,combine:a.combine,mapUv:k&&v(a.map.channel),aoMapUv:H&&v(a.aoMap.channel),lightMapUv:G&&v(a.lightMap.channel),bumpMapUv:V&&v(a.bumpMap.channel),normalMapUv:j&&v(a.normalMap.channel),displacementMapUv:W&&v(a.displacementMap.channel),emissiveMapUv:X&&v(a.emissiveMap.channel),metalnessMapUv:q&&v(a.metalnessMap.channel),roughnessMapUv:Y&&v(a.roughnessMap.channel),anisotropyMapUv:tt&&v(a.anisotropyMap.channel),clearcoatMapUv:et&&v(a.clearcoatMap.channel),clearcoatNormalMapUv:nt&&v(a.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:rt&&v(a.clearcoatRoughnessMap.channel),iridescenceMapUv:at&&v(a.iridescenceMap.channel),iridescenceThicknessMapUv:ot&&v(a.iridescenceThicknessMap.channel),sheenColorMapUv:lt&&v(a.sheenColorMap.channel),sheenRoughnessMapUv:ct&&v(a.sheenRoughnessMap.channel),specularMapUv:ut&&v(a.specularMap.channel),specularColorMapUv:ht&&v(a.specularColorMap.channel),specularIntensityMapUv:dt&&v(a.specularIntensityMap.channel),transmissionMapUv:pt&&v(a.transmissionMap.channel),thicknessMapUv:ft&&v(a.thicknessMap.channel),alphaMapUv:gt&&v(a.alphaMap.channel),vertexTangents:!!M.attributes.tangent&&(j||$),vertexColors:a.vertexColors,vertexAlphas:!0===a.vertexColors&&!!M.attributes.color&&4===M.attributes.color.itemSize,pointsUvs:!0===y.isPoints&&!!M.attributes.uv&&(k||gt),fog:!!x,useFog:!0===a.fog,fogExp2:!!x&&x.isFogExp2,flatShading:!0===a.flatShading,sizeAttenuation:!0===a.sizeAttenuation,logarithmicDepthBuffer:d,skinning:!0===y.isSkinnedMesh,morphTargets:void 0!==M.morphAttributes.position,morphNormals:void 0!==M.morphAttributes.normal,morphColors:void 0!==M.morphAttributes.color,morphTargetsCount:R,morphTextureStride:N,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,numClippingPlanes:o.numPlanes,numClipIntersection:o.numIntersection,dithering:a.dithering,shadowMapEnabled:t.shadowMap.enabled&&u.length>0,shadowMapType:t.shadowMap.type,toneMapping:xt,useLegacyLights:t._useLegacyLights,decodeVideoTexture:k&&!0===a.map.isVideoTexture&&Bt.getTransfer(a.map.colorSpace)===st,premultipliedAlpha:a.premultipliedAlpha,doubleSided:2===a.side,flipSided:a.side===g,useDepthPacking:a.depthPacking>=0,depthPacking:a.depthPacking||0,index0AttributeName:a.index0AttributeName,extensionDerivatives:yt&&!0===a.extensions.derivatives,extensionFragDepth:yt&&!0===a.extensions.fragDepth,extensionDrawBuffers:yt&&!0===a.extensions.drawBuffers,extensionShaderTextureLOD:yt&&!0===a.extensions.shaderTextureLOD,extensionClipCullDistance:yt&&!0===a.extensions.clipCullDistance&&i.has("WEBGL_clip_cull_distance"),extensionMultiDraw:yt&&!0===a.extensions.multiDraw&&i.has("WEBGL_multi_draw"),rendererExtensionFragDepth:h||i.has("EXT_frag_depth"),rendererExtensionDrawBuffers:h||i.has("WEBGL_draw_buffers"),rendererExtensionShaderTextureLod:h||i.has("EXT_shader_texture_lod"),rendererExtensionParallelShaderCompile:i.has("KHR_parallel_shader_compile"),customProgramCacheKey:a.customProgramCacheKey()};return bt.vertexUv1s=c.has(1),bt.vertexUv2s=c.has(2),bt.vertexUv3s=c.has(3),c.clear(),bt},getProgramCacheKey:function(e){const n=[];if(e.shaderID?n.push(e.shaderID):(n.push(e.customVertexShaderID),n.push(e.customFragmentShaderID)),void 0!==e.defines)for(const t in e.defines)n.push(t),n.push(e.defines[t]);return!1===e.isRawShaderMaterial&&(!function(t,e){t.push(e.precision),t.push(e.outputColorSpace),t.push(e.envMapMode),t.push(e.envMapCubeUVHeight),t.push(e.mapUv),t.push(e.alphaMapUv),t.push(e.lightMapUv),t.push(e.aoMapUv),t.push(e.bumpMapUv),t.push(e.normalMapUv),t.push(e.displacementMapUv),t.push(e.emissiveMapUv),t.push(e.metalnessMapUv),t.push(e.roughnessMapUv),t.push(e.anisotropyMapUv),t.push(e.clearcoatMapUv),t.push(e.clearcoatNormalMapUv),t.push(e.clearcoatRoughnessMapUv),t.push(e.iridescenceMapUv),t.push(e.iridescenceThicknessMapUv),t.push(e.sheenColorMapUv),t.push(e.sheenRoughnessMapUv),t.push(e.specularMapUv),t.push(e.specularColorMapUv),t.push(e.specularIntensityMapUv),t.push(e.transmissionMapUv),t.push(e.thicknessMapUv),t.push(e.combine),t.push(e.fogExp2),t.push(e.sizeAttenuation),t.push(e.morphTargetsCount),t.push(e.morphAttributeCount),t.push(e.numDirLights),t.push(e.numPointLights),t.push(e.numSpotLights),t.push(e.numSpotLightMaps),t.push(e.numHemiLights),t.push(e.numRectAreaLights),t.push(e.numDirLightShadows),t.push(e.numPointLightShadows),t.push(e.numSpotLightShadows),t.push(e.numSpotLightShadowsWithMaps),t.push(e.numLightProbes),t.push(e.shadowMapType),t.push(e.toneMapping),t.push(e.numClippingPlanes),t.push(e.numClipIntersection),t.push(e.depthPacking)}(n,e),function(t,e){s.disableAll(),e.isWebGL2&&s.enable(0);e.supportsVertexTextures&&s.enable(1);e.instancing&&s.enable(2);e.instancingColor&&s.enable(3);e.instancingMorph&&s.enable(4);e.matcap&&s.enable(5);e.envMap&&s.enable(6);e.normalMapObjectSpace&&s.enable(7);e.normalMapTangentSpace&&s.enable(8);e.clearcoat&&s.enable(9);e.iridescence&&s.enable(10);e.alphaTest&&s.enable(11);e.vertexColors&&s.enable(12);e.vertexAlphas&&s.enable(13);e.vertexUv1s&&s.enable(14);e.vertexUv2s&&s.enable(15);e.vertexUv3s&&s.enable(16);e.vertexTangents&&s.enable(17);e.anisotropy&&s.enable(18);e.alphaHash&&s.enable(19);e.batching&&s.enable(20);t.push(s.mask),s.disableAll(),e.fog&&s.enable(0);e.useFog&&s.enable(1);e.flatShading&&s.enable(2);e.logarithmicDepthBuffer&&s.enable(3);e.skinning&&s.enable(4);e.morphTargets&&s.enable(5);e.morphNormals&&s.enable(6);e.morphColors&&s.enable(7);e.premultipliedAlpha&&s.enable(8);e.shadowMapEnabled&&s.enable(9);e.useLegacyLights&&s.enable(10);e.doubleSided&&s.enable(11);e.flipSided&&s.enable(12);e.useDepthPacking&&s.enable(13);e.dithering&&s.enable(14);e.transmission&&s.enable(15);e.sheen&&s.enable(16);e.opaque&&s.enable(17);e.pointsUvs&&s.enable(18);e.decodeVideoTexture&&s.enable(19);e.alphaToCoverage&&s.enable(20);t.push(s.mask)}(n,e),n.push(t.outputColorSpace)),n.push(e.customProgramCacheKey),n.join()},getUniforms:function(t){const e=m[t.type];let n;if(e){const t=Di[e];n=ui.clone(t.uniforms)}else n=t.uniforms;return n},acquireProgram:function(e,n){let i;for(let t=0,e=u.length;t0?i.push(u):!0===o.transparent?r.push(u):n.push(u)},unshift:function(t,e,o,s,l,c){const u=a(t,e,o,s,l,c);o.transmission>0?i.unshift(u):!0===o.transparent?r.unshift(u):n.unshift(u)},finish:function(){for(let n=e,i=t.length;n1&&n.sort(t||ja),i.length>1&&i.sort(e||Wa),r.length>1&&r.sort(e||Wa)}}}function qa(){let t=new WeakMap;return{get:function(e,n){const i=t.get(e);let r;return void 0===i?(r=new Xa,t.set(e,[r])):n>=i.length?(r=new Xa,i.push(r)):r=i[n],r},dispose:function(){t=new WeakMap}}}function Ya(){const t={};return{get:function(e){if(void 0!==t[e.id])return t[e.id];let n;switch(e.type){case"DirectionalLight":n={direction:new ne,color:new Mn};break;case"SpotLight":n={position:new ne,direction:new ne,color:new Mn,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":n={position:new ne,color:new Mn,distance:0,decay:0};break;case"HemisphereLight":n={direction:new ne,skyColor:new Mn,groundColor:new Mn};break;case"RectAreaLight":n={color:new Mn,position:new ne,halfWidth:new ne,halfHeight:new ne}}return t[e.id]=n,n}}}let $a=0;function Ka(t,e){return(e.castShadow?2:0)-(t.castShadow?2:0)+(e.map?1:0)-(t.map?1:0)}function Za(t,e){const n=new Ya,i=function(){const t={};return{get:function(e){if(void 0!==t[e.id])return t[e.id];let n;switch(e.type){case"DirectionalLight":case"SpotLight":n={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Ct};break;case"PointLight":n={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Ct,shadowCameraNear:1,shadowCameraFar:1e3}}return t[e.id]=n,n}}}(),r={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 t=0;t<9;t++)r.probe.push(new ne);const a=new ne,o=new Oe,s=new Oe;return{setup:function(a,o){let s=0,l=0,c=0;for(let t=0;t<9;t++)r.probe[t].set(0,0,0);let u=0,h=0,d=0,p=0,f=0,m=0,g=0,v=0,_=0,y=0,x=0;a.sort(Ka);const b=!0===o?Math.PI:1;for(let t=0,e=a.length;t0&&(e.isWebGL2?!0===t.has("OES_texture_float_linear")?(r.rectAreaLTC1=Oi.LTC_FLOAT_1,r.rectAreaLTC2=Oi.LTC_FLOAT_2):(r.rectAreaLTC1=Oi.LTC_HALF_1,r.rectAreaLTC2=Oi.LTC_HALF_2):!0===t.has("OES_texture_float_linear")?(r.rectAreaLTC1=Oi.LTC_FLOAT_1,r.rectAreaLTC2=Oi.LTC_FLOAT_2):!0===t.has("OES_texture_half_float_linear")?(r.rectAreaLTC1=Oi.LTC_HALF_1,r.rectAreaLTC2=Oi.LTC_HALF_2):console.error("THREE.WebGLRenderer: Unable to use RectAreaLight. Missing WebGL extensions.")),r.ambient[0]=s,r.ambient[1]=l,r.ambient[2]=c;const M=r.hash;M.directionalLength===u&&M.pointLength===h&&M.spotLength===d&&M.rectAreaLength===p&&M.hemiLength===f&&M.numDirectionalShadows===m&&M.numPointShadows===g&&M.numSpotShadows===v&&M.numSpotMaps===_&&M.numLightProbes===x||(r.directional.length=u,r.spot.length=d,r.rectArea.length=p,r.point.length=h,r.hemi.length=f,r.directionalShadow.length=m,r.directionalShadowMap.length=m,r.pointShadow.length=g,r.pointShadowMap.length=g,r.spotShadow.length=v,r.spotShadowMap.length=v,r.directionalShadowMatrix.length=m,r.pointShadowMatrix.length=g,r.spotLightMatrix.length=v+_-y,r.spotLightMap.length=_,r.numSpotLightShadowsWithMaps=y,r.numLightProbes=x,M.directionalLength=u,M.pointLength=h,M.spotLength=d,M.rectAreaLength=p,M.hemiLength=f,M.numDirectionalShadows=m,M.numPointShadows=g,M.numSpotShadows=v,M.numSpotMaps=_,M.numLightProbes=x,r.version=$a++)},setupView:function(t,e){let n=0,i=0,l=0,c=0,u=0;const h=e.matrixWorldInverse;for(let e=0,d=t.length;e=a.length?(o=new Ja(t,e),a.push(o)):o=a[r],o},dispose:function(){n=new WeakMap}}}class to extends wn{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 eo extends wn{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}}function no(t,e,n){let i=new Ai;const r=new Ct,a=new Ct,o=new Kt,s=new to({depthPacking:3201}),l=new eo,c={},u=n.maxTextureSize,h={[m]:g,[g]:m,2:2},p=new hi({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new Ct},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;\n#include \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 = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) );\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 = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) );\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( squared_mean - mean * mean );\n\tgl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) );\n}"}),v=p.clone();v.defines.HORIZONTAL_PASS=1;const _=new Bn;_.setAttribute("position",new Cn(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const y=new ri(_,p),x=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=d;let b=this.type;function M(n,i){const a=e.update(y);p.defines.VSM_SAMPLES!==n.blurSamples&&(p.defines.VSM_SAMPLES=n.blurSamples,v.defines.VSM_SAMPLES=n.blurSamples,p.needsUpdate=!0,v.needsUpdate=!0),null===n.mapPass&&(n.mapPass=new Jt(r.x,r.y)),p.uniforms.shadow_pass.value=n.map.texture,p.uniforms.resolution.value=n.mapSize,p.uniforms.radius.value=n.radius,t.setRenderTarget(n.mapPass),t.clear(),t.renderBufferDirect(i,null,a,p,y,null),v.uniforms.shadow_pass.value=n.mapPass.texture,v.uniforms.resolution.value=n.mapSize,v.uniforms.radius.value=n.radius,t.setRenderTarget(n.map),t.clear(),t.renderBufferDirect(i,null,a,v,y,null)}function S(e,n,i,r){let a=null;const o=!0===i.isPointLight?e.customDistanceMaterial:e.customDepthMaterial;if(void 0!==o)a=o;else if(a=!0===i.isPointLight?l:s,t.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){const t=a.uuid,e=n.uuid;let i=c[t];void 0===i&&(i={},c[t]=i);let r=i[e];void 0===r&&(r=a.clone(),i[e]=r,n.addEventListener("dispose",w)),a=r}if(a.visible=n.visible,a.wireframe=n.wireframe,a.side=r===f?null!==n.shadowSide?n.shadowSide:n.side:null!==n.shadowSide?n.shadowSide:h[n.side],a.alphaMap=n.alphaMap,a.alphaTest=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){t.properties.get(a).light=i}return a}function E(n,r,a,o,s){if(!1===n.visible)return;if(n.layers.test(r.layers)&&(n.isMesh||n.isLine||n.isPoints)&&(n.castShadow||n.receiveShadow&&s===f)&&(!n.frustumCulled||i.intersectsObject(n))){n.modelViewMatrix.multiplyMatrices(a.matrixWorldInverse,n.matrixWorld);const i=e.update(n),l=n.material;if(Array.isArray(l)){const e=i.groups;for(let c=0,u=e.length;cu||r.y>u)&&(r.x>u&&(a.x=Math.floor(u/g.x),r.x=a.x*g.x,h.mapSize.x=a.x),r.y>u&&(a.y=Math.floor(u/g.y),r.y=a.y*g.y,h.mapSize.y=a.y)),null===h.map||!0===p||!0===m){const t=this.type!==f?{minFilter:I,magFilter:I}:{};null!==h.map&&h.map.dispose(),h.map=new Jt(r.x,r.y,t),h.map.texture.name=c.name+".shadowMap",h.camera.updateProjectionMatrix()}t.setRenderTarget(h.map),t.clear();const v=h.getViewportCount();for(let t=0;t=1):-1!==I.indexOf("OpenGL ES")&&(N=parseFloat(/^OpenGL ES (\d)/.exec(I)[1]),D=N>=2);let U=null,F={};const k=t.getParameter(t.SCISSOR_BOX),z=t.getParameter(t.VIEWPORT),B=(new Kt).fromArray(k),H=(new Kt).fromArray(z);function G(e,n,r,a){const o=new Uint8Array(4),s=t.createTexture();t.bindTexture(e,s),t.texParameteri(e,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(e,t.TEXTURE_MAG_FILTER,t.NEAREST);for(let s=0;si||a.height>i)&&(r=i/Math.max(a.width,a.height)),r<1||!0===e){if("undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap||"undefined"!=typeof VideoFrame&&t instanceof VideoFrame){const i=e?wt:Math.floor,o=i(r*a.width),s=i(r*a.height);void 0===d&&(d=m(o,s));const l=n?m(o,s):d;l.width=o,l.height=s;return l.getContext("2d").drawImage(t,0,0,o,s),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+a.width+"x"+a.height+") to ("+o+"x"+s+")."),l}return"data"in t&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+a.width+"x"+a.height+")."),t}return t}function v(t){const e=at(t);return Et(e.width)&&Et(e.height)}function _(t,e){return t.generateMipmaps&&e&&t.minFilter!==I&&t.minFilter!==F}function y(e){t.generateMipmap(e)}function x(n,i,r,a,o=!1){if(!1===s)return i;if(null!==n){if(void 0!==t[n])return t[n];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+n+"'")}let l=i;if(i===t.RED&&(r===t.FLOAT&&(l=t.R32F),r===t.HALF_FLOAT&&(l=t.R16F),r===t.UNSIGNED_BYTE&&(l=t.R8)),i===t.RED_INTEGER&&(r===t.UNSIGNED_BYTE&&(l=t.R8UI),r===t.UNSIGNED_SHORT&&(l=t.R16UI),r===t.UNSIGNED_INT&&(l=t.R32UI),r===t.BYTE&&(l=t.R8I),r===t.SHORT&&(l=t.R16I),r===t.INT&&(l=t.R32I)),i===t.RG&&(r===t.FLOAT&&(l=t.RG32F),r===t.HALF_FLOAT&&(l=t.RG16F),r===t.UNSIGNED_BYTE&&(l=t.RG8)),i===t.RG_INTEGER&&(r===t.UNSIGNED_BYTE&&(l=t.RG8UI),r===t.UNSIGNED_SHORT&&(l=t.RG16UI),r===t.UNSIGNED_INT&&(l=t.RG32UI),r===t.BYTE&&(l=t.RG8I),r===t.SHORT&&(l=t.RG16I),r===t.INT&&(l=t.RG32I)),i===t.RGBA){const e=o?ot:Bt.getTransfer(a);r===t.FLOAT&&(l=t.RGBA32F),r===t.HALF_FLOAT&&(l=t.RGBA16F),r===t.UNSIGNED_BYTE&&(l=e===st?t.SRGB8_ALPHA8:t.RGBA8),r===t.UNSIGNED_SHORT_4_4_4_4&&(l=t.RGBA4),r===t.UNSIGNED_SHORT_5_5_5_1&&(l=t.RGB5_A1)}return l!==t.R16F&&l!==t.R32F&&l!==t.RG16F&&l!==t.RG32F&&l!==t.RGBA16F&&l!==t.RGBA32F||e.get("EXT_color_buffer_float"),l}function b(t,e,n){return!0===_(t,n)||t.isFramebufferTexture&&t.minFilter!==I&&t.minFilter!==F?Math.log2(Math.max(e.width,e.height))+1:void 0!==t.mipmaps&&t.mipmaps.length>0?t.mipmaps.length:t.isCompressedTexture&&Array.isArray(t.image)?e.mipmaps.length:1}function M(e){return e===I||1004===e||e===U?t.NEAREST:t.LINEAR}function S(t){const e=t.target;e.removeEventListener("dispose",S),function(t){const e=i.get(t);if(void 0===e.__webglInit)return;const n=t.source,r=p.get(n);if(r){const i=r[e.__cacheKey];i.usedTimes--,0===i.usedTimes&&w(t),0===Object.keys(r).length&&p.delete(n)}i.remove(t)}(e),e.isVideoTexture&&h.delete(e)}function E(e){const n=e.target;n.removeEventListener("dispose",E),function(e){const n=i.get(e);e.depthTexture&&e.depthTexture.dispose();if(e.isWebGLCubeRenderTarget)for(let e=0;e<6;e++){if(Array.isArray(n.__webglFramebuffer[e]))for(let i=0;i0&&a.__version!==e.version){const t=e.image;if(null===t)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else{if(!1!==t.complete)return void K(a,e,r);console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete")}}n.bindTexture(t.TEXTURE_2D,a.__webglTexture,t.TEXTURE0+r)}const R={[O]:t.REPEAT,[D]:t.CLAMP_TO_EDGE,[N]:t.MIRRORED_REPEAT},C={[I]:t.NEAREST,1004:t.NEAREST_MIPMAP_NEAREST,[U]:t.NEAREST_MIPMAP_LINEAR,[F]:t.LINEAR,[k]:t.LINEAR_MIPMAP_NEAREST,[z]:t.LINEAR_MIPMAP_LINEAR},P={512:t.NEVER,519:t.ALWAYS,513:t.LESS,515:t.LEQUAL,514:t.EQUAL,518:t.GEQUAL,516:t.GREATER,517:t.NOTEQUAL};function L(n,a,o){if(a.type!==j||!1!==e.has("OES_texture_float_linear")||a.magFilter!==F&&a.magFilter!==k&&a.magFilter!==U&&a.magFilter!==z&&a.minFilter!==F&&a.minFilter!==k&&a.minFilter!==U&&a.minFilter!==z||console.warn("THREE.WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device."),o?(t.texParameteri(n,t.TEXTURE_WRAP_S,R[a.wrapS]),t.texParameteri(n,t.TEXTURE_WRAP_T,R[a.wrapT]),n!==t.TEXTURE_3D&&n!==t.TEXTURE_2D_ARRAY||t.texParameteri(n,t.TEXTURE_WRAP_R,R[a.wrapR]),t.texParameteri(n,t.TEXTURE_MAG_FILTER,C[a.magFilter]),t.texParameteri(n,t.TEXTURE_MIN_FILTER,C[a.minFilter])):(t.texParameteri(n,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(n,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),n!==t.TEXTURE_3D&&n!==t.TEXTURE_2D_ARRAY||t.texParameteri(n,t.TEXTURE_WRAP_R,t.CLAMP_TO_EDGE),a.wrapS===D&&a.wrapT===D||console.warn("THREE.WebGLRenderer: Texture is not power of two. Texture.wrapS and Texture.wrapT should be set to THREE.ClampToEdgeWrapping."),t.texParameteri(n,t.TEXTURE_MAG_FILTER,M(a.magFilter)),t.texParameteri(n,t.TEXTURE_MIN_FILTER,M(a.minFilter)),a.minFilter!==I&&a.minFilter!==F&&console.warn("THREE.WebGLRenderer: Texture is not power of two. Texture.minFilter should be set to THREE.NearestFilter or THREE.LinearFilter.")),a.compareFunction&&(t.texParameteri(n,t.TEXTURE_COMPARE_MODE,t.COMPARE_REF_TO_TEXTURE),t.texParameteri(n,t.TEXTURE_COMPARE_FUNC,P[a.compareFunction])),!0===e.has("EXT_texture_filter_anisotropic")){if(a.magFilter===I)return;if(a.minFilter!==U&&a.minFilter!==z)return;if(a.type===j&&!1===e.has("OES_texture_float_linear"))return;if(!1===s&&a.type===W&&!1===e.has("OES_texture_half_float_linear"))return;if(a.anisotropy>1||i.get(a).__currentAnisotropy){const o=e.get("EXT_texture_filter_anisotropic");t.texParameterf(n,o.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(a.anisotropy,r.getMaxAnisotropy())),i.get(a).__currentAnisotropy=a.anisotropy}}}function G(e,n){let i=!1;void 0===e.__webglInit&&(e.__webglInit=!0,n.addEventListener("dispose",S));const r=n.source;let a=p.get(r);void 0===a&&(a={},p.set(r,a));const s=function(t){const e=[];return e.push(t.wrapS),e.push(t.wrapT),e.push(t.wrapR||0),e.push(t.magFilter),e.push(t.minFilter),e.push(t.anisotropy),e.push(t.internalFormat),e.push(t.format),e.push(t.type),e.push(t.generateMipmaps),e.push(t.premultiplyAlpha),e.push(t.flipY),e.push(t.unpackAlignment),e.push(t.colorSpace),e.join()}(n);if(s!==e.__cacheKey){void 0===a[s]&&(a[s]={texture:t.createTexture(),usedTimes:0},o.memory.textures++,i=!0),a[s].usedTimes++;const r=a[e.__cacheKey];void 0!==r&&(a[e.__cacheKey].usedTimes--,0===r.usedTimes&&w(n)),e.__cacheKey=s,e.__webglTexture=a[s].texture}return i}function K(e,o,l){let c=t.TEXTURE_2D;(o.isDataArrayTexture||o.isCompressedArrayTexture)&&(c=t.TEXTURE_2D_ARRAY),o.isData3DTexture&&(c=t.TEXTURE_3D);const u=G(e,o),h=o.source;n.bindTexture(c,e.__webglTexture,t.TEXTURE0+l);const d=i.get(h);if(h.version!==d.__version||!0===u){n.activeTexture(t.TEXTURE0+l);const e=Bt.getPrimaries(Bt.workingColorSpace),i=o.colorSpace===et?null:Bt.getPrimaries(o.colorSpace),p=o.colorSpace===et||e===i?t.NONE:t.BROWSER_DEFAULT_WEBGL;t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,o.flipY),t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,o.premultiplyAlpha),t.pixelStorei(t.UNPACK_ALIGNMENT,o.unpackAlignment),t.pixelStorei(t.UNPACK_COLORSPACE_CONVERSION_WEBGL,p);const f=function(t){return!s&&(t.wrapS!==D||t.wrapT!==D||t.minFilter!==I&&t.minFilter!==F)}(o)&&!1===v(o.image);let m=g(o.image,f,!1,r.maxTextureSize);m=rt(o,m);const M=v(m)||s,S=a.convert(o.format,o.colorSpace);let E,w=a.convert(o.type),T=x(o.internalFormat,S,w,o.colorSpace,o.isVideoTexture);L(c,o,M);const A=o.mipmaps,R=s&&!0!==o.isVideoTexture&&36196!==T,C=void 0===d.__version||!0===u,P=h.dataReady,O=b(o,m,M);if(o.isDepthTexture)T=t.DEPTH_COMPONENT,s?T=o.type===j?t.DEPTH_COMPONENT32F:o.type===V?t.DEPTH_COMPONENT24:o.type===X?t.DEPTH24_STENCIL8:t.DEPTH_COMPONENT16:o.type===j&&console.error("WebGLRenderer: Floating point depth texture requires WebGL2."),o.format===Y&&T===t.DEPTH_COMPONENT&&o.type!==H&&o.type!==V&&(console.warn("THREE.WebGLRenderer: Use UnsignedShortType or UnsignedIntType for DepthFormat DepthTexture."),o.type=V,w=a.convert(o.type)),o.format===$&&T===t.DEPTH_COMPONENT&&(T=t.DEPTH_STENCIL,o.type!==X&&(console.warn("THREE.WebGLRenderer: Use UnsignedInt248Type for DepthStencilFormat DepthTexture."),o.type=X,w=a.convert(o.type))),C&&(R?n.texStorage2D(t.TEXTURE_2D,1,T,m.width,m.height):n.texImage2D(t.TEXTURE_2D,0,T,m.width,m.height,0,S,w,null));else if(o.isDataTexture)if(A.length>0&&M){R&&C&&n.texStorage2D(t.TEXTURE_2D,O,T,A[0].width,A[0].height);for(let e=0,i=A.length;e>=1,i>>=1}}else if(A.length>0&&M){if(R&&C){const e=at(A[0]);n.texStorage2D(t.TEXTURE_2D,O,T,e.width,e.height)}for(let e=0,i=A.length;e>u),i=Math.max(1,r.height>>u);c===t.TEXTURE_3D||c===t.TEXTURE_2D_ARRAY?n.texImage3D(c,u,p,e,i,r.depth,0,h,d,null):n.texImage2D(c,u,p,e,i,0,h,d,null)}n.bindFramebuffer(t.FRAMEBUFFER,e),nt(r)?l.framebufferTexture2DMultisampleEXT(t.FRAMEBUFFER,s,c,i.get(o).__webglTexture,0,tt(r)):(c===t.TEXTURE_2D||c>=t.TEXTURE_CUBE_MAP_POSITIVE_X&&c<=t.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&t.framebufferTexture2D(t.FRAMEBUFFER,s,c,i.get(o).__webglTexture,u),n.bindFramebuffer(t.FRAMEBUFFER,null)}function J(e,n,i){if(t.bindRenderbuffer(t.RENDERBUFFER,e),n.depthBuffer&&!n.stencilBuffer){let r=!0===s?t.DEPTH_COMPONENT24:t.DEPTH_COMPONENT16;if(i||nt(n)){const e=n.depthTexture;e&&e.isDepthTexture&&(e.type===j?r=t.DEPTH_COMPONENT32F:e.type===V&&(r=t.DEPTH_COMPONENT24));const i=tt(n);nt(n)?l.renderbufferStorageMultisampleEXT(t.RENDERBUFFER,i,r,n.width,n.height):t.renderbufferStorageMultisample(t.RENDERBUFFER,i,r,n.width,n.height)}else t.renderbufferStorage(t.RENDERBUFFER,r,n.width,n.height);t.framebufferRenderbuffer(t.FRAMEBUFFER,t.DEPTH_ATTACHMENT,t.RENDERBUFFER,e)}else if(n.depthBuffer&&n.stencilBuffer){const r=tt(n);i&&!1===nt(n)?t.renderbufferStorageMultisample(t.RENDERBUFFER,r,t.DEPTH24_STENCIL8,n.width,n.height):nt(n)?l.renderbufferStorageMultisampleEXT(t.RENDERBUFFER,r,t.DEPTH24_STENCIL8,n.width,n.height):t.renderbufferStorage(t.RENDERBUFFER,t.DEPTH_STENCIL,n.width,n.height),t.framebufferRenderbuffer(t.FRAMEBUFFER,t.DEPTH_STENCIL_ATTACHMENT,t.RENDERBUFFER,e)}else{const e=n.textures;for(let r=0;r0&&!0===e.has("WEBGL_multisampled_render_to_texture")&&!1!==n.__useRenderToTexture}function rt(t,n){const i=t.colorSpace,r=t.format,a=t.type;return!0===t.isCompressedTexture||!0===t.isVideoTexture||t.format===dt||i!==it&&i!==et&&(Bt.getTransfer(i)===st?!1===s?!0===e.has("EXT_sRGB")&&r===q?(t.format=dt,t.minFilter=F,t.generateMipmaps=!1):n=jt.sRGBToLinear(n):r===q&&a===B||console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",i)),n}function at(t){return"undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement?(u.width=t.naturalWidth||t.width,u.height=t.naturalHeight||t.height):"undefined"!=typeof VideoFrame&&t instanceof VideoFrame?(u.width=t.displayWidth,u.height=t.displayHeight):(u.width=t.width,u.height=t.height),u}this.allocateTextureUnit=function(){const t=T;return t>=r.maxTextures&&console.warn("THREE.WebGLTextures: Trying to use "+t+" texture units while this GPU supports only "+r.maxTextures),T+=1,t},this.resetTextureUnits=function(){T=0},this.setTexture2D=A,this.setTexture2DArray=function(e,r){const a=i.get(e);e.version>0&&a.__version!==e.version?K(a,e,r):n.bindTexture(t.TEXTURE_2D_ARRAY,a.__webglTexture,t.TEXTURE0+r)},this.setTexture3D=function(e,r){const a=i.get(e);e.version>0&&a.__version!==e.version?K(a,e,r):n.bindTexture(t.TEXTURE_3D,a.__webglTexture,t.TEXTURE0+r)},this.setTextureCube=function(e,o){const l=i.get(e);e.version>0&&l.__version!==e.version?function(e,o,l){if(6!==o.image.length)return;const c=G(e,o),u=o.source;n.bindTexture(t.TEXTURE_CUBE_MAP,e.__webglTexture,t.TEXTURE0+l);const h=i.get(u);if(u.version!==h.__version||!0===c){n.activeTexture(t.TEXTURE0+l);const e=Bt.getPrimaries(Bt.workingColorSpace),i=o.colorSpace===et?null:Bt.getPrimaries(o.colorSpace),d=o.colorSpace===et||e===i?t.NONE:t.BROWSER_DEFAULT_WEBGL;t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,o.flipY),t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,o.premultiplyAlpha),t.pixelStorei(t.UNPACK_ALIGNMENT,o.unpackAlignment),t.pixelStorei(t.UNPACK_COLORSPACE_CONVERSION_WEBGL,d);const p=o.isCompressedTexture||o.image[0].isCompressedTexture,f=o.image[0]&&o.image[0].isDataTexture,m=[];for(let t=0;t<6;t++)m[t]=p||f?f?o.image[t].image:o.image[t]:g(o.image[t],!1,!0,r.maxCubemapSize),m[t]=rt(o,m[t]);const M=m[0],S=v(M)||s,E=a.convert(o.format,o.colorSpace),w=a.convert(o.type),T=x(o.internalFormat,E,w,o.colorSpace),A=s&&!0!==o.isVideoTexture,R=void 0===h.__version||!0===c,C=u.dataReady;let P,O=b(o,M,S);if(L(t.TEXTURE_CUBE_MAP,o,S),p){A&&R&&n.texStorage2D(t.TEXTURE_CUBE_MAP,O,T,M.width,M.height);for(let e=0;e<6;e++){P=m[e].mipmaps;for(let i=0;i0&&O++;const e=at(m[0]);n.texStorage2D(t.TEXTURE_CUBE_MAP,O,T,e.width,e.height)}for(let e=0;e<6;e++)if(f){A?C&&n.texSubImage2D(t.TEXTURE_CUBE_MAP_POSITIVE_X+e,0,0,0,m[e].width,m[e].height,E,w,m[e].data):n.texImage2D(t.TEXTURE_CUBE_MAP_POSITIVE_X+e,0,T,m[e].width,m[e].height,0,E,w,m[e].data);for(let i=0;i1,f=v(e)||s;if(p||(void 0===u.__webglTexture&&(u.__webglTexture=t.createTexture()),u.__version=l.version,o.memory.textures++),d){c.__webglFramebuffer=[];for(let e=0;e<6;e++)if(s&&l.mipmaps&&l.mipmaps.length>0){c.__webglFramebuffer[e]=[];for(let n=0;n0){c.__webglFramebuffer=[];for(let e=0;e0&&!1===nt(e)){c.__webglMultisampledFramebuffer=t.createFramebuffer(),c.__webglColorRenderbuffer=[],n.bindFramebuffer(t.FRAMEBUFFER,c.__webglMultisampledFramebuffer);for(let n=0;n0)for(let i=0;i0)for(let n=0;n0&&!1===nt(e)){const r=e.textures,a=e.width,o=e.height;let s=t.COLOR_BUFFER_BIT;const l=[],u=e.stencilBuffer?t.DEPTH_STENCIL_ATTACHMENT:t.DEPTH_ATTACHMENT,h=i.get(e),d=r.length>1;if(d)for(let e=0;es+c?(l.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!l.inputState.pinching&&o<=s-c&&(l.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:t.handedness,target:this}))}else null!==s&&t.gripSpace&&(r=e.getPose(t.gripSpace,n),null!==r&&(s.matrix.fromArray(r.transform.matrix),s.matrix.decompose(s.position,s.rotation,s.scale),s.matrixWorldNeedsUpdate=!0,r.linearVelocity?(s.hasLinearVelocity=!0,s.linearVelocity.copy(r.linearVelocity)):s.hasLinearVelocity=!1,r.angularVelocity?(s.hasAngularVelocity=!0,s.angularVelocity.copy(r.angularVelocity)):s.hasAngularVelocity=!1));null!==o&&(i=e.getPose(t.targetRaySpace,n),null===i&&null!==r&&(i=r),null!==i&&(o.matrix.fromArray(i.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,i.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(i.linearVelocity)):o.hasLinearVelocity=!1,i.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(i.angularVelocity)):o.hasAngularVelocity=!1,this.dispatchEvent(lo)))}return null!==o&&(o.visible=null!==i),null!==s&&(s.visible=null!==r),null!==l&&(l.visible=null!==a),this}_getHandJoint(t,e){if(void 0===t.joints[e.jointName]){const n=new so;n.matrixAutoUpdate=!1,n.visible=!1,t.joints[e.jointName]=n,t.add(n)}return t.joints[e.jointName]}}class uo{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(t,e,n){if(null===this.texture){const i=new $t;t.properties.get(i).__webglTexture=e.texture,e.depthNear==n.depthNear&&e.depthFar==n.depthFar||(this.depthNear=e.depthNear,this.depthFar=e.depthFar),this.texture=i}}render(t,e){if(null!==this.texture){if(null===this.mesh){const t=e.cameras[0].viewport,n=new hi({extensions:{fragDepth:!0},vertexShader:"\nvoid main() {\n\n\tgl_Position = vec4( position, 1.0 );\n\n}",fragmentShader:"\nuniform sampler2DArray depthColor;\nuniform float depthWidth;\nuniform float depthHeight;\n\nvoid main() {\n\n\tvec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight );\n\n\tif ( coord.x >= 1.0 ) {\n\n\t\tgl_FragDepthEXT = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r;\n\n\t} else {\n\n\t\tgl_FragDepthEXT = 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 ri(new Pi(20,20),n)}t.render(this.mesh,e)}}reset(){this.texture=null,this.mesh=null}}class ho extends mt{constructor(t,e){super();const n=this;let i=null,r=1,a=null,o="local-floor",s=1,l=null,c=null,u=null,h=null,d=null,p=null;const f=new uo,m=e.getContextAttributes();let g=null,v=null;const _=[],y=[],x=new Ct;let b=null;const M=new gi;M.layers.enable(1),M.viewport=new Kt;const S=new gi;S.layers.enable(2),S.viewport=new Kt;const E=[M,S],w=new oo;w.layers.enable(1),w.layers.enable(2);let T=null,A=null;function R(t){const e=y.indexOf(t.inputSource);if(-1===e)return;const n=_[e];void 0!==n&&(n.update(t.inputSource,t.frame,l||a),n.dispatchEvent({type:t.type,data:t.inputSource}))}function C(){i.removeEventListener("select",R),i.removeEventListener("selectstart",R),i.removeEventListener("selectend",R),i.removeEventListener("squeeze",R),i.removeEventListener("squeezestart",R),i.removeEventListener("squeezeend",R),i.removeEventListener("end",C),i.removeEventListener("inputsourceschange",P);for(let t=0;t<_.length;t++){const e=y[t];null!==e&&(y[t]=null,_[t].disconnect(e))}T=null,A=null,f.reset(),t.setRenderTarget(g),d=null,h=null,u=null,i=null,v=null,I.stop(),n.isPresenting=!1,t.setPixelRatio(b),t.setSize(x.width,x.height,!1),n.dispatchEvent({type:"sessionend"})}function P(t){for(let e=0;e=0&&(y[i]=null,_[i].disconnect(n))}for(let e=0;e=y.length){y.push(n),i=t;break}if(null===y[t]){y[t]=n,i=t;break}}if(-1===i)break}const r=_[i];r&&r.connect(n)}}this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(t){let e=_[t];return void 0===e&&(e=new co,_[t]=e),e.getTargetRaySpace()},this.getControllerGrip=function(t){let e=_[t];return void 0===e&&(e=new co,_[t]=e),e.getGripSpace()},this.getHand=function(t){let e=_[t];return void 0===e&&(e=new co,_[t]=e),e.getHandSpace()},this.setFramebufferScaleFactor=function(t){r=t,!0===n.isPresenting&&console.warn("THREE.WebXRManager: Cannot change framebuffer scale while presenting.")},this.setReferenceSpaceType=function(t){o=t,!0===n.isPresenting&&console.warn("THREE.WebXRManager: Cannot change reference space type while presenting.")},this.getReferenceSpace=function(){return l||a},this.setReferenceSpace=function(t){l=t},this.getBaseLayer=function(){return null!==h?h:d},this.getBinding=function(){return u},this.getFrame=function(){return p},this.getSession=function(){return i},this.setSession=async function(c){if(i=c,null!==i){if(g=t.getRenderTarget(),i.addEventListener("select",R),i.addEventListener("selectstart",R),i.addEventListener("selectend",R),i.addEventListener("squeeze",R),i.addEventListener("squeezestart",R),i.addEventListener("squeezeend",R),i.addEventListener("end",C),i.addEventListener("inputsourceschange",P),!0!==m.xrCompatible&&await e.makeXRCompatible(),b=t.getPixelRatio(),t.getSize(x),void 0===i.renderState.layers||!1===t.capabilities.isWebGL2){const n={antialias:void 0!==i.renderState.layers||m.antialias,alpha:!0,depth:m.depth,stencil:m.stencil,framebufferScaleFactor:r};d=new XRWebGLLayer(i,e,n),i.updateRenderState({baseLayer:d}),t.setPixelRatio(1),t.setSize(d.framebufferWidth,d.framebufferHeight,!1),v=new Jt(d.framebufferWidth,d.framebufferHeight,{format:q,type:B,colorSpace:t.outputColorSpace,stencilBuffer:m.stencil})}else{let n=null,a=null,o=null;m.depth&&(o=m.stencil?e.DEPTH24_STENCIL8:e.DEPTH_COMPONENT24,n=m.stencil?$:Y,a=m.stencil?X:V);const s={colorFormat:e.RGBA8,depthFormat:o,scaleFactor:r};u=new XRWebGLBinding(i,e),h=u.createProjectionLayer(s),i.updateRenderState({layers:[h]}),t.setPixelRatio(1),t.setSize(h.textureWidth,h.textureHeight,!1),v=new Jt(h.textureWidth,h.textureHeight,{format:q,type:B,depthTexture:new mr(h.textureWidth,h.textureHeight,a,void 0,void 0,void 0,void 0,void 0,void 0,n),stencilBuffer:m.stencil,colorSpace:t.outputColorSpace,samples:m.antialias?4:0});t.properties.get(v).__ignoreDepthValues=h.ignoreDepthValues}v.isXRRenderTarget=!0,this.setFoveation(s),l=null,a=await i.requestReferenceSpace(o),I.setContext(i),I.start(),n.isPresenting=!0,n.dispatchEvent({type:"sessionstart"})}},this.getEnvironmentBlendMode=function(){if(null!==i)return i.environmentBlendMode};const L=new ne,O=new ne;function D(t,e){null===e?t.matrixWorld.copy(t.matrix):t.matrixWorld.multiplyMatrices(e.matrixWorld,t.matrix),t.matrixWorldInverse.copy(t.matrixWorld).invert()}this.updateCamera=function(t){if(null===i)return;null!==f.texture&&(t.near=f.depthNear,t.far=f.depthFar),w.near=S.near=M.near=t.near,w.far=S.far=M.far=t.far,T===w.near&&A===w.far||(i.updateRenderState({depthNear:w.near,depthFar:w.far}),T=w.near,A=w.far,M.near=T,M.far=A,S.near=T,S.far=A,M.updateProjectionMatrix(),S.updateProjectionMatrix(),t.updateProjectionMatrix());const e=t.parent,n=w.cameras;D(w,e);for(let t=0;t0&&(i.alphaTest.value=r.alphaTest);const a=e.get(r),o=a.envMap,s=a.envMapRotation;if(o&&(i.envMap.value=o,po.copy(s),po.x*=-1,po.y*=-1,po.z*=-1,o.isCubeTexture&&!1===o.isRenderTargetTexture&&(po.y*=-1,po.z*=-1),i.envMapRotation.value.setFromMatrix4(fo.makeRotationFromEuler(po)),i.flipEnvMap.value=o.isCubeTexture&&!1===o.isRenderTargetTexture?-1:1,i.reflectivity.value=r.reflectivity,i.ior.value=r.ior,i.refractionRatio.value=r.refractionRatio),r.lightMap){i.lightMap.value=r.lightMap;const e=!0===t._useLegacyLights?Math.PI:1;i.lightMapIntensity.value=r.lightMapIntensity*e,n(r.lightMap,i.lightMapTransform)}r.aoMap&&(i.aoMap.value=r.aoMap,i.aoMapIntensity.value=r.aoMapIntensity,n(r.aoMap,i.aoMapTransform))}return{refreshFogUniforms:function(e,n){n.color.getRGB(e.fogColor.value,ci(t)),n.isFog?(e.fogNear.value=n.near,e.fogFar.value=n.far):n.isFogExp2&&(e.fogDensity.value=n.density)},refreshMaterialUniforms:function(t,r,a,o,s){r.isMeshBasicMaterial||r.isMeshLambertMaterial?i(t,r):r.isMeshToonMaterial?(i(t,r),function(t,e){e.gradientMap&&(t.gradientMap.value=e.gradientMap)}(t,r)):r.isMeshPhongMaterial?(i(t,r),function(t,e){t.specular.value.copy(e.specular),t.shininess.value=Math.max(e.shininess,1e-4)}(t,r)):r.isMeshStandardMaterial?(i(t,r),function(t,i){t.metalness.value=i.metalness,i.metalnessMap&&(t.metalnessMap.value=i.metalnessMap,n(i.metalnessMap,t.metalnessMapTransform));t.roughness.value=i.roughness,i.roughnessMap&&(t.roughnessMap.value=i.roughnessMap,n(i.roughnessMap,t.roughnessMapTransform));const r=e.get(i).envMap;r&&(t.envMapIntensity.value=i.envMapIntensity)}(t,r),r.isMeshPhysicalMaterial&&function(t,e,i){t.ior.value=e.ior,e.sheen>0&&(t.sheenColor.value.copy(e.sheenColor).multiplyScalar(e.sheen),t.sheenRoughness.value=e.sheenRoughness,e.sheenColorMap&&(t.sheenColorMap.value=e.sheenColorMap,n(e.sheenColorMap,t.sheenColorMapTransform)),e.sheenRoughnessMap&&(t.sheenRoughnessMap.value=e.sheenRoughnessMap,n(e.sheenRoughnessMap,t.sheenRoughnessMapTransform)));e.clearcoat>0&&(t.clearcoat.value=e.clearcoat,t.clearcoatRoughness.value=e.clearcoatRoughness,e.clearcoatMap&&(t.clearcoatMap.value=e.clearcoatMap,n(e.clearcoatMap,t.clearcoatMapTransform)),e.clearcoatRoughnessMap&&(t.clearcoatRoughnessMap.value=e.clearcoatRoughnessMap,n(e.clearcoatRoughnessMap,t.clearcoatRoughnessMapTransform)),e.clearcoatNormalMap&&(t.clearcoatNormalMap.value=e.clearcoatNormalMap,n(e.clearcoatNormalMap,t.clearcoatNormalMapTransform),t.clearcoatNormalScale.value.copy(e.clearcoatNormalScale),e.side===g&&t.clearcoatNormalScale.value.negate()));e.iridescence>0&&(t.iridescence.value=e.iridescence,t.iridescenceIOR.value=e.iridescenceIOR,t.iridescenceThicknessMinimum.value=e.iridescenceThicknessRange[0],t.iridescenceThicknessMaximum.value=e.iridescenceThicknessRange[1],e.iridescenceMap&&(t.iridescenceMap.value=e.iridescenceMap,n(e.iridescenceMap,t.iridescenceMapTransform)),e.iridescenceThicknessMap&&(t.iridescenceThicknessMap.value=e.iridescenceThicknessMap,n(e.iridescenceThicknessMap,t.iridescenceThicknessMapTransform)));e.transmission>0&&(t.transmission.value=e.transmission,t.transmissionSamplerMap.value=i.texture,t.transmissionSamplerSize.value.set(i.width,i.height),e.transmissionMap&&(t.transmissionMap.value=e.transmissionMap,n(e.transmissionMap,t.transmissionMapTransform)),t.thickness.value=e.thickness,e.thicknessMap&&(t.thicknessMap.value=e.thicknessMap,n(e.thicknessMap,t.thicknessMapTransform)),t.attenuationDistance.value=e.attenuationDistance,t.attenuationColor.value.copy(e.attenuationColor));e.anisotropy>0&&(t.anisotropyVector.value.set(e.anisotropy*Math.cos(e.anisotropyRotation),e.anisotropy*Math.sin(e.anisotropyRotation)),e.anisotropyMap&&(t.anisotropyMap.value=e.anisotropyMap,n(e.anisotropyMap,t.anisotropyMapTransform)));t.specularIntensity.value=e.specularIntensity,t.specularColor.value.copy(e.specularColor),e.specularColorMap&&(t.specularColorMap.value=e.specularColorMap,n(e.specularColorMap,t.specularColorMapTransform));e.specularIntensityMap&&(t.specularIntensityMap.value=e.specularIntensityMap,n(e.specularIntensityMap,t.specularIntensityMapTransform))}(t,r,s)):r.isMeshMatcapMaterial?(i(t,r),function(t,e){e.matcap&&(t.matcap.value=e.matcap)}(t,r)):r.isMeshDepthMaterial?i(t,r):r.isMeshDistanceMaterial?(i(t,r),function(t,n){const i=e.get(n).light;t.referencePosition.value.setFromMatrixPosition(i.matrixWorld),t.nearDistance.value=i.shadow.camera.near,t.farDistance.value=i.shadow.camera.far}(t,r)):r.isMeshNormalMaterial?i(t,r):r.isLineBasicMaterial?(function(t,e){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity,e.map&&(t.map.value=e.map,n(e.map,t.mapTransform))}(t,r),r.isLineDashedMaterial&&function(t,e){t.dashSize.value=e.dashSize,t.totalSize.value=e.dashSize+e.gapSize,t.scale.value=e.scale}(t,r)):r.isPointsMaterial?function(t,e,i,r){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity,t.size.value=e.size*i,t.scale.value=.5*r,e.map&&(t.map.value=e.map,n(e.map,t.uvTransform));e.alphaMap&&(t.alphaMap.value=e.alphaMap,n(e.alphaMap,t.alphaMapTransform));e.alphaTest>0&&(t.alphaTest.value=e.alphaTest)}(t,r,a,o):r.isSpriteMaterial?function(t,e){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity,t.rotation.value=e.rotation,e.map&&(t.map.value=e.map,n(e.map,t.mapTransform));e.alphaMap&&(t.alphaMap.value=e.alphaMap,n(e.alphaMap,t.alphaMapTransform));e.alphaTest>0&&(t.alphaTest.value=e.alphaTest)}(t,r):r.isShadowMaterial?(t.color.value.copy(r.color),t.opacity.value=r.opacity):r.isShaderMaterial&&(r.uniformsNeedUpdate=!1)}}}function go(t,e,n,i){let r={},a={},o=[];const s=n.isWebGL2?t.getParameter(t.MAX_UNIFORM_BUFFER_BINDINGS):0;function l(t,e,n,i){const r=t.value,a=e+"_"+n;if(void 0===i[a])return i[a]="number"==typeof r||"boolean"==typeof r?r:r.clone(),!0;{const t=i[a];if("number"==typeof r||"boolean"==typeof r){if(t!==r)return i[a]=r,!0}else if(!1===t.equals(r))return t.copy(r),!0}return!1}function c(t){const e={boundary:0,storage:0};return"number"==typeof t||"boolean"==typeof t?(e.boundary=4,e.storage=4):t.isVector2?(e.boundary=8,e.storage=8):t.isVector3||t.isColor?(e.boundary=16,e.storage=12):t.isVector4?(e.boundary=16,e.storage=16):t.isMatrix3?(e.boundary=48,e.storage=48):t.isMatrix4?(e.boundary=64,e.storage=64):t.isTexture?console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group."):console.warn("THREE.WebGLRenderer: Unsupported uniform value type.",t),e}function u(e){const n=e.target;n.removeEventListener("dispose",u);const i=o.indexOf(n.__bindingPointIndex);o.splice(i,1),t.deleteBuffer(r[n.id]),delete r[n.id],delete a[n.id]}return{bind:function(t,e){const n=e.program;i.uniformBlockBinding(t,n)},update:function(n,h){let d=r[n.id];void 0===d&&(!function(t){const e=t.uniforms;let n=0;const i=16;for(let t=0,r=e.length;t0&&(n+=i-r);t.__size=n,t.__cache={}}(n),d=function(e){const n=function(){for(let t=0;t0),h=!!n.morphAttributes.position,d=!!n.morphAttributes.normal,p=!!n.morphAttributes.color;let f=b;i.toneMapped&&(null!==T&&!0!==T.isXRRenderTarget||(f=M.toneMapping));const m=n.morphAttributes.position||n.morphAttributes.normal||n.morphAttributes.color,g=void 0!==m?m.length:0,v=ht.get(i),y=_.state.lights;if(!0===Z&&(!0===J||t!==R)){const e=t===R&&i.id===A;Mt.setState(i,t,e)}let x=!1;i.version===v.__version?v.needsLights&&v.lightsStateVersion!==y.state.version||v.outputColorSpace!==s||r.isBatchedMesh&&!1===v.batching?x=!0:r.isBatchedMesh||!0!==v.batching?r.isInstancedMesh&&!1===v.instancing?x=!0:r.isInstancedMesh||!0!==v.instancing?r.isSkinnedMesh&&!1===v.skinning?x=!0:r.isSkinnedMesh||!0!==v.skinning?r.isInstancedMesh&&!0===v.instancingColor&&null===r.instanceColor||r.isInstancedMesh&&!1===v.instancingColor&&null!==r.instanceColor||r.isInstancedMesh&&!0===v.instancingMorph&&null===r.morphTexture||r.isInstancedMesh&&!1===v.instancingMorph&&null!==r.morphTexture||v.envMap!==l||!0===i.fog&&v.fog!==a?x=!0:void 0===v.numClippingPlanes||v.numClippingPlanes===Mt.numPlanes&&v.numIntersection===Mt.numIntersection?(v.vertexAlphas!==c||v.vertexTangents!==u||v.morphTargets!==h||v.morphNormals!==d||v.morphColors!==p||v.toneMapping!==f||!0===lt.isWebGL2&&v.morphTargetsCount!==g)&&(x=!0):x=!0:x=!0:x=!0:x=!0:(x=!0,v.__version=i.version);let S=v.currentProgram;!0===x&&(S=Qt(i,e,r));let E=!1,w=!1,C=!1;const P=S.getUniforms(),L=v.uniforms;ct.useProgram(S.program)&&(E=!0,w=!0,C=!0);i.id!==A&&(A=i.id,w=!0);if(E||R!==t){P.setValue(Dt,"projectionMatrix",t.projectionMatrix),P.setValue(Dt,"viewMatrix",t.matrixWorldInverse);const e=P.map.cameraPosition;void 0!==e&&e.setValue(Dt,rt.setFromMatrixPosition(t.matrixWorld)),lt.logarithmicDepthBuffer&&P.setValue(Dt,"logDepthBufFC",2/(Math.log(t.far+1)/Math.LN2)),(i.isMeshPhongMaterial||i.isMeshToonMaterial||i.isMeshLambertMaterial||i.isMeshBasicMaterial||i.isMeshStandardMaterial||i.isShaderMaterial)&&P.setValue(Dt,"isOrthographic",!0===t.isOrthographicCamera),R!==t&&(R=t,w=!0,C=!0)}if(r.isSkinnedMesh){P.setOptional(Dt,r,"bindMatrix"),P.setOptional(Dt,r,"bindMatrixInverse");const t=r.skeleton;t&&(lt.floatVertexTextures?(null===t.boneTexture&&t.computeBoneTexture(),P.setValue(Dt,"boneTexture",t.boneTexture,dt)):console.warn("THREE.WebGLRenderer: SkinnedMesh can only be used with WebGL 2. With WebGL 1 OES_texture_float and vertex textures support is required."))}r.isBatchedMesh&&(P.setOptional(Dt,r,"batchingTexture"),P.setValue(Dt,"batchingTexture",r._matricesTexture,dt));const O=n.morphAttributes;(void 0!==O.position||void 0!==O.normal||void 0!==O.color&&!0===lt.isWebGL2)&&Tt.update(r,n,S);(w||v.receiveShadow!==r.receiveShadow)&&(v.receiveShadow=r.receiveShadow,P.setValue(Dt,"receiveShadow",r.receiveShadow));i.isMeshGouraudMaterial&&null!==i.envMap&&(L.envMap.value=l,L.flipEnvMap.value=l.isCubeTexture&&!1===l.isRenderTargetTexture?-1:1);w&&(P.setValue(Dt,"toneMappingExposure",M.toneMappingExposure),v.needsLights&&(N=C,(D=L).ambientLightColor.needsUpdate=N,D.lightProbe.needsUpdate=N,D.directionalLights.needsUpdate=N,D.directionalLightShadows.needsUpdate=N,D.pointLights.needsUpdate=N,D.pointLightShadows.needsUpdate=N,D.spotLights.needsUpdate=N,D.spotLightShadows.needsUpdate=N,D.rectAreaLights.needsUpdate=N,D.hemisphereLights.needsUpdate=N),a&&!0===i.fog&&yt.refreshFogUniforms(L,a),yt.refreshMaterialUniforms(L,i,U,I,Q),xa.upload(Dt,te(v),L,dt));var D,N;i.isShaderMaterial&&!0===i.uniformsNeedUpdate&&(xa.upload(Dt,te(v),L,dt),i.uniformsNeedUpdate=!1);i.isSpriteMaterial&&P.setValue(Dt,"center",r.center);if(P.setValue(Dt,"modelViewMatrix",r.modelViewMatrix),P.setValue(Dt,"normalMatrix",r.normalMatrix),P.setValue(Dt,"modelMatrix",r.matrixWorld),i.isShaderMaterial||i.isRawShaderMaterial){const t=i.uniformsGroups;for(let e=0,n=t.length;e{function n(){i.forEach((function(t){ht.get(t).currentProgram.isReady()&&i.delete(t)})),0!==i.size?setTimeout(n,10):e(t)}null!==st.get("KHR_parallel_shader_compile")?n():setTimeout(n,10)}))};let Vt=null;function jt(){Xt.stop()}function Wt(){Xt.start()}const Xt=new Ri;function qt(t,e,n,i){if(!1===t.visible)return;if(t.layers.test(e.layers))if(t.isGroup)n=t.renderOrder;else if(t.isLOD)!0===t.autoUpdate&&t.update(e);else if(t.isLight)_.pushLight(t),t.castShadow&&_.pushShadow(t);else if(t.isSprite){if(!t.frustumCulled||K.intersectsSprite(t)){i&&rt.setFromMatrixPosition(t.matrixWorld).applyMatrix4(tt);const e=vt.update(t),r=t.material;r.visible&&v.push(t,e,r,n,rt.z,null)}}else if((t.isMesh||t.isLine||t.isPoints)&&(!t.frustumCulled||K.intersectsObject(t))){const e=vt.update(t),r=t.material;if(i&&(void 0!==t.boundingSphere?(null===t.boundingSphere&&t.computeBoundingSphere(),rt.copy(t.boundingSphere.center)):(null===e.boundingSphere&&e.computeBoundingSphere(),rt.copy(e.boundingSphere.center)),rt.applyMatrix4(t.matrixWorld).applyMatrix4(tt)),Array.isArray(r)){const i=e.groups;for(let a=0,o=i.length;a0&&function(t,e,n,i){const r=!0===n.isScene?n.overrideMaterial:null;if(null!==r)return;const a=lt.isWebGL2;null===Q&&(Q=new Jt(1,1,{generateMipmaps:!0,type:st.has("EXT_color_buffer_half_float")?W:B,minFilter:z,samples:a?4:0}));M.getDrawingBufferSize(et),a?Q.setSize(et.x,et.y):Q.setSize(wt(et.x),wt(et.y));const o=M.getRenderTarget();M.setRenderTarget(Q),M.getClearColor(O),D=M.getClearAlpha(),D<1&&M.setClearColor(16777215,.5);M.clear();const s=M.toneMapping;M.toneMapping=b,$t(t,n,i),dt.updateMultisampleRenderTarget(Q),dt.updateRenderTargetMipmap(Q);let l=!1;for(let t=0,r=e.length;t0&&$t(r,e,n),a.length>0&&$t(a,e,n),o.length>0&&$t(o,e,n),ct.buffers.depth.setTest(!0),ct.buffers.depth.setMask(!0),ct.buffers.color.setMask(!0),ct.setPolygonOffset(!1)}function $t(t,e,n){const i=!0===e.isScene?e.overrideMaterial:null;for(let r=0,a=t.length;r0?x[x.length-1]:null,y.pop(),v=y.length>0?y[y.length-1]:null},this.getActiveCubeFace=function(){return E},this.getActiveMipmapLevel=function(){return w},this.getRenderTarget=function(){return T},this.setRenderTargetTextures=function(t,e,n){ht.get(t.texture).__webglTexture=e,ht.get(t.depthTexture).__webglTexture=n;const i=ht.get(t);i.__hasExternalTextures=!0,i.__autoAllocateDepthBuffer=void 0===n,i.__autoAllocateDepthBuffer||!0===st.has("WEBGL_multisampled_render_to_texture")&&(console.warn("THREE.WebGLRenderer: Render-to-texture extension was disabled because an external texture was provided"),i.__useRenderToTexture=!1)},this.setRenderTargetFramebuffer=function(t,e){const n=ht.get(t);n.__webglFramebuffer=e,n.__useDefaultFramebuffer=void 0===e},this.setRenderTarget=function(t,e=0,n=0){T=t,E=e,w=n;let i=!0,r=null,a=!1,o=!1;if(t){const s=ht.get(t);void 0!==s.__useDefaultFramebuffer?(ct.bindFramebuffer(Dt.FRAMEBUFFER,null),i=!1):void 0===s.__webglFramebuffer?dt.setupRenderTarget(t):s.__hasExternalTextures&&dt.rebindTextures(t,ht.get(t.texture).__webglTexture,ht.get(t.depthTexture).__webglTexture);const l=t.texture;(l.isData3DTexture||l.isDataArrayTexture||l.isCompressedArrayTexture)&&(o=!0);const c=ht.get(t).__webglFramebuffer;t.isWebGLCubeRenderTarget?(r=Array.isArray(c[e])?c[e][n]:c[e],a=!0):r=lt.isWebGL2&&t.samples>0&&!1===dt.useMultisampledRTT(t)?ht.get(t).__webglMultisampledFramebuffer:Array.isArray(c)?c[n]:c,C.copy(t.viewport),P.copy(t.scissor),L=t.scissorTest}else C.copy(G).multiplyScalar(U).floor(),P.copy(Y).multiplyScalar(U).floor(),L=$;if(ct.bindFramebuffer(Dt.FRAMEBUFFER,r)&<.drawBuffers&&i&&ct.drawBuffers(t,r),ct.viewport(C),ct.scissor(P),ct.setScissorTest(L),a){const i=ht.get(t.texture);Dt.framebufferTexture2D(Dt.FRAMEBUFFER,Dt.COLOR_ATTACHMENT0,Dt.TEXTURE_CUBE_MAP_POSITIVE_X+e,i.__webglTexture,n)}else if(o){const i=ht.get(t.texture),r=e||0;Dt.framebufferTextureLayer(Dt.FRAMEBUFFER,Dt.COLOR_ATTACHMENT0,i.__webglTexture,n||0,r)}A=-1},this.readRenderTargetPixels=function(t,e,n,i,r,a,o){if(!t||!t.isWebGLRenderTarget)return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let s=ht.get(t).__webglFramebuffer;if(t.isWebGLCubeRenderTarget&&void 0!==o&&(s=s[o]),s){ct.bindFramebuffer(Dt.FRAMEBUFFER,s);try{const o=t.texture,s=o.format,l=o.type;if(s!==q&&Pt.convert(s)!==Dt.getParameter(Dt.IMPLEMENTATION_COLOR_READ_FORMAT))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");const c=l===W&&(st.has("EXT_color_buffer_half_float")||lt.isWebGL2&&st.has("EXT_color_buffer_float"));if(!(l===B||Pt.convert(l)===Dt.getParameter(Dt.IMPLEMENTATION_COLOR_READ_TYPE)||l===j&&(lt.isWebGL2||st.has("OES_texture_float")||st.has("WEBGL_color_buffer_float"))||c))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");e>=0&&e<=t.width-i&&n>=0&&n<=t.height-r&&Dt.readPixels(e,n,i,r,Pt.convert(s),Pt.convert(l),a)}finally{const t=null!==T?ht.get(T).__webglFramebuffer:null;ct.bindFramebuffer(Dt.FRAMEBUFFER,t)}}},this.copyFramebufferToTexture=function(t,e,n=0){const i=Math.pow(2,-n),r=Math.floor(e.image.width*i),a=Math.floor(e.image.height*i);dt.setTexture2D(e,0),Dt.copyTexSubImage2D(Dt.TEXTURE_2D,n,0,0,t.x,t.y,r,a),ct.unbindTexture()},this.copyTextureToTexture=function(t,e,n,i=0){const r=e.image.width,a=e.image.height,o=Pt.convert(n.format),s=Pt.convert(n.type);dt.setTexture2D(n,0),Dt.pixelStorei(Dt.UNPACK_FLIP_Y_WEBGL,n.flipY),Dt.pixelStorei(Dt.UNPACK_PREMULTIPLY_ALPHA_WEBGL,n.premultiplyAlpha),Dt.pixelStorei(Dt.UNPACK_ALIGNMENT,n.unpackAlignment),e.isDataTexture?Dt.texSubImage2D(Dt.TEXTURE_2D,i,t.x,t.y,r,a,o,s,e.image.data):e.isCompressedTexture?Dt.compressedTexSubImage2D(Dt.TEXTURE_2D,i,t.x,t.y,e.mipmaps[0].width,e.mipmaps[0].height,o,e.mipmaps[0].data):Dt.texSubImage2D(Dt.TEXTURE_2D,i,t.x,t.y,o,s,e.image),0===i&&n.generateMipmaps&&Dt.generateMipmap(Dt.TEXTURE_2D),ct.unbindTexture()},this.copyTextureToTexture3D=function(t,e,n,i,r=0){if(M.isWebGL1Renderer)return void console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: can only be used with WebGL2.");const a=Math.round(t.max.x-t.min.x),o=Math.round(t.max.y-t.min.y),s=t.max.z-t.min.z+1,l=Pt.convert(i.format),c=Pt.convert(i.type);let u;if(i.isData3DTexture)dt.setTexture3D(i,0),u=Dt.TEXTURE_3D;else{if(!i.isDataArrayTexture&&!i.isCompressedArrayTexture)return void console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: only supports THREE.DataTexture3D and THREE.DataTexture2DArray.");dt.setTexture2DArray(i,0),u=Dt.TEXTURE_2D_ARRAY}Dt.pixelStorei(Dt.UNPACK_FLIP_Y_WEBGL,i.flipY),Dt.pixelStorei(Dt.UNPACK_PREMULTIPLY_ALPHA_WEBGL,i.premultiplyAlpha),Dt.pixelStorei(Dt.UNPACK_ALIGNMENT,i.unpackAlignment);const h=Dt.getParameter(Dt.UNPACK_ROW_LENGTH),d=Dt.getParameter(Dt.UNPACK_IMAGE_HEIGHT),p=Dt.getParameter(Dt.UNPACK_SKIP_PIXELS),f=Dt.getParameter(Dt.UNPACK_SKIP_ROWS),m=Dt.getParameter(Dt.UNPACK_SKIP_IMAGES),g=n.isCompressedTexture?n.mipmaps[r]:n.image;Dt.pixelStorei(Dt.UNPACK_ROW_LENGTH,g.width),Dt.pixelStorei(Dt.UNPACK_IMAGE_HEIGHT,g.height),Dt.pixelStorei(Dt.UNPACK_SKIP_PIXELS,t.min.x),Dt.pixelStorei(Dt.UNPACK_SKIP_ROWS,t.min.y),Dt.pixelStorei(Dt.UNPACK_SKIP_IMAGES,t.min.z),n.isDataTexture||n.isData3DTexture?Dt.texSubImage3D(u,r,e.x,e.y,e.z,a,o,s,l,c,g.data):i.isCompressedArrayTexture?Dt.compressedTexSubImage3D(u,r,e.x,e.y,e.z,a,o,s,l,g.data):Dt.texSubImage3D(u,r,e.x,e.y,e.z,a,o,s,l,c,g),Dt.pixelStorei(Dt.UNPACK_ROW_LENGTH,h),Dt.pixelStorei(Dt.UNPACK_IMAGE_HEIGHT,d),Dt.pixelStorei(Dt.UNPACK_SKIP_PIXELS,p),Dt.pixelStorei(Dt.UNPACK_SKIP_ROWS,f),Dt.pixelStorei(Dt.UNPACK_SKIP_IMAGES,m),0===r&&i.generateMipmaps&&Dt.generateMipmap(u),ct.unbindTexture()},this.initTexture=function(t){t.isCubeTexture?dt.setTextureCube(t,0):t.isData3DTexture?dt.setTexture3D(t,0):t.isDataArrayTexture||t.isCompressedArrayTexture?dt.setTexture2DArray(t,0):dt.setTexture2D(t,0),ct.unbindTexture()},this.resetState=function(){E=0,w=0,T=null,ct.reset(),Lt.reset()},"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}get coordinateSystem(){return pt}get outputColorSpace(){return this._outputColorSpace}set outputColorSpace(t){this._outputColorSpace=t;const e=this.getContext();e.drawingBufferColorSpace=t===rt?"display-p3":"srgb",e.unpackColorSpace=Bt.workingColorSpace===at?"display-p3":"srgb"}get useLegacyLights(){return console.warn("THREE.WebGLRenderer: The property .useLegacyLights has been deprecated. Migrate your lighting according to the following guide: https://discourse.threejs.org/t/updates-to-lighting-in-three-js-r155/53733."),this._useLegacyLights}set useLegacyLights(t){console.warn("THREE.WebGLRenderer: The property .useLegacyLights has been deprecated. Migrate your lighting according to the following guide: https://discourse.threejs.org/t/updates-to-lighting-in-three-js-r155/53733."),this._useLegacyLights=t}}(class extends vo{}).prototype.isWebGL1Renderer=!0;class _o extends wn{constructor(t){super(),this.isLineBasicMaterial=!0,this.type="LineBasicMaterial",this.color=new Mn(16777215),this.map=null,this.linewidth=1,this.linecap="round",this.linejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.linewidth=t.linewidth,this.linecap=t.linecap,this.linejoin=t.linejoin,this.fog=t.fog,this}}const yo=new ne,xo=new ne,bo=new Oe,Mo=new Le,So=new Se;class Eo{constructor(){this.type="Curve",this.arcLengthDivisions=200}getPoint(){return console.warn("THREE.Curve: .getPoint() not implemented."),null}getPointAt(t,e){const n=this.getUtoTmapping(t);return this.getPoint(n,e)}getPoints(t=5){const e=[];for(let n=0;n<=t;n++)e.push(this.getPoint(n/t));return e}getSpacedPoints(t=5){const e=[];for(let n=0;n<=t;n++)e.push(this.getPointAt(n/t));return e}getLength(){const t=this.getLengths();return t[t.length-1]}getLengths(t=this.arcLengthDivisions){if(this.cacheArcLengths&&this.cacheArcLengths.length===t+1&&!this.needsUpdate)return this.cacheArcLengths;this.needsUpdate=!1;const e=[];let n,i=this.getPoint(0),r=0;e.push(0);for(let a=1;a<=t;a++)n=this.getPoint(a/t),r+=n.distanceTo(i),e.push(r),i=n;return this.cacheArcLengths=e,e}updateArcLengths(){this.needsUpdate=!0,this.getLengths()}getUtoTmapping(t,e){const n=this.getLengths();let i=0;const r=n.length;let a;a=e||t*n[r-1];let o,s=0,l=r-1;for(;s<=l;)if(i=Math.floor(s+(l-s)/2),o=n[i]-a,o<0)s=i+1;else{if(!(o>0)){l=i;break}l=i-1}if(i=l,n[i]===a)return i/(r-1);const c=n[i];return(i+(a-c)/(n[i+1]-c))/(r-1)}getTangent(t,e){const n=1e-4;let i=t-n,r=t+n;i<0&&(i=0),r>1&&(r=1);const a=this.getPoint(i),o=this.getPoint(r),s=e||(a.isVector2?new Ct:new ne);return s.copy(o).sub(a).normalize(),s}getTangentAt(t,e){const n=this.getUtoTmapping(t);return this.getTangent(n,e)}computeFrenetFrames(t,e){const n=new ne,i=[],r=[],a=[],o=new ne,s=new Oe;for(let e=0;e<=t;e++){const n=e/t;i[e]=this.getTangentAt(n,new ne)}r[0]=new ne,a[0]=new ne;let l=Number.MAX_VALUE;const c=Math.abs(i[0].x),u=Math.abs(i[0].y),h=Math.abs(i[0].z);c<=l&&(l=c,n.set(1,0,0)),u<=l&&(l=u,n.set(0,1,0)),h<=l&&n.set(0,0,1),o.crossVectors(i[0],n).normalize(),r[0].crossVectors(i[0],o),a[0].crossVectors(i[0],r[0]);for(let e=1;e<=t;e++){if(r[e]=r[e-1].clone(),a[e]=a[e-1].clone(),o.crossVectors(i[e-1],i[e]),o.length()>Number.EPSILON){o.normalize();const t=Math.acos(bt(i[e-1].dot(i[e]),-1,1));r[e].applyMatrix4(s.makeRotationAxis(o,t))}a[e].crossVectors(i[e],r[e])}if(!0===e){let e=Math.acos(bt(r[0].dot(r[t]),-1,1));e/=t,i[0].dot(o.crossVectors(r[0],r[t]))>0&&(e=-e);for(let n=1;n<=t;n++)r[n].applyMatrix4(s.makeRotationAxis(i[n],e*n)),a[n].crossVectors(i[n],r[n])}return{tangents:i,normals:r,binormals:a}}clone(){return(new this.constructor).copy(this)}copy(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}toJSON(){const t={metadata:{version:4.6,type:"Curve",generator:"Curve.toJSON"}};return t.arcLengthDivisions=this.arcLengthDivisions,t.type=this.type,t}fromJSON(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}}class wo extends Eo{constructor(t=0,e=0,n=1,i=1,r=0,a=2*Math.PI,o=!1,s=0){super(),this.isEllipseCurve=!0,this.type="EllipseCurve",this.aX=t,this.aY=e,this.xRadius=n,this.yRadius=i,this.aStartAngle=r,this.aEndAngle=a,this.aClockwise=o,this.aRotation=s}getPoint(t,e=new Ct){const n=e,i=2*Math.PI;let r=this.aEndAngle-this.aStartAngle;const a=Math.abs(r)i;)r-=i;r0?0:(Math.floor(Math.abs(l)/r)+1)*r:0===c&&l===r-1&&(l=r-2,c=1),this.closed||l>0?o=i[(l-1)%r]:(Ao.subVectors(i[0],i[1]).add(i[0]),o=Ao);const u=i[l%r],h=i[(l+1)%r];if(this.closed||l+2i.length-2?i.length-1:a+1],u=i[a>i.length-3?i.length-1:a+2];return n.set(Lo(o,s.x,l.x,c.x,u.x),Lo(o,s.y,l.y,c.y,u.y)),n}copy(t){super.copy(t),this.points=[];for(let e=0,n=t.points.length;e0&&v(!0),e>0&&v(!1)),this.setIndex(c),this.setAttribute("position",new On(u,3)),this.setAttribute("normal",new On(h,3)),this.setAttribute("uv",new On(d,2))}copy(t){return super.copy(t),this.parameters=Object.assign({},t.parameters),this}static fromJSON(t){return new Fo(t.radiusTop,t.radiusBottom,t.height,t.radialSegments,t.heightSegments,t.openEnded,t.thetaStart,t.thetaLength)}}class ko extends Fo{constructor(t=1,e=1,n=32,i=1,r=!1,a=0,o=2*Math.PI){super(0,t,e,n,i,r,a,o),this.type="ConeGeometry",this.parameters={radius:t,height:e,radialSegments:n,heightSegments:i,openEnded:r,thetaStart:a,thetaLength:o}}static fromJSON(t){return new ko(t.radius,t.height,t.radialSegments,t.heightSegments,t.openEnded,t.thetaStart,t.thetaLength)}}class zo extends Bn{constructor(t=1,e=32,n=16,i=0,r=2*Math.PI,a=0,o=Math.PI){super(),this.type="SphereGeometry",this.parameters={radius:t,widthSegments:e,heightSegments:n,phiStart:i,phiLength:r,thetaStart:a,thetaLength:o},e=Math.max(3,Math.floor(e)),n=Math.max(2,Math.floor(n));const s=Math.min(a+o,Math.PI);let l=0;const c=[],u=new ne,h=new ne,d=[],p=[],f=[],m=[];for(let d=0;d<=n;d++){const g=[],v=d/n;let _=0;0===d&&0===a?_=.5/e:d===n&&s===Math.PI&&(_=-.5/e);for(let n=0;n<=e;n++){const s=n/e;u.x=-t*Math.cos(i+s*r)*Math.sin(a+v*o),u.y=t*Math.cos(a+v*o),u.z=t*Math.sin(i+s*r)*Math.sin(a+v*o),p.push(u.x,u.y,u.z),h.copy(u).normalize(),f.push(h.x,h.y,h.z),m.push(s+_,1-v),g.push(l++)}c.push(g)}for(let t=0;t0)&&d.push(e,r,l),(t!==n-1||s0){const t=a[0].object;as.setFromNormalAndCoplanarPoint(e.getWorldDirection(as.normal),ds.setFromMatrixPosition(t.matrixWorld)),r!==t&&null!==r&&(o.dispatchEvent({type:"hoveroff",object:r}),n.style.cursor="auto",r=null),r!==t&&(o.dispatchEvent({type:"hoveron",object:t}),n.style.cursor="pointer",r=t)}else null!==r&&(o.dispatchEvent({type:"hoveroff",object:r}),n.style.cursor="auto",r=null);us.copy(ss)}}function u(r){!1!==o.enabled&&(d(r),a.length=0,os.setFromCamera(ss,e),os.intersectObjects(t,o.recursive,a),a.length>0&&(i=!0===o.transformGroup?p(a[0].object):a[0].object,as.setFromNormalAndCoplanarPoint(e.getWorldDirection(as.normal),ds.setFromMatrixPosition(i.matrixWorld)),os.ray.intersectPlane(as,hs)&&("translate"===o.mode?(ps.copy(i.parent.matrixWorld).invert(),ls.copy(hs).sub(ds.setFromMatrixPosition(i.matrixWorld))):"rotate"===o.mode&&(fs.set(0,1,0).applyQuaternion(e.quaternion).normalize(),ms.set(1,0,0).applyQuaternion(e.quaternion).normalize())),n.style.cursor="move",o.dispatchEvent({type:"dragstart",object:i})),us.copy(ss))}function h(){!1!==o.enabled&&(i&&(o.dispatchEvent({type:"dragend",object:i}),i=null),n.style.cursor=r?"pointer":"auto")}function d(t){const e=n.getBoundingClientRect();ss.x=(t.clientX-e.left)/e.width*2-1,ss.y=-(t.clientY-e.top)/e.height*2+1}function p(t,e=null){return t.isGroup&&(e=t),null===t.parent?e:p(t.parent,e)}s(),this.enabled=!0,this.recursive=!0,this.transformGroup=!1,this.activate=s,this.deactivate=l,this.dispose=function(){l()},this.getObjects=function(){return t},this.getRaycaster=function(){return os},this.setObjects=function(e){t=e}}}function vs(t,e,n){var i,r=1;function a(){var a,o,s=i.length,l=0,c=0,u=0;for(a=0;a=(r=(h+d)/2))?h=r:d=r,i=c,!(c=c[s=+o]))return i[s]=u,t;if(e===(a=+t._x.call(null,c.data)))return u.next=c,i?i[s]=u:t._root=u,t;do{i=i?i[s]=new Array(2):t._root=new Array(2),(o=e>=(r=(h+d)/2))?h=r:d=r}while((s=+o)==(l=+(a>=r)));return i[l]=c,i[s]=u,t}function ys(t,e,n){this.node=t,this.x0=e,this.x1=n}function xs(t){return t[0]}function bs(t,e){var n=new Ms(null==e?xs:e,NaN,NaN);return null==t?n:n.addAll(t)}function Ms(t,e,n){this._x=t,this._x0=e,this._x1=n,this._root=void 0}function Ss(t){for(var e={data:t.data},n=e;t=t.next;)n=n.next={data:t.data};return e}var Es=bs.prototype=Ms.prototype;function ws(t,e,n,i){if(isNaN(e)||isNaN(n))return t;var r,a,o,s,l,c,u,h,d,p=t._root,f={data:i},m=t._x0,g=t._y0,v=t._x1,_=t._y1;if(!p)return t._root=f,t;for(;p.length;)if((c=e>=(a=(m+v)/2))?m=a:v=a,(u=n>=(o=(g+_)/2))?g=o:_=o,r=p,!(p=p[h=u<<1|c]))return r[h]=f,t;if(s=+t._x.call(null,p.data),l=+t._y.call(null,p.data),e===s&&n===l)return f.next=p,r?r[h]=f:t._root=f,t;do{r=r?r[h]=new Array(4):t._root=new Array(4),(c=e>=(a=(m+v)/2))?m=a:v=a,(u=n>=(o=(g+_)/2))?g=o:_=o}while((h=u<<1|c)==(d=(l>=o)<<1|s>=a));return r[d]=p,r[h]=f,t}function Ts(t,e,n,i,r){this.node=t,this.x0=e,this.y0=n,this.x1=i,this.y1=r}function As(t){return t[0]}function Rs(t){return t[1]}function Cs(t,e,n){var i=new Ps(null==e?As:e,null==n?Rs:n,NaN,NaN,NaN,NaN);return null==t?i:i.addAll(t)}function Ps(t,e,n,i,r,a){this._x=t,this._y=e,this._x0=n,this._y0=i,this._x1=r,this._y1=a,this._root=void 0}function Ls(t){for(var e={data:t.data},n=e;t=t.next;)n=n.next={data:t.data};return e}Es.copy=function(){var t,e,n=new Ms(this._x,this._x0,this._x1),i=this._root;if(!i)return n;if(!i.length)return n._root=Ss(i),n;for(t=[{source:i,target:n._root=new Array(2)}];i=t.pop();)for(var r=0;r<2;++r)(e=i.source[r])&&(e.length?t.push({source:e,target:i.target[r]=new Array(2)}):i.target[r]=Ss(e));return n},Es.add=function(t){const e=+this._x.call(null,t);return _s(this.cover(e),e,t)},Es.addAll=function(t){Array.isArray(t)||(t=Array.from(t));const e=t.length,n=new Float64Array(e);let i=1/0,r=-1/0;for(let a,o=0;or&&(r=a));if(i>r)return this;this.cover(i).cover(r);for(let i=0;it||t>=n;)switch(r=+(tl||(r=a.x1)=h))&&(a=c[c.length-1],c[c.length-1]=c[c.length-1-o],c[c.length-1-o]=a)}else{var d=Math.abs(t-+this._x.call(null,u.data));d=(o=(h+d)/2))?h=o:d=o,e=u,!(u=u[l=+s]))return this;if(!u.length)break;e[l+1&1]&&(n=e,c=l)}for(;u.data!==t;)if(i=u,!(u=u.next))return this;return(r=u.next)&&delete u.next,i?(r?i.next=r:delete i.next,this):e?(r?e[l]=r:delete e[l],(u=e[0]||e[1])&&u===(e[1]||e[0])&&!u.length&&(n?n[c]=u:this._root=u),this):(this._root=r,this)},Es.removeAll=function(t){for(var e=0,n=t.length;e=(o=(y+M)/2))?y=o:M=o,(p=n>=(s=(x+S)/2))?x=s:S=s,(f=i>=(l=(b+E)/2))?b=l:E=l,a=v,!(v=v[m=f<<2|p<<1|d]))return a[m]=_,t;if(c=+t._x.call(null,v.data),u=+t._y.call(null,v.data),h=+t._z.call(null,v.data),e===c&&n===u&&i===h)return _.next=v,a?a[m]=_:t._root=_,t;do{a=a?a[m]=new Array(8):t._root=new Array(8),(d=e>=(o=(y+M)/2))?y=o:M=o,(p=n>=(s=(x+S)/2))?x=s:S=s,(f=i>=(l=(b+E)/2))?b=l:E=l}while((m=f<<2|p<<1|d)==(g=(h>=l)<<2|(u>=s)<<1|c>=o));return a[g]=v,a[m]=_,t}function Ns(t,e,n,i,r,a,o){this.node=t,this.x0=e,this.y0=n,this.z0=i,this.x1=r,this.y1=a,this.z1=o}function Is(t){return t[0]}function Us(t){return t[1]}function Fs(t){return t[2]}function ks(t,e,n,i){var r=new zs(null==e?Is:e,null==n?Us:n,null==i?Fs:i,NaN,NaN,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function zs(t,e,n,i,r,a,o,s,l){this._x=t,this._y=e,this._z=n,this._x0=i,this._y0=r,this._z0=a,this._x1=o,this._y1=s,this._z1=l,this._root=void 0}function Bs(t){for(var e={data:t.data},n=e;t=t.next;)n=n.next={data:t.data};return e}Os.copy=function(){var t,e,n=new Ps(this._x,this._y,this._x0,this._y0,this._x1,this._y1),i=this._root;if(!i)return n;if(!i.length)return n._root=Ls(i),n;for(t=[{source:i,target:n._root=new Array(4)}];i=t.pop();)for(var r=0;r<4;++r)(e=i.source[r])&&(e.length?t.push({source:e,target:i.target[r]=new Array(4)}):i.target[r]=Ls(e));return n},Os.add=function(t){const e=+this._x.call(null,t),n=+this._y.call(null,t);return ws(this.cover(e,n),e,n,t)},Os.addAll=function(t){var e,n,i,r,a=t.length,o=new Array(a),s=new Array(a),l=1/0,c=1/0,u=-1/0,h=-1/0;for(n=0;nu&&(u=i),rh&&(h=r));if(l>u||c>h)return this;for(this.cover(l,c).cover(u,h),n=0;nt||t>=r||i>e||e>=a;)switch(s=(ed||(a=l.y0)>p||(o=l.x1)=v)<<1|t>=g)&&(l=f[f.length-1],f[f.length-1]=f[f.length-1-c],f[f.length-1-c]=l)}else{var _=t-+this._x.call(null,m.data),y=e-+this._y.call(null,m.data),x=_*_+y*y;if(x=(s=(f+g)/2))?f=s:g=s,(u=o>=(l=(m+v)/2))?m=l:v=l,e=p,!(p=p[h=u<<1|c]))return this;if(!p.length)break;(e[h+1&3]||e[h+2&3]||e[h+3&3])&&(n=e,d=h)}for(;p.data!==t;)if(i=p,!(p=p.next))return this;return(r=p.next)&&delete p.next,i?(r?i.next=r:delete i.next,this):e?(r?e[h]=r:delete e[h],(p=e[0]||e[1]||e[2]||e[3])&&p===(e[3]||e[2]||e[1]||e[0])&&!p.length&&(n?n[d]=p:this._root=p),this):(this._root=r,this)},Os.removeAll=function(t){for(var e=0,n=t.length;e1&&(v=d.y+d.vy-u.y-u.vy||Vs(s)),r>2&&(_=d.z+d.vz-u.z-u.vz||Vs(s)),g*=p=((p=Math.sqrt(g*g+v*v+_*_))-n[m])/p*i*e[m],v*=p,_*=p,d.vx-=g*(f=o[m]),r>1&&(d.vy-=v*f),r>2&&(d.vz-=_*f),u.vx+=g*(f=1-f),r>1&&(u.vy+=v*f),r>2&&(u.vz+=_*f)}function p(){if(i){var r,s,c=i.length,u=t.length,h=new Map(i.map(((t,e)=>[l(t,e,i),t])));for(r=0,a=new Array(c);r"function"==typeof t))||Math.random,r=e.find((t=>[1,2,3].includes(t)))||2,p()},d.links=function(e){return arguments.length?(t=e,p(),d):t},d.id=function(t){return arguments.length?(l=t,d):l},d.iterations=function(t){return arguments.length?(h=+t,d):h},d.strength=function(t){return arguments.length?(c="function"==typeof t?t:Gs(+t),f(),d):c},d.distance=function(t){return arguments.length?(u="function"==typeof t?t:Gs(+t),m(),d):u},d}Hs.copy=function(){var t,e,n=new zs(this._x,this._y,this._z,this._x0,this._y0,this._z0,this._x1,this._y1,this._z1),i=this._root;if(!i)return n;if(!i.length)return n._root=Bs(i),n;for(t=[{source:i,target:n._root=new Array(8)}];i=t.pop();)for(var r=0;r<8;++r)(e=i.source[r])&&(e.length?t.push({source:e,target:i.target[r]=new Array(8)}):i.target[r]=Bs(e));return n},Hs.add=function(t){const e=+this._x.call(null,t),n=+this._y.call(null,t),i=+this._z.call(null,t);return Ds(this.cover(e,n,i),e,n,i,t)},Hs.addAll=function(t){Array.isArray(t)||(t=Array.from(t));const e=t.length,n=new Float64Array(e),i=new Float64Array(e),r=new Float64Array(e);let a=1/0,o=1/0,s=1/0,l=-1/0,c=-1/0,u=-1/0;for(let h,d,p,f,m=0;ml&&(l=d),pc&&(c=p),fu&&(u=f));if(a>l||o>c||s>u)return this;this.cover(a,o,s).cover(l,c,u);for(let a=0;at||t>=o||r>e||e>=s||a>n||n>=l;)switch(u=(ng||(o=h.y0)>v||(s=h.z0)>_||(l=h.x1)=S)<<2|(e>=M)<<1|t>=b)&&(h=y[y.length-1],y[y.length-1]=y[y.length-1-d],y[y.length-1-d]=h)}else{var E=t-+this._x.call(null,x.data),w=e-+this._y.call(null,x.data),T=n-+this._z.call(null,x.data),A=E*E+w*w+T*T;if(A=(l=(v+x)/2))?v=l:x=l,(d=o>=(c=(_+b)/2))?_=c:b=c,(p=s>=(u=(y+M)/2))?y=u:M=u,e=g,!(g=g[f=p<<2|d<<1|h]))return this;if(!g.length)break;(e[f+1&7]||e[f+2&7]||e[f+3&7]||e[f+4&7]||e[f+5&7]||e[f+6&7]||e[f+7&7])&&(n=e,m=f)}for(;g.data!==t;)if(i=g,!(g=g.next))return this;return(r=g.next)&&delete g.next,i?(r?i.next=r:delete i.next,this):e?(r?e[f]=r:delete e[f],(g=e[0]||e[1]||e[2]||e[3]||e[4]||e[5]||e[6]||e[7])&&g===(e[7]||e[6]||e[5]||e[4]||e[3]||e[2]||e[1]||e[0])&&!g.length&&(n?n[m]=g:this._root=g),this):(this._root=r,this)},Hs.removeAll=function(t){for(var e=0,n=t.length;e{}};function Ys(){for(var t,e=0,n=arguments.length,i={};e=0&&(e=t.slice(n+1),t=t.slice(0,n)),t&&!i.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}}))),o=-1,s=a.length;if(!(arguments.length<2)){if(null!=e&&"function"!=typeof e)throw new Error("invalid callback: "+e);for(;++o0)for(var n,i,r=new Array(n),a=0;a=0&&e._call.call(void 0,t),e=e._next;--tl}()}finally{tl=0,function(){var t,e,n=Js,i=1/0;for(;n;)n._call?(i>n._time&&(i=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:Js=e);Qs=t,ml(i)}(),al=0}}function fl(){var t=sl.now(),e=t-rl;e>il&&(ol-=e,rl=t)}function ml(t){tl||(el&&(el=clearTimeout(el)),t-al>24?(t<1/0&&(el=setTimeout(pl,t-sl.now()-ol)),nl&&(nl=clearInterval(nl))):(nl||(rl=sl.now(),nl=setInterval(fl,il)),tl=1,ll(pl)))}hl.prototype=dl.prototype={constructor:hl,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?cl():+n)+(null==e?0:+e),this._next||Qs===this||(Qs?Qs._next=this:Js=this,Qs=this),this._call=t,this._time=n,ml()},stop:function(){this._call&&(this._call=null,this._time=1/0,ml())}};const gl=1664525,vl=1013904223,_l=4294967296;function yl(t){return t.x}function xl(t){return t.y}function bl(t){return t.z}var Ml=Math.PI*(3-Math.sqrt(5)),Sl=20*Math.PI/(9+Math.sqrt(221));function El(t,e){e=e||2;var n,i=Math.min(3,Math.max(1,Math.round(e))),r=1,a=.001,o=1-Math.pow(a,1/300),s=0,l=.6,c=new Map,u=dl(p),h=Ys("tick","end"),d=function(){let t=1;return()=>(t=(gl*t+vl)%_l)/_l}();function p(){f(),h.call("tick",n),r1&&(null==u.fy?u.y+=u.vy*=l:(u.y=u.fy,u.vy=0)),i>2&&(null==u.fz?u.z+=u.vz*=l:(u.z=u.fz,u.vz=0));return n}function m(){for(var e,n=0,r=t.length;n1&&isNaN(e.y)||i>2&&isNaN(e.z)){var a=10*(i>2?Math.cbrt(.5+n):i>1?Math.sqrt(.5+n):n),o=n*Ml,s=n*Sl;1===i?e.x=a:2===i?(e.x=a*Math.cos(o),e.y=a*Math.sin(o)):(e.x=a*Math.sin(o)*Math.cos(s),e.y=a*Math.cos(o),e.z=a*Math.sin(o)*Math.sin(s))}(isNaN(e.vx)||i>1&&isNaN(e.vy)||i>2&&isNaN(e.vz))&&(e.vx=0,i>1&&(e.vy=0),i>2&&(e.vz=0))}}function g(e){return e.initialize&&e.initialize(t,d,i),e}return null==t&&(t=[]),m(),n={tick:f,restart:function(){return u.restart(p),n},stop:function(){return u.stop(),n},numDimensions:function(t){return arguments.length?(i=Math.min(3,Math.max(1,Math.round(t))),c.forEach(g),n):i},nodes:function(e){return arguments.length?(t=e,m(),c.forEach(g),n):t},alpha:function(t){return arguments.length?(r=+t,n):r},alphaMin:function(t){return arguments.length?(a=+t,n):a},alphaDecay:function(t){return arguments.length?(o=+t,n):+o},alphaTarget:function(t){return arguments.length?(s=+t,n):s},velocityDecay:function(t){return arguments.length?(l=1-t,n):1-l},randomSource:function(t){return arguments.length?(d=t,c.forEach(g),n):d},force:function(t,e){return arguments.length>1?(null==e?c.delete(t):c.set(t,g(e)),n):c.get(t)},find:function(){var e,n,r,a,o,s,l=Array.prototype.slice.call(arguments),c=l.shift()||0,u=(i>1?l.shift():null)||0,h=(i>2?l.shift():null)||0,d=l.shift()||1/0,p=0,f=t.length;for(d*=d,p=0;p1?(h.on(t,e),n):h.on(t)}}}function wl(){var t,e,n,i,r,a,o=Gs(-30),s=1,l=1/0,c=.81;function u(i){var a,o=t.length,s=(1===e?bs(t,yl):2===e?Cs(t,yl,xl):3===e?ks(t,yl,xl,bl):null).visitAfter(d);for(r=i,a=0;a1&&(t.y=o/u),e>2&&(t.z=s/u)}else{(n=t).x=n.data.x,e>1&&(n.y=n.data.y),e>2&&(n.z=n.data.z);do{c+=a[n.data.index]}while(n=n.next)}t.value=c}function p(t,o,u,h,d){if(!t.value)return!0;var p=[u,h,d][e-1],f=t.x-n.x,m=e>1?t.y-n.y:0,g=e>2?t.z-n.z:0,v=p-o,_=f*f+m*m+g*g;if(v*v/c<_)return _1&&0===m&&(_+=(m=Vs(i))*m),e>2&&0===g&&(_+=(g=Vs(i))*g),_1&&(n.vy+=m*t.value*r/_),e>2&&(n.vz+=g*t.value*r/_)),!0;if(!(t.length||_>=l)){(t.data!==n||t.next)&&(0===f&&(_+=(f=Vs(i))*f),e>1&&0===m&&(_+=(m=Vs(i))*m),e>2&&0===g&&(_+=(g=Vs(i))*g),_1&&(n.vy+=m*v),e>2&&(n.vz+=g*v))}while(t=t.next)}}return u.initialize=function(n,...r){t=n,i=r.find((t=>"function"==typeof t))||Math.random,e=r.find((t=>[1,2,3].includes(t)))||2,h()},u.strength=function(t){return arguments.length?(o="function"==typeof t?t:Gs(+t),h(),u):o},u.distanceMin=function(t){return arguments.length?(s=t*t,u):Math.sqrt(s)},u.distanceMax=function(t){return arguments.length?(l=t*t,u):Math.sqrt(l)},u.theta=function(t){return arguments.length?(c=t*t,u):Math.sqrt(c)},u}function Tl(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var Al=function(t){!function(t){if(!t)throw new Error("Eventify cannot use falsy object as events subject");for(var e=["on","fire","off"],n=0;n1&&(i=Array.prototype.splice.call(arguments,1));for(var a=0;a0&&(h.fire("changed",o),o.length=0)}function E(t){if("function"!=typeof t)throw new Error("Function is expected to iterate over graph nodes. You passed "+t);for(var n=e.values(),i=n.next();!i.done;){if(t(i.value))return!0;i=n.next()}}},Cl=Al;function Pl(t,e){this.id=t,this.links=null,this.data=e}function Ll(t,e){t.links?t.links.add(e):t.links=new Set([e])}function Ol(t,e,n,i){this.fromId=t,this.toId=e,this.data=n,this.id=i}function Dl(t,e){return t.toString()+"👉 "+e.toString()}var Nl=Tl(Rl),Il={exports:{}},Ul={exports:{}},Fl=function(t){return 0===t?"x":1===t?"y":2===t?"z":"c"+(t+1)};const kl=Fl;var zl=function(t){return function(e,n){let i=n&&n.indent||0,r=n&&void 0!==n.join?n.join:"\n",a=Array(i+1).join(" "),o=[];for(let n=0;n {var}max) {var}max = pos.{var};",{indent:6})}\n }\n\n // Makes the bounds square.\n var maxSideLength = -Infinity;\n ${e("if ({var}max - {var}min > maxSideLength) maxSideLength = {var}max - {var}min ;",{indent:4})}\n\n currentInCache = 0;\n root = newNode();\n ${e("root.min_{var} = {var}min;",{indent:4})}\n ${e("root.max_{var} = {var}min + maxSideLength;",{indent:4})}\n\n i = bodies.length - 1;\n if (i >= 0) {\n root.body = bodies[i];\n }\n while (i--) {\n insert(bodies[i], root);\n }\n }\n\n function insert(newBody) {\n insertStack.reset();\n insertStack.push(root, newBody);\n\n while (!insertStack.isEmpty()) {\n var stackItem = insertStack.pop();\n var node = stackItem.node;\n var body = stackItem.body;\n\n if (!node.body) {\n // This is internal node. Update the total mass of the node and center-of-mass.\n ${e("var {var} = body.pos.{var};",{indent:8})}\n node.mass += body.mass;\n ${e("node.mass_{var} += body.mass * {var};",{indent:8})}\n\n // Recursively insert the body in the appropriate quadrant.\n // But first find the appropriate quadrant.\n var quadIdx = 0; // Assume we are in the 0's quad.\n ${e("var min_{var} = node.min_{var};",{indent:8})}\n ${e("var max_{var} = (min_{var} + node.max_{var}) / 2;",{indent:8})}\n\n${function(e){let n=[],i=Array(e+1).join(" ");for(let e=0;e max_${ql(e)}) {`),n.push(i+` quadIdx = quadIdx + ${Math.pow(2,e)};`),n.push(i+` min_${ql(e)} = max_${ql(e)};`),n.push(i+` max_${ql(e)} = node.max_${ql(e)};`),n.push(i+"}");return n.join("\n")}(8)}\n\n var child = getChild(node, quadIdx);\n\n if (!child) {\n // The node is internal but this quadrant is not taken. Add\n // subnode to it.\n child = newNode();\n ${e("child.min_{var} = min_{var};",{indent:10})}\n ${e("child.max_{var} = max_{var};",{indent:10})}\n child.body = body;\n\n setChild(node, quadIdx, child);\n } else {\n // continue searching in this quadrant.\n insertStack.push(child, body);\n }\n } else {\n // We are trying to add to the leaf node.\n // We have to convert current leaf into internal node\n // and continue adding two nodes.\n var oldBody = node.body;\n node.body = null; // internal nodes do not cary bodies\n\n if (isSamePosition(oldBody.pos, body.pos)) {\n // Prevent infinite subdivision by bumping one node\n // anywhere in this quadrant\n var retriesCount = 3;\n do {\n var offset = random.nextDouble();\n ${e("var d{var} = (node.max_{var} - node.min_{var}) * offset;",{indent:12})}\n\n ${e("oldBody.pos.{var} = node.min_{var} + d{var};",{indent:12})}\n retriesCount -= 1;\n // Make sure we don't bump it out of the box. If we do, next iteration should fix it\n } while (retriesCount > 0 && isSamePosition(oldBody.pos, body.pos));\n\n if (retriesCount === 0 && isSamePosition(oldBody.pos, body.pos)) {\n // This is very bad, we ran out of precision.\n // if we do not return from the method we'll get into\n // infinite loop here. So we sacrifice correctness of layout, and keep the app running\n // Next layout iteration should get larger bounding box in the first step and fix this\n return;\n }\n }\n // Next iteration should subdivide node further.\n insertStack.push(node, oldBody);\n insertStack.push(node, body);\n }\n }\n }\n}\nreturn createQuadTree;\n\n`}function $l(t){let e=Xl(t);return`\n function isSamePosition(point1, point2) {\n ${e("var d{var} = Math.abs(point1.{var} - point2.{var});",{indent:2})}\n \n return ${e("d{var} < 1e-8",{join:" && "})};\n } \n`}function Kl(t){var e=Math.pow(2,t);return`\nfunction setChild(node, idx, child) {\n ${function(){let t=[];for(let n=0;n 0) {\n return this.stack[--this.popIdx];\n }\n },\n reset: function () {\n this.popIdx = 0;\n }\n};\n\nfunction InsertStackElement(node, body) {\n this.node = node; // QuadTree node\n this.body = body; // physical body which needs to be inserted to node\n}\n"}Wl.exports=function(t){let e=Yl(t);return new Function(e)()},Wl.exports.generateQuadTreeFunctionBody=Yl,Wl.exports.getInsertStackCode=Ql,Wl.exports.getQuadNodeCode=Jl,Wl.exports.isSamePosition=$l,Wl.exports.getChildBodyCode=Zl,Wl.exports.setChildBodyCode=Kl;var tc=Wl.exports,ec={exports:{}};ec.exports=function(t){let e=ic(t);return new Function("bodies","settings","random",e)},ec.exports.generateFunctionBody=ic;const nc=zl;function ic(t){let e=nc(t);return`\n var boundingBox = {\n ${e("min_{var}: 0, max_{var}: 0,",{indent:4})}\n };\n\n return {\n box: boundingBox,\n\n update: updateBoundingBox,\n\n reset: resetBoundingBox,\n\n getBestNewPosition: function (neighbors) {\n var ${e("base_{var} = 0",{join:", "})};\n\n if (neighbors.length) {\n for (var i = 0; i < neighbors.length; ++i) {\n let neighborPos = neighbors[i].pos;\n ${e("base_{var} += neighborPos.{var};",{indent:10})}\n }\n\n ${e("base_{var} /= neighbors.length;",{indent:8})}\n } else {\n ${e("base_{var} = (boundingBox.min_{var} + boundingBox.max_{var}) / 2;",{indent:8})}\n }\n\n var springLength = settings.springLength;\n return {\n ${e("{var}: base_{var} + (random.nextDouble() - 0.5) * springLength,",{indent:8})}\n };\n }\n };\n\n function updateBoundingBox() {\n var i = bodies.length;\n if (i === 0) return; // No bodies - no borders.\n\n ${e("var max_{var} = -Infinity;",{indent:4})}\n ${e("var min_{var} = Infinity;",{indent:4})}\n\n while(i--) {\n // this is O(n), it could be done faster with quadtree, if we check the root node bounds\n var bodyPos = bodies[i].pos;\n ${e("if (bodyPos.{var} < min_{var}) min_{var} = bodyPos.{var};",{indent:6})}\n ${e("if (bodyPos.{var} > max_{var}) max_{var} = bodyPos.{var};",{indent:6})}\n }\n\n ${e("boundingBox.min_{var} = min_{var};",{indent:4})}\n ${e("boundingBox.max_{var} = max_{var};",{indent:4})}\n }\n\n function resetBoundingBox() {\n ${e("boundingBox.min_{var} = boundingBox.max_{var} = 0;",{indent:4})}\n }\n`}var rc=ec.exports,ac={exports:{}};const oc=zl;function sc(t){return`\n if (!Number.isFinite(options.dragCoefficient)) throw new Error('dragCoefficient is not a finite number');\n\n return {\n update: function(body) {\n ${oc(t)("body.force.{var} -= options.dragCoefficient * body.velocity.{var};",{indent:6})}\n }\n };\n`}ac.exports=function(t){let e=sc(t);return new Function("options",e)},ac.exports.generateCreateDragForceFunctionBody=sc;var lc=ac.exports,cc={exports:{}};const uc=zl;function hc(t){let e=uc(t);return`\n if (!Number.isFinite(options.springCoefficient)) throw new Error('Spring coefficient is not a number');\n if (!Number.isFinite(options.springLength)) throw new Error('Spring length is not a number');\n\n return {\n /**\n * Updates forces acting on a spring\n */\n update: function (spring) {\n var body1 = spring.from;\n var body2 = spring.to;\n var length = spring.length < 0 ? options.springLength : spring.length;\n ${e("var d{var} = body2.pos.{var} - body1.pos.{var};",{indent:6})}\n var r = Math.sqrt(${e("d{var} * d{var}",{join:" + "})});\n\n if (r === 0) {\n ${e("d{var} = (random.nextDouble() - 0.5) / 50;",{indent:8})}\n r = Math.sqrt(${e("d{var} * d{var}",{join:" + "})});\n }\n\n var d = r - length;\n var coefficient = ((spring.coefficient > 0) ? spring.coefficient : options.springCoefficient) * d / r;\n\n ${e("body1.force.{var} += coefficient * d{var}",{indent:6})};\n body1.springCount += 1;\n body1.springLength += r;\n\n ${e("body2.force.{var} -= coefficient * d{var}",{indent:6})};\n body2.springCount += 1;\n body2.springLength += r;\n }\n };\n`}cc.exports=function(t){let e=hc(t);return new Function("options","random",e)},cc.exports.generateCreateSpringForceFunctionBody=hc;var dc=cc.exports,pc={exports:{}};const fc=zl;function mc(t){let e=fc(t);return`\n var length = bodies.length;\n if (length === 0) return 0;\n\n ${e("var d{var} = 0, t{var} = 0;",{indent:2})}\n\n for (var i = 0; i < length; ++i) {\n var body = bodies[i];\n if (body.isPinned) continue;\n\n if (adaptiveTimeStepWeight && body.springCount) {\n timeStep = (adaptiveTimeStepWeight * body.springLength/body.springCount);\n }\n\n var coeff = timeStep / body.mass;\n\n ${e("body.velocity.{var} += coeff * body.force.{var};",{indent:4})}\n ${e("var v{var} = body.velocity.{var};",{indent:4})}\n var v = Math.sqrt(${e("v{var} * v{var}",{join:" + "})});\n\n if (v > 1) {\n // We normalize it so that we move within timeStep range. \n // for the case when v <= 1 - we let velocity to fade out.\n ${e("body.velocity.{var} = v{var} / v;",{indent:6})}\n }\n\n ${e("d{var} = timeStep * body.velocity.{var};",{indent:4})}\n\n ${e("body.pos.{var} += d{var};",{indent:4})}\n\n ${e("t{var} += Math.abs(d{var});",{indent:4})}\n }\n\n return (${e("t{var} * t{var}",{join:" + "})})/length;\n`}pc.exports=function(t){let e=mc(t);return new Function("bodies","timeStep","adaptiveTimeStepWeight",e)},pc.exports.generateIntegratorFunctionBody=mc;var gc,vc,_c,yc,xc=pc.exports;var bc,Mc={exports:{}};var Sc=function(t){var e=vc?gc:(vc=1,gc=function(t,e,n,i){this.from=t,this.to=e,this.length=n,this.coefficient=i}),n=(yc||(yc=1,_c=function t(e,n){var i;if(e||(e={}),n)for(i in n)if(n.hasOwnProperty(i)){var r=e.hasOwnProperty(i),a=typeof n[i];r&&typeof e[i]===a?"object"===a&&(e[i]=t(e[i],n[i])):e[i]=n[i]}return e}),_c),i=Al;if(t){if(void 0!==t.springCoeff)throw new Error("springCoeff was renamed to springCoefficient");if(void 0!==t.dragCoeff)throw new Error("dragCoeff was renamed to dragCoefficient")}t=n(t,{springLength:10,springCoefficient:.8,gravity:-12,theta:.8,dragCoefficient:.9,timeStep:.5,adaptiveTimeStepWeight:0,dimensions:2,debug:!1});var r=Pc[t.dimensions];if(!r){var a=t.dimensions;r={Body:Ec(a,t.debug),createQuadTree:wc(a),createBounds:Tc(a),createDragForce:Ac(a),createSpringForce:Rc(a),integrate:Cc(a)},Pc[a]=r}var o=r.Body,s=r.createQuadTree,l=r.createBounds,c=r.createDragForce,u=r.createSpringForce,h=r.integrate,d=function(){if(bc)return Mc.exports;function t(t){return new e("number"==typeof t?t:+new Date)}function e(t){this.seed=t}function n(t){return Math.sqrt(2*Math.PI/t)*Math.pow(1/Math.E*(t+1/(12*t-1/(10*t))),t)}function i(){var t=this.seed;return t=4294967295&(3042594569^(t=4251993797+(t=4294967295&(3550635116+(t=374761393+(t=4294967295&(3345072700^(t=t+2127912214+(t<<12)&4294967295)^t>>>19))+(t<<5)&4294967295)^t<<9))+(t<<3)&4294967295)^t>>>16),this.seed=t,(268435455&t)/268435456}return bc=1,Mc.exports=t,Mc.exports.random=t,Mc.exports.randomIterator=function(e,n){var i=n||t();if("function"!=typeof i.next)throw new Error("customRandom does not match expected API: next() function is missing");return{forEach:function(t){var n,r,a;for(n=e.length-1;n>0;--n)r=i.next(n+1),a=e[r],e[r]=e[n],e[n]=a,t(a);e.length&&t(e[0])},shuffle:function(){var t,n,r;for(t=e.length-1;t>0;--t)n=i.next(t+1),r=e[n],e[n]=e[t],e[t]=r;return e}}},e.prototype.next=function(t){return Math.floor(this.nextDouble()*t)},e.prototype.nextDouble=i,e.prototype.uniform=i,e.prototype.gaussian=function(){var t,e,n;do{t=(e=2*this.nextDouble()-1)*e+(n=2*this.nextDouble()-1)*n}while(t>=1||0===t);return e*Math.sqrt(-2*Math.log(t)/t)},e.prototype.levy=function(){var t=1.5,e=Math.pow(n(2.5)*Math.sin(Math.PI*t/2)/(n(1.25)*t*Math.pow(2,.25)),1/t);return this.gaussian()*e/Math.pow(Math.abs(this.gaussian()),1/t)},Mc.exports}().random(42),p=[],f=[],m=s(t,d),g=l(p,t,d),v=u(t,d),_=c(t),y=[],x=new Map,b=0;E("nbody",(function(){if(0===p.length)return;m.insertBodies(p);var t=p.length;for(;t--;){var e=p[t];e.isPinned||(e.reset(),m.updateBodyForce(e),_.update(e))}})),E("spring",(function(){var t=f.length;for(;t--;)v.update(f[t])}));var M={bodies:p,quadTree:m,springs:f,settings:t,addForce:E,removeForce:function(t){var e=y.indexOf(x.get(t));if(e<0)return;y.splice(e,1),x.delete(t)},getForces:function(){return x},step:function(){for(var e=0;enew o(t))(t);return p.push(e),e},removeBody:function(t){if(t){var e=p.indexOf(t);if(!(e<0))return p.splice(e,1),0===p.length&&g.reset(),!0}},addSpring:function(t,n,i,r){if(!t||!n)throw new Error("Cannot add null spring to force simulator");"number"!=typeof i&&(i=-1);var a=new e(t,n,i,r>=0?r:-1);return f.push(a),a},getTotalMovement:function(){return 0},removeSpring:function(t){if(t){var e=f.indexOf(t);return e>-1?(f.splice(e,1),!0):void 0}},getBestNewBodyPosition:function(t){return g.getBestNewPosition(t)},getBBox:S,getBoundingBox:S,invalidateBBox:function(){console.warn("invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call")},gravity:function(e){return void 0!==e?(t.gravity=e,m.options({gravity:e}),this):t.gravity},theta:function(e){return void 0!==e?(t.theta=e,m.options({theta:e}),this):t.theta},random:d};return function(t,e){for(var n in t)Lc(t,e,n)}(t,M),i(M),M;function S(){return g.update(),g.box}function E(t,e){if(x.has(t))throw new Error("Force "+t+" is already added");x.set(t,e),y.push(e)}},Ec=jl,wc=tc,Tc=rc,Ac=lc,Rc=dc,Cc=xc,Pc={};function Lc(t,e,n){if(t.hasOwnProperty(n)&&"function"!=typeof e[n]){var i=Number.isFinite(t[n]);e[n]=i?function(i){if(void 0!==i){if(!Number.isFinite(i))throw new Error("Value of "+n+" should be a valid number.");return t[n]=i,e}return t[n]}:function(i){return void 0!==i?(t[n]=i,e):t[n]}}}Il.exports=function(t,e){if(!t)throw new Error("Graph structure cannot be undefined");var n=(e&&e.createSimulator||Sc)(e);if(Array.isArray(e))throw new Error("Physics settings is expected to be an object");var i=t.version>19?function(e){var n=t.getLinks(e);return n?1+n.size/3:1}:function(e){var n=t.getLinks(e);return n?1+n.length/3:1};e&&"function"==typeof e.nodeMass&&(i=e.nodeMass);var r=new Map,a={},o=0,s=n.settings.springTransform||Dc;o=0,t.forEachNode((function(t){p(t.id),o+=1})),t.forEachLink(m),t.on("changed",d);var l=!1,c={step:function(){if(0===o)return u(!0),!0;var t=n.step();c.lastMove=t,c.fire("step");var e=t/o<=.01;return u(e),e},getNodePosition:function(t){return _(t).pos},setNodePosition:function(t){var e=_(t);e.setPosition.apply(e,Array.prototype.slice.call(arguments,1))},getLinkPosition:function(t){var e=a[t];if(e)return{from:e.from.pos,to:e.to.pos}},getGraphRect:function(){return n.getBBox()},forEachBody:h,pinNode:function(t,e){_(t.id).isPinned=!!e},isNodePinned:function(t){return _(t.id).isPinned},dispose:function(){t.off("changed",d),c.fire("disposed")},getBody:function(t){return r.get(t)},getSpring:function(e,n){var i;if(void 0===n)i="object"!=typeof e?e:e.id;else{var r=t.hasLink(e,n);if(!r)return;i=r.id}return a[i]},getForceVectorLength:function(){var t=0,e=0;return h((function(n){t+=Math.abs(n.force.x),e+=Math.abs(n.force.y)})),Math.sqrt(t*t+e*e)},simulator:n,graph:t,lastMove:0};return Oc(c),c;function u(t){var e;l!==t&&(l=t,e=t,c.fire("stable",e))}function h(t){r.forEach(t)}function d(e){for(var n=0;n=e||n<0||h&&t-c>=a}function m(){var t=zc();if(f(t))return g(t);s=setTimeout(m,function(t){var n=e-(t-l);return h?lu(n,a-(t-c)):n}(t))}function g(t){return s=void 0,d&&i?p(t):(i=r=void 0,o)}function v(){var t=zc(),n=f(t);if(i=arguments,r=this,l=t,n){if(void 0===s)return function(t){return c=t,s=setTimeout(m,e),u?p(t):o}(l);if(h)return clearTimeout(s),s=setTimeout(m,e),p(l)}return void 0===s&&(s=setTimeout(m,e)),o}return e=au(e)||0,Ic(n)&&(u=!!n.leading,a=(h="maxWait"in n)?su(au(n.maxWait)||0,e):a,d="trailing"in n?!!n.trailing:d),v.cancel=function(){void 0!==s&&clearTimeout(s),c=0,i=l=r=s=void 0},v.flush=function(){return void 0===s?o:g(zc())},v}function uu(t,e){for(var n=0;nt.length)&&(e=t.length);for(var n=0,i=new Array(e);n0&&void 0!==arguments[0]?arguments[0]:{},e=Object.assign({},n instanceof Function?n(t):n,{initialised:!1}),i={};function r(e){return a(e,t),s(),r}var a=function(t,n){u.call(r,t,e,n),e.initialised=!0},s=cu((function(){e.initialised&&(d.call(r,e,i),i={})}),1);return p.forEach((function(t){r[t.name]=function(t){var n=t.name,a=t.triggerUpdate,o=void 0!==a&&a,l=t.onChange,c=void 0===l?function(t,e){}:l,u=t.defaultVal,h=void 0===u?null:u;return function(t){var a=e[n];if(!arguments.length)return a;var l=void 0===t?h:t;return e[n]=l,c.call(r,l,e,a),!i.hasOwnProperty(n)&&(i[n]=a),o&&s(),r}}(t)})),Object.keys(o).forEach((function(t){r[t]=function(){for(var n,i=arguments.length,a=new Array(i),s=0;s=e)&&(n=e);else{let i=-1;for(let r of t)null!=(r=e(r,++i,t))&&(n=r)&&(n=r)}return n}function bu(t,e){let n;if(void 0===e)for(const e of t)null!=e&&(n>e||void 0===n&&e>=e)&&(n=e);else{let i=-1;for(let r of t)null!=(r=e(r,++i,t))&&(n>r||void 0===n&&r>=r)&&(n=r)}return n}function Mu(t,e){if(null==t)return{};var n,i,r=function(t,e){if(null==t)return{};var n,i,r={},a=Object.keys(t);for(i=0;i=0||(r[n]=t[n]);return r}(t,e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(r[n]=t[n])}return r}function Su(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var i,r,a,o,s=[],l=!0,c=!1;try{if(a=(n=n.call(t)).next,0===e){if(Object(n)!==n)return;l=!1}else for(;!(l=(i=a.call(n)).done)&&(s.push(i.value),s.length!==e);l=!0);}catch(t){c=!0,r=t}finally{try{if(!l&&null!=n.return&&(o=n.return(),Object(o)!==o))return}finally{if(c)throw r}}return s}}(t,e)||wu(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Eu(t){return function(t){if(Array.isArray(t))return Tu(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||wu(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function wu(t,e){if(t){if("string"==typeof t)return Tu(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Tu(t,e):void 0}}function Tu(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=new Array(e);n0&&void 0!==arguments[0]?arguments[0]:[],e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],i=arguments.length>3&&void 0!==arguments[3]&&arguments[3],r=(e instanceof Array?e.length?e:[void 0]:[e]).map((function(t){return{keyAccessor:t,isProp:!(t instanceof Function)}})),a=t.reduce((function(t,e){var i=t,a=e;return r.forEach((function(t,e){var o,s=t.keyAccessor;if(t.isProp){var l=a,c=l[s],u=Mu(l,[s].map(Au));o=c,a=u}else o=s(a,e);e+11&&void 0!==arguments[1]?arguments[1]:1;i===r.length?Object.keys(e).forEach((function(t){return e[t]=n(e[t])})):Object.values(e).forEach((function(e){return t(e,i+1)}))}(a);var o=a;return i&&(o=[],function t(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[];n.length===r.length?o.push({keys:n,vals:e}):Object.entries(e).forEach((function(e){var i=Su(e,2),r=i[0],a=i[1];return t(a,[].concat(Eu(n),[r]))}))}(a),e instanceof Array&&0===e.length&&1===o.length&&(o[0].keys=[])),o};function Cu(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function Pu(t,e,n){return(e=function(t){var e=function(t,e){if("object"!=typeof t||null===t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var i=n.call(t,e||"default");if("object"!=typeof i)return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===e?String:Number)(t)}(t,"string");return"symbol"==typeof e?e:String(e)}(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function Lu(t,e){if(null==t)return{};var n,i,r=function(t,e){if(null==t)return{};var n,i,r={},a=Object.keys(t);for(i=0;i=0||(r[n]=t[n]);return r}(t,e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(r[n]=t[n])}return r}function Ou(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var i,r,a,o,s=[],l=!0,c=!1;try{if(a=(n=n.call(t)).next,0===e){if(Object(n)!==n)return;l=!1}else for(;!(l=(i=a.call(n)).done)&&(s.push(i.value),s.length!==e);l=!0);}catch(t){c=!0,r=t}finally{try{if(!l&&null!=n.return&&(o=n.return(),Object(o)!==o))return}finally{if(c)throw r}}return s}}(t,e)||Nu(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Du(t){return function(t){if(Array.isArray(t))return Iu(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||Nu(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Nu(t,e){if(t){if("string"==typeof t)return Iu(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Iu(t,e):void 0}}function Iu(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=new Array(e);n1&&(n-=1),n<1/6?t+6*(e-t)*n:n<.5?e:n<2/3?t+(e-t)*(2/3-n)*6:t}if(t=hh(t,360),e=hh(e,100),n=hh(n,100),0===e)i=r=a=n;else{var s=n<.5?n*(1+e):n+e-n*e,l=2*n-s;i=o(l,s,t+1/3),r=o(l,s,t),a=o(l,s,t-1/3)}return{r:255*i,g:255*r,b:255*a}}(t.h,i,a),o=!0,s="hsl"),t.hasOwnProperty("a")&&(n=t.a));var l,c,u;return n=uh(n),{ok:o,format:t.format||s,r:Math.min(255,Math.max(e.r,0)),g:Math.min(255,Math.max(e.g,0)),b:Math.min(255,Math.max(e.b,0)),a:n}}(t);this._originalInput=t,this._r=n.r,this._g=n.g,this._b=n.b,this._a=n.a,this._roundA=Math.round(100*this._a)/100,this._format=e.format||n.format,this._gradientType=e.gradientType,this._r<1&&(this._r=Math.round(this._r)),this._g<1&&(this._g=Math.round(this._g)),this._b<1&&(this._b=Math.round(this._b)),this._ok=n.ok}function Xu(t,e,n){t=hh(t,255),e=hh(e,255),n=hh(n,255);var i,r,a=Math.max(t,e,n),o=Math.min(t,e,n),s=(a+o)/2;if(a==o)i=r=0;else{var l=a-o;switch(r=s>.5?l/(2-a-o):l/(a+o),a){case t:i=(e-n)/l+(e>1)+720)%360;--e;)i.h=(i.h+r)%360,a.push(Wu(i));return a}function sh(t,e){e=e||6;for(var n=Wu(t).toHsv(),i=n.h,r=n.s,a=n.v,o=[],s=1/e;e--;)o.push(Wu({h:i,s:r,v:a})),a=(a+s)%1;return o}Wu.prototype={isDark:function(){return this.getBrightness()<128},isLight:function(){return!this.isDark()},isValid:function(){return this._ok},getOriginalInput:function(){return this._originalInput},getFormat:function(){return this._format},getAlpha:function(){return this._a},getBrightness:function(){var t=this.toRgb();return(299*t.r+587*t.g+114*t.b)/1e3},getLuminance:function(){var t,e,n,i=this.toRgb();return t=i.r/255,e=i.g/255,n=i.b/255,.2126*(t<=.03928?t/12.92:Math.pow((t+.055)/1.055,2.4))+.7152*(e<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4))+.0722*(n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4))},setAlpha:function(t){return this._a=uh(t),this._roundA=Math.round(100*this._a)/100,this},toHsv:function(){var t=qu(this._r,this._g,this._b);return{h:360*t.h,s:t.s,v:t.v,a:this._a}},toHsvString:function(){var t=qu(this._r,this._g,this._b),e=Math.round(360*t.h),n=Math.round(100*t.s),i=Math.round(100*t.v);return 1==this._a?"hsv("+e+", "+n+"%, "+i+"%)":"hsva("+e+", "+n+"%, "+i+"%, "+this._roundA+")"},toHsl:function(){var t=Xu(this._r,this._g,this._b);return{h:360*t.h,s:t.s,l:t.l,a:this._a}},toHslString:function(){var t=Xu(this._r,this._g,this._b),e=Math.round(360*t.h),n=Math.round(100*t.s),i=Math.round(100*t.l);return 1==this._a?"hsl("+e+", "+n+"%, "+i+"%)":"hsla("+e+", "+n+"%, "+i+"%, "+this._roundA+")"},toHex:function(t){return Yu(this._r,this._g,this._b,t)},toHexString:function(t){return"#"+this.toHex(t)},toHex8:function(t){return function(t,e,n,i,r){var a=[fh(Math.round(t).toString(16)),fh(Math.round(e).toString(16)),fh(Math.round(n).toString(16)),fh(gh(i))];if(r&&a[0].charAt(0)==a[0].charAt(1)&&a[1].charAt(0)==a[1].charAt(1)&&a[2].charAt(0)==a[2].charAt(1)&&a[3].charAt(0)==a[3].charAt(1))return a[0].charAt(0)+a[1].charAt(0)+a[2].charAt(0)+a[3].charAt(0);return a.join("")}(this._r,this._g,this._b,this._a,t)},toHex8String:function(t){return"#"+this.toHex8(t)},toRgb:function(){return{r:Math.round(this._r),g:Math.round(this._g),b:Math.round(this._b),a:this._a}},toRgbString:function(){return 1==this._a?"rgb("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+")":"rgba("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+", "+this._roundA+")"},toPercentageRgb:function(){return{r:Math.round(100*hh(this._r,255))+"%",g:Math.round(100*hh(this._g,255))+"%",b:Math.round(100*hh(this._b,255))+"%",a:this._a}},toPercentageRgbString:function(){return 1==this._a?"rgb("+Math.round(100*hh(this._r,255))+"%, "+Math.round(100*hh(this._g,255))+"%, "+Math.round(100*hh(this._b,255))+"%)":"rgba("+Math.round(100*hh(this._r,255))+"%, "+Math.round(100*hh(this._g,255))+"%, "+Math.round(100*hh(this._b,255))+"%, "+this._roundA+")"},toName:function(){return 0===this._a?"transparent":!(this._a<1)&&(ch[Yu(this._r,this._g,this._b,!0)]||!1)},toFilter:function(t){var e="#"+$u(this._r,this._g,this._b,this._a),n=e,i=this._gradientType?"GradientType = 1, ":"";if(t){var r=Wu(t);n="#"+$u(r._r,r._g,r._b,r._a)}return"progid:DXImageTransform.Microsoft.gradient("+i+"startColorstr="+e+",endColorstr="+n+")"},toString:function(t){var e=!!t;t=t||this._format;var n=!1,i=this._a<1&&this._a>=0;return e||!i||"hex"!==t&&"hex6"!==t&&"hex3"!==t&&"hex4"!==t&&"hex8"!==t&&"name"!==t?("rgb"===t&&(n=this.toRgbString()),"prgb"===t&&(n=this.toPercentageRgbString()),"hex"!==t&&"hex6"!==t||(n=this.toHexString()),"hex3"===t&&(n=this.toHexString(!0)),"hex4"===t&&(n=this.toHex8String(!0)),"hex8"===t&&(n=this.toHex8String()),"name"===t&&(n=this.toName()),"hsl"===t&&(n=this.toHslString()),"hsv"===t&&(n=this.toHsvString()),n||this.toHexString()):"name"===t&&0===this._a?this.toName():this.toRgbString()},clone:function(){return Wu(this.toString())},_applyModification:function(t,e){var n=t.apply(null,[this].concat([].slice.call(e)));return this._r=n._r,this._g=n._g,this._b=n._b,this.setAlpha(n._a),this},lighten:function(){return this._applyModification(Qu,arguments)},brighten:function(){return this._applyModification(th,arguments)},darken:function(){return this._applyModification(eh,arguments)},desaturate:function(){return this._applyModification(Ku,arguments)},saturate:function(){return this._applyModification(Zu,arguments)},greyscale:function(){return this._applyModification(Ju,arguments)},spin:function(){return this._applyModification(nh,arguments)},_applyCombination:function(t,e){return t.apply(null,[this].concat([].slice.call(e)))},analogous:function(){return this._applyCombination(oh,arguments)},complement:function(){return this._applyCombination(ih,arguments)},monochromatic:function(){return this._applyCombination(sh,arguments)},splitcomplement:function(){return this._applyCombination(ah,arguments)},triad:function(){return this._applyCombination(rh,[3])},tetrad:function(){return this._applyCombination(rh,[4])}},Wu.fromRatio=function(t,e){if("object"==Gu(t)){var n={};for(var i in t)t.hasOwnProperty(i)&&(n[i]="a"===i?t[i]:mh(t[i]));t=n}return Wu(t,e)},Wu.equals=function(t,e){return!(!t||!e)&&Wu(t).toRgbString()==Wu(e).toRgbString()},Wu.random=function(){return Wu.fromRatio({r:Math.random(),g:Math.random(),b:Math.random()})},Wu.mix=function(t,e,n){n=0===n?0:n||50;var i=Wu(t).toRgb(),r=Wu(e).toRgb(),a=n/100;return Wu({r:(r.r-i.r)*a+i.r,g:(r.g-i.g)*a+i.g,b:(r.b-i.b)*a+i.b,a:(r.a-i.a)*a+i.a})}, -// =4.5;break;case"AAlarge":r=a>=3;break;case"AAAsmall":r=a>=7}return r},Wu.mostReadable=function(t,e,n){var i,r,a,o,s=null,l=0;r=(n=n||{}).includeFallbackColors,a=n.level,o=n.size;for(var c=0;cl&&(l=i,s=Wu(e[c]));return Wu.isReadable(t,s,{level:a,size:o})||!r?s:(n.includeFallbackColors=!1,Wu.mostReadable(t,["#fff","#000"],n))};var lh=Wu.names={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"0ff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"00f",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",burntsienna:"ea7e5d",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"0ff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"f0f",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"663399",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"},ch=Wu.hexNames=function(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[t[n]]=n);return e}(lh);function uh(t){return t=parseFloat(t),(isNaN(t)||t<0||t>1)&&(t=1),t}function hh(t,e){(function(t){return"string"==typeof t&&-1!=t.indexOf(".")&&1===parseFloat(t)})(t)&&(t="100%");var n=function(t){return"string"==typeof t&&-1!=t.indexOf("%")}(t);return t=Math.min(e,Math.max(0,parseFloat(t))),n&&(t=parseInt(t*e,10)/100),Math.abs(t-e)<1e-6?1:t%e/parseFloat(e)}function dh(t){return Math.min(1,Math.max(0,t))}function ph(t){return parseInt(t,16)}function fh(t){return 1==t.length?"0"+t:""+t}function mh(t){return t<=1&&(t=100*t+"%"),t}function gh(t){return Math.round(255*parseFloat(t)).toString(16)}function vh(t){return ph(t)/255}var _h,yh,xh,bh=(yh="[\\s|\\(]+("+(_h="(?:[-\\+]?\\d*\\.\\d+%?)|(?:[-\\+]?\\d+%?)")+")[,|\\s]+("+_h+")[,|\\s]+("+_h+")\\s*\\)?",xh="[\\s|\\(]+("+_h+")[,|\\s]+("+_h+")[,|\\s]+("+_h+")[,|\\s]+("+_h+")\\s*\\)?",{CSS_UNIT:new RegExp(_h),rgb:new RegExp("rgb"+yh),rgba:new RegExp("rgba"+xh),hsl:new RegExp("hsl"+yh),hsla:new RegExp("hsla"+xh),hsv:new RegExp("hsv"+yh),hsva:new RegExp("hsva"+xh),hex3:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex4:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex8:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/});function Mh(t){return!!bh.CSS_UNIT.exec(t)}function Sh(t,e,n){return e=Lh(e),function(t,e){if(e&&("object"==typeof e||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return Nh(t)}(t,Eh()?Reflect.construct(e,n||[],Lh(t).constructor):e.apply(t,n))}function Eh(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(t){}return(Eh=function(){return!!t})()}function wh(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function Th(t){for(var e=1;e=0||(r[n]=t[n]);return r}(t,e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(t);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(r[n]=t[n])}return r}function Nh(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function Ih(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var i,r,a,o,s=[],l=!0,c=!1;try{if(a=(n=n.call(t)).next,0===e){if(Object(n)!==n)return;l=!1}else for(;!(l=(i=a.call(n)).done)&&(s.push(i.value),s.length!==e);l=!0);}catch(t){c=!0,r=t}finally{try{if(!l&&null!=n.return&&(o=n.return(),Object(o)!==o))return}finally{if(c)throw r}}return s}}(t,e)||Fh(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Uh(t){return function(t){if(Array.isArray(t))return kh(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||Fh(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Fh(t,e){if(t){if("string"==typeof t)return kh(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?kh(t,e):void 0}}function kh(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=new Array(e);n2&&void 0!==arguments[2]?arguments[2]:{},i=n.objFilter,r=void 0===i?function(){return!0}:i,a=Dh(n,Gh);return ku(t,e.children.filter(r),(function(t){return e.add(t)}),(function(t){e.remove(t),Hh(t)}),Th({objBindAttr:"__threeObj"},a))}var jh=function(t){return isNaN(t)?parseInt(Wu(t).toHex(),16):t},Wh=function(t){return isNaN(t)?Wu(t).getAlpha():1},Xh=function t(){var e=new vu,n=[],i=[],r=Bu;function a(t){let a=e.get(t);if(void 0===a){if(r!==Bu)return r;e.set(t,a=n.push(t)-1)}return i[a%i.length]}return a.domain=function(t){if(!arguments.length)return n.slice();n=[],e=new vu;for(const i of t)e.has(i)||e.set(i,n.push(i)-1);return a},a.range=function(t){return arguments.length?(i=Array.from(t),a):i.slice()},a.unknown=function(t){return arguments.length?(r=t,a):r},a.copy=function(){return t(n,i).unknown(r)},zu.apply(a,arguments),a}(Hu);function qh(t,e,n){e&&"string"==typeof n&&t.filter((function(t){return!t[n]})).forEach((function(t){t[n]=Xh(e(t))}))}var Yh=window.THREE?window.THREE:{Group:so,Mesh:ri,MeshLambertMaterial:class extends wn{constructor(t){super(),this.isMeshLambertMaterial=!0,this.type="MeshLambertMaterial",this.color=new Mn(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new Mn(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Ct(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new Ge,this.combine=_,this.reflectivity=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.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}},Color:Mn,BufferGeometry:Bn,BufferAttribute:Cn,Matrix4:Oe,Vector3:ne,SphereGeometry:zo,CylinderGeometry:Fo,TubeGeometry:Bo,ConeGeometry:ko,Line:class extends on{constructor(t=new Bn,e=new _o){super(),this.isLine=!0,this.type="Line",this.geometry=t,this.material=e,this.updateMorphTargets()}copy(t,e){return super.copy(t,e),this.material=Array.isArray(t.material)?t.material.slice():t.material,this.geometry=t.geometry,this}computeLineDistances(){const t=this.geometry;if(null===t.index){const e=t.attributes.position,n=[0];for(let t=1,i=e.count;ts)continue;h.applyMatrix4(this.matrixWorld);const a=t.ray.origin.distanceTo(h);at.far||e.push({distance:a,point:u.clone().applyMatrix4(this.matrixWorld),index:n,face:null,faceIndex:null,object:this})}}else{for(let n=Math.max(0,a.start),i=Math.min(f.count,a.start+a.count)-1;ns)continue;h.applyMatrix4(this.matrixWorld);const i=t.ray.origin.distanceTo(h);it.far||e.push({distance:i,point:u.clone().applyMatrix4(this.matrixWorld),index:n,face:null,faceIndex:null,object:this})}}}updateMorphTargets(){const t=this.geometry.morphAttributes,e=Object.keys(t);if(e.length>0){const n=t[e[0]];if(void 0!==n){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=n.length;t2?-60:-30),t<3&&i(e.graphData.nodes,"z"),t<2&&i(e.graphData.nodes,"y")}},dagMode:{onChange:function(t,e){!t&&"d3"===e.forceEngine&&(e.graphData.nodes||[]).forEach((function(t){return t.fx=t.fy=t.fz=void 0}))}},dagLevelDistance:{},dagNodeFilter:{default:function(t){return!0}},onDagError:{triggerUpdate:!1},nodeRelSize:{default:4},nodeId:{default:"id"},nodeVal:{default:"val"},nodeResolution:{default:8},nodeColor:{default:"color"},nodeAutoColorBy:{},nodeOpacity:{default:.75},nodeVisibility:{default:!0},nodeThreeObject:{},nodeThreeObjectExtend:{default:!1},nodePositionUpdate:{triggerUpdate:!1},linkSource:{default:"source"},linkTarget:{default:"target"},linkVisibility:{default:!0},linkColor:{default:"color"},linkAutoColorBy:{},linkOpacity:{default:.2},linkWidth:{},linkResolution:{default:6},linkCurvature:{default:0,triggerUpdate:!1},linkCurveRotation:{default:0,triggerUpdate:!1},linkMaterial:{},linkThreeObject:{},linkThreeObjectExtend:{default:!1},linkPositionUpdate:{triggerUpdate:!1},linkDirectionalArrowLength:{default:0},linkDirectionalArrowColor:{},linkDirectionalArrowRelPos:{default:.5,triggerUpdate:!1},linkDirectionalArrowResolution:{default:8},linkDirectionalParticles:{default:0},linkDirectionalParticleSpeed:{default:.01,triggerUpdate:!1},linkDirectionalParticleWidth:{default:.5},linkDirectionalParticleColor:{},linkDirectionalParticleResolution:{default:4},forceEngine:{default:"d3"},d3AlphaMin:{default:0},d3AlphaDecay:{default:.0228,triggerUpdate:!1,onChange:function(t,e){e.d3ForceLayout.alphaDecay(t)}},d3AlphaTarget:{default:0,triggerUpdate:!1,onChange:function(t,e){e.d3ForceLayout.alphaTarget(t)}},d3VelocityDecay:{default:.4,triggerUpdate:!1,onChange:function(t,e){e.d3ForceLayout.velocityDecay(t)}},ngraphPhysics:{default:{timeStep:20,gravity:-1.2,theta:.8,springLength:30,springCoefficient:8e-4,dragCoefficient:.02}},warmupTicks:{default:0,triggerUpdate:!1},cooldownTicks:{default:1/0,triggerUpdate:!1},cooldownTime:{default:15e3,triggerUpdate:!1},onLoading:{default:function(){},triggerUpdate:!1},onFinishLoading:{default:function(){},triggerUpdate:!1},onUpdate:{default:function(){},triggerUpdate:!1},onFinishUpdate:{default:function(){},triggerUpdate:!1},onEngineTick:{default:function(){},triggerUpdate:!1},onEngineStop:{default:function(){},triggerUpdate:!1}},methods:{refresh:function(t){return t._flushObjects=!0,t._rerender(),this},d3Force:function(t,e,n){return void 0===n?t.d3ForceLayout.force(e):(t.d3ForceLayout.force(e,n),this)},d3ReheatSimulation:function(t){return t.d3ForceLayout.alpha(1),this.resetCountdown(),this},resetCountdown:function(t){return t.cntTicks=0,t.startTickTime=new Date,t.engineRunning=!0,this},tickFrame:function(t){var e,n,i,r,a="ngraph"!==t.forceEngine;return t.engineRunning&&function(){++t.cntTicks>t.cooldownTicks||new Date-t.startTickTime>t.cooldownTime||a&&t.d3AlphaMin>0&&t.d3ForceLayout.alpha()0){var f=s.x-o.x,m=s.y-o.y||0,g=(new Yh.Vector3).subVectors(h,u),v=g.clone().multiplyScalar(l).cross(0!==f||0!==m?new Yh.Vector3(0,0,1):new Yh.Vector3(0,1,0)).applyAxisAngle(g.normalize(),p).add((new Yh.Vector3).addVectors(u,h).divideScalar(2));c=new Yh.QuadraticBezierCurve3(u,v,h)}else{var _=70*l,y=-p,x=y+Math.PI/2;c=new Yh.CubicBezierCurve3(u,new Yh.Vector3(_*Math.cos(x),_*Math.sin(x),0).add(u),new Yh.Vector3(_*Math.cos(y),_*Math.sin(y),0).add(u),h)}e.__curve=c}else e.__curve=null}}t.graphData.links.forEach((function(e){var i=e.__lineObj;if(i){var r=a?e:t.layout.getLinkPosition(t.layout.graph.getLink(e.source,e.target).id),l=r[a?"source":"from"],c=r[a?"target":"to"];if(l&&c&&l.hasOwnProperty("x")&&c.hasOwnProperty("x")){s(e);var u=o(e);if(!t.linkPositionUpdate||!t.linkPositionUpdate(u?i.children[1]:i,{start:{x:l.x,y:l.y,z:l.z},end:{x:c.x,y:c.y,z:c.z}},e)||u){var h=30,d=e.__curve,p=i.children.length?i.children[0]:i;if("Line"===p.type){if(d)p.geometry.setFromPoints(d.getPoints(h));else{var f=p.geometry.getAttribute("position");f&&f.array&&6===f.array.length||p.geometry[Kh]("position",f=new Yh.BufferAttribute(new Float32Array(6),3)),f.array[0]=l.x,f.array[1]=l.y||0,f.array[2]=l.z||0,f.array[3]=c.x,f.array[4]=c.y||0,f.array[5]=c.z||0,f.needsUpdate=!0}p.geometry.computeBoundingSphere()}else if("Mesh"===p.type)if(d){p.geometry.type.match(/^Tube(Buffer)?Geometry$/)||(p.position.set(0,0,0),p.rotation.set(0,0,0),p.scale.set(1,1,1));var m=Math.ceil(10*n(e))/10/2,g=new Yh.TubeGeometry(d,h,m,t.linkResolution,!1);p.geometry.dispose(),p.geometry=g}else{if(!p.geometry.type.match(/^Cylinder(Buffer)?Geometry$/)){var v=Math.ceil(10*n(e))/10/2,_=new Yh.CylinderGeometry(v,v,1,t.linkResolution,1,!1);_[Zh]((new Yh.Matrix4).makeTranslation(0,.5,0)),_[Zh]((new Yh.Matrix4).makeRotationX(Math.PI/2)),p.geometry.dispose(),p.geometry=_}var y=new Yh.Vector3(l.x,l.y||0,l.z||0),x=new Yh.Vector3(c.x,c.y||0,c.z||0),b=y.distanceTo(x);p.position.x=y.x,p.position.y=y.y,p.position.z=y.z,p.scale.z=b,p.parent.localToWorld(x),p.lookAt(x)}}}}}))}(),e=gu(t.linkDirectionalArrowRelPos),n=gu(t.linkDirectionalArrowLength),i=gu(t.nodeVal),t.graphData.links.forEach((function(r){var o=r.__arrowObj;if(o){var s=a?r:t.layout.getLinkPosition(t.layout.graph.getLink(r.source,r.target).id),l=s[a?"source":"from"],c=s[a?"target":"to"];if(l&&c&&l.hasOwnProperty("x")&&c.hasOwnProperty("x")){var u=Math.cbrt(Math.max(0,i(l)||1))*t.nodeRelSize,h=Math.cbrt(Math.max(0,i(c)||1))*t.nodeRelSize,d=n(r),p=e(r),f=r.__curve?function(t){return r.__curve.getPoint(t)}:function(t){var e=function(t,e,n,i){return e[t]+(n[t]-e[t])*i||0};return{x:e("x",l,c,t),y:e("y",l,c,t),z:e("z",l,c,t)}},m=r.__curve?r.__curve.getLength():Math.sqrt(["x","y","z"].map((function(t){return Math.pow((c[t]||0)-(l[t]||0),2)})).reduce((function(t,e){return t+e}),0)),g=u+d+(m-u-h-d)*p,v=f(g/m),_=f((g-d)/m);["x","y","z"].forEach((function(t){return o.position[t]=_[t]}));var y=function(t,e,n){if(Eh())return Reflect.construct.apply(null,arguments);var i=[null];i.push.apply(i,e);var r=new(t.bind.apply(t,i));return n&&Oh(r,n.prototype),r}(Yh.Vector3,Uh(["x","y","z"].map((function(t){return v[t]}))));o.parent.localToWorld(y),o.lookAt(y)}}})),r=gu(t.linkDirectionalParticleSpeed),t.graphData.links.forEach((function(e){var n=e.__photonsObj&&e.__photonsObj.children,i=e.__singleHopPhotonsObj&&e.__singleHopPhotonsObj.children;if(i&&i.length||n&&n.length){var o=a?e:t.layout.getLinkPosition(t.layout.graph.getLink(e.source,e.target).id),s=o[a?"source":"from"],l=o[a?"target":"to"];if(s&&l&&s.hasOwnProperty("x")&&l.hasOwnProperty("x")){var c=r(e),u=e.__curve?function(t){return e.__curve.getPoint(t)}:function(t){var e=function(t,e,n,i){return e[t]+(n[t]-e[t])*i||0};return{x:e("x",s,l,t),y:e("y",s,l,t),z:e("z",s,l,t)}};[].concat(Uh(n||[]),Uh(i||[])).forEach((function(t,e){var i="singleHopPhotons"===t.parent.__linkThreeObjType;if(t.hasOwnProperty("__progressRatio")||(t.__progressRatio=i?0:e/n.length),t.__progressRatio+=c,t.__progressRatio>=1){if(i)return t.parent.remove(t),void Hh(t);t.__progressRatio=t.__progressRatio%1}var r=t.__progressRatio,a=u(r);["x","y","z"].forEach((function(e){return t.position[e]=a[e]}))}))}}})),this},emitParticle:function(t,e){if(e&&t.graphData.links.includes(e)){if(!e.__singleHopPhotonsObj){var n=new Yh.Group;n.__linkThreeObjType="singleHopPhotons",e.__singleHopPhotonsObj=n,t.graphScene.add(n)}var i=gu(t.linkDirectionalParticleWidth),r=Math.ceil(10*i(e))/10/2,a=t.linkDirectionalParticleResolution,o=new Yh.SphereGeometry(r,a,a),s=gu(t.linkColor),l=gu(t.linkDirectionalParticleColor)(e)||s(e)||"#f0f0f0",c=new Yh.Color(jh(l)),u=3*t.linkOpacity,h=new Yh.MeshLambertMaterial({color:c,transparent:!0,opacity:u});e.__singleHopPhotonsObj.add(new Yh.Mesh(o,h))}return this},getGraphBbox:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){return!0};if(!t.initialised)return null;var n=function t(n){var i=[];if(n.geometry){n.geometry.computeBoundingBox();var r=new Yh.Box3;r.copy(n.geometry.boundingBox).applyMatrix4(n.matrixWorld),i.push(r)}return i.concat.apply(i,Uh((n.children||[]).filter((function(t){return!t.hasOwnProperty("__graphObjType")||"node"===t.__graphObjType&&e(t.__data)})).map(t)))}(t.graphScene);return n.length?Object.assign.apply(Object,Uh(["x","y","z"].map((function(t){return Ph({},t,[bu(n,(function(e){return e.min[t]})),xu(n,(function(e){return e.max[t]}))])})))):null}},stateInit:function(){return{d3ForceLayout:El().force("link",Xs()).force("charge",wl()).force("center",vs()).force("dagRadial",null).stop(),engineRunning:!1}},init:function(t,e){e.graphScene=t},update:function(t,e){var n=function(t){return t.some((function(t){return e.hasOwnProperty(t)}))};if(t.engineRunning=!1,t.onUpdate(),null!==t.nodeAutoColorBy&&n(["nodeAutoColorBy","graphData","nodeColor"])&&qh(t.graphData.nodes,gu(t.nodeAutoColorBy),t.nodeColor),null!==t.linkAutoColorBy&&n(["linkAutoColorBy","graphData","linkColor"])&&qh(t.graphData.links,gu(t.linkAutoColorBy),t.linkColor),t._flushObjects||n(["graphData","nodeThreeObject","nodeThreeObjectExtend","nodeVal","nodeColor","nodeVisibility","nodeRelSize","nodeResolution","nodeOpacity"])){var i=gu(t.nodeThreeObject),r=gu(t.nodeThreeObjectExtend),a=gu(t.nodeVal),o=gu(t.nodeColor),s=gu(t.nodeVisibility),l={},c={};Vh(t.graphData.nodes.filter(s),t.graphScene,{purge:t._flushObjects||n(["nodeThreeObject","nodeThreeObjectExtend"]),objFilter:function(t){return"node"===t.__graphObjType},createObj:function(e){var n,a=i(e),o=r(e);return a&&t.nodeThreeObject===a&&(a=a.clone()),a&&!o?n=a:((n=new Yh.Mesh).__graphDefaultObj=!0,a&&o&&n.add(a)),n.__graphObjType="node",n},updateObj:function(e,n){if(e.__graphDefaultObj){var i=a(n)||1,r=Math.cbrt(i)*t.nodeRelSize,s=t.nodeResolution;e.geometry.type.match(/^Sphere(Buffer)?Geometry$/)&&e.geometry.parameters.radius===r&&e.geometry.parameters.widthSegments===s||(l.hasOwnProperty(i)||(l[i]=new Yh.SphereGeometry(r,s,s)),e.geometry.dispose(),e.geometry=l[i]);var u=o(n),h=new Yh.Color(jh(u||"#ffffaa")),d=t.nodeOpacity*Wh(u);"MeshLambertMaterial"===e.material.type&&e.material.color.equals(h)&&e.material.opacity===d||(c.hasOwnProperty(u)||(c[u]=new Yh.MeshLambertMaterial({color:h,transparent:!0,opacity:d})),e.material.dispose(),e.material=c[u])}}})}if(t._flushObjects||n(["graphData","linkThreeObject","linkThreeObjectExtend","linkMaterial","linkColor","linkWidth","linkVisibility","linkResolution","linkOpacity","linkDirectionalArrowLength","linkDirectionalArrowColor","linkDirectionalArrowResolution","linkDirectionalParticles","linkDirectionalParticleWidth","linkDirectionalParticleColor","linkDirectionalParticleResolution"])){var u=gu(t.linkThreeObject),h=gu(t.linkThreeObjectExtend),d=gu(t.linkMaterial),p=gu(t.linkVisibility),f=gu(t.linkColor),m=gu(t.linkWidth),g={},v={},_={},y=t.graphData.links.filter(p);if(Vh(y,t.graphScene,{objBindAttr:"__lineObj",purge:t._flushObjects||n(["linkThreeObject","linkThreeObjectExtend","linkWidth"]),objFilter:function(t){return"link"===t.__graphObjType},exitObj:function(t){var e=t.__data&&t.__data.__singleHopPhotonsObj;e&&(e.parent.remove(e),Hh(e),delete t.__data.__singleHopPhotonsObj)},createObj:function(e){var n,i,r=u(e),a=h(e);if(r&&t.linkThreeObject===r&&(r=r.clone()),!r||a)if(!!m(e))n=new Yh.Mesh;else{var o=new Yh.BufferGeometry;o[Kh]("position",new Yh.BufferAttribute(new Float32Array(6),3)),n=new Yh.Line(o)}return r?a?((i=new Yh.Group).__graphDefaultObj=!0,i.add(n),i.add(r)):i=r:(i=n).__graphDefaultObj=!0,i.renderOrder=10,i.__graphObjType="link",i},updateObj:function(e,n){if(e.__graphDefaultObj){var i=e.children.length?e.children[0]:e,r=Math.ceil(10*m(n))/10,a=!!r;if(a){var o=r/2,s=t.linkResolution;if(!i.geometry.type.match(/^Cylinder(Buffer)?Geometry$/)||i.geometry.parameters.radiusTop!==o||i.geometry.parameters.radialSegments!==s){if(!g.hasOwnProperty(r)){var l=new Yh.CylinderGeometry(o,o,1,s,1,!1);l[Zh]((new Yh.Matrix4).makeTranslation(0,.5,0)),l[Zh]((new Yh.Matrix4).makeRotationX(Math.PI/2)),g[r]=l}i.geometry.dispose(),i.geometry=g[r]}}var c=d(n);if(c)i.material=c;else{var u=f(n),h=new Yh.Color(jh(u||"#f0f0f0")),p=t.linkOpacity*Wh(u),y=a?"MeshLambertMaterial":"LineBasicMaterial";if(i.material.type!==y||!i.material.color.equals(h)||i.material.opacity!==p){var x=a?v:_;x.hasOwnProperty(u)||(x[u]=new Yh[y]({color:h,transparent:p<1,opacity:p,depthWrite:p>=1})),i.material.dispose(),i.material=x[u]}}}}}),t.linkDirectionalArrowLength||e.hasOwnProperty("linkDirectionalArrowLength")){var x=gu(t.linkDirectionalArrowLength),b=gu(t.linkDirectionalArrowColor);Vh(y.filter(x),t.graphScene,{objBindAttr:"__arrowObj",objFilter:function(t){return"arrow"===t.__linkThreeObjType},createObj:function(){var t=new Yh.Mesh(void 0,new Yh.MeshLambertMaterial({transparent:!0}));return t.__linkThreeObjType="arrow",t},updateObj:function(e,n){var i=x(n),r=t.linkDirectionalArrowResolution;if(!e.geometry.type.match(/^Cone(Buffer)?Geometry$/)||e.geometry.parameters.height!==i||e.geometry.parameters.radialSegments!==r){var a=new Yh.ConeGeometry(.25*i,i,r);a.translate(0,i/2,0),a.rotateX(Math.PI/2),e.geometry.dispose(),e.geometry=a}var o=b(n)||f(n)||"#f0f0f0";e.material.color=new Yh.Color(jh(o)),e.material.opacity=3*t.linkOpacity*Wh(o)}})}if(t.linkDirectionalParticles||e.hasOwnProperty("linkDirectionalParticles")){var M=gu(t.linkDirectionalParticles),S=gu(t.linkDirectionalParticleWidth),E=gu(t.linkDirectionalParticleColor),w={},T={};Vh(y.filter(M),t.graphScene,{objBindAttr:"__photonsObj",objFilter:function(t){return"photons"===t.__linkThreeObjType},createObj:function(){var t=new Yh.Group;return t.__linkThreeObjType="photons",t},updateObj:function(e,n){var i,r=Math.round(Math.abs(M(n))),a=!!e.children.length&&e.children[0],o=Math.ceil(10*S(n))/10/2,s=t.linkDirectionalParticleResolution;a&&a.geometry.parameters.radius===o&&a.geometry.parameters.widthSegments===s?i=a.geometry:(T.hasOwnProperty(o)||(T[o]=new Yh.SphereGeometry(o,s,s)),i=T[o],a&&a.geometry.dispose());var l,c=E(n)||f(n)||"#f0f0f0",u=new Yh.Color(jh(c)),h=3*t.linkOpacity;a&&a.material.color.equals(u)&&a.material.opacity===h?l=a.material:(w.hasOwnProperty(c)||(w[c]=new Yh.MeshLambertMaterial({color:u,transparent:!0,opacity:h})),l=w[c],a&&a.material.dispose()),Vh(Uh(new Array(r)).map((function(t,e){return{idx:e}})),e,{idAccessor:function(t){return t.idx},createObj:function(){return new Yh.Mesh(i,l)},updateObj:function(t){t.geometry=i,t.material=l}})}})}}if(t._flushObjects=!1,n(["graphData","nodeId","linkSource","linkTarget","numDimensions","forceEngine","dagMode","dagNodeFilter","dagLevelDistance"])){t.engineRunning=!1,t.graphData.links.forEach((function(e){e.source=e[t.linkSource],e.target=e[t.linkTarget]}));var A,R="ngraph"!==t.forceEngine;if(R){(A=t.d3ForceLayout).stop().alpha(1).numDimensions(t.numDimensions).nodes(t.graphData.nodes);var C=t.d3ForceLayout.force("link");C&&C.id((function(e){return e[t.nodeId]})).links(t.graphData.links);var P=t.dagMode&&function(t,e){var n=t.nodes,i=t.links,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},a=r.nodeFilter,o=void 0===a?function(){return!0}:a,s=r.onLoopError,l=void 0===s?function(t){throw"Invalid DAG structure! Found cycle in node path: ".concat(t.join(" -> "),".")}:s,c={};n.forEach((function(t){return c[e(t)]={data:t,out:[],depth:-1,skip:!o(t)}})),i.forEach((function(t){var n=t.source,i=t.target,r=l(n),a=l(i);if(!c.hasOwnProperty(r))throw"Missing source node with id: ".concat(r);if(!c.hasOwnProperty(a))throw"Missing target node with id: ".concat(a);var o=c[r],s=c[a];function l(t){return"object"===Rh(t)?e(t):t}o.out.push(s)}));var u=[];return function t(n){for(var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,a=function(){var a=n[o];if(-1!==i.indexOf(a)){var s=[].concat(Uh(i.slice(i.indexOf(a))),[a]).map((function(t){return e(t.data)}));return u.some((function(t){return t.length===s.length&&t.every((function(t,e){return t===s[e]}))}))||(u.push(s),l(s)),1}r>a.depth&&(a.depth=r,t(a.out,[].concat(Uh(i),[a]),r+(a.skip?0:1)))},o=0,s=n.length;o1&&(u.vy+=d*m),a>2&&(u.vz+=p*m)}}function u(){if(r){var e,n=r.length;for(o=new Array(n),s=new Array(n),e=0;e[1,2,3].includes(t)))||2,u()},c.strength=function(t){return arguments.length?(l="function"==typeof t?t:Gs(+t),u(),c):l},c.radius=function(e){return arguments.length?(t="function"==typeof e?e:Gs(+e),u(),c):t},c.x=function(t){return arguments.length?(e=+t,c):e},c.y=function(t){return arguments.length?(n=+t,c):n},c.z=function(t){return arguments.length?(i=+t,c):i},c}((function(e){var n=P[e[t.nodeId]]||-1;return("radialin"===t.dagMode?L-n:n)*O})).strength((function(e){return t.dagNodeFilter(e)?1:0})):null)}else{var F=$h.graph();t.graphData.nodes.forEach((function(e){F.addNode(e[t.nodeId])})),t.graphData.links.forEach((function(t){F.addLink(t.source,t.target)})),(A=$h.forcelayout(F,Th({dimensions:t.numDimensions},t.ngraphPhysics))).graph=F}for(var k=0;k0&&t.d3ForceLayout.alpha()2&&void 0!==arguments[2]&&arguments[2],n=function(n){function i(){var n;!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,i);for(var r=arguments.length,a=new Array(r),o=0;o1&&void 0!==arguments[1]?arguments[1]:Object);return Object.keys(t()).forEach((function(t){return n.prototype[t]=function(){var e,n=(e=this.__kapsuleInstance)[t].apply(e,arguments);return n===this.__kapsuleInstance?this:n}})),n}(Jh,(window.THREE?window.THREE:{Group:so}).Group,!0);const td={type:"change"},ed={type:"start"},nd={type:"end"};class id extends mt{constructor(t,e){super();const n=this,i=-1,r=0,a=1,o=2,l=3,c=4;this.object=t,this.domElement=e,this.domElement.style.touchAction="none",this.enabled=!0,this.screen={left:0,top:0,width:0,height:0},this.rotateSpeed=1,this.zoomSpeed=1.2,this.panSpeed=.3,this.noRotate=!1,this.noZoom=!1,this.noPan=!1,this.staticMoving=!1,this.dynamicDampingFactor=.2,this.minDistance=0,this.maxDistance=1/0,this.minZoom=0,this.maxZoom=1/0,this.keys=["KeyA","KeyS","KeyD"],this.mouseButtons={LEFT:s.ROTATE,MIDDLE:s.DOLLY,RIGHT:s.PAN},this.target=new ne;const u=1e-6,h=new ne;let d=1,p=i,f=i,m=0,g=0,v=0;const _=new ne,y=new Ct,x=new Ct,b=new ne,M=new Ct,S=new Ct,E=new Ct,w=new Ct,T=[],A={};this.target0=this.target.clone(),this.position0=this.object.position.clone(),this.up0=this.object.up.clone(),this.zoom0=this.object.zoom,this.handleResize=function(){const t=n.domElement.getBoundingClientRect(),e=n.domElement.ownerDocument.documentElement;n.screen.left=t.left+window.pageXOffset-e.clientLeft,n.screen.top=t.top+window.pageYOffset-e.clientTop,n.screen.width=t.width,n.screen.height=t.height};const R=function(){const t=new Ct;return function(e,i){return t.set((e-n.screen.left)/n.screen.width,(i-n.screen.top)/n.screen.height),t}}(),C=function(){const t=new Ct;return function(e,i){return t.set((e-.5*n.screen.width-n.screen.left)/(.5*n.screen.width),(n.screen.height+2*(n.screen.top-i))/n.screen.width),t}}();function P(t){!1!==n.enabled&&(0===T.length&&(n.domElement.setPointerCapture(t.pointerId),n.domElement.addEventListener("pointermove",L),n.domElement.addEventListener("pointerup",O)),function(t){T.push(t)}(t),"touch"===t.pointerType?function(t){if(1===(z(t),T.length))p=l,x.copy(C(T[0].pageX,T[0].pageY)),y.copy(x);else{p=c;const t=T[0].pageX-T[1].pageX,e=T[0].pageY-T[1].pageY;g=m=Math.sqrt(t*t+e*e);const n=(T[0].pageX+T[1].pageX)/2,i=(T[0].pageY+T[1].pageY)/2;E.copy(R(n,i)),w.copy(E)}n.dispatchEvent(ed)}(t):function(t){if(p===i)switch(t.button){case n.mouseButtons.LEFT:p=r;break;case n.mouseButtons.MIDDLE:p=a;break;case n.mouseButtons.RIGHT:p=o}const e=f!==i?f:p;e!==r||n.noRotate?e!==a||n.noZoom?e!==o||n.noPan||(E.copy(R(t.pageX,t.pageY)),w.copy(E)):(M.copy(R(t.pageX,t.pageY)),S.copy(M)):(x.copy(C(t.pageX,t.pageY)),y.copy(x));n.dispatchEvent(ed)}(t))}function L(t){!1!==n.enabled&&("touch"===t.pointerType?function(t){if(1===(z(t),T.length))y.copy(x),x.copy(C(t.pageX,t.pageY));else{const e=function(t){const e=t.pointerId===T[0].pointerId?T[1]:T[0];return A[e.pointerId]}(t),n=t.pageX-e.x,i=t.pageY-e.y;g=Math.sqrt(n*n+i*i);const r=(t.pageX+e.x)/2,a=(t.pageY+e.y)/2;w.copy(R(r,a))}}(t):function(t){const e=f!==i?f:p;e!==r||n.noRotate?e!==a||n.noZoom?e!==o||n.noPan||w.copy(R(t.pageX,t.pageY)):S.copy(R(t.pageX,t.pageY)):(y.copy(x),x.copy(C(t.pageX,t.pageY)))}(t))}function O(t){!1!==n.enabled&&("touch"===t.pointerType?function(t){switch(T.length){case 0:p=i;break;case 1:p=l,x.copy(C(t.pageX,t.pageY)),y.copy(x);break;case 2:p=c;for(let e=0;e0&&(n.object.isPerspectiveCamera?_.multiplyScalar(t):n.object.isOrthographicCamera?(n.object.zoom=Rt.clamp(n.object.zoom/t,n.minZoom,n.maxZoom),d!==n.object.zoom&&n.object.updateProjectionMatrix()):console.warn("THREE.TrackballControls: Unsupported camera type")),n.staticMoving?M.copy(S):M.y+=(S.y-M.y)*this.dynamicDampingFactor)},this.panCamera=function(){const t=new Ct,e=new ne,i=new ne;return function(){if(t.copy(w).sub(E),t.lengthSq()){if(n.object.isOrthographicCamera){const e=(n.object.right-n.object.left)/n.object.zoom/n.domElement.clientWidth,i=(n.object.top-n.object.bottom)/n.object.zoom/n.domElement.clientWidth;t.x*=e,t.y*=i}t.multiplyScalar(_.length()*n.panSpeed),i.copy(_).cross(n.object.up).setLength(t.x),i.add(e.copy(n.object.up).setLength(t.y)),n.object.position.add(i),n.target.add(i),n.staticMoving?E.copy(w):E.add(t.subVectors(w,E).multiplyScalar(n.dynamicDampingFactor))}}}(),this.checkDistances=function(){n.noZoom&&n.noPan||(_.lengthSq()>n.maxDistance*n.maxDistance&&(n.object.position.addVectors(n.target,_.setLength(n.maxDistance)),M.copy(S)),_.lengthSq()u&&(n.dispatchEvent(td),h.copy(n.object.position))):n.object.isOrthographicCamera?(n.object.lookAt(n.target),(h.distanceToSquared(n.object.position)>u||d!==n.object.zoom)&&(n.dispatchEvent(td),h.copy(n.object.position),d=n.object.zoom)):console.warn("THREE.TrackballControls: Unsupported camera type")},this.reset=function(){p=i,f=i,n.target.copy(n.target0),n.object.position.copy(n.position0),n.object.up.copy(n.up0),n.object.zoom=n.zoom0,n.object.updateProjectionMatrix(),_.subVectors(n.object.position,n.target),n.object.lookAt(n.target),n.dispatchEvent(td),h.copy(n.object.position),d=n.object.zoom},this.dispose=function(){n.domElement.removeEventListener("contextmenu",F),n.domElement.removeEventListener("pointerdown",P),n.domElement.removeEventListener("pointercancel",D),n.domElement.removeEventListener("wheel",U),n.domElement.removeEventListener("pointermove",L),n.domElement.removeEventListener("pointerup",O),window.removeEventListener("keydown",N),window.removeEventListener("keyup",I)},this.domElement.addEventListener("contextmenu",F),this.domElement.addEventListener("pointerdown",P),this.domElement.addEventListener("pointercancel",D),this.domElement.addEventListener("wheel",U,{passive:!1}),window.addEventListener("keydown",N),window.addEventListener("keyup",I),this.handleResize(),this.update()}}const rd={type:"change"},ad={type:"start"},od={type:"end"},sd=new Le,ld=new Ei,cd=Math.cos(70*Rt.DEG2RAD);class ud extends mt{constructor(t,e){super(),this.object=t,this.domElement=e,this.domElement.style.touchAction="none",this.enabled=!0,this.target=new ne,this.cursor=new ne,this.minDistance=0,this.maxDistance=1/0,this.minZoom=0,this.maxZoom=1/0,this.minTargetRadius=0,this.maxTargetRadius=1/0,this.minPolarAngle=0,this.maxPolarAngle=Math.PI,this.minAzimuthAngle=-1/0,this.maxAzimuthAngle=1/0,this.enableDamping=!1,this.dampingFactor=.05,this.enableZoom=!0,this.zoomSpeed=1,this.enableRotate=!0,this.rotateSpeed=1,this.enablePan=!0,this.panSpeed=1,this.screenSpacePanning=!0,this.keyPanSpeed=7,this.zoomToCursor=!1,this.autoRotate=!1,this.autoRotateSpeed=2,this.keys={LEFT:"ArrowLeft",UP:"ArrowUp",RIGHT:"ArrowRight",BOTTOM:"ArrowDown"},this.mouseButtons={LEFT:s.ROTATE,MIDDLE:s.DOLLY,RIGHT:s.PAN},this.touches={ONE:l,TWO:u},this.target0=this.target.clone(),this.position0=this.object.position.clone(),this.zoom0=this.object.zoom,this._domElementKeyEvents=null,this.getPolarAngle=function(){return o.phi},this.getAzimuthalAngle=function(){return o.theta},this.getDistance=function(){return this.object.position.distanceTo(this.target)},this.listenToKeyEvents=function(t){t.addEventListener("keydown",tt),this._domElementKeyEvents=t},this.stopListenToKeyEvents=function(){this._domElementKeyEvents.removeEventListener("keydown",tt),this._domElementKeyEvents=null},this.saveState=function(){n.target0.copy(n.target),n.position0.copy(n.object.position),n.zoom0=n.object.zoom},this.reset=function(){n.target.copy(n.target0),n.object.position.copy(n.position0),n.object.zoom=n.zoom0,n.object.updateProjectionMatrix(),n.dispatchEvent(rd),n.update(),r=i.NONE},this.update=function(){const e=new ne,s=(new ee).setFromUnitVectors(t.up,new ne(0,1,0)),l=s.clone().invert(),c=new ne,u=new ee,h=new ne,m=2*Math.PI;return function(g=null){const v=n.object.position;e.copy(v).sub(n.target),e.applyQuaternion(s),o.setFromVector3(e),n.autoRotate&&r===i.NONE&&L(function(t){return null!==t?2*Math.PI/60*n.autoRotateSpeed*t:2*Math.PI/60/60*n.autoRotateSpeed}(g)),n.enableDamping?(o.theta+=d.theta*n.dampingFactor,o.phi+=d.phi*n.dampingFactor):(o.theta+=d.theta,o.phi+=d.phi);let _=n.minAzimuthAngle,y=n.maxAzimuthAngle;isFinite(_)&&isFinite(y)&&(_<-Math.PI?_+=m:_>Math.PI&&(_-=m),y<-Math.PI?y+=m:y>Math.PI&&(y-=m),o.theta=_<=y?Math.max(_,Math.min(y,o.theta)):o.theta>(_+y)/2?Math.max(_,o.theta):Math.min(y,o.theta)),o.phi=Math.max(n.minPolarAngle,Math.min(n.maxPolarAngle,o.phi)),o.makeSafe(),!0===n.enableDamping?n.target.addScaledVector(f,n.dampingFactor):n.target.add(f),n.target.sub(n.cursor),n.target.clampLength(n.minTargetRadius,n.maxTargetRadius),n.target.add(n.cursor);let x=!1;if(n.zoomToCursor&&T||n.object.isOrthographicCamera)o.radius=z(o.radius);else{const t=o.radius;o.radius=z(o.radius*p),x=t!=o.radius}if(e.setFromSpherical(o),e.applyQuaternion(l),v.copy(n.target).add(e),n.object.lookAt(n.target),!0===n.enableDamping?(d.theta*=1-n.dampingFactor,d.phi*=1-n.dampingFactor,f.multiplyScalar(1-n.dampingFactor)):(d.set(0,0,0),f.set(0,0,0)),n.zoomToCursor&&T){let i=null;if(n.object.isPerspectiveCamera){const t=e.length();i=z(t*p);const r=t-i;n.object.position.addScaledVector(E,r),n.object.updateMatrixWorld(),x=!!r}else if(n.object.isOrthographicCamera){const t=new ne(w.x,w.y,0);t.unproject(n.object);const r=n.object.zoom;n.object.zoom=Math.max(n.minZoom,Math.min(n.maxZoom,n.object.zoom/p)),n.object.updateProjectionMatrix(),x=r!==n.object.zoom;const a=new ne(w.x,w.y,0);a.unproject(n.object),n.object.position.sub(a).add(t),n.object.updateMatrixWorld(),i=e.length()}else console.warn("WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled."),n.zoomToCursor=!1;null!==i&&(this.screenSpacePanning?n.target.set(0,0,-1).transformDirection(n.object.matrix).multiplyScalar(i).add(n.object.position):(sd.origin.copy(n.object.position),sd.direction.set(0,0,-1).transformDirection(n.object.matrix),Math.abs(n.object.up.dot(sd.direction))a||8*(1-u.dot(n.object.quaternion))>a||h.distanceToSquared(n.target)>a)&&(n.dispatchEvent(rd),c.copy(n.object.position),u.copy(n.object.quaternion),h.copy(n.target),!0)}}(),this.dispose=function(){n.domElement.removeEventListener("contextmenu",nt),n.domElement.removeEventListener("pointerdown",Y),n.domElement.removeEventListener("pointercancel",K),n.domElement.removeEventListener("wheel",Z),n.domElement.removeEventListener("pointermove",$),n.domElement.removeEventListener("pointerup",K);n.domElement.getRootNode().removeEventListener("keydown",J,{capture:!0}),null!==n._domElementKeyEvents&&(n._domElementKeyEvents.removeEventListener("keydown",tt),n._domElementKeyEvents=null)};const n=this,i={NONE:-1,ROTATE:0,DOLLY:1,PAN:2,TOUCH_ROTATE:3,TOUCH_PAN:4,TOUCH_DOLLY_PAN:5,TOUCH_DOLLY_ROTATE:6};let r=i.NONE;const a=1e-6,o=new rs,d=new rs;let p=1;const f=new ne,m=new Ct,g=new Ct,v=new Ct,_=new Ct,y=new Ct,x=new Ct,b=new Ct,M=new Ct,S=new Ct,E=new ne,w=new Ct;let T=!1;const A=[],R={};let C=!1;function P(t){const e=Math.abs(.01*t);return Math.pow(.95,n.zoomSpeed*e)}function L(t){d.theta-=t}function O(t){d.phi-=t}const D=function(){const t=new ne;return function(e,n){t.setFromMatrixColumn(n,0),t.multiplyScalar(-e),f.add(t)}}(),N=function(){const t=new ne;return function(e,i){!0===n.screenSpacePanning?t.setFromMatrixColumn(i,1):(t.setFromMatrixColumn(i,0),t.crossVectors(n.object.up,t)),t.multiplyScalar(e),f.add(t)}}(),I=function(){const t=new ne;return function(e,i){const r=n.domElement;if(n.object.isPerspectiveCamera){const a=n.object.position;t.copy(a).sub(n.target);let o=t.length();o*=Math.tan(n.object.fov/2*Math.PI/180),D(2*e*o/r.clientHeight,n.object.matrix),N(2*i*o/r.clientHeight,n.object.matrix)}else n.object.isOrthographicCamera?(D(e*(n.object.right-n.object.left)/n.object.zoom/r.clientWidth,n.object.matrix),N(i*(n.object.top-n.object.bottom)/n.object.zoom/r.clientHeight,n.object.matrix)):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."),n.enablePan=!1)}}();function U(t){n.object.isPerspectiveCamera||n.object.isOrthographicCamera?p/=t:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),n.enableZoom=!1)}function F(t){n.object.isPerspectiveCamera||n.object.isOrthographicCamera?p*=t:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),n.enableZoom=!1)}function k(t,e){if(!n.zoomToCursor)return;T=!0;const i=n.domElement.getBoundingClientRect(),r=t-i.left,a=e-i.top,o=i.width,s=i.height;w.x=r/o*2-1,w.y=-a/s*2+1,E.set(w.x,w.y,1).unproject(n.object).sub(n.object.position).normalize()}function z(t){return Math.max(n.minDistance,Math.min(n.maxDistance,t))}function B(t){m.set(t.clientX,t.clientY)}function H(t){_.set(t.clientX,t.clientY)}function G(t){if(1===A.length)m.set(t.pageX,t.pageY);else{const e=rt(t),n=.5*(t.pageX+e.x),i=.5*(t.pageY+e.y);m.set(n,i)}}function V(t){if(1===A.length)_.set(t.pageX,t.pageY);else{const e=rt(t),n=.5*(t.pageX+e.x),i=.5*(t.pageY+e.y);_.set(n,i)}}function j(t){const e=rt(t),n=t.pageX-e.x,i=t.pageY-e.y,r=Math.sqrt(n*n+i*i);b.set(0,r)}function W(t){if(1==A.length)g.set(t.pageX,t.pageY);else{const e=rt(t),n=.5*(t.pageX+e.x),i=.5*(t.pageY+e.y);g.set(n,i)}v.subVectors(g,m).multiplyScalar(n.rotateSpeed);const e=n.domElement;L(2*Math.PI*v.x/e.clientHeight),O(2*Math.PI*v.y/e.clientHeight),m.copy(g)}function X(t){if(1===A.length)y.set(t.pageX,t.pageY);else{const e=rt(t),n=.5*(t.pageX+e.x),i=.5*(t.pageY+e.y);y.set(n,i)}x.subVectors(y,_).multiplyScalar(n.panSpeed),I(x.x,x.y),_.copy(y)}function q(t){const e=rt(t),i=t.pageX-e.x,r=t.pageY-e.y,a=Math.sqrt(i*i+r*r);M.set(0,a),S.set(0,Math.pow(M.y/b.y,n.zoomSpeed)),U(S.y),b.copy(M);k(.5*(t.pageX+e.x),.5*(t.pageY+e.y))}function Y(t){!1!==n.enabled&&(0===A.length&&(n.domElement.setPointerCapture(t.pointerId),n.domElement.addEventListener("pointermove",$),n.domElement.addEventListener("pointerup",K)),function(t){for(let e=0;e0?U(P(S.y)):S.y<0&&F(P(S.y)),b.copy(M),n.update()}(t);break;case i.PAN:if(!1===n.enablePan)return;!function(t){y.set(t.clientX,t.clientY),x.subVectors(y,_).multiplyScalar(n.panSpeed),I(x.x,x.y),_.copy(y),n.update()}(t)}}(t))}function K(t){switch(function(t){delete R[t.pointerId];for(let e=0;e0&&U(P(t.deltaY)),n.update()}(function(t){const e=t.deltaMode,n={clientX:t.clientX,clientY:t.clientY,deltaY:t.deltaY};switch(e){case 1:n.deltaY*=16;break;case 2:n.deltaY*=100}t.ctrlKey&&!C&&(n.deltaY*=10);return n}(t)),n.dispatchEvent(od))}function J(t){if("Control"===t.key){C=!0;n.domElement.getRootNode().addEventListener("keyup",Q,{passive:!0,capture:!0})}}function Q(t){if("Control"===t.key){C=!1;n.domElement.getRootNode().removeEventListener("keyup",Q,{passive:!0,capture:!0})}}function tt(t){!1!==n.enabled&&!1!==n.enablePan&&function(t){let e=!1;switch(t.code){case n.keys.UP:t.ctrlKey||t.metaKey||t.shiftKey?O(2*Math.PI*n.rotateSpeed/n.domElement.clientHeight):I(0,n.keyPanSpeed),e=!0;break;case n.keys.BOTTOM:t.ctrlKey||t.metaKey||t.shiftKey?O(-2*Math.PI*n.rotateSpeed/n.domElement.clientHeight):I(0,-n.keyPanSpeed),e=!0;break;case n.keys.LEFT:t.ctrlKey||t.metaKey||t.shiftKey?L(2*Math.PI*n.rotateSpeed/n.domElement.clientHeight):I(n.keyPanSpeed,0),e=!0;break;case n.keys.RIGHT:t.ctrlKey||t.metaKey||t.shiftKey?L(-2*Math.PI*n.rotateSpeed/n.domElement.clientHeight):I(-n.keyPanSpeed,0),e=!0}e&&(t.preventDefault(),n.update())}(t)}function et(t){switch(it(t),A.length){case 1:switch(n.touches.ONE){case l:if(!1===n.enableRotate)return;G(t),r=i.TOUCH_ROTATE;break;case c:if(!1===n.enablePan)return;V(t),r=i.TOUCH_PAN;break;default:r=i.NONE}break;case 2:switch(n.touches.TWO){case u:if(!1===n.enableZoom&&!1===n.enablePan)return;!function(t){n.enableZoom&&j(t),n.enablePan&&V(t)}(t),r=i.TOUCH_DOLLY_PAN;break;case h:if(!1===n.enableZoom&&!1===n.enableRotate)return;!function(t){n.enableZoom&&j(t),n.enableRotate&&G(t)}(t),r=i.TOUCH_DOLLY_ROTATE;break;default:r=i.NONE}break;default:r=i.NONE}r!==i.NONE&&n.dispatchEvent(ad)}function nt(t){!1!==n.enabled&&t.preventDefault()}function it(t){let e=R[t.pointerId];void 0===e&&(e=new Ct,R[t.pointerId]=e),e.set(t.pageX,t.pageY)}function rt(t){const e=t.pointerId===A[0]?A[1]:A[0];return R[e]}n.domElement.addEventListener("contextmenu",nt),n.domElement.addEventListener("pointerdown",Y),n.domElement.addEventListener("pointercancel",K),n.domElement.addEventListener("wheel",Z,{passive:!1});n.domElement.getRootNode().addEventListener("keydown",J,{passive:!0,capture:!0}),this.update()}}const hd={type:"change"};class dd extends mt{constructor(t,e){super(),this.object=t,this.domElement=e,this.enabled=!0,this.movementSpeed=1,this.rollSpeed=.005,this.dragToLook=!1,this.autoForward=!1;const n=this,i=1e-6,r=new ee,a=new ne;this.tmpQuaternion=new ee,this.status=0,this.moveState={up:0,down:0,left:0,right:0,forward:0,back:0,pitchUp:0,pitchDown:0,yawLeft:0,yawRight:0,rollLeft:0,rollRight:0},this.moveVector=new ne(0,0,0),this.rotationVector=new ne(0,0,0),this.keydown=function(t){if(!t.altKey&&!1!==this.enabled){switch(t.code){case"ShiftLeft":case"ShiftRight":this.movementSpeedMultiplier=.1;break;case"KeyW":this.moveState.forward=1;break;case"KeyS":this.moveState.back=1;break;case"KeyA":this.moveState.left=1;break;case"KeyD":this.moveState.right=1;break;case"KeyR":this.moveState.up=1;break;case"KeyF":this.moveState.down=1;break;case"ArrowUp":this.moveState.pitchUp=1;break;case"ArrowDown":this.moveState.pitchDown=1;break;case"ArrowLeft":this.moveState.yawLeft=1;break;case"ArrowRight":this.moveState.yawRight=1;break;case"KeyQ":this.moveState.rollLeft=1;break;case"KeyE":this.moveState.rollRight=1}this.updateMovementVector(),this.updateRotationVector()}},this.keyup=function(t){if(!1!==this.enabled){switch(t.code){case"ShiftLeft":case"ShiftRight":this.movementSpeedMultiplier=1;break;case"KeyW":this.moveState.forward=0;break;case"KeyS":this.moveState.back=0;break;case"KeyA":this.moveState.left=0;break;case"KeyD":this.moveState.right=0;break;case"KeyR":this.moveState.up=0;break;case"KeyF":this.moveState.down=0;break;case"ArrowUp":this.moveState.pitchUp=0;break;case"ArrowDown":this.moveState.pitchDown=0;break;case"ArrowLeft":this.moveState.yawLeft=0;break;case"ArrowRight":this.moveState.yawRight=0;break;case"KeyQ":this.moveState.rollLeft=0;break;case"KeyE":this.moveState.rollRight=0}this.updateMovementVector(),this.updateRotationVector()}},this.pointerdown=function(t){if(!1!==this.enabled)if(this.dragToLook)this.status++;else{switch(t.button){case 0:this.moveState.forward=1;break;case 2:this.moveState.back=1}this.updateMovementVector()}},this.pointermove=function(t){if(!1!==this.enabled&&(!this.dragToLook||this.status>0)){const e=this.getContainerDimensions(),n=e.size[0]/2,i=e.size[1]/2;this.moveState.yawLeft=-(t.pageX-e.offset[0]-n)/n,this.moveState.pitchDown=(t.pageY-e.offset[1]-i)/i,this.updateRotationVector()}},this.pointerup=function(t){if(!1!==this.enabled){if(this.dragToLook)this.status--,this.moveState.yawLeft=this.moveState.pitchDown=0;else{switch(t.button){case 0:this.moveState.forward=0;break;case 2:this.moveState.back=0}this.updateMovementVector()}this.updateRotationVector()}},this.pointercancel=function(){!1!==this.enabled&&(this.dragToLook?(this.status=0,this.moveState.yawLeft=this.moveState.pitchDown=0):(this.moveState.forward=0,this.moveState.back=0,this.updateMovementVector()),this.updateRotationVector())},this.contextMenu=function(t){!1!==this.enabled&&t.preventDefault()},this.update=function(t){if(!1===this.enabled)return;const e=t*n.movementSpeed,o=t*n.rollSpeed;n.object.translateX(n.moveVector.x*e),n.object.translateY(n.moveVector.y*e),n.object.translateZ(n.moveVector.z*e),n.tmpQuaternion.set(n.rotationVector.x*o,n.rotationVector.y*o,n.rotationVector.z*o,1).normalize(),n.object.quaternion.multiply(n.tmpQuaternion),(a.distanceToSquared(n.object.position)>i||8*(1-r.dot(n.object.quaternion))>i)&&(n.dispatchEvent(hd),r.copy(n.object.quaternion),a.copy(n.object.position))},this.updateMovementVector=function(){const t=this.moveState.forward||this.autoForward&&!this.moveState.back?1:0;this.moveVector.x=-this.moveState.left+this.moveState.right,this.moveVector.y=-this.moveState.down+this.moveState.up,this.moveVector.z=-t+this.moveState.back},this.updateRotationVector=function(){this.rotationVector.x=-this.moveState.pitchDown+this.moveState.pitchUp,this.rotationVector.y=-this.moveState.yawRight+this.moveState.yawLeft,this.rotationVector.z=-this.moveState.rollRight+this.moveState.rollLeft},this.getContainerDimensions=function(){return this.domElement!=document?{size:[this.domElement.offsetWidth,this.domElement.offsetHeight],offset:[this.domElement.offsetLeft,this.domElement.offsetTop]}:{size:[window.innerWidth,window.innerHeight],offset:[0,0]}},this.dispose=function(){this.domElement.removeEventListener("contextmenu",o),this.domElement.removeEventListener("pointerdown",l),this.domElement.removeEventListener("pointermove",s),this.domElement.removeEventListener("pointerup",c),this.domElement.removeEventListener("pointercancel",u),window.removeEventListener("keydown",h),window.removeEventListener("keyup",d)};const o=this.contextMenu.bind(this),s=this.pointermove.bind(this),l=this.pointerdown.bind(this),c=this.pointerup.bind(this),u=this.pointercancel.bind(this),h=this.keydown.bind(this),d=this.keyup.bind(this);this.domElement.addEventListener("contextmenu",o),this.domElement.addEventListener("pointerdown",l),this.domElement.addEventListener("pointermove",s),this.domElement.addEventListener("pointerup",c),this.domElement.addEventListener("pointercancel",u),window.addEventListener("keydown",h),window.addEventListener("keyup",d),this.updateMovementVector(),this.updateRotationVector()}}const pd={name:"CopyShader",uniforms:{tDiffuse:{value:null},opacity:{value:1}},vertexShader:"\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvUv = uv;\n\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n\n\t\t}",fragmentShader:"\n\n\t\tuniform float opacity;\n\n\t\tuniform sampler2D tDiffuse;\n\n\t\tvarying vec2 vUv;\n\n\t\tvoid main() {\n\n\t\t\tvec4 texel = texture2D( tDiffuse, vUv );\n\t\t\tgl_FragColor = opacity * texel;\n\n\n\t\t}"};class fd{constructor(){this.isPass=!0,this.enabled=!0,this.needsSwap=!0,this.clear=!1,this.renderToScreen=!1}setSize(){}render(){console.error("THREE.Pass: .render() must be implemented in derived pass.")}dispose(){}}const md=new Vi(-1,1,1,-1,0,1);const gd=new class extends Bn{constructor(){super(),this.setAttribute("position",new On([-1,3,0,-1,-1,0,3,-1,0],3)),this.setAttribute("uv",new On([0,2,0,0,2,0],2))}};class vd{constructor(t){this._mesh=new ri(gd,t)}dispose(){this._mesh.geometry.dispose()}render(t){t.render(this._mesh,md)}get material(){return this._mesh.material}set material(t){this._mesh.material=t}}class _d extends fd{constructor(t,e){super(),this.textureID=void 0!==e?e:"tDiffuse",t instanceof hi?(this.uniforms=t.uniforms,this.material=t):t&&(this.uniforms=ui.clone(t.uniforms),this.material=new hi({name:void 0!==t.name?t.name:"unspecified",defines:Object.assign({},t.defines),uniforms:this.uniforms,vertexShader:t.vertexShader,fragmentShader:t.fragmentShader})),this.fsQuad=new vd(this.material)}render(t,e,n){this.uniforms[this.textureID]&&(this.uniforms[this.textureID].value=n.texture),this.fsQuad.material=this.material,this.renderToScreen?(t.setRenderTarget(null),this.fsQuad.render(t)):(t.setRenderTarget(e),this.clear&&t.clear(t.autoClearColor,t.autoClearDepth,t.autoClearStencil),this.fsQuad.render(t))}dispose(){this.material.dispose(),this.fsQuad.dispose()}}class yd extends fd{constructor(t,e){super(),this.scene=t,this.camera=e,this.clear=!0,this.needsSwap=!1,this.inverse=!1}render(t,e,n){const i=t.getContext(),r=t.state;let a,o;r.buffers.color.setMask(!1),r.buffers.depth.setMask(!1),r.buffers.color.setLocked(!0),r.buffers.depth.setLocked(!0),this.inverse?(a=0,o=1):(a=1,o=0),r.buffers.stencil.setTest(!0),r.buffers.stencil.setOp(i.REPLACE,i.REPLACE,i.REPLACE),r.buffers.stencil.setFunc(i.ALWAYS,a,4294967295),r.buffers.stencil.setClear(o),r.buffers.stencil.setLocked(!0),t.setRenderTarget(n),this.clear&&t.clear(),t.render(this.scene,this.camera),t.setRenderTarget(e),this.clear&&t.clear(),t.render(this.scene,this.camera),r.buffers.color.setLocked(!1),r.buffers.depth.setLocked(!1),r.buffers.color.setMask(!0),r.buffers.depth.setMask(!0),r.buffers.stencil.setLocked(!1),r.buffers.stencil.setFunc(i.EQUAL,1,4294967295),r.buffers.stencil.setOp(i.KEEP,i.KEEP,i.KEEP),r.buffers.stencil.setLocked(!0)}}class xd extends fd{constructor(){super(),this.needsSwap=!1}render(t){t.state.buffers.stencil.setLocked(!1),t.state.buffers.stencil.setTest(!1)}}class bd{constructor(t,e){if(this.renderer=t,this._pixelRatio=t.getPixelRatio(),void 0===e){const n=t.getSize(new Ct);this._width=n.width,this._height=n.height,(e=new Jt(this._width*this._pixelRatio,this._height*this._pixelRatio,{type:W})).texture.name="EffectComposer.rt1"}else this._width=e.width,this._height=e.height;this.renderTarget1=e,this.renderTarget2=e.clone(),this.renderTarget2.texture.name="EffectComposer.rt2",this.writeBuffer=this.renderTarget1,this.readBuffer=this.renderTarget2,this.renderToScreen=!0,this.passes=[],this.copyPass=new _d(pd),this.copyPass.material.blending=0,this.clock=new Jo}swapBuffers(){const t=this.readBuffer;this.readBuffer=this.writeBuffer,this.writeBuffer=t}addPass(t){this.passes.push(t),t.setSize(this._width*this._pixelRatio,this._height*this._pixelRatio)}insertPass(t,e){this.passes.splice(e,0,t),t.setSize(this._width*this._pixelRatio,this._height*this._pixelRatio)}removePass(t){const e=this.passes.indexOf(t);-1!==e&&this.passes.splice(e,1)}isLastEnabledPass(t){for(let e=t+1;e1?i-1:0),a=1;a=0&&r<1?(s=a,l=o):r>=1&&r<2?(s=o,l=a):r>=2&&r<3?(l=a,c=o):r>=3&&r<4?(l=o,c=a):r>=4&&r<5?(s=o,c=a):r>=5&&r<6&&(s=a,c=o);var u=n-a/2;return i(s+u,l+u,c+u)}var Nd={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"639",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"};var Id=/^#[a-fA-F0-9]{6}$/,Ud=/^#[a-fA-F0-9]{8}$/,Fd=/^#[a-fA-F0-9]{3}$/,kd=/^#[a-fA-F0-9]{4}$/,zd=/^rgb\(\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*\)$/i,Bd=/^rgb(?:a)?\(\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,)?\s*(\d{1,3})\s*(?:,|\/)\s*([-+]?\d*[.]?\d+[%]?)\s*\)$/i,Hd=/^hsl\(\s*(\d{0,3}[.]?[0-9]+(?:deg)?)\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*\)$/i,Gd=/^hsl(?:a)?\(\s*(\d{0,3}[.]?[0-9]+(?:deg)?)\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,)?\s*(\d{1,3}[.]?[0-9]?)%\s*(?:,|\/)\s*([-+]?\d*[.]?\d+[%]?)\s*\)$/i;function Vd(t){if("string"!=typeof t)throw new Pd(3);var e=function(t){if("string"!=typeof t)return t;var e=t.toLowerCase();return Nd[e]?"#"+Nd[e]:t}(t);if(e.match(Id))return{red:parseInt(""+e[1]+e[2],16),green:parseInt(""+e[3]+e[4],16),blue:parseInt(""+e[5]+e[6],16)};if(e.match(Ud)){var n=parseFloat((parseInt(""+e[7]+e[8],16)/255).toFixed(2));return{red:parseInt(""+e[1]+e[2],16),green:parseInt(""+e[3]+e[4],16),blue:parseInt(""+e[5]+e[6],16),alpha:n}}if(e.match(Fd))return{red:parseInt(""+e[1]+e[1],16),green:parseInt(""+e[2]+e[2],16),blue:parseInt(""+e[3]+e[3],16)};if(e.match(kd)){var i=parseFloat((parseInt(""+e[4]+e[4],16)/255).toFixed(2));return{red:parseInt(""+e[1]+e[1],16),green:parseInt(""+e[2]+e[2],16),blue:parseInt(""+e[3]+e[3],16),alpha:i}}var r=zd.exec(e);if(r)return{red:parseInt(""+r[1],10),green:parseInt(""+r[2],10),blue:parseInt(""+r[3],10)};var a=Bd.exec(e.substring(0,50));if(a)return{red:parseInt(""+a[1],10),green:parseInt(""+a[2],10),blue:parseInt(""+a[3],10),alpha:parseFloat(""+a[4])>1?parseFloat(""+a[4])/100:parseFloat(""+a[4])};var o=Hd.exec(e);if(o){var s="rgb("+Dd(parseInt(""+o[1],10),parseInt(""+o[2],10)/100,parseInt(""+o[3],10)/100)+")",l=zd.exec(s);if(!l)throw new Pd(4,e,s);return{red:parseInt(""+l[1],10),green:parseInt(""+l[2],10),blue:parseInt(""+l[3],10)}}var c=Gd.exec(e.substring(0,50));if(c){var u="rgb("+Dd(parseInt(""+c[1],10),parseInt(""+c[2],10)/100,parseInt(""+c[3],10)/100)+")",h=zd.exec(u);if(!h)throw new Pd(4,e,u);return{red:parseInt(""+h[1],10),green:parseInt(""+h[2],10),blue:parseInt(""+h[3],10),alpha:parseFloat(""+c[4])>1?parseFloat(""+c[4])/100:parseFloat(""+c[4])}}throw new Pd(5)}function jd(t){return function(t){var e,n=t.red/255,i=t.green/255,r=t.blue/255,a=Math.max(n,i,r),o=Math.min(n,i,r),s=(a+o)/2;if(a===o)return void 0!==t.alpha?{hue:0,saturation:0,lightness:s,alpha:t.alpha}:{hue:0,saturation:0,lightness:s};var l=a-o,c=s>.5?l/(2-a-o):l/(a+o);switch(a){case n:e=(i-r)/l+(i=1?Kd(t,e,n):"rgba("+t+","+e+","+n+","+i+")";if("object"==typeof t&&void 0===e&&void 0===n&&void 0===i)return t.alpha>=1?Kd(t.red,t.green,t.blue):"rgba("+t.red+","+t.green+","+t.blue+","+t.alpha+")";throw new Pd(7)}var Jd=function(t){return"number"==typeof t.red&&"number"==typeof t.green&&"number"==typeof t.blue&&("number"!=typeof t.alpha||void 0===t.alpha)},Qd=function(t){return"number"==typeof t.red&&"number"==typeof t.green&&"number"==typeof t.blue&&"number"==typeof t.alpha},tp=function(t){return"number"==typeof t.hue&&"number"==typeof t.saturation&&"number"==typeof t.lightness&&("number"!=typeof t.alpha||void 0===t.alpha)},ep=function(t){return"number"==typeof t.hue&&"number"==typeof t.saturation&&"number"==typeof t.lightness&&"number"==typeof t.alpha};function np(t){if("object"!=typeof t)throw new Pd(8);if(Qd(t))return Zd(t);if(Jd(t))return Kd(t);if(ep(t))return function(t,e,n,i){if("number"==typeof t&&"number"==typeof e&&"number"==typeof n&&"number"==typeof i)return i>=1?$d(t,e,n):"rgba("+Dd(t,e,n)+","+i+")";if("object"==typeof t&&void 0===e&&void 0===n&&void 0===i)return t.alpha>=1?$d(t.hue,t.saturation,t.lightness):"rgba("+Dd(t.hue,t.saturation,t.lightness)+","+t.alpha+")";throw new Pd(2)}(t);if(tp(t))return function(t,e,n){if("number"==typeof t&&"number"==typeof e&&"number"==typeof n)return $d(t,e,n);if("object"==typeof t&&void 0===e&&void 0===n)return $d(t.hue,t.saturation,t.lightness);throw new Pd(1)}(t);throw new Pd(8)}function ip(t,e,n){return function(){var i=n.concat(Array.prototype.slice.call(arguments));return i.length>=e?t.apply(this,i):ip(t,e,i)}}function rp(t){return ip(t,t.length,[])}function ap(t,e,n){return Math.max(t,Math.min(e,n))}rp((function(t,e){if("transparent"===e)return e;var n=jd(e);return np(Sd({},n,{hue:n.hue+parseFloat(t)}))})),rp((function(t,e){if("transparent"===e)return e;var n=jd(e);return np(Sd({},n,{lightness:ap(0,1,n.lightness-parseFloat(t))}))})),rp((function(t,e){if("transparent"===e)return e;var n=jd(e);return np(Sd({},n,{saturation:ap(0,1,n.saturation-parseFloat(t))}))})),rp((function(t,e){if("transparent"===e)return e;var n=jd(e);return np(Sd({},n,{lightness:ap(0,1,n.lightness+parseFloat(t))}))}));var op=rp((function(t,e,n){if("transparent"===e)return n;if("transparent"===n)return e;if(0===t)return n;var i=Vd(e),r=Sd({},i,{alpha:"number"==typeof i.alpha?i.alpha:1}),a=Vd(n),o=Sd({},a,{alpha:"number"==typeof a.alpha?a.alpha:1}),s=r.alpha-o.alpha,l=2*parseFloat(t)-1,c=((l*s==-1?l:l+s)/(1+l*s)+1)/2,u=1-c;return Zd({red:Math.floor(r.red*c+o.red*u),green:Math.floor(r.green*c+o.green*u),blue:Math.floor(r.blue*c+o.blue*u),alpha:r.alpha*parseFloat(t)+o.alpha*(1-parseFloat(t))})})),sp=op;var lp=rp((function(t,e){if("transparent"===e)return e;var n=Vd(e);return Zd(Sd({},n,{alpha:ap(0,1,(100*("number"==typeof n.alpha?n.alpha:1)+100*parseFloat(t))/100)}))}));rp((function(t,e){if("transparent"===e)return e;var n=jd(e);return np(Sd({},n,{saturation:ap(0,1,n.saturation+parseFloat(t))}))})),rp((function(t,e){return"transparent"===e?e:np(Sd({},jd(e),{hue:parseFloat(t)}))})),rp((function(t,e){return"transparent"===e?e:np(Sd({},jd(e),{lightness:parseFloat(t)}))})),rp((function(t,e){return"transparent"===e?e:np(Sd({},jd(e),{saturation:parseFloat(t)}))})),rp((function(t,e){return"transparent"===e?e:sp(parseFloat(t),"rgb(0, 0, 0)",e)})),rp((function(t,e){return"transparent"===e?e:sp(parseFloat(t),"rgb(255, 255, 255)",e)})),rp((function(t,e){if("transparent"===e)return e;var n=Vd(e);return Zd(Sd({},n,{alpha:ap(0,1,+(100*("number"==typeof n.alpha?n.alpha:1)-100*parseFloat(t)).toFixed(2)/100)}))}));var cp=Object.freeze({Linear:Object.freeze({None:function(t){return t},In:function(t){return this.None(t)},Out:function(t){return this.None(t)},InOut:function(t){return this.None(t)}}),Quadratic:Object.freeze({In:function(t){return t*t},Out:function(t){return t*(2-t)},InOut:function(t){return(t*=2)<1?.5*t*t:-.5*(--t*(t-2)-1)}}),Cubic:Object.freeze({In:function(t){return t*t*t},Out:function(t){return--t*t*t+1},InOut:function(t){return(t*=2)<1?.5*t*t*t:.5*((t-=2)*t*t+2)}}),Quartic:Object.freeze({In:function(t){return t*t*t*t},Out:function(t){return 1- --t*t*t*t},InOut:function(t){return(t*=2)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)}}),Quintic:Object.freeze({In:function(t){return t*t*t*t*t},Out:function(t){return--t*t*t*t*t+1},InOut:function(t){return(t*=2)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)}}),Sinusoidal:Object.freeze({In:function(t){return 1-Math.sin((1-t)*Math.PI/2)},Out:function(t){return Math.sin(t*Math.PI/2)},InOut:function(t){return.5*(1-Math.sin(Math.PI*(.5-t)))}}),Exponential:Object.freeze({In:function(t){return 0===t?0:Math.pow(1024,t-1)},Out:function(t){return 1===t?1:1-Math.pow(2,-10*t)},InOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?.5*Math.pow(1024,t-1):.5*(2-Math.pow(2,-10*(t-1)))}}),Circular:Object.freeze({In:function(t){return 1-Math.sqrt(1-t*t)},Out:function(t){return Math.sqrt(1- --t*t)},InOut:function(t){return(t*=2)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)}}),Elastic:Object.freeze({In:function(t){return 0===t?0:1===t?1:-Math.pow(2,10*(t-1))*Math.sin(5*(t-1.1)*Math.PI)},Out:function(t){return 0===t?0:1===t?1:Math.pow(2,-10*t)*Math.sin(5*(t-.1)*Math.PI)+1},InOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?-.5*Math.pow(2,10*(t-1))*Math.sin(5*(t-1.1)*Math.PI):.5*Math.pow(2,-10*(t-1))*Math.sin(5*(t-1.1)*Math.PI)+1}}),Back:Object.freeze({In:function(t){var e=1.70158;return 1===t?1:t*t*((e+1)*t-e)},Out:function(t){var e=1.70158;return 0===t?0:--t*t*((e+1)*t+e)+1},InOut:function(t){var e=2.5949095;return(t*=2)<1?t*t*((e+1)*t-e)*.5:.5*((t-=2)*t*((e+1)*t+e)+2)}}),Bounce:Object.freeze({In:function(t){return 1-cp.Bounce.Out(1-t)},Out:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},InOut:function(t){return t<.5?.5*cp.Bounce.In(2*t):.5*cp.Bounce.Out(2*t-1)+.5}}),generatePow:function(t){return void 0===t&&(t=4),t=(t=t1e4?1e4:t,{In:function(e){return Math.pow(e,t)},Out:function(e){return 1-Math.pow(1-e,t)},InOut:function(e){return e<.5?Math.pow(2*e,t)/2:(1-Math.pow(2-2*e,t))/2+.5}}}}),up=function(){return performance.now()},hp=function(){function t(){this._tweens={},this._tweensAddedDuringUpdate={}}return t.prototype.getAll=function(){var t=this;return Object.keys(this._tweens).map((function(e){return t._tweens[e]}))},t.prototype.removeAll=function(){this._tweens={}},t.prototype.add=function(t){this._tweens[t.getId()]=t,this._tweensAddedDuringUpdate[t.getId()]=t},t.prototype.remove=function(t){delete this._tweens[t.getId()],delete this._tweensAddedDuringUpdate[t.getId()]},t.prototype.update=function(t,e){void 0===t&&(t=up()),void 0===e&&(e=!1);var n=Object.keys(this._tweens);if(0===n.length)return!1;for(;n.length>0;){this._tweensAddedDuringUpdate={};for(var i=0;i1?a(t[n],t[n-1],n-i):a(t[r],t[r+1>n?n:r+1],i-r)},Bezier:function(t,e){for(var n=0,i=t.length-1,r=Math.pow,a=dp.Utils.Bernstein,o=0;o<=i;o++)n+=r(1-e,i-o)*r(e,o)*t[o]*a(i,o);return n},CatmullRom:function(t,e){var n=t.length-1,i=n*e,r=Math.floor(i),a=dp.Utils.CatmullRom;return t[0]===t[n]?(e<0&&(r=Math.floor(i=n*(1+e))),a(t[(r-1+n)%n],t[r],t[(r+1)%n],t[(r+2)%n],i-r)):e<0?t[0]-(a(t[0],t[0],t[1],t[1],-i)-t[0]):e>1?t[n]-(a(t[n],t[n],t[n-1],t[n-1],i-n)-t[n]):a(t[r?r-1:0],t[r],t[n1;i--)n*=i;return t[e]=n,n}}(),CatmullRom:function(t,e,n,i,r){var a=.5*(n-t),o=.5*(i-e),s=r*r;return(2*e-2*n+a+o)*(r*s)+(-3*e+3*n-2*a-o)*s+a*r+e}}},pp=function(){function t(){}return t.nextId=function(){return t._nextId++},t._nextId=0,t}(),fp=new hp,mp=function(){function t(t,e){void 0===e&&(e=fp),this._object=t,this._group=e,this._isPaused=!1,this._pauseStart=0,this._valuesStart={},this._valuesEnd={},this._valuesStartRepeat={},this._duration=1e3,this._isDynamic=!1,this._initialRepeat=0,this._repeat=0,this._yoyo=!1,this._isPlaying=!1,this._reversed=!1,this._delayTime=0,this._startTime=0,this._easingFunction=cp.Linear.None,this._interpolationFunction=dp.Linear,this._chainedTweens=[],this._onStartCallbackFired=!1,this._onEveryStartCallbackFired=!1,this._id=pp.nextId(),this._isChainStopped=!1,this._propertiesAreSetUp=!1,this._goToEnd=!1}return t.prototype.getId=function(){return this._id},t.prototype.isPlaying=function(){return this._isPlaying},t.prototype.isPaused=function(){return this._isPaused},t.prototype.getDuration=function(){return this._duration},t.prototype.to=function(t,e){if(void 0===e&&(e=1e3),this._isPlaying)throw new Error("Can not call Tween.to() while Tween is already started or paused. Stop the Tween first.");return this._valuesEnd=t,this._propertiesAreSetUp=!1,this._duration=e<0?0:e,this},t.prototype.duration=function(t){return void 0===t&&(t=1e3),this._duration=t<0?0:t,this},t.prototype.dynamic=function(t){return void 0===t&&(t=!1),this._isDynamic=t,this},t.prototype.start=function(t,e){if(void 0===t&&(t=up()),void 0===e&&(e=!1),this._isPlaying)return this;if(this._group&&this._group.add(this),this._repeat=this._initialRepeat,this._reversed)for(var n in this._reversed=!1,this._valuesStartRepeat)this._swapEndStartRepeatValues(n),this._valuesStart[n]=this._valuesStartRepeat[n];if(this._isPlaying=!0,this._isPaused=!1,this._onStartCallbackFired=!1,this._onEveryStartCallbackFired=!1,this._isChainStopped=!1,this._startTime=t,this._startTime+=this._delayTime,!this._propertiesAreSetUp||e){if(this._propertiesAreSetUp=!0,!this._isDynamic){var i={};for(var r in this._valuesEnd)i[r]=this._valuesEnd[r];this._valuesEnd=i}this._setupProperties(this._object,this._valuesStart,this._valuesEnd,this._valuesStartRepeat,e)}return this},t.prototype.startFromCurrentValues=function(t){return this.start(t,!0)},t.prototype._setupProperties=function(t,e,n,i,r){for(var a in n){var o=t[a],s=Array.isArray(o),l=s?"array":typeof o,c=!s&&Array.isArray(n[a]);if("undefined"!==l&&"function"!==l){if(c){if(0===(g=n[a]).length)continue;for(var u=[o],h=0,d=g.length;ha)return!1;e&&this.start(t,!0)}if(this._goToEnd=!1,tl)return 1;var t=Math.trunc(o/s),e=o-t*s,n=Math.min(e/r._duration,1);return 0===n&&o===r._duration?1:n}(),u=this._easingFunction(c);if(this._updateProperties(this._object,this._valuesStart,this._valuesEnd,u),this._onUpdateCallback&&this._onUpdateCallback(this._object,c),0===this._duration||o>=this._duration){if(this._repeat>0){var h=Math.min(Math.trunc((o-this._duration)/s)+1,this._repeat);for(i in isFinite(this._repeat)&&(this._repeat-=h),this._valuesStartRepeat)this._yoyo||"string"!=typeof this._valuesEnd[i]||(this._valuesStartRepeat[i]=this._valuesStartRepeat[i]+parseFloat(this._valuesEnd[i])),this._yoyo&&this._swapEndStartRepeatValues(i),this._valuesStart[i]=this._valuesStartRepeat[i];return this._yoyo&&(this._reversed=!this._reversed),this._startTime+=s*h,this._onRepeatCallback&&this._onRepeatCallback(this._object),this._onEveryStartCallbackFired=!1,!0}this._onCompleteCallback&&this._onCompleteCallback(this._object);for(var d=0,p=this._chainedTweens.length;dt.length)&&(e=t.length);for(var n=0,i=new Array(e);n0&&(e.object.backgroundBlurriness=this.backgroundBlurriness),1!==this.backgroundIntensity&&(e.object.backgroundIntensity=this.backgroundIntensity),e.object.backgroundRotation=this.backgroundRotation.toArray(),e.object.environmentRotation=this.environmentRotation.toArray(),e}},PerspectiveCamera:gi,Raycaster:es,SRGBColorSpace:nt,TextureLoader:class extends jo{constructor(t){super(t)}load(t,e,n,i){const r=new $t,a=new Wo(this.manager);return a.setCrossOrigin(this.crossOrigin),a.setPath(this.path),a.load(t,(function(t){r.image=t,r.needsUpdate=!0,void 0!==e&&e(r)}),n,i),r}},Vector2:Ct,Vector3:ne,Box3:ae,Color:Mn,Mesh:ri,SphereGeometry:zo,MeshBasicMaterial:Tn,BackSide:g,EventDispatcher:mt,MOUSE:s,Quaternion:ee,Spherical:rs,Clock:Jo},Ep=mu({props:{width:{default:window.innerWidth,onChange:function(t,e,n){isNaN(t)&&(e.width=n)}},height:{default:window.innerHeight,onChange:function(t,e,n){isNaN(t)&&(e.height=n)}},backgroundColor:{default:"#000011"},backgroundImageUrl:{},onBackgroundImageLoaded:{},showNavInfo:{default:!0},skyRadius:{default:5e4},objects:{default:[]},lights:{default:[]},enablePointerInteraction:{default:!0,onChange:function(t,e){e.hoverObj=null,e.toolTipElem&&(e.toolTipElem.innerHTML="")},triggerUpdate:!1},lineHoverPrecision:{default:1,triggerUpdate:!1},hoverOrderComparator:{default:function(){return-1},triggerUpdate:!1},hoverFilter:{default:function(){return!0},triggerUpdate:!1},tooltipContent:{triggerUpdate:!1},hoverDuringDrag:{default:!1,triggerUpdate:!1},clickAfterDrag:{default:!1,triggerUpdate:!1},onHover:{default:function(){},triggerUpdate:!1},onClick:{default:function(){},triggerUpdate:!1},onRightClick:{triggerUpdate:!1}},methods:{tick:function(t){if(t.initialised){if(t.controls.update&&t.controls.update(t.clock.getDelta()),t.postProcessingComposer?t.postProcessingComposer.render():t.renderer.render(t.scene,t.camera),t.extraRenderers.forEach((function(e){return e.render(t.scene,t.camera)})),t.enablePointerInteraction){var e=null;if(t.hoverDuringDrag||!t.isPointerDragging){var n=this.intersectingObjects(t.pointerPos.x,t.pointerPos.y).filter((function(e){return t.hoverFilter(e.object)})).sort((function(e,n){return t.hoverOrderComparator(e.object,n.object)})),i=n.length?n[0]:null;e=i?i.object:null,t.intersectionPoint=i?i.point:null}e!==t.hoverObj&&(t.onHover(e,t.hoverObj),t.toolTipElem.innerHTML=e&&gu(t.tooltipContent)(e)||"",t.hoverObj=e)}vp()}return this},getPointerPos:function(t){var e=t.pointerPos;return{x:e.x,y:e.y}},cameraPosition:function(t,e,n,i){var r=t.camera;if(e&&t.initialised){var a=e,o=n||{x:0,y:0,z:0};if(i){var s=Object.assign({},r.position),l=h();new mp(s).to(a,i).easing(cp.Quadratic.Out).onUpdate(c).start(),new mp(l).to(o,i/3).easing(cp.Quadratic.Out).onUpdate(u).start()}else c(a),u(o);return this}return Object.assign({},r.position,{lookAt:h()});function c(t){var e=t.x,n=t.y,i=t.z;void 0!==e&&(r.position.x=e),void 0!==n&&(r.position.y=n),void 0!==i&&(r.position.z=i)}function u(e){var n=new Sp.Vector3(e.x,e.y,e.z);t.controls.target?t.controls.target=n:r.lookAt(n)}function h(){return Object.assign(new Sp.Vector3(0,0,-1e3).applyQuaternion(r.quaternion).add(r.position))}},zoomToFit:function(t){for(var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:10,i=arguments.length,r=new Array(i>3?i-3:0),a=3;a2&&void 0!==arguments[2]?arguments[2]:0,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:10,r=t.camera;if(e){var a=new Sp.Vector3(0,0,0),o=2*Math.max.apply(Math,xp(Object.entries(e).map((function(t){var e=yp(t,2),n=e[0],i=e[1];return Math.max.apply(Math,xp(i.map((function(t){return Math.abs(a[n]-t)}))))})))),s=(1-2*i/t.height)*r.fov,l=o/Math.atan(s*Math.PI/180),c=l/r.aspect,u=Math.max(l,c);if(u>0){var h=a.clone().sub(r.position).normalize().multiplyScalar(-u);this.cameraPosition(h,a,n)}}return this},getBbox:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:function(){return!0},n=new Sp.Box3(new Sp.Vector3(0,0,0),new Sp.Vector3(0,0,0)),i=t.objects.filter(e);return i.length?(i.forEach((function(t){return n.expandByObject(t)})),Object.assign.apply(Object,xp(["x","y","z"].map((function(t){return e={},i=t,r=[n.min[t],n.max[t]],(i=_p(i))in e?Object.defineProperty(e,i,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[i]=r,e;var e,i,r}))))):null},getScreenCoords:function(t,e,n,i){var r=new Sp.Vector3(e,n,i);return r.project(this.camera()),{x:(r.x+1)*t.width/2,y:-(r.y-1)*t.height/2}},getSceneCoords:function(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,r=new Sp.Vector2(e/t.width*2-1,-n/t.height*2+1),a=new Sp.Raycaster;return a.setFromCamera(r,t.camera),Object.assign({},a.ray.at(i,new Sp.Vector3))},intersectingObjects:function(t,e,n){var i=new Sp.Vector2(e/t.width*2-1,-n/t.height*2+1),r=new Sp.Raycaster;return r.params.Line.threshold=t.lineHoverPrecision,r.setFromCamera(i,t.camera),r.intersectObjects(t.objects,!0)},renderer:function(t){return t.renderer},scene:function(t){return t.scene},camera:function(t){return t.camera},postProcessingComposer:function(t){return t.postProcessingComposer},controls:function(t){return t.controls},tbControls:function(t){return t.controls}},stateInit:function(){return{scene:new Sp.Scene,camera:new Sp.PerspectiveCamera,clock:new Sp.Clock}},init:function(t,e){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=n.controlType,r=void 0===i?"trackball":i,a=n.rendererConfig,o=void 0===a?{}:a,s=n.extraRenderers,l=void 0===s?[]:s,c=n.waitForLoadComplete,u=void 0===c||c;t.innerHTML="",t.appendChild(e.container=document.createElement("div")),e.container.className="scene-container",e.container.style.position="relative",e.container.appendChild(e.navInfo=document.createElement("div")),e.navInfo.className="scene-nav-info",e.navInfo.textContent={orbit:"Left-click: rotate, Mouse-wheel/middle-click: zoom, Right-click: pan",trackball:"Left-click: rotate, Mouse-wheel/middle-click: zoom, Right-click: pan",fly:"WASD: move, R|F: up | down, Q|E: roll, up|down: pitch, left|right: yaw"}[r]||"",e.navInfo.style.display=e.showNavInfo?null:"none",e.toolTipElem=document.createElement("div"),e.toolTipElem.classList.add("scene-tooltip"),e.container.appendChild(e.toolTipElem),e.pointerPos=new Sp.Vector2,e.pointerPos.x=-2,e.pointerPos.y=-2,["pointermove","pointerdown"].forEach((function(t){return e.container.addEventListener(t,(function(n){if("pointerdown"===t&&(e.isPointerPressed=!0),!e.isPointerDragging&&"pointermove"===n.type&&(n.pressure>0||e.isPointerPressed)&&("touch"!==n.pointerType||void 0===n.movementX||[n.movementX,n.movementY].some((function(t){return Math.abs(t)>1})))&&(e.isPointerDragging=!0),e.enablePointerInteraction){var i=(r=e.container,a=r.getBoundingClientRect(),o=window.pageXOffset||document.documentElement.scrollLeft,s=window.pageYOffset||document.documentElement.scrollTop,{top:a.top+s,left:a.left+o});e.pointerPos.x=n.pageX-i.left,e.pointerPos.y=n.pageY-i.top,e.toolTipElem.style.top="".concat(e.pointerPos.y,"px"),e.toolTipElem.style.left="".concat(e.pointerPos.x,"px"),e.toolTipElem.style.transform="translate(-".concat(e.pointerPos.x/e.width*100,"%, ").concat(e.height-e.pointerPos.y<100?"calc(-100% - 8px)":"21px",")")}var r,a,o,s}),{passive:!0})})),e.container.addEventListener("pointerup",(function(t){e.isPointerPressed=!1,e.isPointerDragging&&(e.isPointerDragging=!1,!e.clickAfterDrag)||requestAnimationFrame((function(){0===t.button&&e.onClick(e.hoverObj||null,t,e.intersectionPoint),2===t.button&&e.onRightClick&&e.onRightClick(e.hoverObj||null,t,e.intersectionPoint)}))}),{passive:!0,capture:!0}),e.container.addEventListener("contextmenu",(function(t){e.onRightClick&&t.preventDefault()})),e.renderer=new Sp.WebGLRenderer(Object.assign({antialias:!0,alpha:!0},o)),e.renderer.setPixelRatio(Math.min(2,window.devicePixelRatio)),e.container.appendChild(e.renderer.domElement),e.extraRenderers=l,e.extraRenderers.forEach((function(t){t.domElement.style.position="absolute",t.domElement.style.top="0px",t.domElement.style.pointerEvents="none",e.container.appendChild(t.domElement)})),e.postProcessingComposer=new bd(e.renderer),e.postProcessingComposer.addPass(new Md(e.scene,e.camera)),e.controls=new{trackball:id,orbit:ud,fly:dd}[r](e.camera,e.renderer.domElement),"fly"===r&&(e.controls.movementSpeed=300,e.controls.rollSpeed=Math.PI/6,e.controls.dragToLook=!0),"trackball"!==r&&"orbit"!==r||(e.controls.minDistance=.1,e.controls.maxDistance=e.skyRadius,e.controls.addEventListener("start",(function(){e.controlsEngaged=!0})),e.controls.addEventListener("change",(function(){e.controlsEngaged&&(e.controlsDragging=!0)})),e.controls.addEventListener("end",(function(){e.controlsEngaged=!1,e.controlsDragging=!1}))),[e.renderer,e.postProcessingComposer].concat(xp(e.extraRenderers)).forEach((function(t){return t.setSize(e.width,e.height)})),e.camera.aspect=e.width/e.height,e.camera.updateProjectionMatrix(),e.camera.position.z=1e3,e.scene.add(e.skysphere=new Sp.Mesh),e.skysphere.visible=!1,e.loadComplete=e.scene.visible=!u,window.scene=e.scene},update:function(t,e){if(t.width&&t.height&&(e.hasOwnProperty("width")||e.hasOwnProperty("height"))&&(t.container.style.width="".concat(t.width,"px"),t.container.style.height="".concat(t.height,"px"),[t.renderer,t.postProcessingComposer].concat(xp(t.extraRenderers)).forEach((function(e){return e.setSize(t.width,t.height)})),t.camera.aspect=t.width/t.height,t.camera.updateProjectionMatrix()),e.hasOwnProperty("skyRadius")&&t.skyRadius&&(t.controls.hasOwnProperty("maxDistance")&&e.skyRadius&&(t.controls.maxDistance=Math.min(t.controls.maxDistance,t.skyRadius)),t.camera.far=2.5*t.skyRadius,t.camera.updateProjectionMatrix(),t.skysphere.geometry=new Sp.SphereGeometry(t.skyRadius)),e.hasOwnProperty("backgroundColor")){var n=Vd(t.backgroundColor).alpha;void 0===n&&(n=1),t.renderer.setClearColor(new Sp.Color(lp(1,t.backgroundColor)),n)}function i(){t.loadComplete=t.scene.visible=!0}e.hasOwnProperty("backgroundImageUrl")&&(t.backgroundImageUrl?(new Sp.TextureLoader).load(t.backgroundImageUrl,(function(e){e.colorSpace=Sp.SRGBColorSpace,t.skysphere.material=new Sp.MeshBasicMaterial({map:e,side:Sp.BackSide}),t.skysphere.visible=!0,t.onBackgroundImageLoaded&&setTimeout(t.onBackgroundImageLoaded),!t.loadComplete&&i()})):(t.skysphere.visible=!1,t.skysphere.material.map=null,!t.loadComplete&&i())),e.hasOwnProperty("showNavInfo")&&(t.navInfo.style.display=t.showNavInfo?null:"none"),e.hasOwnProperty("lights")&&((e.lights||[]).forEach((function(e){return t.scene.remove(e)})),t.lights.forEach((function(e){return t.scene.add(e)}))),e.hasOwnProperty("objects")&&((e.objects||[]).forEach((function(e){return t.scene.remove(e)})),t.objects.forEach((function(e){return t.scene.add(e)})))}});function wp(t,e){var n=new e;return n._destructor&&n._destructor(),{linkProp:function(e){return{default:n[e](),onChange:function(n,i){i[t][e](n)},triggerUpdate:!1}},linkMethod:function(e){return function(n){for(var i=n[t],r=arguments.length,a=new Array(r>1?r-1:0),o=1;o3?r-3:0),o=3;ot.length)&&(n=t.length);for(var e=0,r=new Array(n);e=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),p.hasOwnProperty(n)?{space:p[n],local:t}:t}function y(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===d&&n.documentElement.namespaceURI===d?n.createElement(t):n.createElementNS(e,t)}}function v(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function _(t){var n=g(t);return(n.local?v:y)(n)}function m(){}function x(t){return null==t?m:function(){return this.querySelector(t)}}function b(){return[]}function w(t){return null==t?b:function(){return this.querySelectorAll(t)}}function k(t){return function(){return function(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}(t.apply(this,arguments))}}function M(t){return function(){return this.matches(t)}}function z(t){return function(n){return n.matches(t)}}var A=Array.prototype.find;function S(){return this.firstElementChild}var C=Array.prototype.filter;function E(){return Array.from(this.children)}function O(t){return new Array(t.length)}function N(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function P(t,n,e,r,i,o){for(var a,u=0,s=n.length,l=o.length;un?1:t>=n?0:NaN}function I(t){return function(){this.removeAttribute(t)}}function U(t){return function(){this.removeAttributeNS(t.space,t.local)}}function F(t,n){return function(){this.setAttribute(t,n)}}function L(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function q(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function B(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function $(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function H(t){return function(){this.style.removeProperty(t)}}function V(t,n,e){return function(){this.style.setProperty(t,n,e)}}function X(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function G(t,n){return t.style.getPropertyValue(n)||$(t).getComputedStyle(t,null).getPropertyValue(n)}function Y(t){return function(){delete this[t]}}function W(t,n){return function(){this[t]=n}}function Z(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function Q(t){return t.trim().split(/^|\s+/)}function K(t){return t.classList||new J(t)}function J(t){this._node=t,this._names=Q(t.getAttribute("class")||"")}function tt(t,n){for(var e=K(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var wt=[null];function kt(t,n){this._groups=t,this._parents=n}function Mt(){return new kt([[document.documentElement]],wt)}function zt(t){return"string"==typeof t?new kt([[document.querySelector(t)]],[document.documentElement]):new kt([[t]],wt)}function At(t,n){if(t=function(t){let n;for(;n=t.sourceEvent;)t=n;return t}(t),void 0===n&&(n=t.currentTarget),n){var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();return r.x=t.clientX,r.y=t.clientY,[(r=r.matrixTransform(n.getScreenCTM().inverse())).x,r.y]}if(n.getBoundingClientRect){var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}}return[t.pageX,t.pageY]}kt.prototype=Mt.prototype={constructor:kt,select:function(t){"function"!=typeof t&&(t=x(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i=x&&(x=m+1);!(_=y[x])&&++x=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=D);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?H:"function"==typeof n?X:V)(t,n,null==e?"":e)):G(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?Y:"function"==typeof n?Z:W)(t,n)):this.node()[t]},classed:function(t,n){var e=Q(t+"");if(arguments.length<2){for(var r=K(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}}))}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?_t:vt,r=0;r{}};function Ct(){for(var t,n=0,e=arguments.length,r={};n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}}))),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o()=>t;function Ft(t,{sourceEvent:n,subject:e,target:r,identifier:i,active:o,x:a,y:u,dx:s,dy:l,dispatch:c}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},subject:{value:e,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:i,enumerable:!0,configurable:!0},active:{value:o,enumerable:!0,configurable:!0},x:{value:a,enumerable:!0,configurable:!0},y:{value:u,enumerable:!0,configurable:!0},dx:{value:s,enumerable:!0,configurable:!0},dy:{value:l,enumerable:!0,configurable:!0},_:{value:c}})}function Lt(t){return!t.ctrlKey&&!t.button}function qt(){return this.parentNode}function Bt(t,n){return null==n?{x:t.x,y:t.y}:n}function $t(){return navigator.maxTouchPoints||"ontouchstart"in this}function Ht(t,n,e){t.prototype=n.prototype=e,e.constructor=t}function Vt(t,n){var e=Object.create(t.prototype);for(var r in n)e[r]=n[r];return e}function Xt(){}Ft.prototype.on=function(){var t=this._.on.apply(this._,arguments);return t===this._?this:t};var Gt=.7,Yt=1/Gt,Wt="\\s*([+-]?\\d+)\\s*",Zt="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",Qt="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",Kt=/^#([0-9a-f]{3,8})$/,Jt=new RegExp(`^rgb\\(${Wt},${Wt},${Wt}\\)$`),tn=new RegExp(`^rgb\\(${Qt},${Qt},${Qt}\\)$`),nn=new RegExp(`^rgba\\(${Wt},${Wt},${Wt},${Zt}\\)$`),en=new RegExp(`^rgba\\(${Qt},${Qt},${Qt},${Zt}\\)$`),rn=new RegExp(`^hsl\\(${Zt},${Qt},${Qt}\\)$`),on=new RegExp(`^hsla\\(${Zt},${Qt},${Qt},${Zt}\\)$`),an={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};function un(){return this.rgb().formatHex()}function sn(){return this.rgb().formatRgb()}function ln(t){var n,e;return t=(t+"").trim().toLowerCase(),(n=Kt.exec(t))?(e=n[1].length,n=parseInt(n[1],16),6===e?cn(n):3===e?new dn(n>>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):8===e?hn(n>>24&255,n>>16&255,n>>8&255,(255&n)/255):4===e?hn(n>>12&15|n>>8&240,n>>8&15|n>>4&240,n>>4&15|240&n,((15&n)<<4|15&n)/255):null):(n=Jt.exec(t))?new dn(n[1],n[2],n[3],1):(n=tn.exec(t))?new dn(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=nn.exec(t))?hn(n[1],n[2],n[3],n[4]):(n=en.exec(t))?hn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=rn.exec(t))?mn(n[1],n[2]/100,n[3]/100,1):(n=on.exec(t))?mn(n[1],n[2]/100,n[3]/100,n[4]):an.hasOwnProperty(t)?cn(an[t]):"transparent"===t?new dn(NaN,NaN,NaN,0):null}function cn(t){return new dn(t>>16&255,t>>8&255,255&t,1)}function hn(t,n,e,r){return r<=0&&(t=n=e=NaN),new dn(t,n,e,r)}function fn(t,n,e,r){return 1===arguments.length?((i=t)instanceof Xt||(i=ln(i)),i?new dn((i=i.rgb()).r,i.g,i.b,i.opacity):new dn):new dn(t,n,e,null==r?1:r);var i}function dn(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function pn(){return`#${_n(this.r)}${_n(this.g)}${_n(this.b)}`}function gn(){const t=yn(this.opacity);return`${1===t?"rgb(":"rgba("}${vn(this.r)}, ${vn(this.g)}, ${vn(this.b)}${1===t?")":`, ${t})`}`}function yn(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function vn(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function _n(t){return((t=vn(t))<16?"0":"")+t.toString(16)}function mn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new bn(t,n,e,r)}function xn(t){if(t instanceof bn)return new bn(t.h,t.s,t.l,t.opacity);if(t instanceof Xt||(t=ln(t)),!t)return new bn;if(t instanceof bn)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,s=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&s<1?0:a,new bn(a,u,s,t.opacity)}function bn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function wn(t){return(t=(t||0)%360)<0?t+360:t}function kn(t){return Math.max(0,Math.min(1,t||0))}function Mn(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}Ht(Xt,ln,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:un,formatHex:un,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return xn(this).formatHsl()},formatRgb:sn,toString:sn}),Ht(dn,fn,Vt(Xt,{brighter(t){return t=null==t?Yt:Math.pow(Yt,t),new dn(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?Gt:Math.pow(Gt,t),new dn(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new dn(vn(this.r),vn(this.g),vn(this.b),yn(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:pn,formatHex:pn,formatHex8:function(){return`#${_n(this.r)}${_n(this.g)}${_n(this.b)}${_n(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:gn,toString:gn})),Ht(bn,(function(t,n,e,r){return 1===arguments.length?xn(t):new bn(t,n,e,null==r?1:r)}),Vt(Xt,{brighter(t){return t=null==t?Yt:Math.pow(Yt,t),new bn(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?Gt:Math.pow(Gt,t),new bn(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new dn(Mn(t>=240?t-240:t+120,i,r),Mn(t,i,r),Mn(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new bn(wn(this.h),kn(this.s),kn(this.l),yn(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=yn(this.opacity);return`${1===t?"hsl(":"hsla("}${wn(this.h)}, ${100*kn(this.s)}%, ${100*kn(this.l)}%${1===t?")":`, ${t})`}`}}));var zn=t=>()=>t;function An(t){return 1==(t=+t)?Sn:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):zn(isNaN(n)?e:n)}}function Sn(t,n){var e=n-t;return e?function(t,n){return function(e){return t+e*n}}(t,e):zn(isNaN(t)?n:t)}var Cn=function t(n){var e=An(n);function r(t,n){var r=e((t=fn(t)).r,(n=fn(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=Sn(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function En(t,n){return t=+t,n=+n,function(e){return t*(1-e)+n*e}}var On=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,Nn=new RegExp(On.source,"g");function Pn(t,n){var e,r,i,o=On.lastIndex=Nn.lastIndex=0,a=-1,u=[],s=[];for(t+="",n+="";(e=On.exec(t))&&(r=Nn.exec(n));)(i=r.index)>o&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,s.push({i:a,x:En(e,r)})),o=Nn.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:En(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,s),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:En(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,s),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:En(t,e)},{i:u-2,x:En(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,s),o=a=null,function(t){for(var n,e=-1,r=s.length;++e=0&&n._call.call(void 0,t),n=n._next;--Hn}()}finally{Hn=0,function(){var t,n,e=qn,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:qn=n);Bn=t,oe(r)}(),Wn=0}}function ie(){var t=Qn.now(),n=t-Yn;n>Gn&&(Zn-=n,Yn=t)}function oe(t){Hn||(Vn&&(Vn=clearTimeout(Vn)),t-Wn>24?(t<1/0&&(Vn=setTimeout(re,t-Qn.now()-Zn)),Xn&&(Xn=clearInterval(Xn))):(Xn||(Yn=Qn.now(),Xn=setInterval(ie,Gn)),Hn=1,Kn(re)))}function ae(t,n,e){var r=new ne;return n=null==n?0:+n,r.restart((e=>{r.stop(),t(e+n)}),n,e),r}ne.prototype=ee.prototype={constructor:ne,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Jn():+e)+(null==n?0:+n),this._next||Bn===this||(Bn?Bn._next=this:qn=this,Bn=this),this._call=t,this._time=e,oe()},stop:function(){this._call&&(this._call=null,this._time=1/0,oe())}};var ue=Ct("start","end","cancel","interrupt"),se=[],le=0,ce=1,he=2,fe=3,de=4,pe=5,ge=6;function ye(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(t){e.state=ce,e.timer.restart(a,e.delay,e.time),e.delay<=t&&a(t-e.delay)}function a(o){var l,c,h,f;if(e.state!==ce)return s();for(l in i)if((f=i[l]).name===e.name){if(f.state===fe)return ae(a);f.state===de?(f.state=ge,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[l]):+lle)throw new Error("too late; already scheduled");return e}function _e(t,n){var e=me(t,n);if(e.state>fe)throw new Error("too late; already running");return e}function me(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function xe(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>he&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t}))}(n)?ve:_e;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=g(t),r="transform"===e?Fn:Me;return this.attrTween(t,"function"==typeof n?(e.local?Oe:Ee)(e,r,ke(this,"attr."+t,n)):null==n?(e.local?Ae:ze)(e):(e.local?Ce:Se)(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=g(t);return this.tween(e,(r.local?Ne:Pe)(r,n))},style:function(t,n,e){var r="transform"==(t+="")?Un:Me;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=G(this,t),a=(this.style.removeProperty(t),G(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,Ue(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=G(this,t),u=e(this),s=u+"";return null==u&&(this.style.removeProperty(t),s=u=G(this,t)),a===s?null:a===r&&s===i?o:(i=s,o=n(r=a,u))}}(t,r,ke(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var s=_e(this,t),l=s.on,c=null==s.value[a]?o||(o=Ue(n)):void 0;l===e&&i===c||(r=(e=l).copy()).on(u,i=c),s.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=G(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n.call(this,r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(ke(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var n="text";if(arguments.length<1)return(n=this.tween(n))&&n._value;if(null==t)return this.tween(n,null);if("function"!=typeof t)throw new Error;return this.tween(n,function(t){var n,e;function r(){var r=t.apply(this,arguments);return r!==e&&(n=(e=r)&&function(t){return function(n){this.textContent=t.call(this,n)}}(r)),n}return r._value=t,r}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}(this._id))},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=me(this.node(),e).tween,o=0,a=i.length;o()=>t;function Xe(t,{sourceEvent:n,target:e,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:n,enumerable:!0,configurable:!0},target:{value:e,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function Ge(t,n,e){this.k=t,this.x=n,this.y=e}Ge.prototype={constructor:Ge,scale:function(t){return 1===t?this:new Ge(this.k*t,this.x,this.y)},translate:function(t,n){return 0===t&0===n?this:new Ge(this.k,this.x+this.k*t,this.y+this.k*n)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Ye=new Ge(1,0,0);function We(t){for(;!t.__zoom;)if(!(t=t.parentNode))return Ye;return t.__zoom}function Ze(t){t.stopImmediatePropagation()}function Qe(t){t.preventDefault(),t.stopImmediatePropagation()}function Ke(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Je(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function tr(){return this.__zoom||Ye}function nr(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function er(){return navigator.maxTouchPoints||"ontouchstart"in this}function rr(t,n,e){var r=t.invertX(n[0][0])-e[0][0],i=t.invertX(n[1][0])-e[1][0],o=t.invertY(n[0][1])-e[0][1],a=t.invertY(n[1][1])-e[1][1];return t.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}function ir(){var t,n,e,r=Ke,i=Je,o=rr,a=nr,u=er,s=[0,1/0],l=[[-1/0,-1/0],[1/0,1/0]],c=250,h=$n,f=Ct("start","zoom","end"),d=500,p=150,g=0,y=10;function v(t){t.property("__zoom",tr).on("wheel.zoom",M,{passive:!1}).on("mousedown.zoom",z).on("dblclick.zoom",A).filter(u).on("touchstart.zoom",S).on("touchmove.zoom",C).on("touchend.zoom touchcancel.zoom",E).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(s[0],Math.min(s[1],n)))===t.k?t:new Ge(n,t.x,t.y)}function m(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new Ge(t.k,r,i)}function x(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function b(t,n,e,r){t.on("start.zoom",(function(){w(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){w(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=w(t,o).event(r),u=i.apply(t,o),s=null==e?x(u):"function"==typeof e?e.apply(t,o):e,l=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),c=t.__zoom,f="function"==typeof n?n.apply(t,o):n,d=h(c.invert(s).concat(l/c.k),f.invert(s).concat(l/f.k));return function(t){if(1===t)t=f;else{var n=d(t),e=l/n[2];t=new Ge(e,s[0]-n[0]*e,s[1]-n[1]*e)}a.zoom(null,t)}}))}function w(t,n,e){return!e&&t.__zooming||new k(t,n)}function k(t,n){this.that=t,this.args=n,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,n),this.taps=0}function M(t,...n){if(r.apply(this,arguments)){var e=w(this,n).event(t),i=this.__zoom,u=Math.max(s[0],Math.min(s[1],i.k*Math.pow(2,a.apply(this,arguments)))),c=At(t);if(e.wheel)e.mouse[0][0]===c[0]&&e.mouse[0][1]===c[1]||(e.mouse[1]=i.invert(e.mouse[0]=c)),clearTimeout(e.wheel);else{if(i.k===u)return;e.mouse=[c,i.invert(c)],xe(this),e.start()}Qe(t),e.wheel=setTimeout((function(){e.wheel=null,e.end()}),p),e.zoom("mouse",o(m(_(i,u),e.mouse[0],e.mouse[1]),e.extent,l))}}function z(t,...n){if(!e&&r.apply(this,arguments)){var i=t.currentTarget,a=w(this,n,!0).event(t),u=zt(t.view).on("mousemove.zoom",(function(t){if(Qe(t),!a.moved){var n=t.clientX-c,e=t.clientY-h;a.moved=n*n+e*e>g}a.event(t).zoom("mouse",o(m(a.that.__zoom,a.mouse[0]=At(t,i),a.mouse[1]),a.extent,l))}),!0).on("mouseup.zoom",(function(t){u.on("mousemove.zoom mouseup.zoom",null),It(t.view,a.moved),Qe(t),a.event(t).end()}),!0),s=At(t,i),c=t.clientX,h=t.clientY;Dt(t.view),Ze(t),a.mouse=[s,this.__zoom.invert(s)],xe(this),a.start()}}function A(t,...n){if(r.apply(this,arguments)){var e=this.__zoom,a=At(t.changedTouches?t.changedTouches[0]:t,this),u=e.invert(a),s=e.k*(t.shiftKey?.5:2),h=o(m(_(e,s),a,u),i.apply(this,n),l);Qe(t),c>0?zt(this).transition().duration(c).call(b,h,a,t):zt(this).call(v.transform,h,a,t)}}function S(e,...i){if(r.apply(this,arguments)){var o,a,u,s,l=e.touches,c=l.length,h=w(this,i,e.changedTouches.length===c).event(e);for(Ze(e),a=0;a=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e=i)&&(e=i)}return e}function lr(t,n){let e;if(void 0===n)for(const n of t)null!=n&&(e>n||void 0===e&&n>=n)&&(e=n);else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(e>i||void 0===e&&i>=i)&&(e=i)}return e}var cr="object"==typeof global&&global&&global.Object===Object&&global,hr="object"==typeof self&&self&&self.Object===Object&&self,fr=cr||hr||Function("return this")(),dr=fr.Symbol,pr=Object.prototype,gr=pr.hasOwnProperty,yr=pr.toString,vr=dr?dr.toStringTag:void 0;var _r=Object.prototype.toString;var mr="[object Null]",xr="[object Undefined]",br=dr?dr.toStringTag:void 0;function wr(t){return null==t?void 0===t?xr:mr:br&&br in Object(t)?function(t){var n=gr.call(t,vr),e=t[vr];try{t[vr]=void 0;var r=!0}catch(t){}var i=yr.call(t);return r&&(n?t[vr]=e:delete t[vr]),i}(t):function(t){return _r.call(t)}(t)}var kr="[object Symbol]";var Mr=/\s/;var zr=/^\s+/;function Ar(t){return t?t.slice(0,function(t){for(var n=t.length;n--&&Mr.test(t.charAt(n)););return n}(t)+1).replace(zr,""):t}function Sr(t){var n=typeof t;return null!=t&&("object"==n||"function"==n)}var Cr=NaN,Er=/^[-+]0x[0-9a-f]+$/i,Or=/^0b[01]+$/i,Nr=/^0o[0-7]+$/i,Pr=parseInt;function jr(t){if("number"==typeof t)return t;if(function(t){return"symbol"==typeof t||function(t){return null!=t&&"object"==typeof t}(t)&&wr(t)==kr}(t))return Cr;if(Sr(t)){var n="function"==typeof t.valueOf?t.valueOf():t;t=Sr(n)?n+"":n}if("string"!=typeof t)return 0===t?t:+t;t=Ar(t);var e=Or.test(t);return e||Nr.test(t)?Pr(t.slice(2),e?2:8):Er.test(t)?Cr:+t}var Tr=function(){return fr.Date.now()},Rr="Expected a function",Dr=Math.max,Ir=Math.min;function Ur(t,n,e){var r,i,o,a,u,s,l=0,c=!1,h=!1,f=!0;if("function"!=typeof t)throw new TypeError(Rr);function d(n){var e=r,o=i;return r=i=void 0,l=n,a=t.apply(o,e)}function p(t){var e=t-s;return void 0===s||e>=n||e<0||h&&t-l>=o}function g(){var t=Tr();if(p(t))return y(t);u=setTimeout(g,function(t){var e=n-(t-s);return h?Ir(e,o-(t-l)):e}(t))}function y(t){return u=void 0,f&&r?d(t):(r=i=void 0,a)}function v(){var t=Tr(),e=p(t);if(r=arguments,i=this,s=t,e){if(void 0===u)return function(t){return l=t,u=setTimeout(g,n),c?d(t):a}(s);if(h)return clearTimeout(u),u=setTimeout(g,n),d(s)}return void 0===u&&(u=setTimeout(g,n)),a}return n=jr(n)||0,Sr(e)&&(c=!!e.leading,o=(h="maxWait"in e)?Dr(jr(e.maxWait)||0,n):o,f="trailing"in e?!!e.trailing:f),v.cancel=function(){void 0!==u&&clearTimeout(u),l=0,r=s=i=u=void 0},v.flush=function(){return void 0===u?a:y(Tr())},v}var Fr=Object.freeze({Linear:Object.freeze({None:function(t){return t},In:function(t){return this.None(t)},Out:function(t){return this.None(t)},InOut:function(t){return this.None(t)}}),Quadratic:Object.freeze({In:function(t){return t*t},Out:function(t){return t*(2-t)},InOut:function(t){return(t*=2)<1?.5*t*t:-.5*(--t*(t-2)-1)}}),Cubic:Object.freeze({In:function(t){return t*t*t},Out:function(t){return--t*t*t+1},InOut:function(t){return(t*=2)<1?.5*t*t*t:.5*((t-=2)*t*t+2)}}),Quartic:Object.freeze({In:function(t){return t*t*t*t},Out:function(t){return 1- --t*t*t*t},InOut:function(t){return(t*=2)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2)}}),Quintic:Object.freeze({In:function(t){return t*t*t*t*t},Out:function(t){return--t*t*t*t*t+1},InOut:function(t){return(t*=2)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)}}),Sinusoidal:Object.freeze({In:function(t){return 1-Math.sin((1-t)*Math.PI/2)},Out:function(t){return Math.sin(t*Math.PI/2)},InOut:function(t){return.5*(1-Math.sin(Math.PI*(.5-t)))}}),Exponential:Object.freeze({In:function(t){return 0===t?0:Math.pow(1024,t-1)},Out:function(t){return 1===t?1:1-Math.pow(2,-10*t)},InOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?.5*Math.pow(1024,t-1):.5*(2-Math.pow(2,-10*(t-1)))}}),Circular:Object.freeze({In:function(t){return 1-Math.sqrt(1-t*t)},Out:function(t){return Math.sqrt(1- --t*t)},InOut:function(t){return(t*=2)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)}}),Elastic:Object.freeze({In:function(t){return 0===t?0:1===t?1:-Math.pow(2,10*(t-1))*Math.sin(5*(t-1.1)*Math.PI)},Out:function(t){return 0===t?0:1===t?1:Math.pow(2,-10*t)*Math.sin(5*(t-.1)*Math.PI)+1},InOut:function(t){return 0===t?0:1===t?1:(t*=2)<1?-.5*Math.pow(2,10*(t-1))*Math.sin(5*(t-1.1)*Math.PI):.5*Math.pow(2,-10*(t-1))*Math.sin(5*(t-1.1)*Math.PI)+1}}),Back:Object.freeze({In:function(t){var n=1.70158;return 1===t?1:t*t*((n+1)*t-n)},Out:function(t){var n=1.70158;return 0===t?0:--t*t*((n+1)*t+n)+1},InOut:function(t){var n=2.5949095;return(t*=2)<1?t*t*((n+1)*t-n)*.5:.5*((t-=2)*t*((n+1)*t+n)+2)}}),Bounce:Object.freeze({In:function(t){return 1-Fr.Bounce.Out(1-t)},Out:function(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},InOut:function(t){return t<.5?.5*Fr.Bounce.In(2*t):.5*Fr.Bounce.Out(2*t-1)+.5}}),generatePow:function(t){return void 0===t&&(t=4),t=(t=t1e4?1e4:t,{In:function(n){return Math.pow(n,t)},Out:function(n){return 1-Math.pow(1-n,t)},InOut:function(n){return n<.5?Math.pow(2*n,t)/2:(1-Math.pow(2-2*n,t))/2+.5}}}}),Lr=function(){return performance.now()},qr=function(){function t(){this._tweens={},this._tweensAddedDuringUpdate={}}return t.prototype.getAll=function(){var t=this;return Object.keys(this._tweens).map((function(n){return t._tweens[n]}))},t.prototype.removeAll=function(){this._tweens={}},t.prototype.add=function(t){this._tweens[t.getId()]=t,this._tweensAddedDuringUpdate[t.getId()]=t},t.prototype.remove=function(t){delete this._tweens[t.getId()],delete this._tweensAddedDuringUpdate[t.getId()]},t.prototype.update=function(t,n){void 0===t&&(t=Lr()),void 0===n&&(n=!1);var e=Object.keys(this._tweens);if(0===e.length)return!1;for(;e.length>0;){this._tweensAddedDuringUpdate={};for(var r=0;r1?o(t[e],t[e-1],e-r):o(t[i],t[i+1>e?e:i+1],r-i)},Bezier:function(t,n){for(var e=0,r=t.length-1,i=Math.pow,o=Br.Utils.Bernstein,a=0;a<=r;a++)e+=i(1-n,r-a)*i(n,a)*t[a]*o(r,a);return e},CatmullRom:function(t,n){var e=t.length-1,r=e*n,i=Math.floor(r),o=Br.Utils.CatmullRom;return t[0]===t[e]?(n<0&&(i=Math.floor(r=e*(1+n))),o(t[(i-1+e)%e],t[i],t[(i+1)%e],t[(i+2)%e],r-i)):n<0?t[0]-(o(t[0],t[0],t[1],t[1],-r)-t[0]):n>1?t[e]-(o(t[e],t[e],t[e-1],t[e-1],r-e)-t[e]):o(t[i?i-1:0],t[i],t[e1;r--)e*=r;return t[n]=e,e}}(),CatmullRom:function(t,n,e,r,i){var o=.5*(e-t),a=.5*(r-n),u=i*i;return(2*n-2*e+o+a)*(i*u)+(-3*n+3*e-2*o-a)*u+o*i+n}}},$r=function(){function t(){}return t.nextId=function(){return t._nextId++},t._nextId=0,t}(),Hr=new qr,Vr=function(){function t(t,n){void 0===n&&(n=Hr),this._object=t,this._group=n,this._isPaused=!1,this._pauseStart=0,this._valuesStart={},this._valuesEnd={},this._valuesStartRepeat={},this._duration=1e3,this._isDynamic=!1,this._initialRepeat=0,this._repeat=0,this._yoyo=!1,this._isPlaying=!1,this._reversed=!1,this._delayTime=0,this._startTime=0,this._easingFunction=Fr.Linear.None,this._interpolationFunction=Br.Linear,this._chainedTweens=[],this._onStartCallbackFired=!1,this._onEveryStartCallbackFired=!1,this._id=$r.nextId(),this._isChainStopped=!1,this._propertiesAreSetUp=!1,this._goToEnd=!1}return t.prototype.getId=function(){return this._id},t.prototype.isPlaying=function(){return this._isPlaying},t.prototype.isPaused=function(){return this._isPaused},t.prototype.getDuration=function(){return this._duration},t.prototype.to=function(t,n){if(void 0===n&&(n=1e3),this._isPlaying)throw new Error("Can not call Tween.to() while Tween is already started or paused. Stop the Tween first.");return this._valuesEnd=t,this._propertiesAreSetUp=!1,this._duration=n<0?0:n,this},t.prototype.duration=function(t){return void 0===t&&(t=1e3),this._duration=t<0?0:t,this},t.prototype.dynamic=function(t){return void 0===t&&(t=!1),this._isDynamic=t,this},t.prototype.start=function(t,n){if(void 0===t&&(t=Lr()),void 0===n&&(n=!1),this._isPlaying)return this;if(this._group&&this._group.add(this),this._repeat=this._initialRepeat,this._reversed)for(var e in this._reversed=!1,this._valuesStartRepeat)this._swapEndStartRepeatValues(e),this._valuesStart[e]=this._valuesStartRepeat[e];if(this._isPlaying=!0,this._isPaused=!1,this._onStartCallbackFired=!1,this._onEveryStartCallbackFired=!1,this._isChainStopped=!1,this._startTime=t,this._startTime+=this._delayTime,!this._propertiesAreSetUp||n){if(this._propertiesAreSetUp=!0,!this._isDynamic){var r={};for(var i in this._valuesEnd)r[i]=this._valuesEnd[i];this._valuesEnd=r}this._setupProperties(this._object,this._valuesStart,this._valuesEnd,this._valuesStartRepeat,n)}return this},t.prototype.startFromCurrentValues=function(t){return this.start(t,!0)},t.prototype._setupProperties=function(t,n,e,r,i){for(var o in e){var a=t[o],u=Array.isArray(a),s=u?"array":typeof a,l=!u&&Array.isArray(e[o]);if("undefined"!==s&&"function"!==s){if(l){if(0===(y=e[o]).length)continue;for(var c=[a],h=0,f=y.length;ho)return!1;n&&this.start(t,!0)}if(this._goToEnd=!1,ts)return 1;var t=Math.trunc(a/u),n=a-t*u,e=Math.min(n/i._duration,1);return 0===e&&a===i._duration?1:e}(),c=this._easingFunction(l);if(this._updateProperties(this._object,this._valuesStart,this._valuesEnd,c),this._onUpdateCallback&&this._onUpdateCallback(this._object,l),0===this._duration||a>=this._duration){if(this._repeat>0){var h=Math.min(Math.trunc((a-this._duration)/u)+1,this._repeat);for(r in isFinite(this._repeat)&&(this._repeat-=h),this._valuesStartRepeat)this._yoyo||"string"!=typeof this._valuesEnd[r]||(this._valuesStartRepeat[r]=this._valuesStartRepeat[r]+parseFloat(this._valuesEnd[r])),this._yoyo&&this._swapEndStartRepeatValues(r),this._valuesStart[r]=this._valuesStartRepeat[r];return this._yoyo&&(this._reversed=!this._reversed),this._startTime+=u*h,this._onRepeatCallback&&this._onRepeatCallback(this._object),this._onEveryStartCallbackFired=!1,!0}this._onCompleteCallback&&this._onCompleteCallback(this._object);for(var f=0,d=this._chainedTweens.length;ft.length)&&(n=t.length);for(var e=0,r=new Array(n);e0&&void 0!==arguments[0]?arguments[0]:{},n=Object.assign({},e instanceof Function?e(t):e,{initialised:!1}),r={};function i(n){return o(n,t),u(),i}var o=function(t,e){c.call(i,t,n,e),n.initialised=!0},u=Ur((function(){n.initialised&&(f.call(i,n,r),r={})}),1);return d.forEach((function(t){i[t.name]=function(t){var e=t.name,o=t.triggerUpdate,a=void 0!==o&&o,s=t.onChange,l=void 0===s?function(t,n){}:s,c=t.defaultVal,h=void 0===c?null:c;return function(t){var o=n[e];if(!arguments.length)return o;var s=void 0===t?h:t;return n[e]=s,l.call(i,s,n,o),!r.hasOwnProperty(e)&&(r[e]=o),a&&u(),i}}(t)})),Object.keys(a).forEach((function(t){i[t]=function(){for(var e,r=arguments.length,o=new Array(r),u=0;u1&&(e-=1),e<1/6?t+6*(n-t)*e:e<.5?n:e<2/3?t+(n-t)*(2/3-e)*6:t}if(t=Mi(t,360),n=Mi(n,100),e=Mi(e,100),0===n)r=i=o=e;else{var u=e<.5?e*(1+n):e+n-e*n,s=2*e-u;r=a(s,u,t+1/3),i=a(s,u,t),o=a(s,u,t-1/3)}return{r:255*r,g:255*i,b:255*o}}(t.h,r,o),a=!0,u="hsl"),t.hasOwnProperty("a")&&(e=t.a));var s,l,c;return e=ki(e),{ok:a,format:t.format||u,r:Math.min(255,Math.max(n.r,0)),g:Math.min(255,Math.max(n.g,0)),b:Math.min(255,Math.max(n.b,0)),a:e}}(t);this._originalInput=t,this._r=e.r,this._g=e.g,this._b=e.b,this._a=e.a,this._roundA=Math.round(100*this._a)/100,this._format=n.format||e.format,this._gradientType=n.gradientType,this._r<1&&(this._r=Math.round(this._r)),this._g<1&&(this._g=Math.round(this._g)),this._b<1&&(this._b=Math.round(this._b)),this._ok=e.ok}function oi(t,n,e){t=Mi(t,255),n=Mi(n,255),e=Mi(e,255);var r,i,o=Math.max(t,n,e),a=Math.min(t,n,e),u=(o+a)/2;if(o==a)r=i=0;else{var s=o-a;switch(i=u>.5?s/(2-o-a):s/(o+a),o){case t:r=(n-e)/s+(n>1)+720)%360;--n;)r.h=(r.h+i)%360,o.push(ii(r));return o}function xi(t,n){n=n||6;for(var e=ii(t).toHsv(),r=e.h,i=e.s,o=e.v,a=[],u=1/n;n--;)a.push(ii({h:r,s:i,v:o})),o=(o+u)%1;return a}ii.prototype={isDark:function(){return this.getBrightness()<128},isLight:function(){return!this.isDark()},isValid:function(){return this._ok},getOriginalInput:function(){return this._originalInput},getFormat:function(){return this._format},getAlpha:function(){return this._a},getBrightness:function(){var t=this.toRgb();return(299*t.r+587*t.g+114*t.b)/1e3},getLuminance:function(){var t,n,e,r=this.toRgb();return t=r.r/255,n=r.g/255,e=r.b/255,.2126*(t<=.03928?t/12.92:Math.pow((t+.055)/1.055,2.4))+.7152*(n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4))+.0722*(e<=.03928?e/12.92:Math.pow((e+.055)/1.055,2.4))},setAlpha:function(t){return this._a=ki(t),this._roundA=Math.round(100*this._a)/100,this},toHsv:function(){var t=ai(this._r,this._g,this._b);return{h:360*t.h,s:t.s,v:t.v,a:this._a}},toHsvString:function(){var t=ai(this._r,this._g,this._b),n=Math.round(360*t.h),e=Math.round(100*t.s),r=Math.round(100*t.v);return 1==this._a?"hsv("+n+", "+e+"%, "+r+"%)":"hsva("+n+", "+e+"%, "+r+"%, "+this._roundA+")"},toHsl:function(){var t=oi(this._r,this._g,this._b);return{h:360*t.h,s:t.s,l:t.l,a:this._a}},toHslString:function(){var t=oi(this._r,this._g,this._b),n=Math.round(360*t.h),e=Math.round(100*t.s),r=Math.round(100*t.l);return 1==this._a?"hsl("+n+", "+e+"%, "+r+"%)":"hsla("+n+", "+e+"%, "+r+"%, "+this._roundA+")"},toHex:function(t){return ui(this._r,this._g,this._b,t)},toHexString:function(t){return"#"+this.toHex(t)},toHex8:function(t){return function(t,n,e,r,i){var o=[Si(Math.round(t).toString(16)),Si(Math.round(n).toString(16)),Si(Math.round(e).toString(16)),Si(Ei(r))];if(i&&o[0].charAt(0)==o[0].charAt(1)&&o[1].charAt(0)==o[1].charAt(1)&&o[2].charAt(0)==o[2].charAt(1)&&o[3].charAt(0)==o[3].charAt(1))return o[0].charAt(0)+o[1].charAt(0)+o[2].charAt(0)+o[3].charAt(0);return o.join("")}(this._r,this._g,this._b,this._a,t)},toHex8String:function(t){return"#"+this.toHex8(t)},toRgb:function(){return{r:Math.round(this._r),g:Math.round(this._g),b:Math.round(this._b),a:this._a}},toRgbString:function(){return 1==this._a?"rgb("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+")":"rgba("+Math.round(this._r)+", "+Math.round(this._g)+", "+Math.round(this._b)+", "+this._roundA+")"},toPercentageRgb:function(){return{r:Math.round(100*Mi(this._r,255))+"%",g:Math.round(100*Mi(this._g,255))+"%",b:Math.round(100*Mi(this._b,255))+"%",a:this._a}},toPercentageRgbString:function(){return 1==this._a?"rgb("+Math.round(100*Mi(this._r,255))+"%, "+Math.round(100*Mi(this._g,255))+"%, "+Math.round(100*Mi(this._b,255))+"%)":"rgba("+Math.round(100*Mi(this._r,255))+"%, "+Math.round(100*Mi(this._g,255))+"%, "+Math.round(100*Mi(this._b,255))+"%, "+this._roundA+")"},toName:function(){return 0===this._a?"transparent":!(this._a<1)&&(wi[ui(this._r,this._g,this._b,!0)]||!1)},toFilter:function(t){var n="#"+si(this._r,this._g,this._b,this._a),e=n,r=this._gradientType?"GradientType = 1, ":"";if(t){var i=ii(t);e="#"+si(i._r,i._g,i._b,i._a)}return"progid:DXImageTransform.Microsoft.gradient("+r+"startColorstr="+n+",endColorstr="+e+")"},toString:function(t){var n=!!t;t=t||this._format;var e=!1,r=this._a<1&&this._a>=0;return n||!r||"hex"!==t&&"hex6"!==t&&"hex3"!==t&&"hex4"!==t&&"hex8"!==t&&"name"!==t?("rgb"===t&&(e=this.toRgbString()),"prgb"===t&&(e=this.toPercentageRgbString()),"hex"!==t&&"hex6"!==t||(e=this.toHexString()),"hex3"===t&&(e=this.toHexString(!0)),"hex4"===t&&(e=this.toHex8String(!0)),"hex8"===t&&(e=this.toHex8String()),"name"===t&&(e=this.toName()),"hsl"===t&&(e=this.toHslString()),"hsv"===t&&(e=this.toHsvString()),e||this.toHexString()):"name"===t&&0===this._a?this.toName():this.toRgbString()},clone:function(){return ii(this.toString())},_applyModification:function(t,n){var e=t.apply(null,[this].concat([].slice.call(n)));return this._r=e._r,this._g=e._g,this._b=e._b,this.setAlpha(e._a),this},lighten:function(){return this._applyModification(fi,arguments)},brighten:function(){return this._applyModification(di,arguments)},darken:function(){return this._applyModification(pi,arguments)},desaturate:function(){return this._applyModification(li,arguments)},saturate:function(){return this._applyModification(ci,arguments)},greyscale:function(){return this._applyModification(hi,arguments)},spin:function(){return this._applyModification(gi,arguments)},_applyCombination:function(t,n){return t.apply(null,[this].concat([].slice.call(n)))},analogous:function(){return this._applyCombination(mi,arguments)},complement:function(){return this._applyCombination(yi,arguments)},monochromatic:function(){return this._applyCombination(xi,arguments)},splitcomplement:function(){return this._applyCombination(_i,arguments)},triad:function(){return this._applyCombination(vi,[3])},tetrad:function(){return this._applyCombination(vi,[4])}},ii.fromRatio=function(t,n){if("object"==ni(t)){var e={};for(var r in t)t.hasOwnProperty(r)&&(e[r]="a"===r?t[r]:Ci(t[r]));t=e}return ii(t,n)},ii.equals=function(t,n){return!(!t||!n)&&ii(t).toRgbString()==ii(n).toRgbString()},ii.random=function(){return ii.fromRatio({r:Math.random(),g:Math.random(),b:Math.random()})},ii.mix=function(t,n,e){e=0===e?0:e||50;var r=ii(t).toRgb(),i=ii(n).toRgb(),o=e/100;return ii({r:(i.r-r.r)*o+r.r,g:(i.g-r.g)*o+r.g,b:(i.b-r.b)*o+r.b,a:(i.a-r.a)*o+r.a})}, -// =4.5;break;case"AAlarge":i=o>=3;break;case"AAAsmall":i=o>=7}return i},ii.mostReadable=function(t,n,e){var r,i,o,a,u=null,s=0;i=(e=e||{}).includeFallbackColors,o=e.level,a=e.size;for(var l=0;ls&&(s=r,u=ii(n[l]));return ii.isReadable(t,u,{level:o,size:a})||!i?u:(e.includeFallbackColors=!1,ii.mostReadable(t,["#fff","#000"],e))};var bi=ii.names={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"0ff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000",blanchedalmond:"ffebcd",blue:"00f",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",burntsienna:"ea7e5d",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"0ff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkgrey:"a9a9a9",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkslategrey:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dimgrey:"696969",dodgerblue:"1e90ff",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"f0f",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",grey:"808080",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgray:"d3d3d3",lightgreen:"90ee90",lightgrey:"d3d3d3",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslategray:"789",lightslategrey:"789",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"0f0",limegreen:"32cd32",linen:"faf0e6",magenta:"f0f",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370db",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"db7093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",rebeccapurple:"663399",red:"f00",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",slategrey:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",wheat:"f5deb3",white:"fff",whitesmoke:"f5f5f5",yellow:"ff0",yellowgreen:"9acd32"},wi=ii.hexNames=function(t){var n={};for(var e in t)t.hasOwnProperty(e)&&(n[t[e]]=e);return n}(bi);function ki(t){return t=parseFloat(t),(isNaN(t)||t<0||t>1)&&(t=1),t}function Mi(t,n){(function(t){return"string"==typeof t&&-1!=t.indexOf(".")&&1===parseFloat(t)})(t)&&(t="100%");var e=function(t){return"string"==typeof t&&-1!=t.indexOf("%")}(t);return t=Math.min(n,Math.max(0,parseFloat(t))),e&&(t=parseInt(t*n,10)/100),Math.abs(t-n)<1e-6?1:t%n/parseFloat(n)}function zi(t){return Math.min(1,Math.max(0,t))}function Ai(t){return parseInt(t,16)}function Si(t){return 1==t.length?"0"+t:""+t}function Ci(t){return t<=1&&(t=100*t+"%"),t}function Ei(t){return Math.round(255*parseFloat(t)).toString(16)}function Oi(t){return Ai(t)/255}var Ni,Pi,ji,Ti=(Pi="[\\s|\\(]+("+(Ni="(?:[-\\+]?\\d*\\.\\d+%?)|(?:[-\\+]?\\d+%?)")+")[,|\\s]+("+Ni+")[,|\\s]+("+Ni+")\\s*\\)?",ji="[\\s|\\(]+("+Ni+")[,|\\s]+("+Ni+")[,|\\s]+("+Ni+")[,|\\s]+("+Ni+")\\s*\\)?",{CSS_UNIT:new RegExp(Ni),rgb:new RegExp("rgb"+Pi),rgba:new RegExp("rgba"+ji),hsl:new RegExp("hsl"+Pi),hsla:new RegExp("hsla"+ji),hsv:new RegExp("hsv"+Pi),hsva:new RegExp("hsva"+ji),hex3:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex6:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,hex4:/^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,hex8:/^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/});function Ri(t){return!!Ti.CSS_UNIT.exec(t)}function Di(t,n){for(var e=0;et.length)&&(n=t.length);for(var e=0,r=new Array(n);e0&&void 0!==arguments[0]?arguments[0]:6;!function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")}(this,t),this.csBits=n,this.registry=["__reserved for background__"]}var n,e,r;return n=t,e=[{key:"register",value:function(t){if(this.registry.length>=Math.pow(2,24-this.csBits))return null;var n,e=this.registry.length,r=Li(e,this.csBits),i=(n=e+(r<<24-this.csBits),"#".concat(Math.min(n,Math.pow(2,24)).toString(16).padStart(6,"0")));return this.registry.push(t),i}},{key:"lookup",value:function(t){var n,e,r,i,o="string"==typeof t?(n=ii(t).toRgb(),e=n.r,r=n.g,i=n.b,Fi(e,r,i)):Fi.apply(void 0,Ii(t));if(!o)return null;var a=o&Math.pow(2,24-this.csBits)-1,u=o>>24-this.csBits&Math.pow(2,this.csBits)-1;return Li(a,this.csBits)!==u||a>=this.registry.length?null:this.registry[a]}}],e&&Di(n.prototype,e),r&&Di(n,r),Object.defineProperty(n,"prototype",{writable:!1}),t}();function Bi(t,n,e){var r,i=1;function o(){var o,a,u=r.length,s=0,l=0,c=0;for(o=0;o=(i=(h+f)/2))?h=i:f=i,r=l,!(l=l[u=+a]))return r[u]=c,t;if(n===(o=+t._x.call(null,l.data)))return c.next=l,r?r[u]=c:t._root=c,t;do{r=r?r[u]=new Array(2):t._root=new Array(2),(a=n>=(i=(h+f)/2))?h=i:f=i}while((u=+a)==(s=+(o>=i)));return r[s]=l,r[u]=c,t}function Hi(t,n,e){this.node=t,this.x0=n,this.x1=e}function Vi(t){return t[0]}function Xi(t,n){var e=new Gi(null==n?Vi:n,NaN,NaN);return null==t?e:e.addAll(t)}function Gi(t,n,e){this._x=t,this._x0=n,this._x1=e,this._root=void 0}function Yi(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var Wi=Xi.prototype=Gi.prototype;function Zi(t,n,e,r){if(isNaN(n)||isNaN(e))return t;var i,o,a,u,s,l,c,h,f,d=t._root,p={data:r},g=t._x0,y=t._y0,v=t._x1,_=t._y1;if(!d)return t._root=p,t;for(;d.length;)if((l=n>=(o=(g+v)/2))?g=o:v=o,(c=e>=(a=(y+_)/2))?y=a:_=a,i=d,!(d=d[h=c<<1|l]))return i[h]=p,t;if(u=+t._x.call(null,d.data),s=+t._y.call(null,d.data),n===u&&e===s)return p.next=d,i?i[h]=p:t._root=p,t;do{i=i?i[h]=new Array(4):t._root=new Array(4),(l=n>=(o=(g+v)/2))?g=o:v=o,(c=e>=(a=(y+_)/2))?y=a:_=a}while((h=c<<1|l)==(f=(s>=a)<<1|u>=o));return i[f]=d,i[h]=p,t}function Qi(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Ki(t){return t[0]}function Ji(t){return t[1]}function to(t,n,e){var r=new no(null==n?Ki:n,null==e?Ji:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function no(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function eo(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}Wi.copy=function(){var t,n,e=new Gi(this._x,this._x0,this._x1),r=this._root;if(!r)return e;if(!r.length)return e._root=Yi(r),e;for(t=[{source:r,target:e._root=new Array(2)}];r=t.pop();)for(var i=0;i<2;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(2)}):r.target[i]=Yi(n));return e},Wi.add=function(t){const n=+this._x.call(null,t);return $i(this.cover(n),n,t)},Wi.addAll=function(t){Array.isArray(t)||(t=Array.from(t));const n=t.length,e=new Float64Array(n);let r=1/0,i=-1/0;for(let o,a=0;ai&&(i=o));if(r>i)return this;this.cover(r).cover(i);for(let r=0;rt||t>=e;)switch(i=+(ts||(i=o.x1)=h))&&(o=l[l.length-1],l[l.length-1]=l[l.length-1-a],l[l.length-1-a]=o)}else{var f=Math.abs(t-+this._x.call(null,c.data));f=(a=(h+f)/2))?h=a:f=a,n=c,!(c=c[s=+u]))return this;if(!c.length)break;n[s+1&1]&&(e=n,l=s)}for(;c.data!==t;)if(r=c,!(c=c.next))return this;return(i=c.next)&&delete c.next,r?(i?r.next=i:delete r.next,this):n?(i?n[s]=i:delete n[s],(c=n[0]||n[1])&&c===(n[1]||n[0])&&!c.length&&(e?e[l]=c:this._root=c),this):(this._root=i,this)},Wi.removeAll=function(t){for(var n=0,e=t.length;n=(a=(m+w)/2))?m=a:w=a,(d=e>=(u=(x+k)/2))?x=u:k=u,(p=r>=(s=(b+M)/2))?b=s:M=s,o=v,!(v=v[g=p<<2|d<<1|f]))return o[g]=_,t;if(l=+t._x.call(null,v.data),c=+t._y.call(null,v.data),h=+t._z.call(null,v.data),n===l&&e===c&&r===h)return _.next=v,o?o[g]=_:t._root=_,t;do{o=o?o[g]=new Array(8):t._root=new Array(8),(f=n>=(a=(m+w)/2))?m=a:w=a,(d=e>=(u=(x+k)/2))?x=u:k=u,(p=r>=(s=(b+M)/2))?b=s:M=s}while((g=p<<2|d<<1|f)==(y=(h>=s)<<2|(c>=u)<<1|l>=a));return o[y]=v,o[g]=_,t}function oo(t,n,e,r,i,o,a){this.node=t,this.x0=n,this.y0=e,this.z0=r,this.x1=i,this.y1=o,this.z1=a}function ao(t){return t[0]}function uo(t){return t[1]}function so(t){return t[2]}function lo(t,n,e,r){var i=new co(null==n?ao:n,null==e?uo:e,null==r?so:r,NaN,NaN,NaN,NaN,NaN,NaN);return null==t?i:i.addAll(t)}function co(t,n,e,r,i,o,a,u,s){this._x=t,this._y=n,this._z=e,this._x0=r,this._y0=i,this._z0=o,this._x1=a,this._y1=u,this._z1=s,this._root=void 0}function ho(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}ro.copy=function(){var t,n,e=new no(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=eo(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=eo(n));return e},ro.add=function(t){const n=+this._x.call(null,t),e=+this._y.call(null,t);return Zi(this.cover(n,e),n,e,t)},ro.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),s=1/0,l=1/0,c=-1/0,h=-1/0;for(e=0;ec&&(c=r),ih&&(h=i));if(s>c||l>h)return this;for(this.cover(s,l).cover(c,h),e=0;et||t>=i||r>n||n>=o;)switch(u=(nf||(o=s.y0)>d||(a=s.x1)=v)<<1|t>=y)&&(s=p[p.length-1],p[p.length-1]=p[p.length-1-l],p[p.length-1-l]=s)}else{var _=t-+this._x.call(null,g.data),m=n-+this._y.call(null,g.data),x=_*_+m*m;if(x=(u=(p+y)/2))?p=u:y=u,(c=a>=(s=(g+v)/2))?g=s:v=s,n=d,!(d=d[h=c<<1|l]))return this;if(!d.length)break;(n[h+1&3]||n[h+2&3]||n[h+3&3])&&(e=n,f=h)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[h]=i:delete n[h],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[f]=d:this._root=d),this):(this._root=i,this)},ro.removeAll=function(t){for(var n=0,e=t.length;n1&&(v=f.y+f.vy-c.y-c.vy||go(u)),i>2&&(_=f.z+f.vz-c.z-c.vz||go(u)),y*=d=((d=Math.sqrt(y*y+v*v+_*_))-e[g])/d*r*n[g],v*=d,_*=d,f.vx-=y*(p=a[g]),i>1&&(f.vy-=v*p),i>2&&(f.vz-=_*p),c.vx+=y*(p=1-p),i>1&&(c.vy+=v*p),i>2&&(c.vz+=_*p)}function d(){if(r){var i,u,l=r.length,c=t.length,h=new Map(r.map(((t,n)=>[s(t,n,r),t])));for(i=0,o=new Array(l);i"function"==typeof t))||Math.random,i=n.find((t=>[1,2,3].includes(t)))||2,d()},f.links=function(n){return arguments.length?(t=n,d(),f):t},f.id=function(t){return arguments.length?(s=t,f):s},f.iterations=function(t){return arguments.length?(h=+t,f):h},f.strength=function(t){return arguments.length?(l="function"==typeof t?t:po(+t),p(),f):l},f.distance=function(t){return arguments.length?(c="function"==typeof t?t:po(+t),g(),f):c},f}fo.copy=function(){var t,n,e=new co(this._x,this._y,this._z,this._x0,this._y0,this._z0,this._x1,this._y1,this._z1),r=this._root;if(!r)return e;if(!r.length)return e._root=ho(r),e;for(t=[{source:r,target:e._root=new Array(8)}];r=t.pop();)for(var i=0;i<8;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(8)}):r.target[i]=ho(n));return e},fo.add=function(t){const n=+this._x.call(null,t),e=+this._y.call(null,t),r=+this._z.call(null,t);return io(this.cover(n,e,r),n,e,r,t)},fo.addAll=function(t){Array.isArray(t)||(t=Array.from(t));const n=t.length,e=new Float64Array(n),r=new Float64Array(n),i=new Float64Array(n);let o=1/0,a=1/0,u=1/0,s=-1/0,l=-1/0,c=-1/0;for(let h,f,d,p,g=0;gs&&(s=f),dl&&(l=d),pc&&(c=p));if(o>s||a>l||u>c)return this;this.cover(o,a,u).cover(s,l,c);for(let o=0;ot||t>=a||i>n||n>=u||o>e||e>=s;)switch(c=(ey||(a=h.y0)>v||(u=h.z0)>_||(s=h.x1)=k)<<2|(n>=w)<<1|t>=b)&&(h=m[m.length-1],m[m.length-1]=m[m.length-1-f],m[m.length-1-f]=h)}else{var M=t-+this._x.call(null,x.data),z=n-+this._y.call(null,x.data),A=e-+this._z.call(null,x.data),S=M*M+z*z+A*A;if(S=(s=(v+x)/2))?v=s:x=s,(f=a>=(l=(_+b)/2))?_=l:b=l,(d=u>=(c=(m+w)/2))?m=c:w=c,n=y,!(y=y[p=d<<2|f<<1|h]))return this;if(!y.length)break;(n[p+1&7]||n[p+2&7]||n[p+3&7]||n[p+4&7]||n[p+5&7]||n[p+6&7]||n[p+7&7])&&(e=n,g=p)}for(;y.data!==t;)if(r=y,!(y=y.next))return this;return(i=y.next)&&delete y.next,r?(i?r.next=i:delete r.next,this):n?(i?n[p]=i:delete n[p],(y=n[0]||n[1]||n[2]||n[3]||n[4]||n[5]||n[6]||n[7])&&y===(n[7]||n[6]||n[5]||n[4]||n[3]||n[2]||n[1]||n[0])&&!y.length&&(e?e[g]=y:this._root=y),this):(this._root=i,this)},fo.removeAll=function(t){for(var n=0,e=t.length;n(t=(mo*t+xo)%bo)/bo}();function d(){p(),h.call("tick",e),i1&&(null==c.fy?c.y+=c.vy*=s:(c.y=c.fy,c.vy=0)),r>2&&(null==c.fz?c.z+=c.vz*=s:(c.z=c.fz,c.vz=0));return e}function g(){for(var n,e=0,i=t.length;e1&&isNaN(n.y)||r>2&&isNaN(n.z)){var o=10*(r>2?Math.cbrt(.5+e):r>1?Math.sqrt(.5+e):e),a=e*zo,u=e*Ao;1===r?n.x=o:2===r?(n.x=o*Math.cos(a),n.y=o*Math.sin(a)):(n.x=o*Math.sin(a)*Math.cos(u),n.y=o*Math.cos(a),n.z=o*Math.sin(a)*Math.sin(u))}(isNaN(n.vx)||r>1&&isNaN(n.vy)||r>2&&isNaN(n.vz))&&(n.vx=0,r>1&&(n.vy=0),r>2&&(n.vz=0))}}function y(n){return n.initialize&&n.initialize(t,f,r),n}return null==t&&(t=[]),g(),e={tick:p,restart:function(){return c.restart(d),e},stop:function(){return c.stop(),e},numDimensions:function(t){return arguments.length?(r=Math.min(3,Math.max(1,Math.round(t))),l.forEach(y),e):r},nodes:function(n){return arguments.length?(t=n,g(),l.forEach(y),e):t},alpha:function(t){return arguments.length?(i=+t,e):i},alphaMin:function(t){return arguments.length?(o=+t,e):o},alphaDecay:function(t){return arguments.length?(a=+t,e):+a},alphaTarget:function(t){return arguments.length?(u=+t,e):u},velocityDecay:function(t){return arguments.length?(s=1-t,e):1-s},randomSource:function(t){return arguments.length?(f=t,l.forEach(y),e):f},force:function(t,n){return arguments.length>1?(null==n?l.delete(t):l.set(t,y(n)),e):l.get(t)},find:function(){var n,e,i,o,a,u,s=Array.prototype.slice.call(arguments),l=s.shift()||0,c=(r>1?s.shift():null)||0,h=(r>2?s.shift():null)||0,f=s.shift()||1/0,d=0,p=t.length;for(f*=f,d=0;d1?(h.on(t,n),e):h.on(t)}}}function Co(){var t,n,e,r,i,o,a=po(-30),u=1,s=1/0,l=.81;function c(r){var o,a=t.length,u=(1===n?Xi(t,wo):2===n?to(t,wo,ko):3===n?lo(t,wo,ko,Mo):null).visitAfter(f);for(i=r,o=0;o1&&(t.y=a/c),n>2&&(t.z=u/c)}else{(e=t).x=e.data.x,n>1&&(e.y=e.data.y),n>2&&(e.z=e.data.z);do{l+=o[e.data.index]}while(e=e.next)}t.value=l}function d(t,a,c,h,f){if(!t.value)return!0;var d=[c,h,f][n-1],p=t.x-e.x,g=n>1?t.y-e.y:0,y=n>2?t.z-e.z:0,v=d-a,_=p*p+g*g+y*y;if(v*v/l<_)return _1&&0===g&&(_+=(g=go(r))*g),n>2&&0===y&&(_+=(y=go(r))*y),_1&&(e.vy+=g*t.value*i/_),n>2&&(e.vz+=y*t.value*i/_)),!0;if(!(t.length||_>=s)){(t.data!==e||t.next)&&(0===p&&(_+=(p=go(r))*p),n>1&&0===g&&(_+=(g=go(r))*g),n>2&&0===y&&(_+=(y=go(r))*y),_1&&(e.vy+=g*v),n>2&&(e.vz+=y*v))}while(t=t.next)}}return c.initialize=function(e,...i){t=e,r=i.find((t=>"function"==typeof t))||Math.random,n=i.find((t=>[1,2,3].includes(t)))||2,h()},c.strength=function(t){return arguments.length?(a="function"==typeof t?t:po(+t),h(),c):a},c.distanceMin=function(t){return arguments.length?(u=t*t,c):Math.sqrt(u)},c.distanceMax=function(t){return arguments.length?(s=t*t,c):Math.sqrt(s)},c.theta=function(t){return arguments.length?(l=t*t,c):Math.sqrt(l)},c}const{abs:Eo,cos:Oo,sin:No,acos:Po,atan2:jo,sqrt:To,pow:Ro}=Math;function Do(t){return t<0?-Ro(-t,1/3):Ro(t,1/3)}const Io=Math.PI,Uo=2*Io,Fo=Io/2,Lo=Number.MAX_SAFE_INTEGER||9007199254740991,qo=Number.MIN_SAFE_INTEGER||-9007199254740991,Bo={x:0,y:0,z:0},$o={Tvalues:[-.06405689286260563,.06405689286260563,-.1911188674736163,.1911188674736163,-.3150426796961634,.3150426796961634,-.4337935076260451,.4337935076260451,-.5454214713888396,.5454214713888396,-.6480936519369755,.6480936519369755,-.7401241915785544,.7401241915785544,-.820001985973903,.820001985973903,-.8864155270044011,.8864155270044011,-.9382745520027328,.9382745520027328,-.9747285559713095,.9747285559713095,-.9951872199970213,.9951872199970213],Cvalues:[.12793819534675216,.12793819534675216,.1258374563468283,.1258374563468283,.12167047292780339,.12167047292780339,.1155056680537256,.1155056680537256,.10744427011596563,.10744427011596563,.09761865210411388,.09761865210411388,.08619016153195327,.08619016153195327,.0733464814110803,.0733464814110803,.05929858491543678,.05929858491543678,.04427743881741981,.04427743881741981,.028531388628933663,.028531388628933663,.0123412297999872,.0123412297999872],arcfn:function(t,n){const e=n(t);let r=e.x*e.x+e.y*e.y;return void 0!==e.z&&(r+=e.z*e.z),To(r)},compute:function(t,n,e){if(0===t)return n[0].t=0,n[0];const r=n.length-1;if(1===t)return n[r].t=1,n[r];const i=1-t;let o=n;if(0===r)return n[0].t=t,n[0];if(1===r){const n={x:i*o[0].x+t*o[1].x,y:i*o[0].y+t*o[1].y,t:t};return e&&(n.z=i*o[0].z+t*o[1].z),n}if(r<4){let n,a,u,s=i*i,l=t*t,c=0;2===r?(o=[o[0],o[1],o[2],Bo],n=s,a=i*t*2,u=l):3===r&&(n=s*i,a=s*t*3,u=i*l*3,c=t*l);const h={x:n*o[0].x+a*o[1].x+u*o[2].x+c*o[3].x,y:n*o[0].y+a*o[1].y+u*o[2].y+c*o[3].y,t:t};return e&&(h.z=n*o[0].z+a*o[1].z+u*o[2].z+c*o[3].z),h}const a=JSON.parse(JSON.stringify(n));for(;a.length>1;){for(let n=0;n1;i--,o--){const t=[];for(let e,i=0;io.x.min&&(n=o.x.min),e>o.y.min&&(e=o.y.min),r0&&(a.c1=n,a.c2=r,a.s1=t,a.s2=e,o.push(a))}))})),o},makeshape:function(t,n,e){const r=n.points.length,i=t.points.length,o=$o.makeline(n.points[r-1],t.points[0]),a=$o.makeline(t.points[i-1],n.points[0]),u={startcap:o,forward:t,back:n,endcap:a,bbox:$o.findbbox([o,t,n,a]),intersections:function(t){return $o.shapeintersections(u,u.bbox,t,t.bbox,e)}};return u},getminmax:function(t,n,e){if(!e)return{min:0,max:0};let r,i,o=Lo,a=qo;-1===e.indexOf(0)&&(e=[0].concat(e)),-1===e.indexOf(1)&&e.push(1);for(let u=0,s=e.length;ua&&(a=i[n]);return{min:o,mid:(o+a)/2,max:a,size:a-o}},align:function(t,n){const e=n.p1.x,r=n.p1.y,i=-jo(n.p2.y-r,n.p2.x-e);return t.map((function(t){return{x:(t.x-e)*Oo(i)-(t.y-r)*No(i),y:(t.x-e)*No(i)+(t.y-r)*Oo(i)}}))},roots:function(t,n){n=n||{p1:{x:0,y:0},p2:{x:1,y:0}};const e=t.length-1,r=$o.align(t,n),i=function(t){return 0<=t&&t<=1};if(2===e){const t=r[0].y,n=r[1].y,e=r[2].y,o=t-2*n+e;if(0!==o){const r=-To(n*n-t*e),a=-t+n;return[-(r+a)/o,-(-r+a)/o].filter(i)}return n!==e&&0===o?[(2*n-e)/(2*n-2*e)].filter(i):[]}const o=r[0].y,a=r[1].y,u=r[2].y;let s=3*a-o-3*u+r[3].y,l=3*o-6*a+3*u,c=-3*o+3*a,h=o;if($o.approximately(s,0)){if($o.approximately(l,0))return $o.approximately(c,0)?[]:[-h/c].filter(i);const t=To(c*c-4*l*h),n=2*l;return[(t-c)/n,(-c-t)/n].filter(i)}l/=s,c/=s,h/=s;const f=(3*c-l*l)/3,d=f/3,p=(2*l*l*l-9*l*c+27*h)/27,g=p/2,y=g*g+d*d*d;let v,_,m,x,b;if(y<0){const t=-f/3,n=To(t*t*t),e=-p/(2*n),r=Po(e<-1?-1:e>1?1:e),o=2*Do(n);return m=o*Oo(r/3)-l/3,x=o*Oo((r+Uo)/3)-l/3,b=o*Oo((r+2*Uo)/3)-l/3,[m,x,b].filter(i)}if(0===y)return v=g<0?Do(-g):-Do(g),m=2*v-l/3,x=-v-l/3,[m,x].filter(i);{const t=To(y);return v=Do(-g+t),_=Do(g+t),[v-_-l/3].filter(i)}},droots:function(t){if(3===t.length){const n=t[0],e=t[1],r=t[2],i=n-2*e+r;if(0!==i){const t=-To(e*e-n*r),o=-n+e;return[-(t+o)/i,-(-t+o)/i]}return e!==r&&0===i?[(2*e-r)/(2*(e-r))]:[]}if(2===t.length){const n=t[0],e=t[1];return n!==e?[n/(n-e)]:[]}return[]},curvature:function(t,n,e,r,i){let o,a,u,s,l=0,c=0;const h=$o.compute(t,n),f=$o.compute(t,e),d=h.x*h.x+h.y*h.y;if(r?(o=To(Ro(h.y*f.z-f.y*h.z,2)+Ro(h.z*f.x-f.z*h.x,2)+Ro(h.x*f.y-f.x*h.y,2)),a=Ro(d+h.z*h.z,1.5)):(o=h.x*f.y-h.y*f.x,a=Ro(d,1.5)),0===o||0===a)return{k:0,r:0};if(l=o/a,c=a/o,!i){const i=$o.curvature(t-.001,n,e,r,!0).k,o=$o.curvature(t+.001,n,e,r,!0).k;s=(o-l+(l-i))/2,u=(Eo(o-l)+Eo(l-i))/2}return{k:l,r:c,dk:s,adk:u}},inflections:function(t){if(t.length<4)return[];const n=$o.align(t,{p1:t[0],p2:t.slice(-1)[0]}),e=n[2].x*n[1].y,r=n[3].x*n[1].y,i=n[1].x*n[2].y,o=18*(-3*e+2*r+3*i-n[3].x*n[2].y),a=18*(3*e-r-3*i),u=18*(i-e);if($o.approximately(o,0)){if(!$o.approximately(a,0)){let t=-u/a;if(0<=t&&t<=1)return[t]}return[]}const s=2*o;if($o.approximately(s,0))return[];const l=a*a-4*o*u;if(l<0)return[];const c=Math.sqrt(l);return[(c-a)/s,-(a+c)/s].filter((function(t){return 0<=t&&t<=1}))},bboxoverlap:function(t,n){const e=["x","y"],r=e.length;for(let i,o,a,u,s=0;s=u)return!1;return!0},expandbox:function(t,n){n.x.mint.x.max&&(t.x.max=n.x.max),n.y.max>t.y.max&&(t.y.max=n.y.max),n.z&&n.z.max>t.z.max&&(t.z.max=n.z.max),t.x.mid=(t.x.min+t.x.max)/2,t.y.mid=(t.y.min+t.y.max)/2,t.z&&(t.z.mid=(t.z.min+t.z.max)/2),t.x.size=t.x.max-t.x.min,t.y.size=t.y.max-t.y.min,t.z&&(t.z.size=t.z.max-t.z.min)},pairiteration:function(t,n,e){const r=t.bbox(),i=n.bbox(),o=1e5,a=e||.5;if(r.x.size+r.y.sizek||k>M)&&(w+=Uo),w>M&&(b=M,M=w,w=b)):M4){if(1!==arguments.length)throw new Error("Only new Bezier(point[]) is accepted for 4th and higher order curves");r=!0}}else if(6!==i&&8!==i&&9!==i&&12!==i&&1!==arguments.length)throw new Error("Only new Bezier(point[]) is accepted for 4th and higher order curves");const o=this._3d=!r&&(9===i||12===i)||t&&t[0]&&void 0!==t[0].z,a=this.points=[];for(let t=0,e=o?3:2;tt+Vo(n.y)),0)0}length(){return $o.length(this.derivative.bind(this))}static getABC(t=2,n,e,r,i=.5){const o=$o.projectionratio(i,t),a=1-o,u={x:o*n.x+a*r.x,y:o*n.y+a*r.y},s=$o.abcratio(i,t);return{A:{x:e.x+(e.x-u.x)/s,y:e.y+(e.y-u.y)/s},B:e,C:u,S:n,E:r}}getABC(t,n){n=n||this.get(t);let e=this.points[0],r=this.points[this.order];return Jo.getABC(this.order,e,n,r,t)}getLUT(t){if(this.verify(),t=t||100,this._lut.length===t+1)return this._lut;this._lut=[],t++,this._lut=[];for(let n,e,r=0;r1?1:h,s=this.compute(h),s.t=h,s.d=l,s}get(t){return this.compute(t)}point(t){return this.points[t]}compute(t){return this.ratios?$o.computeWithRatios(t,this.points,this.ratios,this._3d):$o.compute(t,this.points,this._3d,this.ratios)}raise(){const t=this.points,n=[t[0]],e=t.length;for(let r,i,o=1;o1;){e=[];for(let o,a=0,u=n.length-1;a=0&&t<=1})),n=n.concat(t[e].sort($o.numberSort))}.bind(this)),t.values=n.sort($o.numberSort).filter((function(t,e){return n.indexOf(t)===e})),t}bbox(){const t=this.extrema(),n={};return this.dims.forEach(function(e){n[e]=$o.getminmax(this,e,t[e])}.bind(this)),n}overlaps(t){const n=this.bbox(),e=t.bbox();return $o.bboxoverlap(n,e)}offset(t,n){if(void 0!==n){const e=this.get(t),r=this.normal(t),i={c:e,n:r,x:e.x+r.x*n,y:e.y+r.y*n};return this._3d&&(i.z=e.z+r.z*n),i}if(this._linear){const n=this.normal(0),e=this.points.map((function(e){const r={x:e.x+t*n.x,y:e.y+t*n.y};return e.z&&n.z&&(r.z=e.z+t*n.z),r}));return[new Jo(e)]}return this.reduce().map((function(n){return n._linear?n.offset(t)[0]:n.scale(t)}))}simple(){if(3===this.order){const t=$o.angle(this.points[0],this.points[3],this.points[1]),n=$o.angle(this.points[0],this.points[3],this.points[2]);if(t>0&&n<0||t<0&&n>0)return!1}const t=this.normal(0),n=this.normal(1);let e=t.x*n.x+t.y*n.y;return this._3d&&(e+=t.z*n.z),Vo(Zo(e))(1-i/r)*n+i/r*e));return new Jo(this.points.map(((n,e)=>({x:n.x+t.x*i[e],y:n.y+t.y*i[e]}))))}scale(t){const n=this.order;let e=!1;if("function"==typeof t&&(e=t),e&&2===n)return this.raise().scale(e);const r=this.clockwise,i=this.points;if(this._linear)return this.translate(this.normal(0),e?e(0):t,e?e(1):t);const o=e?e(0):t,a=e?e(1):t,u=[this.offset(0,10),this.offset(1,10)],s=[],l=$o.lli4(u[0],u[0].c,u[1],u[1].c);if(!l)throw new Error("cannot scale this curve. Try reducing it first.");return[0,1].forEach((function(t){const e=s[t*n]=$o.copy(i[t*n]);e.x+=(t?a:o)*u[t].n.x,e.y+=(t?a:o)*u[t].n.y})),e?([0,1].forEach((function(o){if(2!==n||!o){var a=i[o+1],u={x:a.x-l.x,y:a.y-l.y},c=e?e((o+1)/n):t;e&&!r&&(c=-c);var h=Qo(u.x*u.x+u.y*u.y);u.x/=h,u.y/=h,s[o+1]={x:a.x+c*u.x,y:a.y+c*u.y}}})),new Jo(s)):([0,1].forEach((t=>{if(2===n&&t)return;const e=s[t*n],r=this.derivative(t),o={x:e.x+r.x,y:e.y+r.y};s[t+1]=$o.lli4(e,o,l,i[t+1])})),new Jo(s))}outline(t,n,e,r){if(n=void 0===n?t:n,this._linear){const i=this.normal(0),o=this.points[0],a=this.points[this.points.length-1];let u,s,l;void 0===e&&(e=t,r=n),u={x:o.x+i.x*t,y:o.y+i.y*t},l={x:a.x+i.x*e,y:a.y+i.y*e},s={x:(u.x+l.x)/2,y:(u.y+l.y)/2};const c=[u,s,l];u={x:o.x-i.x*n,y:o.y-i.y*n},l={x:a.x-i.x*r,y:a.y-i.y*r},s={x:(u.x+l.x)/2,y:(u.y+l.y)/2};const h=[l,s,u],f=$o.makeline(h[2],c[0]),d=$o.makeline(c[2],h[0]),p=[f,new Jo(c),d,new Jo(h)];return new Ho(p)}const i=this.reduce(),o=i.length,a=[];let u,s=[],l=0,c=this.length();const h=void 0!==e&&void 0!==r;function f(t,n,e,r,i){return function(o){const a=r/e,u=(r+i)/e,s=n-t;return $o.map(o,0,1,t+a*s,t+u*s)}}i.forEach((function(i){const o=i.length();h?(a.push(i.scale(f(t,e,c,l,o))),s.push(i.scale(f(-n,-r,c,l,o)))):(a.push(i.scale(t)),s.push(i.scale(-n))),l+=o})),s=s.map((function(t){return u=t.points,u[3]?t.points=[u[3],u[2],u[1],u[0]]:t.points=[u[2],u[1],u[0]],t})).reverse();const d=a[0].points[0],p=a[o-1].points[a[o-1].points.length-1],g=s[o-1].points[s[o-1].points.length-1],y=s[0].points[0],v=$o.makeline(g,d),_=$o.makeline(p,y),m=[v].concat(a).concat([_]).concat(s);return new Ho(m)}outlineshapes(t,n,e){n=n||t;const r=this.outline(t,n).curves,i=[];for(let t=1,n=r.length;t1,o.endcap.virtual=t{var o=this.get(t);return $o.between(o.x,n,r)&&$o.between(o.y,e,i)}))}selfintersects(t){const n=this.reduce(),e=n.length-2,r=[];for(let i,o,a,u=0;u0&&(i=i.concat(n))})),i}arcs(t){return t=t||.5,this._iterate(t,[])}_error(t,n,e,r){const i=(r-e)/4,o=this.get(e+i),a=this.get(r-i),u=$o.dist(t,n),s=$o.dist(t,o),l=$o.dist(t,a);return Vo(s-u)+Vo(l-u)}_iterate(t,n){let e,r=0,i=1;do{e=0,i=1;let o,a,u,s,l,c=this.get(r),h=!1,f=!1,d=i,p=1;do{if(f=h,s=u,d=(r+i)/2,o=this.get(d),a=this.get(i),u=$o.getccenter(c,o,a),u.interval={start:r,end:i},h=this._error(u,c,r,i)<=t,l=f&&!h,l||(p=i),h){if(i>=1){if(u.interval.end=p=1,s=u,i>1){let t={x:u.x+u.r*Yo(u.e),y:u.y+u.r*Wo(u.e)};u.e+=$o.angle({x:u.x,y:u.y},t,this.get(1))}break}i+=(i-r)/2}else i=d}while(!l&&e++<100);if(e>=100)break;s=s||u,n.push(s),r=p}while(i<1);return n}}function ta(t,n){if(null==t)return{};var e,r,i=function(t,n){if(null==t)return{};var e,r,i={},o=Object.keys(t);for(r=0;r=0||(i[e]=t[e]);return i}(t,n);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,e)&&(i[e]=t[e])}return i}function na(t,n){return function(t){if(Array.isArray(t))return t}(t)||function(t,n){var e=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=e){var r,i,o,a,u=[],s=!0,l=!1;try{if(o=(e=e.call(t)).next,0===n){if(Object(e)!==e)return;s=!1}else for(;!(s=(r=o.call(e)).done)&&(u.push(r.value),u.length!==n);s=!0);}catch(t){l=!0,i=t}finally{try{if(!s&&null!=e.return&&(a=e.return(),Object(a)!==a))return}finally{if(l)throw i}}return u}}(t,n)||ra(t,n)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function ea(t){return function(t){if(Array.isArray(t))return ia(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||ra(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function ra(t,n){if(t){if("string"==typeof t)return ia(t,n);var e=Object.prototype.toString.call(t).slice(8,-1);return"Object"===e&&t.constructor&&(e=t.constructor.name),"Map"===e||"Set"===e?Array.from(t):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?ia(t,n):void 0}}function ia(t,n){(null==n||n>t.length)&&(n=t.length);for(var e=0,r=new Array(n);et.cooldownTicks||new Date-t.startTickTime>t.cooldownTime||t.d3AlphaMin>0&&t.forceLayout.alpha()0){var a=Math.atan2(r.y-e.y,r.x-e.x),u=i*n,s={x:(e.x+r.x)/2+u*Math.cos(a-Math.PI/2),y:(e.y+r.y)/2+u*Math.sin(a-Math.PI/2)};t.__controlPoints=[s.x,s.y]}else{var l=70*n;t.__controlPoints=[r.x,r.y-l,r.x+l,r.y]}}));var f=[],d=[],p=h;if(t.linkCanvasObject){var g=[],y=[];h.forEach((function(t){return({before:f,after:d,replace:g}[a(t)]||y).push(t)})),p=[].concat(c(f),d,y),f=f.concat(g)}u.save(),f.forEach((function(n){return t.linkCanvasObject(n,u,t.globalScale)})),u.restore();var v=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],e=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],r=arguments.length>3&&void 0!==arguments[3]&&arguments[3],i=(n instanceof Array?n.length?n:[void 0]:[n]).map((function(t){return{keyAccessor:t,isProp:!(t instanceof Function)}})),o=t.reduce((function(t,n){var r=t,o=n;return i.forEach((function(t,n){var a,u=t.keyAccessor;if(t.isProp){var s=o,l=s[u],c=ta(s,[u].map(oa));a=l,o=c}else a=u(o,n);n+11&&void 0!==arguments[1]?arguments[1]:1;r===i.length?Object.keys(n).forEach((function(t){return n[t]=e(n[t])})):Object.values(n).forEach((function(n){return t(n,r+1)}))}(o);var a=o;return r&&(a=[],function t(n){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[];e.length===i.length?a.push({keys:e,vals:n}):Object.entries(n).forEach((function(n){var r=na(n,2),i=r[0],o=r[1];return t(o,[].concat(ea(e),[i]))}))}(o),n instanceof Array&&0===n.length&&1===a.length&&(a[0].keys=[])),a}(p,[e,r,i]);u.save(),Object.entries(v).forEach((function(n){var e=l(n,2),r=e[0],o=e[1],a=r&&"undefined"!==r?r:"rgba(0,0,0,0.15)";Object.entries(o).forEach((function(n){var e=l(n,2),r=e[0],o=e[1],h=(r||1)/t.globalScale+s;Object.entries(o).forEach((function(t){var n=l(t,2);n[0];var e=n[1],r=i(e[0]);u.beginPath(),e.forEach((function(t){var n=t.source,e=t.target;if(n&&e&&n.hasOwnProperty("x")&&e.hasOwnProperty("x")){u.moveTo(n.x,n.y);var r=t.__controlPoints;r?u[2===r.length?"quadraticCurveTo":"bezierCurveTo"].apply(u,c(r).concat([e.x,e.y])):u.lineTo(e.x,e.y)}})),u.strokeStyle=a,u.lineWidth=h,u.setLineDash(r||[]),u.stroke()}))}))})),u.restore(),u.save(),d.forEach((function(n){return t.linkCanvasObject(n,u,t.globalScale)})),u.restore()}(),!t.isShadow&&(e=ti(t.linkDirectionalArrowLength),r=ti(t.linkDirectionalArrowRelPos),i=ti(t.linkVisibility),o=ti(t.linkDirectionalArrowColor||t.linkColor),a=ti(t.nodeVal),(u=t.ctx).save(),t.graphData.links.filter(i).forEach((function(i){var s=e(i);if(s&&!(s<0)){var l=i.source,h=i.target;if(l&&h&&l.hasOwnProperty("x")&&h.hasOwnProperty("x")){var f=Math.sqrt(Math.max(0,a(l)||1))*t.nodeRelSize,d=Math.sqrt(Math.max(0,a(h)||1))*t.nodeRelSize,p=Math.min(1,Math.max(0,r(i))),g=o(i)||"rgba(0,0,0,0.28)",y=s/1.6/2,v=i.__controlPoints&&n(Jo,[l.x,l.y].concat(c(i.__controlPoints),[h.x,h.y])),_=v?function(t){return v.get(t)}:function(t){return{x:l.x+(h.x-l.x)*t||0,y:l.y+(h.y-l.y)*t||0}},m=v?v.length():Math.sqrt(Math.pow(h.x-l.x,2)+Math.pow(h.y-l.y,2)),x=f+s+(m-f-d-s)*p,b=_(x/m),w=_((x-s)/m),k=_((x-.8*s)/m),M=Math.atan2(b.y-w.y,b.x-w.x)-Math.PI/2;u.beginPath(),u.moveTo(b.x,b.y),u.lineTo(w.x+y*Math.cos(M),w.y+y*Math.sin(M)),u.lineTo(k.x,k.y),u.lineTo(w.x-y*Math.cos(M),w.y-y*Math.sin(M)),u.fillStyle=g,u.fill()}}})),u.restore()),!t.isShadow&&function(){var e=ti(t.linkDirectionalParticles),r=ti(t.linkDirectionalParticleSpeed),i=ti(t.linkDirectionalParticleWidth),o=ti(t.linkVisibility),a=ti(t.linkDirectionalParticleColor||t.linkColor),u=t.ctx;u.save(),t.graphData.links.filter(o).forEach((function(o){var s=e(o);if(o.hasOwnProperty("__photons")&&o.__photons.length){var l=o.source,h=o.target;if(l&&h&&l.hasOwnProperty("x")&&h.hasOwnProperty("x")){var f=r(o),d=o.__photons||[],p=Math.max(0,i(o)/2)/Math.sqrt(t.globalScale),g=a(o)||"rgba(0,0,0,0.28)";u.fillStyle=g;var y=o.__controlPoints?n(Jo,[l.x,l.y].concat(c(o.__controlPoints),[h.x,h.y])):null,v=0,_=!1;d.forEach((function(t){var n=!!t.__singleHop;if(t.hasOwnProperty("__progressRatio")||(t.__progressRatio=n?0:v/s),!n&&v++,t.__progressRatio+=f,t.__progressRatio>=1){if(n)return void(_=!0);t.__progressRatio=t.__progressRatio%1}var e=t.__progressRatio,r=y?y.get(e):{x:l.x+(h.x-l.x)*e||0,y:l.y+(h.y-l.y)*e||0};u.beginPath(),u.arc(r.x,r.y,p,0,2*Math.PI,!1),u.fill()})),_&&(o.__photons=o.__photons.filter((function(t){return!t.__singleHop||t.__progressRatio<=1})))}}})),u.restore()}(),function(){var n=ti(t.nodeVisibility),e=ti(t.nodeVal),r=ti(t.nodeColor),i=ti(t.nodeCanvasObjectMode),o=t.ctx,a=t.isShadow/t.globalScale,u=t.graphData.nodes.filter(n);o.save(),u.forEach((function(n){var u=i(n);if(!t.nodeCanvasObject||"before"!==u&&"replace"!==u||(t.nodeCanvasObject(n,o,t.globalScale),"replace"!==u)){var s=Math.sqrt(Math.max(0,e(n)||1))*t.nodeRelSize+a;o.beginPath(),o.arc(n.x,n.y,s,0,2*Math.PI,!1),o.fillStyle=r(n)||"rgba(31, 120, 180, 0.92)",o.fill(),t.nodeCanvasObject&&"after"===u&&t.nodeCanvasObject(n,t.ctx,t.globalScale)}else o.restore()})),o.restore()}(),this},emitParticle:function(t,n){return n&&(!n.__photons&&(n.__photons=[]),n.__photons.push({__singleHop:!0})),this}},stateInit:function(){return{forceLayout:So().force("link",_o()).force("charge",Co()).force("center",Bi()).force("dagRadial",null).stop(),engineRunning:!1}},init:function(t,n){n.ctx=t},update:function(t){t.engineRunning=!1,t.onUpdate(),null!==t.nodeAutoColorBy&&ca(t.graphData.nodes,ti(t.nodeAutoColorBy),t.nodeColor),null!==t.linkAutoColorBy&&ca(t.graphData.links,ti(t.linkAutoColorBy),t.linkColor),t.graphData.links.forEach((function(n){n.source=n[t.linkSource],n.target=n[t.linkTarget]})),t.forceLayout.stop().alpha(1).nodes(t.graphData.nodes);var n=t.forceLayout.force("link");n&&n.id((function(n){return n[t.nodeId]})).links(t.graphData.links);var e=t.dagMode&&function(t,n){var e=t.nodes,r=t.links,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=i.nodeFilter,s=void 0===o?function(){return!0}:o,h=i.onLoopError,f=void 0===h?function(t){throw"Invalid DAG structure! Found cycle in node path: ".concat(t.join(" -> "),".")}:h,d={};e.forEach((function(t){return d[n(t)]={data:t,out:[],depth:-1,skip:!s(t)}})),r.forEach((function(t){var e=t.source,r=t.target,i=l(e),o=l(r);if(!d.hasOwnProperty(i))throw"Missing source node with id: ".concat(i);if(!d.hasOwnProperty(o))throw"Missing target node with id: ".concat(o);var u=d[i],s=d[o];function l(t){return"object"===a(t)?n(t):t}u.out.push(s)}));var p=[];return function t(e){for(var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,o=function(){var o=e[a];if(-1!==r.indexOf(o)){var u=[].concat(c(r.slice(r.indexOf(o))),[o]).map((function(t){return n(t.data)}));return p.some((function(t){return t.length===u.length&&t.every((function(t,n){return t===u[n]}))}))||(p.push(u),f(u)),1}i>o.depth&&(o.depth=i,t(o.out,[].concat(c(r),[o]),i+(o.skip?0:1)))},a=0,u=e.length;a1&&(c.vy+=f*g),o>2&&(c.vz+=d*g)}}function c(){if(i){var n,e=i.length;for(a=new Array(e),u=new Array(e),n=0;n[1,2,3].includes(t)))||2,c()},l.strength=function(t){return arguments.length?(s="function"==typeof t?t:po(+t),c(),l):s},l.radius=function(n){return arguments.length?(t="function"==typeof n?n:po(+n),c(),l):t},l.x=function(t){return arguments.length?(n=+t,l):n},l.y=function(t){return arguments.length?(e=+t,l):e},l.z=function(t){return arguments.length?(r=+t,l):r},l}((function(n){var o=e[n[t.nodeId]]||-1;return("radialin"===t.dagMode?r-o:o)*i})).strength((function(n){return t.dagNodeFilter(n)?1:0})):null);for(var f=0;f0&&t.forceLayout.alpha()1?r-1:0),o=1;o1&&void 0!==arguments[1]?arguments[1]:0,e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:10,r=arguments.length,i=new Array(r>3?r-3:0),o=3;o1&&void 0!==arguments[1]?arguments[1]:function(){return!0},e=ti(t.nodeVal),r=function(n){return Math.sqrt(Math.max(0,e(n)||1))*t.nodeRelSize},i=t.graphData.nodes.filter(n).map((function(t){return{x:t.x,y:t.y,r:r(t)}}));return i.length?{x:[lr(i,(function(t){return t.x-t.r})),sr(i,(function(t){return t.x+t.r}))],y:[lr(i,(function(t){return t.y-t.r})),sr(i,(function(t){return t.y+t.r}))]}:null},pauseAnimation:function(t){return t.animationFrameRequestId&&(cancelAnimationFrame(t.animationFrameRequestId),t.animationFrameRequestId=null),this},resumeAnimation:function(t){return t.animationFrameRequestId||this._animationCycle(),this},_destructor:function(){this.pauseAnimation(),this.graphData({nodes:[],links:[]})}},_a),stateInit:function(){return{lastSetZoom:1,zoom:ir(),forceGraph:new da,shadowGraph:(new da).cooldownTicks(0).nodeColor("__indexColor").linkColor("__indexColor").isShadow(!0),colorTracker:new qi}},init:function(t,n){var e=this;t.innerHTML="";var r=document.createElement("div");r.classList.add("force-graph-container"),r.style.position="relative",t.appendChild(r),n.canvas=document.createElement("canvas"),n.backgroundColor&&(n.canvas.style.background=n.backgroundColor),r.appendChild(n.canvas),n.shadowCanvas=document.createElement("canvas");var o=n.canvas.getContext("2d"),a=n.shadowCanvas.getContext("2d",{willReadFrequently:!0}),u={x:-1e12,y:-1e12},s=function(){var t=null,e=window.devicePixelRatio,r=u.x>0&&u.y>0?a.getImageData(u.x*e,u.y*e,1,1):null;return r&&(t=n.colorTracker.lookup(r.data)),t};zt(n.canvas).call(function(){var t,n,e,r,i=Lt,o=qt,a=Bt,u=$t,s={},l=Ct("start","drag","end"),c=0,h=0;function f(t){t.on("mousedown.drag",d).filter(u).on("touchstart.drag",y).on("touchmove.drag",v,Pt).on("touchend.drag touchcancel.drag",_).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function d(a,u){if(!r&&i.call(this,a,u)){var s=m(this,o.call(this,a,u),a,u,"mouse");s&&(zt(a.view).on("mousemove.drag",p,jt).on("mouseup.drag",g,jt),Dt(a.view),Tt(a),e=!1,t=a.clientX,n=a.clientY,s("start",a))}}function p(r){if(Rt(r),!e){var i=r.clientX-t,o=r.clientY-n;e=i*i+o*o>h}s.mouse("drag",r)}function g(t){zt(t.view).on("mousemove.drag mouseup.drag",null),It(t.view,e),Rt(t),s.mouse("end",t)}function y(t,n){if(i.call(this,t,n)){var e,r,a=t.changedTouches,u=o.call(this,t,n),s=a.length;for(e=0;e0||n.isPointerPressed)&&("touch"!==e.pointerType||void 0===e.movementX||[e.movementX,e.movementY].some((function(t){return Math.abs(t)>1})))&&(n.isPointerDragging=!0);var i,o,a,s=(i=r.getBoundingClientRect(),o=window.pageXOffset||document.documentElement.scrollLeft,a=window.pageYOffset||document.documentElement.scrollTop,{top:i.top+a,left:i.left+o});u.x=e.pageX-s.left,u.y=e.pageY-s.top,l.style.top="".concat(u.y,"px"),l.style.left="".concat(u.x,"px"),l.style.transform="translate(-".concat(u.x/n.width*100,"%, ").concat(n.height-u.y<100?"calc(-100% - 8px)":"21px",")")}),{passive:!0})})),r.addEventListener("pointerup",(function(t){if(n.isPointerPressed=!1,n.isPointerDragging)n.isPointerDragging=!1;else{var e=[t,n.pointerDownEvent];requestAnimationFrame((function(){if(0===t.button)if(n.hoverObj){var r=n["on".concat(n.hoverObj.type,"Click")];r&&r.apply(void 0,[n.hoverObj.d].concat(e))}else n.onBackgroundClick&&n.onBackgroundClick.apply(n,e);if(2===t.button)if(n.hoverObj){var i=n["on".concat(n.hoverObj.type,"RightClick")];i&&i.apply(void 0,[n.hoverObj.d].concat(e))}else n.onBackgroundRightClick&&n.onBackgroundRightClick.apply(n,e)}))}}),{passive:!0}),r.addEventListener("contextmenu",(function(t){return!(n.onBackgroundRightClick||n.onNodeRightClick||n.onLinkRightClick)||(t.preventDefault(),!1)})),n.forceGraph(o),n.shadowGraph(a);var c=function(t,n,e){var r=!0,i=!0;if("function"!=typeof t)throw new TypeError("Expected a function");return Sr(e)&&(r="leading"in e?!!e.leading:r,i="trailing"in e?!!e.trailing:i),Ur(t,n,{leading:r,maxWait:n,trailing:i})}((function(){ba(a,n.width,n.height),n.shadowGraph.linkWidth((function(t){return ti(n.linkWidth)(t)+n.linkHoverPrecision}));var t=We(n.canvas);n.shadowGraph.globalScale(t.k).tickFrame()}),800);n.flushShadowCanvas=c.flush,(this._animationCycle=function t(){var e=!n.autoPauseRedraw||!!n.needsRedraw||n.forceGraph.isEngineRunning()||n.graphData.links.some((function(t){return t.__photons&&t.__photons.length}));if(n.needsRedraw=!1,n.enablePointerInteraction){var r=n.isPointerDragging?null:s();if(r!==n.hoverObj){var i=n.hoverObj,a=i?i.type:null,u=r?r.type:null;if(a&&a!==u){var h=n["on".concat(a,"Hover")];h&&h(null,i.d)}if(u){var f=n["on".concat(u,"Hover")];f&&f(r.d,a===u?i.d:null)}var d=r&&ti(n["".concat(r.type.toLowerCase(),"Label")])(r.d)||"";l.style.visibility=d?"visible":"hidden",l.innerHTML=d,n.canvas.classList[r&&n["on".concat(u,"Click")]||!r&&n.onBackgroundClick?"add":"remove"]("clickable"),n.hoverObj=r}e&&c()}if(e){ba(o,n.width,n.height);var p=We(n.canvas).k;n.onRenderFramePre&&n.onRenderFramePre(o,p),n.forceGraph.globalScale(p).tickFrame(),n.onRenderFramePost&&n.onRenderFramePost(o,p)}Gr(),n.animationFrameRequestId=requestAnimationFrame(t)})()},update:function(t){}});return wa})); diff --git a/crates/codegraph-viz/src/api.rs b/crates/codegraph-viz/src/api.rs deleted file mode 100644 index f413d9d4a..000000000 --- a/crates/codegraph-viz/src/api.rs +++ /dev/null @@ -1,227 +0,0 @@ -use axum::{ - extract::{Path, Query, State}, - http::StatusCode, - response::{IntoResponse, Response}, - Json, -}; -use codegraph_api::GraphApi; -use codegraph_core::Symbol; -use codegraph_graph::SharedGraphIndex; -use serde::Deserialize; -use serde_json::json; -use std::collections::HashMap; -use std::sync::Arc; - -#[derive(Clone)] -pub struct AppState { - pub shared_index: Arc, - pub boot_json: String, -} - -#[derive(Deserialize)] -pub struct SearchParams { - pub q: String, - #[serde(default = "default_search_limit")] - pub limit: u32, -} - -fn default_search_limit() -> u32 { - 20 -} - -#[derive(Deserialize)] -pub struct SubgraphParams { - pub seed: Option, - pub query: Option, - #[serde(default = "default_depth")] - pub depth: u32, - pub limit: Option, -} - -fn default_depth() -> u32 { - 2 -} - -#[derive(Deserialize)] -pub struct DepthParams { - #[serde(default = "default_depth")] - pub depth: u32, -} - -#[derive(Deserialize)] -pub struct SearchFlowParams { - pub pattern: String, -} - -#[derive(Deserialize)] -pub struct FilesParams { - pub prefix: Option, -} - -pub async fn status(State(state): State) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - Json(api.stats().await) -} - -pub async fn search( - State(state): State, - Query(params): Query, -) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - match api.search(¶ms.q, params.limit).await { - Ok(hits) => Json(hits).into_response(), - Err(e) => api_error(e), - } -} - -pub async fn symbol(State(state): State, Path(id): Path) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - match api.symbol_by_id(id).await { - Some(s) => Json(s).into_response(), - None => ( - StatusCode::NOT_FOUND, - Json(json!({ "error": "symbol not found" })), - ) - .into_response(), - } -} - -pub async fn flow(State(state): State, Path(id): Path) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - match api.flow(id).await { - Ok(f) => Json(f).into_response(), - Err(e) => api_error(e), - } -} - -pub async fn search_flow( - State(state): State, - Query(params): Query, -) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - match api.search_flow_pattern(¶ms.pattern).await { - Ok(hits) => Json(hits).into_response(), - Err(e) => api_error(e), - } -} - -pub async fn callers( - State(state): State, - Path(id): Path, - Query(params): Query, -) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - match api.callers(id, params.depth).await { - Ok(hits) => Json(hits).into_response(), - Err(e) => api_error(e), - } -} - -pub async fn callees(State(state): State, Path(id): Path) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - match api.callees(id).await { - Ok(hits) => Json(hits).into_response(), - Err(e) => api_error(e), - } -} - -pub async fn files( - State(state): State, - Query(params): Query, -) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - Json(api.files(params.prefix.as_deref().unwrap_or("")).await) -} - -/// Subgraph cho UI: BFS callers + callees quanh seed → nodes + call edges. -pub async fn subgraph( - State(state): State, - Query(params): Query, -) -> impl IntoResponse { - let api = GraphApi::new_with_index(state.shared_index.clone()); - let idx = api.index().await; - let depth = params.depth.max(1) as usize; - let limit = params.limit.unwrap_or(300).max(1) as usize; - - let seed = if let Some(id) = params.seed { - idx.symbol_by_id(id) - } else if let Some(q) = params.query.as_deref().filter(|q| !q.is_empty()) { - idx.search_symbol(q, None, 1) - .await - .ok() - .and_then(|mut v| v.pop()) - } else { - None - }; - let Some(seed) = seed else { - return ( - StatusCode::NOT_FOUND, - Json(json!({ "error": "no seed found" })), - ) - .into_response(); - }; - - let mut nodes: HashMap = HashMap::new(); - let mut edges: Vec = Vec::new(); - nodes.insert(seed.id, seed.clone()); - let mut frontier = vec![seed.id]; - let mut truncated = false; - for _ in 0..depth { - let mut next = Vec::new(); - for &id in &frontier { - let mut fresh = Vec::new(); - if let Ok(callees) = idx.callees(id).await { - for c in callees { - edges.push(json!({ "from": id, "to": c.id, "kind": "calls" })); - if !nodes.contains_key(&c.id) { - fresh.push(c); - } - } - } - if let Ok(callers) = idx.callers(id, 1).await { - for c in callers { - edges.push(json!({ "from": c.id, "to": id, "kind": "calls" })); - if !nodes.contains_key(&c.id) { - fresh.push(c); - } - } - } - for c in fresh { - nodes.insert(c.id, c.clone()); - next.push(c.id); - } - } - frontier = next; - if frontier.is_empty() { - break; - } - if nodes.len() >= limit { - truncated = true; - break; - } - } - - let nodes: Vec = nodes.into_values().collect(); - Json(json!({ - "nodes": nodes, - "edges": edges, - "seed": seed, - "truncated": truncated, - })) - .into_response() -} - -pub async fn boot(State(state): State) -> impl IntoResponse { - ( - [(axum::http::header::CONTENT_TYPE, "application/json")], - state.boot_json, - ) -} - -fn api_error(e: codegraph_core::Error) -> Response { - ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ "error": e.to_string() })), - ) - .into_response() -} diff --git a/crates/codegraph-viz/src/assets.rs b/crates/codegraph-viz/src/assets.rs deleted file mode 100644 index b8d783d14..000000000 --- a/crates/codegraph-viz/src/assets.rs +++ /dev/null @@ -1,17 +0,0 @@ -use rust_embed::Embed; - -#[derive(Embed)] -#[folder = "assets/"] -pub struct Asset; - -pub fn content_type(path: &str) -> &'static str { - if path.ends_with(".html") { - "text/html; charset=utf-8" - } else if path.ends_with(".js") { - "application/javascript; charset=utf-8" - } else if path.ends_with(".css") { - "text/css; charset=utf-8" - } else { - "application/octet-stream" - } -} diff --git a/crates/codegraph-viz/src/lib.rs b/crates/codegraph-viz/src/lib.rs deleted file mode 100644 index d0140a930..000000000 --- a/crates/codegraph-viz/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Local HTTP server + embedded web UI for graph visualization. - -pub mod api; -mod assets; -mod server; - -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BootConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub target: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub prefix: Option, - pub depth: u32, -} - -#[derive(Debug, Clone)] -pub struct VizConfig { - pub port: u16, - pub open_browser: bool, - pub boot: BootConfig, -} - -/// Serve UI trên index đã persist tại `db_path` (`.codegraph/db.sqlite`). -pub async fn run(db_path: PathBuf, config: VizConfig) -> anyhow::Result<()> { - server::serve(db_path, config).await -} diff --git a/crates/codegraph-viz/src/server.rs b/crates/codegraph-viz/src/server.rs deleted file mode 100644 index 91cdde5c0..000000000 --- a/crates/codegraph-viz/src/server.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::api::{self, AppState}; -use crate::assets::{content_type, Asset}; -use crate::VizConfig; -use axum::{ - body::Body, - http::{header, StatusCode, Uri}, - response::{IntoResponse, Response}, - routing::get, - Router, -}; -use codegraph_graph::SharedGraphIndex; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::sync::Arc; -use tower_http::compression::CompressionLayer; - -pub async fn serve(db_path: PathBuf, config: VizConfig) -> anyhow::Result<()> { - let boot_json = serde_json::to_string(&config.boot)?; - // Index sống trong chính file db (`.codegraph/db.sqlite`) — không sidecar. - let shared_index = Arc::new(SharedGraphIndex::open(Some(db_path)).await?); - let state = AppState { - shared_index, - boot_json, - }; - - let app = Router::new() - .route("/api/status", get(api::status)) - .route("/api/search", get(api::search)) - .route("/api/symbol/{id}", get(api::symbol)) - .route("/api/flow/{id}", get(api::flow)) - .route("/api/search_flow", get(api::search_flow)) - .route("/api/subgraph", get(api::subgraph)) - .route("/api/files", get(api::files)) - .route("/api/callers/{id}", get(api::callers)) - .route("/api/callees/{id}", get(api::callees)) - .route("/api/boot", get(api::boot)) - .fallback(static_handler) - .layer(CompressionLayer::new()) - .with_state(state); - - let addr = SocketAddr::from(([127, 0, 0, 1], config.port)); - let url = format!("http://{addr}"); - tracing::info!("codegraph visualize at {url}"); - - if config.open_browser { - if let Err(e) = open::that(&url) { - tracing::warn!("failed to open browser: {e}"); - } - } - - let listener = tokio::net::TcpListener::bind(addr).await?; - axum::serve(listener, app).await?; - Ok(()) -} - -async fn static_handler(uri: Uri) -> impl IntoResponse { - let path = uri.path().trim_start_matches('/'); - let path = if path.is_empty() { "index.html" } else { path }; - - match Asset::get(path) { - Some(content) => Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, content_type(path)) - .body(Body::from(content.data.into_owned())) - .unwrap(), - None if !path.contains('.') => match Asset::get("index.html") { - Some(content) => Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "text/html; charset=utf-8") - .body(Body::from(content.data.into_owned())) - .unwrap(), - None => not_found(), - }, - None => not_found(), - } -} - -fn not_found() -> Response { - Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("not found")) - .unwrap() -} diff --git a/crates/codegraph-viz/tests/http.rs b/crates/codegraph-viz/tests/http.rs deleted file mode 100644 index b60d449cb..000000000 --- a/crates/codegraph-viz/tests/http.rs +++ /dev/null @@ -1,127 +0,0 @@ -use axum::Router; -use codegraph_core::{ScopeLevel, Symbol, SymbolKind, SYMBOL_BASE}; -use codegraph_graph::{GraphIndex, ParseResult, SharedGraphIndex}; -use codegraph_viz::api::{self, AppState}; -use codegraph_viz::{BootConfig, VizConfig}; -use std::collections::HashMap; -use std::sync::Arc; - -fn sym(id: u64, name: &str) -> Symbol { - Symbol { - id, - name: name.to_string(), - kind: SymbolKind::Function, - scope: ScopeLevel::Global, - scope_id: 0, - type_ref: 0, - type_name: None, - file: "src/main.rs".into(), - line: 1, - end_line: 1, - signature: None, - doc: None, - annotations: Vec::new(), - language: "rust".into(), - } -} - -/// Seed index sqlite: main → helper. -async fn seed_index(db_path: &str) { - let mut idx = GraphIndex::open(db_path).await.unwrap(); - let r = ParseResult { - path: "src/main.rs".into(), - language: "rust".into(), - bytes: 10, - lines: 5, - symbols: vec![sym(SYMBOL_BASE, "main"), sym(SYMBOL_BASE + 1, "helper")], - chains: HashMap::from([(SYMBOL_BASE, vec![SYMBOL_BASE, SYMBOL_BASE + 1])]), - calls: Vec::new(), - }; - idx.ingest(&[r]).await.unwrap(); -} - -async fn test_router(db_path: std::path::PathBuf) -> Router { - let boot = BootConfig { - target: None, - prefix: None, - depth: 2, - }; - let shared_index = Arc::new(SharedGraphIndex::open(Some(db_path)).await.unwrap()); - let state = AppState { - shared_index, - boot_json: serde_json::to_string(&boot).unwrap(), - }; - Router::new() - .route("/api/status", axum::routing::get(api::status)) - .route("/api/subgraph", axum::routing::get(api::subgraph)) - .route("/api/flow/{id}", axum::routing::get(api::flow)) - .with_state(state) -} - -#[tokio::test] -async fn http_status_subgraph_and_flow() { - let dir = tempfile::tempdir().unwrap(); - let db_path = dir.path().join("db.sqlite"); - seed_index(&db_path.to_string_lossy()).await; - let app = test_router(db_path).await; - - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - - let base = format!("http://{addr}"); - let client = reqwest::Client::new(); - - let status: serde_json::Value = client - .get(format!("{base}/api/status")) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); - assert_eq!(status["symbols"], 2); - assert_eq!(status["chains"], 1); - assert_eq!(status["edges"], 1); - - let sub: serde_json::Value = client - .get(format!("{base}/api/subgraph?query=main&depth=1")) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); - assert_eq!(sub["nodes"].as_array().unwrap().len(), 2); - assert_eq!(sub["edges"].as_array().unwrap().len(), 1); - assert_eq!(sub["seed"]["id"], SYMBOL_BASE); - - let flow: serde_json::Value = client - .get(format!("{base}/api/flow/{SYMBOL_BASE}")) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); - assert_eq!(flow["chain"].as_array().unwrap().len(), 2); - assert_eq!(flow["chain_desc"][0], "main"); - assert_eq!(flow["chain_desc"][1], "helper"); -} - -#[test] -fn viz_config_serializes_boot() { - let boot = BootConfig { - target: Some("foo".into()), - prefix: None, - depth: 3, - }; - let cfg = VizConfig { - port: 7421, - open_browser: false, - boot, - }; - assert_eq!(cfg.port, 7421); -} diff --git a/crates/codegraph/Cargo.toml b/crates/codegraph/Cargo.toml index 7ba9b6e5a..7ded2aef5 100644 --- a/crates/codegraph/Cargo.toml +++ b/crates/codegraph/Cargo.toml @@ -13,11 +13,11 @@ path = "src/main.rs" [dependencies] codegraph-core = { path = "../codegraph-core" } codegraph-extract = { path = "../codegraph-extract" } -codegraph-graph = { path = "../codegraph-graph", features = ["sqlite"] } +codegraph-graph = { path = "../codegraph-graph", features = ["sqlite", "bloom-search"] } codegraph-context = { path = "../codegraph-context" } codegraph-mcp = { path = "../codegraph-mcp" } codegraph-installer = { path = "../codegraph-installer" } -codegraph-viz = { path = "../codegraph-viz", optional = true } +codegraph-sboxes = { path = "../codegraph-sboxes" } dirs = { workspace = true } clap = { workspace = true } tokio = { workspace = true } @@ -33,5 +33,4 @@ console = "0.15" indicatif = "0.18.6" [features] -default = ["visualize"] -visualize = ["dep:codegraph-viz"] +default = [] diff --git a/crates/codegraph/src/main.rs b/crates/codegraph/src/main.rs index 1e39ee821..520732e26 100644 --- a/crates/codegraph/src/main.rs +++ b/crates/codegraph/src/main.rs @@ -8,9 +8,6 @@ use std::sync::Arc; mod watcher; -pub(crate) const CODEGRAPH_DIR: &str = ".codegraph"; -const DB_FILE: &str = "db.sqlite"; - #[derive(Parser, Debug)] #[command( name = "codegraph", @@ -38,14 +35,22 @@ enum Cmd { Init { #[arg(long, default_value_t = false, help = "Disable indexing")] no_index: bool, - #[arg(long, default_value_t = true, help = "Show live progress bar during indexing")] + #[arg( + long, + default_value_t = true, + help = "Show live progress bar during indexing" + )] progress: bool, }, /// Remove the .codegraph/ directory. Uninit, /// Full re-index. Index { - #[arg(long, default_value_t = true, help = "Show live progress bar during indexing")] + #[arg( + long, + default_value_t = true, + help = "Show live progress bar during indexing" + )] progress: bool, }, /// Show index health. @@ -77,21 +82,18 @@ enum Cmd { }, /// Configure agents (alias for the agent setup step in `init`). Install, - /// Launch local web UI to explore the knowledge graph. - #[cfg(feature = "visualize")] - Visualize { - #[arg(long, default_value_t = 7421)] - port: u16, - #[arg(long)] - open: bool, - #[arg(long)] - target: Option, - #[arg(long)] - prefix: Option, - #[arg(long, default_value_t = 2)] - depth: u32, - #[arg(long)] - no_browser: bool, + /// Run a function in the behavior-verification sandbox: compile the + /// function (and its in-group callees) to machine code, bind external + /// callees to Rhai mocks, run it, and print the observed-behavior trace. + Sandbox { + /// Entry function name (substring; first match wins). + function: String, + /// Comma-separated abstract arg values (i64) for the entry function. + #[arg(long, default_value = "")] + args: String, + /// Do not print the trace, only the return value. + #[arg(long, default_value_t = false)] + quiet: bool, }, } @@ -132,15 +134,11 @@ fn main() -> Result<()> { } => cmd_context(&root, &target, depth, source), Cmd::Serve { mcp } => cmd_serve(&root, mcp), Cmd::Install => cmd_agents(&root), - #[cfg(feature = "visualize")] - Cmd::Visualize { - port, - open, - target, - prefix, - depth, - no_browser, - } => cmd_visualize(&root, port, open, target, prefix, depth, no_browser), + Cmd::Sandbox { + function, + args, + quiet, + } => cmd_sandbox(&root, &function, &args, quiet), } } @@ -213,17 +211,12 @@ fn cmd_default(root: &Utf8Path) -> Result<()> { " • {} Configure/install AI agent integrations", style("codegraph install").green() ); - #[cfg(feature = "visualize")] - eprintln!( - " • {} Explore the graph in your browser", - style("codegraph visualize").green() - ); eprintln!(); Ok(()) } fn db_path(root: &Utf8Path) -> Utf8PathBuf { - root.join(CODEGRAPH_DIR).join(DB_FILE) + codegraph_extract::project_db_path(root) } fn ensure_initialized(root: &Utf8Path) -> Result<()> { @@ -288,14 +281,7 @@ fn block_on_index(root: &Utf8Path, db_path: &Utf8Path, progress: bool) -> Result } fn cmd_init(root: &Utf8Path, do_index: bool, show_progress: bool) -> Result<()> { - let dir = root.join(CODEGRAPH_DIR); - std::fs::create_dir_all(&dir)?; - std::fs::write(dir.join(".gitignore"), "*\n")?; - std::fs::write(dir.join("version"), env!("CARGO_PKG_VERSION"))?; - let config_path = dir.join("config.toml"); - if !config_path.exists() { - std::fs::write(&config_path, codegraph_extract::DEFAULT_CONFIG_TOML)?; - } + let dir = codegraph_extract::init_project(root)?; eprintln!("initialized {}", dir); if do_index { @@ -402,7 +388,7 @@ fn cmd_agents(root: &Utf8Path) -> Result<()> { } fn cmd_uninit(root: &Utf8Path) -> Result<()> { - let dir = root.join(CODEGRAPH_DIR); + let dir = codegraph_extract::project_dir(root); if dir.exists() { std::fs::remove_dir_all(&dir)?; eprintln!("removed {}", dir); @@ -476,7 +462,9 @@ fn cmd_files(root: &Utf8Path, prefix: Option<&str>) -> Result<()> { Ok::<_, anyhow::Error>(if prefix.is_empty() { all } else { - all.into_iter().filter(|f| f.path.starts_with(&prefix)).collect() + all.into_iter() + .filter(|f| f.path.starts_with(&prefix)) + .collect() }) })?; let mut out = std::io::stdout().lock(); @@ -522,38 +510,90 @@ fn cmd_serve(root: &Utf8Path, mcp: bool) -> Result<()> { .build()?; rt.block_on(async { watcher::spawn(root.to_path_buf(), db_path.clone()); - let mcp_server = McpServer::new(Some(db_path.into_std_path_buf())).await?; + let mcp_server = + McpServer::new(root.to_path_buf(), Some(db_path.into_std_path_buf())).await?; mcp_server.run_stdio().await })?; Ok(()) } -#[cfg(feature = "visualize")] -fn cmd_visualize( - root: &Utf8Path, - port: u16, - open: bool, - target: Option, - prefix: Option, - depth: u32, - no_browser: bool, -) -> Result<()> { - use codegraph_viz::{BootConfig, VizConfig}; - - ensure_initialized(root).context("init the index before visualize")?; +/// `codegraph sandbox ` — compile a function group to machine code, +/// bind external callees to Rhai mocks, run it, and print the observed trace. +fn cmd_sandbox(root: &Utf8Path, function: &str, args: &str, quiet: bool) -> Result<()> { + use codegraph_core::SymbolKind; + use codegraph_sboxes::SboxConfig; + + ensure_initialized(root)?; let db_path = db_path(root); - let config = VizConfig { - port, - open_browser: open && !no_browser, - boot: BootConfig { - target, - prefix, - depth, - }, - }; + let function = function.to_string(); + let args: Vec = args + .split(',') + .filter(|s| !s.trim().is_empty()) + .map(|s| s.trim().parse().map_err(|e| anyhow!("bad arg `{s}`: {e}"))) + .collect::>()?; + let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build()?; - rt.block_on(codegraph_viz::run(db_path.into_std_path_buf(), config))?; + let (ret, trace, group_names) = rt.block_on(async { + let sgi = Arc::new( + codegraph_graph::SharedGraphIndex::open(Some(db_path.into_std_path_buf())).await?, + ); + let idx = sgi.ensure_fresh().await; + + // Resolve the entry function (substring, first function match). + let hits = idx + .search_symbol(&function, Some(SymbolKind::Function), 1) + .await?; + let entry = hits + .first() + .ok_or_else(|| anyhow!("no function matching `{function}`"))?; + let entry_id = entry.id; + + // Build the group: the entry plus every callee in its flow that is a + // known symbol (so those calls compile to real machine code instead of + // a mock). Unresolved/external calls stay mocked. + let flow = idx.flow(entry_id).await?; + let mut ids = vec![entry_id]; + let mut seen = std::collections::HashSet::from([entry_id]); + for &e in &flow.chain { + if codegraph_core::is_marker(e) { + continue; + } + if e != entry_id && idx.symbol_by_id(e).is_some() && seen.insert(e) { + ids.push(e); + } + } + ids.sort_unstable(); + + let config = SboxConfig::load(&root.to_path_buf()).unwrap_or_default(); + let mut module = codegraph_sboxes::compile(&idx, &ids, &config).await?; + let (ret, trace) = module.run(&args); + + let mut names: Vec = ids + .iter() + .filter_map(|id| idx.symbol_by_id(*id)) + .map(|s| s.name) + .collect(); + names.sort(); + Ok::<_, anyhow::Error>((ret, trace, names)) + })?; + + println!("group: {}", group_names.join(", ")); + println!("return: {ret}"); + if quiet { + return Ok(()); + } + for (i, name) in trace.mock_names().iter().enumerate() { + println!(" {i}: call {name}"); + } + for c in &trace.conds { + println!( + " {:>4}: {} -> {}", + c.idx, + c.kind.as_str(), + if c.result { "taken" } else { "skipped" } + ); + } Ok(()) } diff --git a/crates/codegraph/src/watcher.rs b/crates/codegraph/src/watcher.rs index 52c2ebadd..f35dde52f 100644 --- a/crates/codegraph/src/watcher.rs +++ b/crates/codegraph/src/watcher.rs @@ -31,7 +31,7 @@ fn run(root: Utf8PathBuf, db_path: Utf8PathBuf) -> Result<()> { )?; debouncer.watch(root.as_std_path(), RecursiveMode::Recursive)?; - let ignored_dirs = [root.join(crate::CODEGRAPH_DIR), root.join(".git")]; + let ignored_dirs = [codegraph_extract::project_dir(&root), root.join(".git")]; let mut gitignore_builder = GitignoreBuilder::new(root.as_std_path()); gitignore_builder.add(root.join(".gitignore")); let gitignore = gitignore_builder.build().unwrap_or_else(|_| {