Browse Source

fix

master
茹嘉辉 1 month ago
parent
commit
a6ddcd4a35
4 changed files with 871 additions and 51 deletions
  1. +644
    -20
      src/App.vue
  2. +30
    -1
      src/store.ts
  3. +121
    -17
      src/wayline.ts
  4. +76
    -13
      src/waypoint.ts

+ 644
- 20
src/App.vue View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { onMounted, watch, ref } from 'vue'
import { Viewer, ScreenSpaceEventHandler, ScreenSpaceEventType, defined, Cartographic, sampleTerrainMostDetailed, Cartesian3, Math as cesiumMath, Ion } from 'cesium'
import { Wayline } from './wayline'
import { useStore } from './store'
@ -7,6 +7,142 @@ import { createViewer } from './basemap'
import * as egm96 from 'egm96-universal'
const store = useStore()
const isDraggingSlider = ref(false)
const isMinimapExpanded = ref(false)
//
declare global {
interface Window {
wayline?: Wayline
}
}
//
watch(() => store.lensMode, (newMode) => {
if (window.wayline) {
const minimapViewer = window.wayline.minimapViewer
if (minimapViewer) {
function fovFromZoom(zoom: number, fov1: number = 1.3418): number {
const k = Math.tan(fov1 / 2)
return 2 * Math.atan(k / zoom)
}
if (newMode === 'wide') {
// 广1x
const currentFov = fovFromZoom(1)
store.updateZoomFactor(1)
minimapViewer.camera.frustum.fov = currentFov
} else {
// 1x1x
if (store.zoomFactor === 1) {
const currentFov = fovFromZoom(1)
minimapViewer.camera.frustum.fov = currentFov
}
}
}
}
})
//
watch(() => store.zoomFactor, () => {
// getSliderPosition()
})
//
function getSliderPosition(): number {
if (store.lensMode === 'wide') {
return 0 // 广1x
}
// 1x(0%)56x(100%)
const minZoom = 1
const maxZoom = 56
const zoom = store.zoomFactor
const percentage = ((zoom - minZoom) / (maxZoom - minZoom)) * 100
// bottom: 0% => 1x, bottom: 100% => 56x
return percentage
}
function zoomFromPosition(percentage: number): number {
// percentage: 0% = (1x), 100% = (56x)
const minZoom = 1
const maxZoom = 56
const zoom = minZoom + (percentage / 100) * (maxZoom - minZoom)
return Math.round(Math.max(minZoom, Math.min(maxZoom, zoom)))
}
function onSliderMouseDown(event: MouseEvent) {
if (store.lensMode === 'wide') return // 广
const target = event.target as HTMLElement
if (!target.classList.contains('minimap-zoom-track') && !target.classList.contains('zoom-track')) return
isDraggingSlider.value = true
updateZoomFromEvent(event)
}
function onSliderMouseMove(event: MouseEvent) {
if (!isDraggingSlider.value || store.lensMode === 'wide') return
updateZoomFromEvent(event)
}
function onSliderMouseUp() {
isDraggingSlider.value = false
}
function onHandleMouseDown(event: MouseEvent) {
if (store.lensMode === 'wide') return
event.stopPropagation()
isDraggingSlider.value = true
const handleMouseMove = (e: MouseEvent) => {
if (isDraggingSlider.value && store.lensMode !== 'wide') {
// track
const handle = event.target as HTMLElement
const track = handle.closest('.minimap-zoom-track') || handle.closest('.zoom-track')
if (track) {
const fakeEvent = { ...e, currentTarget: track, target: track } as MouseEvent
updateZoomFromEvent(fakeEvent)
}
}
}
const handleMouseUp = () => {
isDraggingSlider.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
function updateZoomFromEvent(event: MouseEvent) {
if (store.lensMode === 'wide') return
// minimap-zoom-trackzoom-track
let sliderElement = (event.currentTarget || event.target) as HTMLElement
if (!sliderElement || (!sliderElement.classList.contains('zoom-track') && !sliderElement.classList.contains('minimap-zoom-track'))) {
// handletrack
sliderElement = sliderElement.closest('.minimap-zoom-track') as HTMLElement || sliderElement.closest('.zoom-track') as HTMLElement
if (!sliderElement) return
}
const rect = sliderElement.getBoundingClientRect()
const y = event.clientY - rect.top
const percentage = Math.max(0, Math.min(100, (y / rect.height) * 100))
const newZoom = zoomFromPosition(percentage)
if (window.wayline) {
const minimapViewer = window.wayline.minimapViewer
if (minimapViewer) {
function fovFromZoom(zoom: number, fov1: number = 1.3418): number {
const k = Math.tan(fov1 / 2)
return 2 * Math.atan(k / zoom)
}
const currentFov = fovFromZoom(newZoom)
store.updateZoomFactor(newZoom)
minimapViewer.camera.frustum.fov = currentFov
}
}
}
function toggleMinimapExpand() {
isMinimapExpanded.value = !isMinimapExpanded.value
}
let viewer: Viewer
Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI4NDU1Zjk5ZS0xOWQzLTQ2ZDEtYTk5MC03NmFlMGIwYTE4YzIiLCJpZCI6MTMwMTIsInNjb3BlcyI6WyJhc3IiLCJnYyJdLCJpYXQiOjE1NjIzMTc2OTh9.1gS4dOGsPgIB_Xd5ERV_LPBPrj12oVUX4ZtyQyoYw58'
@ -67,19 +203,86 @@ async function getTerrainPosition(cartographic: Cartographic) {
<template>
<div id="cesium-viewer" ref="viewerDivRef"></div>
<div id="minimap">
<div class="zoom-frame warning" style="width: 200px; height: 150px;">
<div class="top left corner"></div>
<div class="top right corner"></div>
<div class="bottom left corner"></div>
<div class="bottom right corner"></div><span class="frame-title map-text-shadow warning">
{{ store.zoomFactor }}X
<span class="uranus-icon uranus-icon-cursor-pointer warning-tooltips warning-tooltips warning-tooltips">
<svg class="svgfont svgfont-warning_outline" style="width: 1em; height: 1em;">
<use xlink:href="#warning_outline"></use>
</svg>
<div id="minimap" :class="{ 'expanded': isMinimapExpanded }">
<!-- 小框放大按钮 -->
<button class="minimap-expand-btn" @click="toggleMinimapExpand" :title="isMinimapExpanded ? '缩小' : '放大'">
<svg v-if="!isMinimapExpanded" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2H6V3H3V6H2V2ZM10 2H14V6H13V3H10V2ZM2 10V14H6V13H3V10H2ZM13 10V13H10V14H14V10H13Z" fill="white"/>
</svg>
<svg v-else width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 5H13V7H11V5ZM5 5H7V7H5V5ZM11 9H13V11H11V9ZM5 9H7V11H5V9Z" fill="white"/>
</svg>
</button>
<!-- Minimap HUD 覆盖层按截图布局 -->
<div class="minimap-hud">
<!-- 中间对焦框zoom-frame -->
<div class="zoom-frame warning">
<div class="top left corner"></div>
<div class="top right corner"></div>
<div class="bottom left corner"></div>
<div class="bottom right corner"></div>
<span class="frame-title map-text-shadow warning">
{{ store.zoomFactor }}X
</span>
</span>
</div>
<!-- 顶部居中镜头切换 -->
<div class="minimap-lens-controls">
<button
:class="['minimap-lens-btn', { 'active': store.lensMode === 'wide' }]"
@click="store.setLensMode('wide')"
>
广角 {{ store.lensMode === 'wide' ? store.zoomFactor : 1 }}X
</button>
<button
:class="['minimap-lens-btn', { 'active': store.lensMode === 'zoom' }]"
@click="store.setLensMode('zoom')"
>
变焦 {{ store.lensMode === 'zoom' ? store.zoomFactor : 1 }}X
</button>
</div>
<!-- 中上偏航角 -->
<div class="minimap-yaw">
偏航角 {{ Math.round(store.yawAngle) }}
</div>
<!-- 左侧云台 -->
<div class="minimap-gimbal">
<div class="minimap-gimbal-title">云台</div>
<div class="minimap-gimbal-value">{{ Math.round(store.gimbalPitch) }}</div>
</div>
<!-- 底部居中保存 -->
<button class="minimap-save-btn" type="button">
保存
</button>
</div>
<div class="minimap-zoom-slider" :class="{ 'disabled': store.lensMode === 'wide' }">
<div class="minimap-zoom-marks">
<span class="minimap-zoom-mark">56x</span>
<span class="minimap-zoom-mark">14x</span>
<span class="minimap-zoom-mark">7x</span>
<span class="minimap-zoom-mark">2x</span>
<span class="minimap-zoom-mark">1x</span>
</div>
<div
class="minimap-zoom-track"
:class="{ 'disabled': store.lensMode === 'wide' }"
@mousedown="onSliderMouseDown"
@mousemove="onSliderMouseMove"
@mouseup="onSliderMouseUp"
@mouseleave="onSliderMouseUp"
>
<div
class="minimap-zoom-handle"
:class="{ 'disabled': store.lensMode === 'wide' }"
:style="{ bottom: getSliderPosition() + '%' }"
@mousedown.stop="onHandleMouseDown"
></div>
</div>
</div>
</div>
<div style="position: fixed; bottom: 20px; left: 200px;">
@ -87,6 +290,49 @@ async function getTerrainPosition(cartographic: Cartographic) {
<img id="compass" src="/compass.svg" draggable="false">
</div>
<div class="point-list-container">
<div class="point-list-header">航点列表 ({{ store.pointList.length }})</div>
<div class="point-list-content">
<div
v-for="point in store.pointList"
:key="point.index"
:class="['point-item', { 'selected': point.selected }]"
>
<div class="point-index">#{{ point.index + 1 }}</div>
<div class="point-info">
<div class="point-row">
<span class="label">经度:</span>
<span class="value">{{ point.longitude.toFixed(6) }}°</span>
</div>
<div class="point-row">
<span class="label">纬度:</span>
<span class="value">{{ point.latitude.toFixed(6) }}°</span>
</div>
<div class="point-row">
<span class="label">ASL:</span>
<span class="value">{{ point.asl.toFixed(1) }} m</span>
</div>
<div class="point-row">
<span class="label">HAE:</span>
<span class="value">{{ point.alt.toFixed(1) }} m</span>
</div>
<div class="point-row">
<span class="label">航向:</span>
<span class="value">{{ point.heading.toFixed(1) }}°</span>
</div>
<div class="point-row">
<span class="label">俯仰:</span>
<span class="value">{{ point.pitch.toFixed(1) }}°</span>
</div>
</div>
</div>
<div v-if="store.pointList.length === 0" class="empty-message">
暂无航点按空格键创建航点
</div>
</div>
</div>
</template>
<style scoped>
@ -101,6 +347,33 @@ async function getTerrainPosition(cartographic: Cartographic) {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.25);
border: 2px solid rgba(255, 255, 255, 0.65);
border-radius: 10px;
transition: all 0.3s ease;
z-index: 1000;
overflow: hidden;
}
#minimap::before {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.22);
pointer-events: none;
z-index: 1;
}
/* Cesium minimap canvas fill */
#minimap :deep(.cesium-viewer) {
position: absolute;
inset: 0;
z-index: 0;
}
#minimap.expanded {
width: 600px;
height: 450px;
}
._dji_compass_p0y8b_78 {
@ -108,9 +381,6 @@ async function getTerrainPosition(cartographic: Cartographic) {
}
.zoom-frame {
position: fixed;
right: 20px;
bottom: 20px;
position: absolute;
top: 50%;
left: 50%;
@ -118,6 +388,8 @@ async function getTerrainPosition(cartographic: Cartographic) {
z-index: 999;
user-select: none;
pointer-events: none;
width: 200px;
height: 150px;
}
.zoom-frame .corner {
@ -152,10 +424,12 @@ async function getTerrainPosition(cartographic: Cartographic) {
}
.zoom-frame .frame-title {
color: #00ee8b;
bottom: -8px;
right: 0;
transform: translateY(100%);
color: #fff;
top: -10px;
left: 50%;
transform: translate(-50%, -100%);
font-weight: 700;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
}
.frame-title {
@ -180,4 +454,354 @@ async function getTerrainPosition(cartographic: Cartographic) {
transform: translate(-50%, -100%);
white-space: nowrap;
}
.point-list-container {
position: fixed;
top: 20px;
left: 20px;
width: 300px;
max-height: calc(100vh - 40px);
background: rgba(0, 0, 0, 0.8);
border: 2px solid #00D690;
border-radius: 8px;
color: white;
font-family: sans-serif;
z-index: 1000;
display: flex;
flex-direction: column;
}
.point-list-header {
padding: 12px 16px;
background: rgba(0, 214, 144, 0.2);
border-bottom: 1px solid #00D690;
font-weight: 600;
font-size: 16px;
}
.point-list-content {
overflow-y: auto;
padding: 8px;
max-height: calc(100vh - 100px);
}
.point-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
padding: 12px;
margin-bottom: 8px;
transition: all 0.3s ease;
}
.point-item:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(0, 214, 144, 0.5);
}
.point-item.selected {
background: rgba(0, 214, 144, 0.2);
border-color: #00D690;
box-shadow: 0 0 10px rgba(0, 214, 144, 0.3);
}
.point-index {
font-weight: 600;
font-size: 18px;
color: #00D690;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.point-info {
font-size: 12px;
}
.point-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.point-row .label {
color: rgba(255, 255, 255, 0.7);
}
.point-row .value {
color: white;
font-weight: 500;
}
.empty-message {
text-align: center;
padding: 40px 20px;
color: rgba(255, 255, 255, 0.5);
font-size: 14px;
}
/* 顶部中央:镜头模式切换 */
/* 左上角:放大/缩小按钮(按截图) */
.minimap-expand-btn {
position: absolute;
top: 10px;
left: 10px;
width: 26px;
height: 26px;
background: rgba(0, 0, 0, 0.55);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 6px;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
z-index: 3;
}
.minimap-expand-btn:hover {
background: rgba(0, 0, 0, 0.75);
border-color: rgba(255, 255, 255, 0.6);
}
/* HUD 覆盖层容器 */
.minimap-hud {
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
}
/* 顶部居中:镜头切换(按截图两段按钮) */
.minimap-lens-controls {
position: absolute;
top: 14px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 0;
background: rgba(0, 0, 0, 0.45);
border-radius: 6px;
padding: 4px;
pointer-events: auto;
}
.minimap-lens-btn {
padding: 10px 22px;
background: rgba(255, 255, 255, 0.10);
border: none;
border-radius: 4px;
color: rgba(255, 255, 255, 0.70);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.minimap-lens-btn:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.minimap-lens-btn:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.minimap-lens-btn.active {
background: #2F80ED;
color: #fff;
}
.minimap-lens-btn:not(.active):hover {
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.8);
}
/* 中上:偏航角 */
.minimap-yaw {
position: absolute;
top: 72px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.92);
font-size: 18px;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
}
/* 左侧:云台 */
.minimap-gimbal {
position: absolute;
top: 120px;
left: 48px;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
font-weight: 700;
text-align: center;
}
.minimap-gimbal-title {
font-size: 16px;
margin-bottom: 6px;
}
.minimap-gimbal-value {
font-size: 22px;
}
/* 底部:保存按钮 */
.minimap-save-btn {
position: absolute;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
width: 160px;
height: 44px;
border: none;
border-radius: 8px;
background: #2F80ED;
color: #fff;
font-size: 16px;
font-weight: 700;
cursor: pointer;
pointer-events: auto;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.minimap-save-btn:hover {
filter: brightness(1.05);
}
/* Minimap内的变焦滑块(右侧竖条) */
.minimap-zoom-slider {
position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
display: flex;
align-items: center;
gap: 10px;
z-index: 3;
pointer-events: auto;
}
.minimap-zoom-marks {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 180px;
color: rgba(255, 255, 255, 0.9);
font-size: 12px;
font-weight: 600;
}
.minimap-zoom-mark {
text-align: right;
min-width: 25px;
}
.minimap-zoom-track {
position: relative;
width: 6px;
height: 180px;
background: rgba(255, 255, 255, 0.35);
border-radius: 10px;
cursor: pointer;
}
.minimap-zoom-handle {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 18px;
height: 18px;
background: white;
border-radius: 50%;
cursor: grab;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: box-shadow 0.2s;
}
.minimap-zoom-handle:active {
cursor: grabbing;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
.minimap-zoom-slider.disabled .minimap-zoom-track,
.minimap-zoom-track.disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.minimap-zoom-handle.disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
/* 小框放大按钮 */
.minimap-expand-btn {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
z-index: 1001;
}
.minimap-expand-btn:hover {
background: rgba(0, 0, 0, 0.8);
border-color: rgba(255, 255, 255, 0.5);
}
/* 云台和偏航角数据显示 */
.gimbal-data-display {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 1000;
background: rgba(0, 0, 0, 0.6);
padding: 12px 20px;
border-radius: 8px;
}
.data-line {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
color: white;
font-size: 14px;
}
.data-label {
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
.data-value {
color: white;
font-weight: 600;
font-size: 16px;
min-width: 50px;
text-align: right;
}
</style>

+ 30
- 1
src/store.ts View File

@ -1,12 +1,41 @@
import { defineStore } from 'pinia';
export interface WayPointInfo {
index: number;
asl: number;
alt: number;
fov: number;
selected: boolean;
longitude: number;
latitude: number;
heading: number;
pitch: number;
roll: number;
}
export type LensMode = 'wide' | 'zoom';
export const useStore = defineStore('main', {
state: () => ({
zoomFactor: 0
zoomFactor: 1,
lensMode: 'wide' as LensMode,
pointList: [] as WayPointInfo[],
gimbalPitch: 0, // 云台俯仰角(度)
yawAngle: 0 // 偏航角(度)
}),
actions: {
updateZoomFactor(zoomFactor: number) {
this.zoomFactor = zoomFactor;
},
updatePointList(pointList: WayPointInfo[]) {
this.pointList = pointList;
},
setLensMode(mode: LensMode) {
this.lensMode = mode;
},
updateGimbalData(pitch: number, yaw: number) {
this.gimbalPitch = pitch;
this.yawAngle = yaw;
}
}
})

+ 121
- 17
src/wayline.ts View File

@ -81,6 +81,17 @@ export class Wayline {
this.setupDragEvent()
this.setupMovementEvent()
this.setupSelectEvent()
this.syncPointListToStore()
// 初始化云台数据
const store = useStore()
const initialPitch = CesiumMath.toDegrees(this.minimapViewer.camera.pitch)
const initialHeading = this.minimapViewer.camera.heading
let initialYaw = CesiumMath.toDegrees(initialHeading)
if (initialYaw > 180) {
initialYaw = initialYaw - 360
}
store.updateGimbalData(initialPitch, initialYaw)
}
createFrustum(): void {
@ -104,6 +115,7 @@ export class Wayline {
this.selectedWaypoint = wayPoint
wayPoint.select()
this.syncPointListToStore()
}
setupSelectEvent(): void {
@ -115,6 +127,7 @@ export class Wayline {
this.selectedWaypoint?.unselect()
this.selectedWaypoint = (object.id as any).wayPoint
this.selectedWaypoint.select()
this.syncPointListToStore()
}
}, ScreenSpaceEventType.LEFT_CLICK)
}
@ -124,16 +137,25 @@ export class Wayline {
let isDragging = false
let isLifting = false
let isAdjustingHeight = false
let draggingWayPoint: WayPoint | null = null
const scene = viewer.scene
handler.setInputAction((click: ScreenSpaceEventHandler.PositionedEvent) => {
const object = scene.pick(click.position)
if (object && object.id && object.id.wayPoint && object.id.wayPoint.selected) {
isDragging = true
draggingWayPoint = object.id.wayPoint
scene.screenSpaceCameraController.enableRotate = false
if (object && object.id) {
// 检查是否点击了高度调整按钮
if ((object.id as any).heightAdjustButton) {
isAdjustingHeight = true
draggingWayPoint = (object.id as any).heightAdjustButton
scene.screenSpaceCameraController.enableRotate = false
}
// 检查是否点击了航点三角形(billboard)
else if ((object.id as any).wayPoint && (object.id as any).wayPoint.selected) {
isDragging = true
draggingWayPoint = (object.id as any).wayPoint
scene.screenSpaceCameraController.enableRotate = false
}
}
}, ScreenSpaceEventType.LEFT_DOWN)
@ -146,10 +168,13 @@ export class Wayline {
}, ScreenSpaceEventType.LEFT_DOWN, KeyboardEventModifier.ALT)
handler.setInputAction(() => {
if (isDragging) {
if (isDragging || isAdjustingHeight) {
isDragging = false
isAdjustingHeight = false
this.minimapViewer.camera.position = draggingWayPoint!.position.clone()
if (draggingWayPoint) {
this.minimapViewer.camera.position = draggingWayPoint.position.clone()
}
draggingWayPoint = null
scene.screenSpaceCameraController.enableRotate = true
}
@ -162,13 +187,32 @@ export class Wayline {
handler.setInputAction((movement: ScreenSpaceEventHandler.MotionEvent) => {
const object = scene.pick(movement.endPosition)
if (object && object.id && (object.id as any).wayPoint && (object.id as any).wayPoint.selectd) {
scene.canvas.style.cursor = 'move'
if (object && object.id) {
if ((object.id as any).heightAdjustButton) {
scene.canvas.style.cursor = 'ns-resize' // 上下调整光标
} else if ((object.id as any).wayPoint && (object.id as any).wayPoint.selected) {
scene.canvas.style.cursor = 'move'
} else {
scene.canvas.style.cursor = 'default'
}
} else {
scene.canvas.style.cursor = 'default'
}
if (isDragging && draggingWayPoint) {
// 高度调整:点击高度按钮后拖拽调整高度
if (isAdjustingHeight && draggingWayPoint) {
const deltaY = movement.endPosition.y - movement.startPosition.y
const deltaHeight = deltaY * getVerticalMetersPerPixel(viewer, draggingWayPoint.position)
const lngLat = Cartographic.fromCartesian(draggingWayPoint.position)
lngLat.height -= deltaHeight
draggingWayPoint.asl -= deltaHeight
draggingWayPoint.alt -= deltaHeight
draggingWayPoint.updatePosition(Cartesian3.fromRadians(lngLat.longitude, lngLat.latitude, lngLat.height))
}
// 左右平移:点击三角形后拖拽进行水平移动
else if (isDragging && draggingWayPoint) {
const endPosition = scene.globe.pick(scene.camera.getPickRay(movement.endPosition), scene)
const startPosition = scene.globe.pick(scene.camera.getPickRay(movement.startPosition), scene)
@ -294,6 +338,11 @@ export class Wayline {
heading = heading % (2 * Math.PI)
if (heading < 0) heading += 2 * Math.PI
// 限制pitch在-90度(正朝下)到+45度(朝上)之间
const minPitch = -Math.PI / 2 // -90度
const maxPitch = Math.PI / 4 // +45度
pitch = CesiumMath.clamp(pitch, minPitch, maxPitch)
camera.setView({
destination: position,
orientation: {
@ -303,6 +352,15 @@ export class Wayline {
}
})
// 更新云台数据和偏航角到store
const store = useStore()
const pitchDegrees = CesiumMath.toDegrees(pitch)
let yawDegrees = CesiumMath.toDegrees(heading)
if (yawDegrees > 180) {
yawDegrees = yawDegrees - 360
}
store.updateGimbalData(pitchDegrees, yawDegrees)
requestAnimationFrame(updateCameraControl)
}
@ -354,7 +412,11 @@ export class Wayline {
heading = heading % (2 * Math.PI)
if (heading < 0) heading += 2 * Math.PI
pitch = CesiumMath.clamp(pitch, -Math.PI / 2, Math.PI / 2)
// 限制pitch在-90度(正朝下)到+45度(朝上)之间,水平是0
// Cesium中pitch: -90度 = -Math.PI/2, +45度 = Math.PI/4, 0度 = 0
const minPitch = -Math.PI / 2 // -90度
const maxPitch = Math.PI / 4 // +45度
pitch = CesiumMath.clamp(pitch, minPitch, maxPitch)
const position = Cartesian3.clone(minimapViewer.camera.position)
minimapViewer.camera.setView({
@ -365,6 +427,14 @@ export class Wayline {
roll: minimapViewer.camera.roll
}
})
// 更新云台数据和偏航角到store
const pitchDegrees = CesiumMath.toDegrees(pitch)
let yawDegrees = CesiumMath.toDegrees(heading)
if (yawDegrees > 180) {
yawDegrees = yawDegrees - 360
}
store.updateGimbalData(pitchDegrees, yawDegrees)
}, ScreenSpaceEventType.MOUSE_MOVE)
handler.setInputAction(() => {
@ -377,20 +447,34 @@ export class Wayline {
}
let currentFov = minimapViewer.camera.frustum.fov
let zoomFactor = 2
const minZoomFactor = 2
const maxZoomFactor = 200
let zoomFactor = store.zoomFactor || 1
handler.setInputAction(function (movement: any) {
const delta = movement
const currentMode = store.lensMode
zoomFactor += delta > 0 ? 2 : -2
zoomFactor = CesiumMath.clamp(zoomFactor, minZoomFactor, maxZoomFactor)
currentFov = fovFromZoom(zoomFactor)
if (currentMode === 'wide') {
// 广角模式只能1x
zoomFactor = 1
} else {
// 变焦模式可以1-56x
const step = delta > 0 ? 1 : -1
zoomFactor += step
zoomFactor = CesiumMath.clamp(zoomFactor, 1, 56)
}
currentFov = fovFromZoom(zoomFactor)
store.updateZoomFactor(zoomFactor)
minimapViewer.camera.frustum.fov = currentFov
}, ScreenSpaceEventType.WHEEL)
// 初始化时根据模式设置变焦
if (store.lensMode === 'wide') {
zoomFactor = 1
currentFov = fovFromZoom(zoomFactor)
store.updateZoomFactor(zoomFactor)
minimapViewer.camera.frustum.fov = currentFov
}
}
createWayline(): void {
@ -491,6 +575,26 @@ export class Wayline {
)
}
syncPointListToStore(): void {
const store = useStore()
const pointListInfo = this.pointList.map(point => {
const cartographic = Cartographic.fromCartesian(point.position)
return {
index: point.index,
asl: point.asl,
alt: point.alt,
fov: point.fov,
selected: point.selected,
longitude: CesiumMath.toDegrees(cartographic.longitude),
latitude: CesiumMath.toDegrees(cartographic.latitude),
heading: CesiumMath.toDegrees(point.orientation.heading),
pitch: CesiumMath.toDegrees(point.orientation.pitch),
roll: CesiumMath.toDegrees(point.orientation.roll)
}
})
store.updatePointList(pointListInfo)
}
destroy(): void {
viewer.entities.remove(this.wayLineEntity)
viewer.entities.remove(this.takeOffPointEntity)

+ 76
- 13
src/waypoint.ts View File

@ -7,6 +7,7 @@ import {
HeightReference,
LabelStyle,
VerticalOrigin,
HorizontalOrigin,
PerspectiveFrustum,
DebugCameraPrimitive,
Entity,
@ -28,6 +29,7 @@ export class WayPoint {
selected!: boolean
heightLabel!: Entity
wayPointEntity!: Entity
heightAdjustButton!: Entity
orientation: { heading: number; pitch: number; roll: number }
debugCameraPrimitive: any
distanceLabel1: any
@ -91,11 +93,20 @@ export class WayPoint {
const pointList = this.wayline.pointList
this.position = position
this.wayPointEntity.position = position
this.heightLabel.position = position
this.debugCameraPrimitive._camera.position = position
if (this.heightLabel) {
this.heightLabel.position = position
}
if (this.heightAdjustButton) {
this.heightAdjustButton.position = position
}
if (this.debugCameraPrimitive) {
this.debugCameraPrimitive._camera.position = position
}
const text = `ASL: ${this.asl.toFixed(1)} m\nHAE: ${this.alt.toFixed(1)} m`
this.heightLabel.label.text = text
if (this.heightLabel) {
this.heightLabel.label.text = text
}
const index = this.index
const prev = pointList[index - 1]
@ -107,8 +118,10 @@ export class WayPoint {
position
)
const midpoint = Cartesian3.lerp(prev.position, position, 0.5, new Cartesian3())
this.distanceLabel1.position = midpoint
this.distanceLabel1.label.text = distance.toFixed(1) + 'm'
if (this.distanceLabel1) {
this.distanceLabel1.position = midpoint
this.distanceLabel1.label.text = distance.toFixed(1) + 'm'
}
}
if (next) {
@ -117,9 +130,13 @@ export class WayPoint {
next.position
)
const midpoint = Cartesian3.lerp(position, next.position, 0.5, new Cartesian3())
this.distanceLabel2.position = midpoint
this.distanceLabel2.label.text = distance.toFixed(1) + 'm'
if (this.distanceLabel2) {
this.distanceLabel2.position = midpoint
this.distanceLabel2.label.text = distance.toFixed(1) + 'm'
}
}
this.wayline.syncPointListToStore()
}
select(): void {
@ -128,6 +145,7 @@ export class WayPoint {
this.showDistanceLabels()
this.showHeightLabel()
this.showHeightAdjustButton()
const camera = new Camera(viewer.scene)
camera.frustum = new PerspectiveFrustum({
@ -155,17 +173,20 @@ export class WayPoint {
})
viewer.scene.primitives.add(this.debugCameraPrimitive)
this.wayline.syncPointListToStore()
}
unselect(): void {
this.selected = false
this.wayPointEntity.billboard.image = generateNumberSvgDataUrl(this.index + 1)
viewer.entities.remove(this.distanceLabel0)
viewer.entities.remove(this.distanceLabel1)
viewer.entities.remove(this.distanceLabel2)
viewer.entities.remove(this.heightLabel)
viewer.scene.primitives.remove(this.debugCameraPrimitive)
if (this.distanceLabel0) viewer.entities.remove(this.distanceLabel0)
if (this.distanceLabel1) viewer.entities.remove(this.distanceLabel1)
if (this.distanceLabel2) viewer.entities.remove(this.distanceLabel2)
if (this.heightLabel) viewer.entities.remove(this.heightLabel)
if (this.heightAdjustButton) viewer.entities.remove(this.heightAdjustButton)
if (this.debugCameraPrimitive) viewer.scene.primitives.remove(this.debugCameraPrimitive)
this.wayline.syncPointListToStore()
}
distanceLabel0(distanceLabel0: any) {
throw new Error('Method not implemented.')
@ -175,13 +196,20 @@ export class WayPoint {
const pointList = this.wayline.pointList
viewer.entities.remove(this.wayPointEntity)
viewer.scene.primitives.remove(this.debugCameraPrimitive)
if (this.heightAdjustButton) {
viewer.entities.remove(this.heightAdjustButton)
}
if (this.debugCameraPrimitive) {
viewer.scene.primitives.remove(this.debugCameraPrimitive)
}
pointList.splice(this.index, 1)
for (let i = this.index; i < pointList.length; i++) {
pointList[i].wayPointEntity.billboard.image = generateNumberSvgDataUrl(i + 1)
pointList[i].index = i
}
this.wayline.syncPointListToStore()
}
private showHeightLabel(): void {
@ -206,6 +234,24 @@ export class WayPoint {
})
}
private showHeightAdjustButton(): void {
const { position } = this
const buttonUrl = generateHeightAdjustButtonSvgDataUrl()
this.heightAdjustButton = viewer.entities.add({
position,
heightAdjustButton: this,
billboard: {
image: buttonUrl,
scale: 0.8,
verticalOrigin: VerticalOrigin.CENTER,
horizontalOrigin: HorizontalOrigin.LEFT,
disableDepthTestDistance: Number.POSITIVE_INFINITY,
pixelOffset: new Cartesian2(25, 0) // 在航点右侧显示
}
})
}
private showDistanceLabels(): void {
const index = this.index
const pointList = this.wayline.pointList
@ -310,3 +356,20 @@ function generateHighlightedNumberSvgDataUrl(number: number): string {
return `data:image/svg+xml,${encoded}`
}
function generateHeightAdjustButtonSvgDataUrl(): string {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 40" width="24" height="40">
<rect width="24" height="40" rx="4" fill="#00D690" fill-opacity="0.9" stroke="#FFF" stroke-width="1.5"/>
<path d="M12 8 L8 12 L12 16 M12 8 L16 12 L12 16" stroke="#FFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="20" x2="12" y2="32" stroke="#FFF" stroke-width="2" stroke-linecap="round"/>
<path d="M12 32 L8 28 L12 24 M12 32 L16 28 L12 24" stroke="#FFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`
const encoded = encodeURIComponent(svg)
.replace(/'/g, '%27')
.replace(/"/g, '%22')
return `data:image/svg+xml,${encoded}`
}

Loading…
Cancel
Save