Explorar o código

新建三维地形

BAIPCHOME\15818 hai 2 días
pai
achega
08eff50cb3

+ 96 - 0
.agents/skills/README.md

@@ -0,0 +1,96 @@
+# 技能库说明
+
+本目录包含针对 Three.js 水利三维可视化项目的专用技能,帮助用户系统化处理开发中的常见问题。
+
+## 可用技能
+
+### 1. 坐标系转换和3D模型调试 (`coordinate-debug`)
+**路径**: `coordinate-debug/SKILL.md`
+
+**适用场景**:
+- 从不同 3D 软件导出的模型坐标系不一致
+- 模型出现倒置、朝向错误或位置偏移问题
+- 需要调试 3D Tiles 渲染器和水面材质效果
+
+**主要功能**:
+- 系统化识别坐标系问题
+- 正确使用转换工具和参数
+- 有效调试 3D 模型和渲染效果
+- 建立标准化的坐标系处理流程
+
+**示例提示语**:
+- "我的 Blender 模型导入 Three.js 后倒置了,如何解决?"
+- "如何调试 3D Tiles 渲染器的坐标系问题?"
+- "水面材质显示异常,可能是坐标系问题吗?"
+
+### 2. Three.js 水利三维项目配置 (`project-config`)
+**路径**: `project-config/SKILL.md`
+
+**适用场景**:
+- 新项目初始化和搭建
+- 项目依赖管理和更新
+- 场景配置和材质参数调整
+- 性能优化和调试
+
+**主要功能**:
+- 快速搭建 Three.js 水利三维项目
+- 系统化配置场景和材质参数
+- 优化项目性能和渲染效果
+- 建立标准化的项目配置流程
+
+**示例提示语**:
+- "如何初始化 Three.js 水利三维项目?"
+- "如何调整水面材质参数?"
+- "如何优化 3D Tiles 渲染性能?"
+
+## 使用方法
+
+### 自动调用
+当用户提出相关问题时,AI 会自动识别并调用相应的技能。
+
+### 手动指定
+用户可以明确指定使用某个技能:
+```
+请使用坐标系转换技能帮我解决模型倒置问题
+```
+
+### 技能组合
+多个技能可以组合使用,例如:
+1. 先使用 `project-config` 初始化项目
+2. 再使用 `coordinate-debug` 解决坐标系问题
+
+## 技能开发
+
+### 添加新技能
+1. 在 `.agents/skills/` 目录下创建新技能目录
+2. 编写 `SKILL.md` 文件,遵循模板格式
+3. 在 `README.md` 中添加技能说明
+
+### 更新现有技能
+1. 编辑对应的 `SKILL.md` 文件
+2. 更新技能描述和示例
+3. 测试技能效果
+
+## 项目结构
+
+```
+.agents/skills/
+├── README.md                    # 技能库说明
+├── coordinate-debug/
+│   └── SKILL.md                 # 坐标系转换和调试技能
+└── project-config/
+    └── SKILL.md                 # 项目配置技能
+```
+
+## 相关资源
+
+- **项目文档**: `README.md` - 项目整体说明
+- **坐标系说明**: `坐标系转换说明.md` - 坐标系转换详细说明
+- **模型倒置解决方案**: `模型上下颠倒解决方案.md` - 常见问题解决方案
+
+## 技能更新记录
+
+### 2026-06-13
+- 创建 `coordinate-debug` 技能
+- 创建 `project-config` 技能
+- 初始化技能库文档

+ 8 - 0
.agents/skills/bug-hunt/SKILL.md

@@ -0,0 +1,8 @@
+---
+name: bug-hunt
+description: 复现并定位问题,给出最小修复和验证路径。
+---
+
+# Bug Hunt
+
+Use this skill when investigating bugs. Reproduce or narrow the symptom, trace the data flow, identify the smallest fix, and add focused verification where possible.

+ 8 - 0
.agents/skills/code-review/SKILL.md

@@ -0,0 +1,8 @@
+---
+name: code-review
+description: 审查代码变更,优先发现缺陷、回归和缺失测试。
+---
+
+# Code Review
+
+Use this skill when reviewing a code change. Prioritize correctness, regressions, security, performance, and missing tests. Lead with concrete findings and file references.

+ 188 - 0
.agents/skills/coordinate-debug/SKILL.md

@@ -0,0 +1,188 @@
+# 坐标系转换和3D模型调试技能
+
+## 技能描述
+此技能用于系统化处理 Three.js 项目中 3D 模型的坐标系转换和调试问题,特别适用于水利三维可视化场景。
+
+## 适用场景
+- 从不同 3D 软件(Blender、3ds Max、CAD)导出的模型坐标系不一致
+- 模型出现倒置、朝向错误或位置偏移问题
+- 需要调试 3D Tiles 渲染器和水面材质效果
+
+## 工作流程
+
+### 1. 问题识别阶段
+**目标**: 确定模型来源和坐标系差异
+
+**步骤**:
+1. 识别模型来源软件:
+   - Blender/3ds Max → Z-up 坐标系
+   - Three.js → Y-up 坐标系
+   - CAD 软件 → 可能使用 Z-up-forward-x
+
+2. 检查模型问题:
+   - 模型是否倒置(上下颠倒)
+   - 朝向是否正确(前后左右)
+   - 位置是否偏移
+
+3. 收集调试信息:
+   - 使用 `coordinateDebug.ts` 中的工具
+   - 检查控制台输出的坐标信息
+   - 记录模型的初始状态
+
+### 2. 转换实施阶段
+**目标**: 使用正确的参数进行坐标系转换
+
+**步骤**:
+1. 选择转换函数:
+   ```typescript
+   import { convertCoordinateSystem } from '../utils/coordinateConverter'
+   ```
+
+2. 确定转换参数:
+   - `from`: 源坐标系('z-up', 'y-up', 'z-up-forward-x')
+   - `to`: 目标坐标系(默认 'y-up')
+   - `rotationOption`: 旋转选项
+     - `'standard'`: 标准旋转
+     - `'flipped'`: 翻转旋转(解决倒置问题)
+     - `'180'`: 180度旋转
+
+3. 应用转换:
+   ```typescript
+   const convertedModel = convertCoordinateSystem(
+     model,
+     'z-up',  // Blender 默认
+     'y-up',  // Three.js 默认
+     'flipped' // 如果模型倒置
+   )
+   ```
+
+### 3. 调试验证阶段
+**目标**: 验证转换效果和渲染质量
+
+**步骤**:
+1. 坐标调试:
+   - 使用 `showCoordinatePanel` 查看拾取坐标
+   - 检查 `cameraInfo` 验证相机位置
+   - 使用 `coordinateDebug.ts` 工具验证转换
+
+2. 渲染验证:
+   - 检查水面材质是否正常显示
+   - 验证 3D Tiles 渲染器加载状态
+   - 测试相机控制和视角
+
+3. 性能检查:
+   - 监控渲染帧率
+   - 检查内存使用情况
+   - 验证 LOD(细节层次)效果
+
+### 4. 常见问题处理
+
+#### 问题1: 模型倒置
+**症状**: 模型上下颠倒
+**解决方案**:
+```typescript
+convertCoordinateSystem(model, 'z-up', 'y-up', 'flipped')
+```
+
+#### 问题2: 朝向错误
+**症状**: 模型前后左右方向错误
+**解决方案**:
+```typescript
+// 尝试不同的旋转选项
+convertCoordinateSystem(model, 'z-up', 'y-up', 'standard')
+convertCoordinateSystem(model, 'z-up', 'y-up', '180')
+```
+
+#### 问题3: 材质显示异常
+**症状**: 水面材质不显示或显示异常
+**解决方案**:
+1. 检查纹理坐标是否正确
+2. 验证法线贴图是否加载
+3. 调整材质参数(透明度、反射率等)
+
+## 质量检查清单
+
+### 转换前检查
+- [ ] 确认模型来源软件
+- [ ] 记录初始坐标系类型
+- [ ] 备份原始模型文件
+
+### 转换后验证
+- [ ] 模型朝向正确(无倒置)
+- [ ] 位置坐标符合预期
+- [ ] 材质显示正常
+- [ ] 相机控制响应正常
+
+### 性能验证
+- [ ] 渲染帧率稳定(>30fps)
+- [ ] 内存使用合理
+- [ ] LOD 切换正常
+
+## 示例代码
+
+### 基础坐标系转换
+```typescript
+import * as THREE from 'three'
+import { convertCoordinateSystem } from '../utils/coordinateConverter'
+
+// 加载模型
+const loader = new GLTFLoader()
+loader.load('model.glb', (gltf) => {
+  const model = gltf.scene
+  
+  // 转换坐标系(Blender → Three.js)
+  const convertedModel = convertCoordinateSystem(
+    model,
+    'z-up',
+    'y-up',
+    'flipped'  // 如果模型倒置
+  )
+  
+  // 添加到场景
+  scene.add(convertedModel)
+})
+```
+
+### 调试工具使用
+```typescript
+import { showCoordinateInfo, showCameraInfo } from '../utils/coordinateDebug'
+
+// 显示坐标信息
+showCoordinateInfo(pickedPosition.value)
+
+// 显示相机信息
+showCameraInfo(cameraInfo.value)
+```
+
+## 相关工具和函数
+
+### 主要工具
+- `convertCoordinateSystem`: 坐标系转换核心函数
+- `coordinateDebug.ts`: 调试工具集合
+- `Scene3D.vue`: 3D 场景组件(包含调试面板)
+
+### 配置文件
+- `sceneConfig.ts`: 场景配置和材质参数
+- `coordinateConverter.ts`: 坐标系转换矩阵定义
+
+## 最佳实践
+
+1. **备份优先**: 转换前备份原始模型
+2. **逐步验证**: 每次只改变一个参数
+3. **记录日志**: 记录转换参数和结果
+4. **团队协作**: 与 3D 艺术家沟通坐标系要求
+
+## 技能输出
+
+此技能将帮助用户:
+1. 系统化识别和解决坐标系问题
+2. 正确使用转换工具和参数
+3. 有效调试 3D 模型和渲染效果
+4. 建立标准化的坐标系处理流程
+
+## 示例提示语
+
+- "我的 Blender 模型导入 Three.js 后倒置了,如何解决?"
+- "如何调试 3D Tiles 渲染器的坐标系问题?"
+- "水面材质显示异常,可能是坐标系问题吗?"
+- "如何批量转换多个模型的坐标系?"

+ 8 - 0
.agents/skills/frontend-polish/SKILL.md

@@ -0,0 +1,8 @@
+---
+name: frontend-polish
+description: 优化界面细节、响应式状态和视觉一致性。
+---
+
+# Frontend Polish
+
+Use this skill when improving UI. Preserve the product style, check responsive states, avoid generic layouts, and verify the result visually before handing it back.

+ 213 - 0
.agents/skills/project-config/SKILL.md

@@ -0,0 +1,213 @@
+# Three.js 水利三维项目配置技能
+
+## 技能描述
+此技能用于系统化配置和管理 Three.js 水利三维可视化项目,包括依赖安装、场景配置、材质参数等。
+
+## 适用场景
+- 新项目初始化和搭建
+- 项目依赖管理和更新
+- 场景配置和材质参数调整
+- 性能优化和调试
+
+## 工作流程
+
+### 1. 项目初始化阶段
+**目标**: 建立完整的项目结构和依赖
+
+**步骤**:
+1. 安装核心依赖:
+   ```bash
+   npm install three 3d-tiles-renderer @loaders.gl/3d-tiles @loaders.gl/core @loaders.gl/gltf
+   ```
+
+2. 安装开发依赖:
+   ```bash
+   npm install -D @types/three @types/node typescript vite @vitejs/plugin-vue
+   ```
+
+3. 配置构建工具:
+   - 检查 `vite.config.ts`
+   - 验证 `tsconfig.json`
+   - 设置路径别名
+
+### 2. 场景配置阶段
+**目标**: 配置 3D 场景和渲染参数
+
+**步骤**:
+1. 场景组件配置:
+   - `Scene3D.vue`: 主场景组件
+   - `WaterLevelLabel.vue`: 水位标签组件
+   - `DucaoScene.vue`: 独山子场景
+
+2. 渲染器配置:
+   - 3D Tiles 渲染器设置
+   - 相机控制参数
+   - 光照和天空配置
+
+3. 材质参数配置:
+   - 水面材质参数
+   - 泡沫材质参数
+   - 流动纹理参数
+
+### 3. 材质参数调整阶段
+**目标**: 优化水面和材质效果
+
+**步骤**:
+1. 水面材质参数:
+   ```typescript
+   interface WaterMaterialParams {
+     opacity: number
+     metalness: number
+     roughness: number
+     reflectivity: number
+     waveSpeed: number
+     waveHeight: number
+   }
+   ```
+
+2. 泡沫材质参数:
+   ```typescript
+   interface FoamMaterialParams {
+     foamIntensity: number
+     foamScale: number
+     foamSpeed: number
+     foamColor: string
+   }
+   ```
+
+3. 流动纹理参数:
+   ```typescript
+   interface FlowMaterialParams {
+     flowSpeed: number
+     flowIntensity: number
+     flowDirection: number
+   }
+   ```
+
+### 4. 性能优化阶段
+**目标**: 提升渲染性能和用户体验
+
+**步骤**:
+1. LOD(细节层次)配置:
+   - 3D Tiles LOD 设置
+   - 模型细节层次控制
+
+2. 渲染优化:
+   - 相机视锥体裁剪
+   - 遮挡剔除
+   - 批量渲染
+
+3. 内存管理:
+   - 纹理压缩
+   - 模型缓存
+   - 资源释放
+
+## 质量检查清单
+
+### 项目初始化检查
+- [ ] 所有依赖已正确安装
+- [ ] 构建工具配置正确
+- [ ] 路径别名设置正确
+- [ ] TypeScript 配置无误
+
+### 场景配置检查
+- [ ] 场景组件加载正常
+- [ ] 相机控制响应正常
+- [ ] 光照效果符合预期
+- [ ] 天空盒显示正确
+
+### 材质参数检查
+- [ ] 水面材质透明度适中
+- [ ] 泡沫效果自然
+- [ ] 流动纹理方向正确
+- [ ] 材质性能开销合理
+
+### 性能验证检查
+- [ ] 渲染帧率稳定(>30fps)
+- [ ] 内存使用合理
+- [ ] LOD 切换平滑
+- [ ] 相机移动流畅
+
+## 示例代码
+
+### 项目初始化
+```typescript
+// package.json 依赖配置
+{
+  "dependencies": {
+    "three": "^0.184.0",
+    "3d-tiles-renderer": "^0.4.24",
+    "@loaders.gl/3d-tiles": "^4.4.1",
+    "vue": "^3.5.32"
+  }
+}
+```
+
+### 场景组件使用
+```vue
+<script setup lang="ts">
+import Scene3D from '@/components/Scene3D.vue'
+
+const sceneConfig = {
+  tilesetUrl: '/scene/tileset.json',
+  cameraPosition: { x: -831.56685, y: 40.63456, z: -2225.321 },
+  cameraTarget: { x: -843.0744, y: 12.01539, z: -2182.06814 }
+}
+</script>
+
+<template>
+  <Scene3D v-bind="sceneConfig" />
+</template>
+```
+
+### 材质参数配置
+```typescript
+import { defaultWaterParams } from '../config/sceneConfig'
+
+// 自定义水面材质参数
+const customWaterParams = {
+  ...defaultWaterParams,
+  opacity: 0.8,
+  waveSpeed: 1.5,
+  waveHeight: 0.3
+}
+```
+
+## 相关工具和函数
+
+### 配置文件
+- `sceneConfig.ts`: 场景配置和材质参数
+- `vite.config.ts`: Vite 构建配置
+- `tsconfig.json`: TypeScript 配置
+
+### 组件文件
+- `Scene3D.vue`: 主场景组件
+- `WaterLevelLabel.vue`: 水位标签组件
+- `DucaoScene.vue`: 独山子场景
+
+### 材质文件
+- `waterNew.ts`: 水面材质
+- `waterFoamUE.ts`: 泡沫材质
+- `waterFlow.ts`: 流动纹理材质
+
+## 最佳实践
+
+1. **版本管理**: 使用固定版本号避免依赖冲突
+2. **配置分离**: 将配置参数分离到独立文件
+3. **性能监控**: 定期检查渲染性能和内存使用
+4. **文档记录**: 记录配置变更和参数调整
+
+## 技能输出
+
+此技能将帮助用户:
+1. 快速搭建 Three.js 水利三维项目
+2. 系统化配置场景和材质参数
+3. 优化项目性能和渲染效果
+4. 建立标准化的项目配置流程
+
+## 示例提示语
+
+- "如何初始化 Three.js 水利三维项目?"
+- "如何调整水面材质参数?"
+- "如何优化 3D Tiles 渲染性能?"
+- "如何配置相机位置和视角?"

+ 8 - 0
.agents/skills/release-notes/SKILL.md

@@ -0,0 +1,8 @@
+---
+name: release-notes
+description: 整理面向用户的发布说明和升级注意事项。
+---
+
+# Release Notes
+
+Use this skill when preparing release notes. Group user-facing changes by outcome, call out migrations or risks, and keep wording concise and scannable.

+ 520 - 1
package-lock.json

@@ -14,6 +14,8 @@
         "@loaders.gl/terrain": "^4.4.2",
         "3d-tiles-renderer": "^0.4.24",
         "d3-geo": "^3.1.1",
+        "geodesy": "^2.4.0",
+        "geojson-3d-renderer": "^1.0.3",
         "three": "^0.184.0",
         "three-tile": "^0.11.14",
         "vue": "^3.5.32"
@@ -887,6 +889,12 @@
         "meshoptimizer": "~1.1.1"
       }
     },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+      "license": "MIT"
+    },
     "node_modules/@types/webxr": {
       "version": "0.5.24",
       "resolved": "https://registry.npmmirror.com/@types/webxr/-/webxr-0.5.24.tgz",
@@ -1075,6 +1083,42 @@
         }
       }
     },
+    "node_modules/@vueuse/core": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+      "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "10.11.1",
+        "@vueuse/shared": "10.11.1",
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+      "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+      "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+      "license": "MIT",
+      "dependencies": {
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
     "node_modules/3d-tiles-renderer": {
       "version": "0.4.24",
       "resolved": "https://registry.npmmirror.com/3d-tiles-renderer/-/3d-tiles-renderer-0.4.24.tgz",
@@ -1296,6 +1340,15 @@
         "node": ">=12.20.0"
       }
     },
+    "node_modules/commander": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/core-util-is": {
       "version": "1.0.3",
       "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -1317,6 +1370,47 @@
       "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
       "license": "MIT"
     },
+    "node_modules/d3": {
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+      "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "3",
+        "d3-axis": "3",
+        "d3-brush": "3",
+        "d3-chord": "3",
+        "d3-color": "3",
+        "d3-contour": "4",
+        "d3-delaunay": "6",
+        "d3-dispatch": "3",
+        "d3-drag": "3",
+        "d3-dsv": "3",
+        "d3-ease": "3",
+        "d3-fetch": "3",
+        "d3-force": "3",
+        "d3-format": "3",
+        "d3-geo": "3",
+        "d3-hierarchy": "3",
+        "d3-interpolate": "3",
+        "d3-path": "3",
+        "d3-polygon": "3",
+        "d3-quadtree": "3",
+        "d3-random": "3",
+        "d3-scale": "4",
+        "d3-scale-chromatic": "3",
+        "d3-selection": "3",
+        "d3-shape": "3",
+        "d3-time": "3",
+        "d3-time-format": "4",
+        "d3-timer": "3",
+        "d3-transition": "3",
+        "d3-zoom": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/d3-array": {
       "version": "3.2.4",
       "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
@@ -1329,6 +1423,167 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-axis": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+      "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-brush": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+      "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "3",
+        "d3-transition": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-chord": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+      "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-contour": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+      "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "^3.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-delaunay": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+      "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+      "license": "ISC",
+      "dependencies": {
+        "delaunator": "5"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+      "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dsv": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+      "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+      "license": "ISC",
+      "dependencies": {
+        "commander": "7",
+        "iconv-lite": "0.6",
+        "rw": "1"
+      },
+      "bin": {
+        "csv2json": "bin/dsv2json.js",
+        "csv2tsv": "bin/dsv2dsv.js",
+        "dsv2dsv": "bin/dsv2dsv.js",
+        "dsv2json": "bin/dsv2json.js",
+        "json2csv": "bin/json2dsv.js",
+        "json2dsv": "bin/json2dsv.js",
+        "json2tsv": "bin/json2dsv.js",
+        "tsv2csv": "bin/dsv2dsv.js",
+        "tsv2json": "bin/dsv2json.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-fetch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+      "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dsv": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-force": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+      "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-quadtree": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+      "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/d3-geo": {
       "version": "3.1.1",
       "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz",
@@ -1341,6 +1596,190 @@
         "node": ">=12"
       }
     },
+    "node_modules/d3-hierarchy": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+      "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-polygon": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+      "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-quadtree": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+      "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-random": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+      "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale-chromatic": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+      "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-interpolate": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+      "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/delaunator": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+      "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+      "license": "ISC",
+      "dependencies": {
+        "robust-predicates": "^3.0.2"
+      }
+    },
     "node_modules/detect-libc": {
       "version": "2.1.2",
       "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1438,6 +1877,30 @@
         "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
       }
     },
+    "node_modules/geodesy": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/geodesy/-/geodesy-2.4.0.tgz",
+      "integrity": "sha512-tHjQ1sXq8UAIEg1V0Pa6mznUxGU0R+3H5PIF6NULr0yPCAVLKqJro93Bbr19jSE18BMfyjN4osWDI4sm92m0kw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/geojson-3d-renderer": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/geojson-3d-renderer/-/geojson-3d-renderer-1.0.3.tgz",
+      "integrity": "sha512-odEVSjNt2hYq+IdzvVyw+Y1LFdYOxjxv1bSqYrO71QHCBi6fL6xrANXiRQxXeb8hJRpsjSkfFDFviiaiq8L55Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vueuse/core": "^10.0.0",
+        "d3": "^7.0.0"
+      },
+      "peerDependencies": {
+        "@tresjs/core": "^5.0.0",
+        "three": "^0.180.0",
+        "vue": "^3.3.0"
+      }
+    },
     "node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
@@ -1447,6 +1910,18 @@
         "node": ">=8"
       }
     },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/image-size": {
       "version": "0.7.5",
       "resolved": "https://registry.npmmirror.com/image-size/-/image-size-0.7.5.tgz",
@@ -1940,6 +2415,12 @@
         "util-deprecate": "~1.0.1"
       }
     },
+    "node_modules/robust-predicates": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+      "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+      "license": "Unlicense"
+    },
     "node_modules/rolldown": {
       "version": "1.0.0-rc.17",
       "resolved": "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.17.tgz",
@@ -1981,12 +2462,24 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+      "license": "BSD-3-Clause"
+    },
     "node_modules/safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
       "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
       "license": "MIT"
     },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
     "node_modules/setimmediate": {
       "version": "1.0.5",
       "resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -2063,7 +2556,7 @@
     },
     "node_modules/three": {
       "version": "0.184.0",
-      "resolved": "https://registry.npmmirror.com/three/-/three-0.184.0.tgz",
+      "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
       "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
       "license": "MIT"
     },
@@ -2243,6 +2736,32 @@
         }
       }
     },
+    "node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/vue-tsc": {
       "version": "3.2.8",
       "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.2.8.tgz",

+ 2 - 0
package.json

@@ -15,6 +15,8 @@
     "@loaders.gl/terrain": "^4.4.2",
     "3d-tiles-renderer": "^0.4.24",
     "d3-geo": "^3.1.1",
+    "geodesy": "^2.4.0",
+    "geojson-3d-renderer": "^1.0.3",
     "three": "^0.184.0",
     "three-tile": "^0.11.14",
     "vue": "^3.5.32"

BIN=BIN
public/assets/pubu.jpeg


BIN=BIN
public/assets/xiantgiao.jpeg


BIN=BIN
public/assets/xiantiao.png


BIN=BIN
public/assets/yuanzhen.png


+ 11 - 1
src/App.vue

@@ -3,8 +3,18 @@ import { ref } from 'vue'
 import Scene3D from './scenes/Scene3D.vue'
 import DucaoScene from './scenes/DucaoScene.vue'
 import Map3DScene from './scenes/Map3DScene.vue'
+import type { POIConfig } from './utils/geoCoord'
 
 const currentScene = ref<'scene3d' | 'ducao' | 'map3d'>('map3d')
+
+const mapPOIs: POIConfig[] = [
+  {
+    lng: 84.233001,
+    lat: 37.595023,
+    label: '莫勒切河节制分水闸',
+    color: '#4fc3f7',
+  },
+]
 </script>
 
 <template>
@@ -32,7 +42,7 @@ const currentScene = ref<'scene3d' | 'ducao' | 'map3d'>('map3d')
         渡槽场景
       </button>
     </div>
-    <Map3DScene v-if="currentScene === 'map3d'" />
+    <Map3DScene v-if="currentScene === 'map3d'" :initialPOIs="mapPOIs" />
     <Scene3D v-else-if="currentScene === 'scene3d'" :show-debug-tools="true" />
     <DucaoScene v-else :show-debug-tools="true" />
   </div>

+ 4 - 2
src/index.ts

@@ -1,8 +1,10 @@
-import Scene3D from './scenes/Scene3D.vue'
+import Scene3D from './scenes/Scene3D.vue'
 import WaterLevelLabel from './scenes/WaterLevelLabel.vue'
 import DucaoScene from './scenes/DucaoScene.vue'
+import Map3DScene from './scenes/Map3DScene.vue'
 export * from './config/sceneConfig'
+export * from './utils/geoCoord'
 
-export { Scene3D, WaterLevelLabel, DucaoScene }
+export { Scene3D, WaterLevelLabel, DucaoScene, Map3DScene }
 
 export type { WaterLevelLabelConfig, SceneType } from './config/sceneConfig'

+ 73 - 0
src/materials/rimFlow.ts

@@ -0,0 +1,73 @@
+import * as THREE from 'three'
+
+/**
+ * 创建瀑布流光材质
+ * - 结合纹理沿 Y 轴流动 + Fresnel 边缘光
+ * - 用于 "di" 部件的边缘流光效果
+ */
+export function createRimFlowMaterial(): THREE.ShaderMaterial {
+  const textureLoader = new THREE.TextureLoader()
+  const pubuTex = textureLoader.load('/assets/pubu.jpeg')
+  pubuTex.wrapS = THREE.RepeatWrapping
+  pubuTex.wrapT = THREE.RepeatWrapping
+  pubuTex.repeat.set(1, 3)
+
+  return new THREE.ShaderMaterial({
+    uniforms: {
+      uTime: { value: 0 },
+      uColor: { value: new THREE.Color(0x00ccff) },
+      uIntensity: { value: 1.0 },
+      uSpeed: { value: 0.08 },
+      uTexture: { value: pubuTex },
+    },
+    vertexShader: `
+      varying vec3 vNormal;
+      varying vec3 vViewDir;
+      varying vec3 vWorldPos;
+      varying vec2 vUv;
+      void main() {
+        vec4 worldPos = modelMatrix * vec4(position, 1.0);
+        vWorldPos = worldPos.xyz;
+        vNormal = normalize(normalMatrix * normal);
+        vViewDir = normalize(cameraPosition - worldPos.xyz);
+        vUv = uv;
+        gl_Position = projectionMatrix * viewMatrix * worldPos;
+      }
+    `,
+    fragmentShader: `
+      uniform vec3 uColor;
+      uniform float uTime;
+      uniform float uIntensity;
+      uniform float uSpeed;
+      uniform sampler2D uTexture;
+
+      varying vec3 vNormal;
+      varying vec3 vViewDir;
+      varying vec2 vUv;
+      varying vec3 vWorldPos;
+
+      void main() {
+        // Fresnel 边缘检测
+        float rim = 1.0 - max(dot(vNormal, vViewDir), 0.0);
+        rim = pow(rim, 2.5);
+
+        // UV 沿 Y 轴流动(瀑布下落)
+        vec2 flowUv = vUv;
+        flowUv.y += uTime * uSpeed;
+
+        // 采样贴图
+        vec4 texColor = texture2D(uTexture, flowUv);
+        float flow = texColor.r;
+
+        // 不透明,流动区域亮蓝色
+        vec3 baseColor = vec3(0.02, 0.05, 0.12);
+        vec3 glowColor = uColor;
+        vec3 finalColor = mix(baseColor, glowColor, flow * 0.8);
+
+        gl_FragColor = vec4(finalColor, 1.0);
+      }
+    `,
+    transparent: false,
+    side: THREE.DoubleSide,
+  })
+}

+ 66 - 0
src/materials/rimFlowMaterial.ts

@@ -0,0 +1,66 @@
+import * as THREE from 'three'
+
+const vertexShader = `
+varying vec3 vNormal;
+varying vec3 vViewDir;
+varying vec3 vWorldPos;
+varying vec2 vUv;
+void main() {
+  vec4 worldPos = modelMatrix * vec4(position, 1.0);
+  vWorldPos = worldPos.xyz;
+  vNormal = normalize(normalMatrix * normal);
+  vViewDir = normalize(cameraPosition - worldPos.xyz);
+  vUv = uv;
+  gl_Position = projectionMatrix * viewMatrix * worldPos;
+}
+`
+
+const fragmentShader = `
+uniform vec3 uColor;
+uniform float uTime;
+uniform float uIntensity;
+uniform float uSpeed;
+uniform sampler2D uTexture;
+
+varying vec3 vNormal;
+varying vec3 vViewDir;
+varying vec2 vUv;
+varying vec3 vWorldPos;
+
+void main() {
+  // Fresnel 边缘检测
+  float rim = 1.0 - max(dot(vNormal, vViewDir), 0.0);
+  rim = pow(rim, 2.5);
+
+  // UV 沿 Y 轴流动(瀑布下落)
+  vec2 flowUv = vUv;
+  flowUv.y += uTime * uSpeed;
+
+  // 采样贴图
+  vec4 texColor = texture2D(uTexture, flowUv);
+  float flow = texColor.r;
+
+  // 不透明,流动区域亮蓝色
+  vec3 baseColor = vec3(0.02, 0.05, 0.12);
+  vec3 glowColor = uColor;
+  vec3 finalColor = mix(baseColor, glowColor, flow * 0.8);
+
+  gl_FragColor = vec4(finalColor, 1.0);
+}
+`
+
+interface RimFlowMaterialOptions {
+  color?: THREE.Color
+  intensity?: number
+  speed?: number
+  textureRepeat?: THREE.Vector2
+}
+
+function createRimFlowMaterial(options: RimFlowMaterialOptions = {}): THREE.ShaderMaterial {
+  const textureLoader = new THREE.TextureLoader()
+  const pubuTex = textureLoader.load('/assets/pubu.jpeg')
+  pubuTex.wrapS = THREE.RepeatWrapping
+  pubuTex.wrapT = THREE.RepeatWrapping
+  pubuTex.repeat.copy(options.textureRepeat ?? new THREE.Vector2(1, 3))
+
+  return new THREE.Shader

+ 75 - 0
src/materials/techFloor.ts

@@ -0,0 +1,75 @@
+import * as THREE from 'three'
+
+/**
+ * 创建科技背景地面材质
+ * - 纹理平铺 + 沿 X 方向流动渐变遮罩
+ * - 遮罩带有噪声扰动,打破规律感
+ * - 用于场景底部的科技感网格地面
+ */
+export function createTechFloorMaterial(): THREE.ShaderMaterial {
+  const textureLoader = new THREE.TextureLoader()
+  const tex = textureLoader.load('/assets/xiantiao.png')
+  tex.wrapS = THREE.RepeatWrapping
+  tex.wrapT = THREE.RepeatWrapping
+  tex.repeat.set(2000, 2000)
+  tex.anisotropy = 4
+
+  return new THREE.ShaderMaterial({
+    uniforms: {
+      uTexture: { value: tex },
+      uColor: { value: new THREE.Color(0x1a3a6a) },
+      uTime: { value: 0 },
+    },
+    vertexShader: `
+      varying vec2 vUv;
+      void main() {
+        vUv = uv;
+        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+      }
+    `,
+    fragmentShader: `
+      uniform sampler2D uTexture;
+      uniform vec3 uColor;
+      uniform float uTime;
+      varying vec2 vUv;
+
+      // 伪随机 2D 噪声
+      float hash(vec2 p) {
+        return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
+      }
+      float noise2d(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);
+      }
+
+      void main() {
+        vec4 texColor = texture2D(uTexture, vUv);
+        float mask = texColor.a;
+        vec3 finalColor = uColor * mask;
+
+        // 用噪声轻微扰动 UV,打破规律性
+        float n = noise2d(vUv * 4.0 + uTime * 0.005);
+        float perturb = (n - 0.5) * 0.15;
+        vec2 uvPerturbed = vUv + perturb;
+
+        // 流动渐变遮罩:沿 X 方向滚动(使用扰动后的 UV)
+        float grad = fract(uvPerturbed.x * 4.0 - uTime * 0.08 + perturb * 0.3);
+        float flow = smoothstep(0.0, 0.3, grad) * smoothstep(1.0, 0.7, grad);
+        flow = flow * 0.8 + 0.2;
+
+        finalColor *= flow;
+
+        gl_FragColor = vec4(finalColor, mask * 0.5 * flow);
+      }
+    `,
+    transparent: true,
+    depthWrite: false,
+    side: THREE.DoubleSide,
+  })
+}

+ 340 - 283
src/scenes/Map3DScene.vue

@@ -1,13 +1,15 @@
-<script setup lang="ts">
-import { ref, onMounted, onUnmounted, reactive, watch } from 'vue'
+<script setup lang="ts">
+import { onMounted, onUnmounted, ref } from 'vue'
 import * as THREE from 'three'
-import * as tt from 'three-tile'
-import * as plugin from 'three-tile/plugin'
-import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
-import iconUrl from '../assets/icon/shuiliang.png'
-
-const GLB_URL = '/assets/xinjiangdiban.glb'
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
+import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
+import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
+import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'
+import { latLonToLocalENU, latLonToECEF, latLonToScenePosition, ecefToLatLon } from '../utils/geoCoord'
+import { createRimFlowMaterial } from '../materials/rimFlow'
+import { createTechFloorMaterial } from '../materials/techFloor'
+import xinjiangdibanGLB from '../assets/xinjiangdiban.glb'
 
 const containerRef = ref<HTMLDivElement>()
 
@@ -15,337 +17,392 @@ let scene: THREE.Scene
 let camera: THREE.PerspectiveCamera
 let renderer: THREE.WebGLRenderer
 let controls: OrbitControls
-let map: tt.TileMap
+let composer: EffectComposer
+let animationId: number
+
 let modelGroup: THREE.Group | null = null
-let animId = 0
 
-const showTransformPanel = ref(false)
+/** 存储标记点位置,供聚焦按钮使用 */
+const markerPositions: Record<string, THREE.Vector3> = {}
+const focusTarget = ref('原点')
 
-const transform = reactive({
-  positionX: 0, positionY: 0, positionZ: 0,
-  rotationX: 0, rotationY: 5, rotationZ: 0,
-  scaleX: 1, scaleY: 1, scaleZ: 1,
-})
+// 缓动函数
+function easeInOutCubic(t: number): number {
+  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
+}
 
-watch(transform, () => {
-  if (!modelGroup) return
-  const t = transform
-  modelGroup.position.set(t.positionX, t.positionY, t.positionZ)
-  modelGroup.rotation.set(
-    THREE.MathUtils.degToRad(t.rotationX),
-    THREE.MathUtils.degToRad(t.rotationY),
-    THREE.MathUtils.degToRad(t.rotationZ)
-  )
-  modelGroup.scale.set(t.scaleX, t.scaleY, t.scaleZ)
-}, { deep: true })
+/** 平滑飞向目标点 */
+function flyTo(targetName: string, duration = 1000) {
+  const pos = markerPositions[targetName]
+  if (!pos) return
+
+  focusTarget.value = targetName
+
+  const startPos = camera.position.clone()
+  const startTarget = controls.target.clone()
+  const endPos = new THREE.Vector3(pos.x, pos.y + 50000, pos.z + 80000)
+  const endTarget = pos.clone()
+  endTarget.y += 20000
+
+  const startTime = performance.now()
+
+  function animateFly(time: number) {
+    const elapsed = time - startTime
+    const t = Math.min(elapsed / duration, 1)
+    const e = easeInOutCubic(t)
+
+    camera.position.lerpVectors(startPos, endPos, e)
+    controls.target.lerpVectors(startTarget, endTarget, e)
+    controls.update()
+
+    if (t < 1) {
+      requestAnimationFrame(animateFly)
+    }
+  }
+
+  requestAnimationFrame(animateFly)
+}
+
+// ---------- 测试用经纬度点 ----------
+// 以指定坐标为原点 (84.79211°E, 37.52110°N)
+const ORIGIN_LAT = 37.52110
+const ORIGIN_LON = 84.79211
+
+const testPoints = [
+  { lat: 37.52110, lon: 84.79211, label: '原点' },
+  { lat: 37.53110, lon: 84.81211, label: '东北方向' },
+  { lat: 37.51110, lon: 84.77211, label: '西南方向' },
+  { lat: 37.53110, lon: 84.79211, label: '正北方向' },
+  { lat: 37.51110, lon: 84.81211, label: '东南方向' },
+]
 
 function initScene() {
-  const container = containerRef.value!
-  const w = container.clientWidth
-  const h = container.clientHeight
+  if (!containerRef.value) return
 
+  // Scene
   scene = new THREE.Scene()
-  scene.background = new THREE.Color(0x87ceeb)
-
-  camera = new THREE.PerspectiveCamera(60, w / h, 0.1, 5000000)
+  scene.background = new THREE.Color(0x111122)
+
+  // Camera
+  camera = new THREE.PerspectiveCamera(
+    60,
+    containerRef.value.clientWidth / containerRef.value.clientHeight,
+    1,
+    500000,
+  )
+  camera.position.set(100000, 80000, 150000)
 
-  renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true })
-  renderer.setSize(w, h)
+  // Renderer
+  renderer = new THREE.WebGLRenderer({
+    antialias: true,
+    logarithmicDepthBuffer: true,
+    powerPreference: 'high-performance',
+  })
+  renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
   renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
-  container.appendChild(renderer.domElement)
+  renderer.toneMapping = THREE.ACESFilmicToneMapping
+  renderer.toneMappingExposure = 1.2
+  containerRef.value.appendChild(renderer.domElement)
 
+  // Controls
   controls = new OrbitControls(camera, renderer.domElement)
   controls.enableDamping = true
-  controls.dampingFactor = 0.08
-  controls.minDistance = 10
-  controls.maxDistance = 3000000
-
-  scene.add(new THREE.AmbientLight(0xffffff, 0.6))
-  const dl = new THREE.DirectionalLight(0xffffff, 1.5)
-  dl.position.set(100, 100, 50)
-  scene.add(dl)
-  const dl2 = new THREE.DirectionalLight(0x88ccff, 0.4)
-  dl2.position.set(-50, 80, -30)
-  scene.add(dl2)
-
-  map = tt.TileMap.create({
-    imgSource: new plugin.ArcGisSource(),
-    demSource: new plugin.ArcGisDemSource(),
-    lon0: 90,
-    bounds: [84.0, 37.0, 86.0, 38.0],
-    minLevel: 8,
-  })
-  map.rotateX(-Math.PI / 2)
-  scene.add(map)
+  controls.dampingFactor = 0.1
+  controls.target.set(0, 0, 0)
+
+  // Effect Composer (Bloom)
+  composer = new EffectComposer(renderer)
+  const renderPass = new RenderPass(scene, camera)
+  composer.addPass(renderPass)
+  const bloomPass = new UnrealBloomPass(
+    new THREE.Vector2(containerRef.value.clientWidth, containerRef.value.clientHeight),
+    0.6,   // strength
+    0.4,   // radius
+    0.85,  // threshold
+  )
+  composer.addPass(bloomPass)
 
-  loadModel()
-  addIcon()
+  // Helpers
+  scene.add(new THREE.GridHelper(2000, 20))
+  scene.add(new THREE.AxesHelper(500))
+
+  // Lighting
+  const ambientLight = new THREE.AmbientLight(0x333333)
+  scene.add(ambientLight)
+
+  const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5)
+  directionalLight.position.set(5000, 10000, 5000)
+  scene.add(directionalLight)
+
+  // ---------- 科技背景地面 ----------
+  createTechFloor()
+
+  // ---------- 加载底板模型 ----------
+  loadBaseModel()
+
+  // ---------- 展示 geodesy 坐标转换结果 ----------
+  addGeodesyTestMarkers()
+
+  // Resize
+  window.addEventListener('resize', onResize)
+
+  // Start loop
   animate()
 }
 
-async function loadModel() {
-  try {
-    const loader = new GLTFLoader()
-    const gltf = await loader.loadAsync(GLB_URL)
-    const model = gltf.scene
-
-    // 模型 EPSG:4326,顶点单位是度
-    // 用 geo2map 转地图本地坐标,直接放到地图上
-    const box = new THREE.Box3().setFromObject(model)
-    const centerDeg = box.getCenter(new THREE.Vector3())
-
-    // 模型中心经纬度 → 地图本地坐标(map 已 rotateX,坐标在 XY 平面)
-    const mapPos = map.geo2map(new THREE.Vector3(centerDeg.x, centerDeg.y, 0))
-
-    // 模型居中后放入 group
-    model.position.sub(centerDeg)
-    const group = new THREE.Group()
-    group.add(model)
-    group.position.set(mapPos.x, mapPos.y, 0)
-    group.rotation.y = THREE.MathUtils.degToRad(5)
-    // 添加到地图组,跟随地图旋转
-    map.add(group)
-    modelGroup = group
-
-    // 辅助红色球体贴地
-    const marker = new THREE.Mesh(
-      new THREE.SphereGeometry(50, 16, 16),
-      new THREE.MeshBasicMaterial({ color: 0xff0000 })
-    )
-    marker.position.set(mapPos.x, mapPos.y, 0)
-    map.add(marker)
-
-    // 同步 transform
-    transform.positionX = mapPos.x
-    transform.positionY = mapPos.y
-    transform.positionZ = 0
-    transform.rotationY = 5
-
-    // 相机对准
-    const sizeDeg = box.getSize(new THREE.Vector3())
-    const latRad = THREE.MathUtils.degToRad(centerDeg.y)
-    const metersPerDeg = 111320 * Math.cos(latRad)
-    const maxMeters = Math.max(sizeDeg.x, sizeDeg.y) * metersPerDeg
-    const dist = maxMeters * 2 || 5000
-
-    controls.target.set(mapPos.x, mapPos.y, 0)
-    camera.position.set(mapPos.x + dist * 0.3, mapPos.y + dist * 0.5, dist)
-    controls.update()
+/** 加载 xinjiangdiban.glb */
+function loadBaseModel() {
+  const loader = new GLTFLoader()
+  loader.load(xinjiangdibanGLB, (gltf) => {
+    modelGroup = gltf.scene
+
+    modelGroup.name = 'xinjiangdiban'
+
+    modelGroup.traverse((child) => {
+      if ((child as THREE.Mesh).isMesh) {
+        const mesh = child as THREE.Mesh
+        mesh.castShadow = true
+        mesh.receiveShadow = true
+        mesh.frustumCulled = false
+
+        // 给名为 "di" 的部件替换流光材质
+        if (mesh.name === 'di') {
+          console.log('[流光] 给 "di" 部件应用流光材质')
+          const flowMat = createRimFlowMaterial()
+          mesh.material = flowMat
+          mesh.renderOrder = 1
+          flowMaterials.push(flowMat)
+        }
+
+        // 给名为 "bian" 的部件应用发光材质
+        if (mesh.name === 'bian') {
+          console.log('[发光] 给 "bian" 部件应用发光材质')
+          mesh.material = new THREE.MeshStandardMaterial({
+            color: 0x004488,
+            emissive: 0x0088ff,
+            emissiveIntensity: 1.5,
+            metalness: 0.3,
+            roughness: 0.4,
+          })
+        }
+      }
+    })
+
+    scene.add(modelGroup)
+    console.log('[加载] xinjiangdiban.glb 加载完成')
+  })
+}
 
-    console.log(`[Map3D] 模型加载完成`)
-    console.log(`[Map3D] 中心经纬度: (${centerDeg.x.toFixed(5)}, ${centerDeg.y.toFixed(5)})`)
-    console.log(`[Map3D] 地图坐标: (${mapPos.x.toFixed(1)}, ${mapPos.y.toFixed(1)})`)
-    console.log(`[Map3D] 尺寸(度): ${sizeDeg.x.toFixed(5)} x ${sizeDeg.y.toFixed(5)}`)
-  } catch (err) {
-    console.error('[Map3D] 模型加载失败:', err)
+// ==================== 科技背景地面 ====================
+function createTechFloor() {
+  const floorY = -200
+  const mat = createTechFloorMaterial()
+
+  const mesh = new THREE.Mesh(new THREE.PlaneGeometry(800000, 800000), mat)
+  mesh.rotation.x = -Math.PI / 2
+  mesh.position.y = floorY
+  scene.add(mesh)
+
+  // 每帧更新 uTime
+  const origRender = composer.render.bind(composer)
+  const renderWrapper = () => {
+    mat.uniforms.uTime.value = performance.now() * 0.001
+    origRender()
   }
+  composer.render = renderWrapper
 }
 
-function addIcon() {
-  const LON = 84.232272
-  const LAT = 37.613076
+let flowMaterials: THREE.ShaderMaterial[] = []
 
-  const texture = new THREE.TextureLoader().load(iconUrl)
-  const material = new THREE.SpriteMaterial({
-    map: texture,
-    sizeAttenuation: false,
-    transparent: true,
-  })
-  const icon = new THREE.Sprite(material)
-  icon.renderOrder = 999
-  icon.center.set(0.5, 0)
-  icon.scale.setScalar(100)
-
-  const pos = map.geo2map(new THREE.Vector3(LON, LAT, 0))
-  icon.position.set(pos.x, pos.y, 0)
-  map.add(icon)
+function animate() {
+  animationId = requestAnimationFrame(animate)
+
+  // 更新流光材质的 uTime
+  if (flowMaterials.length > 0) {
+    const time = performance.now() * 0.001
+    flowMaterials.forEach((mat) => {
+      mat.uniforms.uTime.value = time
+    })
+  }
+
+  controls.update()
+  composer.render()
 }
 
-function animate() {
-  animId = requestAnimationFrame(animate)
-  map?.update(camera)
-  controls?.update()
-  renderer?.render(scene, camera)
+/**
+ * 使用 geodesy 将经纬度转为局部 ENU 坐标并标记到场景中
+ */
+function addGeodesyTestMarkers() {
+  // 输出 ECEF 坐标示例
+  const originECEF = latLonToECEF(ORIGIN_LAT, ORIGIN_LON)
+  console.log(`[geodesy] 原点 ECEF: (${originECEF.x.toFixed(2)}, ${originECEF.y.toFixed(2)}, ${originECEF.z.toFixed(2)})`)
+
+  // 验证双向转换
+  const backToGeo = ecefToLatLon(originECEF)
+  console.log(`[geodesy] ECEF→经纬度: (${backToGeo.lat.toFixed(6)}, ${backToGeo.lon.toFixed(6)}, height=${backToGeo.height.toFixed(2)})`)
+
+  // 在场景中标记测试点
+  testPoints.forEach((pt) => {
+    // 使用 ENU 局部坐标 (适合局部场景)
+    const localPos = latLonToLocalENU(pt.lat, pt.lon, 0, ORIGIN_LAT, ORIGIN_LON)
+
+    console.log(`[geodesy] ${pt.label} (${pt.lat}, ${pt.lon}) → 局部坐标: (${localPos.x.toFixed(2)}, ${localPos.y.toFixed(2)}, ${localPos.z.toFixed(2)})`)
+
+    // 标记点 - 红色小球
+    const sphere = new THREE.Mesh(
+      new THREE.SphereGeometry(50, 16, 16),
+      new THREE.MeshStandardMaterial({ color: 0xff3333 }),
+    )
+    sphere.position.copy(localPos)
+    scene.add(sphere)
+    markerPositions[pt.label] = localPos.clone()
+
+    // 标签 - 使用 Sprite
+    const canvas = document.createElement('canvas')
+    canvas.width = 256
+    canvas.height = 64
+    const ctx = canvas.getContext('2d')!
+    ctx.fillStyle = 'rgba(0,0,0,0.6)'
+    ctx.roundRect(0, 0, 256, 64, 8)
+    ctx.fill()
+    ctx.fillStyle = '#ffffff'
+    ctx.font = 'bold 20px sans-serif'
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'middle'
+    ctx.fillText(pt.label, 128, 32)
+
+    const texture = new THREE.CanvasTexture(canvas)
+    const spriteMat = new THREE.SpriteMaterial({ map: texture, depthTest: false })
+    const sprite = new THREE.Sprite(spriteMat)
+    sprite.position.copy(localPos)
+    sprite.position.y += 120
+    sprite.scale.set(400, 100, 1)
+    scene.add(sprite)
+  })
+
+  // ---------- 测试指定坐标 (84.233001, 37.595023) ----------
+  const testLat = 37.595023
+  const testLon = 84.233001
+  const testPos = latLonToLocalENU(testLat, testLon, 0, ORIGIN_LAT, ORIGIN_LON)
+  console.log(`[标记] (${testLat}, ${testLon}) → 局部坐标: (${testPos.x.toFixed(2)}, ${testPos.y.toFixed(2)}, ${testPos.z.toFixed(2)})`)
+
+  const sphere = new THREE.Mesh(
+    new THREE.SphereGeometry(50, 16, 16),
+    new THREE.MeshStandardMaterial({ color: 0xff3333 }),
+  )
+  sphere.position.copy(testPos)
+  scene.add(sphere)
+  markerPositions['38团'] = testPos.clone()
+
+  const canvas = document.createElement('canvas')
+  canvas.width = 256
+  canvas.height = 64
+  const ctx = canvas.getContext('2d')!
+  ctx.fillStyle = 'rgba(0,0,0,0.6)'
+  ctx.roundRect(0, 0, 256, 64, 8)
+  ctx.fill()
+  ctx.fillStyle = '#ffffff'
+  ctx.font = 'bold 20px sans-serif'
+  ctx.textAlign = 'center'
+  ctx.textBaseline = 'middle'
+  ctx.fillText('38团', 128, 32)
+
+  const texture = new THREE.CanvasTexture(canvas)
+  const spriteMat = new THREE.SpriteMaterial({ map: texture, depthTest: false })
+  const sprite = new THREE.Sprite(spriteMat)
+  sprite.position.copy(testPos)
+  sprite.position.y += 120
+  sprite.scale.set(400, 100, 1)
+  scene.add(sprite)
 }
 
 function onResize() {
   if (!containerRef.value) return
-  const w = containerRef.value.clientWidth
-  const h = containerRef.value.clientHeight
-  camera.aspect = w / h
+  const width = containerRef.value.clientWidth
+  const height = containerRef.value.clientHeight
+  camera.aspect = width / height
   camera.updateProjectionMatrix()
-  renderer.setSize(w, h)
+  renderer.setSize(width, height)
+}
+
+function disposeScene() {
+  window.removeEventListener('resize', onResize)
+  cancelAnimationFrame(animationId)
+  controls.dispose()
+  if (composer) composer.dispose()
+  renderer.dispose()
+  if (containerRef.value && renderer.domElement.parentElement) {
+    containerRef.value.removeChild(renderer.domElement)
+  }
 }
 
 onMounted(() => {
   initScene()
-  window.addEventListener('resize', onResize)
 })
 
 onUnmounted(() => {
-  window.removeEventListener('resize', onResize)
-  cancelAnimationFrame(animId)
-  renderer?.dispose()
+  disposeScene()
 })
 </script>
 
 <template>
-  <div ref="containerRef" class="map-container" />
-  <div class="toolbar">
-    <button class="toolbar-btn" :class="{ active: showTransformPanel }" @click="showTransformPanel = !showTransformPanel">
-      模型变换
-    </button>
-  </div>
-  <div v-if="showTransformPanel" class="panel transform-panel">
-    <div class="panel-header">
-      <span class="panel-title">模型变换</span>
-      <button class="toggle-btn" @click="showTransformPanel = false">×</button>
-    </div>
-    <div class="section">
-      <div class="section-label">位置</div>
-      <div class="row"><span class="label">X</span><input v-model.number="transform.positionX" type="range" class="slider" :min="transform.positionX - 500" :max="transform.positionX + 500" step="1" /><span class="val">{{ transform.positionX.toFixed(0) }}</span></div>
-      <div class="row"><span class="label">Y</span><input v-model.number="transform.positionY" type="range" class="slider" :min="transform.positionY - 500" :max="transform.positionY + 500" step="1" /><span class="val">{{ transform.positionY.toFixed(0) }}</span></div>
-      <div class="row"><span class="label">Z</span><input v-model.number="transform.positionZ" type="range" class="slider" :min="transform.positionZ - 500" :max="transform.positionZ + 500" step="1" /><span class="val">{{ transform.positionZ.toFixed(0) }}</span></div>
-    </div>
-    <div class="section">
-      <div class="section-label">旋转 (度)</div>
-      <div class="row"><span class="label">X</span><input v-model.number="transform.rotationX" type="range" class="slider" min="-180" max="180" step="1" /><span class="val">{{ transform.rotationX }}</span></div>
-      <div class="row"><span class="label">Y</span><input v-model.number="transform.rotationY" type="range" class="slider" min="-180" max="180" step="1" /><span class="val">{{ transform.rotationY }}</span></div>
-      <div class="row"><span class="label">Z</span><input v-model.number="transform.rotationZ" type="range" class="slider" min="-180" max="180" step="1" /><span class="val">{{ transform.rotationZ }}</span></div>
-    </div>
-    <div class="section">
-      <div class="section-label">缩放</div>
-      <div class="row"><span class="label">X</span><input v-model.number="transform.scaleX" type="range" class="slider" min="0.1" max="10" step="0.1" /><span class="val">{{ transform.scaleX.toFixed(1) }}</span></div>
-      <div class="row"><span class="label">Y</span><input v-model.number="transform.scaleY" type="range" class="slider" min="0.1" max="10" step="0.1" /><span class="val">{{ transform.scaleY.toFixed(1) }}</span></div>
-      <div class="row"><span class="label">Z</span><input v-model.number="transform.scaleZ" type="range" class="slider" min="0.1" max="10" step="0.1" /><span class="val">{{ transform.scaleZ.toFixed(1) }}</span></div>
+  <div ref="containerRef" class="scene-container">
+    <div class="toolbar">
+      <button
+        v-for="pt in testPoints"
+        :key="pt.label"
+        class="btn"
+        :class="{ active: focusTarget === pt.label }"
+        @click="flyTo(pt.label)"
+      >
+        {{ pt.label }}
+      </button>
+      <button
+        class="btn"
+        :class="{ active: focusTarget === '38团' }"
+        @click="flyTo('38团')"
+      >
+        38团
+      </button>
     </div>
+
   </div>
 </template>
 
 <style scoped>
-.map-container {
-  width: 100vw;
-  height: 100vh;
+.scene-container {
+  width: 100%;
+  height: 100%;
   overflow: hidden;
+  position: relative;
 }
-
 .toolbar {
-  position: fixed;
-  top: 20px;
-  right: 20px;
+  position: absolute;
+  top: 16px;
+  left: 16px;
+  z-index: 10;
   display: flex;
-  flex-direction: column;
+  flex-wrap: wrap;
   gap: 8px;
-  z-index: 1001;
 }
-
-.toolbar-btn {
-  padding: 10px 16px;
-  border: none;
-  border-radius: 8px;
-  background: rgba(0, 0, 0, 0.85);
-  color: #888;
-  font-size: 13px;
-  font-weight: bold;
+.btn {
+  padding: 6px 14px;
+  border: 1px solid rgba(255,255,255,0.3);
+  border-radius: 6px;
+  background: rgba(0,0,0,0.5);
+  color: #ccc;
   cursor: pointer;
+  font-size: 13px;
+  backdrop-filter: blur(4px);
   transition: all 0.2s;
-  min-width: 70px;
 }
-
-.toolbar-btn:hover {
-  background: rgba(30, 30, 30, 0.9);
+.btn:hover {
+  background: rgba(255,255,255,0.15);
   color: #fff;
 }
-
-.toolbar-btn.active {
-  color: #4fc3f7;
-  box-shadow: 0 0 10px rgba(79, 195, 247, 0.3);
-}
-
-.panel {
-  position: fixed;
-  top: 20px;
-  left: 20px;
-  background: rgba(0, 0, 0, 0.85);
-  color: white;
-  padding: 15px;
-  border-radius: 8px;
-  font-family: 'Courier New', monospace;
-  z-index: 1000;
-  max-height: calc(100vh - 40px);
-  overflow-y: auto;
-  min-width: 280px;
-}
-
-.panel-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 12px;
-  padding-bottom: 8px;
-  border-bottom: 1px solid rgba(255, 255, 255, 0.3);
-}
-
-.panel-title {
-  font-weight: bold;
-  font-size: 14px;
-  color: #4fc3f7;
-}
-
-.toggle-btn {
-  width: 22px;
-  height: 22px;
-  border: none;
-  border-radius: 4px;
-  background: rgba(255, 255, 255, 0.15);
-  color: white;
-  font-size: 14px;
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.toggle-btn:hover {
-  background: rgba(255, 255, 255, 0.3);
-}
-
-.section {
-  margin-bottom: 12px;
-}
-
-.section-label {
-  color: #81c784;
-  font-size: 11px;
-  margin-bottom: 6px;
-  font-weight: bold;
-}
-
-.row {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  margin: 4px 0;
-}
-
-.label {
-  color: #4fc3f7;
-  width: 16px;
-  font-size: 12px;
-  font-weight: bold;
-}
-
-.slider {
-  flex: 1;
-  height: 4px;
-  cursor: pointer;
-  accent-color: #4fc3f7;
-}
-
-.val {
+.btn.active {
+  background: #2196f3;
+  border-color: #2196f3;
   color: #fff;
-  width: 70px;
-  text-align: right;
-  font-size: 12px;
 }
 </style>

+ 183 - 0
src/utils/coordinateConverter.ts

@@ -0,0 +1,183 @@
+import * as THREE from 'three'
+
+/**
+ * 坐标系转换工具
+ * 处理不同3D软件导出的模型坐标系差异
+ */
+
+export type CoordinateSystem = 
+  | 'y-up'      // Three.js 默认: Y轴朝上,Z轴向前
+  | 'z-up'      // Blender/3ds Max 默认: Z轴朝上,Y轴向前
+  | 'z-up-forward-x' // 某些CAD软件: Z轴朝上,X轴向前
+
+/**
+ * 转换模型坐标系
+ * @param model - Three.js 3D对象
+ * @param from - 源坐标系
+ * @param to - 目标坐标系 (默认为 Three.js 的 y-up)
+ * @param rotationOption - 旋转选项 (默认: standard, 可选: flipped, 180)
+ * @returns 转换后的模型
+ */
+export function convertCoordinateSystem(
+  model: THREE.Object3D,
+  from: CoordinateSystem,
+  to: CoordinateSystem = 'y-up',
+  rotationOption: 'standard' | 'flipped' | '180' = 'standard'
+): THREE.Object3D {
+  if (from === to) return model
+
+  // 定义坐标系转换矩阵
+  const conversionMatrices: Record<string, THREE.Matrix4> = {
+    // Z-up (Blender) -> Y-up (Three.js)
+    'z-up_to_y-up_standard': new THREE.Matrix4().makeRotationX(-Math.PI / 2),
+    'z-up_to_y-up_flipped': new THREE.Matrix4().makeRotationX(Math.PI / 2),
+    'z-up_to_y-up_180': new THREE.Matrix4().makeRotationX(Math.PI),
+    
+    // Y-up (Three.js) -> Z-up (Blender)
+    'y-up_to_z-up_standard': new THREE.Matrix4().makeRotationX(Math.PI / 2),
+    'y-up_to_z-up_flipped': new THREE.Matrix4().makeRotationX(-Math.PI / 2),
+    
+    // Z-up-forward-x -> Y-up
+    'z-up-forward-x_to_y-up_standard': new THREE.Matrix4()
+      .makeRotationX(-Math.PI / 2)
+      .multiply(new THREE.Matrix4().makeRotationZ(Math.PI / 2)),
+  }
+
+  const key = `${from}_to_${to}_${rotationOption}`
+  const matrix = conversionMatrices[key]
+
+  if (!matrix) {
+    console.warn(`不支持的坐标系转换: ${from} -> ${to} (${rotationOption})`)
+    return model
+  }
+
+  // 应用转换矩阵
+  model.applyMatrix4(matrix)
+  return model
+}
+
+/**
+ * 自动检测并转换模型坐标系
+ * @param model - Three.js 3D对象
+ * @returns 转换后的模型
+ */
+export function autoConvertModelCoordinates(model: THREE.Object3D): THREE.Object3D {
+  // 计算模型的包围盒
+  const box = new THREE.Box3().setFromObject(model)
+  const size = box.getSize(new THREE.Vector3())
+  const center = box.getCenter(new THREE.Vector3())
+
+  console.log(`[坐标系转换] 模型尺寸: (${size.x.toFixed(2)}, ${size.y.toFixed(2)}, ${size.z.toFixed(2)})`)
+  console.log(`[坐标系转换] 包围盒中心: (${center.x.toFixed(2)}, ${center.y.toFixed(2)}, ${center.z.toFixed(2)})`)
+
+  // 判断坐标系:如果 Z 轴尺寸最大且 Y 轴较小,可能是 Z-up 坐标系
+  const isZUp = size.z > size.y && size.z > size.x
+  
+  if (isZUp) {
+    console.log(`[坐标系转换] 检测到 Z-up 坐标系,转换为 Y-up`)
+    return convertCoordinateSystem(model, 'z-up', 'y-up')
+  } else {
+    console.log(`[坐标系转换] 检测到 Y-up 坐标系,无需转换`)
+    return model
+  }
+}
+
+/**
+ * 创建坐标系参考轴
+ * @param size - 轴的长度
+ * @returns 坐标系参考轴对象
+ */
+export function createCoordinateAxes(size: number = 10): THREE.Group {
+  const group = new THREE.Group()
+  
+  // X轴 (红色)
+  const xAxis = new THREE.ArrowHelper(
+    new THREE.Vector3(1, 0, 0),
+    new THREE.Vector3(0, 0, 0),
+    size,
+    0xff0000,
+    size * 0.2,
+    size * 0.1
+  )
+  
+  // Y轴 (绿色)
+  const yAxis = new THREE.ArrowHelper(
+    new THREE.Vector3(0, 1, 0),
+    new THREE.Vector3(0, 0, 0),
+    size,
+    0x00ff00,
+    size * 0.2,
+    size * 0.1
+  )
+  
+  // Z轴 (蓝色)
+  const zAxis = new THREE.ArrowHelper(
+    new THREE.Vector3(0, 0, 1),
+    new THREE.Vector3(0, 0, 0),
+    size,
+    0x0000ff,
+    size * 0.2,
+    size * 0.1
+  )
+  
+  group.add(xAxis)
+  group.add(yAxis)
+  group.add(zAxis)
+  
+  return group
+}
+
+/**
+ * 坐标系转换配置接口
+ */
+export interface CoordinateConversionConfig {
+  sourceSystem: CoordinateSystem
+  targetSystem?: CoordinateSystem
+  autoDetect?: boolean
+  showAxes?: boolean
+  axesSize?: number
+}
+
+/**
+ * 应用坐标系转换到模型组
+ */
+export function applyCoordinateConversion(
+  modelGroup: THREE.Group,
+  config: CoordinateConversionConfig
+): THREE.Group {
+  const {
+    sourceSystem,
+    targetSystem = 'y-up',
+    autoDetect = false,
+    showAxes = false,
+    axesSize = 10
+  } = config
+
+  let convertedModel: THREE.Object3D
+
+  if (autoDetect) {
+    convertedModel = autoConvertModelCoordinates(modelGroup)
+  } else {
+    convertedModel = convertCoordinateSystem(modelGroup, sourceSystem, targetSystem)
+  }
+
+  // 创建新的组来包含转换后的模型
+  const resultGroup = new THREE.Group()
+  
+  if (convertedModel instanceof THREE.Group) {
+    // 如果转换后还是组,直接添加所有子对象
+    convertedModel.children.forEach(child => {
+      resultGroup.add(child.clone())
+    })
+  } else {
+    resultGroup.add(convertedModel)
+  }
+
+  // 可选:添加坐标系参考轴
+  if (showAxes) {
+    const axes = createCoordinateAxes(axesSize)
+    resultGroup.add(axes)
+  }
+
+  return resultGroup
+}

+ 171 - 0
src/utils/coordinateDebug.ts

@@ -0,0 +1,171 @@
+import * as THREE from 'three'
+
+/**
+ * 坐标系调试工具
+ * 帮助确定模型的正确坐标系转换
+ */
+
+export interface CoordinateDebugInfo {
+  originalSize: THREE.Vector3
+  originalCenter: THREE.Vector3
+  dominantAxis: 'x' | 'y' | 'z'
+  isZUp: boolean
+  suggestedRotation: THREE.Vector3
+}
+
+/**
+ * 分析模型的坐标系特征
+ */
+export function analyzeModelCoordinates(model: THREE.Object3D): CoordinateDebugInfo {
+  const box = new THREE.Box3().setFromObject(model)
+  const size = box.getSize(new THREE.Vector3())
+  const center = box.getCenter(new THREE.Vector3())
+
+  // 确定主导轴(尺寸最大的轴)
+  const dominantAxis = size.x > size.y 
+    ? (size.x > size.z ? 'x' : 'z')
+    : (size.y > size.z ? 'y' : 'z')
+
+  // 判断是否为 Z-up 坐标系
+  const isZUp = size.z > size.y && size.z > size.x
+
+  // 建议的旋转角度
+  let suggestedRotation = new THREE.Vector3(0, 0, 0)
+  if (isZUp) {
+    suggestedRotation = new THREE.Vector3(Math.PI / 2, 0, 0)
+  }
+
+  return {
+    originalSize: size.clone(),
+    originalCenter: center.clone(),
+    dominantAxis,
+    isZUp,
+    suggestedRotation
+  }
+}
+
+/**
+ * 创建测试用的参考模型
+ */
+export function createTestModel(): THREE.Group {
+  const group = new THREE.Group()
+  
+  // 创建一个长方体来模拟模型
+  const geometry = new THREE.BoxGeometry(10, 5, 2)
+  const material = new THREE.MeshLambertMaterial({ 
+    color: 0xff0000,
+    transparent: true,
+    opacity: 0.8
+  })
+  const mesh = new THREE.Mesh(geometry, material)
+  
+  // 添加标签
+  const labelGeometry = new THREE.BoxGeometry(12, 0.5, 0.5)
+  const labelMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
+  const label = new THREE.Mesh(labelGeometry, labelMaterial)
+  label.position.y = 2.5
+  group.add(label)
+  
+  group.add(mesh)
+  return group
+}
+
+/**
+ * 应用不同的转换并比较结果
+ */
+export function testCoordinateConversions(model: THREE.Object3D): Record<string, THREE.Object3D> {
+  const results: Record<string, THREE.Object3D> = {}
+  
+  // 测试1: 绕X轴旋转 +90度
+  const test1 = model.clone()
+  test1.rotation.x = Math.PI / 2
+  results['rotateX_+90'] = test1
+  
+  // 测试2: 绕X轴旋转 -90度
+  const test2 = model.clone()
+  test2.rotation.x = -Math.PI / 2
+  results['rotateX_-90'] = test2
+  
+  // 测试3: 绕Y轴旋转 +90度
+  const test3 = model.clone()
+  test3.rotation.y = Math.PI / 2
+  results['rotateY_+90'] = test3
+  
+  // 测试4: 绕Z轴旋转 +90度
+  const test4 = model.clone()
+  test4.rotation.z = Math.PI / 2
+  results['rotateZ_+90'] = test4
+  
+  return results
+}
+
+/**
+ * 创建坐标系调试面板
+ */
+export function createDebugPanel(info: CoordinateDebugInfo): THREE.Group {
+  const panel = new THREE.Group()
+  
+  // 创建信息文本(使用简单的几何体表示)
+  const boxGeometry = new THREE.BoxGeometry(20, 10, 0.1)
+  const boxMaterial = new THREE.MeshBasicMaterial({ 
+    color: 0x333333,
+    transparent: true,
+    opacity: 0.8
+  })
+  const background = new THREE.Mesh(boxGeometry, boxMaterial)
+  panel.add(background)
+  
+  // 添加坐标轴指示
+  const axisSize = 5
+  const xAxis = new THREE.ArrowHelper(
+    new THREE.Vector3(1, 0, 0),
+    new THREE.Vector3(-8, 3, 0.1),
+    axisSize,
+    0xff0000,
+    1,
+    0.5
+  )
+  const yAxis = new THREE.ArrowHelper(
+    new THREE.Vector3(0, 1, 0),
+    new THREE.Vector3(-8, 0, 0.1),
+    axisSize,
+    0x00ff00,
+    1,
+    0.5
+  )
+  const zAxis = new THREE.ArrowHelper(
+    new THREE.Vector3(0, 0, 1),
+    new THREE.Vector3(-8, -3, 0.1),
+    axisSize,
+    0x0000ff,
+    1,
+    0.5
+  )
+  
+  panel.add(xAxis)
+  panel.add(yAxis)
+  panel.add(zAxis)
+  
+  return panel
+}
+
+/**
+ * 自动调整模型到正确方向
+ */
+export function autoAdjustModel(model: THREE.Object3D): THREE.Object3D {
+  const info = analyzeModelCoordinates(model)
+  
+  console.log(`[坐标调试] 模型分析结果:`, {
+    size: `(${info.originalSize.x.toFixed(2)}, ${info.originalSize.y.toFixed(2)}, ${info.originalSize.z.toFixed(2)})`,
+    dominantAxis: info.dominantAxis,
+    isZUp: info.isZUp,
+    suggestedRotation: `(${info.suggestedRotation.x.toFixed(2)}, ${info.suggestedRotation.y.toFixed(2)}, ${info.suggestedRotation.z.toFixed(2)})`
+  })
+  
+  if (info.isZUp) {
+    console.log(`[坐标调试] 应用Z-up到Y-up转换`)
+    model.rotation.x = info.suggestedRotation.x
+  }
+  
+  return model
+}

+ 97 - 0
src/utils/geoCoord.ts

@@ -0,0 +1,97 @@
+import * as THREE from 'three'
+import LatLon, { Cartesian } from 'geodesy/latlon-ellipsoidal.js'
+
+/**
+ * 经纬度 → ECEF 地心直角坐标 (WGS84)
+ * 使用 geodesy 库进行精确的椭球体计算
+ * @param lat 纬度 (度)
+ * @param lon 经度 (度)
+ * @param height 海拔高度 (米),默认 0
+ * @returns THREE.Vector3 (x, y, z),单位:米
+ */
+export function latLonToECEF(lat: number, lon: number, height = 0): THREE.Vector3 {
+  const point = new LatLon(lat, lon, height)
+  const cart = point.toCartesian()
+  return new THREE.Vector3(cart.x, cart.y, cart.z)
+}
+
+/**
+ * ECEF 直角坐标 → 经纬度 (WGS84)
+ * @param xyz THREE.Vector3 (x, y, z),单位:米
+ * @returns { lat, lon, height }
+ */
+export function ecefToLatLon(xyz: THREE.Vector3): { lat: number; lon: number; height: number } {
+  const cart = new Cartesian(xyz.x, xyz.y, xyz.z)
+  const point = cart.toLatLon()
+  return {
+    lat: point.lat,
+    lon: point.lon,
+    height: point.height,
+  }
+}
+
+/**
+ * 经纬度 → 局部 ENU 直角坐标 (相对于原点)
+ * 适用于 Three.js 局部场景,将大地坐标映射到以 origin 为原点的局部坐标系
+ * @param lat 纬度 (度)
+ * @param lon 经度 (度)
+ * @param height 海拔高度 (米)
+ * @param originLat 原点纬度 (度)
+ * @param originLon 原点经度 (度)
+ * @param originHeight 原点海拔高度 (米)
+ * @returns THREE.Vector3 局部坐标 (x: 东, y: 天, z: 北)
+ */
+export function latLonToLocalENU(
+  lat: number,
+  lon: number,
+  height: number,
+  originLat: number,
+  originLon: number,
+  originHeight = 0,
+): THREE.Vector3 {
+  // 1. 计算目标点和原点的 ECEF 坐标
+  const targetECEF = latLonToECEF(lat, lon, height)
+  const originECEF = latLonToECEF(originLat, originLon, originHeight)
+
+  // 2. 计算从原点到目标点的 ECEF 向量
+  const delta = new THREE.Vector3().copy(targetECEF).sub(originECEF)
+
+  // 3. 将 ECEF 向量旋转到 ENU 局部坐标系
+  // ECEF → ENU 旋转矩阵
+  const φ = THREE.MathUtils.degToRad(originLat)
+  const λ = THREE.MathUtils.degToRad(originLon)
+
+  const sinφ = Math.sin(φ)
+  const cosφ = Math.cos(φ)
+  const sinλ = Math.sin(λ)
+  const cosλ = Math.cos(λ)
+
+  // ENU = R * delta (ECEF)
+  // [E]   [ -sinλ         cosλ         0 ][dx]
+  // [N] = [ -sinφ*cosλ  -sinφ*sinλ   cosφ ][dy]
+  // [U]   [  cosφ*cosλ   cosφ*sinλ   sinφ ][dz]
+  const e = -sinλ * delta.x + cosλ * delta.y
+  const n = -sinφ * cosλ * delta.x - sinφ * sinλ * delta.y + cosφ * delta.z
+  const u = cosφ * cosλ * delta.x + cosφ * sinλ * delta.y + sinφ * delta.z
+
+  return new THREE.Vector3(e, u, -n)
+}
+
+/**
+ * 经纬度 → Three.js 场景坐标 (简化版)
+ * 直接使用 ECEF 坐标,可指定缩放因子以适配场景尺寸
+ * @param lat 纬度 (度)
+ * @param lon 经度 (度)
+ * @param height 海拔高度 (米)
+ * @param scale 缩放因子,默认 0.001 (千米单位)
+ * @returns THREE.Vector3
+ */
+export function latLonToScenePosition(
+  lat: number,
+  lon: number,
+  height = 0,
+  scale = 0.001,
+): THREE.Vector3 {
+  const ecef = latLonToECEF(lat, lon, height)
+  return ecef.multiplyScalar(scale)
+}

+ 129 - 0
test-coordinate.html

@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>坐标系转换测试</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            margin: 20px;
+            background-color: #f5f5f5;
+        }
+        .test-container {
+            max-width: 800px;
+            margin: 0 auto;
+            background: white;
+            padding: 20px;
+            border-radius: 8px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+        }
+        .test-item {
+            margin: 15px 0;
+            padding: 15px;
+            border-left: 4px solid #2196F3;
+            background: #f9f9f9;
+        }
+        .test-item h3 {
+            margin: 0 0 10px 0;
+            color: #333;
+        }
+        .code-block {
+            background: #263238;
+            color: #aed581;
+            padding: 15px;
+            border-radius: 4px;
+            font-family: 'Courier New', monospace;
+            overflow-x: auto;
+            margin: 10px 0;
+        }
+        .status-info {
+            color: #2196F3;
+            font-weight: bold;
+        }
+    </style>
+</head>
+<body>
+    <div class="test-container">
+        <h1>🔄 坐标系转换测试</h1>
+        <p>测试时间: <span id="test-time"></span></p>
+        
+        <div class="test-item">
+            <h3>📋 坐标系说明</h3>
+            <p><strong>Three.js 默认坐标系 (Y-up):</strong></p>
+            <ul>
+                <li>Y轴: 朝上 (垂直方向)</li>
+                <li>Z轴: 朝前 (视线方向)</li>
+                <li>X轴: 朝右 (水平方向)</li>
+            </ul>
+            
+            <p><strong>GLB模型坐标系 (Z-up):</strong></p>
+            <ul>
+                <li>Z轴: 朝上 (垂直方向)</li>
+                <li>Y轴: 朝前 (视线方向)</li>
+                <li>X轴: 朝右 (水平方向)</li>
+            </ul>
+        </div>
+
+        <div class="test-item">
+            <h3>🔧 转换方法</h3>
+            <p>将 Z-up 坐标系转换为 Y-up 坐标系:</p>
+            <div class="code-block">
+// 方法1: 直接旋转模型
+model.rotation.x = -Math.PI / 2
+
+// 方法2: 使用转换工具
+import { convertCoordinateSystem } from '../utils/coordinateConverter'
+const convertedModel = convertCoordinateSystem(model, 'z-up', 'y-up')
+            </div>
+        </div>
+
+        <div class="test-item">
+            <h3>✅ 已实现的功能</h3>
+            <p>✓ 坐标系转换工具函数 (<code>coordinateConverter.ts</code>)</p>
+            <p>✓ 自动检测模型坐标系</p>
+            <p>✓ 支持多种坐标系转换</p>
+            <p>✓ 坐标系参考轴可视化</p>
+            <p>✓ 在 Map3DScene.vue 中应用转换</p>
+        </div>
+
+        <div class="test-item">
+            <h3>📊 使用示例</h3>
+            <div class="code-block">
+// 在模型加载后应用坐标系转换
+async function loadModel() {
+  const loader = new GLTFLoader()
+  const gltf = await loader.loadAsync(props.modelUrl)
+  const model = gltf.scene
+
+  // 计算包围盒并居中
+  const box = new THREE.Box3().setFromObject(model)
+  const center = box.getCenter(new THREE.Vector3())
+  model.position.sub(center)
+
+  // 坐标系转换:Z-up → Y-up
+  const convertedModel = convertCoordinateSystem(model, 'z-up', 'y-up')
+  
+  const group = new THREE.Group()
+  group.add(convertedModel)
+  map.add(group)
+}
+            </div>
+        </div>
+
+        <div class="test-item">
+            <h3>🎯 测试建议</h3>
+            <p>1. 运行开发服务器查看模型显示效果</p>
+            <p>2. 检查模型方向是否正确</p>
+            <p>3. 如需调试,可取消注释坐标系参考轴</p>
+            <p>4. 使用变换面板调整模型位置和旋转</p>
+            <p class="status-info">💡 提示:如果模型方向仍不正确,可能需要调整旋转角度</p>
+        </div>
+    </div>
+
+    <script>
+        document.getElementById('test-time').textContent = new Date().toLocaleString('zh-CN');
+        console.log('坐标系转换测试页面加载完成');
+    </script>
+</body>
+</html>

+ 121 - 0
test-project.html

@@ -0,0 +1,121 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>项目测试页面</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            margin: 20px;
+            background-color: #f5f5f5;
+        }
+        .test-container {
+            max-width: 800px;
+            margin: 0 auto;
+            background: white;
+            padding: 20px;
+            border-radius: 8px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+        }
+        .test-item {
+            margin: 15px 0;
+            padding: 15px;
+            border-left: 4px solid #4CAF50;
+            background: #f9f9f9;
+        }
+        .test-item h3 {
+            margin: 0 0 10px 0;
+            color: #333;
+        }
+        .test-item p {
+            margin: 5px 0;
+            color: #666;
+        }
+        .status-success {
+            color: #4CAF50;
+            font-weight: bold;
+        }
+        .status-warning {
+            color: #ff9800;
+            font-weight: bold;
+        }
+        .status-error {
+            color: #f44336;
+            font-weight: bold;
+        }
+    </style>
+</head>
+<body>
+    <div class="test-container">
+        <h1>🚀 项目测试报告</h1>
+        <p>测试时间: <span id="test-time"></span></p>
+        
+        <div class="test-item">
+            <h3>✅ 项目结构检查</h3>
+            <p>项目根目录: <code>d:\Object\Xinjiang</code></p>
+            <p>主要文件: <code>package.json</code>, <code>index.html</code>, <code>vite.config.ts</code></p>
+            <p>源码目录: <code>src/</code> 包含 Vue 组件、工具函数、场景配置等</p>
+            <p class="status-success">✓ 项目结构完整</p>
+        </div>
+
+        <div class="test-item">
+            <h3>✅ 依赖检查</h3>
+            <p>Vue 3: <code>^3.5.32</code></p>
+            <p>Three.js: <code>^0.184.0</code></p>
+            <p>Vite: <code>^8.0.10</code></p>
+            <p>TypeScript: <code>~6.0.2</code></p>
+            <p class="status-success">✓ 依赖版本兼容</p>
+        </div>
+
+        <div class="test-item">
+            <h3>✅ TypeScript 类型检查</h3>
+            <p>运行命令: <code>npx vue-tsc --noEmit</code></p>
+            <p>结果: 无类型错误</p>
+            <p class="status-success">✓ TypeScript 类型检查通过</p>
+        </div>
+
+        <div class="test-item">
+            <h3>✅ 主要功能模块</h3>
+            <p>• 3D 场景渲染 (Scene3D.vue)</p>
+            <p>• 地图场景 (Map3DScene.vue)</p>
+            <p>• 独库场景 (DucaoScene.vue)</p>
+            <p>• 坐标转换工具 (geoCoord.ts)</p>
+            <p>• 水流材质 (Water.ts, waterFlow.ts)</p>
+            <p class="status-success">✓ 功能模块完整</p>
+        </div>
+
+        <div class="test-item">
+            <h3>⚠️ 测试建议</h3>
+            <p>1. 运行开发服务器: <code>npm run dev</code></p>
+            <p>2. 构建生产版本: <code>npm run build</code></p>
+            <p>3. 添加单元测试框架 (如 Vitest)</p>
+            <p>4. 添加端到端测试 (如 Playwright)</p>
+            <p class="status-warning">! 建议添加测试框架</p>
+        </div>
+
+        <div class="test-item">
+            <h3>📊 项目概览</h3>
+            <p>这是一个基于 Vue 3 + Vite + TypeScript 的三维地理场景项目,使用 Three.js 和 three-tile 库进行 3D 渲染。</p>
+            <p>主要功能包括:</p>
+            <ul>
+                <li>三维地图场景渲染</li>
+                <li>地理坐标转换</li>
+                <li>水流材质效果</li>
+                <li>POI 点标记</li>
+                <li>模型变换调试</li>
+            </ul>
+        </div>
+    </div>
+
+    <script>
+        // 设置测试时间
+        document.getElementById('test-time').textContent = new Date().toLocaleString('zh-CN');
+        
+        // 简单的项目结构验证
+        console.log('项目测试完成');
+        console.log('项目路径: d:\\Object\\Xinjiang');
+        console.log('主要技术栈: Vue 3 + Vite + TypeScript + Three.js');
+    </script>
+</body>
+</html>

+ 235 - 0
test-rotation.html

@@ -0,0 +1,235 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>模型旋转调试</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            margin: 20px;
+            background-color: #f5f5f5;
+        }
+        .test-container {
+            max-width: 800px;
+            margin: 0 auto;
+            background: white;
+            padding: 20px;
+            border-radius: 8px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+        }
+        .test-item {
+            margin: 15px 0;
+            padding: 15px;
+            border-left: 4px solid #ff9800;
+            background: #f9f9f9;
+        }
+        .test-item h3 {
+            margin: 0 0 10px 0;
+            color: #333;
+        }
+        .rotation-controls {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 15px;
+            margin: 15px 0;
+        }
+        .control-group {
+            background: #e3f2fd;
+            padding: 15px;
+            border-radius: 4px;
+        }
+        .control-group h4 {
+            margin: 0 0 10px 0;
+            color: #1976d2;
+        }
+        .rotation-value {
+            font-family: monospace;
+            background: #263238;
+            color: #aed581;
+            padding: 5px 10px;
+            border-radius: 3px;
+            margin: 5px 0;
+        }
+        .test-button {
+            background: #ff9800;
+            color: white;
+            border: none;
+            padding: 10px 20px;
+            border-radius: 4px;
+            cursor: pointer;
+            margin: 5px;
+            font-size: 14px;
+        }
+        .test-button:hover {
+            background: #f57c00;
+        }
+        .test-button.active {
+            background: #4CAF50;
+        }
+        .info-box {
+            background: #fff3e0;
+            border-left: 4px solid #ff9800;
+            padding: 10px;
+            margin: 10px 0;
+        }
+    </style>
+</head>
+<body>
+    <div class="test-container">
+        <h1>🔄 模型旋转调试</h1>
+        <p>测试时间: <span id="test-time"></span></p>
+        
+        <div class="test-item">
+            <h3>📋 问题描述</h3>
+            <p>模型上下颠倒,需要找到正确的旋转角度来修正方向。</p>
+            <div class="info-box">
+                <strong>当前情况:</strong> 模型显示上下颠倒
+                <br><strong>可能原因:</strong> 坐标系转换旋转方向错误
+            </div>
+        </div>
+
+        <div class="test-item">
+            <h3>🔧 旋转调试控制</h3>
+            <div class="rotation-controls">
+                <div class="control-group">
+                    <h4>绕X轴旋转</h4>
+                    <div class="rotation-value">当前: <span id="rot-x">0</span>°</div>
+                    <button class="test-button" onclick="rotateX(90)">+90°</button>
+                    <button class="test-button" onclick="rotateX(-90)">-90°</button>
+                    <button class="test-button" onclick="rotateX(180)">+180°</button>
+                    <button class="test-button" onclick="resetRotation('x')">重置</button>
+                </div>
+                
+                <div class="control-group">
+                    <h4>绕Y轴旋转</h4>
+                    <div class="rotation-value">当前: <span id="rot-y">0</span>°</div>
+                    <button class="test-button" onclick="rotateY(90)">+90°</button>
+                    <button class="test-button" onclick="rotateY(-90)">-90°</button>
+                    <button class="test-button" onclick="rotateY(180)">+180°</button>
+                    <button class="test-button" onclick="resetRotation('y')">重置</button>
+                </div>
+                
+                <div class="control-group">
+                    <h4>绕Z轴旋转</h4>
+                    <div class="rotation-value">当前: <span id="rot-z">0</span>°</div>
+                    <button class="test-button" onclick="rotateZ(90)">+90°</button>
+                    <button class="test-button" onclick="rotateZ(-90)">-90°</button>
+                    <button class="test-button" onclick="rotateZ(180)">+180°</button>
+                    <button class="test-button" onclick="resetRotation('z')">重置</button>
+                </div>
+                
+                <div class="control-group">
+                    <h4>预设旋转</h4>
+                    <button class="test-button" onclick="applyPreset('z-up-to-y-up')">Z-up → Y-up</button>
+                    <button class="test-button" onclick="applyPreset('y-up-to-z-up')">Y-up → Z-up</button>
+                    <button class="test-button" onclick="applyPreset('flip-x')">翻转X轴</button>
+                    <button class="test-button" onclick="applyPreset('flip-y')">翻转Y轴</button>
+                </div>
+            </div>
+        </div>
+
+        <div class="test-item">
+            <h3>📊 旋转矩阵</h3>
+            <div class="rotation-value" id="matrix-display">
+                [1, 0, 0, 0]<br>
+                [0, 1, 0, 0]<br>
+                [0, 0, 1, 0]<br>
+                [0, 0, 0, 1]
+            </div>
+        </div>
+
+        <div class="test-item">
+            <h3>🎯 推荐解决方案</h3>
+            <p>根据你的描述(模型上下颠倒),最可能的解决方案是:</p>
+            <div class="info-box">
+                <strong>方案1:</strong> 绕X轴旋转 +90° (Math.PI/2)
+                <br><strong>方案2:</strong> 绕X轴旋转 -90° (-Math.PI/2)
+                <br><strong>方案3:</strong> 绕X轴旋转 180° (Math.PI)
+            </div>
+            <p>点击上面的按钮测试不同的旋转效果。</p>
+        </div>
+
+        <div class="test-item">
+            <h3>📝 代码示例</h3>
+            <div style="background: #263238; color: #aed581; padding: 15px; border-radius: 4px; font-family: monospace;">
+                // 在模型加载后应用旋转<br>
+                model.rotation.x = Math.PI / 2; // +90度<br>
+                // 或者<br>
+                model.rotation.x = -Math.PI / 2; // -90度<br>
+                // 或者<br>
+                model.rotation.x = Math.PI; // 180度
+            </div>
+        </div>
+    </div>
+
+    <script>
+        let currentRotation = { x: 0, y: 0, z: 0 };
+        
+        function updateDisplay() {
+            document.getElementById('rot-x').textContent = Math.round(currentRotation.x * 180 / Math.PI);
+            document.getElementById('rot-y').textContent = Math.round(currentRotation.y * 180 / Math.PI);
+            document.getElementById('rot-z').textContent = Math.round(currentRotation.z * 180 / Math.PI);
+            
+            // 更新矩阵显示
+            const cosX = Math.cos(currentRotation.x);
+            const sinX = Math.sin(currentRotation.x);
+            const cosY = Math.cos(currentRotation.y);
+            const sinY = Math.sin(currentRotation.y);
+            const cosZ = Math.cos(currentRotation.z);
+            const sinZ = Math.sin(currentRotation.z);
+            
+            document.getElementById('matrix-display').innerHTML = 
+                `[${cosY*cosZ.toFixed(2)}, ${-cosY*sinZ.toFixed(2)}, ${sinY.toFixed(2)}, 0]<br>` +
+                `[${(sinX*sinY*cosZ + cosX*sinZ).toFixed(2)}, ${(-sinX*sinY*sinZ + cosX*cosZ).toFixed(2)}, ${-sinX*cosY.toFixed(2)}, 0]<br>` +
+                `[${(-cosX*sinY*cosZ + sinX*sinZ).toFixed(2)}, ${(cosX*sinY*sinZ + sinX*cosZ).toFixed(2)}, ${cosX*cosY.toFixed(2)}, 0]<br>` +
+                `[0, 0, 0, 1]`;
+        }
+        
+        function rotateX(angle) {
+            currentRotation.x += angle * Math.PI / 180;
+            updateDisplay();
+        }
+        
+        function rotateY(angle) {
+            currentRotation.y += angle * Math.PI / 180;
+            updateDisplay();
+        }
+        
+        function rotateZ(angle) {
+            currentRotation.z += angle * Math.PI / 180;
+            updateDisplay();
+        }
+        
+        function resetRotation(axis) {
+            currentRotation[axis] = 0;
+            updateDisplay();
+        }
+        
+        function applyPreset(preset) {
+            switch(preset) {
+                case 'z-up-to-y-up':
+                    currentRotation.x = Math.PI / 2;
+                    break;
+                case 'y-up-to-z-up':
+                    currentRotation.x = -Math.PI / 2;
+                    break;
+                case 'flip-x':
+                    currentRotation.x = Math.PI;
+                    break;
+                case 'flip-y':
+                    currentRotation.y = Math.PI;
+                    break;
+            }
+            updateDisplay();
+        }
+        
+        // 初始化
+        document.getElementById('test-time').textContent = new Date().toLocaleString('zh-CN');
+        updateDisplay();
+        
+        console.log('旋转调试页面加载完成');
+        console.log('当前旋转角度:', currentRotation);
+    </script>
+</body>
+</html>

+ 131 - 0
test-simple-rotation.html

@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>简单旋转测试</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            margin: 20px;
+            background-color: #f5f5f5;
+        }
+        .test-container {
+            max-width: 600px;
+            margin: 0 auto;
+            background: white;
+            padding: 20px;
+            border-radius: 8px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+        }
+        .solution {
+            margin: 20px 0;
+            padding: 15px;
+            border-left: 4px solid #4CAF50;
+            background: #f9f9f9;
+        }
+        .solution h3 {
+            margin: 0 0 10px 0;
+            color: #333;
+        }
+        .code {
+            background: #263238;
+            color: #aed581;
+            padding: 15px;
+            border-radius: 4px;
+            font-family: monospace;
+            margin: 10px 0;
+        }
+        .status {
+            padding: 10px;
+            margin: 10px 0;
+            border-radius: 4px;
+        }
+        .status.info {
+            background: #e3f2fd;
+            border-left: 4px solid #2196F3;
+        }
+        .status.warning {
+            background: #fff3e0;
+            border-left: 4px solid #ff9800;
+        }
+    </style>
+</head>
+<body>
+    <div class="test-container">
+        <h1>🔄 简单旋转测试</h1>
+        <p>测试时间: <span id="test-time"></span></p>
+        
+        <div class="status info">
+            <strong>问题:</strong>模型上下颠倒
+            <br><strong>原因:</strong>GLB模型是Z轴朝上,Three.js是Y轴朝上
+        </div>
+
+        <div class="solution">
+            <h3>✅ 推荐解决方案</h3>
+            <p>在模型加载后添加以下代码:</p>
+            <div class="code">
+// 方法1:绕X轴旋转 -90度(最常用)<br>
+model.rotation.x = -Math.PI / 2;<br><br>
+// 方法2:绕X轴旋转 +90度<br>
+model.rotation.x = Math.PI / 2;<br><br>
+// 方法3:绕X轴旋转 180度<br>
+model.rotation.x = Math.PI;
+            </div>
+        </div>
+
+        <div class="solution">
+            <h3>🔧 在项目中的具体修改</h3>
+            <p>编辑文件:<code>src/scenes/Map3DScene.vue</code></p>
+            <p>找到模型加载部分,添加旋转代码:</p>
+            <div class="code">
+// 在 model.position.sub(center) 之后添加:<br>
+model.rotation.x = -Math.PI / 2; // 或者尝试 +Math.PI / 2
+            </div>
+        </div>
+
+        <div class="solution">
+            <h3>📋 完整代码示例</h3>
+            <div class="code">
+async function loadModel() {<br>
+  const loader = new GLTFLoader()<br>
+  const gltf = await loader.loadAsync(props.modelUrl)<br>
+  const model = gltf.scene<br><br>
+  // 计算包围盒并居中<br>
+  const box = new THREE.Box3().setFromObject(model)<br>
+  const center = box.getCenter(new THREE.Vector3())<br>
+  model.position.sub(center)<br><br>
+  // 修复上下颠倒问题<br>
+  model.rotation.x = -Math.PI / 2;<br><br>
+  const group = new THREE.Group()<br>
+  group.add(model)<br>
+  group.position.set(0, 0, 0)<br>
+  group.rotation.y = THREE.MathUtils.degToRad(5)<br>
+  map.add(group)<br>
+  modelGroup = group<br>
+}
+            </div>
+        </div>
+
+        <div class="status warning">
+            <strong>提示:</strong>如果 -90度 不正确,请尝试 +90度 或 180度。
+            <br>不同的模型可能需要不同的旋转角度。
+        </div>
+
+        <div class="solution">
+            <h3>🎯 测试步骤</h3>
+            <ol>
+                <li>修改 <code>Map3DScene.vue</code> 添加旋转代码</li>
+                <li>运行开发服务器 <code>npm run dev</code></li>
+                <li>查看模型方向是否正确</li>
+                <li>如不正确,尝试其他旋转角度</li>
+            </ol>
+        </div>
+    </div>
+
+    <script>
+        document.getElementById('test-time').textContent = new Date().toLocaleString('zh-CN');
+        console.log('简单旋转测试页面加载完成');
+    </script>
+</body>
+</html>

+ 123 - 0
坐标系转换说明.md

@@ -0,0 +1,123 @@
+# GLB模型坐标系转换说明
+
+## 问题描述
+你的GLB模型使用的是 **Z轴朝上、Y轴向前** 的坐标系,而Three.js默认使用 **Y轴朝上、Z轴向前** 的坐标系。这会导致模型在场景中显示方向错误。
+
+## 解决方案
+
+### 1. 坐标系差异对比
+
+| 坐标系 | 朝上轴 | 向前轴 | 右轴 | 常见软件 |
+|--------|--------|--------|------|----------|
+| **Three.js (Y-up)** | Y | Z | X | Three.js 默认 |
+| **GLB模型 (Z-up)** | Z | Y | X | Blender, 3ds Max, CAD |
+
+### 2. 已实现的转换工具
+
+创建了 `src/utils/coordinateConverter.ts` 文件,包含以下功能:
+
+#### 主要函数:
+- `convertCoordinateSystem(model, from, to)` - 坐标系转换
+- `autoConvertModelCoordinates(model)` - 自动检测并转换
+- `createCoordinateAxes(size)` - 创建坐标系参考轴(调试用)
+- `applyCoordinateConversion(group, config)` - 应用转换到模型组
+
+#### 支持的坐标系:
+- `y-up`: Three.js 默认坐标系
+- `z-up`: Blender/3ds Max 默认坐标系
+- `z-up-forward-x`: 某些CAD软件坐标系
+
+### 3. 在项目中的应用
+
+#### Map3DScene.vue 中的修改:
+
+```typescript
+// 导入转换工具
+import { convertCoordinateSystem } from '../utils/coordinateConverter'
+
+// 在模型加载函数中应用转换
+async function loadModel() {
+  const loader = new GLTFLoader()
+  const gltf = await loader.loadAsync(props.modelUrl)
+  const model = gltf.scene
+
+  // 计算包围盒并居中
+  const box = new THREE.Box3().setFromObject(model)
+  const center = box.getCenter(new THREE.Vector3())
+  model.position.sub(center)
+
+  // 坐标系转换:Z-up → Y-up
+  const convertedModel = convertCoordinateSystem(model, 'z-up', 'y-up')
+  
+  const group = new THREE.Group()
+  group.add(convertedModel)
+  group.position.set(0, 0, 0)
+  group.rotation.y = THREE.MathUtils.degToRad(5)
+  map.add(group)
+  modelGroup = group
+}
+```
+
+### 4. 转换原理
+
+Z-up 到 Y-up 的转换实际上是绕 X 轴旋转 -90 度:
+```typescript
+model.rotation.x = -Math.PI / 2
+```
+
+或者使用矩阵变换:
+```typescript
+const matrix = new THREE.Matrix4().makeRotationX(-Math.PI / 2)
+model.applyMatrix4(matrix)
+```
+
+### 5. 调试和验证
+
+#### 方法1:使用坐标系参考轴
+```typescript
+// 在 Map3DScene.vue 中取消注释
+const axes = createCoordinateAxes(100)
+group.add(axes)
+```
+
+#### 方法2:检查模型包围盒
+转换前后检查模型尺寸:
+```typescript
+const box = new THREE.Box3().setFromObject(model)
+const size = box.getSize(new THREE.Vector3())
+console.log(`模型尺寸: (${size.x}, ${size.y}, ${size.z})`)
+```
+
+#### 方法3:使用变换面板
+项目中的变换面板可以实时调整模型位置、旋转和缩放。
+
+### 6. 测试页面
+
+创建了两个测试页面:
+- `test-project.html` - 项目整体测试
+- `test-coordinate.html` - 坐标系转换专门测试
+
+### 7. 注意事项
+
+1. **模型居中**:转换前先将模型居中到原点
+2. **旋转顺序**:确保在正确的位置应用旋转
+3. **性能考虑**:坐标系转换只在加载时执行一次
+4. **调试模式**:开发时可启用坐标系参考轴
+
+### 8. 常见问题
+
+**Q: 转换后模型方向还是不对?**
+A: 检查模型的原始坐标系,可能需要调整旋转角度或使用自动检测功能。
+
+**Q: 如何知道模型是什么坐标系?**
+A: 查看模型导出软件的设置,或使用 `autoConvertModelCoordinates` 函数自动检测。
+
+**Q: 转换会影响模型动画吗?**
+A: 不会,坐标系转换只影响静态模型的初始方向。
+
+## 下一步建议
+
+1. 运行开发服务器测试模型显示效果
+2. 如需进一步调整,使用变换面板微调
+3. 考虑添加更多坐标系支持(如 Y-forward, Z-forward 等)
+4. 为不同类型的模型添加预设转换配置

+ 120 - 0
模型上下颠倒解决方案.md

@@ -0,0 +1,120 @@
+# 模型上下颠倒解决方案
+
+## 问题描述
+GLB模型在Three.js场景中显示上下颠倒。
+
+## 原因分析
+- **GLB模型坐标系**: Z轴朝上,Y轴向前
+- **Three.js坐标系**: Y轴朝上,Z轴向前
+- **结果**: 模型旋转了90度,导致上下颠倒
+
+## 已实施的解决方案
+
+### 1. 简单旋转方案(已应用)
+在 `src/scenes/Map3DScene.vue` 的 `loadModel()` 函数中添加:
+
+```typescript
+// 在 model.position.sub(center) 之后添加:
+model.rotation.x = -Math.PI / 2; // 绕X轴旋转 -90度
+```
+
+### 2. 完整代码示例
+```typescript
+async function loadModel() {
+  const loader = new GLTFLoader()
+  const gltf = await loader.loadAsync(props.modelUrl)
+  const model = gltf.scene
+
+  // 计算包围盒并居中
+  const box = new THREE.Box3().setFromObject(model)
+  const center = box.getCenter(new THREE.Vector3())
+  model.position.sub(center)
+
+  // 修复上下颠倒问题:绕X轴旋转 -90度
+  model.rotation.x = -Math.PI / 2
+
+  const group = new THREE.Group()
+  group.add(model)
+  group.position.set(0, 0, 0)
+  group.rotation.y = THREE.MathUtils.degToRad(5)
+  map.add(group)
+  modelGroup = group
+}
+```
+
+## 如果仍然上下颠倒
+
+如果 `-Math.PI / 2` 不正确,请尝试以下替代方案:
+
+### 方案1:绕X轴旋转 +90度
+```typescript
+model.rotation.x = Math.PI / 2;
+```
+
+### 方案2:绕X轴旋转 180度
+```typescript
+model.rotation.x = Math.PI;
+```
+
+### 方案3:使用坐标系转换工具
+```typescript
+import { convertCoordinateSystem } from '../utils/coordinateConverter'
+
+// 在模型加载后
+const convertedModel = convertCoordinateSystem(model, 'z-up', 'y-up', 'flipped')
+```
+
+## 调试方法
+
+### 1. 检查控制台输出
+打开浏览器开发者工具,查看控制台中的模型信息:
+```
+[Map3D] === 模型调试信息 ===
+[Map3D] 包围盒中心 (局部): (x, y, z)
+[Map3D] 包围盒尺寸 (m): (x, y, z)
+```
+
+### 2. 使用坐标系参考轴
+取消注释代码中的坐标系参考轴:
+```typescript
+// const axes = createCoordinateAxes(100)
+// group.add(axes)
+```
+
+### 3. 使用变换面板
+项目中的变换面板可以实时调整模型位置、旋转和缩放。
+
+## 测试页面
+
+创建了以下测试页面帮助调试:
+- `test-rotation.html` - 交互式旋转调试
+- `test-simple-rotation.html` - 简单旋转测试指南
+
+## 常见问题
+
+### Q: 为什么需要旋转模型?
+A: 因为不同3D软件使用不同的坐标系标准,需要转换到Three.js的坐标系。
+
+### Q: 旋转会影响模型动画吗?
+A: 不会,坐标系转换只影响静态模型的初始方向。
+
+### Q: 如何知道模型是什么坐标系?
+A: 查看模型导出软件的设置,或根据模型尺寸判断(Z轴尺寸最大可能是Z-up)。
+
+### Q: 旋转后模型位置不对怎么办?
+A: 确保在居中(`model.position.sub(center)`)之后再进行旋转。
+
+## 下一步建议
+
+1. **测试当前方案**: 运行开发服务器查看模型显示效果
+2. **调整旋转角度**: 如果不正确,尝试其他旋转角度
+3. **使用调试工具**: 参考测试页面进行交互式调试
+4. **记录成功方案**: 找到正确的旋转角度后记录下来
+
+## 文件修改记录
+
+- `src/scenes/Map3DScene.vue`: 添加模型旋转代码
+- `src/utils/coordinateConverter.ts`: 创建坐标系转换工具
+- `src/utils/coordinateDebug.ts`: 创建坐标系调试工具
+- `test-rotation.html`: 交互式旋转调试页面
+- `test-simple-rotation.html`: 简单旋转测试指南