BAI 21 時間 前
コミット
325a84781c

BIN
XinJiang.7z → XinJiang615.7z


BIN
dist.7z


+ 11 - 2
src/App.vue

@@ -3,9 +3,10 @@ import { ref } from 'vue'
 import Scene3D from './scenes/Scene3D.vue'
 import DucaoScene from './scenes/DucaoScene.vue'
 import Map3DScene from './scenes/Map3DScene.vue'
+import HydraulicDiagram3D from './scenes/HydraulicDiagram3D.vue'
 import type { POIConfig } from './utils/geoCoord'
 
-const currentScene = ref<'scene3d' | 'ducao' | 'map3d'>('map3d')
+const currentScene = ref<'scene3d' | 'ducao' | 'map3d' | 'hydraulic'>('map3d')
 
 const mapPOIs: POIConfig[] = [
   {
@@ -41,10 +42,18 @@ const mapPOIs: POIConfig[] = [
       >
         渡槽场景
       </button>
+      <button
+        class="switch-btn"
+        :class="{ active: currentScene === 'hydraulic' }"
+        @click="currentScene = 'hydraulic'"
+      >
+        组态图
+      </button>
     </div>
     <Map3DScene v-if="currentScene === 'map3d'" :initialPOIs="mapPOIs" />
     <Scene3D v-else-if="currentScene === 'scene3d'" :show-debug-tools="true" />
-    <DucaoScene v-else :show-debug-tools="true" />
+    <DucaoScene v-else-if="currentScene === 'ducao'" :show-debug-tools="true" />
+    <HydraulicDiagram3D v-else-if="currentScene === 'hydraulic'" />
   </div>
 </template>
 

BIN
src/assets/icon/T_Dadian_02.png


BIN
src/assets/icon/gongcheng.png


BIN
src/assets/icon/gongchengdown.png


+ 167 - 0
src/composables/useIconMarkers.ts

@@ -0,0 +1,167 @@
+import { ref, type Ref } from 'vue'
+import * as THREE from 'three'
+import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
+import { latLonToLocalENU } from '../utils/geoCoord'
+import type { MarkerItem } from '../config/markerConfig'
+import markersConfig from '../config/markerConfig'
+import dadianPng from '../assets/icon/T_Dadian_02.png'
+
+export function useIconMarkers(
+  containerRef: Ref<HTMLDivElement | undefined>,
+  originLat: number,
+  originLon: number,
+) {
+  let scene: THREE.Scene | null = null
+  let camera: THREE.PerspectiveCamera | null = null
+  let iconMarkerGroup: THREE.Group | null = null
+
+  const markerPositions: Record<string, THREE.Vector3> = {}
+  const selectedMarker = ref<MarkerItem | null>(null)
+  const popupStyle = ref({ left: '0px', top: '0px' })
+
+  function init(sc: THREE.Scene, cam: THREE.PerspectiveCamera) {
+    scene = sc
+    camera = cam
+  }
+
+  function worldToScreen(worldPos: THREE.Vector3): { left: number; top: number } {
+    if (!camera || !containerRef.value) return { left: 0, top: 0 }
+    const vec = worldPos.clone().project(camera)
+    return {
+      left: (vec.x * 0.5 + 0.5) * containerRef.value.clientWidth,
+      top: (-vec.y * 0.5 + 0.5) * containerRef.value.clientHeight,
+    }
+  }
+
+  function openPopup(item: MarkerItem, worldPos: THREE.Vector3) {
+    selectedMarker.value = item
+    const screen = worldToScreen(worldPos)
+    popupStyle.value = {
+      left: `${screen.left}px`,
+      top: `${screen.top}px`,
+    }
+  }
+
+  function closePopup() {
+    selectedMarker.value = null
+  }
+
+  function updatePopupPosition() {
+    if (!selectedMarker.value || !camera) return
+    const pos = markerPositions[selectedMarker.value.label]
+    if (!pos) return
+    const screen = worldToScreen(pos)
+    popupStyle.value = {
+      left: `${screen.left}px`,
+      top: `${screen.top}px`,
+    }
+  }
+
+  /**
+   * 创建图标标记:小圆点 + 文字,锚点为圆点中心
+   * labelPos 控制文字在图标的上/下/左/右
+   */
+  function addIconMarkers(): Record<string, THREE.Vector3> {
+    if (!scene) return markerPositions
+
+    iconMarkerGroup = new THREE.Group()
+    const iconSize = 24
+
+    markersConfig.forEach((item) => {
+      const localPos = latLonToLocalENU(item.lat, item.lon, 0, originLat, originLon)
+      localPos.y += 10
+
+      const pos = item.labelPos || 'top'
+
+      // 容器 = 图标大小,锚点中心
+      const div = document.createElement('div')
+      div.className = 'icon-marker'
+      div.style.cssText = `
+        position: relative;
+        width: ${iconSize}px;
+        height: ${iconSize}px;
+        color: #fff;
+        cursor: pointer;
+        pointer-events: auto;
+        transform: translate(-50%, -100%);
+      `
+
+      // 文字
+      const labelSpan = document.createElement('span')
+      labelSpan.style.position = 'absolute'
+      labelSpan.style.color = '#fff'
+      labelSpan.style.font = '11px "Microsoft YaHei", sans-serif'
+      labelSpan.style.textShadow = '0 0 8px rgba(0,0,0,0.95)'
+      labelSpan.style.whiteSpace = 'nowrap'
+      labelSpan.style.pointerEvents = 'none'
+      // 位置
+      if (pos === 'top') {
+        labelSpan.style.bottom = '100%'
+        labelSpan.style.left = '50%'
+        labelSpan.style.transform = 'translate(-50%, -4px)'
+      } else if (pos === 'bottom') {
+        labelSpan.style.top = '100%'
+        labelSpan.style.left = '50%'
+        labelSpan.style.transform = 'translate(-50%, 4px)'
+      } else if (pos === 'left') {
+        labelSpan.style.right = '100%'
+        labelSpan.style.top = '50%'
+        labelSpan.style.transform = 'translate(-4px, -50%)'
+      } else if (pos === 'right') {
+        labelSpan.style.left = '100%'
+        labelSpan.style.top = '50%'
+        labelSpan.style.transform = 'translate(4px, -50%)'
+      }
+      labelSpan.textContent = item.label
+
+      // 圆点
+      const iconDiv = document.createElement('div')
+      iconDiv.style.cssText = `
+        width: 100%;
+        height: 100%;
+        background: url(${dadianPng}) no-repeat center/contain;
+        pointer-events: none;
+      `
+
+      div.appendChild(iconDiv)
+      div.appendChild(labelSpan)
+
+      div.addEventListener('click', (e) => {
+        e.stopPropagation()
+        openPopup(item, localPos)
+      })
+
+      const label = new CSS2DObject(div)
+      label.position.copy(localPos)
+      iconMarkerGroup!.add(label)
+
+      markerPositions[item.label] = localPos.clone()
+    })
+
+    scene.add(iconMarkerGroup)
+    return markerPositions
+  }
+
+  function dispose() {
+    if (iconMarkerGroup && scene) {
+      scene.remove(iconMarkerGroup)
+      iconMarkerGroup.traverse((child) => {
+        if ((child as CSS2DObject).isCSS2DObject) {
+          ;(child as CSS2DObject).element.remove()
+        }
+      })
+      iconMarkerGroup = null
+    }
+  }
+
+  return {
+    markerPositions,
+    selectedMarker,
+    popupStyle,
+    init,
+    addIconMarkers,
+    closePopup,
+    updatePopupPosition,
+    dispose,
+  }
+}

+ 66 - 0
src/config/markerConfig.ts

@@ -0,0 +1,66 @@
+/**
+ * 地图图标标记配置
+ *
+ * 使用方式:
+ *   在 markers 数组里添加对象 { lat, lon, label },
+ *   label 会显示在图标上方。
+ *
+ *   场景会自动读取此配置,将图标渲染到对应经纬度位置。
+ */
+export interface GateData {
+  /** 闸门开度 */
+  opening: { current: number; max: number; unit: string; percent: number; target: number }
+  /** 运行状态 */
+  status: '正在开启' | '正在关闭' | '停止' | '稳定' | '卡滞'
+  /** 限位信号 */
+  limitSignals: { upper: boolean; lower: boolean; emergency: boolean }
+  /** 荷重/拉力 (吨) */
+  load: { current: number; warning: boolean; protected: boolean }
+  /** 左右同步 */
+  sync: { deviation: number; status: '同步' | '偏差' | '异常' }
+  /** 水位 */
+  waterLevel: { upstream: number; downstream: number; diff: number; unit: string }
+  /** 流量 */
+  flow: { realtime: number; accumulated: number; unit: string }
+}
+
+export interface MarkerItem {
+  lat: number
+  lon: number
+  label: string
+  /** 弹窗展示的详细信息(可选) */
+  info?: string
+  /** 文字相对图标位置: 'top' | 'bottom' | 'left' | 'right',默认 'top' */
+  labelPos?: 'top' | 'bottom' | 'left' | 'right'
+  /** 闸门监控数据(可选,有此字段才会显示详细面板) */
+  gateData?: GateData
+}
+
+function mockGateData(): GateData {
+  const openPercent = Math.round(Math.random() * 100)
+  return {
+    opening: { current: +(openPercent * 0.05).toFixed(2), max: 5.00, unit: 'm', percent: openPercent, target: +(Math.random() * 5).toFixed(2) },
+    status: ['稳定', '停止', '稳定', '稳定', '正在开启'][Math.floor(Math.random() * 5)] as GateData['status'],
+    limitSignals: { upper: false, lower: false, emergency: false },
+    load: { current: +(Math.random() * 100).toFixed(1), warning: Math.random() > 0.9, protected: false },
+    sync: { deviation: +(Math.random() * 30).toFixed(1), status: Math.random() > 0.85 ? ('偏差' as const) : ('同步' as const) },
+    waterLevel: { upstream: +(Math.random() * 10 + 50).toFixed(2), downstream: +(Math.random() * 8 + 48).toFixed(2), diff: 0, unit: 'm' },
+    flow: { realtime: +(Math.random() * 200 + 50).toFixed(1), accumulated: +(Math.random() * 100000).toFixed(0), unit: 'm³/s' },
+  }
+}
+
+const markers: MarkerItem[] = [
+  // ===== 在此添加标记点 =====
+  { lat: 37.509336, lon: 84.286164, label: '分水闸', info: '莫勒切河节制分水闸', labelPos: 'bottom', gateData: mockGateData() },
+  { lat: 37.628984, lon: 84.195182, label: '1#沉砂池', info: '38团一期沉砂池控制闸', labelPos: 'bottom', gateData: mockGateData() },
+  { lat: 37.574900, lon: 84.519057, label: '2#沉砂池', info: '38团二期沉砂池控制闸', labelPos: 'top', gateData: mockGateData() },
+  { lat: 37.461249, lon: 84.898796, label: '退水闸', info: '退水闸', labelPos: 'top', gateData: mockGateData() },
+  { lat: 37.281879, lon: 85.121654, label: '条形沉砂池', info: '条形沉砂池', labelPos: 'top', gateData: mockGateData() },
+  { lat: 37.191278, lon: 85.220484, label: '渠首', info: '渠首', labelPos: 'bottom', gateData: mockGateData() },
+  { lat: 37.327064, lon: 84.400808, label: '莫勒切河', info: '莫勒切河水源', labelPos: 'top' },
+  { lat: 37.127483, lon: 84.519898, label: '石门水库', info: '石门水库', labelPos: 'top' },
+  { lat: 37.757372, lon: 84.185478, label: '团场', info: '灌区团场', labelPos: 'top' },
+]
+
+export { mockGateData }
+export default markers

+ 1 - 2
src/scenes/DucaoScene.vue

@@ -169,8 +169,7 @@ function initScene() {
   container.appendChild(renderer.domElement)
 
   controls = new OrbitControls(camera, renderer.domElement)
-  controls.enableDamping = true
-  controls.dampingFactor = 0.03
+  controls.enableDamping = false
   controls.screenSpacePanning = false
   controls.mouseButtons = {
     LEFT: THREE.MOUSE.PAN,

+ 330 - 0
src/scenes/HydraulicDiagram3D.vue

@@ -0,0 +1,330 @@
+<script setup lang="ts">
+import { onMounted, onUnmounted, ref } from 'vue'
+import * as THREE from 'three'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
+import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
+
+const containerRef = ref<HTMLDivElement>()
+
+let scene: THREE.Scene
+let camera: THREE.PerspectiveCamera
+let renderer: THREE.WebGLRenderer
+let labelRenderer: CSS2DRenderer
+let controls: OrbitControls
+let animationId: number
+let flowDots: THREE.Mesh[] = []
+
+interface DeviceNode {
+  id: string; label: string; x: number; z: number; value: string; color: number
+}
+
+const nodes: DeviceNode[] = [
+  { id: 'quShou',   label: '渠首',     x: -300, z: -70,  value: '引水枢纽',   color: 0x4dd0e1 },
+  { id: 'tiaoXing', label: '条形沉砂池', x: -150, z: -70, value: '12.8万m³',  color: 0x4dd0e1 },
+  { id: 'gate1',    label: '退水闸',   x: 0,    z: -70,  value: '退水',      color: 0x4dd0e1 },
+  { id: 'chen2',    label: '2#沉砂池', x: 150,  z: -70,  value: '38团二期',   color: 0x4dd0e1 },
+  { id: 'chen1',    label: '1#沉砂池', x: 300,  z: -70,  value: '38团一期',   color: 0x4dd0e1 },
+  { id: 'river',    label: '莫勒切河', x: -150, z: 70,   value: '来水',      color: 0x4fc3f7 },
+  { id: 'fenShui',  label: '节制分水闸', x: 0,   z: 70,  value: '分水',      color: 0x4dd0e1 },
+]
+const nodeMap = new Map(nodes.map(n => [n.id, n]))
+
+const pipes = [
+  // 线路1:渠首 → 条形沉砂池 → 退水闸 → 2#沉砂池
+  { from: 'quShou', to: 'tiaoXing' },
+  { from: 'tiaoXing', to: 'gate1' },
+  { from: 'gate1', to: 'chen2' },
+  // 线路2:莫勒切河 → 节制分水闸 → 2#沉砂池 + 1#沉砂池
+  { from: 'river', to: 'fenShui' },
+  { from: 'fenShui', to: 'chen2' },
+  { from: 'fenShui', to: 'chen1' },
+  // 2#沉砂池 → 1#沉砂池
+  { from: 'chen2', to: 'chen1' },
+]
+
+// 流动管线 Shader
+const pipeVertexShader = `
+  attribute float aDistance;
+  uniform float uTime;
+  varying float vDist;
+  void main() {
+    vDist = aDistance + uTime * 0.15;
+    vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
+    gl_Position = projectionMatrix * mvPos;
+  }
+`
+
+const pipeFragmentShader = `
+  uniform vec3 uColor;
+  uniform float uOpacity;
+  varying float vDist;
+  void main() {
+    // 流动条纹:沿管线方向亮暗交替
+    float flow = sin((vDist - 0.5) * 6.283 * 3.0) * 0.5 + 0.5;
+    flow = pow(flow, 2.0);
+    float glow = 0.3 + 0.7 * flow;
+    gl_FragColor = vec4(uColor * glow, uOpacity * (0.5 + 0.5 * flow));
+  }
+`
+
+/** 创建一条带流动效果的管线(Tube 网格,支持线宽) */
+function createFlowLine(pts: THREE.Vector3[]) {
+  // 用 CatmullRom 但 tension=0 保持直线
+  const curve = new THREE.CatmullRomCurve3(pts, false, 'catmullrom', 0)
+  const tubeGeo = new THREE.TubeGeometry(curve, 64, 1.8, 8, false)
+
+  // 重新计算顶点距离属性
+  const posAttr = tubeGeo.attributes.position
+  const vertCount = posAttr.count
+  const distances = new Float32Array(vertCount)
+  let totalLen = 0
+  // 按 TubeGeometry 的环结构估算距离
+  for (let i = 0; i < vertCount; i++) {
+    const x = posAttr.getX(i), y = posAttr.getY(i), z = posAttr.getZ(i)
+    if (i > 0) {
+      totalLen += Math.sqrt(
+        (x - posAttr.getX(i-1))**2 + (y - posAttr.getY(i-1))**2 + (z - posAttr.getZ(i-1))**2
+      )
+    }
+    distances[i] = totalLen
+  }
+  const maxLen = distances[vertCount - 1] || 1
+  for (let i = 0; i < vertCount; i++) distances[i] /= maxLen
+
+  tubeGeo.setAttribute('aDistance', new THREE.Float32BufferAttribute(distances, 1))
+
+  const mat = new THREE.ShaderMaterial({
+    uniforms: {
+      uTime: { value: 0 },
+      uColor: { value: new THREE.Color(0x4dd0e1) },
+      uOpacity: { value: 0.9 },
+    },
+    vertexShader: pipeVertexShader,
+    fragmentShader: pipeFragmentShader,
+    transparent: true,
+    blending: THREE.AdditiveBlending,
+    depthWrite: false,
+  })
+
+  const mesh = new THREE.Mesh(tubeGeo, mat);
+  (mesh as any)._pipeMat = mat
+  return mesh
+}
+
+// 存储所有管线材质
+const pipeMats: THREE.ShaderMaterial[] = []
+
+/** 构建折线路径点 */
+function buildPoints(a: DeviceNode, b: DeviceNode, from: string, to: string): THREE.Vector3[] {
+  // 分水闸 → 1#沉砂池:先右后下
+  if (from === 'fenShui' && to === 'chen1') {
+    return [
+      new THREE.Vector3(a.x, 14, a.z),
+      new THREE.Vector3(b.x, 14, a.z),
+      new THREE.Vector3(b.x, 14, b.z),
+    ]
+  }
+  // 同行/同列:直线
+  if (a.z === b.z || a.x === b.x) {
+    return [
+      new THREE.Vector3(a.x, 14, a.z),
+      new THREE.Vector3(b.x, 14, b.z),
+    ]
+  }
+  // 标准 L 折线:先水平再垂直
+  return [
+    new THREE.Vector3(a.x, 14, a.z),
+    new THREE.Vector3(b.x, 14, a.z),
+    new THREE.Vector3(b.x, 14, b.z),
+  ]
+}
+
+function initScene() {
+  const el = containerRef.value
+  if (!el) { console.warn('[H3D] no container'); return }
+  const w = el.clientWidth, h = el.clientHeight
+  console.log('[H3D] init', w, h)
+  if (w === 0 || h === 0) return
+
+  scene = new THREE.Scene()
+  scene.background = new THREE.Color(0x0a1a2e)
+
+  camera = new THREE.PerspectiveCamera(40, w / h, 1, 2000)
+  camera.position.set(300, 350, 450)
+
+  renderer = new THREE.WebGLRenderer({ antialias: true })
+  renderer.setSize(w, h)
+  renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
+  el.appendChild(renderer.domElement)
+
+  labelRenderer = new CSS2DRenderer()
+  labelRenderer.setSize(w, h)
+  labelRenderer.domElement.style.position = 'absolute'
+  labelRenderer.domElement.style.top = '0'
+  labelRenderer.domElement.style.left = '0'
+  labelRenderer.domElement.style.pointerEvents = 'none'
+  el.appendChild(labelRenderer.domElement)
+
+  controls = new OrbitControls(camera, renderer.domElement)
+  controls.enableDamping = true
+  controls.dampingFactor = 0.1
+  controls.target.set(0, 0, 0)
+  controls.update()
+
+  // 灯光
+  scene.add(new THREE.AmbientLight(0x334466, 0.6))
+  const dl = new THREE.DirectionalLight(0xffffcc, 1.2)
+  dl.position.set(200, 500, 300)
+  scene.add(dl)
+  const fl = new THREE.DirectionalLight(0x4488ff, 0.4)
+  fl.position.set(-200, 100, -200)
+  scene.add(fl)
+
+  // 地面网格
+  scene.add(new THREE.GridHelper(600, 20, 0x1a3a5c, 0x0d2a4a))
+
+  // 创建设备节点
+  nodes.forEach(n => {
+    const g = new THREE.Group()
+    // 底座
+    const base = new THREE.Mesh(
+      new THREE.BoxGeometry(50, 6, 50),
+      new THREE.MeshStandardMaterial({ color: 0x1a3050, roughness: 0.6, metalness: 0.3 })
+    )
+    base.position.y = 3; base.receiveShadow = true; base.castShadow = true
+    g.add(base)
+    // 主体
+    const body = new THREE.Mesh(
+      new THREE.BoxGeometry(36, 24, 36),
+      new THREE.MeshStandardMaterial({ color: n.color, emissive: 0x0088ff, emissiveIntensity: 0.15, roughness: 0.3, metalness: 0.5 })
+    )
+    body.position.y = 18; body.castShadow = true
+    g.add(body)
+    // 顶条
+    const rim = new THREE.Mesh(
+      new THREE.BoxGeometry(40, 2, 40),
+      new THREE.MeshStandardMaterial({ color: n.color, emissive: 0x0088ff, emissiveIntensity: 0.6 })
+    )
+    rim.position.y = 31
+    g.add(rim)
+    g.position.set(n.x, 0, n.z)
+    scene.add(g)
+  })
+
+  // 管道(Shader 流动线)
+  pipes.forEach(p => {
+    const a = nodeMap.get(p.from)!, b = nodeMap.get(p.to)!
+    const pts = buildPoints(a, b, p.from, p.to)
+    const line = createFlowLine(pts)
+    scene.add(line)
+    pipeMats.push((line as any)._pipeMat)
+    // 流动球 - 沿折线运动
+    const dot = new THREE.Mesh(
+      new THREE.SphereGeometry(4, 8, 8),
+      new THREE.MeshStandardMaterial({ color: 0x4dd0e1, emissive: 0x4dd0e1, emissiveIntensity: 2 })
+    )
+    dot.userData = { pts, t: Math.random(), speed: 0.3 + Math.random() * 0.2 }
+    scene.add(dot)
+    flowDots.push(dot)
+  })
+
+  // CSS2D 标签
+  nodes.forEach(n => {
+    const div = document.createElement('div')
+    div.style.cssText = 'text-align:center;pointer-events:none'
+    const t = document.createElement('div')
+    t.style.cssText = 'color:#fff;font:bold 13px "Microsoft YaHei",sans-serif;text-shadow:0 0 12px rgba(0,0,0,0.95)'
+    t.textContent = n.label
+    const v = document.createElement('div')
+    v.style.cssText = 'color:#4fc3f7;font:11px "Microsoft YaHei",sans-serif;text-shadow:0 0 8px rgba(0,0,0,0.9)'
+    v.textContent = n.value
+    div.appendChild(t)
+    div.appendChild(v)
+    const label = new CSS2DObject(div)
+    label.position.set(n.x, 42, n.z)
+    scene.add(label)
+  })
+
+  console.log('[H3D] 初始化完成, 节点:', nodes.length, '管道:', pipes.length)
+
+  window.addEventListener('resize', onResize)
+  animate()
+}
+
+function animate() {
+  animationId = requestAnimationFrame(animate)
+
+  const time = performance.now() * 0.001
+  pipeMats.forEach(mat => { mat.uniforms.uTime.value = time })
+
+  flowDots.forEach(dot => {
+    const { pts, t, speed } = dot.userData
+    const nt = (t + 0.004 * speed) % 1
+    dot.userData.t = nt
+    // 沿折线分段插值
+    const segCount = pts.length - 1
+    const seg = nt * segCount
+    const idx = Math.min(Math.floor(seg), segCount - 1)
+    const frac = seg - idx
+    const a = pts[idx], b = pts[idx + 1]
+    dot.position.set(
+      a.x + (b.x - a.x) * frac,
+      (a.y + (b.y - a.y) * frac) + 0.5,
+      a.z + (b.z - a.z) * frac,
+    )
+  })
+  controls.update()
+  renderer.render(scene, camera)
+  labelRenderer.render(scene, camera)
+}
+
+function onResize() {
+  if (!containerRef.value) return
+  const w = containerRef.value.clientWidth, h = containerRef.value.clientHeight
+  camera.aspect = w / h; camera.updateProjectionMatrix()
+  renderer.setSize(w, h); labelRenderer.setSize(w, h)
+}
+
+function disposeScene() {
+  window.removeEventListener('resize', onResize)
+  cancelAnimationFrame(animationId)
+  controls?.dispose()
+  renderer?.dispose()
+  if (labelRenderer) labelRenderer.domElement.remove()
+  pipeMats.length = 0
+  flowDots = []
+}
+
+onMounted(() => { try { initScene() } catch (e) { console.error('[H3D] error:', e) } })
+onUnmounted(() => disposeScene())
+</script>
+
+<template>
+  <div ref="containerRef" class="diagram-container">
+    <div class="header">
+      <span class="title">38团水利工程组态图</span>
+      <span class="hint">拖拽旋转 · 滚轮缩放</span>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.diagram-container {
+  width: 100%; height: 100%;
+  position: relative; overflow: hidden;
+  background: #0a1a2e;
+}
+.header {
+  position: absolute; top: 16px; left: 24px; z-index: 10;
+  display: flex; align-items: center; gap: 20px;
+}
+.title {
+  font: bold 18px "Microsoft YaHei", sans-serif;
+  color: #4dd0e1;
+  text-shadow: 0 0 20px rgba(77,208,225,0.3);
+}
+.hint {
+  color: rgba(255,255,255,0.35);
+  font: 12px "Microsoft YaHei", sans-serif;
+}
+</style>

+ 431 - 55
src/scenes/Map3DScene.vue

@@ -1,4 +1,4 @@
-<script setup lang="ts">
+<script setup lang="ts">
 import { onMounted, onUnmounted, ref } from 'vue'
 import * as THREE from 'three'
 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
@@ -6,16 +6,20 @@ 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 { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js'
+import { latLonToLocalENU, latLonToECEF, ecefToLatLon } from '../utils/geoCoord'
 import { createRimFlowMaterial } from '../materials/rimFlow'
 import { createTechFloorMaterial } from '../materials/techFloor'
 import xinjiangdibanGLB from '../assets/xinjiangdiban.glb'
+import markersConfig from '../config/markerConfig'
+import { useIconMarkers } from '../composables/useIconMarkers'
 
 const containerRef = ref<HTMLDivElement>()
 
 let scene: THREE.Scene
 let camera: THREE.PerspectiveCamera
 let renderer: THREE.WebGLRenderer
+let labelRenderer: CSS2DRenderer
 let controls: OrbitControls
 let composer: EffectComposer
 let animationId: number
@@ -23,9 +27,12 @@ let animationId: number
 let modelGroup: THREE.Group | null = null
 
 /** 存储标记点位置,供聚焦按钮使用 */
-const markerPositions: Record<string, THREE.Vector3> = {}
+let markerPositions: Record<string, THREE.Vector3> = {}
 const focusTarget = ref('原点')
 
+const iconMarkers = useIconMarkers(containerRef, ORIGIN_LAT, ORIGIN_LON)
+const { selectedMarker, popupStyle, closePopup } = iconMarkers
+
 // 缓动函数
 function easeInOutCubic(t: number): number {
   return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2
@@ -90,7 +97,11 @@ function initScene() {
     1,
     500000,
   )
-  camera.position.set(100000, 80000, 150000)
+  // 初始视角:往左转30度 + 拉近
+  camera.position.set(-48000, 50000, 97000)
+
+  // 注入 scene / camera 到 composable
+  iconMarkers.init(scene, camera)
 
   // Renderer
   renderer = new THREE.WebGLRenderer({
@@ -104,21 +115,32 @@ function initScene() {
   renderer.toneMappingExposure = 1.2
   containerRef.value.appendChild(renderer.domElement)
 
-  // Controls
+  // CSS2D Renderer(屏幕标签层)
+  labelRenderer = new CSS2DRenderer()
+  labelRenderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
+  labelRenderer.domElement.style.position = 'absolute'
+  labelRenderer.domElement.style.top = '0'
+  labelRenderer.domElement.style.left = '0'
+  labelRenderer.domElement.style.pointerEvents = 'none'  // 让点击穿透
+  containerRef.value.appendChild(labelRenderer.domElement)
+
+  // Controls — 右键旋转,禁止平移
   controls = new OrbitControls(camera, renderer.domElement)
   controls.enableDamping = true
   controls.dampingFactor = 0.1
+  controls.enablePan = false
+  controls.mouseButtons = { LEFT: null, MIDDLE: null, RIGHT: THREE.MOUSE.ROTATE }
   controls.target.set(0, 0, 0)
 
-  // Effect Composer (Bloom)
+  // 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
+    0.08,  // strength — 很弱,不干扰线条
+    0.2,   // radius
+    0.6,   // threshold
   )
   composer.addPass(bloomPass)
 
@@ -143,11 +165,26 @@ function initScene() {
   // ---------- 展示 geodesy 坐标转换结果 ----------
   addGeodesyTestMarkers()
 
+  // ---------- 渲染配置中的图标标记 ----------
+  markerPositions = iconMarkers.addIconMarkers()
+
+  // ---------- 创建点间的 Shader 光带 (直连) ----------
+  addConnectionLines()
+
+  // 禁止右键菜单
+  renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault())
+
   // Resize
   window.addEventListener('resize', onResize)
 
   // Start loop
   animate()
+
+  // 点击空白处关闭弹窗
+  containerRef.value.addEventListener('click', (e) => {
+    if ((e.target as HTMLElement).closest('.popup-content')) return
+    iconMarkers.closePopup()
+  })
 }
 
 /** 加载 xinjiangdiban.glb */
@@ -214,19 +251,197 @@ function createTechFloor() {
 
 let flowMaterials: THREE.ShaderMaterial[] = []
 
+/** 连线顺序:渠首→条形沉砂池→退水闸→2#沉砂池→1号沉砂池 */
+const CONNECTION_ORDER = ['渠首', '条形沉砂池', '退水闸', '2#沉砂池', '1#沉砂池']
+
+/** 第二条线:石门水库→莫勒切河→分水闸→1#沉砂池→团场 */
+const CONNECTION_ORDER_2 = ['石门水库', '莫勒切河', '分水闸', '1#沉砂池', '团场']
+
+// ---- 流动箭头光带 (Shader 实现) ----
+let arrowRibbonMaterials: THREE.ShaderMaterial[] = []
+
+const arrowVertexShader = `
+varying vec2 vUv;
+void main() {
+  vUv = uv;
+  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
+}
+`
+
+const arrowFragmentShader = `
+uniform float uTime;
+uniform vec3 uColor;
+
+varying vec2 vUv;
+
+void main() {
+  // vUv.x = 沿路径方向(0→1), vUv.y = 垂直方向(0→1, 中心0.5)
+
+  // 沿路径流动的脉冲波
+  float dx = vUv.x - uTime * 0.1;
+  float wave = sin(dx * 20.0) * 0.5 + 0.5;
+  // 细分脉冲波形成断续的段
+  float seg = floor(dx * 14.0);
+  float segPhase = fract(dx * 14.0);
+  float pulse = smoothstep(0.0, 0.05, segPhase) * (1.0 - smoothstep(0.4, 1.0, segPhase));
+  pulse *= 0.6 + 0.4 * wave;
+
+  // 垂直方向衰减 (中心亮, 边缘淡)
+  float dy = abs(vUv.y - 0.5) * 2.0;
+  float vertFalloff = 1.0 - smoothstep(0.0, 1.0, dy);
+
+  float alpha = pulse * vertFalloff * 0.7;
+  alpha = clamp(alpha, 0.0, 1.0);
+
+  vec3 color = uColor + vec3(0.2, 0.4, 0.6) * pulse;
+  gl_FragColor = vec4(color, alpha);
+}
+`
+
+/** 沿路径构建箭头光带 */
+function createFlowArrowRibbon(pts: THREE.Vector3[], width = 300, color = '#2196f3') {
+  if (pts.length < 2) return
+
+  const positions: number[] = []
+  const uvs: number[] = []
+
+  // 计算每段长度和总长
+  const segLens: number[] = []
+  let totalLen = 0
+  for (let i = 0; i < pts.length - 1; i++) {
+    const d = pts[i].distanceTo(pts[i + 1])
+    segLens.push(d)
+    totalLen += d
+  }
+
+  let uOffset = 0
+  for (let i = 0; i < pts.length - 1; i++) {
+    const p1 = pts[i]
+    const p2 = pts[i + 1]
+    const dir = new THREE.Vector3().copy(p2).sub(p1).normalize()
+    const up = new THREE.Vector3(0, 1, 0)
+    const side = new THREE.Vector3().crossVectors(dir, up)
+    if (side.length() < 0.001) {
+      side.crossVectors(dir, new THREE.Vector3(1, 0, 0))
+    }
+    side.normalize().multiplyScalar(width / 2)
+
+    const a = p1.clone().add(side)
+    const b = p1.clone().sub(side)
+    const c = p2.clone().add(side)
+    const d = p2.clone().sub(side)
+
+    const u0 = uOffset / totalLen
+    const u1 = (uOffset + segLens[i]) / totalLen
+
+    // 两个三角形: a-b-c, b-d-c
+    positions.push(a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z)
+    positions.push(b.x, b.y, b.z, d.x, d.y, d.z, c.x, c.y, c.z)
+    uvs.push(u0, 1, u0, 0, u1, 1)
+    uvs.push(u0, 0, u1, 0, u1, 1)
+
+    uOffset += segLens[i]
+  }
+
+  const geo = new THREE.BufferGeometry()
+  geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3))
+  geo.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2))
+
+  const mat = new THREE.ShaderMaterial({
+    uniforms: {
+      uTime: { value: 0 },
+      uColor: { value: new THREE.Color(color) },
+    },
+    vertexShader: arrowVertexShader,
+    fragmentShader: arrowFragmentShader,
+    transparent: true,
+    side: THREE.DoubleSide,
+    depthWrite: false,
+    depthTest: false,
+    blending: THREE.AdditiveBlending,
+  })
+
+  const mesh = new THREE.Mesh(geo, mat)
+  mesh.renderOrder = 1000
+  scene.add(mesh)
+  arrowRibbonMaterials.push(mat)
+}
+
+/** 在两点间构建一段矩形条 Mesh (粗线) */
+function buildLineSegment(p1: THREE.Vector3, p2: THREE.Vector3, width: number, color: number): THREE.Mesh {
+  const dir = new THREE.Vector3().copy(p2).sub(p1).normalize()
+  const up = new THREE.Vector3(0, 1, 0)
+  const side = new THREE.Vector3().crossVectors(dir, up)
+  if (side.length() < 0.001) side.crossVectors(dir, new THREE.Vector3(1, 0, 0))
+  side.normalize().multiplyScalar(width / 2)
+
+  const a = p1.clone().add(side)
+  const b = p1.clone().sub(side)
+  const c = p2.clone().add(side)
+  const d = p2.clone().sub(side)
+
+  const pos = new Float32Array([
+    a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z,
+    b.x, b.y, b.z, d.x, d.y, d.z, c.x, c.y, c.z,
+  ])
+  const geo = new THREE.BufferGeometry()
+  geo.setAttribute('position', new THREE.BufferAttribute(pos, 3))
+  const mat = new THREE.MeshBasicMaterial({
+    color,
+    side: THREE.DoubleSide,
+    depthWrite: false,
+    depthTest: false,
+  })
+  const mesh = new THREE.Mesh(geo, mat)
+  mesh.renderOrder = 999
+  return mesh
+}
+
+/** 通用:穿起指定标记点列表,画粗线 + 箭头光带 */
+function addRouteLine(order: string[], color: number, ribbonColor: string, ribbonWidth: number) {
+  const pts: THREE.Vector3[] = []
+  for (const label of order) {
+    const pos = markerPositions[label]
+    if (pos) {
+      const yOffset = (label === '渠首' || label === '条形沉砂池') ? 1200 : 500
+      pts.push(pos.clone().add(new THREE.Vector3(0, yOffset, 0)))
+    } else {
+      console.warn(`[连线] 未找到标记点: ${label}`)
+    }
+  }
+  if (pts.length < 2) return
+
+  // 每段之间画粗线
+  for (let i = 0; i < pts.length - 1; i++) {
+    scene.add(buildLineSegment(pts[i], pts[i + 1], 600, color))
+  }
+
+  // 箭头光带
+  createFlowArrowRibbon(pts, ribbonWidth, ribbonColor)
+}
+
+/** 创建所有线路 */
+function addConnectionLines() {
+  addRouteLine(CONNECTION_ORDER, 0x2196f3, '#2196f3', 800)
+  addRouteLine(CONNECTION_ORDER_2, 0x2196f3, '#2196f3', 800)
+  addRouteLine(['分水闸', '2#沉砂池'], 0x2196f3, '#2196f3', 800)
+}
+
 function animate() {
   animationId = requestAnimationFrame(animate)
 
-  // 更新流光材质的 uTime
-  if (flowMaterials.length > 0) {
-    const time = performance.now() * 0.001
-    flowMaterials.forEach((mat) => {
-      mat.uniforms.uTime.value = time
-    })
-  }
+  // 更新所有箭头光带的 uTime
+  const time = performance.now() * 0.001
+  arrowRibbonMaterials.forEach((mat) => {
+    mat.uniforms.uTime.value = time
+  })
 
   controls.update()
+
   composer.render()
+
+  labelRenderer.render(scene, camera)
+  iconMarkers.updatePopupPosition()
 }
 
 /**
@@ -323,6 +538,7 @@ function onResize() {
   camera.aspect = width / height
   camera.updateProjectionMatrix()
   renderer.setSize(width, height)
+  labelRenderer.setSize(width, height)
 }
 
 function disposeScene() {
@@ -331,6 +547,10 @@ function disposeScene() {
   controls.dispose()
   if (composer) composer.dispose()
   renderer.dispose()
+  iconMarkers.dispose()
+  if (labelRenderer) {
+    labelRenderer.domElement.remove()
+  }
   if (containerRef.value && renderer.domElement.parentElement) {
     containerRef.value.removeChild(renderer.domElement)
   }
@@ -347,23 +567,79 @@ onUnmounted(() => {
 
 <template>
   <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
+      v-if="selectedMarker"
+      class="popup-overlay"
+      :style="popupStyle"
+      @click.stop
+    >
+      <div class="popup-content">
+        <div class="popup-header">
+          <div class="popup-title">{{ selectedMarker.label }}</div>
+          <div v-if="selectedMarker.info" class="popup-info">{{ selectedMarker.info }}</div>
+          <button class="popup-close" @click="closePopup">✕</button>
+        </div>
+
+        <!-- 闸门监控面板 (有 gateData 才显示) -->
+        <template v-if="selectedMarker.gateData">
+          <div class="gate-panel">
+            <!-- 闸门开度 -->
+            <div class="gate-row">
+              <span class="gate-label">闸门开度</span>
+              <span class="gate-value">{{ selectedMarker.gateData.opening.current }}/{{ selectedMarker.gateData.opening.max }}{{ selectedMarker.gateData.opening.unit }}</span>
+              <div class="gate-bar-wrap">
+                <div class="gate-bar" :style="{ width: selectedMarker.gateData.opening.percent + '%' }"></div>
+              </div>
+              <span class="gate-sub">目标 {{ selectedMarker.gateData.opening.target }}{{ selectedMarker.gateData.opening.unit }}</span>
+            </div>
+
+            <!-- 运行状态 -->
+            <div class="gate-row">
+              <span class="gate-label">运行状态</span>
+              <span class="gate-value" :class="'status-' + selectedMarker.gateData.status">{{ selectedMarker.gateData.status }}</span>
+            </div>
+
+            <!-- 限位信号 -->
+            <div class="gate-row">
+              <span class="gate-label">限位信号</span>
+              <span class="gate-tag" :class="selectedMarker.gateData.limitSignals.upper ? 'tag-on' : 'tag-off'">上限</span>
+              <span class="gate-tag" :class="selectedMarker.gateData.limitSignals.lower ? 'tag-on' : 'tag-off'">下限</span>
+              <span class="gate-tag" :class="selectedMarker.gateData.limitSignals.emergency ? 'tag-on tag-danger' : 'tag-off'">极限</span>
+            </div>
+
+            <!-- 荷重/拉力 -->
+            <div class="gate-row">
+              <span class="gate-label">荷重/拉力</span>
+              <span class="gate-value" :class="{ 'text-warning': selectedMarker.gateData.load.warning }">{{ selectedMarker.gateData.load.current }} t</span>
+              <span v-if="selectedMarker.gateData.load.warning" class="gate-tag tag-danger">过载预警</span>
+              <span v-if="selectedMarker.gateData.load.protected" class="gate-tag tag-danger">过载保护</span>
+            </div>
+
+            <!-- 左右同步 -->
+            <div class="gate-row">
+              <span class="gate-label">左右同步</span>
+              <span class="gate-value" :class="'sync-' + selectedMarker.gateData.sync.status">{{ selectedMarker.gateData.sync.status }}</span>
+              <span class="gate-sub">偏差 {{ selectedMarker.gateData.sync.deviation }} mm</span>
+            </div>
+
+            <!-- 水位 -->
+            <div class="gate-row">
+              <span class="gate-label">水位</span>
+              <span class="gate-sub">上游 {{ selectedMarker.gateData.waterLevel.upstream }}{{ selectedMarker.gateData.waterLevel.unit }}</span>
+              <span class="gate-sub">下游 {{ selectedMarker.gateData.waterLevel.downstream }}{{ selectedMarker.gateData.waterLevel.unit }}</span>
+              <span class="gate-sub" v-if="selectedMarker.gateData.waterLevel.diff">差 {{ selectedMarker.gateData.waterLevel.diff }}{{ selectedMarker.gateData.waterLevel.unit }}</span>
+            </div>
+
+            <!-- 流量 -->
+            <div class="gate-row">
+              <span class="gate-label">流量</span>
+              <span class="gate-value">{{ selectedMarker.gateData.flow.realtime }} {{ selectedMarker.gateData.flow.unit }}</span>
+              <span class="gate-sub">累计 {{ selectedMarker.gateData.flow.accumulated }} {{ selectedMarker.gateData.flow.unit }}</span>
+            </div>
+          </div>
+        </template>
+      </div>
     </div>
 
   </div>
@@ -376,33 +652,133 @@ onUnmounted(() => {
   overflow: hidden;
   position: relative;
 }
-.toolbar {
+
+/* 弹窗样式 */
+.popup-overlay {
   position: absolute;
-  top: 16px;
-  left: 16px;
-  z-index: 10;
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8px;
+  z-index: 20;
+  transform: translate(-50%, -100%) translateY(-20px);
+  pointer-events: auto;
+}
+.popup-content {
+  position: relative;
+  background: rgba(10, 20, 40, 0.95);
+  border: 1px solid rgba(0, 150, 255, 0.5);
+  border-radius: 10px;
+  padding: 0;
+  min-width: 240px;
+  max-width: 260px;
+  backdrop-filter: blur(12px);
+  box-shadow: 0 4px 24px rgba(0,0,0,0.6), 0 0 20px rgba(0,150,255,0.15);
 }
-.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;
+.popup-header {
+  position: relative;
+  padding: 12px 36px 8px 14px;
+  border-bottom: 1px solid rgba(0,150,255,0.2);
+}
+.popup-title {
+  color: #64b5f6;
+  font: bold 14px "Microsoft YaHei", sans-serif;
+}
+.popup-info {
+  color: #aaa;
+  font: 11px "Microsoft YaHei", sans-serif;
+  margin-top: 2px;
+}
+.popup-close {
+  position: absolute;
+  top: 6px;
+  right: 8px;
+  background: none;
+  border: none;
+  color: #666;
   cursor: pointer;
-  font-size: 13px;
-  backdrop-filter: blur(4px);
-  transition: all 0.2s;
+  font-size: 16px;
+  padding: 2px 6px;
+  line-height: 1;
+  border-radius: 4px;
+  transition: all 0.15s;
 }
-.btn:hover {
-  background: rgba(255,255,255,0.15);
+.popup-close:hover {
   color: #fff;
+  background: rgba(255,255,255,0.1);
 }
-.btn.active {
-  background: #2196f3;
-  border-color: #2196f3;
-  color: #fff;
+
+/* 闸门监控面板 */
+.gate-panel {
+  padding: 8px 14px 12px;
+}
+.gate-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 5px 0;
+  border-bottom: 1px solid rgba(255,255,255,0.04);
+  flex-wrap: wrap;
 }
+.gate-row:last-child {
+  border-bottom: none;
+}
+.gate-label {
+  color: #8899aa;
+  font: 11px "Microsoft YaHei", sans-serif;
+  min-width: 62px;
+  flex-shrink: 0;
+}
+.gate-value {
+  color: #8cf;
+  font: 12px/1.4 "Microsoft YaHei", sans-serif;
+  font-weight: bold;
+}
+.gate-sub {
+  color: #778;
+  font: 10px "Microsoft YaHei", sans-serif;
+}
+.gate-bar-wrap {
+  flex: 1;
+  min-width: 40px;
+  height: 4px;
+  background: rgba(255,255,255,0.08);
+  border-radius: 2px;
+  overflow: hidden;
+}
+.gate-bar {
+  height: 100%;
+  background: linear-gradient(90deg, #2196f3, #64b5f6);
+  border-radius: 2px;
+  transition: width 0.3s;
+}
+.gate-tag {
+  font: 10px "Microsoft YaHei", sans-serif;
+  padding: 1px 6px;
+  border-radius: 3px;
+  border: 1px solid transparent;
+}
+.tag-on {
+  color: #4caf50;
+  border-color: #4caf50;
+  background: rgba(76,175,80,0.1);
+}
+.tag-off {
+  color: #555;
+  border-color: #444;
+  background: rgba(255,255,255,0.03);
+}
+.tag-danger {
+  color: #f44336 !important;
+  border-color: #f44336 !important;
+  background: rgba(244,67,54,0.1) !important;
+}
+
+/* 状态颜色 */
+.status-稳定, .status-停止 { color: #4caf50; }
+.status-正在开启 { color: #2196f3; }
+.status-正在关闭 { color: #ff9800; }
+.status-卡滞 { color: #f44336; }
+
+.sync-同步 { color: #4caf50; }
+.sync-偏差 { color: #ff9800; }
+.sync-异常 { color: #f44336; }
+
+.text-warning { color: #ff9800 !important; }
 </style>

+ 1 - 2
src/scenes/Scene3D.vue

@@ -935,8 +935,7 @@ function initScene() {
   depthRenderTarget.depthTexture = new THREE.DepthTexture(pixelWidth, pixelHeight)
 
   controls = new OrbitControls(camera, renderer.domElement)
-  controls.enableDamping = true
-  controls.dampingFactor = 0.03
+  controls.enableDamping = false
   controls.screenSpacePanning = false
   controls.minDistance = 0
   controls.maxDistance = Infinity

+ 0 - 129
test-coordinate.html

@@ -1,129 +0,0 @@
-<!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>

+ 0 - 121
test-project.html

@@ -1,121 +0,0 @@
-<!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>

+ 0 - 235
test-rotation.html

@@ -1,235 +0,0 @@
-<!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>

+ 0 - 131
test-simple-rotation.html

@@ -1,131 +0,0 @@
-<!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>

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

@@ -1,123 +0,0 @@
-# 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. 为不同类型的模型添加预设转换配置

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

@@ -1,120 +0,0 @@
-# 模型上下颠倒解决方案
-
-## 问题描述
-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`: 简单旋转测试指南