Scene3D.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. <script setup lang="ts">
  2. import { onMounted, onUnmounted, ref, watch } from 'vue'
  3. import * as THREE from 'three'
  4. import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
  5. import { Sky } from 'three/examples/jsm/objects/Sky.js'
  6. import { Water, createWaterNormalTexture } from './Water'
  7. import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
  8. const containerRef = ref<HTMLDivElement>()
  9. const pickedPosition = ref<{ x: number; y: number; z: number } | null>(null)
  10. const cameraInfo = ref({
  11. position: { x: 0, y: 0, z: 0 },
  12. target: { x: 0, y: 0, z: 0 },
  13. distance: 0,
  14. minDistance: 0,
  15. maxDistance: 0,
  16. })
  17. const waterParams = ref({
  18. alpha: 0.65,
  19. sunColor: '#ffffff',
  20. waterColor: '#78a6a0',
  21. distortionScale: 30.0,
  22. noiseScale: 1.0,
  23. fresnelBias: 0.15,
  24. fresnelPower: 4.0,
  25. fresnelStrength: 1.0,
  26. flowSpeed: 0.5,
  27. flowDirectionX: 1.0,
  28. flowDirectionY: 1.0,
  29. })
  30. const showCoordinatePanel = ref(false)
  31. const showCameraPanel = ref(false)
  32. const showWaterPanel = ref(false)
  33. let scene: THREE.Scene
  34. let camera: THREE.PerspectiveCamera
  35. let renderer: THREE.WebGLRenderer
  36. let controls: OrbitControls
  37. let sky: Sky
  38. let water: Water
  39. let waterMesh: THREE.Mesh
  40. let sunDirection: THREE.Vector3
  41. let animationId: number
  42. let tilesetGroup: THREE.Group | null = null
  43. let raycaster: THREE.Raycaster
  44. let mouse: THREE.Vector2
  45. function createSky() {
  46. sky = new Sky()
  47. sky.scale.setScalar(50000)
  48. scene.add(sky)
  49. const uniforms = sky.material.uniforms
  50. uniforms.turbidity.value = 6
  51. uniforms.rayleigh.value = 0.1
  52. uniforms.mieCoefficient.value = 0.005
  53. uniforms.mieDirectionalG.value = 0.7
  54. uniforms.sunPosition.value.set(20, 30, 10)
  55. uniforms.cloudScale.value = 0.0008
  56. uniforms.cloudSpeed.value = 0.00015
  57. uniforms.cloudCoverage.value = 0.6
  58. uniforms.cloudDensity.value = 0.6
  59. uniforms.cloudElevation.value = 0.9
  60. }
  61. function createWaterSurface() {
  62. const textureLoader = new THREE.TextureLoader()
  63. const waterNormals = createWaterNormalTexture(textureLoader)
  64. water = new Water(renderer, camera, scene, {
  65. textureWidth: 512,
  66. textureHeight: 512,
  67. waterNormals,
  68. alpha: waterParams.value.alpha,
  69. sunDirection,
  70. sunColor: new THREE.Color(waterParams.value.sunColor),
  71. waterColor: new THREE.Color(waterParams.value.waterColor),
  72. distortionScale: waterParams.value.distortionScale,
  73. noiseScale: waterParams.value.noiseScale,
  74. fresnelBias: waterParams.value.fresnelBias,
  75. fresnelPower: waterParams.value.fresnelPower,
  76. fresnelStrength: waterParams.value.fresnelStrength,
  77. flowDirection: new THREE.Vector2(waterParams.value.flowDirectionX, waterParams.value.flowDirectionY),
  78. side: THREE.DoubleSide,
  79. fog: true,
  80. })
  81. waterMesh = new THREE.Mesh(
  82. new THREE.PlaneGeometry(200, 200, 60, 60),
  83. water.material
  84. )
  85. waterMesh.add(water)
  86. waterMesh.rotation.x = -Math.PI / 2
  87. waterMesh.position.set(840.85714, 7.47851, 2179.50782)
  88. waterMesh.receiveShadow = true
  89. scene.add(waterMesh)
  90. }
  91. async function load3DTiles() {
  92. const tilesetUrl = 'http://localhost:9003/model/scence/tileset.json'
  93. const gltfLoader = new GLTFLoader()
  94. try {
  95. const response = await fetch(tilesetUrl)
  96. const tileset = await response.json()
  97. console.log('Tileset loaded:', tileset)
  98. console.log('Root tile:', tileset.root)
  99. console.log('Root tile content:', tileset.root?.content)
  100. console.log('Root tile children count:', tileset.root?.children?.length)
  101. tilesetGroup = new THREE.Group()
  102. async function loadTile(tile: any, parentGroup: THREE.Group, depth: number = 0): Promise<void> {
  103. console.log(`${' '.repeat(depth)}Processing tile at depth ${depth}`)
  104. console.log(`${' '.repeat(depth)}Tile content:`, tile.content)
  105. console.log(`${' '.repeat(depth)}Tile has transform:`, !!tile.transform)
  106. if (tile.content && tile.content.uri) {
  107. const tileUrl = new URL(tile.content.uri, tilesetUrl).href
  108. console.log(`${' '.repeat(depth)}Loading tile from: ${tileUrl}`)
  109. try {
  110. const response = await fetch(tileUrl, { method: 'HEAD' })
  111. if (!response.ok) {
  112. throw new Error(`HTTP ${response.status}: ${response.statusText}`)
  113. }
  114. console.log(`${' '.repeat(depth)}File exists, content-type: ${response.headers.get('content-type')}`)
  115. const gltf = await new Promise((resolve, reject) => {
  116. gltfLoader.load(
  117. tileUrl,
  118. (gltf) => resolve(gltf),
  119. (progress) => {
  120. if (progress.lengthComputable) {
  121. const percentComplete = (progress.loaded / progress.total) * 100
  122. console.log(`${' '.repeat(depth)}Loading progress: ${percentComplete.toFixed(2)}%`)
  123. }
  124. },
  125. (error) => {
  126. console.error(`${' '.repeat(depth)}GLTFLoader error:`, error)
  127. reject(error)
  128. }
  129. )
  130. }) as { scene: THREE.Group }
  131. if (gltf && gltf.scene) {
  132. const mesh = gltf.scene.clone()
  133. console.log(`${' '.repeat(depth)}GLB scene children count:`, mesh.children.length)
  134. console.log(`${' '.repeat(depth)}GLB scene:`, mesh)
  135. mesh.traverse((child) => {
  136. if (child instanceof THREE.Mesh) {
  137. console.log(`${' '.repeat(depth)}Found mesh:`, child.name)
  138. console.log(`${' '.repeat(depth)}Mesh geometry:`, child.geometry)
  139. console.log(`${' '.repeat(depth)}Mesh material:`, child.material)
  140. console.log(`${' '.repeat(depth)}Mesh position:`, child.position)
  141. console.log(`${' '.repeat(depth)}Mesh visible:`, child.visible)
  142. }
  143. })
  144. if (tile.transform) {
  145. const matrix = new THREE.Matrix4().fromArray(tile.transform)
  146. mesh.applyMatrix4(matrix)
  147. }
  148. parentGroup.add(mesh)
  149. console.log(`${' '.repeat(depth)}Successfully loaded tile: ${tile.content.uri}`)
  150. }
  151. } catch (error) {
  152. console.warn(`${' '.repeat(depth)}Error loading tile:`, tile.content.uri, error)
  153. }
  154. }
  155. if (tile.children && tile.children.length > 0) {
  156. console.log(`${' '.repeat(depth)}Tile has ${tile.children.length} children`)
  157. for (const child of tile.children) {
  158. await loadTile(child, parentGroup, depth + 1)
  159. }
  160. }
  161. }
  162. if (tileset.root) {
  163. await loadTile(tileset.root, tilesetGroup)
  164. }
  165. console.log('Tileset group children count:', tilesetGroup.children.length)
  166. scene.add(tilesetGroup)
  167. const boundingBox = new THREE.Box3().setFromObject(tilesetGroup)
  168. const center = boundingBox.getCenter(new THREE.Vector3())
  169. const size = boundingBox.getSize(new THREE.Vector3())
  170. const maxDim = Math.max(size.x, size.y, size.z)
  171. console.log('Tileset bounding box center:', center)
  172. console.log('Tileset size:', size)
  173. if (maxDim > 0) {
  174. tilesetGroup.position.copy(center).multiplyScalar(-1)
  175. waterMesh.position.set(840.85714, 7.47851, 2179.50782)
  176. controls.target.set(842.3117, 9.27789, 2178.09268)
  177. controls.update()
  178. console.log('Tileset positioned')
  179. }
  180. } catch (error) {
  181. console.error('Error loading 3D Tiles:', error)
  182. console.error('Error details:', (error as Error).message)
  183. }
  184. }
  185. function initScene() {
  186. const container = containerRef.value!
  187. scene = new THREE.Scene()
  188. scene.fog = new THREE.FogExp2(0xd4d4d4, 0.004)
  189. camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 100000)
  190. camera.position.set(838.15445, 22.22044, 2209.96548)
  191. renderer = new THREE.WebGLRenderer({ antialias: true })
  192. renderer.setSize(container.clientWidth, container.clientHeight)
  193. renderer.domElement.style.width = '100%'
  194. renderer.domElement.style.height = '100%'
  195. renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  196. renderer.toneMapping = THREE.ACESFilmicToneMapping
  197. renderer.toneMappingExposure = 1.0
  198. renderer.shadowMap.enabled = true
  199. renderer.shadowMap.type = THREE.PCFShadowMap
  200. container.appendChild(renderer.domElement)
  201. controls = new OrbitControls(camera, renderer.domElement)
  202. controls.enableDamping = true
  203. controls.dampingFactor = 0.03
  204. controls.screenSpacePanning = false
  205. controls.minDistance = 0
  206. controls.maxDistance = Infinity
  207. controls.mouseButtons = {
  208. LEFT: THREE.MOUSE.PAN,
  209. MIDDLE: THREE.MOUSE.DOLLY,
  210. RIGHT: THREE.MOUSE.ROTATE,
  211. }
  212. controls.target.set(842.3117, 9.27789, 2178.09268)
  213. controls.maxPolarAngle = Math.PI / 2.1
  214. controls.minDistance = 20
  215. controls.maxDistance = 100
  216. createSky()
  217. const hemisphereLight = new THREE.HemisphereLight(0xd4d4d4, 0x3d6b4a, 0.6)
  218. scene.add(hemisphereLight)
  219. sunDirection = new THREE.Vector3(20, 30, 10).normalize()
  220. const sunLight = new THREE.DirectionalLight(0xffeedd, 2.0)
  221. sunLight.position.set(20, 30, 10)
  222. sunLight.castShadow = true
  223. sunLight.shadow.mapSize.width = 2048
  224. sunLight.shadow.mapSize.height = 2048
  225. sunLight.shadow.camera.near = 0.5
  226. sunLight.shadow.camera.far = 60
  227. sunLight.shadow.camera.left = -20
  228. sunLight.shadow.camera.right = 20
  229. sunLight.shadow.camera.top = 20
  230. sunLight.shadow.camera.bottom = -20
  231. scene.add(sunLight)
  232. createWaterSurface()
  233. load3DTiles()
  234. initRaycaster()
  235. animate()
  236. }
  237. function initRaycaster() {
  238. raycaster = new THREE.Raycaster()
  239. mouse = new THREE.Vector2()
  240. const container = containerRef.value!
  241. container.addEventListener('click', onMouseClick)
  242. }
  243. function onMouseClick(event: MouseEvent) {
  244. const container = containerRef.value!
  245. const rect = container.getBoundingClientRect()
  246. mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
  247. mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
  248. raycaster.setFromCamera(mouse, camera)
  249. const intersectObjects: THREE.Object3D[] = []
  250. if (tilesetGroup) {
  251. intersectObjects.push(tilesetGroup)
  252. }
  253. if (waterMesh) {
  254. intersectObjects.push(waterMesh)
  255. }
  256. const intersects = raycaster.intersectObjects(intersectObjects, true)
  257. if (intersects.length > 0) {
  258. const point = intersects[0].point
  259. pickedPosition.value = {
  260. x: Number((point.x * 100).toFixed(3)),
  261. y: Number((point.y * 100).toFixed(3)),
  262. z: Number((point.z * 100).toFixed(3))
  263. }
  264. console.log('Picked position (cm):', pickedPosition.value)
  265. }
  266. }
  267. function animate() {
  268. animationId = requestAnimationFrame(animate)
  269. sky.material.uniforms.time.value += 0.001
  270. water.material.uniforms.time.value += 0.005 * waterParams.value.flowSpeed
  271. water.render()
  272. controls.update()
  273. cameraInfo.value.position = {
  274. x: Number((camera.position.x * 100).toFixed(3)),
  275. y: Number((camera.position.y * 100).toFixed(3)),
  276. z: Number((camera.position.z * 100).toFixed(3)),
  277. }
  278. cameraInfo.value.target = {
  279. x: Number((controls.target.x * 100).toFixed(3)),
  280. y: Number((controls.target.y * 100).toFixed(3)),
  281. z: Number((controls.target.z * 100).toFixed(3)),
  282. }
  283. cameraInfo.value.distance = Number(camera.position.distanceTo(controls.target).toFixed(3))
  284. cameraInfo.value.minDistance = controls.minDistance
  285. cameraInfo.value.maxDistance = controls.maxDistance
  286. renderer.render(scene, camera)
  287. }
  288. function onResize() {
  289. if (!containerRef.value) return
  290. const width = containerRef.value.clientWidth
  291. const height = containerRef.value.clientHeight
  292. camera.aspect = width / height
  293. camera.updateProjectionMatrix()
  294. renderer.setSize(width, height)
  295. }
  296. onMounted(() => {
  297. initScene()
  298. window.addEventListener('resize', onResize)
  299. })
  300. onUnmounted(() => {
  301. cancelAnimationFrame(animationId)
  302. window.removeEventListener('resize', onResize)
  303. if (containerRef.value) {
  304. containerRef.value.removeEventListener('click', onMouseClick)
  305. }
  306. if (tilesetGroup) {
  307. scene.remove(tilesetGroup)
  308. }
  309. renderer.dispose()
  310. controls.dispose()
  311. })
  312. watch(() => waterParams.value, (newParams) => {
  313. if (water && water.material && water.material.uniforms) {
  314. water.material.uniforms.alpha.value = newParams.alpha
  315. water.material.uniforms.sunColor.value.set(newParams.sunColor)
  316. water.material.uniforms.waterColor.value.set(newParams.waterColor)
  317. water.material.uniforms.distortionScale.value = newParams.distortionScale
  318. water.material.uniforms.noiseScale.value = newParams.noiseScale
  319. water.material.uniforms.fresnelBias.value = newParams.fresnelBias
  320. water.material.uniforms.fresnelPower.value = newParams.fresnelPower
  321. water.material.uniforms.fresnelStrength.value = newParams.fresnelStrength
  322. water.material.uniforms.flowDirection.value.set(newParams.flowDirectionX, newParams.flowDirectionY)
  323. }
  324. }, { deep: true })
  325. </script>
  326. <template>
  327. <div ref="containerRef" class="scene-container" />
  328. <div class="toolbar">
  329. <button class="toolbar-btn" :class="{ active: showCoordinatePanel }" @click="showCoordinatePanel = !showCoordinatePanel">坐标</button>
  330. <button class="toolbar-btn" :class="{ active: showCameraPanel }" @click="showCameraPanel = !showCameraPanel">相机</button>
  331. <button class="toolbar-btn" :class="{ active: showWaterPanel }" @click="showWaterPanel = !showWaterPanel">水面</button>
  332. </div>
  333. <div v-if="showCoordinatePanel && pickedPosition" class="panel coordinate-panel">
  334. <div class="panel-header">
  335. <span class="panel-title">拾取坐标 (cm)</span>
  336. <button class="toggle-btn" @click="showCoordinatePanel = false">×</button>
  337. </div>
  338. <div class="coordinate-item">
  339. <span class="coordinate-label">X:</span>
  340. <span class="coordinate-value">{{ pickedPosition.x }}</span>
  341. </div>
  342. <div class="coordinate-item">
  343. <span class="coordinate-label">Y:</span>
  344. <span class="coordinate-value">{{ pickedPosition.y }}</span>
  345. </div>
  346. <div class="coordinate-item">
  347. <span class="coordinate-label">Z:</span>
  348. <span class="coordinate-value">{{ pickedPosition.z }}</span>
  349. </div>
  350. </div>
  351. <div v-if="showCameraPanel" class="panel camera-panel">
  352. <div class="panel-header">
  353. <span class="panel-title">相机信息</span>
  354. <button class="toggle-btn" @click="showCameraPanel = false">×</button>
  355. </div>
  356. <div class="camera-section">
  357. <div class="section-label">位置 (cm)</div>
  358. <div class="camera-item">
  359. <span class="camera-label">X:</span>
  360. <span class="camera-value">{{ cameraInfo.position.x }}</span>
  361. </div>
  362. <div class="camera-item">
  363. <span class="camera-label">Y:</span>
  364. <span class="camera-value">{{ cameraInfo.position.y }}</span>
  365. </div>
  366. <div class="camera-item">
  367. <span class="camera-label">Z:</span>
  368. <span class="camera-value">{{ cameraInfo.position.z }}</span>
  369. </div>
  370. </div>
  371. <div class="camera-section">
  372. <div class="section-label">目标点 (cm)</div>
  373. <div class="camera-item">
  374. <span class="camera-label">X:</span>
  375. <span class="camera-value">{{ cameraInfo.target.x }}</span>
  376. </div>
  377. <div class="camera-item">
  378. <span class="camera-label">Y:</span>
  379. <span class="camera-value">{{ cameraInfo.target.y }}</span>
  380. </div>
  381. <div class="camera-item">
  382. <span class="camera-label">Z:</span>
  383. <span class="camera-value">{{ cameraInfo.target.z }}</span>
  384. </div>
  385. </div>
  386. <div class="camera-section">
  387. <div class="section-label">缩放距离 (m)</div>
  388. <div class="camera-item">
  389. <span class="camera-label">当前:</span>
  390. <span class="camera-value">{{ cameraInfo.distance }}</span>
  391. </div>
  392. <div class="camera-item">
  393. <span class="camera-label">最近:</span>
  394. <span class="camera-value">{{ cameraInfo.minDistance }}m</span>
  395. </div>
  396. <div class="camera-item">
  397. <span class="camera-label">最远:</span>
  398. <span class="camera-value">{{ cameraInfo.maxDistance }}m</span>
  399. </div>
  400. </div>
  401. </div>
  402. <div v-if="showWaterPanel" class="panel water-panel">
  403. <div class="panel-header">
  404. <span class="panel-title">水面参数调节</span>
  405. <button class="toggle-btn" @click="showWaterPanel = false">×</button>
  406. </div>
  407. <div class="water-section">
  408. <div class="section-label">透明度</div>
  409. <div class="slider-item">
  410. <input type="range" v-model.number="waterParams.alpha" min="0" max="1" step="0.01" />
  411. <span class="slider-value">{{ waterParams.alpha.toFixed(2) }}</span>
  412. </div>
  413. </div>
  414. <div class="water-section">
  415. <div class="section-label">水流速度</div>
  416. <div class="slider-item">
  417. <input type="range" v-model.number="waterParams.flowSpeed" min="0" max="3" step="0.1" />
  418. <span class="slider-value">{{ waterParams.flowSpeed.toFixed(1) }}</span>
  419. </div>
  420. </div>
  421. <div class="water-section">
  422. <div class="section-label">流向 X</div>
  423. <div class="slider-item">
  424. <input type="range" v-model.number="waterParams.flowDirectionX" min="-2" max="2" step="0.1" />
  425. <span class="slider-value">{{ waterParams.flowDirectionX.toFixed(1) }}</span>
  426. </div>
  427. </div>
  428. <div class="water-section">
  429. <div class="section-label">流向 Z</div>
  430. <div class="slider-item">
  431. <input type="range" v-model.number="waterParams.flowDirectionY" min="-2" max="2" step="0.1" />
  432. <span class="slider-value">{{ waterParams.flowDirectionY.toFixed(1) }}</span>
  433. </div>
  434. </div>
  435. <div class="water-section">
  436. <div class="section-label">水颜色</div>
  437. <div class="color-item">
  438. <input type="color" v-model="waterParams.waterColor" />
  439. <span class="color-value">{{ waterParams.waterColor }}</span>
  440. </div>
  441. </div>
  442. <div class="water-section">
  443. <div class="section-label">太阳光颜色</div>
  444. <div class="color-item">
  445. <input type="color" v-model="waterParams.sunColor" />
  446. <span class="color-value">{{ waterParams.sunColor }}</span>
  447. </div>
  448. </div>
  449. <div class="water-section">
  450. <div class="section-label">波纹强度</div>
  451. <div class="slider-item">
  452. <input type="range" v-model.number="waterParams.distortionScale" min="0" max="100" step="1" />
  453. <span class="slider-value">{{ waterParams.distortionScale.toFixed(0) }}</span>
  454. </div>
  455. </div>
  456. <div class="water-section">
  457. <div class="section-label">噪声缩放</div>
  458. <div class="slider-item">
  459. <input type="range" v-model.number="waterParams.noiseScale" min="0.1" max="5" step="0.1" />
  460. <span class="slider-value">{{ waterParams.noiseScale.toFixed(1) }}</span>
  461. </div>
  462. </div>
  463. <div class="water-section">
  464. <div class="section-label">菲涅尔偏移</div>
  465. <div class="slider-item">
  466. <input type="range" v-model.number="waterParams.fresnelBias" min="0" max="1" step="0.01" />
  467. <span class="slider-value">{{ waterParams.fresnelBias.toFixed(2) }}</span>
  468. </div>
  469. </div>
  470. <div class="water-section">
  471. <div class="section-label">菲涅尔功率</div>
  472. <div class="slider-item">
  473. <input type="range" v-model.number="waterParams.fresnelPower" min="0.1" max="10" step="0.1" />
  474. <span class="slider-value">{{ waterParams.fresnelPower.toFixed(1) }}</span>
  475. </div>
  476. </div>
  477. <div class="water-section">
  478. <div class="section-label">菲涅尔强度</div>
  479. <div class="slider-item">
  480. <input type="range" v-model.number="waterParams.fresnelStrength" min="0" max="3" step="0.1" />
  481. <span class="slider-value">{{ waterParams.fresnelStrength.toFixed(1) }}</span>
  482. </div>
  483. </div>
  484. </div>
  485. </template>
  486. <style scoped>
  487. .scene-container {
  488. position: fixed;
  489. top: 0;
  490. left: 0;
  491. width: 100vw;
  492. height: 100vh;
  493. overflow: hidden;
  494. }
  495. .toolbar {
  496. position: fixed;
  497. top: 20px;
  498. right: 20px;
  499. display: flex;
  500. flex-direction: column;
  501. gap: 8px;
  502. z-index: 1001;
  503. }
  504. .toolbar-btn {
  505. padding: 10px 16px;
  506. border: none;
  507. border-radius: 8px;
  508. background: rgba(0, 0, 0, 0.85);
  509. color: #888;
  510. font-size: 13px;
  511. font-weight: bold;
  512. cursor: pointer;
  513. transition: all 0.2s;
  514. min-width: 70px;
  515. }
  516. .toolbar-btn:hover {
  517. background: rgba(30, 30, 30, 0.9);
  518. color: #fff;
  519. }
  520. .toolbar-btn.active {
  521. color: #fff;
  522. box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
  523. }
  524. .toolbar-btn:nth-child(1).active { color: #4fc3f7; }
  525. .toolbar-btn:nth-child(2).active { color: #ff9800; }
  526. .toolbar-btn:nth-child(3).active { color: #4dd0e1; }
  527. .panel {
  528. position: fixed;
  529. top: 20px;
  530. left: 20px;
  531. background: rgba(0, 0, 0, 0.85);
  532. color: white;
  533. padding: 15px;
  534. border-radius: 8px;
  535. font-family: 'Courier New', monospace;
  536. z-index: 1000;
  537. max-height: calc(100vh - 40px);
  538. overflow-y: auto;
  539. }
  540. .coordinate-panel { min-width: 200px; }
  541. .camera-panel { min-width: 200px; }
  542. .water-panel { min-width: 240px; }
  543. .panel-header {
  544. display: flex;
  545. justify-content: space-between;
  546. align-items: center;
  547. margin-bottom: 12px;
  548. padding-bottom: 8px;
  549. border-bottom: 1px solid rgba(255, 255, 255, 0.3);
  550. }
  551. .panel-title {
  552. font-weight: bold;
  553. font-size: 14px;
  554. }
  555. .coordinate-panel .panel-title { color: #4fc3f7; }
  556. .camera-panel .panel-title { color: #ff9800; }
  557. .water-panel .panel-title { color: #4dd0e1; }
  558. .toggle-btn {
  559. width: 22px;
  560. height: 22px;
  561. border: none;
  562. border-radius: 4px;
  563. background: rgba(255, 255, 255, 0.15);
  564. color: white;
  565. font-size: 14px;
  566. line-height: 1;
  567. cursor: pointer;
  568. display: flex;
  569. align-items: center;
  570. justify-content: center;
  571. transition: background 0.2s;
  572. }
  573. .toggle-btn:hover {
  574. background: rgba(255, 255, 255, 0.3);
  575. }
  576. .coordinate-item {
  577. display: flex;
  578. justify-content: space-between;
  579. margin: 5px 0;
  580. }
  581. .coordinate-label {
  582. color: #81c784;
  583. font-weight: bold;
  584. }
  585. .coordinate-value {
  586. color: #fff;
  587. }
  588. .camera-section {
  589. margin-bottom: 10px;
  590. }
  591. .section-label {
  592. color: #81c784;
  593. font-size: 11px;
  594. margin-bottom: 5px;
  595. font-weight: bold;
  596. }
  597. .camera-item {
  598. display: flex;
  599. justify-content: space-between;
  600. margin: 3px 0;
  601. }
  602. .camera-label {
  603. color: #4fc3f7;
  604. }
  605. .camera-value {
  606. color: #fff;
  607. }
  608. .water-section {
  609. margin-bottom: 12px;
  610. }
  611. .water-section .section-label {
  612. color: #81c784;
  613. font-size: 11px;
  614. margin-bottom: 6px;
  615. font-weight: bold;
  616. }
  617. .slider-item {
  618. display: flex;
  619. align-items: center;
  620. gap: 10px;
  621. }
  622. .slider-item input[type="range"] {
  623. flex: 1;
  624. height: 6px;
  625. -webkit-appearance: none;
  626. background: #444;
  627. border-radius: 3px;
  628. outline: none;
  629. }
  630. .slider-item input[type="range"]::-webkit-slider-thumb {
  631. -webkit-appearance: none;
  632. width: 14px;
  633. height: 14px;
  634. background: #4dd0e1;
  635. border-radius: 50%;
  636. cursor: pointer;
  637. }
  638. .slider-value {
  639. min-width: 45px;
  640. text-align: right;
  641. color: #fff;
  642. font-size: 11px;
  643. }
  644. .color-item {
  645. display: flex;
  646. align-items: center;
  647. gap: 10px;
  648. }
  649. .color-item input[type="color"] {
  650. width: 40px;
  651. height: 28px;
  652. border: none;
  653. border-radius: 4px;
  654. cursor: pointer;
  655. background: transparent;
  656. }
  657. .color-item input[type="color"]::-webkit-color-swatch-wrapper {
  658. padding: 0;
  659. }
  660. .color-item input[type="color"]::-webkit-color-swatch {
  661. border: 1px solid rgba(255, 255, 255, 0.3);
  662. border-radius: 4px;
  663. }
  664. .color-value {
  665. color: #fff;
  666. font-size: 11px;
  667. }
  668. </style>