Ver código fonte

添加3DTile,调整相机角度,调整水面位置

WQQ 1 mês atrás
pai
commit
7381f567df
4 arquivos alterados com 1483 adições e 40 exclusões
  1. 851 5
      package-lock.json
  2. 4 0
      package.json
  3. 616 30
      src/components/Scene3D.vue
  4. 12 5
      src/components/Water.ts

+ 851 - 5
package-lock.json

@@ -8,6 +8,10 @@
       "name": "threeengine",
       "version": "0.0.0",
       "dependencies": {
+        "@loaders.gl/3d-tiles": "^4.4.1",
+        "@loaders.gl/core": "^4.4.1",
+        "@loaders.gl/gltf": "^4.4.1",
+        "3d-tiles-renderer": "^0.4.24",
         "three": "^0.184.0",
         "vue": "^3.5.32"
       },
@@ -114,6 +118,285 @@
       "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
       "license": "MIT"
     },
+    "node_modules/@loaders.gl/3d-tiles": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/3d-tiles/-/3d-tiles-4.4.1.tgz",
+      "integrity": "sha512-837MynN5/lqVbuZcqdxFb0CMfT8v0yRlX7TUFKIBdmkS7AeRRrgcrB+XKblrkdZINUcxOs2N/YLVkwC9wLH1Uw==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/compression": "4.4.1",
+        "@loaders.gl/crypto": "4.4.1",
+        "@loaders.gl/draco": "4.4.1",
+        "@loaders.gl/gltf": "4.4.1",
+        "@loaders.gl/images": "4.4.1",
+        "@loaders.gl/loader-utils": "4.4.1",
+        "@loaders.gl/math": "4.4.1",
+        "@loaders.gl/tiles": "4.4.1",
+        "@loaders.gl/zip": "4.4.1",
+        "@math.gl/core": "^4.1.0",
+        "@math.gl/culling": "^4.1.0",
+        "@math.gl/geospatial": "^4.1.0",
+        "@probe.gl/log": "^4.1.1",
+        "long": "^5.2.1"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/compression": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/compression/-/compression-4.4.1.tgz",
+      "integrity": "sha512-MKtGbqHBH7xRVFKyB3E9xRqRMwNW8H72OKpUBDdFwP+hQ0mjHZuud0GeYm5pP50+7o3J2PrES06kHTwT4fg7oQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/loader-utils": "4.4.1",
+        "@loaders.gl/worker-utils": "4.4.1",
+        "@types/pako": "^1.0.1",
+        "fflate": "0.7.4",
+        "pako": "1.0.11",
+        "snappyjs": "^0.6.1"
+      },
+      "optionalDependencies": {
+        "@types/brotli": "^1.3.0",
+        "brotli": "^1.3.2",
+        "lz4js": "^0.2.0",
+        "zstd-codec": "^0.1"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/compression/node_modules/fflate": {
+      "version": "0.7.4",
+      "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.7.4.tgz",
+      "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==",
+      "license": "MIT"
+    },
+    "node_modules/@loaders.gl/core": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/core/-/core-4.4.1.tgz",
+      "integrity": "sha512-/s4IuvCCQUepvhjLnmePwQppGko2d1pxRS+sp7lyExU0uiqo5dVsAKaCZ2VnddBkFWgDVb/wvcZUBmv/dWcj0Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/loader-utils": "4.4.1",
+        "@loaders.gl/schema": "4.4.1",
+        "@loaders.gl/schema-utils": "4.4.1",
+        "@loaders.gl/worker-utils": "4.4.1",
+        "@probe.gl/log": "^4.1.1"
+      }
+    },
+    "node_modules/@loaders.gl/crypto": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/crypto/-/crypto-4.4.1.tgz",
+      "integrity": "sha512-ORhS9GSYr9uVTU4I2Taa46XBgPPG+nKErKcyDGIXov3gs0EtgMqs8nU4epuLbsJN3+du6FkQaILyGSZlTxbA7Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/loader-utils": "4.4.1",
+        "@loaders.gl/worker-utils": "4.4.1",
+        "@types/crypto-js": "^4.0.2"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/draco": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/draco/-/draco-4.4.1.tgz",
+      "integrity": "sha512-EcapVlkP8Pz53VKg9pYRQUzqm9jH+A+7vGE1kV8nkv63lN8/qtFzBSWMiC6IX1CwxjKJDEINU9Sh8YB1AfMwbQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/loader-utils": "4.4.1",
+        "@loaders.gl/schema": "4.4.1",
+        "@loaders.gl/schema-utils": "4.4.1",
+        "@loaders.gl/worker-utils": "4.4.1",
+        "draco3d": "1.5.7"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/gltf": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/gltf/-/gltf-4.4.1.tgz",
+      "integrity": "sha512-9ESHEm3YoMgsQh8QS1N99uwA+cij6p6xhCmZnHX4rQnqHm0jvE5RAHlGV1D/Xjvr4PR8IiXaBn/QDl/qdGIxkw==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/draco": "4.4.1",
+        "@loaders.gl/images": "4.4.1",
+        "@loaders.gl/loader-utils": "4.4.1",
+        "@loaders.gl/schema": "4.4.1",
+        "@loaders.gl/textures": "4.4.1",
+        "@math.gl/core": "^4.1.0"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/images": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/images/-/images-4.4.1.tgz",
+      "integrity": "sha512-v9A4BliEKGxhLuEbh0Ke8ElUlp04KxpKIknUtXXWoEaszAMTSrHI3YhaL/JdRlHraC1VUF/sjzbSBFkKh7nxJg==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/loader-utils": "4.4.1"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/loader-utils": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/loader-utils/-/loader-utils-4.4.1.tgz",
+      "integrity": "sha512-waosL7VtVRfXsNOXtAM3rOjZyNQD0lQBlhuB5/oY+E+lNzYNFlzgiGXiDOwBpcs7dK7kW2Vv8+KcxyIGIyXOtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/schema": "4.4.1",
+        "@loaders.gl/worker-utils": "4.4.1",
+        "@probe.gl/log": "^4.1.1",
+        "@probe.gl/stats": "^4.1.1"
+      }
+    },
+    "node_modules/@loaders.gl/math": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/math/-/math-4.4.1.tgz",
+      "integrity": "sha512-xenAPOAUd7HDlus5V/g4LKVh1l7FpyVRSYXa+g7tBj91xzhRYgLEXSxdrGfRNAFMDOSGC1ITwCGQwlwSX4Mpxw==",
+      "license": "MIT",
+      "dependencies": {
+        "@math.gl/core": "^4.1.0"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/schema": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/schema/-/schema-4.4.1.tgz",
+      "integrity": "sha512-s7NjEnyK6jZvJJSWj/mHq+S9mHRHVzIYtFP+C7sMf1gVCQbdkt6OSAMUWRzwPr9+whQNVWjZ9pbLsI/IPW3zvw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "^7946.0.7",
+        "apache-arrow": ">= 17.0.0"
+      }
+    },
+    "node_modules/@loaders.gl/schema-utils": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/schema-utils/-/schema-utils-4.4.1.tgz",
+      "integrity": "sha512-4upip2O6MFaWzk68/lnna7P2uRj9NQ8MIk/ff3CLbciP5/9lKl1qyuzObz5JrJRYzfGB6I81vpOn6FSVQ6m6KQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/schema": "4.4.1",
+        "@types/geojson": "^7946.0.7",
+        "apache-arrow": ">= 17.0.0"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/textures": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/textures/-/textures-4.4.1.tgz",
+      "integrity": "sha512-r1//6sO29GOHso+IvXQ3GrvXZ4cl03VWc34XcnXPn3sAV7O96uRGd5xkyx60lMYAl7Jv7qK/smT3z4Mdxdd4aA==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/images": "4.4.1",
+        "@loaders.gl/loader-utils": "4.4.1",
+        "@loaders.gl/schema": "4.4.1",
+        "@loaders.gl/worker-utils": "4.4.1",
+        "@math.gl/types": "^4.1.0",
+        "ktx-parse": "^0.7.0",
+        "texture-compressor": "^1.0.2"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/tiles": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/tiles/-/tiles-4.4.1.tgz",
+      "integrity": "sha512-EbF81/c1oXJocVAKR0rx+vWSOnmBBWWhM7pZpYk6oNUQAJfA99APhiRNstAJiJomAgqAxr7vfnhXHjPZg6osZw==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/loader-utils": "4.4.1",
+        "@loaders.gl/math": "4.4.1",
+        "@math.gl/core": "^4.1.0",
+        "@math.gl/culling": "^4.1.0",
+        "@math.gl/geospatial": "^4.1.0",
+        "@math.gl/web-mercator": "^4.1.0",
+        "@probe.gl/stats": "^4.1.1"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/worker-utils": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/worker-utils/-/worker-utils-4.4.1.tgz",
+      "integrity": "sha512-ovMyIyj9dlChuHuD64Bel7Mir2UYlmLqlZ9MMzVxzTTLvaudJoNAXi6Disp0ooxwF62ZqjNXXutaSbS6UDeuIg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@loaders.gl/zip": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/@loaders.gl/zip/-/zip-4.4.1.tgz",
+      "integrity": "sha512-fV7oqREEzzqYl2/b4tiM+J4qeSq6pB4gw1hHngpCtVyjVwWVtsNH2r1ly9kkv4XssIdXJxPcrX/GR0mDIwmp6w==",
+      "license": "MIT",
+      "dependencies": {
+        "@loaders.gl/compression": "4.4.1",
+        "@loaders.gl/crypto": "4.4.1",
+        "@loaders.gl/loader-utils": "4.4.1",
+        "jszip": "^3.1.5",
+        "md5": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@loaders.gl/core": "~4.4.0"
+      }
+    },
+    "node_modules/@math.gl/core": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/@math.gl/core/-/core-4.1.0.tgz",
+      "integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==",
+      "license": "MIT",
+      "dependencies": {
+        "@math.gl/types": "4.1.0"
+      }
+    },
+    "node_modules/@math.gl/culling": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/@math.gl/culling/-/culling-4.1.0.tgz",
+      "integrity": "sha512-jFmjFEACnP9kVl8qhZxFNhCyd47qPfSVmSvvjR0/dIL6R9oD5zhR1ub2gN16eKDO/UM7JF9OHKU3EBIfeR7gtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@math.gl/core": "4.1.0",
+        "@math.gl/types": "4.1.0"
+      }
+    },
+    "node_modules/@math.gl/geospatial": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/@math.gl/geospatial/-/geospatial-4.1.0.tgz",
+      "integrity": "sha512-BzsUhpVvnmleyYF6qdqJIip6FtIzJmnWuPTGhlBuPzh7VBHLonCFSPtQpbkRuoyAlbSyaGXcVt6p6lm9eK2vtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@math.gl/core": "4.1.0",
+        "@math.gl/types": "4.1.0"
+      }
+    },
+    "node_modules/@math.gl/types": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/@math.gl/types/-/types-4.1.0.tgz",
+      "integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==",
+      "license": "MIT"
+    },
+    "node_modules/@math.gl/web-mercator": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/@math.gl/web-mercator/-/web-mercator-4.1.0.tgz",
+      "integrity": "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==",
+      "license": "MIT",
+      "dependencies": {
+        "@math.gl/core": "4.1.0"
+      }
+    },
     "node_modules/@napi-rs/wasm-runtime": {
       "version": "1.1.4",
       "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
@@ -143,6 +426,27 @@
         "url": "https://github.com/sponsors/Boshen"
       }
     },
+    "node_modules/@probe.gl/env": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/@probe.gl/env/-/env-4.1.1.tgz",
+      "integrity": "sha512-+68seNDMVsEegRB47pFA/Ws1Fjy8agcFYXxzorKToyPcD6zd+gZ5uhwoLd7TzsSw6Ydns//2KEszWn+EnNHTbA==",
+      "license": "MIT"
+    },
+    "node_modules/@probe.gl/log": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/@probe.gl/log/-/log-4.1.1.tgz",
+      "integrity": "sha512-kcZs9BT44pL7hS1OkRGKYRXI/SN9KejUlPD+BY40DguRLzdC5tLG/28WGMyfKdn/51GT4a0p+0P8xvDn1Ez+Kg==",
+      "license": "MIT",
+      "dependencies": {
+        "@probe.gl/env": "4.1.1"
+      }
+    },
+    "node_modules/@probe.gl/stats": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/@probe.gl/stats/-/stats-4.1.1.tgz",
+      "integrity": "sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g==",
+      "license": "MIT"
+    },
     "node_modules/@rolldown/binding-android-arm64": {
       "version": "1.0.0-rc.17",
       "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
@@ -407,6 +711,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.21",
+      "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.21.tgz",
+      "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.8.0"
+      }
+    },
     "node_modules/@tweenjs/tween.js": {
       "version": "23.1.3",
       "resolved": "https://registry.npmmirror.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
@@ -425,16 +738,55 @@
         "tslib": "^2.4.0"
       }
     },
+    "node_modules/@types/brotli": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmmirror.com/@types/brotli/-/brotli-1.3.5.tgz",
+      "integrity": "sha512-9xoNr+bcxT236/7ZgcWw/6Pb2RRetE13p4bFy1xYSckKwyOiRfmInay8baUWZgH7/284Wl6IPe7+nOI9+OQg/A==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/command-line-args": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmmirror.com/@types/command-line-args/-/command-line-args-5.2.3.tgz",
+      "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/command-line-usage": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmmirror.com/@types/command-line-usage/-/command-line-usage-5.0.4.tgz",
+      "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/crypto-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.2.2.tgz",
+      "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.16",
+      "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz",
+      "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "24.12.2",
       "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz",
       "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "undici-types": "~7.16.0"
       }
     },
+    "node_modules/@types/pako": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/@types/pako/-/pako-1.0.7.tgz",
+      "integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==",
+      "license": "MIT"
+    },
     "node_modules/@types/stats.js": {
       "version": "0.17.4",
       "resolved": "https://registry.npmmirror.com/@types/stats.js/-/stats.js-0.17.4.tgz",
@@ -645,6 +997,40 @@
         }
       }
     },
+    "node_modules/3d-tiles-renderer": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmmirror.com/3d-tiles-renderer/-/3d-tiles-renderer-0.4.24.tgz",
+      "integrity": "sha512-1n21vaWoV5e+N6rTfK1nv0Bsgu9ptbY6d9ICHnT6W1llloIhUGmGzx5/JH+B7t9XggjL0aJASn0Z5sFOXXvYjw==",
+      "license": "Apache-2.0",
+      "peerDependencies": {
+        "@babylonjs/core": ">=8.0.0",
+        "@babylonjs/loaders": ">=8.0.0",
+        "@react-three/fiber": "^8.17.9 || ^9.0.0",
+        "react": "^18.3.1 || ^19.0.0",
+        "react-dom": "^18.3.1 || ^19.0.0",
+        "three": ">=0.167.0"
+      },
+      "peerDependenciesMeta": {
+        "@babylonjs/core": {
+          "optional": true
+        },
+        "@babylonjs/loaders": {
+          "optional": true
+        },
+        "@react-three/fiber": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "react-dom": {
+          "optional": true
+        },
+        "three": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/alien-signals": {
       "version": "3.1.2",
       "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.2.tgz",
@@ -652,6 +1038,201 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/apache-arrow": {
+      "version": "21.1.0",
+      "resolved": "https://registry.npmmirror.com/apache-arrow/-/apache-arrow-21.1.0.tgz",
+      "integrity": "sha512-kQrYLxhC+NTVVZ4CCzGF6L/uPVOzJmD1T3XgbiUnP7oTeVFOFgEUu6IKNwCDkpFoBVqDKQivlX4RUFqqnWFlEA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@swc/helpers": "^0.5.11",
+        "@types/command-line-args": "^5.2.3",
+        "@types/command-line-usage": "^5.0.4",
+        "@types/node": "^24.0.3",
+        "command-line-args": "^6.0.1",
+        "command-line-usage": "^7.0.1",
+        "flatbuffers": "^25.1.24",
+        "json-bignum": "^0.0.3",
+        "tslib": "^2.6.2"
+      },
+      "bin": {
+        "arrow2csv": "bin/arrow2csv.js"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "license": "MIT",
+      "dependencies": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "node_modules/array-back": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmmirror.com/array-back/-/array-back-6.2.3.tgz",
+      "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.17"
+      }
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/brotli": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmmirror.com/brotli/-/brotli-1.3.3.tgz",
+      "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "base64-js": "^1.1.2"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/chalk-template": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/chalk-template/-/chalk-template-0.4.0.tgz",
+      "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^4.1.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk-template?sponsor=1"
+      }
+    },
+    "node_modules/charenc": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz",
+      "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/command-line-args": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/command-line-args/-/command-line-args-6.0.2.tgz",
+      "integrity": "sha512-AIjYVxrV9X752LmPDLbVYv8aMCuHPSLZJXEo2qo/xJfv+NYhaZ4sMSF01rM+gHPaMgvPM0l5D/F+Qx+i2WfSmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "array-back": "^6.2.3",
+        "find-replace": "^5.0.2",
+        "lodash.camelcase": "^4.3.0",
+        "typical": "^7.3.0"
+      },
+      "engines": {
+        "node": ">=12.20"
+      },
+      "peerDependencies": {
+        "@75lb/nature": "latest"
+      },
+      "peerDependenciesMeta": {
+        "@75lb/nature": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/command-line-usage": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmmirror.com/command-line-usage/-/command-line-usage-7.0.4.tgz",
+      "integrity": "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==",
+      "license": "MIT",
+      "dependencies": {
+        "array-back": "^6.2.2",
+        "chalk-template": "^0.4.0",
+        "table-layout": "^4.1.1",
+        "typical": "^7.3.0"
+      },
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "license": "MIT"
+    },
+    "node_modules/crypt": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz",
+      "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/csstype": {
       "version": "3.2.3",
       "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
@@ -668,6 +1249,12 @@
         "node": ">=8"
       }
     },
+    "node_modules/draco3d": {
+      "version": "1.5.7",
+      "resolved": "https://registry.npmmirror.com/draco3d/-/draco3d-1.5.7.tgz",
+      "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
+      "license": "Apache-2.0"
+    },
     "node_modules/entities": {
       "version": "7.0.1",
       "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
@@ -711,6 +1298,29 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/find-replace": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/find-replace/-/find-replace-5.0.2.tgz",
+      "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "@75lb/nature": "latest"
+      },
+      "peerDependenciesMeta": {
+        "@75lb/nature": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/flatbuffers": {
+      "version": "25.9.23",
+      "resolved": "https://registry.npmmirror.com/flatbuffers/-/flatbuffers-25.9.23.tgz",
+      "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==",
+      "license": "Apache-2.0"
+    },
     "node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -726,6 +1336,86 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/image-size": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmmirror.com/image-size/-/image-size-0.7.5.tgz",
+      "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==",
+      "license": "MIT",
+      "bin": {
+        "image-size": "bin/image-size.js"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/immediate": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
+      "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+      "license": "MIT"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
+      "license": "MIT"
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "license": "MIT"
+    },
+    "node_modules/json-bignum": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmmirror.com/json-bignum/-/json-bignum-0.0.3.tgz",
+      "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/jszip": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
+      "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+      "license": "(MIT OR GPL-3.0-or-later)",
+      "dependencies": {
+        "lie": "~3.3.0",
+        "pako": "~1.0.2",
+        "readable-stream": "~2.3.6",
+        "setimmediate": "^1.0.5"
+      }
+    },
+    "node_modules/ktx-parse": {
+      "version": "0.7.1",
+      "resolved": "https://registry.npmmirror.com/ktx-parse/-/ktx-parse-0.7.1.tgz",
+      "integrity": "sha512-FeA3g56ksdFNwjXJJsc1CCc7co+AJYDp6ipIp878zZ2bU8kWROatLYf39TQEd4/XRSUvBXovQ8gaVKWPXsCLEQ==",
+      "license": "MIT"
+    },
+    "node_modules/lie": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
+      "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+      "license": "MIT",
+      "dependencies": {
+        "immediate": "~3.0.5"
+      }
+    },
     "node_modules/lightningcss": {
       "version": "1.32.0",
       "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -987,6 +1677,25 @@
         "url": "https://opencollective.com/parcel"
       }
     },
+    "node_modules/lodash.camelcase": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+      "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+      "license": "MIT"
+    },
+    "node_modules/long": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
+      "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/lz4js": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/lz4js/-/lz4js-0.2.0.tgz",
+      "integrity": "sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==",
+      "license": "ISC",
+      "optional": true
+    },
     "node_modules/magic-string": {
       "version": "0.30.21",
       "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
@@ -996,6 +1705,17 @@
         "@jridgewell/sourcemap-codec": "^1.5.5"
       }
     },
+    "node_modules/md5": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz",
+      "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "charenc": "0.0.2",
+        "crypt": "0.0.2",
+        "is-buffer": "~1.1.6"
+      }
+    },
     "node_modules/meshoptimizer": {
       "version": "1.1.1",
       "resolved": "https://registry.npmmirror.com/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
@@ -1028,6 +1748,12 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/pako": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
+      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+      "license": "(MIT AND Zlib)"
+    },
     "node_modules/path-browserify": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -1082,6 +1808,27 @@
         "node": "^10 || ^12 || >=14"
       }
     },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "license": "MIT"
+    },
+    "node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
     "node_modules/rolldown": {
       "version": "1.0.0-rc.17",
       "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.17.tgz",
@@ -1123,6 +1870,24 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "license": "MIT"
+    },
+    "node_modules/setimmediate": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
+      "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+      "license": "MIT"
+    },
+    "node_modules/snappyjs": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmmirror.com/snappyjs/-/snappyjs-0.6.1.tgz",
+      "integrity": "sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==",
+      "license": "MIT"
+    },
     "node_modules/source-map-js": {
       "version": "1.2.1",
       "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1132,6 +1897,59 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/table-layout": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/table-layout/-/table-layout-4.1.1.tgz",
+      "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==",
+      "license": "MIT",
+      "dependencies": {
+        "array-back": "^6.2.2",
+        "wordwrapjs": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=12.17"
+      }
+    },
+    "node_modules/texture-compressor": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/texture-compressor/-/texture-compressor-1.0.2.tgz",
+      "integrity": "sha512-dStVgoaQ11mA5htJ+RzZ51ZxIZqNOgWKAIvtjLrW1AliQQLCmrDqNzQZ8Jh91YealQ95DXt4MEduLzJmbs6lig==",
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^1.0.10",
+        "image-size": "^0.7.4"
+      },
+      "bin": {
+        "texture-compressor": "bin/texture-compressor.js"
+      }
+    },
     "node_modules/three": {
       "version": "0.184.0",
       "resolved": "https://registry.npmmirror.com/three/-/three-0.184.0.tgz",
@@ -1159,9 +1977,7 @@
       "version": "2.8.1",
       "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
-      "dev": true,
-      "license": "0BSD",
-      "optional": true
+      "license": "0BSD"
     },
     "node_modules/typescript": {
       "version": "6.0.3",
@@ -1177,11 +1993,25 @@
         "node": ">=14.17"
       }
     },
+    "node_modules/typical": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmmirror.com/typical/-/typical-7.3.0.tgz",
+      "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.17"
+      }
+    },
     "node_modules/undici-types": {
       "version": "7.16.0",
       "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
       "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
-      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
       "license": "MIT"
     },
     "node_modules/vite": {
@@ -1306,6 +2136,22 @@
       "peerDependencies": {
         "typescript": ">=5.0.0"
       }
+    },
+    "node_modules/wordwrapjs": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/wordwrapjs/-/wordwrapjs-5.1.1.tgz",
+      "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.17"
+      }
+    },
+    "node_modules/zstd-codec": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmmirror.com/zstd-codec/-/zstd-codec-0.1.5.tgz",
+      "integrity": "sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==",
+      "license": "MIT",
+      "optional": true
     }
   }
 }

+ 4 - 0
package.json

@@ -9,6 +9,10 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@loaders.gl/3d-tiles": "^4.4.1",
+    "@loaders.gl/core": "^4.4.1",
+    "@loaders.gl/gltf": "^4.4.1",
+    "3d-tiles-renderer": "^0.4.24",
     "three": "^0.184.0",
     "vue": "^3.5.32"
   },

+ 616 - 30
src/components/Scene3D.vue

@@ -1,11 +1,38 @@
 <script setup lang="ts">
-import { onMounted, onUnmounted, ref } from 'vue'
+import { onMounted, onUnmounted, ref, watch } from 'vue'
 import * as THREE from 'three'
 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
 import { Sky } from 'three/examples/jsm/objects/Sky.js'
 import { Water, createWaterNormalTexture } from './Water'
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
 
 const containerRef = ref<HTMLDivElement>()
+const pickedPosition = ref<{ x: number; y: number; z: number } | null>(null)
+const cameraInfo = ref({
+  position: { x: 0, y: 0, z: 0 },
+  target: { x: 0, y: 0, z: 0 },
+  distance: 0,
+  minDistance: 0,
+  maxDistance: 0,
+})
+
+const waterParams = ref({
+  alpha: 0.65,
+  sunColor: '#ffffff',
+  waterColor: '#78a6a0',
+  distortionScale: 30.0,
+  noiseScale: 1.0,
+  fresnelBias: 0.15,
+  fresnelPower: 4.0,
+  fresnelStrength: 1.0,
+  flowSpeed: 0.5,
+  flowDirectionX: 1.0,
+  flowDirectionY: 1.0,
+})
+
+const showCoordinatePanel = ref(false)
+const showCameraPanel = ref(false)
+const showWaterPanel = ref(false)
 
 let scene: THREE.Scene
 let camera: THREE.PerspectiveCamera
@@ -13,8 +40,12 @@ let renderer: THREE.WebGLRenderer
 let controls: OrbitControls
 let sky: Sky
 let water: Water
+let waterMesh: THREE.Mesh
 let sunDirection: THREE.Vector3
 let animationId: number
+let tilesetGroup: THREE.Group | null = null
+let raycaster: THREE.Raycaster
+let mouse: THREE.Vector2
 
 function createSky() {
   sky = new Sky()
@@ -35,20 +66,6 @@ function createSky() {
   uniforms.cloudElevation.value = 0.9
 }
 
-function createGround() {
-  const groundGeometry = new THREE.PlaneGeometry(200, 200, 1, 1)
-  const groundMaterial = new THREE.MeshStandardMaterial({
-    color: 0x808080,
-    roughness: 0.95,
-    metalness: 0.0,
-  })
-  const ground = new THREE.Mesh(groundGeometry, groundMaterial)
-  ground.rotation.x = -Math.PI / 2
-  ground.position.y = -0.5
-  ground.receiveShadow = true
-  scene.add(ground)
-}
-
 function createWaterSurface() {
   const textureLoader = new THREE.TextureLoader()
   const waterNormals = createWaterNormalTexture(textureLoader)
@@ -57,30 +74,149 @@ function createWaterSurface() {
     textureWidth: 512,
     textureHeight: 512,
     waterNormals,
-    alpha: 0.65,
+    alpha: waterParams.value.alpha,
     sunDirection,
-    sunColor: 0xffffff,
-    waterColor: 0x78a6a0,
-    distortionScale: 30.0,
-    noiseScale: 1.0,
-    fresnelBias: 0.15,
-    fresnelPower: 4.0,
-    fresnelStrength: 1.0,
+    sunColor: new THREE.Color(waterParams.value.sunColor),
+    waterColor: new THREE.Color(waterParams.value.waterColor),
+    distortionScale: waterParams.value.distortionScale,
+    noiseScale: waterParams.value.noiseScale,
+    fresnelBias: waterParams.value.fresnelBias,
+    fresnelPower: waterParams.value.fresnelPower,
+    fresnelStrength: waterParams.value.fresnelStrength,
+    flowDirection: new THREE.Vector2(waterParams.value.flowDirectionX, waterParams.value.flowDirectionY),
     side: THREE.DoubleSide,
     fog: true,
   })
 
-  const waterMesh = new THREE.Mesh(
+  waterMesh = new THREE.Mesh(
     new THREE.PlaneGeometry(200, 200, 60, 60),
     water.material
   )
   waterMesh.add(water)
   waterMesh.rotation.x = -Math.PI / 2
-  waterMesh.position.y = 0
+  waterMesh.position.set(840.85714, 7.47851, 2179.50782)
   waterMesh.receiveShadow = true
   scene.add(waterMesh)
 }
 
+async function load3DTiles() {
+  const tilesetUrl = 'http://localhost:9003/model/scence/tileset.json'
+  const gltfLoader = new GLTFLoader()
+  
+  try {
+    const response = await fetch(tilesetUrl)
+    const tileset = await response.json()
+    
+    console.log('Tileset loaded:', tileset)
+    console.log('Root tile:', tileset.root)
+    console.log('Root tile content:', tileset.root?.content)
+    console.log('Root tile children count:', tileset.root?.children?.length)
+    
+    tilesetGroup = new THREE.Group()
+    
+    async function loadTile(tile: any, parentGroup: THREE.Group, depth: number = 0): Promise<void> {
+      console.log(`${'  '.repeat(depth)}Processing tile at depth ${depth}`)
+      console.log(`${'  '.repeat(depth)}Tile content:`, tile.content)
+      console.log(`${'  '.repeat(depth)}Tile has transform:`, !!tile.transform)
+      
+      if (tile.content && tile.content.uri) {
+        const tileUrl = new URL(tile.content.uri, tilesetUrl).href
+        console.log(`${'  '.repeat(depth)}Loading tile from: ${tileUrl}`)
+        
+        try {
+          const response = await fetch(tileUrl, { method: 'HEAD' })
+          if (!response.ok) {
+            throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+          }
+          console.log(`${'  '.repeat(depth)}File exists, content-type: ${response.headers.get('content-type')}`)
+          
+          const gltf = await new Promise((resolve, reject) => {
+            gltfLoader.load(
+              tileUrl,
+              (gltf) => resolve(gltf),
+              (progress) => {
+                if (progress.lengthComputable) {
+                  const percentComplete = (progress.loaded / progress.total) * 100
+                  console.log(`${'  '.repeat(depth)}Loading progress: ${percentComplete.toFixed(2)}%`)
+                }
+              },
+              (error) => {
+                console.error(`${'  '.repeat(depth)}GLTFLoader error:`, error)
+                reject(error)
+              }
+            )
+          }) as { scene: THREE.Group }
+          
+          if (gltf && gltf.scene) {
+            const mesh = gltf.scene.clone()
+            
+            console.log(`${'  '.repeat(depth)}GLB scene children count:`, mesh.children.length)
+            console.log(`${'  '.repeat(depth)}GLB scene:`, mesh)
+            
+            mesh.traverse((child) => {
+              if (child instanceof THREE.Mesh) {
+                console.log(`${'  '.repeat(depth)}Found mesh:`, child.name)
+                console.log(`${'  '.repeat(depth)}Mesh geometry:`, child.geometry)
+                console.log(`${'  '.repeat(depth)}Mesh material:`, child.material)
+                console.log(`${'  '.repeat(depth)}Mesh position:`, child.position)
+                console.log(`${'  '.repeat(depth)}Mesh visible:`, child.visible)
+              }
+            })
+            
+            if (tile.transform) {
+              const matrix = new THREE.Matrix4().fromArray(tile.transform)
+              mesh.applyMatrix4(matrix)
+            }
+            
+            parentGroup.add(mesh)
+            console.log(`${'  '.repeat(depth)}Successfully loaded tile: ${tile.content.uri}`)
+          }
+        } catch (error) {
+          console.warn(`${'  '.repeat(depth)}Error loading tile:`, tile.content.uri, error)
+        }
+      }
+      
+      if (tile.children && tile.children.length > 0) {
+        console.log(`${'  '.repeat(depth)}Tile has ${tile.children.length} children`)
+        for (const child of tile.children) {
+          await loadTile(child, parentGroup, depth + 1)
+        }
+      }
+    }
+    
+    if (tileset.root) {
+      await loadTile(tileset.root, tilesetGroup)
+    }
+    
+    console.log('Tileset group children count:', tilesetGroup.children.length)
+    
+    scene.add(tilesetGroup)
+    
+    const boundingBox = new THREE.Box3().setFromObject(tilesetGroup)
+    const center = boundingBox.getCenter(new THREE.Vector3())
+    const size = boundingBox.getSize(new THREE.Vector3())
+    const maxDim = Math.max(size.x, size.y, size.z)
+    
+    console.log('Tileset bounding box center:', center)
+    console.log('Tileset size:', size)
+    
+    if (maxDim > 0) {
+      tilesetGroup.position.copy(center).multiplyScalar(-1)
+
+      waterMesh.position.set(840.85714, 7.47851, 2179.50782)
+
+      controls.target.set(842.3117, 9.27789, 2178.09268)
+      controls.update()
+
+      console.log('Tileset positioned')
+    }
+    
+  } catch (error) {
+    console.error('Error loading 3D Tiles:', error)
+    console.error('Error details:', (error as Error).message)
+  }
+}
+
 function initScene() {
   const container = containerRef.value!
 
@@ -88,7 +224,7 @@ function initScene() {
   scene.fog = new THREE.FogExp2(0xd4d4d4, 0.004)
 
   camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 100000)
-  camera.position.set(12, 8, 12)
+  camera.position.set(838.15445, 22.22044, 2209.96548)
 
   renderer = new THREE.WebGLRenderer({ antialias: true })
   renderer.setSize(container.clientWidth, container.clientHeight)
@@ -105,13 +241,17 @@ function initScene() {
   controls.enableDamping = true
   controls.dampingFactor = 0.03
   controls.screenSpacePanning = false
+  controls.minDistance = 0
+  controls.maxDistance = Infinity
   controls.mouseButtons = {
     LEFT: THREE.MOUSE.PAN,
     MIDDLE: THREE.MOUSE.DOLLY,
     RIGHT: THREE.MOUSE.ROTATE,
   }
-  controls.target.set(0, 1, 0)
+  controls.target.set(842.3117, 9.27789, 2178.09268)
   controls.maxPolarAngle = Math.PI / 2.1
+  controls.minDistance = 20
+  controls.maxDistance = 100
 
   createSky()
 
@@ -133,19 +273,78 @@ function initScene() {
   sunLight.shadow.camera.bottom = -20
   scene.add(sunLight)
 
-  createGround()
-
   createWaterSurface()
 
+  load3DTiles()
+
+  initRaycaster()
+
   animate()
 }
 
+function initRaycaster() {
+  raycaster = new THREE.Raycaster()
+  mouse = new THREE.Vector2()
+  
+  const container = containerRef.value!
+  
+  container.addEventListener('click', onMouseClick)
+}
+
+function onMouseClick(event: MouseEvent) {
+  const container = containerRef.value!
+  const rect = container.getBoundingClientRect()
+  
+  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
+  mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
+  
+  raycaster.setFromCamera(mouse, camera)
+  
+  const intersectObjects: THREE.Object3D[] = []
+  
+  if (tilesetGroup) {
+    intersectObjects.push(tilesetGroup)
+  }
+  if (waterMesh) {
+    intersectObjects.push(waterMesh)
+  }
+  
+  const intersects = raycaster.intersectObjects(intersectObjects, true)
+  
+  if (intersects.length > 0) {
+    const point = intersects[0].point
+    pickedPosition.value = {
+      x: Number((point.x * 100).toFixed(3)),
+      y: Number((point.y * 100).toFixed(3)),
+      z: Number((point.z * 100).toFixed(3))
+    }
+    
+    console.log('Picked position (cm):', pickedPosition.value)
+  }
+}
+
 function animate() {
   animationId = requestAnimationFrame(animate)
   sky.material.uniforms.time.value += 0.001
-  water.material.uniforms.time.value += 0.005
+  water.material.uniforms.time.value += 0.005 * waterParams.value.flowSpeed
   water.render()
+
   controls.update()
+
+  cameraInfo.value.position = {
+    x: Number((camera.position.x * 100).toFixed(3)),
+    y: Number((camera.position.y * 100).toFixed(3)),
+    z: Number((camera.position.z * 100).toFixed(3)),
+  }
+  cameraInfo.value.target = {
+    x: Number((controls.target.x * 100).toFixed(3)),
+    y: Number((controls.target.y * 100).toFixed(3)),
+    z: Number((controls.target.z * 100).toFixed(3)),
+  }
+  cameraInfo.value.distance = Number(camera.position.distanceTo(controls.target).toFixed(3))
+  cameraInfo.value.minDistance = controls.minDistance
+  cameraInfo.value.maxDistance = controls.maxDistance
+
   renderer.render(scene, camera)
 }
 
@@ -166,13 +365,196 @@ onMounted(() => {
 onUnmounted(() => {
   cancelAnimationFrame(animationId)
   window.removeEventListener('resize', onResize)
+  
+  if (containerRef.value) {
+    containerRef.value.removeEventListener('click', onMouseClick)
+  }
+  
+  if (tilesetGroup) {
+    scene.remove(tilesetGroup)
+  }
+  
   renderer.dispose()
   controls.dispose()
 })
+
+watch(() => waterParams.value, (newParams) => {
+  if (water && water.material && water.material.uniforms) {
+    water.material.uniforms.alpha.value = newParams.alpha
+    water.material.uniforms.sunColor.value.set(newParams.sunColor)
+    water.material.uniforms.waterColor.value.set(newParams.waterColor)
+    water.material.uniforms.distortionScale.value = newParams.distortionScale
+    water.material.uniforms.noiseScale.value = newParams.noiseScale
+    water.material.uniforms.fresnelBias.value = newParams.fresnelBias
+    water.material.uniforms.fresnelPower.value = newParams.fresnelPower
+    water.material.uniforms.fresnelStrength.value = newParams.fresnelStrength
+    water.material.uniforms.flowDirection.value.set(newParams.flowDirectionX, newParams.flowDirectionY)
+  }
+}, { deep: true })
 </script>
 
 <template>
   <div ref="containerRef" class="scene-container" />
+  <div class="toolbar">
+    <button class="toolbar-btn" :class="{ active: showCoordinatePanel }" @click="showCoordinatePanel = !showCoordinatePanel">坐标</button>
+    <button class="toolbar-btn" :class="{ active: showCameraPanel }" @click="showCameraPanel = !showCameraPanel">相机</button>
+    <button class="toolbar-btn" :class="{ active: showWaterPanel }" @click="showWaterPanel = !showWaterPanel">水面</button>
+  </div>
+
+  <div v-if="showCoordinatePanel && pickedPosition" class="panel coordinate-panel">
+    <div class="panel-header">
+      <span class="panel-title">拾取坐标 (cm)</span>
+      <button class="toggle-btn" @click="showCoordinatePanel = false">×</button>
+    </div>
+    <div class="coordinate-item">
+      <span class="coordinate-label">X:</span>
+      <span class="coordinate-value">{{ pickedPosition.x }}</span>
+    </div>
+    <div class="coordinate-item">
+      <span class="coordinate-label">Y:</span>
+      <span class="coordinate-value">{{ pickedPosition.y }}</span>
+    </div>
+    <div class="coordinate-item">
+      <span class="coordinate-label">Z:</span>
+      <span class="coordinate-value">{{ pickedPosition.z }}</span>
+    </div>
+  </div>
+
+  <div v-if="showCameraPanel" class="panel camera-panel">
+    <div class="panel-header">
+      <span class="panel-title">相机信息</span>
+      <button class="toggle-btn" @click="showCameraPanel = false">×</button>
+    </div>
+    <div class="camera-section">
+      <div class="section-label">位置 (cm)</div>
+      <div class="camera-item">
+        <span class="camera-label">X:</span>
+        <span class="camera-value">{{ cameraInfo.position.x }}</span>
+      </div>
+      <div class="camera-item">
+        <span class="camera-label">Y:</span>
+        <span class="camera-value">{{ cameraInfo.position.y }}</span>
+      </div>
+      <div class="camera-item">
+        <span class="camera-label">Z:</span>
+        <span class="camera-value">{{ cameraInfo.position.z }}</span>
+      </div>
+    </div>
+    <div class="camera-section">
+      <div class="section-label">目标点 (cm)</div>
+      <div class="camera-item">
+        <span class="camera-label">X:</span>
+        <span class="camera-value">{{ cameraInfo.target.x }}</span>
+      </div>
+      <div class="camera-item">
+        <span class="camera-label">Y:</span>
+        <span class="camera-value">{{ cameraInfo.target.y }}</span>
+      </div>
+      <div class="camera-item">
+        <span class="camera-label">Z:</span>
+        <span class="camera-value">{{ cameraInfo.target.z }}</span>
+      </div>
+    </div>
+    <div class="camera-section">
+      <div class="section-label">缩放距离 (m)</div>
+      <div class="camera-item">
+        <span class="camera-label">当前:</span>
+        <span class="camera-value">{{ cameraInfo.distance }}</span>
+      </div>
+      <div class="camera-item">
+        <span class="camera-label">最近:</span>
+        <span class="camera-value">{{ cameraInfo.minDistance }}m</span>
+      </div>
+      <div class="camera-item">
+        <span class="camera-label">最远:</span>
+        <span class="camera-value">{{ cameraInfo.maxDistance }}m</span>
+      </div>
+    </div>
+  </div>
+
+  <div v-if="showWaterPanel" class="panel water-panel">
+    <div class="panel-header">
+      <span class="panel-title">水面参数调节</span>
+      <button class="toggle-btn" @click="showWaterPanel = false">×</button>
+    </div>
+    <div class="water-section">
+      <div class="section-label">透明度</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.alpha" min="0" max="1" step="0.01" />
+        <span class="slider-value">{{ waterParams.alpha.toFixed(2) }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">水流速度</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.flowSpeed" min="0" max="3" step="0.1" />
+        <span class="slider-value">{{ waterParams.flowSpeed.toFixed(1) }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">流向 X</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.flowDirectionX" min="-2" max="2" step="0.1" />
+        <span class="slider-value">{{ waterParams.flowDirectionX.toFixed(1) }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">流向 Z</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.flowDirectionY" min="-2" max="2" step="0.1" />
+        <span class="slider-value">{{ waterParams.flowDirectionY.toFixed(1) }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">水颜色</div>
+      <div class="color-item">
+        <input type="color" v-model="waterParams.waterColor" />
+        <span class="color-value">{{ waterParams.waterColor }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">太阳光颜色</div>
+      <div class="color-item">
+        <input type="color" v-model="waterParams.sunColor" />
+        <span class="color-value">{{ waterParams.sunColor }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">波纹强度</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.distortionScale" min="0" max="100" step="1" />
+        <span class="slider-value">{{ waterParams.distortionScale.toFixed(0) }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">噪声缩放</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.noiseScale" min="0.1" max="5" step="0.1" />
+        <span class="slider-value">{{ waterParams.noiseScale.toFixed(1) }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">菲涅尔偏移</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.fresnelBias" min="0" max="1" step="0.01" />
+        <span class="slider-value">{{ waterParams.fresnelBias.toFixed(2) }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">菲涅尔功率</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.fresnelPower" min="0.1" max="10" step="0.1" />
+        <span class="slider-value">{{ waterParams.fresnelPower.toFixed(1) }}</span>
+      </div>
+    </div>
+    <div class="water-section">
+      <div class="section-label">菲涅尔强度</div>
+      <div class="slider-item">
+        <input type="range" v-model.number="waterParams.fresnelStrength" min="0" max="3" step="0.1" />
+        <span class="slider-value">{{ waterParams.fresnelStrength.toFixed(1) }}</span>
+      </div>
+    </div>
+  </div>
 </template>
 
 <style scoped>
@@ -184,4 +566,208 @@ onUnmounted(() => {
   height: 100vh;
   overflow: hidden;
 }
+
+.toolbar {
+  position: fixed;
+  top: 20px;
+  right: 20px;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  z-index: 1001;
+}
+
+.toolbar-btn {
+  padding: 10px 16px;
+  border: none;
+  border-radius: 8px;
+  background: rgba(0, 0, 0, 0.85);
+  color: #888;
+  font-size: 13px;
+  font-weight: bold;
+  cursor: pointer;
+  transition: all 0.2s;
+  min-width: 70px;
+}
+
+.toolbar-btn:hover {
+  background: rgba(30, 30, 30, 0.9);
+  color: #fff;
+}
+
+.toolbar-btn.active {
+  color: #fff;
+  box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
+}
+
+.toolbar-btn:nth-child(1).active { color: #4fc3f7; }
+.toolbar-btn:nth-child(2).active { color: #ff9800; }
+.toolbar-btn:nth-child(3).active { color: #4dd0e1; }
+
+.panel {
+  position: fixed;
+  top: 20px;
+  left: 20px;
+  background: rgba(0, 0, 0, 0.85);
+  color: white;
+  padding: 15px;
+  border-radius: 8px;
+  font-family: 'Courier New', monospace;
+  z-index: 1000;
+  max-height: calc(100vh - 40px);
+  overflow-y: auto;
+}
+
+.coordinate-panel { min-width: 200px; }
+.camera-panel { min-width: 200px; }
+.water-panel { min-width: 240px; }
+
+.panel-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.3);
+}
+
+.panel-title {
+  font-weight: bold;
+  font-size: 14px;
+}
+
+.coordinate-panel .panel-title { color: #4fc3f7; }
+.camera-panel .panel-title { color: #ff9800; }
+.water-panel .panel-title { color: #4dd0e1; }
+
+.toggle-btn {
+  width: 22px;
+  height: 22px;
+  border: none;
+  border-radius: 4px;
+  background: rgba(255, 255, 255, 0.15);
+  color: white;
+  font-size: 14px;
+  line-height: 1;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: background 0.2s;
+}
+
+.toggle-btn:hover {
+  background: rgba(255, 255, 255, 0.3);
+}
+
+.coordinate-item {
+  display: flex;
+  justify-content: space-between;
+  margin: 5px 0;
+}
+
+.coordinate-label {
+  color: #81c784;
+  font-weight: bold;
+}
+
+.coordinate-value {
+  color: #fff;
+}
+
+.camera-section {
+  margin-bottom: 10px;
+}
+
+.section-label {
+  color: #81c784;
+  font-size: 11px;
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+
+.camera-item {
+  display: flex;
+  justify-content: space-between;
+  margin: 3px 0;
+}
+
+.camera-label {
+  color: #4fc3f7;
+}
+
+.camera-value {
+  color: #fff;
+}
+
+.water-section {
+  margin-bottom: 12px;
+}
+
+.water-section .section-label {
+  color: #81c784;
+  font-size: 11px;
+  margin-bottom: 6px;
+  font-weight: bold;
+}
+
+.slider-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.slider-item input[type="range"] {
+  flex: 1;
+  height: 6px;
+  -webkit-appearance: none;
+  background: #444;
+  border-radius: 3px;
+  outline: none;
+}
+
+.slider-item input[type="range"]::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  width: 14px;
+  height: 14px;
+  background: #4dd0e1;
+  border-radius: 50%;
+  cursor: pointer;
+}
+
+.slider-value {
+  min-width: 45px;
+  text-align: right;
+  color: #fff;
+  font-size: 11px;
+}
+
+.color-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.color-item input[type="color"] {
+  width: 40px;
+  height: 28px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  background: transparent;
+}
+
+.color-item input[type="color"]::-webkit-color-swatch-wrapper {
+  padding: 0;
+}
+
+.color-item input[type="color"]::-webkit-color-swatch {
+  border: 1px solid rgba(255, 255, 255, 0.3);
+  border-radius: 4px;
+}
+
+.color-value {
+  color: #fff;
+  font-size: 11px;
+}
 </style>

+ 12 - 5
src/components/Water.ts

@@ -16,6 +16,7 @@ interface WaterOptions {
   fresnelBias?: number
   fresnelPower?: number
   fresnelStrength?: number
+  flowDirection?: THREE.Vector2
   side?: THREE.Side
   fog?: boolean
 }
@@ -37,7 +38,8 @@ const WaterShader = {
       waterColor: { value: new THREE.Color(0x555555) },
       fresnelBias: { value: 0.3 },
       fresnelPower: { value: 3.0 },
-      fresnelStrength: { value: 1.0 }
+      fresnelStrength: { value: 1.0 },
+      flowDirection: { value: new THREE.Vector2(1.0, 1.0) }
     }
   ]),
   vertexShader: [
@@ -84,6 +86,7 @@ const WaterShader = {
     'uniform float fresnelBias;',
     'uniform float fresnelPower;',
     'uniform float fresnelStrength;',
+    'uniform vec2 flowDirection;',
 
     'varying vec4 mirrorCoord;',
     'varying vec3 worldPosition;',
@@ -102,10 +105,11 @@ const WaterShader = {
 
     'vec3 getNoise(in vec2 uv)',
     '{',
-    '  vec2 uv0 = uv / (103.0 * noiseScale) + vec2(time / 17.0, time / 29.0);',
-    '  vec2 uv1 = uv / (107.0 * noiseScale) - vec2(time / -19.0, time / 31.0);',
-    '  vec2 uv2 = uv / (vec2(8907.0, 9803.0) * noiseScale) + vec2(time / 101.0, time / 97.0);',
-    '  vec2 uv3 = uv / (vec2(1091.0, 1027.0) * noiseScale) - vec2(time / 109.0, time / -113.0);',
+    '  vec2 flow = flowDirection * time * 0.05;',
+    '  vec2 uv0 = uv / (103.0 * noiseScale) + flow;',
+    '  vec2 uv1 = uv / (107.0 * noiseScale) - flow.yx;',
+    '  vec2 uv2 = uv / (vec2(8907.0, 9803.0) * noiseScale) + flow * 0.5;',
+    '  vec2 uv3 = uv / (vec2(1091.0, 1027.0) * noiseScale) - flow * 0.3;',
     '  vec4 noise = texture2D(normalSampler, uv0) +',
     '    texture2D(normalSampler, uv1) +',
     '    texture2D(normalSampler, uv2) +',
@@ -171,6 +175,7 @@ export class Water extends THREE.Object3D {
   fresnelBias: number
   fresnelPower: number
   fresnelStrength: number
+  flowDirection: THREE.Vector2
   side: THREE.Side
   fog: boolean
   matrixNeedsUpdate: boolean
@@ -206,6 +211,7 @@ export class Water extends THREE.Object3D {
     this.fresnelBias = options.fresnelBias ?? 0.3
     this.fresnelPower = options.fresnelPower ?? 3.0
     this.fresnelStrength = options.fresnelStrength ?? 1.0
+    this.flowDirection = options.flowDirection ?? new THREE.Vector2(1.0, 1.0)
     this.side = options.side ?? THREE.FrontSide
     this.fog = options.fog ?? false
     this.matrixNeedsUpdate = true
@@ -252,6 +258,7 @@ export class Water extends THREE.Object3D {
     this.material.uniforms.fresnelPower.value = this.fresnelPower
     this.material.uniforms.fresnelStrength.value = this.fresnelStrength
     this.material.uniforms.eye.value = this.eye
+    this.material.uniforms.flowDirection.value = this.flowDirection
 
     if (!THREE.MathUtils.isPowerOfTwo(textureWidth) || !THREE.MathUtils.isPowerOfTwo(textureHeight)) {
       this.texture.texture.generateMipmaps = false