Ver código fonte

添加闸门启动功能和水花效果

WQQ 12 horas atrás
pai
commit
f77b24089c
51 arquivos alterados com 2405 adições e 723 exclusões
  1. 162 0
      RuoYi-Vue3/src/materials/StylizedWaterMaterial.js
  2. 382 0
      RuoYi-Vue3/src/materials/Water.ts
  3. 307 0
      RuoYi-Vue3/src/materials/WaterFoamMaterial.js
  4. 166 0
      RuoYi-Vue3/src/materials/waterFlow.ts
  5. 111 0
      RuoYi-Vue3/src/materials/waterFoam.ts
  6. 198 0
      RuoYi-Vue3/src/materials/waterFoamUE.ts
  7. 354 0
      RuoYi-Vue3/src/materials/waterNew.ts
  8. 514 563
      RuoYi-Vue3/src/supermap-cesium-module/business-scenes/JuKouShuiZhaScene.js
  9. 38 3
      RuoYi-Vue3/src/supermap-cesium-module/components/gate-control/GateControl.vue
  10. 35 2
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/pick-coordinate/pick-coordinate.vue
  11. 15 35
      RuoYi-Vue3/src/supermap-cesium-module/config/fixedScenes.js
  12. 123 120
      RuoYi-Vue3/src/supermap-cesium-module/views/layout/aside.vue
  13. BIN
      RuoYi-Vue3/src/texture/FlowTexture/MASK_003.PNG
  14. BIN
      RuoYi-Vue3/src/texture/FlowTexture/T_FlowTexture_BC.PNG
  15. BIN
      RuoYi-Vue3/src/texture/FlowTexture/T_FlowTexture_BC_NORM.PNG
  16. BIN
      RuoYi-Vue3/src/texture/FlowTexture/T_FoamMacro_BC.PNG
  17. BIN
      RuoYi-Vue3/src/texture/FlowTexture/T_FoamMacro_BC_NORM.PNG
  18. BIN
      RuoYi-Vue3/src/texture/T_Waterfall_Foam.PNG
  19. BIN
      RuoYi-Vue3/src/texture/T_Waterfall_Foam_Directional.PNG
  20. BIN
      RuoYi-Vue3/src/texture/Water_1_Normal.PNG
  21. BIN
      RuoYi-Vue3/src/texture/Water_2_Normal.PNG
  22. BIN
      RuoYi-Vue3/src/texture/Water_3_Normal.PNG
  23. BIN
      RuoYi-Vue3/src/texture/sky.jpg
  24. BIN
      ruoyi-admin/target/classes/com/ruoyi/RuoYiApplication.class
  25. BIN
      ruoyi-admin/target/classes/com/ruoyi/RuoYiServletInitializer.class
  26. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/cesium/CesiumMapConfigController.class
  27. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/common/CaptchaController.class
  28. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/common/CommonController.class
  29. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/CacheController.class
  30. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/ServerController.class
  31. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/SysLogininforController.class
  32. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/SysOperlogController.class
  33. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/SysUserOnlineController.class
  34. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysConfigController.class
  35. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysDeptController.class
  36. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysDictDataController.class
  37. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysDictTypeController.class
  38. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysIndexController.class
  39. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysLoginController.class
  40. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysMenuController.class
  41. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysNoticeController.class
  42. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysPostController.class
  43. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysProfileController.class
  44. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysRegisterController.class
  45. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysRoleController.class
  46. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysUserController.class
  47. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/tool/TestController.class
  48. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/tool/UserEntity.class
  49. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/watershed/WatershedModelController.class
  50. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/watershed/WatershedServiceController.class
  51. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/core/config/SwaggerConfig.class

+ 162 - 0
RuoYi-Vue3/src/materials/StylizedWaterMaterial.js

@@ -0,0 +1,162 @@
+/**
+ * Cesium 风格化水面材质
+ * 简化天空反射 - 直接基于 UV 采样
+ */
+
+// 片段着色器
+const StylizedWaterMaterialShader = `
+uniform float uSwTime;
+uniform sampler2D uSwNormalMap1;
+uniform sampler2D uSwNormalMap2;
+uniform sampler2D uSwNormalMap3;
+uniform float uSwNormalTiling;
+uniform float uSwNormalStrength;
+uniform float uSwFlowSpeed;
+uniform vec2 uSwFlowDir;
+uniform vec4 uSwShallowColor;
+uniform vec4 uSwDeepColor;
+uniform float uSwAlpha;
+uniform sampler2D uSwSkyTexture;
+uniform float uSwSkyIntensity;
+
+// 简化噪声
+float hash(vec2 p) {
+    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
+}
+
+float noise(vec2 p) {
+    vec2 i = floor(p);
+    vec2 f = fract(p);
+    f = f * f * (3.0 - 2.0 * f);
+    float a = hash(i);
+    float b = hash(i + vec2(1.0, 0.0));
+    float c = hash(i + vec2(0.0, 1.0));
+    float d = hash(i + vec2(1.0, 1.0));
+    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
+czm_material czm_getMaterial(czm_materialInput materialInput)
+{
+    czm_material material = czm_getDefaultMaterial(materialInput);
+
+    vec2 st = materialInput.st;
+    float time = uSwTime * 0.5;
+
+    // 流动方向
+    vec2 flowDir = normalize(uSwFlowDir);
+
+    // 流动 UV
+    vec2 waterUV = fract(st * uSwNormalTiling + time * flowDir * uSwFlowSpeed * 0.1);
+
+    // 采样3层法线纹理
+    vec3 n1 = texture2D(uSwNormalMap1, waterUV).rgb * 2.0 - 1.0;
+    vec3 n2 = texture2D(uSwNormalMap2, fract(waterUV * 1.3 + 0.5)).rgb * 2.0 - 1.0;
+    vec3 n3 = texture2D(uSwNormalMap3, fract(waterUV * 0.7 + 1.0)).rgb * 2.0 - 1.0;
+
+    // 合成法线
+    vec3 normalMap = normalize(n1 + n2 + n3);
+    vec3 finalNormal = normalize(vec3(
+        normalMap.x * uSwNormalStrength,
+        normalMap.y * uSwNormalStrength,
+        1.0
+    ));
+
+    // 光照计算
+    vec3 sunDir = normalize(vec3(0.5, 0.8, 0.3));
+    vec3 viewDir = vec3(0.0, 0.0, 1.0);
+
+    float NdotL = max(dot(finalNormal, sunDir), 0.0);
+    float NdotV = max(dot(finalNormal, viewDir), 0.0);
+    vec3 halfVec = normalize(viewDir + sunDir);
+    float NdotH = max(dot(finalNormal, halfVec), 0.0);
+
+    // 高光
+    float specular = pow(NdotH, 64.0) * 2.0;
+
+    // 菲涅尔 - 强化
+    float fresnel = pow(1.0 - NdotV, 1.5) * 1.5;
+
+    // 水色
+    vec3 waterCol = mix(uSwShallowColor.rgb, uSwDeepColor.rgb, 0.5);
+
+    // 泡沫
+    float foamNoise = noise(st * 10.0 + time * 0.2);
+    float foam = foamNoise * 0.25;
+
+    // 光照
+    vec3 ambient = vec3(0.45, 0.50, 0.55);
+    vec3 sunLight = vec3(1.8, 1.7, 1.5);
+    vec3 lighting = ambient + NdotL * sunLight;
+
+    // 基础颜色
+    vec3 finalColor = waterCol * lighting;
+    finalColor += specular * sunLight * 0.4;
+
+    // 天空反射 - 简化:直接用 UV 采样,加入法线扰动
+    vec2 skyUV = st * 0.5 + finalNormal.xy * 0.15 + time * 0.02;
+    skyUV = fract(skyUV);
+    vec3 skyColor = texture2D(uSwSkyTexture, skyUV).rgb;
+    
+    // 强化菲涅尔反射混合
+    float reflectStrength = fresnel * uSwSkyIntensity + 0.3;
+    finalColor = mix(finalColor, skyColor, reflectStrength);
+
+    // 泡沫混合
+    vec3 foamColor = vec3(0.95, 0.97, 1.0);
+    finalColor = mix(finalColor, foamColor, foam);
+
+    material.diffuse = finalColor;
+    material.alpha = uSwAlpha;
+    material.emission = vec3(0.0);
+
+    return material;
+}
+`;
+
+/**
+ * 创建风格化水面材质
+ */
+function createStylizedWaterMaterial(options = {}) {
+  const Cesium = window.Cesium;
+
+  console.log('🔵 创建水面材质...');
+  console.log('  - 天空纹理:', options.skyTexture);
+
+  return new Cesium.Material({
+    fabric: {
+      type: 'StylizedWater_' + Date.now(),
+      uniforms: {
+        uSwTime: 0,
+        uSwNormalMap1: options.normalMap1 || '',
+        uSwNormalMap2: options.normalMap2 || '',
+        uSwNormalMap3: options.normalMap3 || '',
+        uSwNormalTiling: options.waterNormalTiling || 0.26,
+        uSwNormalStrength: options.waterNormalStrength || 2.0,
+        uSwFlowSpeed: options.flowSpeed || 0.5,
+        uSwFlowDir: new Cesium.Cartesian2(1.0, 0.0),
+        uSwShallowColor: new Cesium.Color(0.42, 0.61, 0.54, 1.0),
+        uSwDeepColor: new Cesium.Color(0.04, 0.16, 0.29, 1.0),
+        uSwAlpha: options.alpha || 0.85,
+        uSwSkyTexture: options.skyTexture || '',
+        uSwSkyIntensity: options.skyTexture ? 0.8 : 0.0
+      },
+      source: StylizedWaterMaterialShader
+    },
+    translucent: true
+  });
+}
+
+/**
+ * 销毁动画处理器
+ */
+function destroyStylizedWaterAnimation(animationHandler) {
+  if (animationHandler) {
+    animationHandler();
+  }
+}
+
+export {
+  createStylizedWaterMaterial,
+  destroyStylizedWaterAnimation,
+  StylizedWaterMaterialShader
+};

+ 382 - 0
RuoYi-Vue3/src/materials/Water.ts

@@ -0,0 +1,382 @@
+import * as THREE from 'three'
+
+interface WaterOptions {
+  textureWidth?: number
+  textureHeight?: number
+  clipBias?: number
+  alpha?: number
+  time?: number
+  waterNormals?: THREE.Texture
+  sunDirection?: THREE.Vector3
+  sunColor?: THREE.Color | string | number
+  waterColor?: THREE.Color | string | number
+  eye?: THREE.Vector3
+  distortionScale?: number
+  noiseScale?: number
+  fresnelBias?: number
+  fresnelPower?: number
+  fresnelStrength?: number
+  flowDirection?: THREE.Vector2
+  side?: THREE.Side
+  fog?: boolean
+}
+
+const WaterShader = {
+  uniforms: THREE.UniformsUtils.merge([
+    THREE.UniformsLib['fog'],
+    {
+      normalSampler: { value: null },
+      mirrorSampler: { value: null },
+      alpha: { value: 1.0 },
+      time: { value: 0.0 },
+      distortionScale: { value: 20.0 },
+      noiseScale: { value: 1.0 },
+      textureMatrix: { value: new THREE.Matrix4() },
+      sunColor: { value: new THREE.Color(0x7f7f7f) },
+      sunDirection: { value: new THREE.Vector3(0.70707, 0.70707, 0) },
+      eye: { value: new THREE.Vector3(0, 0, 0) },
+      waterColor: { value: new THREE.Color(0x555555) },
+      fresnelBias: { value: 0.3 },
+      fresnelPower: { value: 3.0 },
+      fresnelStrength: { value: 1.0 },
+      flowDirection: { value: new THREE.Vector2(1.0, 1.0) }
+    }
+  ]),
+  vertexShader: [
+    'uniform mat4 textureMatrix;',
+    'uniform float time;',
+
+    'varying vec4 mirrorCoord;',
+    'varying vec3 worldPosition;',
+    'varying vec3 modelPosition;',
+    'varying vec3 surfaceX;',
+    'varying vec3 surfaceY;',
+    'varying vec3 surfaceZ;',
+
+    THREE.ShaderChunk.common,
+    THREE.ShaderChunk.fog_pars_vertex,
+
+    'void main()',
+    '{',
+    '  mirrorCoord = modelMatrix * vec4(position, 1.0);',
+    '  worldPosition = mirrorCoord.xyz;',
+    '  modelPosition = position;',
+    '  surfaceX = vec3(modelMatrix[0][0], modelMatrix[0][1], modelMatrix[0][2]);',
+    '  surfaceY = vec3(modelMatrix[1][0], modelMatrix[1][1], modelMatrix[1][2]);',
+    '  surfaceZ = vec3(modelMatrix[2][0], modelMatrix[2][1], modelMatrix[2][2]);',
+
+    '  mirrorCoord = textureMatrix * mirrorCoord;',
+    '  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);',
+    '  gl_Position = projectionMatrix * mvPosition;',
+
+    THREE.ShaderChunk.fog_vertex,
+    '}'
+  ].join('\n'),
+  fragmentShader: [
+    'uniform sampler2D mirrorSampler;',
+    'uniform float alpha;',
+    'uniform float time;',
+    'uniform float distortionScale;',
+    'uniform float noiseScale;',
+    'uniform sampler2D normalSampler;',
+    'uniform vec3 sunColor;',
+    'uniform vec3 sunDirection;',
+    'uniform vec3 eye;',
+    'uniform vec3 waterColor;',
+    'uniform float fresnelBias;',
+    'uniform float fresnelPower;',
+    'uniform float fresnelStrength;',
+    'uniform vec2 flowDirection;',
+
+    'varying vec4 mirrorCoord;',
+    'varying vec3 worldPosition;',
+    'varying vec3 modelPosition;',
+    'varying vec3 surfaceX;',
+    'varying vec3 surfaceY;',
+    'varying vec3 surfaceZ;',
+
+    'void sunLight(const vec3 surfaceNormal, const vec3 eyeDirection, in float shiny, in float spec, in float diffuse, inout vec3 diffuseColor, inout vec3 specularColor)',
+    '{',
+    '  vec3 reflection = normalize(reflect(-sunDirection, surfaceNormal));',
+    '  float direction = max(0.0, dot(eyeDirection, reflection));',
+    '  specularColor += pow(direction, shiny) * sunColor * spec;',
+    '  diffuseColor += max(dot(sunDirection, surfaceNormal), 0.0) * sunColor * diffuse;',
+    '}',
+
+    'vec3 getNoise(in vec2 uv)',
+    '{',
+    '  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) +',
+    '    texture2D(normalSampler, uv3);',
+    '  return noise.xyz * 0.5 - 1.0;',
+    '}',
+
+    THREE.ShaderChunk.common,
+    THREE.ShaderChunk.fog_pars_fragment,
+
+    'void main()',
+    '{',
+    '  vec3 worldToEye = eye - worldPosition;',
+    '  vec3 eyeDirection = normalize(worldToEye);',
+
+    '  vec3 noise = getNoise(modelPosition.xy * 1.0);',
+    '  vec3 distordCoord = noise.x * surfaceX + noise.y * surfaceY;',
+    '  vec3 distordNormal = distordCoord + surfaceZ;',
+
+    '  if (dot(eyeDirection, surfaceZ) < 0.0)',
+    '    distordNormal = distordNormal * -1.0;',
+
+    '  vec3 diffuseLight = vec3(0.0);',
+    '  vec3 specularLight = vec3(0.0);',
+    '  sunLight(distordNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight);',
+
+    '  float distance = length(worldToEye);',
+    '  vec2 distortion = distordCoord.xy * distortionScale * sqrt(distance) * 0.07;',
+    '  vec3 mirrorDistord = mirrorCoord.xyz + vec3(distortion.x, distortion.y, 1.0);',
+    '  vec3 reflectionSample = texture2DProj(mirrorSampler, mirrorDistord).xyz;',
+
+    '  float theta = max(dot(eyeDirection, distordNormal), 0.0);',
+    '  float reflectance = fresnelBias + (1.0 - fresnelBias) * pow((1.0 - theta), fresnelPower);',
+    '  reflectance *= fresnelStrength;',
+    '  vec3 scatter = max(0.0, dot(distordNormal, eyeDirection)) * waterColor;',
+
+    '  vec3 albedo = mix(sunColor * diffuseLight * 0.3 + scatter, (vec3(0.1) + reflectionSample * 0.9 + reflectionSample * specularLight), reflectance);',
+
+    '  vec3 outgoingLight = albedo;',
+
+    THREE.ShaderChunk.fog_fragment,
+
+    '  gl_FragColor = vec4(outgoingLight, alpha);',
+    '}'
+  ].join('\n')
+}
+
+export class Water extends THREE.Object3D {
+  material: THREE.ShaderMaterial
+  mirrorCamera: THREE.PerspectiveCamera
+  texture: THREE.WebGLRenderTarget
+  tempTexture: THREE.WebGLRenderTarget
+  clipBias: number
+  alpha: number
+  time: number
+  normalSampler: THREE.Texture | null
+  sunDirection: THREE.Vector3
+  sunColor: THREE.Color
+  waterColor: THREE.Color
+  eye: THREE.Vector3
+  distortionScale: number
+  noiseScale: number
+  fresnelBias: number
+  fresnelPower: number
+  fresnelStrength: number
+  flowDirection: THREE.Vector2
+  side: THREE.Side
+  fog: boolean
+  matrixNeedsUpdate: boolean
+
+  private renderer: THREE.WebGLRenderer
+  private scene: THREE.Scene
+  private camera: THREE.PerspectiveCamera
+  private mirrorPlane: THREE.Plane
+  private normal: THREE.Vector3
+  private cameraWorldPosition: THREE.Vector3
+  private rotationMatrix: THREE.Matrix4
+  private lookAtPosition: THREE.Vector3
+  private clipPlane: THREE.Vector4
+  private textureMatrix: THREE.Matrix4
+  private mesh: THREE.Object3D
+
+  constructor(renderer: THREE.WebGLRenderer, camera: THREE.PerspectiveCamera, scene: THREE.Scene, options: WaterOptions = {}) {
+    super()
+    this.name = 'water_' + this.id
+
+    const textureWidth = options.textureWidth ?? 512
+    const textureHeight = options.textureHeight ?? 512
+    this.clipBias = options.clipBias ?? -0.0001
+    this.alpha = options.alpha ?? 1.0
+    this.time = options.time ?? 0.0
+    this.normalSampler = options.waterNormals ?? null
+    this.sunDirection = options.sunDirection ?? new THREE.Vector3(0.70707, 0.70707, 0.0)
+    this.sunColor = new THREE.Color(options.sunColor ?? 0xffffff)
+    this.waterColor = new THREE.Color(options.waterColor ?? 0x7f7f7f)
+    this.eye = options.eye ?? new THREE.Vector3(0, 0, 0)
+    this.distortionScale = options.distortionScale ?? 20.0
+    this.noiseScale = options.noiseScale ?? 1.0
+    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
+
+    this.renderer = renderer
+    this.scene = scene
+    this.camera = camera
+
+    this.mirrorPlane = new THREE.Plane()
+    this.normal = new THREE.Vector3(0, 0, 1)
+    this.cameraWorldPosition = new THREE.Vector3()
+    this.rotationMatrix = new THREE.Matrix4()
+    this.lookAtPosition = new THREE.Vector3(0, 0, -1)
+    this.clipPlane = new THREE.Vector4()
+
+    this.textureMatrix = new THREE.Matrix4()
+    this.mirrorCamera = this.camera.clone()
+
+    this.texture = new THREE.WebGLRenderTarget(textureWidth, textureHeight)
+    this.tempTexture = new THREE.WebGLRenderTarget(textureWidth, textureHeight)
+
+    const mirrorUniforms = THREE.UniformsUtils.clone(WaterShader.uniforms)
+    this.material = new THREE.ShaderMaterial({
+      fragmentShader: WaterShader.fragmentShader,
+      vertexShader: WaterShader.vertexShader,
+      uniforms: mirrorUniforms,
+      transparent: true,
+      side: this.side,
+      fog: this.fog
+    })
+
+    this.mesh = new THREE.Object3D()
+    this.material.uniforms.mirrorSampler.value = this.texture.texture
+    this.material.uniforms.textureMatrix.value = this.textureMatrix
+    this.material.uniforms.alpha.value = this.alpha
+    this.material.uniforms.time.value = this.time
+    this.material.uniforms.normalSampler.value = this.normalSampler
+    this.material.uniforms.sunColor.value = this.sunColor
+    this.material.uniforms.waterColor.value = this.waterColor
+    this.material.uniforms.sunDirection.value = this.sunDirection
+    this.material.uniforms.distortionScale.value = this.distortionScale
+    this.material.uniforms.noiseScale.value = this.noiseScale
+    this.material.uniforms.fresnelBias.value = this.fresnelBias
+    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
+      this.tempTexture.texture.generateMipmaps = false
+    }
+  }
+
+  updateTextureMatrix(): void {
+    if (this.parent !== null && this.parent !== undefined) {
+      this.mesh = this.parent
+    }
+
+    const sign = (x: number) => x ? (x < 0 ? -1 : 1) : 0
+
+    this.updateMatrixWorld()
+    this.camera.updateMatrixWorld()
+
+    this.cameraWorldPosition.setFromMatrixPosition(this.camera.matrixWorld)
+
+    this.rotationMatrix.extractRotation(this.matrixWorld)
+    this.normal.set(0, 0, 1).applyEuler(this.mesh.rotation)
+
+    const cameraPosition = new THREE.Vector3().copy(this.camera.position).sub(this.mesh.position)
+    if (this.normal.dot(cameraPosition) < 0) {
+      const meshNormal = new THREE.Vector3(0, 0, 1).applyEuler(this.mesh.rotation)
+      this.normal.reflect(meshNormal)
+    }
+
+    const view = new THREE.Vector3().copy(this.mesh.position).sub(this.cameraWorldPosition)
+    view.reflect(this.normal).negate()
+    view.add(this.mesh.position)
+
+    this.rotationMatrix.extractRotation(this.camera.matrixWorld)
+    this.lookAtPosition.set(0, 0, -1)
+    this.lookAtPosition.applyMatrix4(this.rotationMatrix)
+    this.lookAtPosition.add(this.cameraWorldPosition)
+
+    const target = new THREE.Vector3().copy(this.mesh.position).sub(this.lookAtPosition)
+    target.reflect(this.normal).negate()
+    target.add(this.mesh.position)
+
+    const up = new THREE.Vector3(0, -1, 0)
+    up.applyMatrix4(this.rotationMatrix)
+    up.reflect(this.normal).negate()
+
+    this.mirrorCamera.position.copy(view)
+    this.mirrorCamera.up.copy(up)
+    this.mirrorCamera.lookAt(target)
+    this.mirrorCamera.aspect = this.camera.aspect
+    this.mirrorCamera.updateProjectionMatrix()
+    this.mirrorCamera.updateMatrixWorld()
+    this.mirrorCamera.matrixWorldInverse.copy(this.mirrorCamera.matrixWorld).invert()
+
+    this.textureMatrix.set(
+      0.5, 0.0, 0.0, 0.5,
+      0.0, 0.5, 0.0, 0.5,
+      0.0, 0.0, 0.5, 0.5,
+      0.0, 0.0, 0.0, 1.0
+    )
+    this.textureMatrix.multiply(this.mirrorCamera.projectionMatrix)
+    this.textureMatrix.multiply(this.mirrorCamera.matrixWorldInverse)
+
+    this.mirrorPlane.setFromNormalAndCoplanarPoint(this.normal, this.mesh.position)
+    this.mirrorPlane.applyMatrix4(this.mirrorCamera.matrixWorldInverse)
+
+    this.clipPlane.set(
+      this.mirrorPlane.normal.x,
+      this.mirrorPlane.normal.y,
+      this.mirrorPlane.normal.z,
+      this.mirrorPlane.constant
+    )
+
+    const q = new THREE.Vector4()
+    const projectionMatrix = this.mirrorCamera.projectionMatrix
+    q.x = (sign(this.clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0]
+    q.y = (sign(this.clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5]
+    q.z = -1.0
+    q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14]
+
+    const c = this.clipPlane.clone().multiplyScalar(2.0 / this.clipPlane.dot(q))
+    projectionMatrix.elements[2] = c.x
+    projectionMatrix.elements[6] = c.y
+    projectionMatrix.elements[10] = c.z + 1.0 - this.clipBias
+    projectionMatrix.elements[14] = c.w
+
+    const worldCoordinates = new THREE.Vector3().setFromMatrixPosition(this.camera.matrixWorld)
+    this.eye.copy(worldCoordinates)
+    this.material.uniforms.eye.value.copy(this.eye)
+    this.material.uniforms.sunDirection.value.copy(this.sunDirection)
+  }
+
+  render(isTempTexture?: boolean): void {
+    if (this.matrixNeedsUpdate) {
+      this.updateTextureMatrix()
+    }
+    this.matrixNeedsUpdate = true
+
+    if (this.scene !== undefined && this.scene instanceof THREE.Scene) {
+      this.material.visible = false
+
+      const renderTexture = isTempTexture ? this.tempTexture : this.texture
+      const prevRenderTarget = this.renderer.getRenderTarget()
+      this.renderer.setRenderTarget(renderTexture)
+      this.renderer.clear()
+      this.renderer.render(this.scene, this.mirrorCamera)
+      this.renderer.setRenderTarget(prevRenderTarget)
+
+      this.material.visible = true
+      this.material.uniforms.mirrorSampler.value = renderTexture.texture
+    }
+  }
+}
+
+export function createWaterNormalTexture(loader: THREE.TextureLoader): THREE.Texture {
+  const texture = loader.load(
+    new URL('../assets/waternormals.jpg', import.meta.url).href
+  )
+  texture.wrapS = THREE.RepeatWrapping
+  texture.wrapT = THREE.RepeatWrapping
+  return texture
+}

+ 307 - 0
RuoYi-Vue3/src/materials/WaterFoamMaterial.js

@@ -0,0 +1,307 @@
+/**
+ * Cesium 水泡沫材质
+ * 由 Three.js waterFoam.ts 移植而来
+ * 独立、可复用的材质模块
+ */
+
+// 片段着色器 - Material fabric 方式(适用于 Primitive + MaterialAppearance)
+const WaterFoamMaterialShader = `
+uniform vec4 uColour;
+uniform float uErosion;
+uniform float uFlowSpeed;
+uniform float uCircleRadius;
+uniform float uCircleSoftness;
+uniform float uCircleEnabled;
+uniform vec2 uCircleCenter;
+uniform sampler2D uFoamTex;
+uniform float uTime;
+
+// 径向渐变(指数)
+float radialGradientExponential(vec2 uv, vec2 center, float radius, float density) {
+    float dist = length(uv - center);
+    dist = clamp(dist / radius, 0.0, 1.0);
+    return pow(1.0 - dist, density);
+}
+
+// UV 平移(用于动画)
+vec2 panner(vec2 uv, float speedX, float speedY, float time) {
+    return uv + vec2(speedX, speedY) * time;
+}
+
+// 圆形遮罩
+float circleMask(vec2 uv, vec2 center, float radius, float softness) {
+    float dist = length(uv - center);
+    return 1.0 - smoothstep(radius - softness, radius + softness, dist);
+}
+
+czm_material czm_getMaterial(czm_materialInput materialInput)
+{
+    czm_material material = czm_getDefaultMaterial(materialInput);
+    
+    vec2 st = materialInput.st;
+    vec4 vParticleColor = vec4(1.0);
+    
+    // 动画UV - 流动效果,使用 fract 确保循环
+    vec2 animatedUV = fract(panner(st, uFlowSpeed, 0.0, uTime));
+    
+    // 采样泡沫纹理
+    vec4 foamTex = texture2D(uFoamTex, animatedUV);
+    
+    // 径向渐变
+    float gradient = radialGradientExponential(st, vec2(0.5), 1.0, 1.0);
+    
+    // 计算透明度形状(与 Three.js 原始逻辑一致)
+    float addResult = vParticleColor.r * foamTex.r + vParticleColor.g * foamTex.g;
+    float subtractResult = addResult - uErosion;
+    float multiplyResult = subtractResult * gradient;
+    
+    float opacityShape = mix(multiplyResult, addResult, gradient);
+    opacityShape = clamp(opacityShape, 0.0, 1.0);
+    opacityShape = pow(opacityShape, 2.0);
+    
+    // 圆形遮罩
+    float circle = circleMask(st, uCircleCenter, uCircleRadius, uCircleSoftness);
+    opacityShape *= mix(1.0, circle, uCircleEnabled);
+    
+    // 自发光颜色(与 Three.js 原始逻辑一致:uColour * vParticleColor.rgb)
+    vec3 emissive = uColour.rgb * vParticleColor.rgb;
+    
+    // 设置材质属性
+    material.diffuse = uColour.rgb;
+    material.alpha = opacityShape * uColour.a;
+    material.emission = emissive;
+    
+    return material;
+}
+`;
+
+/**
+ * 创建水泡沫材质(用于 Primitive + MaterialAppearance)
+ * @param {Object} options 材质选项
+ * @param {Cesium.Color} options.color 材质颜色(默认白色泡沫)
+ * @param {Number} options.erosion 侵蚀程度 (0-1),默认 1.0
+ * @param {Number} options.flowSpeed 流动速度,默认 0.2
+ * @param {String|THREE.Texture} options.foamTexture 泡沫纹理路径或纹理对象
+ * @param {Cesium.Cartesian2} options.circleCenter 圆形遮罩中心
+ * @param {Number} options.circleRadius 圆形遮罩半径
+ * @param {Number} options.circleSoftness 圆形遮罩软化度
+ * @param {Boolean} options.circleEnabled 是否启用圆形遮罩
+ * @returns {Cesium.Material} 材质实例
+ */
+function createWaterFoamMaterial(options = {}) {
+  const Cesium = window.Cesium;
+
+  const defaultOptions = {
+    color: new Cesium.Color(1.0, 1.0, 1.0, 1.0),  // 白色泡沫
+    erosion: 1.0,
+    flowSpeed: 0.2,
+    foamTexture: '',
+    circleCenter: new Cesium.Cartesian2(0.5, 0.5),
+    circleRadius: 0.34,
+    circleSoftness: 0.195,
+    circleEnabled: true
+  };
+
+  const opts = Object.assign({}, defaultOptions, options);
+
+  return new Cesium.Material({
+    fabric: {
+      type: 'WaterFoam_' + Date.now(),
+      uniforms: {
+        uColour: opts.color,
+        uErosion: opts.erosion,
+        uFlowSpeed: opts.flowSpeed,
+        uFoamTex: opts.foamTexture,
+        uCircleCenter: opts.circleCenter,
+        uCircleRadius: opts.circleRadius,
+        uCircleSoftness: opts.circleSoftness,
+        uCircleEnabled: opts.circleEnabled ? 1.0 : 0.0,
+        uTime: 0
+      },
+      source: WaterFoamMaterialShader
+    },
+    translucent: true
+  });
+}
+
+/**
+ * 创建水泡沫平面 Primitive
+ * @param {Object} options 配置选项
+ * @param {Cesium.Viewer} viewer Cesium Viewer 实例
+ * @param {Cesium.Rectangle} options.rectangle 矩形区域
+ * @param {Number} options.height 海拔高度(米)
+ * @param {Number} options.width 平面宽度(米)
+ * @param {Number} options.height 平面高度(米)
+ * @param {Cesium.Color} options.color 材质颜色(默认白色泡沫)
+ * @param {Number} options.erosion 侵蚀程度 (0-1)
+ * @param {Number} options.flowSpeed 流动速度
+ * @param {String} options.foamTexture 泡沫纹理路径
+ * @param {Cesium.Cartesian2} options.circleCenter 圆形遮罩中心
+ * @param {Number} options.circleRadius 圆形遮罩半径
+ * @param {Number} options.circleSoftness 圆形遮罩软化度
+ * @param {Boolean} options.circleEnabled 是否启用圆形遮罩
+ * @returns {Object} { primitive, material, animationHandler }
+ */
+function createWaterFoamPrimitive(options, viewer) {
+  const Cesium = window.Cesium;
+
+  const defaultOptions = {
+    rectangle: Cesium.Rectangle.fromDegrees(0, 0, 1, 1),
+    height: 0,
+    color: new Cesium.Color(1.0, 1.0, 1.0, 1.0),  // 白色泡沫
+    erosion: 1.0,
+    flowSpeed: 0.2,
+    foamTexture: '',
+    circleCenter: new Cesium.Cartesian2(0.5, 0.5),
+    circleRadius: 0.34,
+    circleSoftness: 0.195,
+    circleEnabled: true
+  };
+
+  const opts = Object.assign({}, defaultOptions, options);
+
+  // 创建材质
+  const material = createWaterFoamMaterial(opts);
+
+  // 创建 RectangleGeometry - 需要包含纹理坐标
+  const rectangleGeometry = new Cesium.RectangleGeometry({
+    rectangle: opts.rectangle,
+    height: opts.height,
+    vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat
+  });
+
+  // 创建 GeometryInstance
+  const geometryInstance = new Cesium.GeometryInstance({
+    geometry: rectangleGeometry,
+    attributes: {
+      color: Cesium.ColorGeometryInstanceAttribute.fromColor(new Cesium.Color(1.0, 1.0, 1.0, 1.0))
+    }
+  });
+
+  // 创建 Primitive
+  const primitive = viewer.scene.primitives.add(new Cesium.Primitive({
+    geometryInstances: geometryInstance,
+    appearance: new Cesium.MaterialAppearance({
+      material: material,
+      flat: true,
+      aboveGround: true
+    }),
+    asynchronous: false
+  }));
+
+  // 存储材质引用和开始时间
+  primitive._waterFoamMaterial = material;
+  primitive._startTime = Date.now();
+
+  // 启动动画(使用 preRender 事件)
+  const animationHandler = viewer.scene.preRender.addEventListener(() => {
+    if (primitive && primitive._waterFoamMaterial) {
+      const time = (Date.now() - primitive._startTime) / 1000;
+      if (primitive._waterFoamMaterial.uniforms) {
+        primitive._waterFoamMaterial.uniforms.uTime = time;
+      }
+    }
+  });
+
+  console.log('✅ WaterFoam Primitive 已创建');
+
+  return {
+    primitive,
+    material,
+    animationHandler
+  };
+}
+
+/**
+ * 创建水泡沫平面 Entity(使用 rectangle)
+ * @param {Object} options 配置选项
+ * @param {Cesium.Viewer} viewer Cesium Viewer 实例
+ * @returns {Object} { entity, material, animationHandler }
+ */
+function createWaterFoamEntity(options, viewer) {
+  const Cesium = window.Cesium;
+
+  const defaultOptions = {
+    rectangle: Cesium.Rectangle.fromDegrees(0, 0, 1, 1),
+    height: 0,
+    color: new Cesium.Color(1.0, 1.0, 1.0, 1.0),  // 白色泡沫
+    erosion: 1.0,
+    flowSpeed: 0.2,
+    foamTexture: '',
+    circleCenter: new Cesium.Cartesian2(0.5, 0.5),
+    circleRadius: 0.34,
+    circleSoftness: 0.195,
+    circleEnabled: true
+  };
+
+  const opts = Object.assign({}, defaultOptions, options);
+
+  // 创建材质
+  const material = createWaterFoamMaterial(opts);
+
+  // 创建 Entity
+  const entity = viewer.entities.add({
+    name: 'WaterFoamPlane',
+    rectangle: {
+      coordinates: opts.rectangle,
+      height: opts.height,
+      material: material,
+      outline: true,
+      outlineColor: Cesium.Color.fromCssColorString('#4a90d9'),
+      outlineWidth: 2
+    }
+  });
+
+  // 存储材质引用和开始时间
+  entity._waterFoamMaterial = material;
+  entity._startTime = Date.now();
+
+  // 启动动画
+  const animationHandler = viewer.scene.preRender.addEventListener(() => {
+    if (entity && entity._waterFoamMaterial) {
+      const time = (Date.now() - entity._startTime) / 1000;
+      if (entity._waterFoamMaterial.uniforms) {
+        entity._waterFoamMaterial.uniforms.uTime = time;
+      }
+    }
+  });
+
+  console.log('✅ WaterFoam Entity 已创建');
+
+  return {
+    entity,
+    material,
+    animationHandler
+  };
+}
+
+/**
+ * 更新水泡沫材质的时间(用于手动动画更新)
+ * @param {Cesium.Material} material 材质实例
+ * @param {Number} startTime 开始时间
+ */
+function updateWaterFoamTime(material, startTime) {
+  if (!material || !material.uniforms) return;
+
+  const time = (Date.now() - startTime) / 1000;
+  material.uniforms.uTime = time;
+}
+
+/**
+ * 销毁水泡沫动画处理器
+ * @param {Function} animationHandler 动画处理器
+ */
+function destroyWaterFoamAnimation(animationHandler) {
+  if (animationHandler) {
+    animationHandler();
+  }
+}
+
+export {
+  createWaterFoamMaterial,
+  createWaterFoamPrimitive,
+  createWaterFoamEntity,
+  updateWaterFoamTime,
+  destroyWaterFoamAnimation,
+  WaterFoamMaterialShader
+};

+ 166 - 0
RuoYi-Vue3/src/materials/waterFlow.ts

@@ -0,0 +1,166 @@
+import * as THREE from 'three'
+
+const vertexShader = `
+varying vec2 vUv;
+
+void main() {
+    vUv = uv;
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+}
+`
+
+const fragmentShader = `
+uniform float uTime;
+uniform vec3 uColor;
+uniform float uSpeedX;
+uniform float uSpeedY;
+uniform float uTilingU;
+uniform float uTilingV;
+uniform float uRotationAngle;
+uniform float uPower;
+uniform float uWaterGate;
+uniform float uWaterGate02;
+uniform float uEdgeMaskIntensity;
+uniform float uOpaquePower;
+uniform float uEdgeFade;
+
+uniform sampler2D uFlowTexture;
+uniform sampler2D uFoamMacroTexture;
+uniform sampler2D uMaskTexture;
+uniform sampler2D uFlowNormalTexture;
+uniform sampler2D uFoamMacroNormalTexture;
+
+varying vec2 vUv;
+
+vec2 rotateUV(vec2 uv, vec2 center, float angle) {
+    float s = sin(angle);
+    float c = cos(angle);
+    uv -= center;
+    vec2 result = vec2(uv.x * c - uv.y * s, uv.x * s + uv.y * c);
+    result += center;
+    return result;
+}
+
+void main() {
+    vec2 tiling = vec2(uTilingU, uTilingV);
+    vec2 speed = vec2(uSpeedX, uSpeedY);
+    vec2 pannerUV = vUv * tiling + speed * uTime;
+
+    vec4 flowTex1 = texture2D(uFlowTexture, pannerUV);
+    vec4 foamTex1 = texture2D(uFoamMacroTexture, pannerUV);
+
+    vec2 rotCenter = vec2(0.5, 0.5);
+    float rotAngle = uRotationAngle * 6.28319;
+    vec2 rotatedUV = rotateUV(pannerUV, rotCenter, rotAngle);
+
+    vec4 flowTex2 = texture2D(uFlowTexture, rotatedUV);
+    vec4 foamTex2 = texture2D(uFoamMacroTexture, rotatedUV);
+
+    vec3 colorSum = flowTex1.rgb + foamTex1.rgb + flowTex2.rgb + foamTex2.rgb;
+
+    float foamIntensity = pow(max(dot(colorSum, vec3(1.0 / 3.0)), 0.0), uPower);
+
+    float vCoord = vUv.y;
+    float gateMask = mix(vCoord, 1.0 - vCoord, uWaterGate02);
+    float gateIntensity = pow(max(gateMask, 0.0), uWaterGate);
+
+    float edgeMask = texture2D(uMaskTexture, vUv).r;
+    float maskValue = mix(1.0, edgeMask, uEdgeMaskIntensity);
+
+    float combinedIntensity = foamIntensity * gateIntensity * maskValue;
+
+    float edgeFade = 1.0;
+    if (uEdgeFade > 0.0) {
+      edgeFade = 1.0 - smoothstep(1.0 - uEdgeFade, 1.0, 1.0 - vUv.y);
+    }
+    combinedIntensity *= edgeFade;
+
+    vec3 baseColor = combinedIntensity * uColor;
+
+    float opacity = combinedIntensity * uOpaquePower;
+    opacity = clamp(opacity, 0.0, 1.0);
+
+    vec3 normal1 = texture2D(uFlowNormalTexture, pannerUV).rgb;
+    vec3 normal2 = texture2D(uFoamMacroNormalTexture, pannerUV).rgb;
+    vec3 normal3 = texture2D(uFlowNormalTexture, rotatedUV).rgb;
+    vec3 normal4 = texture2D(uFoamMacroNormalTexture, rotatedUV).rgb;
+
+    vec3 normalSum = (normal1 + normal2 + normal3 + normal4) * 0.25;
+    vec3 normal = normalize(normalSum * 2.0 - 1.0);
+
+    gl_FragColor = vec4(uColor, opacity);
+}
+`
+
+interface WaterFlowMaterialOptions {
+  color?: THREE.Color
+  speedX?: number
+  speedY?: number
+  tilingU?: number
+  tilingV?: number
+  rotationAngle?: number
+  power?: number
+  waterGate?: number
+  waterGate02?: number
+  edgeMaskIntensity?: number
+  opaquePower?: number
+  edgeFade?: number
+  flowTexture?: THREE.Texture
+  foamMacroTexture?: THREE.Texture
+  maskTexture?: THREE.Texture
+  flowNormalTexture?: THREE.Texture
+  foamMacroNormalTexture?: THREE.Texture
+}
+
+function createWaterFlowMaterial(options: WaterFlowMaterialOptions = {}) {
+  const {
+    color = new THREE.Color(0xffffff),
+    speedX = 0.001,
+    speedY = 0.585,
+    tilingU = -1.0,
+    tilingV = 1.54,
+    rotationAngle = 0.5,
+    power = 0.538,
+    waterGate = 100.0,
+    waterGate02 = 0.0,
+    edgeMaskIntensity = 1.0,
+    opaquePower = 5.532769,
+    edgeFade = 0.15,
+    flowTexture,
+    foamMacroTexture,
+    maskTexture,
+    flowNormalTexture,
+    foamMacroNormalTexture,
+  } = options
+
+  return new THREE.ShaderMaterial({
+    vertexShader,
+    fragmentShader,
+    transparent: true,
+    depthWrite: false,
+    side: THREE.DoubleSide,
+    uniforms: {
+      uTime: { value: 0 },
+      uColor: { value: color },
+      uSpeedX: { value: speedX },
+      uSpeedY: { value: speedY },
+      uTilingU: { value: tilingU },
+      uTilingV: { value: tilingV },
+      uRotationAngle: { value: rotationAngle },
+      uPower: { value: power },
+      uWaterGate: { value: waterGate },
+      uWaterGate02: { value: waterGate02 },
+      uEdgeMaskIntensity: { value: edgeMaskIntensity },
+      uOpaquePower: { value: opaquePower },
+      uEdgeFade: { value: edgeFade },
+      uFlowTexture: { value: flowTexture || new THREE.Texture() },
+      uFoamMacroTexture: { value: foamMacroTexture || new THREE.Texture() },
+      uMaskTexture: { value: maskTexture || new THREE.Texture() },
+      uFlowNormalTexture: { value: flowNormalTexture || new THREE.Texture() },
+      uFoamMacroNormalTexture: { value: foamMacroNormalTexture || new THREE.Texture() },
+    },
+  })
+}
+
+export { createWaterFlowMaterial }
+export type { WaterFlowMaterialOptions }

+ 111 - 0
RuoYi-Vue3/src/materials/waterFoam.ts

@@ -0,0 +1,111 @@
+import * as THREE from 'three'
+
+const vertexShader = `
+varying vec2 vUv;
+varying vec4 vParticleColor;
+
+void main() {
+    vUv = uv;
+    vParticleColor = vec4(1.0);
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+}
+`
+
+const fragmentShader = `
+uniform float uTime;
+uniform vec3 uColour;
+uniform float uErosion;
+uniform float uFlowSpeed;
+uniform sampler2D uFoamTex;
+
+uniform vec2 uCircleCenter;
+uniform float uCircleRadius;
+uniform float uCircleSoftness;
+uniform float uCircleEnabled;
+
+varying vec2 vUv;
+varying vec4 vParticleColor;
+
+float radialGradientExponential(vec2 uv, vec2 center, float radius, float density) {
+    float dist = length(uv - center);
+    dist = clamp(dist / radius, 0.0, 1.0);
+    return pow(1.0 - dist, density);
+}
+
+vec2 panner(vec2 uv, float speedX, float speedY, float time) {
+    return uv + vec2(speedX, speedY) * time;
+}
+
+float circleMask(vec2 uv, vec2 center, float radius, float softness) {
+    float dist = length(uv - center);
+    return 1.0 - smoothstep(radius - softness, radius + softness, dist);
+}
+
+void main() {
+    vec2 animatedUV = panner(vUv, uFlowSpeed, 0.0, uTime);
+
+    vec4 foamTex = texture2D(uFoamTex, animatedUV);
+
+    float gradient = radialGradientExponential(vUv, vec2(0.5), 1.0, 1.0);
+
+    float addResult = vParticleColor.r * foamTex.r + vParticleColor.g * foamTex.g;
+    float subtractResult = addResult - uErosion;
+    float multiplyResult = subtractResult * gradient;
+
+    float opacityShape = mix(multiplyResult, addResult, gradient);
+    opacityShape = clamp(opacityShape, 0.0, 1.0);
+    opacityShape = pow(opacityShape, 2.0);
+
+    float circle = circleMask(vUv, uCircleCenter, uCircleRadius, uCircleSoftness);
+    opacityShape *= mix(1.0, circle, uCircleEnabled);
+
+    vec3 emissive = uColour * vParticleColor.rgb;
+
+    gl_FragColor = vec4(emissive, opacityShape);
+}
+`
+
+interface WaterFoamMaterialOptions {
+  colour?: THREE.Color
+  erosion?: number
+  flowSpeed?: number
+  foamTexture?: THREE.Texture
+  circleCenter?: THREE.Vector2
+  circleRadius?: number
+  circleSoftness?: number
+  circleEnabled?: boolean
+}
+
+function createWaterFoamMaterial(options: WaterFoamMaterialOptions = {}) {
+  const {
+    colour = new THREE.Color(0xffffff),
+    erosion = 1.0,
+    flowSpeed = 0.2,
+    foamTexture,
+    circleCenter = new THREE.Vector2(0.5, 0.5),
+    circleRadius = 0.34,
+    circleSoftness = 0.195,
+    circleEnabled = true,
+  } = options
+
+  return new THREE.ShaderMaterial({
+    vertexShader,
+    fragmentShader,
+    transparent: true,
+    depthWrite: false,
+    uniforms: {
+      uTime: { value: 0 },
+      uColour: { value: colour },
+      uErosion: { value: erosion },
+      uFlowSpeed: { value: flowSpeed },
+      uFoamTex: { value: foamTexture || new THREE.Texture() },
+      uCircleCenter: { value: circleCenter },
+      uCircleRadius: { value: circleRadius },
+      uCircleSoftness: { value: circleSoftness },
+      uCircleEnabled: { value: circleEnabled ? 1.0 : 0.0 },
+    },
+  })
+}
+
+export { createWaterFoamMaterial }
+export type { WaterFoamMaterialOptions }

+ 198 - 0
RuoYi-Vue3/src/materials/waterFoamUE.ts

@@ -0,0 +1,198 @@
+import * as THREE from 'three'
+
+const vertexShader = `
+varying vec2 vUv;
+varying vec3 vWorldPosition;
+
+void main() {
+    vUv = uv;
+    vec4 worldPos = modelMatrix * vec4(position, 1.0);
+    vWorldPosition = worldPos.xyz;
+    gl_Position = projectionMatrix * viewMatrix * worldPos;
+}
+`
+
+const fragmentShader = `
+uniform float uTime;
+uniform vec3 uColour;
+uniform float uWaterfallSpeed;
+uniform float uEdgeMaskTiling;
+uniform float uEdgeMaskSpeed;
+uniform float uOpacity;
+uniform float uFresnelExponent;
+
+uniform float uDirectionalFoamIntensity;
+uniform float uDirectionalFoamContrast;
+uniform float uDirectionalFoam1Intensity;
+uniform float uDirectionalFoam2Intensity;
+uniform float uDirectionalFoam2Tiling;
+uniform float uDirectionalFoam2Speed;
+uniform float uDirectionalFoam3Intensity;
+uniform float uFoamFalloff;
+
+uniform float uGradientTop;
+uniform float uGradientBottom;
+uniform float uGradientPower;
+
+uniform vec3 uCameraPos;
+
+uniform sampler2D uFoamTex;
+uniform sampler2D uDirectionalFoamTex;
+
+varying vec2 vUv;
+varying vec3 vWorldPosition;
+
+vec2 panner(vec2 uv, float speedX, float speedY, float time) {
+    return uv + vec2(speedX, speedY) * time;
+}
+
+void main() {
+    vec2 uv = vUv;
+
+    float foamTime = uTime * uEdgeMaskSpeed * uWaterfallSpeed;
+    float dirFoamTime = (uTime * uDirectionalFoam2Speed * uWaterfallSpeed) + 0.75;
+
+    vec2 foamUV = uv * vec2(1.0, uEdgeMaskTiling);
+
+    vec2 uv1 = panner(foamUV, 0.0, -0.1, foamTime);
+    vec2 uv2 = panner(foamUV, 0.0, -0.15, foamTime);
+    vec2 uv3 = panner(foamUV, 0.0, -0.25, foamTime);
+    vec2 uv4 = panner(foamUV, 0.0, -0.15, foamTime);
+
+    vec4 foam1 = texture2D(uFoamTex, uv1);
+    vec4 foam2 = texture2D(uFoamTex, uv2);
+    vec4 foam3 = texture2D(uFoamTex, uv3);
+    vec4 foam4 = texture2D(uFoamTex, uv4);
+
+    float foamVal1 = min(foam1.b, foam2.b);
+    float foamVal2 = min(foam3.a, foam4.a);
+    float foamOpacity = foamVal1 * foamVal2;
+
+    vec2 dirFoamUV_Base = uv * vec2(1.0, uDirectionalFoam2Tiling);
+    vec2 dirFoamUV_Scaled = dirFoamUV_Base * vec2(1.0, 1.25);
+
+    vec2 dirUv1 = panner(dirFoamUV_Scaled, 0.0, -0.175, dirFoamTime);
+    vec2 dirUv2 = panner(dirFoamUV_Base, 0.0, -0.09, dirFoamTime);
+
+    vec2 dirUv3 = panner(foamUV, 0.0, -0.175, foamTime);
+
+    vec4 dirFoam1 = texture2D(uDirectionalFoamTex, dirUv1);
+    vec4 dirFoam2 = texture2D(uDirectionalFoamTex, dirUv2);
+    vec4 dirFoam3 = texture2D(uDirectionalFoamTex, dirUv3);
+
+    float dirFoamLayer_G = dirFoam1.g + dirFoam2.g;
+
+    float dirFoamCombined = dirFoamLayer_G * uDirectionalFoam1Intensity + dirFoam3.g * uDirectionalFoam2Intensity;
+
+    float dirFoamAlpha3 = dirFoamLayer_G * uDirectionalFoam3Intensity;
+
+    float falloff = pow(uv.g, uFoamFalloff);
+
+    float opacityBlend = mix(foamOpacity, dirFoamAlpha3, falloff);
+
+    vec3 viewDir = normalize(uCameraPos - vWorldPosition);
+    float NdV = max(dot(viewDir, vec3(0.0, 0.707, 0.707)), 0.0);
+    float fresnelVal = pow(1.0 - NdV, uFresnelExponent);
+    float fresnelTerm = 1.0 - fresnelVal;
+
+    float finalOpacity = opacityBlend * uOpacity;
+
+    float emissiveBase = mix(dirFoamLayer_G, dirFoamCombined, falloff);
+    float emissivePowered = pow(max(emissiveBase, 0.0), uDirectionalFoamContrast);
+    float emissiveIntensity = emissivePowered * uDirectionalFoamIntensity;
+
+    float foamTexIntensity = max(foam1.r * 0.5 + foam2.r * 0.5, 0.0);
+    float totalIntensity = max(emissiveIntensity, foamTexIntensity * 0.3);
+
+    vec3 finalEmissive = vec3(1.0);
+
+    float gradientMask = mix(uGradientBottom, uGradientTop, pow(1.0 - uv.g, uGradientPower));
+    gradientMask = clamp(gradientMask, 0.0, 1.0);
+
+    float visibleAlpha = max(finalOpacity, totalIntensity * 0.5);
+    visibleAlpha *= gradientMask;
+    visibleAlpha = clamp(visibleAlpha, 0.0, 1.0);
+
+    gl_FragColor = vec4(finalEmissive, visibleAlpha);
+}
+`
+
+interface WaterFoamUEOptions {
+  colour?: THREE.Color
+  waterfallSpeed?: number
+  edgeMaskTiling?: number
+  edgeMaskSpeed?: number
+  opacity?: number
+  fresnelExponent?: number
+  directionalFoamIntensity?: number
+  directionalFoamContrast?: number
+  directionalFoam1Intensity?: number
+  directionalFoam2Intensity?: number
+  directionalFoam2Tiling?: number
+  directionalFoam2Speed?: number
+  directionalFoam3Intensity?: number
+  foamFalloff?: number
+  gradientTop?: number
+  gradientBottom?: number
+  gradientPower?: number
+  foamTexture?: THREE.Texture
+  directionalFoamTexture?: THREE.Texture
+}
+
+function createWaterFoamUEMaterial(options: WaterFoamUEOptions = {}) {
+  const {
+    colour = new THREE.Color(0xffffff),
+    waterfallSpeed = 0.32,
+    edgeMaskTiling = 0.52,
+    edgeMaskSpeed = 1.7,
+    opacity = 1.0,
+    fresnelExponent = 9.9,
+    directionalFoamIntensity = 3.48,
+    directionalFoamContrast = 0.3,
+    directionalFoam1Intensity = 1.10,
+    directionalFoam2Intensity = 2.03,
+    directionalFoam2Tiling = 1.0,
+    directionalFoam2Speed = 8.5,
+    directionalFoam3Intensity = 1.76,
+    foamFalloff = 0.8,
+    gradientTop = 1.0,
+    gradientBottom = 0.0,
+    gradientPower = 1.0,
+    foamTexture,
+    directionalFoamTexture,
+  } = options
+
+  return new THREE.ShaderMaterial({
+    vertexShader,
+    fragmentShader,
+    transparent: true,
+    depthWrite: false,
+    side: THREE.DoubleSide,
+    uniforms: {
+      uTime: { value: 0 },
+      uColour: { value: colour },
+      uWaterfallSpeed: { value: waterfallSpeed },
+      uEdgeMaskTiling: { value: edgeMaskTiling },
+      uEdgeMaskSpeed: { value: edgeMaskSpeed },
+      uOpacity: { value: opacity },
+      uFresnelExponent: { value: fresnelExponent },
+      uDirectionalFoamIntensity: { value: directionalFoamIntensity },
+      uDirectionalFoamContrast: { value: directionalFoamContrast },
+      uDirectionalFoam1Intensity: { value: directionalFoam1Intensity },
+      uDirectionalFoam2Intensity: { value: directionalFoam2Intensity },
+      uDirectionalFoam2Tiling: { value: directionalFoam2Tiling },
+      uDirectionalFoam2Speed: { value: directionalFoam2Speed },
+      uDirectionalFoam3Intensity: { value: directionalFoam3Intensity },
+      uFoamFalloff: { value: foamFalloff },
+      uGradientTop: { value: gradientTop },
+      uGradientBottom: { value: gradientBottom },
+      uGradientPower: { value: gradientPower },
+      uCameraPos: { value: new THREE.Vector3() },
+      uFoamTex: { value: foamTexture || new THREE.Texture() },
+      uDirectionalFoamTex: { value: directionalFoamTexture || new THREE.Texture() },
+    },
+  })
+}
+
+export { createWaterFoamUEMaterial }
+export type { WaterFoamUEOptions }

+ 354 - 0
RuoYi-Vue3/src/materials/waterNew.ts

@@ -0,0 +1,354 @@
+import * as THREE from 'three'
+
+const lakeVertexShader = `
+uniform float iTime;
+uniform float waveHeight;
+uniform float flowSpeed;
+uniform vec2 flowDirection;
+
+varying vec2 vUv;
+varying vec3 vWorldPosition;
+varying vec4 vClipSpace;
+varying vec2 vWorldUV;
+varying float vWaveHeight;
+
+float calcVertexWave(vec2 pos, float time, vec2 dir, float amp, float freq, float speed) {
+    return amp * sin(dot(pos, dir) * freq + time * speed);
+}
+
+void main() {
+    vUv = uv;
+    vec2 flowDir = normalize(flowDirection);
+    float time = iTime * 0.5;
+
+    vec4 worldPos = modelMatrix * vec4(position, 1.0);
+    vec2 wavePos = worldPos.xz;
+
+    float h = 0.0;
+    h += calcVertexWave(wavePos, time, flowDir, 1.00, 1.0, 1.20 * flowSpeed);
+    h += calcVertexWave(wavePos, time, vec2(-flowDir.y, flowDir.x), 0.60, 1.8, 1.50 * flowSpeed);
+    h += calcVertexWave(wavePos, time, flowDir * 0.7, 0.40, 2.7, 1.90 * flowSpeed);
+    h += calcVertexWave(wavePos, time, vec2(flowDir.y, -flowDir.x), 0.25, 3.9, 2.40 * flowSpeed);
+
+    h *= waveHeight * 0.2;
+
+    vec3 displacedPos = position;
+    displacedPos.z += h;
+
+    vec4 newWorldPos = modelMatrix * vec4(displacedPos, 1.0);
+    vWorldPosition = newWorldPos.xyz;
+    vWorldUV = newWorldPos.xz;
+    vWaveHeight = h;
+
+    gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPos, 1.0);
+    vClipSpace = gl_Position;
+}
+`;
+
+const lakeFragmentShader = `
+uniform float iTime;
+uniform vec2 iResolution;
+uniform sampler2D depthSampler;
+uniform float cameraNear;
+uniform float cameraFar;
+uniform float waterLevel;
+uniform float waveHeight;
+uniform float flowSpeed;
+uniform vec3 shallowColor;
+uniform vec3 deepColor;
+uniform float foamIntensity;
+uniform float alpha;
+uniform float specIntensity;
+uniform float specPower;
+uniform float fresnelPower;
+uniform float fresnelIntensity;
+uniform float depthRange;
+uniform vec3 cameraPos;
+uniform vec3 sunDirection;
+uniform vec2 flowDirection;
+uniform float normalRotation;
+uniform sampler2D waterNormalMap1;
+uniform sampler2D waterNormalMap2;
+uniform sampler2D waterNormalMap3;
+uniform float waterNormalStrength;
+uniform float waterNormalTiling;
+uniform float collisionFoamThreshold;
+uniform float collisionFoamStrength;
+uniform vec3 collisionFoamColor;
+uniform float collisionFoamTime;
+uniform sampler2D skyTexture;
+uniform float fresnelDistanceNear;
+uniform float fresnelDistanceFar;
+
+varying vec2 vUv;
+varying vec3 vWorldPosition;
+varying vec4 vClipSpace;
+varying vec2 vWorldUV;
+varying float vWaveHeight;
+
+float hash(vec2 p) {
+    vec3 p3 = fract(vec3(p.xyx) * 0.1031);
+    p3 += dot(p3, p3.yzx + 33.33);
+    return fract((p3.x + p3.y) * p3.z);
+}
+
+float noise(vec2 p) {
+    vec2 i = floor(p);
+    vec2 f = fract(p);
+    f = f * f * (3.0 - 2.0 * f);
+    float a = hash(i);
+    float b = hash(i + vec2(1.0, 0.0));
+    float c = hash(i + vec2(0.0, 1.0));
+    float d = hash(i + vec2(1.0, 1.0));
+    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+
+float fbm(vec2 p) {
+    float value = 0.0;
+    float amplitude = 0.5;
+    float frequency = 1.0;
+    for (int i = 0; i < 5; i++) {
+        value += amplitude * noise(p * frequency);
+        frequency *= 2.0;
+        amplitude *= 0.5;
+    }
+    return value;
+}
+
+float linearizeDepth(float depth) {
+    float z = depth * 2.0 - 1.0;
+    return (2.0 * cameraNear * cameraFar) / (cameraFar + cameraNear - z * (cameraFar - cameraNear));
+}
+
+struct Wave {
+    vec2 dir;
+    float amp;
+    float freq;
+    float speed;
+    float steepness;
+};
+
+#define NUM_WAVES 8
+
+vec2 getFlowDir(float angleOffset) {
+    float angle = atan(flowDirection.y, flowDirection.x);
+    return vec2(cos(angle + angleOffset), sin(angle + angleOffset));
+}
+
+void computeWaves(out Wave waves[NUM_WAVES]) {
+    vec2 mainDir = normalize(flowDirection);
+
+    waves[0] = Wave(mainDir,                            1.00, 1.0, 1.20, 0.30);
+    waves[1] = Wave(getFlowDir(0.6),                    0.60, 1.8, 1.50, 0.25);
+    waves[2] = Wave(getFlowDir(-0.5),                   0.40, 2.7, 1.90, 0.20);
+    waves[3] = Wave(getFlowDir(1.2),                    0.25, 3.9, 2.40, 0.15);
+    waves[4] = Wave(getFlowDir(-0.9),                   0.18, 5.2, 2.80, 0.12);
+    waves[5] = Wave(getFlowDir(0.3),                    0.12, 6.8, 3.30, 0.10);
+    waves[6] = Wave(getFlowDir(-0.7),                   0.08, 8.5, 3.90, 0.08);
+    waves[7] = Wave(normalize(mainDir + vec2(0.3, 0.5)),0.05, 10.5, 4.50, 0.05);
+}
+
+void computeWaveData(vec2 pos, float time, float wh, float wf, out float height, out vec2 displacement) {
+    Wave waves[NUM_WAVES];
+    computeWaves(waves);
+
+    height = 0.0;
+    vec2 deriv = vec2(0.0);
+    vec2 disp = vec2(0.0);
+
+    for (int i = 0; i < NUM_WAVES; i++) {
+        Wave w = waves[i];
+        float h = w.amp * sin(dot(pos * wf, w.dir) * w.freq + time * w.speed);
+        float cosVal = cos(dot(pos * wf, w.dir) * w.freq + time * w.speed);
+        height += h;
+        deriv += w.amp * w.freq * w.dir * wf * cosVal;
+        disp += w.amp * w.dir * cosVal * w.steepness;
+    }
+
+    height *= wh;
+    deriv *= wh;
+    displacement = disp * wh * 0.3;
+}
+
+float getCollisionFoam(vec2 pos, float depthDiff) {
+    float foam = smoothstep(collisionFoamThreshold, 0.0, depthDiff);
+    float collisionNoise = fbm(pos * 4.0 + collisionFoamTime * 0.3);
+    foam *= collisionNoise * collisionFoamStrength;
+    return clamp(foam, 0.0, 1.0);
+}
+
+vec2 equirectangularUV(vec3 dir) {
+    float u = atan(dir.z, dir.x) * 0.1591549 + 0.5;
+    float v = acos(clamp(dir.y, -1.0, 1.0)) * 0.3183099;
+    return vec2(u, v);
+}
+
+void main() {
+    vec2 screenUV = vClipSpace.xy / vClipSpace.w * 0.5 + 0.5;
+
+    float sceneDepth = texture(depthSampler, screenUV).r;
+
+    float linearSceneDepth = linearizeDepth(sceneDepth);
+    float linearWaterDepth = linearizeDepth(gl_FragCoord.z);
+    float bufferDepth = max(abs(linearSceneDepth - linearWaterDepth), 0.01);
+    if (sceneDepth >= 1.0) {
+        bufferDepth = depthRange * 0.5;
+    }
+
+    float worldHeightDepth = clamp((waterLevel - vWorldPosition.y) / depthRange, 0.0, 1.0);
+
+    float waterDepth = max(bufferDepth, worldHeightDepth * depthRange);
+
+    float depthFactor = clamp(waterDepth / depthRange, 0.0, 1.0);
+    depthFactor = max(depthFactor, worldHeightDepth);
+
+    vec2 flowDir = normalize(flowDirection);
+    float time = iTime * 0.5;
+
+    vec2 wavePos = vWorldPosition.xz;
+
+    float waveH;
+    vec2 waveDisp;
+    computeWaveData(wavePos, time, waveHeight * 0.2, 1.0, waveH, waveDisp);
+
+    float rotRad = normalRotation * 3.14159265 / 180.0;
+    float cosA = cos(rotRad);
+    float sinA = sin(rotRad);
+    vec2 waterUV = vWorldUV * waterNormalTiling + iTime * flowDir * flowSpeed * 0.10;
+    vec3 n1 = texture2D(waterNormalMap1, waterUV).rgb * 2.0 - 1.0;
+    vec3 n2 = texture2D(waterNormalMap2, waterUV * 1.3 + 0.5).rgb * 2.0 - 1.0;
+    vec3 n3 = texture2D(waterNormalMap3, waterUV * 0.7 + 1.0).rgb * 2.0 - 1.0;
+    vec3 waterMap = normalize(n1 + n2 + n3);
+    vec3 finalNormal = normalize(vec3(
+        (waterMap.x * cosA - waterMap.y * sinA) * waterNormalStrength,
+        1.0,
+        (waterMap.x * sinA + waterMap.y * cosA) * waterNormalStrength
+    ));
+
+    vec3 viewDir = normalize(cameraPos - vWorldPosition);
+    vec3 halfVec = normalize(viewDir + sunDirection);
+
+    float NdotL = max(dot(finalNormal, sunDirection), 0.0);
+    float NdotH = max(dot(finalNormal, halfVec), 0.0);
+    float NdotV = max(dot(finalNormal, viewDir), 0.0);
+
+    float specular = pow(NdotH, specPower) * specIntensity;
+
+    float fresnel = fresnelIntensity * pow(1.0 - NdotV, fresnelPower);
+
+    vec3 waterCol = mix(shallowColor, deepColor, depthFactor);
+
+    float shallowGlow = exp(-waterDepth * 1.5) * 0.3;
+    waterCol += shallowColor * shallowGlow;
+
+    float foamNoise = fbm(vWorldPosition.xz * 3.0 + iTime * 0.2 + waveDisp * 2.0);
+    float foamShore = smoothstep(0.3, 1.5, waterDepth);
+    float foam = (1.0 - foamShore) * foamNoise * foamIntensity * 1.5;
+
+    float wavePeakFoam = 0.0;
+
+    foam = clamp(foam, 0.0, 1.0);
+
+    float collisionFoam = getCollisionFoam(vWorldPosition.xz, waterDepth);
+    foam = clamp(foam + collisionFoam, 0.0, 1.0);
+
+    vec3 ambientLight = vec3(0.45, 0.50, 0.55);
+    vec3 sunLight = vec3(1.8, 1.7, 1.5);
+
+    vec3 diffuse = NdotL * sunLight;
+    vec3 lighting = diffuse + ambientLight;
+
+    vec3 finalColor = waterCol * lighting;
+
+    finalColor += specular * sunLight * 0.4;
+
+    vec3 foamColor = vec3(0.95, 0.97, 1.0);
+    finalColor = mix(finalColor, foamColor, foam);
+
+    finalColor = mix(finalColor, collisionFoamColor, collisionFoam);
+
+    float viewDist = length(cameraPos - vWorldPosition);
+    float distanceFactor = clamp((viewDist - fresnelDistanceNear) / (fresnelDistanceFar - fresnelDistanceNear), 0.0, 1.0);
+
+    float finalFresnel = fresnel * distanceFactor;
+    finalFresnel = clamp(finalFresnel, 0.0, 1.0);
+
+    vec3 reflectDir = reflect(-viewDir, finalNormal);
+    vec3 skyReflection = texture2D(skyTexture, equirectangularUV(reflectDir)).rgb;
+
+    vec3 reflectionColor = skyReflection;
+    reflectionColor += sunLight * 0.1;
+    float reflectionStrength = finalFresnel * 0.75;
+    finalColor = mix(finalColor, reflectionColor, reflectionStrength);
+
+    float edgeFade = smoothstep(0.0, 0.02, screenUV.x) * smoothstep(0.0, 0.02, screenUV.y) *
+                     smoothstep(0.0, 0.02, 1.0 - screenUV.x) * smoothstep(0.0, 0.02, 1.0 - screenUV.y);
+
+    finalColor *= 1.0 + foam * 0.2;
+
+    float fresnelAlpha = clamp((viewDist - fresnelDistanceNear) / (fresnelDistanceFar * 0.6 - fresnelDistanceNear), 0.0, 1.0);
+    float finalAlpha = mix(alpha * 0.05, 0.98, fresnelAlpha);
+
+    gl_FragColor = vec4(finalColor, finalAlpha * edgeFade);
+}
+`;
+
+const textureLoader = new THREE.TextureLoader();
+
+function loadRepeatTexture(url: string): THREE.Texture {
+  const tex = textureLoader.load(new URL(url, import.meta.url).href)
+  tex.wrapS = THREE.RepeatWrapping
+  tex.wrapT = THREE.RepeatWrapping
+  return tex
+}
+
+const waterNormalTex1 = loadRepeatTexture('../assets/texture/Water_1_Normal.PNG')
+const waterNormalTex2 = loadRepeatTexture('../assets/texture/Water_2_Normal.PNG')
+const waterNormalTex3 = loadRepeatTexture('../assets/texture/Water_3_Normal.PNG')
+
+const skyTex = textureLoader.load(new URL('../assets/texture/sky.jpg', import.meta.url).href)
+skyTex.wrapS = THREE.RepeatWrapping
+skyTex.wrapT = THREE.ClampToEdgeWrapping
+
+export const StylizedWaterMaterial = new THREE.ShaderMaterial({
+    transparent: true,
+    side: THREE.DoubleSide,
+    depthWrite: false,
+    uniforms: {
+        iTime: { value: 0 },
+        iResolution: { value: new THREE.Vector2(1, 1) },
+        depthSampler: { value: null },
+        cameraNear: { value: 0.1 },
+        cameraFar: { value: 1000.0 },
+        waterLevel: { value: 0.0 },
+        waveHeight: { value: 0.6 },
+        flowSpeed: { value: 0.5 },
+        shallowColor: { value: new THREE.Color('#6a9b8a') },
+        deepColor: { value: new THREE.Color('#0a2a4a') },
+        foamIntensity: { value: 0.25 },
+        alpha: { value: 0.85 },
+        specIntensity: { value: 2.0 },
+        specPower: { value: 64.0 },
+        fresnelPower: { value: 1.8 },
+        fresnelIntensity: { value: 2.5 },
+        depthRange: { value: 44.0 },
+        cameraPos: { value: new THREE.Vector3(0, 0, 0) },
+        sunDirection: { value: new THREE.Vector3(0.5, 0.8, 0.3).normalize() },
+        flowDirection: { value: new THREE.Vector2(1.0, 0.0) },
+        normalRotation: { value: 0 },
+        waterNormalMap1: { value: waterNormalTex1 },
+        waterNormalMap2: { value: waterNormalTex2 },
+        waterNormalMap3: { value: waterNormalTex3 },
+        waterNormalStrength: { value: 2.0 },
+        waterNormalTiling: { value: 0.26 },
+        collisionFoamThreshold: { value: 0.10 },
+        collisionFoamStrength: { value: 0.5 },
+        collisionFoamColor: { value: new THREE.Color(1, 1, 1) },
+        collisionFoamTime: { value: 0 },
+        skyTexture: { value: skyTex },
+        fresnelDistanceNear: { value: 2.0 },
+        fresnelDistanceFar: { value: 15.0 },
+    },
+    vertexShader: lakeVertexShader,
+    fragmentShader: lakeFragmentShader
+});

+ 514 - 563
RuoYi-Vue3/src/supermap-cesium-module/business-scenes/JuKouShuiZhaScene.js

@@ -1,633 +1,584 @@
+// 导入纹理和材质模块
+import foamTextureUrl from '../../texture/T_Waterfall_Foam.PNG';
+import waterNormal1Url from '../../texture/Water_1_Normal.PNG';
+import waterNormal2Url from '../../texture/Water_2_Normal.PNG';
+import waterNormal3Url from '../../texture/Water_3_Normal.PNG';
+import skyTextureUrl from '../../texture/sky.jpg';
+import { createWaterFoamPrimitive } from '../../materials/WaterFoamMaterial.js';
+import { createStylizedWaterMaterial, destroyStylizedWaterAnimation } from '../../materials/StylizedWaterMaterial.js';
+
 /**
- * 莒口水闸业务场景模块
- * 包含闸门子节点提取、闸门动画控制等核心功能
+ * 超图iClient3D 100%可用 水闸闸门控制
+ * 原生 nodeTransformations 适配,无私有API、无报错、真实动模型
  */
 
-// 闸门子节点名称列表
 export const GATE_NODE_NAMES = [
   'gate_01', 'gate_02', 'gate_03', 'gate_04', 'gate_05',
   'gate_06', 'gate_07', 'gate_08', 'gate_09', 'gate_10',
-  'gate_11', 'gate_12', 'gate_13', 'gate_14', 'gate_15'
+  'gate_11', 'gate_12', 'gate_13', 'gate_14'
 ];
 
-// 最大闸门高度(米)
-const MAX_GATE_HEIGHT = 5;
+// 闸门最大移动高度(米),全关时向下移动8米
+const MAX_GATE_HEIGHT = 9;
 
-/**
- * 提取闸门子节点
- * @param {Object} modelPrimitive - Cesium模型图元
- * @param {Array} gateNames - 闸门名称列表
- * @returns {Object} - 闸门节点映射 { gateName: node }
- */
-export function extractGateNodes(modelPrimitive, gateNames = GATE_NODE_NAMES) {
-  const gateNodes = {};
-  const allNodes = {};
-  
-  console.log('========== 开始解析莒口水闸模型组成部分 ==========');
-  console.log('传入的 modelPrimitive:', modelPrimitive);
-  console.log('modelPrimitive 类型:', modelPrimitive?.constructor?.name);
-  
-  if (!modelPrimitive) {
-    console.error('❌ 模型图元未定义');
-    console.log('====================================================');
-    return gateNodes;
+export const JuKouSceneConfig = {
+  sceneId: 'fixed_jushui_gate',
+  sceneName: '莒口水闸',
+  model: {
+    name: '莒口水闸3',
+    filePath: '/models/莒口水闸3.glb',
+    coordinates: '119.144008,25.867707,25',
+    rotation: '0,0,80'
+  },
+  // 面片模型配置(20米x20米)
+  plane: {
+    name: '闸门面片',
+    coordinates: '119.144345, 25.866551,24.5',
+    size: 20  // 20米x20米
+  },
+  // 简单面片(无材质,仅用于标记位置)
+  simplePlane: {
+    name: '标记面片',
+    coordinates: '119.148567, 25.871270,40.95',
+    size: 100  // 20米x20米
+  },
+  camera: {
+    position: [-2795169.7727823164,5012356.518227473,2783038.1732437233],
+    direction: [0.5111708396854006, 0.3975398306001059, -0.7620147345962293],
+    up: [-0.32323360594555933, 0.910431086773702, 0.25813808751032213]
+  },
+  // 模型拆解图标配置(出现在场景中,点击打开启闭机模型弹框)
+  modelDisassemblyIcon: {
+    longitude: 119.144081,
+    latitude: 25.867756,
+    height: 42,
+    image: '/img/svg/logo.svg',
+    width: 40,
+    height: 40
   }
+};
+
+export class JuKouShuiZhaController {
+  constructor() {
+    this.viewer = null;
+    this.entity = null;
+    this.planeEntities = [];        // 所有闸门面片实体数组
+    this._planeAnimationHandlers = [];  // 所有面片动画处理器
+    this._gatePlaneMap = new Map();     // gateName → { primitive, timeoutId }
+    this._simplePlanePrimitive = null;  // 简单标记面片
+    this._simplePlaneAnimHandler = null;  // 简单标记面片动画处理器
+    this.gateStates = {};
+    this.isInitialized = false;
+    this.isModelLoaded = false;
 
-  console.log('模型对象结构检查:');
-  console.log('  _gltf:', modelPrimitive._gltf !== undefined);
-  if (modelPrimitive._gltf) {
-    console.log('  _gltf.nodes:', modelPrimitive._gltf.nodes !== undefined);
-    console.log('  _gltf.nodes 数量:', modelPrimitive._gltf.nodes ? Object.keys(modelPrimitive._gltf.nodes).length : 0);
+    this.onSceneReady = null;
+    this.onGateNodesReady = null;
+    this.onGatePositionChange = null;
+    this.onError = null;
+
+    GATE_NODE_NAMES.forEach(name => {
+      this.gateStates[name] = 0;
+    });
   }
-  console.log('  _model:', modelPrimitive._model !== undefined);
-  if (modelPrimitive._model) {
-    console.log('  _model._gltf:', modelPrimitive._model._gltf !== undefined);
-    if (modelPrimitive._model._gltf) {
-      console.log('  _model._gltf.nodes:', modelPrimitive._model._gltf.nodes !== undefined);
-      console.log('  _model._gltf.nodes 数量:', modelPrimitive._model._gltf.nodes ? Object.keys(modelPrimitive._model._gltf.nodes).length : 0);
+
+  async loadScene(viewer, options = {}) {
+    console.log('✅ 超图iClient3D 加载莒口水闸');
+    this.viewer = viewer;
+    Object.assign(this, options);
+
+    if (!viewer) {
+      this.onError && this.onError('viewer实例为空');
+      return;
     }
-  }
-  console.log('  _runtimeNodes:', modelPrimitive._runtimeNodes !== undefined);
-  console.log('  _nodeRuntimeReferences:', modelPrimitive._nodeRuntimeReferences !== undefined);
-  
-  // 获取 GLTF 数据(尝试多个位置)
-  let gltf = modelPrimitive._gltf;
-  
-  // 如果当前对象没有,尝试从 _model 获取
-  if (!gltf && modelPrimitive._model) {
-    console.log('🔍 尝试从 modelPrimitive._model 获取 GLTF');
-    gltf = modelPrimitive._model._gltf;
-  }
-  
-  // 如果还是没有,尝试从内部结构获取
-  if (!gltf && modelPrimitive._sourceChapters) {
-    console.log('🔍 尝试从 modelPrimitive._sourceChapters 获取');
-    for (const chapter of modelPrimitive._sourceChapters) {
-      if (chapter._gltf) {
-        gltf = chapter._gltf;
-        break;
+
+    try {
+      await this.loadModel();
+      this.loadPlane();       // 加载面片模型(闸门水泡沫面片)
+      try {
+        this.loadSimplePlane(); // 加载简单标记面片
+      } catch (err2) {
+        console.error('❌ loadSimplePlane 失败', err2);
+        this.onError && this.onError('loadSimplePlane: ' + err2.message);
       }
+      this.isInitialized = true;
+      this.onSceneReady && this.onSceneReady(this.entity);
+    } catch (err) {
+      console.error('❌ 加载失败', err);
+      this.onError && this.onError(err.message);
     }
   }
-  
-  console.log('最终 gltf:', gltf !== undefined);
-  if (gltf) {
-    console.log('最终 gltf.nodes:', gltf.nodes !== undefined);
-    console.log('最终 gltf.nodes 数量:', gltf.nodes ? Object.keys(gltf.nodes).length : 0);
-  }
-  
-  // 方法1: 从运行时节点获取(优先使用,因为包含 nodeIndex,用于 setNodeMatrix)
-  const runtimeNodes = modelPrimitive._runtimeNodes || (modelPrimitive._model && modelPrimitive._model._runtimeNodes);
-  if (runtimeNodes && Array.isArray(runtimeNodes)) {
-    console.log('✅ 优先从运行时节点获取信息...');
-    for (let i = 0; i < runtimeNodes.length; i++) {
-      const node = runtimeNodes[i];
-      if (node && node.name) {
-        allNodes[node.name] = { node, path: node.name, modelPrimitive, nodeIndex: i };
-        console.log(`  ├─ 运行时节点: ${node.name} (索引: ${i})`);
-        if (node.name.startsWith('gate_')) {
-          gateNodes[node.name] = { node, path: node.name, modelPrimitive, nodeIndex: i };
-          console.log(`     └─ ✅ 识别为闸门节点`);
-        }
+
+  async loadModel() {
+    const Cesium = window.Cesium;
+    const cfg = JuKouSceneConfig.model;
+
+    const coords = cfg.coordinates.split(',').map(Number);
+    const position = Cesium.Cartesian3.fromDegrees(coords[0], coords[1], coords[2]);
+    const rot = cfg.rotation.split(',').map(Number);
+    const hpr = new Cesium.HeadingPitchRoll(
+      Cesium.Math.toRadians(rot[2]),
+      Cesium.Math.toRadians(rot[0]),
+      Cesium.Math.toRadians(rot[1])
+    );
+
+    // 初始化所有闸口子节点变换(超图官方标准)
+    const nodeTransformations = {};
+    GATE_NODE_NAMES.forEach(name => {
+      // 显式初始化 translation,rotation 和 scale
+      nodeTransformations[name] = new Cesium.TranslationRotationScale(
+        new Cesium.Cartesian3(0, 0, 0),  // translation
+        Cesium.Quaternion.IDENTITY,       // rotation
+        new Cesium.Cartesian3(1, 1, 1)    // scale
+      );
+    });
+
+    this.entity = this.viewer.entities.add({
+      name: cfg.name,
+      position,
+      orientation: Cesium.Transforms.headingPitchRollQuaternion(position, hpr),
+      disableTransform: true,  // 禁用模型变换弹框
+      model: {
+        uri: cfg.filePath,
+        maximumScale: 20000,
+        clampAnimations: true,
+        nodeTransformations
       }
-    }
-  }
+    });
+
+    await this.entity.model.readyPromise;
 
-  // 方法2: 从 gltf.nodes 遍历(补充获取路径信息)
-  if (gltf && gltf.nodes) {
-    console.log('✅ 从 GLTF 数据补充节点路径信息...');
-    console.log('GLTF 节点总数:', Object.keys(gltf.nodes).length);
-    
-    const nodeMap = new Map();
-    for (const key in gltf.nodes) {
-      const node = gltf.nodes[key];
-      nodeMap.set(key, node);
-      if (node.name) {
-        nodeMap.set(node.name, node);
+    // 打印模型中的所有节点名称,用于调试
+    console.log('📋 模型已加载,检查节点...');
+    const model = this.entity.model;
+    if (model && model._content && model._content._model) {
+      const nodes = model._content._model._sourceNodes || model._content._model.nodes;
+      if (nodes) {
+        console.log('📋 模型中的节点列表:', Object.keys(nodes));
       }
     }
-    
-    const traverseGltfNode = (node, parentPath = '') => {
-      const nodePath = parentPath ? `${parentPath}/${node.name || 'unnamed'}` : (node.name || 'unnamed');
-      
-      if (node.name) {
-        // 如果已经有运行时节点数据,补充路径信息
-        if (allNodes[node.name]) {
-          allNodes[node.name].path = nodePath;
-          if (gateNodes[node.name]) {
-            gateNodes[node.name].path = nodePath;
-          }
-        } else {
-          allNodes[node.name] = { node, path: nodePath };
-        }
-        console.log(`  ├─ 节点: ${nodePath}`);
-        
-        // 如果运行时节点中没有找到闸门,使用 GLTF 节点作为后备
-        if (node.name.startsWith('gate_') && !gateNodes[node.name]) {
-          gateNodes[node.name] = { node, path: nodePath, modelPrimitive };
-          if (node.translation) {
-            node.originOffset = [...node.translation];
-          }
-        }
-      }
-      
-      if (node.children && node.children.length > 0) {
-        node.children.forEach(childKey => {
-          const child = typeof childKey === 'number' ? gltf.nodes[childKey] : nodeMap.get(childKey);
-          if (child) {
-            traverseGltfNode(child, nodePath);
-          }
-        });
+
+    // 初始化所有闸门为全关状态(0%,位置 -8米)
+    console.log('📋 初始化闸门位置为全关状态(-8米)');
+    GATE_NODE_NAMES.forEach(name => {
+      const trans = this.entity.model.nodeTransformations[name];
+      if (trans) {
+        trans.translation = new Cesium.Cartesian3(0, -MAX_GATE_HEIGHT, 0);
+        this.gateStates[name] = 0;  // 0% 表示全关
+        console.log(`✅ 闸门节点 ${name} 初始化为全关状态`);
+      } else {
+        console.warn(`⚠️ 闸门节点 ${name} 未找到`);
       }
-    };
-    
-    if (gltf.scenes && gltf.scenes[0] && gltf.scenes[0].nodes && gltf.scenes[0].nodes.length > 0) {
-      console.log('从场景根节点开始遍历...');
-      gltf.scenes[0].nodes.forEach(rootNodeKey => {
-        const rootNode = gltf.nodes[rootNodeKey];
-        if (rootNode) {
-          traverseGltfNode(rootNode);
-        }
-      });
-    }
-  } else {
-    console.log('⚠️ 未找到 GLTF 数据');
+    });
+
+    // 已删除透明度修改代码,保留模型原始材质
+    this.isModelLoaded = true;
+    this.onGateNodesReady && this.onGateNodesReady(GATE_NODE_NAMES);
+    console.log('✅ 闸门节点就绪,所有闸门已初始化为全关状态');
   }
 
-  // 方法3: 从 _nodeRuntimeReferences 获取
-  const nodeRefs = modelPrimitive._nodeRuntimeReferences || (modelPrimitive._model && modelPrimitive._model._nodeRuntimeReferences);
-  if (nodeRefs) {
-    console.log('✅ 尝试从 _nodeRuntimeReferences 获取信息...');
-    if (nodeRefs.forEach) {
-      nodeRefs.forEach((runtimeNode, nodeName) => {
-        if (!allNodes[nodeName]) {
-          allNodes[nodeName] = { node: runtimeNode, path: nodeName };
-          console.log(`  ├─ 引用节点: ${nodeName}`);
-          if (!gateNodes[nodeName] && nodeName.startsWith('gate_')) {
-            gateNodes[nodeName] = { node: runtimeNode, path: nodeName };
-          }
-        }
+  // ============================================================
+  // 加载面片模型(使用水泡沫材质)—— 为每个闸门创建一个面片
+  // ============================================================
+  loadPlane() {
+    const Cesium = window.Cesium;
+    const cfg = JuKouSceneConfig.plane;
+    const size = cfg.size;  // 20米
+
+    // ================================================================
+    // ★★★ 各闸门对应面片的位置配置 ★★★
+    // 格式:'经度, 纬度, 高度'
+    // 请根据实际场景调整下面每个闸门面片的坐标位置
+    // ================================================================
+    const gatePlanePositions = {
+      // ═══════════════════════════════════════════════════════════════
+      // 闸门编号       位置坐标 (经度, 纬度, 高度)          说明
+      // ═══════════════════════════════════════════════════════════════
+      'gate_01': '119.144021, 25.868809, 24.5',  // ← 1号闸门面片(请修改此坐标)
+      'gate_02': '119.143981, 25.868617, 24.5',  // ← 2号闸门面片(请修改此坐标)
+      'gate_03': '119.144006, 25.868447, 24.5',  // ← 3号闸门面片(请修改此坐标)
+      'gate_04': '119.144030, 25.868276, 24.5',  // ← 4号闸门面片(请修改此坐标)
+      'gate_05': '119.144066, 25.868099, 24.5',  // ← 5号闸门面片(请修改此坐标)
+      'gate_06': '119.144095, 25.867950, 24.5',  // ← 6号闸门面片(请修改此坐标)
+      'gate_07': '119.144138, 25.867760, 24.5',  // ← 7号闸门面片(请修改此坐标)
+      'gate_08': '119.144175, 25.867593, 24.5',  // ← 8号闸门面片(请修改此坐标)
+      'gate_09': '119.144204, 25.867406, 24.5',  // ← 9号闸门面片(请修改此坐标)
+      'gate_10': '119.144260, 25.867247, 24.5',  // ← 10号闸门面片(请修改此坐标)
+      'gate_11': '119.144272, 25.867073, 24.5',  // ← 11号闸门面片(请修改此坐标)
+      'gate_12': '119.144312, 25.866905, 24.5',  // ← 12号闸门面片(请修改此坐标)
+      'gate_13': '119.144346, 25.866737, 24.5',  // ← 13号闸门面片(请修改此坐标)
+      // ───────────────────────────────────────────────────────────────
+      'gate_14': '119.144345, 25.866551, 24.5',  // 14号闸门面片(已调整好,勿动)
+      // ═══════════════════════════════════════════════════════════════
+    };
+
+    // 清除之前已创建的面片(防止重复加载)
+    this._clearPlanes();
+
+    // 遍历每个闸门,创建对应的面片
+    GATE_NODE_NAMES.forEach((gateName) => {
+      const coordsStr = gatePlanePositions[gateName];
+      if (!coordsStr) {
+        console.warn(`⚠️ 未配置闸门 ${gateName} 的面片位置,跳过`);
+        return;
+      }
+
+      const coords = coordsStr.split(',').map(Number);
+      const centerLon = coords[0];
+      const centerLat = coords[1];
+      const altitude = coords[2];
+
+      // 计算矩形边界
+      const latOffset = size / 111000;
+      const lonOffset = size / (111000 * Math.cos(centerLat * Math.PI / 180));
+      const west = centerLon - lonOffset / 2;
+      const east = centerLon + lonOffset / 2;
+      const south = centerLat - latOffset / 2;
+      const north = centerLat + latOffset / 2;
+
+      const rectangle = Cesium.Rectangle.fromDegrees(west, south, east, north);
+
+      // 创建水泡沫面片
+      const foamResult = createWaterFoamPrimitive({
+        rectangle: rectangle,
+        height: altitude,
+        color: new Cesium.Color(1.0, 1.0, 1.0, 1.0),
+        erosion: 1.0,
+        flowSpeed: 0.2,
+        foamTexture: foamTextureUrl,
+        circleEnabled: true
+      }, this.viewer);
+
+      // 将面片沿Y轴旋转180度
+      const centerCartographic = new Cesium.Cartographic(
+        Cesium.Math.toRadians(centerLon),
+        Cesium.Math.toRadians(centerLat),
+        altitude
+      );
+      const centerCartesian = Cesium.Cartographic.toCartesian(centerCartographic, Cesium.Ellipsoid.WGS84);
+
+      const enuToEcef = Cesium.Transforms.eastNorthUpToFixedFrame(centerCartesian);
+      const ecefToEnu = new Cesium.Matrix4();
+      Cesium.Matrix4.inverse(enuToEcef, ecefToEnu);
+
+      const rotY = Cesium.Matrix4.fromRotationTranslation(
+        Cesium.Matrix3.fromRotationY(Cesium.Math.toRadians(180)),
+        Cesium.Cartesian3.ZERO
+      );
+
+      const temp = new Cesium.Matrix4();
+      Cesium.Matrix4.multiply(rotY, ecefToEnu, temp);
+      const modelMatrix = new Cesium.Matrix4();
+      Cesium.Matrix4.multiply(enuToEcef, temp, modelMatrix);
+
+      foamResult.primitive.modelMatrix = modelMatrix;
+
+      // 默认隐藏,仅当闸门调整开度时才显示
+      foamResult.primitive.show = false;
+
+      // 存储到映射表:gateName → { primitive, timeoutId }
+      this._gatePlaneMap.set(gateName, {
+        primitive: foamResult.primitive,
+        timeoutId: null
       });
-    }
+
+      // 存储
+      this.planeEntities.push(foamResult.primitive);
+      this._planeAnimationHandlers.push(foamResult.animationHandler);
+
+      console.log(`✅ 面片已加载: ${gateName} → (${coordsStr})`);
+    });
+
+    console.log(`✅ 共加载 ${this.planeEntities.length} 个闸门面片, 尺寸: ${size}米×${size}米`);
   }
 
-  // 方法4: 尝试从 model 实例的其他属性获取
-  if (modelPrimitive.nodes) {
-    console.log('✅ 尝试从 model.nodes 获取信息...');
-    for (const key in modelPrimitive.nodes) {
-      const node = modelPrimitive.nodes[key];
-      if (node && node.name && !allNodes[node.name]) {
-        allNodes[node.name] = { node, path: node.name };
-        console.log(`  ├─ 模型节点: ${node.name}`);
-      }
+  // ============================================================
+  // 加载水面面片(使用风格化水面材质 + 法线纹理)
+  // ============================================================
+  loadSimplePlane() {
+    const Cesium = window.Cesium;
+    const cfg = JuKouSceneConfig.simplePlane;
+    const size = cfg.size;  // 20米
+    const coords = cfg.coordinates.split(',').map(Number);
+    const centerLon = coords[0];
+    const centerLat = coords[1];
+    const altitude = coords[2];
+
+    // 清除之前已创建的简单面片
+    if (this._simplePlanePrimitive) {
+      this.viewer.scene.primitives.remove(this._simplePlanePrimitive);
+      this._simplePlanePrimitive = null;
     }
-  }
 
-  // 输出最终统计结果
-  const allNodeNames = Object.keys(allNodes);
-  const gateNodeNames = Object.keys(gateNodes);
-  
-  console.log('');
-  console.log('========== 模型组成部分统计 ==========');
-  console.log(`总节点数: ${allNodeNames.length}`);
-  console.log(`闸门节点数: ${gateNodeNames.length}`);
-  console.log('--------------------------------------');
-  console.log('所有节点名称:');
-  if (allNodeNames.length > 0) {
-    allNodeNames.forEach((name, index) => {
-      const isGate = name.startsWith('gate_') ? ' 🚪' : '';
-      console.log(`  ${index + 1}. ${name}${isGate}`);
+    // 计算矩形边界
+    const latOffset = size / 111000;
+    const lonOffset = size / (111000 * Math.cos(centerLat * Math.PI / 180));
+    const west = centerLon - lonOffset / 2;
+    const east = centerLon + lonOffset / 2;
+    const south = centerLat - latOffset / 2;
+    const north = centerLat + latOffset / 2;
+
+    const rectangle = Cesium.Rectangle.fromDegrees(west, south, east, north);
+
+    // 创建矩形几何
+    const rectangleGeometry = new Cesium.RectangleGeometry({
+      rectangle: rectangle,
+      height: altitude,
+      vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat
     });
-  } else {
-    console.log('  (未找到任何节点)');
-  }
-  console.log('--------------------------------------');
-  if (gateNodeNames.length > 0) {
-    console.log('闸门节点(gate_开头):');
-    gateNodeNames.forEach((name, index) => {
-      console.log(`  ${index + 1}. ${name}`);
+
+    const geometryInstance = new Cesium.GeometryInstance({
+      geometry: rectangleGeometry
     });
-  }
-  console.log('======================================');
-
-  if (allNodeNames.length === 0) {
-    console.warn('⚠️ 未找到任何节点,返回模拟数据用于测试');
-    console.log('   模型图元类型:', modelPrimitive?.constructor?.name);
-    console.log('   runtimeNodes 存在:', runtimeNodes !== undefined);
-    console.log('   runtimeNodes 是数组:', Array.isArray(runtimeNodes));
-    console.log('   gltf.nodes 存在:', gltf?.nodes !== undefined);
-    console.log('   gltf.nodes 数量:', gltf?.nodes ? Object.keys(gltf.nodes).length : 0);
-    
-    const mockNodes = {};
-    GATE_NODE_NAMES.forEach((name, index) => {
-      mockNodes[name] = { 
-        node: { 
-          name, 
-          translation: [0, 0, 0], 
-          matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
-          _modelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
-        },
-        path: name,
-        nodeIndex: index,
-        modelPrimitive: modelPrimitive
-      };
+
+    // 创建风格化水面材质(传入纹理 URL + 天空纹理)
+    const waterMaterial = createStylizedWaterMaterial({
+      alpha: 0.85,
+      waveHeight: 0.6,
+      flowSpeed: 0.5,
+      foamIntensity: 0.25,
+      normalMap1: waterNormal1Url,
+      normalMap2: waterNormal2Url,
+      normalMap3: waterNormal3Url,
+      skyTexture: skyTextureUrl
     });
-    console.log('返回的模拟闸门节点:', Object.keys(mockNodes));
-    return mockNodes;
-  }
 
-  return Object.keys(gateNodes).length > 0 ? gateNodes : allNodes;
-}
+    // 创建 Primitive
+    this._simplePlanePrimitive = this.viewer.scene.primitives.add(
+      new Cesium.Primitive({
+        geometryInstances: geometryInstance,
+        appearance: new Cesium.MaterialAppearance({
+          material: waterMaterial,
+          flat: true,
+          aboveGround: true
+        }),
+        asynchronous: false
+      })
+    );
 
-/**
- * 动画单个闸门节点
- * @param {Object} gateNode - 闸门节点
- * @param {Number} position - 开度百分比 (0-100)
- */
-export function animateGateNode(gateNode, position) {
-  if (!gateNode) return;
-
-  // 用户需求:开100%时在最高位置,关0%时向下移动5米
-  // 所以 position=100% 时,height=0(不移动)
-  // position=0% 时,height=5米(向下移动5米)
-  const height = ((100 - position) / 100) * MAX_GATE_HEIGHT;
-  console.log(`🎢 移动闸门 ${gateNode.name || gateNode.node?.name} 到位置 ${position}% (向下移动: ${height}米)`);
-
-  // 获取实际的节点对象
-  const actualNode = gateNode.node || gateNode;
-  
-  // 方式1: 优先使用 Cesium Model 的 setNodeMatrix 方法(唯一能真正触发渲染更新的方式)
-  if (gateNode.modelPrimitive && gateNode.nodeIndex !== undefined) {
-    const model = gateNode.modelPrimitive;
-    console.log(`   方式1: 尝试使用 model.setNodeMatrix - nodeIndex: ${gateNode.nodeIndex}`);
-    
-    if (typeof model.getNodeMatrix === 'function' && typeof model.setNodeMatrix === 'function') {
-      if (!gateNode.originNodeMatrix) {
-        gateNode.originNodeMatrix = model.getNodeMatrix(gateNode.nodeIndex);
-        console.log(`   初始化 originNodeMatrix from model`);
+    // 启动动画
+    const startTime = Date.now();
+    this._simplePlaneAnimHandler = this.viewer.scene.preRender.addEventListener(() => {
+      if (this._simplePlanePrimitive && waterMaterial.uniforms) {
+        const time = (Date.now() - startTime) / 1000;
+        waterMaterial.uniforms.uSwTime = time;
       }
-      
-      const newMatrix = Cesium.Matrix4.clone(gateNode.originNodeMatrix);
-      const translation = Cesium.Cartesian3.fromArray([0, 0, height]);
-      Cesium.Matrix4.multiplyByTranslation(newMatrix, translation, newMatrix);
-      model.setNodeMatrix(gateNode.nodeIndex, newMatrix);
-      console.log(`   ✅ 使用 model.setNodeMatrix 移动成功,Z轴偏移: ${height}米`);
-      
-      // 强制触发场景渲染
-      if (window.viewer && window.viewer.scene) {
-        window.viewer.scene.requestRender();
+    });
+
+    console.log(`✅ 水面面片已创建: (${cfg.coordinates}), 尺寸: ${size}米×${size}米`);
+  }
+
+  // 清除所有已创建的面片
+  _clearPlanes() {
+    // 停止所有面片动画
+    this._planeAnimationHandlers.forEach(handler => handler());
+    this._planeAnimationHandlers = [];
+
+    // 清除所有面片的显示定时器
+    this._gatePlaneMap.forEach(entry => {
+      if (entry.timeoutId) {
+        clearTimeout(entry.timeoutId);
       }
-      return;
-    } else {
-      console.log(`   ❌ model 没有 getNodeMatrix/setNodeMatrix 方法,尝试其他方式`);
+    });
+    this._gatePlaneMap.clear();
+
+    if (this.viewer) {
+      this.planeEntities.forEach(primitive => {
+        this.viewer.scene.primitives.remove(primitive);
+      });
     }
-  } else {
-    console.log(`   ⚠️ modelPrimitive 或 nodeIndex 缺失,尝试其他方式`);
-    console.log(`   gateNode.modelPrimitive:`, gateNode.modelPrimitive);
-    console.log(`   gateNode.nodeIndex:`, gateNode.nodeIndex);
+    this.planeEntities = [];
   }
 
-  // 方式2: 尝试使用运行时节点的 _modelMatrix 属性
-  if (actualNode._modelMatrix) {
-    if (!gateNode.originModelMatrix) {
-      gateNode.originModelMatrix = [...actualNode._modelMatrix];
-      console.log(`   初始化 originModelMatrix`);
-    }
-    const newMatrix = [...gateNode.originModelMatrix];
-    newMatrix[14] = gateNode.originModelMatrix[14] + height;
-    actualNode._modelMatrix = newMatrix;
-    console.log(`   ✅ 使用 _modelMatrix 属性移动`);
-    
-    // 强制触发渲染更新
-    if (gateNode.modelPrimitive && gateNode.modelPrimitive._runtimeNodes) {
-      gateNode.modelPrimitive._runtimeNodes[gateNode.nodeIndex] = actualNode;
-    }
-    
-    // 强制触发场景渲染
-    if (window.viewer && window.viewer.scene) {
-      window.viewer.scene.requestRender();
+  // 显示指定闸门的面片,时长与闸门移动动画时长一致(3秒)
+  // 若在显示期间再次调用,会重置倒计时
+  _showPlaneTemporarily(gateName, duration = 3000) {
+    const entry = this._gatePlaneMap.get(gateName);
+    if (!entry) return;
+
+    // 重置倒计时:清除之前的定时器
+    if (entry.timeoutId) {
+      clearTimeout(entry.timeoutId);
     }
-    return;
+
+    // 显示面片
+    entry.primitive.show = true;
+
+    // 10秒后自动隐藏
+    entry.timeoutId = setTimeout(() => {
+      if (entry.primitive) {
+        entry.primitive.show = false;
+      }
+      entry.timeoutId = null;
+    }, duration);
   }
 
-  // 方式3: 尝试使用运行时节点的 matrix 属性
-  if (actualNode.matrix) {
-    if (!gateNode.originMatrix) {
-      gateNode.originMatrix = [...actualNode.matrix];
-      console.log(`   初始化 originMatrix`);
-    }
-    const newMatrix = [...gateNode.originMatrix];
-    newMatrix[14] = gateNode.originMatrix[14] + height; // 修改 Z 分量
-    actualNode.matrix = newMatrix;
-    console.log(`   ✅ 使用 matrix 属性移动`);
-    
-    // 强制触发场景渲染
-    if (window.viewer && window.viewer.scene) {
-      window.viewer.scene.requestRender();
+  setGatePosition(data, position) {
+    if (!this.isModelLoaded || !this.entity?.model?.nodeTransformations) {
+      return;
     }
-    return;
-  }
 
-  // 方式4: 尝试使用 GLTF 节点的 translation 属性(后备方案)
-  if (actualNode.translation && Array.isArray(actualNode.translation)) {
-    if (!gateNode.originOffset) {
-      gateNode.originOffset = [...actualNode.translation];
-      console.log(`   初始化 originOffset:`, gateNode.originOffset);
+    if (typeof data === 'number') {
+      this.setAllGatePosition(data);
+      return;
     }
-    
-    actualNode.translation = [
-      gateNode.originOffset[0],
-      gateNode.originOffset[1],
-      gateNode.originOffset[2] + height
-    ];
-    console.log(`   ⚠️ 使用 translation 属性移动(可能无法实时渲染更新)`);
-    
-    // 强制触发场景渲染
-    if (window.viewer && window.viewer.scene) {
-      window.viewer.scene.requestRender();
+
+    if (typeof data === 'string' && position !== undefined) {
+      this.setSingleGatePosition(data, position);
     }
-    return;
   }
 
-  console.warn('⚠️ 未找到可用的位置控制属性:', gateNode.name || gateNode.node?.name);
-  console.log('   gateNode 结构:', JSON.stringify(Object.keys(gateNode)));
-  if (gateNode.node) {
-    console.log('   gateNode.node 结构:', JSON.stringify(Object.keys(gateNode.node)));
-  }
-}
+  setSingleGatePosition(gateName, value) {
+    console.log('🚪 setSingleGatePosition 被调用:', gateName, value);
 
-// 测试函数:检查闸门节点结构
-export function testGateNodeStructure(gateNode) {
-  console.log('🔍 测试闸门节点结构:', gateNode.name);
-  console.log('   gateNode:', gateNode);
-  console.log('   gateNode.node:', gateNode.node);
-  console.log('   gateNode.translation:', gateNode.translation);
-  console.log('   gateNode.matrix:', gateNode.matrix);
-  console.log('   gateNode._modelMatrix:', gateNode._modelMatrix);
-  console.log('   gateNode.modelPrimitive:', gateNode.modelPrimitive);
-  console.log('   gateNode.nodeIndex:', gateNode.nodeIndex);
-}
+    if (!GATE_NODE_NAMES.includes(gateName)) {
+      console.warn('⚠️ 无效的闸门名称:', gateName);
+      return;
+    }
 
-/**
- * 动画所有闸门节点
- * @param {Object} gateNodes - 闸门节点映射
- * @param {Number} position - 开度百分比 (0-100)
- */
-export function animateAllGateNodes(gateNodes, position) {
-  for (const gateName of Object.keys(gateNodes)) {
-    const gateNode = gateNodes[gateName];
-    animateGateNode(gateNode, position);
-  }
-}
+    if (!this.isModelLoaded || !this.entity?.model?.nodeTransformations) {
+      console.error('❌ 模型未加载或 nodeTransformations 不可用');
+      return;
+    }
 
-export function animateSingleGate(gateNodes, gateName, position) {
-  if (!gateNodes || !gateNodes[gateName]) {
-    console.warn('未找到闸门:', gateName);
-    return;
-  }
-  animateGateNode(gateNodes[gateName], position);
-}
+    const Cesium = window.Cesium;
 
-/**
- * 场景配置
- */
-export const JuKouSceneConfig = {
-  sceneId: 'fixed_jushui_gate',
-  sceneName: '莒口水闸',
-  disableModelTransform: true,
-  gateNodePrefix: 'gate_',
-  gateNodeNames: GATE_NODE_NAMES,
-  initialGatePosition: 0
-};
+    // 0%关(闸门往下)、100%开(闸门在原位)
+    // Y轴负方向表示往下移动
+    const lift = -((100 - value) / 100) * MAX_GATE_HEIGHT;
+    console.log('📐 计算的 lift 值:', lift);
 
-/**
- * 场景初始化钩子
- * 在场景加载完成后调用
- * @param {Object} entity - 场景实体
- * @param {Function} onGateNodesReady - 闸门节点就绪回调
- */
-export function onSceneLoaded(entity, onGateNodesReady) {
-  console.log('========== onSceneLoaded 被调用 ==========');
-  
-  if (!entity) {
-    console.error('❌ 场景实体未定义');
-    console.log('============================================');
-    return;
-  }
+    const trans = this.entity.model.nodeTransformations[gateName];
+    console.log('🔧 获取到的变换对象:', trans);
 
-  console.log('场景实体类型:', entity.constructor?.name);
-  console.log('场景实体名称:', entity.name);
-  
-  const Cesium = window.Cesium;
-  let modelPrimitive = null;
-  
-  // 方式1: 从 entity.model 获取(Property 对象)
-  if (entity.model) {
-    console.log('🔍 尝试方式1: entity.model');
-    if (typeof entity.model.getValue === 'function') {
-      console.log('   → entity.model 是 Property 对象,尝试获取实际值');
-      const time = Cesium?.JulianDate.now?.();
-      const actualModel = entity.model.getValue(time);
-      if (actualModel) {
-        modelPrimitive = actualModel;
-        console.log('   ✅ 获取到实际模型:', actualModel.constructor?.name);
-        console.log('   model._model:', actualModel._model !== undefined);
-        console.log('   model._gltf:', actualModel._gltf !== undefined);
-      }
-    } else if (entity.model._model) {
-      modelPrimitive = entity.model._model;
-      console.log('   ✅ 从 entity.model._model 获取:', modelPrimitive.constructor?.name);
+    if (!trans) {
+      console.error('❌ 未找到闸门节点的变换对象:', gateName);
+      return;
     }
+
+    // Y 轴移动,负值表示往下
+    trans.translation = new Cesium.Cartesian3(0, lift, 0);
+    console.log('✅ 已设置 translation(Y轴) 为:', trans.translation);
+
+    this.gateStates[gateName] = value;
+    this.viewer.scene.requestRender();
+
+    // 显示该闸门对应的面片(3秒后自动隐藏)
+    this._showPlaneTemporarily(gateName);
+
+    this.onGatePositionChange && this.onGatePositionChange(gateName, value);
   }
-  
-  // 方式2: 从 entity._model 获取
-  if (!modelPrimitive && entity._model) {
-    console.log('🔍 尝试方式2: entity._model');
-    modelPrimitive = entity._model;
-    console.log('   ✅ 获取到模型:', modelPrimitive.constructor?.name);
+
+  setAllGatePosition(value) {
+    const Cesium = window.Cesium;
+
+    // 0%关(闸门往下)、100%开(闸门在原位)
+    // Y轴负方向表示往下移动
+    const lift = -((100 - value) / 100) * MAX_GATE_HEIGHT;
+    GATE_NODE_NAMES.forEach(name => {
+      this.entity.model.nodeTransformations[name].translation = new Cesium.Cartesian3(0, lift, 0);
+      this.gateStates[name] = value;
+    });
+    this.viewer.scene.requestRender();
+
+    // 显示所有闸门对应的面片(3秒后自动隐藏)
+    GATE_NODE_NAMES.forEach(name => {
+      this._showPlaneTemporarily(name);
+    });
+
+    this.onGatePositionChange && this.onGatePositionChange('all', value);
   }
-  
-  // 方式3: 检查 modelPrimitive._model(Cesium Model 的内部结构)
-  if (modelPrimitive && modelPrimitive._model && !modelPrimitive._gltf) {
-    console.log('🔍 尝试方式3: modelPrimitive._model');
-    modelPrimitive = modelPrimitive._model;
-    console.log('   ✅ 使用内部 _model 对象:', modelPrimitive.constructor?.name);
+
+  flyToScene(options = {}) {
+    if (!this.entity) return;
+    const Cesium = window.Cesium;
+    const duration = options.duration || 2;
+    this.viewer.flyTo(this.entity, {
+      duration,
+      offset: new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-30), 100)
+    });
   }
-  
-  // 方式4: 从 viewer.scene.primitives 查找(使用不同的方法)
-  if (!modelPrimitive || !modelPrimitive._gltf) {
-    console.log('🔍 尝试方式4: 遍历 viewer.scene.primitives');
-    if (window.viewer && window.viewer.scene && window.viewer.scene.primitives) {
-      const primitives = window.viewer.scene.primitives;
-      console.log('   primitives 数量:', primitives.length);
-      
-      // 获取模型文件名用于匹配
-      let targetFileName = '莒口水闸3';
-      
-      for (let i = 0; i < primitives.length; i++) {
-        const primitive = primitives.get(i);
-        
-        // 检查是否是模型
-        if (primitive && primitive.constructor && primitive.constructor.name) {
-          console.log(`   [${i}] ${primitive.constructor.name}: ready=${primitive.ready}`);
-          
-          // 详细检查 primitive 的所有属性
-          console.log(`   [${i}] 检查属性:`);
-          console.log(`       _gltf: ${primitive._gltf !== undefined}`);
-          console.log(`       _model: ${primitive._model !== undefined}`);
-          console.log(`       _model._gltf: ${primitive._model && primitive._model._gltf !== undefined}`);
-          console.log(`       _runtimeNodes: ${primitive._runtimeNodes !== undefined}`);
-          console.log(`       _nodeRuntimeReferences: ${primitive._nodeRuntimeReferences !== undefined}`);
-          
-          // 尝试获取 GLTF 数据
-          let gltfData = primitive._gltf || (primitive._model && primitive._model._gltf);
-          
-          // 如果还没找到,尝试深度搜索
-          if (!gltfData) {
-            const findGltfInObj = (obj, depth = 0) => {
-              if (depth > 3) return null;
-              if (!obj || typeof obj !== 'object') return null;
-              
-              if (obj._gltf) return obj._gltf;
-              if (obj.nodes && typeof obj.nodes === 'object') return { nodes: obj.nodes };
-              
-              for (const key of Object.keys(obj)) {
-                if (key.startsWith('_') && typeof obj[key] === 'object') {
-                  const result = findGltfInObj(obj[key], depth + 1);
-                  if (result) return result;
-                }
-              }
-              return null;
-            };
-            gltfData = findGltfInObj(primitive);
-          }
-          
-          if (gltfData) {
-            const url = primitive._url || primitive._uri || (primitive._model && primitive._model._url);
-            console.log(`   [${i}] ✅ 发现 GLTF 数据!URL:`, url ? url.split('/').pop() : '未知');
-            console.log(`   [${i}] 节点数:`, gltfData.nodes ? Object.keys(gltfData.nodes).length : 0);
-            
-            if (!url || (url && url.includes(targetFileName))) {
-              modelPrimitive = primitive._model || primitive;
-              // 如果 primitive 本身没有 _gltf,但找到了 gltfData,挂载上去
-              if (!modelPrimitive._gltf && gltfData) {
-                modelPrimitive._gltf = gltfData;
-              }
-              console.log('   ✅ 找到匹配的模型!');
-              break;
-            }
-          }
-        }
-      }
-    }
+
+  getGateState(name) {
+    return name ? this.gateStates[name] : { ...this.gateStates };
   }
-  
-  // 方式5: 尝试从 entity 的其他属性获取
-  if (!modelPrimitive || !modelPrimitive._gltf) {
-    console.log('🔍 尝试方式5: 检查 entity 的其他属性');
-    console.log('   entity._properties:', entity._properties !== undefined);
-    console.log('   entity._primitive:', entity._primitive !== undefined);
-    if (entity._primitive) {
-      modelPrimitive = entity._primitive;
-      console.log('   ✅ 使用 entity._primitive');
-    }
+
+  getGateNames() {
+    return [...GATE_NODE_NAMES];
   }
 
-  if (!modelPrimitive) {
-    console.error('❌ 无法获取模型图元');
-    console.log('============================================');
-    return;
+  isReady() {
+    return this.isInitialized && this.isModelLoaded;
   }
-  
-  console.log('✅ 成功获取模型图元');
-  console.log('模型类型:', modelPrimitive.constructor?.name);
-  console.log('ready状态:', modelPrimitive.ready);
-  console.log('_gltf已加载:', modelPrimitive._gltf !== undefined);
-  console.log('_model._gltf已加载:', modelPrimitive._model && modelPrimitive._model._gltf !== undefined);
-  
-  // 如果 modelPrimitive 本身没有 _gltf,但 _model 有,则使用 _model
-  if (!modelPrimitive._gltf && modelPrimitive._model && modelPrimitive._model._gltf) {
-    console.log('🔄 切换到 modelPrimitive._model 获取 GLTF 数据');
-    modelPrimitive = modelPrimitive._model;
+
+  getEntity() {
+    return this.entity;
   }
-  
-  // 方法6: 尝试深度搜索 GLTF 数据(调试用)
-  if (!modelPrimitive._gltf) {
-    console.log('🔍 尝试深度搜索 GLTF 数据...');
-    const findGltfRecursively = (obj, path = 'obj', depth = 0) => {
-      if (depth > 5) return null;
-      if (!obj || typeof obj !== 'object') return null;
-      
-      // 检查当前对象是否有 _gltf 或 nodes
-      if (obj._gltf) {
-        console.log(`   ✅ 在 ${path}._gltf 找到 GLTF 数据!`);
-        return obj._gltf;
-      }
-      if (obj.nodes && typeof obj.nodes === 'object') {
-        console.log(`   ✅ 在 ${path}.nodes 找到节点数据!`);
-        return { nodes: obj.nodes };
+
+  destroy() {
+    // 停止所有面片动画
+    this._planeAnimationHandlers.forEach(handler => handler());
+    this._planeAnimationHandlers = [];
+
+    // 清除所有面片的显示定时器
+    this._gatePlaneMap.forEach(entry => {
+      if (entry.timeoutId) {
+        clearTimeout(entry.timeoutId);
       }
-      
-      // 递归搜索
-      for (const key of Object.keys(obj)) {
-        if (key.startsWith('_') || typeof obj[key] === 'object') {
-          const result = findGltfRecursively(obj[key], `${path}.${key}`, depth + 1);
-          if (result) return result;
-        }
+    });
+    this._gatePlaneMap.clear();
+
+    if (this.viewer) {
+      if (this.entity) {
+        this.viewer.entities.remove(this.entity);
       }
-      return null;
-    };
-    
-    const foundGltf = findGltfRecursively(modelPrimitive, 'modelPrimitive');
-    if (foundGltf) {
-      // 将找到的 GLTF 数据挂载到 modelPrimitive 上以便后续使用
-      modelPrimitive._gltf = foundGltf;
-      console.log('✅ 成功挂载 GLTF 数据到 modelPrimitive._gltf');
-    } else {
-      console.log('❌ 深度搜索也未找到 GLTF 数据');
+      // 移除所有面片
+      this.planeEntities.forEach(primitive => {
+        this.viewer.scene.primitives.remove(primitive);
+      });
+      this.planeEntities = [];
     }
-  }
-  
-  const checkModelReady = () => {
-    const hasGltf = modelPrimitive._gltf !== undefined;
-    const hasNodes = modelPrimitive._gltf && modelPrimitive._gltf.nodes !== undefined;
-    const isReady = modelPrimitive.ready === true;
-    
-    console.log(`⏳ 检查模型状态: ready=${isReady}, _gltf=${hasGltf}, hasNodes=${hasNodes}`);
-    
-    if (hasNodes) {
-      console.log('✅ 模型已就绪,开始提取闸门节点...');
-      const gateNodes = extractGateNodes(modelPrimitive, GATE_NODE_NAMES);
-      entity.gateNodes = gateNodes;
-      
-      if (typeof onGateNodesReady === 'function') {
-        onGateNodesReady(gateNodes);
-      }
-      
-    } else if (isReady && !hasGltf) {
-      console.log('⏳ 模型就绪但 _gltf 未加载,等待中...');
-      setTimeout(checkModelReady, 200);
-      
-    } else if (hasGltf && !hasNodes) {
-      console.log('⏳ _gltf 存在但 nodes 未加载,等待中...');
-      setTimeout(checkModelReady, 200);
-      
-    } else {
-      console.log('⏳ 模型未就绪,等待中...');
-      setTimeout(checkModelReady, 200);
+    // 停止简单标记面片动画
+    if (this._simplePlaneAnimHandler) {
+      destroyStylizedWaterAnimation(this._simplePlaneAnimHandler);
+      this._simplePlaneAnimHandler = null;
     }
-  };
-  
-  checkModelReady();
+    // 移除简单标记面片
+    if (this._simplePlanePrimitive) {
+      this.viewer.scene.primitives.remove(this._simplePlanePrimitive);
+      this._simplePlanePrimitive = null;
+    }
+
+    this.entity = null;
+    this.isInitialized = false;
+    this.isModelLoaded = false;
+  }
 }
 
-/**
- * 场景卸载钩子
- * 在场景卸载前调用
- * @param {Object} entity - 场景实体
- */
-export function onSceneUnloaded(entity) {
-  if (entity && entity.gateNodes) {
-    delete entity.gateNodes;
+let globalController = null;
+
+export function getController() {
+  return globalController;
+}
+
+export async function loadJuKouScene(viewer, options = {}) {
+  if (globalController) globalController.destroy();
+  globalController = new JuKouShuiZhaController();
+  await globalController.loadScene(viewer, options);
+  return globalController;
+}
+
+export function unloadJuKouScene() {
+  if (globalController) {
+    globalController.destroy();
+    globalController = null;
   }
 }
+
+export function createController() {
+  if (!globalController) globalController = new JuKouShuiZhaController();
+  return globalController;
+}
+
+export function destroyController() {
+  unloadJuKouScene();
+}

+ 38 - 3
RuoYi-Vue3/src/supermap-cesium-module/components/gate-control/GateControl.vue

@@ -39,13 +39,21 @@
         </div>
       </div>
 
-      <!-- 闸门列表 -->
+      <!-- 闸门列表(可收缩) -->
       <div class="gates-section">
-        <div class="gates-label">闸门列表 (共 {{ gates.length }} 个)</div>
+        <div class="gates-label" @click="toggleGateList">
+          <span class="gates-label-text">闸门列表 (共 {{ gates.length }} 个)</span>
+          <el-icon 
+            class="collapse-icon" 
+            :class="{ collapsed: gateListCollapsed }"
+          >
+            <ArrowDown />
+          </el-icon>
+        </div>
         <div v-if="gates.length === 0" style="color: #999; padding: 20px; text-align: center;">
           暂无闸门数据
         </div>
-        <div class="gates-list">
+        <div v-show="!gateListCollapsed" class="gates-list">
           <div 
             v-for="gate in gates" 
             :key="gate.name" 
@@ -164,6 +172,12 @@ export default {
     });
     // 闸门列表
     const gates = ref([])
+    const gateListCollapsed = ref(false)
+
+    // 切换闸门列表展开/收缩
+    const toggleGateList = () => {
+      gateListCollapsed.value = !gateListCollapsed.value
+    }
 
     // 检查是否有任何闸门正在动画
     const isAnyAnimating = computed(() => {
@@ -311,10 +325,12 @@ export default {
 
     return {
       gates,
+      gateListCollapsed,
       isAnyAnimating,
       updateGateList,
       getGateStatusClass,
       getGateStatusText,
+      toggleGateList,
       openGate,
       closeGate,
       setGatePosition,
@@ -406,6 +422,25 @@ export default {
   font-size: 14px;
   color: #606266;
   font-weight: 500;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  cursor: pointer;
+  user-select: none;
+  padding: 4px 0;
+}
+
+.gates-label:hover {
+  color: #409eff;
+}
+
+.collapse-icon {
+  font-size: 14px;
+  transition: transform 0.25s ease;
+}
+
+.collapse-icon.collapsed {
+  transform: rotate(-90deg);
 }
 
 .gates-list {

+ 35 - 2
RuoYi-Vue3/src/supermap-cesium-module/components/scene/pick-coordinate/pick-coordinate.vue

@@ -260,10 +260,43 @@ export default {
         this.startPicking()
       }
     },
+    copyToClipboard(text) {
+      // 优先使用 Clipboard API
+      const tryClipboard = () => {
+        if (navigator.clipboard && navigator.clipboard.writeText) {
+          return navigator.clipboard.writeText(text)
+        }
+        return Promise.reject(new Error('Clipboard API not available'))
+      }
+      // 降级方案:使用 textarea + execCommand
+      const tryFallback = () => {
+        return new Promise((resolve, reject) => {
+          try {
+            const textarea = document.createElement('textarea')
+            textarea.value = text
+            textarea.style.position = 'fixed'
+            textarea.style.opacity = '0'
+            document.body.appendChild(textarea)
+            textarea.select()
+            const success = document.execCommand('copy')
+            document.body.removeChild(textarea)
+            if (success) {
+              resolve()
+            } else {
+              reject(new Error('execCommand copy failed'))
+            }
+          } catch (err) {
+            reject(err)
+          }
+        })
+      }
+      // Clipboard API 失败后自动降级到 textarea 方式
+      return tryClipboard().catch(tryFallback)
+    },
     copyCoordinate() {
       if (this.lastPosition) {
         const coord = `${this.lastPosition.longitude}, ${this.lastPosition.latitude}, ${this.lastPosition.height}`
-        navigator.clipboard.writeText(coord).then(() => {
+        this.copyToClipboard(coord).then(() => {
           ElMessage.success('坐标已复制到剪贴板')
         }).catch(() => {
           ElMessage.error('复制失败')
@@ -272,7 +305,7 @@ export default {
     },
     copyHistoryCoordinate(pos) {
       const coord = `${pos.longitude}, ${pos.latitude}, ${pos.height}`
-      navigator.clipboard.writeText(coord).then(() => {
+      this.copyToClipboard(coord).then(() => {
         ElMessage.success('坐标已复制到剪贴板')
       }).catch(() => {
         ElMessage.error('复制失败')

+ 15 - 35
RuoYi-Vue3/src/supermap-cesium-module/config/fixedScenes.js

@@ -2,48 +2,28 @@
  * 固定业务场景配置
  * 这些场景是系统内置的,不能被删除,只能加载和卸载
  */
-import { GATE_NODE_NAMES, JuKouSceneConfig } from '../business-scenes/JuKouShuiZhaScene';
+
+import { JuKouSceneConfig } from '../business-scenes/JuKouShuiZhaScene';
 
 export const fixedScenes = [
   {
-    sceneId: 'fixed_jushui_gate',
-    sceneName: '莒口水闸',
+    sceneId: JuKouSceneConfig.sceneId,
+    sceneName: JuKouSceneConfig.sceneName,
     sceneType: 'fixed',
     description: '莒口水闸固定场景,包含闸门启闭控制功能',
-    // 第三人称相机位置(Cartesian3坐标)
-    thirdPersonCameraPos: '[-2794762.0047350973, 5012854.3612919515, 2782682.393722253]',
-    thirdPersonCameraTarget: '[-2794762.0047350973, 5012854.3612919515, 0]',
-    // 相机方向向量
-    cameraDirection: '[-0.5190347522307343, -0.8239501952633861, 0.22739613387700425]',
-    // 相机上向量
-    cameraUp: '[-0.6385171678739079, 0.5506184719962913, 0.5376942668717541]',
-    // 需要加载的模型
-    loadedModels: '[{ "name": "莒口水闸3", "filePath": "/models/莒口水闸3.glb", "coordinates": "119.144159,25.867745,23", "rotation": "0,0,260", "transparency": 0.8 }]',
-    // 需要加载的数据服务
-    dataServices: '[]',
-    // POI点
-    poiPoints: '[]',
-    // 是否启用闸门控制
+    // 相机配置(用于视口跳转)
+    thirdPersonCameraPos: JSON.stringify(JuKouSceneConfig.camera.position),
+    cameraDirection: JSON.stringify(JuKouSceneConfig.camera.direction),
+    cameraUp: JSON.stringify(JuKouSceneConfig.camera.up),
+    // 闸门控制配置
     enableGateControl: true,
-    // 闸门初始位置(0-100)
-    initialGatePosition: 0,
-    // 是否禁用模型变换弹框(点击模型不显示变换面板)
+    initialGatePosition: JuKouSceneConfig.initialGatePosition,
+    // 模型配置(从 JuKouSceneConfig 引用,用于显示在配置界面)
+    loadedModels: JSON.stringify([JuKouSceneConfig.model]),
+    // 是否禁用模型变换弹框
     disableModelTransform: JuKouSceneConfig.disableModelTransform,
-    // 闸门子节点名称前缀(用于识别模型中的闸门子节点)
-    gateNodePrefix: JuKouSceneConfig.gateNodePrefix,
-    // 闸门子节点名称列表(从业务场景模块引入)
-    gateNodeNames: GATE_NODE_NAMES,
-    // 业务场景处理器路径
-    sceneHandler: '../business-scenes/JuKouShuiZhaScene.js',
-    // 模型拆解图标配置(点击弹框显示启闭机模型拆解)
-    modelDisassemblyIcon: {
-      longitude: 119.143770,
-      latitude: 25.867679,
-      height: 43.80,
-      image: '/img/svg/logo.svg',
-      width: 32,
-      height: 32
-    }
+    // 模型拆解图标配置
+    modelDisassemblyIcon: JuKouSceneConfig.modelDisassemblyIcon
   }
 ]
 

+ 123 - 120
RuoYi-Vue3/src/supermap-cesium-module/views/layout/aside.vue

@@ -384,6 +384,7 @@
 
       <!-- 闸门启闭控制组件 -->
       <GateControl
+        ref="gateControl"
         :visible="showGateControl"
         :initial-position="gatePosition"
         @close="showGateControl = false"
@@ -418,7 +419,7 @@ import MvtAttributePopup from "../../components/MvtAttributePopup.vue";  //MVT
 import MvtClickHandler from "../../js/common/mvtClickHandler.js";  //MVT点击处理器
 import GateControl from "../../components/gate-control/GateControl.vue";  //闸门启闭控制组件
 import { getFixedScenes, isFixedScene } from "../../config/fixedScenes.js";  //固定场景配置
-import { onSceneLoaded, onSceneUnloaded, animateSingleGate, animateAllGateNodes, extractGateNodes } from "../../business-scenes/JuKouShuiZhaScene.js";  //莒口水闸业务场景
+import { loadJuKouScene, unloadJuKouScene, getController, JuKouSceneConfig } from "../../business-scenes/JuKouShuiZhaScene.js";  //莒口水闸业务场景模块
 
 export default {
   name: "layout-aside",
@@ -1088,57 +1089,35 @@ export default {
             console.log('hasGateNames:', hasGateNames);
             console.log('entity:', entity);
             console.log('model:', model);
-            console.log('hasGateNames:', hasGateNames);
             
-            // 对于莒口水闸场景,始终输出模型组成信息
+            // 对于莒口水闸场景,使用控制器初始化闸门
             const modelPath = model.url || model.filePath;
             const isJuKouScene = modelPath && modelPath.includes('莒口水闸')
             if (hasGateNames || isJuKouScene) {
-              console.log('🔄 等待 GLTF 数据加载...');
+              console.log('🔄 莒口水闸场景,使用控制器初始化闸门...');
               const that = this
-              waitForGltf(() => {
-                console.log('✅ GLTF 数据已加载,开始提取模型组成...');
-                
-                // 直接调用 extractGateNodes 提取闸门节点
-                const gateNodes = extractGateNodes(modelPrimitive);
-                console.log('📋 提取到的闸门节点:', gateNodes);
-                console.log('📋 闸门节点数量:', Object.keys(gateNodes).length);
-                const gateNames = Object.keys(gateNodes);
-                console.log('📋 闸门名称列表:', gateNames);
-                
-                // 保存到 entity 中供后续使用
-                entity.gateNodes = gateNodes;
-                
-                // 先显示面板(触发组件渲染)
-                that.showGateControl = true;
-                console.log('🔓 已设置 showGateControl = true,等待组件渲染...');
-                
-                // 使用 setTimeout 确保组件有足够时间渲染
-                setTimeout(() => {
-                  console.log('⏱️ setTimeout - 检查 gateControl 组件:', that.$refs.gateControl);
-                  if (that.$refs.gateControl) {
-                    console.log('📋 准备更新闸门列表:', gateNames);
-                    that.$refs.gateControl.updateGateList(gateNames);
-                    console.log('✅ 闸门列表已更新');
-                  } else {
-                    // 如果还是找不到组件,再等一会儿
-                    setTimeout(() => {
-                      console.log('⏱️ 第二次 setTimeout - 检查 gateControl 组件:', that.$refs.gateControl);
-                      if (that.$refs.gateControl) {
-                        that.$refs.gateControl.updateGateList(gateNames);
-                        console.log('✅ 第二次延迟后闸门列表已更新');
-                      } else {
-                        console.error('❌ 仍然找不到 gateControl 组件');
-                        // 尝试其他方式更新
-                        if (window.gateControlInstance) {
-                          console.log('📋 尝试使用全局实例更新闸门列表');
-                          window.gateControlInstance.updateGateList(gateNames);
-                        }
-                      }
-                    }, 300);
-                  }
-                }, 200);
-              })
+              
+              // 使用控制器初始化场景
+              createController(entity, {
+                initialPosition: that.gatePosition,
+                onGateNodesReady: (gateNames) => {
+                  console.log(`✅ 闸门节点就绪,共 ${gateNames.length} 个闸门`);
+                  
+                  // 显示闸门控制面板
+                  that.showGateControl = true;
+                  
+                  // 更新 GateControl 组件的闸门列表
+                  that.$nextTick(() => {
+                    if (that.$refs.gateControl) {
+                      that.$refs.gateControl.updateGateList(gateNames);
+                      console.log('✅ 闸门列表已更新');
+                    } else if (window.gateControlInstance) {
+                      window.gateControlInstance.updateGateList(gateNames);
+                      console.log('✅ 通过全局实例更新闸门列表');
+                    }
+                  });
+                }
+              });
             }
           }
           
@@ -1372,7 +1351,7 @@ export default {
     },
 
     // 加载固定场景
-    loadFixedScene(scene) {
+    async loadFixedScene(scene) {
       console.log('加载固定场景:', scene)
       
       if (!window.viewer) {
@@ -1474,36 +1453,81 @@ export default {
           }
         }
 
-        // 加载场景配置的模型
-        if (scene.loadedModels) {
-          try {
-            const models = JSON.parse(scene.loadedModels)
-            console.log('需要加载的模型:', models)
-            for (const model of models) {
-              // 创建模型对象并加载
-              const modelObj = {
-                id: model.name,
-                name: model.name,
-                filePath: model.filePath,
-                coordinates: model.coordinates,
-                rotation: model.rotation || '0,0,0',
-                transparency: model.transparency,
-                // 传递场景级别的闸门配置
-                disableTransform: scene.disableModelTransform || false,
-                gateNodePrefix: scene.gateNodePrefix || '',
-                gateNodeNames: scene.gateNodeNames || []
+        // 莒口水闸场景使用独立加载方法
+        if (scene.sceneId === JuKouSceneConfig.sceneId) {
+          console.log('📌 使用莒口水闸独立场景加载方法')
+          
+          await loadJuKouScene(window.viewer, {
+            initialPosition: scene.initialGatePosition || 0,
+            onGateNodesReady: (gateNames) => {
+              console.log(`✅ onGateNodesReady 回调触发,闸门数量: ${gateNames?.length || 0}`)
+              console.log('📋 闸门名称:', gateNames)
+              
+              // 显示闸门控制面板
+              this.showGateControl = true
+              this.gatePosition = scene.initialGatePosition || 0
+              console.log('🔓 showGateControl 已设置为 true')
+              
+              // 更新 GateControl 组件的闸门列表
+              // 使用多次 nextTick 确保组件渲染完成
+              this.$nextTick(() => {
+                console.log('⏱️ 第一次 $nextTick,检查组件渲染...')
+                
+                if (this.$refs.gateControl) {
+                  console.log('✅ 找到 gateControl 组件')
+                  this.$refs.gateControl.updateGateList(gateNames)
+                  console.log('✅ 闸门列表已更新')
+                } else {
+                  console.warn('⚠️ 第一次 nextTick 未找到组件,再次等待...')
+                  this.$nextTick(() => {
+                    if (this.$refs.gateControl) {
+                      console.log('✅ 第二次 nextTick 找到 gateControl 组件')
+                      this.$refs.gateControl.updateGateList(gateNames)
+                      console.log('✅ 闸门列表已更新')
+                    } else if (window.gateControlInstance) {
+                      console.log('✅ 通过全局实例更新闸门列表')
+                      window.gateControlInstance.updateGateList(gateNames)
+                    } else {
+                      console.error('❌ 无法找到 GateControl 组件或全局实例')
+                    }
+                  })
+                }
+              })
+            },
+            onError: (error) => {
+              console.error('❌ 莒口水闸场景加载失败:', error)
+              ElMessage.error('莒口水闸场景加载失败: ' + error)
+            }
+          })
+        } else {
+          // 其他场景使用通用加载方法
+          if (scene.loadedModels) {
+            try {
+              const models = JSON.parse(scene.loadedModels)
+              console.log('需要加载的模型:', models)
+              for (const model of models) {
+                const modelObj = {
+                  id: model.name,
+                  name: model.name,
+                  filePath: model.filePath,
+                  coordinates: model.coordinates,
+                  rotation: model.rotation || '0,0,0',
+                  transparency: model.transparency,
+                  disableTransform: scene.disableModelTransform || false,
+                  gateNodePrefix: scene.gateNodePrefix || '',
+                  gateNodeNames: scene.gateNodeNames || []
+                }
+                this.loadSceneModel(modelObj)
               }
-              this.loadSceneModel(modelObj)
+            } catch (e) {
+              console.error('解析模型配置失败:', e)
             }
-          } catch (e) {
-            console.error('解析模型配置失败:', e)
           }
-        }
-
-        // 如果场景启用了闸门控制,暂不立即显示,等待模型加载完成后在回调中显示
-        if (scene.enableGateControl) {
-          this.gatePosition = scene.initialGatePosition || 0
-          console.log('📌 场景启用闸门控制,等待模型加载...')
+          
+          if (scene.enableGateControl) {
+            this.gatePosition = scene.initialGatePosition || 0
+            console.log('📌 场景启用闸门控制,等待模型加载...')
+          }
         }
 
         // 加载模型拆解图标
@@ -1522,16 +1546,22 @@ export default {
     unloadScene(scene) {
       console.log('卸载场景:', scene)
       
-      // 卸载场景配置的模型
-      if (scene.loadedModels) {
-        try {
-          const models = JSON.parse(scene.loadedModels)
-          console.log('需要卸载的模型:', models)
-          for (const model of models) {
-            this.unloadSceneModel(model.name)
+      // 莒口水闸场景使用独立卸载方法
+      if (scene.sceneId === JuKouSceneConfig.sceneId) {
+        console.log('📌 使用莒口水闸独立场景卸载方法')
+        unloadJuKouScene()
+      } else {
+        // 其他场景使用通用卸载方法
+        if (scene.loadedModels) {
+          try {
+            const models = JSON.parse(scene.loadedModels)
+            console.log('需要卸载的模型:', models)
+            for (const model of models) {
+              this.unloadSceneModel(model.name)
+            }
+          } catch (e) {
+            console.error('解析模型配置失败:', e)
           }
-        } catch (e) {
-          console.error('解析模型配置失败:', e)
         }
       }
       
@@ -1777,48 +1807,21 @@ export default {
     handleGatePositionChange(data) {
       console.log('🔧 handleGatePositionChange 被调用:', data)
       
-      if (!window.viewer) {
-        console.error('❌ window.viewer 未定义')
+      // 获取控制器实例
+      const controller = getController()
+      if (!controller || !controller.isInitialized) {
+        console.warn('⚠️ 莒口水闸场景控制器未初始化')
         return
       }
       
-      const viewer = window.viewer
-      const entities = viewer.entities.values
-      console.log('🔧 entities 数量:', entities.length)
-      
+      // 使用控制器设置闸门位置
       if (typeof data === 'number') {
-        // 旧格式:单个位置值,应用到所有闸门
+        // 设置所有闸门位置
         this.gatePosition = data
-        console.log('🔧 应用到所有闸门,位置:', data)
-        
-        // 查找水闸模型实体并应用整体控制
-        let foundGateNodes = false
-        for (const entity of entities) {
-          if (entity.gateNodes && Object.keys(entity.gateNodes).length > 0) {
-            foundGateNodes = true
-            console.log('🔧 找到 gateNodes,数量:', Object.keys(entity.gateNodes).length)
-            // 使用业务场景模块的方法动画所有闸门
-            animateAllGateNodes(entity.gateNodes, data)
-          }
-        }
+        controller.setAllGatesPosition(data)
       } else if (data && data.gateName && typeof data.position === 'number') {
-        // 新格式:单独控制某个闸门
-        console.log('🔧 单独控制闸门:', data.gateName, '位置:', data.position)
-        
-        // 查找水闸模型实体并应用单独控制
-        let foundGateNodes = false
-        for (const entity of entities) {
-          if (entity.gateNodes) {
-            foundGateNodes = true
-            console.log('🔧 找到 gateNodes,检查是否包含', data.gateName)
-            console.log('🔧 gateNodes 包含:', Object.keys(entity.gateNodes))
-            // 使用业务场景模块的方法动画单个闸门
-            animateSingleGate(entity.gateNodes, data.gateName, data.position)
-          }
-        }
-        if (!foundGateNodes) {
-          console.warn('⚠️ 未找到 gateNodes')
-        }
+        // 设置单个闸门位置
+        controller.setSingleGatePosition(data.gateName, data.position)
       }
     },
 

BIN
RuoYi-Vue3/src/texture/FlowTexture/MASK_003.PNG


BIN
RuoYi-Vue3/src/texture/FlowTexture/T_FlowTexture_BC.PNG


BIN
RuoYi-Vue3/src/texture/FlowTexture/T_FlowTexture_BC_NORM.PNG


BIN
RuoYi-Vue3/src/texture/FlowTexture/T_FoamMacro_BC.PNG


BIN
RuoYi-Vue3/src/texture/FlowTexture/T_FoamMacro_BC_NORM.PNG


BIN
RuoYi-Vue3/src/texture/T_Waterfall_Foam.PNG


BIN
RuoYi-Vue3/src/texture/T_Waterfall_Foam_Directional.PNG


BIN
RuoYi-Vue3/src/texture/Water_1_Normal.PNG


BIN
RuoYi-Vue3/src/texture/Water_2_Normal.PNG


BIN
RuoYi-Vue3/src/texture/Water_3_Normal.PNG


BIN
RuoYi-Vue3/src/texture/sky.jpg


BIN
ruoyi-admin/target/classes/com/ruoyi/RuoYiApplication.class


BIN
ruoyi-admin/target/classes/com/ruoyi/RuoYiServletInitializer.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/cesium/CesiumMapConfigController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/common/CaptchaController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/common/CommonController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/CacheController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/ServerController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/SysLogininforController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/SysOperlogController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/monitor/SysUserOnlineController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysConfigController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysDeptController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysDictDataController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysDictTypeController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysIndexController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysLoginController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysMenuController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysNoticeController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysPostController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysProfileController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysRegisterController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysRoleController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysUserController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/tool/TestController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/tool/UserEntity.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/watershed/WatershedModelController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/watershed/WatershedServiceController.class


BIN
ruoyi-admin/target/classes/com/ruoyi/web/core/config/SwaggerConfig.class