@yangyongtao/gaea 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,593 @@
1
+ <template>
2
+ <div class="weather-panel" v-drag>
3
+ <div class="panel-header title">
4
+ <span class="panel-title">天气控制</span>
5
+ <button class="close-btn" @click="handleClose">×</button>
6
+ </div>
7
+
8
+ <div class="panel-body">
9
+ <!-- 雨 -->
10
+ <div class="weather-section">
11
+ <div class="weather-header">
12
+ <span class="weather-label"><i class="fas fa-cloud-rain"></i> 下雨</span>
13
+ <label class="switch">
14
+ <input type="checkbox" v-model="rainEnabled" @change="onRainChange">
15
+ <span class="slider"></span>
16
+ </label>
17
+ </div>
18
+ <div v-show="rainEnabled" class="weather-sliders">
19
+ <div class="slider-row">
20
+ <label>方向</label>
21
+ <input type="range" min="-1.5" max="1.5" step="0.1" v-model.number="rainDirection" @input="onRainDirectionChange" />
22
+ <span class="val">{{ rainDirection.toFixed(1) }}</span>
23
+ </div>
24
+ <div class="slider-row">
25
+ <label>速度</label>
26
+ <input type="range" min="0" max="100" step="5" v-model.number="rainSpeed" @input="onRainSpeedChange" />
27
+ <span class="val">{{ rainSpeed }}</span>
28
+ </div>
29
+ <div class="slider-row">
30
+ <label>雨量</label>
31
+ <input type="range" min="0" max="1000" step="5" v-model.number="rainCount" @input="onRainCountChange" />
32
+ <span class="val">{{ rainCount }}</span>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- 台风标注 -->
38
+ <div class="weather-section">
39
+ <div class="weather-header">
40
+ <span class="weather-label"><i class="fas fa-wind"></i> 台风路线</span>
41
+ </div>
42
+ <!-- 地图拾取模式开关 -->
43
+ <div class="pick-row">
44
+ <button
45
+ class="btn"
46
+ :class="typhoonPicking ? 'btn-picking-active' : 'btn-picking'"
47
+ @click="toggleTyphoonPickMode"
48
+ >
49
+ <i class="fas" :class="typhoonPicking ? 'fa-check-circle' : 'fa-map-marker-alt'"></i>
50
+ {{ typhoonPicking ? '拾取中(点击地图添加点位)...' : '地图拾取台风点位' }}
51
+ </button>
52
+ </div>
53
+ <!-- 台风点位列表 -->
54
+ <div class="typhoon-point-list" v-if="typhoonPoints.length > 0">
55
+ <div class="typhoon-point-header">
56
+ <span>台风点位 ({{ typhoonPoints.length }})</span>
57
+ <button class="btn-clear-small" @click="clearTyphoonPoints">清空</button>
58
+ </div>
59
+ <div class="typhoon-point-items">
60
+ <div v-for="(tp, i) in typhoonPoints" :key="i" class="typhoon-point-item">
61
+ <span class="tp-index">{{ i + 1 }}</span>
62
+ <span class="tp-coords">{{ tp.lng.toFixed(6) }}, {{ tp.lat.toFixed(6) }}</span>
63
+ <button class="tp-delete" @click="removeTyphoonPoint(i)">×</button>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ <div class="typhoon-btns">
68
+ <button class="btn btn-typhoon-load" @click="loadTyphoonRoute" :disabled="typhoonLoading || typhoonPoints.length === 0">
69
+ <i class="fas fa-wind"></i> {{ typhoonLoading ? '加载中...' : '加载台风路线' }}
70
+ </button>
71
+ <button class="btn btn-typhoon-clear" @click="clearAllTyphoon">
72
+ 清除全部
73
+ </button>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </template>
79
+
80
+ <script setup>
81
+ import { ref, onBeforeUnmount } from 'vue';
82
+ import { GaeaApp } from '../GaeaMethods.js';
83
+
84
+ const props = defineProps({
85
+ appInstance: { type: Object, default: null }
86
+ });
87
+ const emit = defineEmits(['close']);
88
+
89
+ // 使用传入的 appInstance 或创建默认实例
90
+ const APP = props.appInstance || (window.APP || new GaeaApp());
91
+
92
+ // ---- 天气状态 ----
93
+ const rainEnabled = ref(false);
94
+
95
+ // 雨参数
96
+ const rainDirection = ref(0);
97
+ const rainSpeed = ref(5);
98
+ const rainCount = ref(30);
99
+
100
+ // ---- Gaea 对象(惰性初始化) ----
101
+ let fullScreenQuad = null;
102
+ let sun = null;
103
+ let environment = null;
104
+
105
+ const initGaea = () => {
106
+ if (fullScreenQuad) return;
107
+ fullScreenQuad = Gaea.World.Instance.FullScreenQuad;
108
+ sun = Gaea.Sun.ConvertBy(Gaea.World.Instance.GetNode('Sun'));
109
+ environment = Gaea.Environment.ConvertBy(
110
+ Gaea.GaeaResourceLoader.Instance.LoadLoaclResource('res://default_env.tres')
111
+ );
112
+ };
113
+
114
+ // ---- 雨 ----
115
+ const onRainChange = () => {
116
+ initGaea();
117
+ if (!fullScreenQuad) return;
118
+
119
+ if (rainEnabled.value) {
120
+
121
+ fullScreenQuad.Visible = true;
122
+ fullScreenQuad.NeedAtmosphere = true;
123
+
124
+ // --- 日照 ---
125
+ sun.TimeString = '2022-09-15 00:00:00';
126
+ sun.LightColor = new Gaea.Color(1, 1, 1, 1);
127
+ sun.LightEnergy = 0.05;
128
+ environment.BackgroundEnergy = 0.11;
129
+ environment.GlowBlendMode = 1;
130
+
131
+ // --- 下雨参数 ---
132
+ fullScreenQuad.RainyCount = rainCount.value;
133
+ fullScreenQuad.Speed = rainSpeed.value;
134
+ fullScreenQuad.WindDirection = rainDirection.value;
135
+ fullScreenQuad.Rainy = true;
136
+ } else {
137
+ // --- 关闭下雨 ---
138
+ sun.TimeString = '2022-09-15 12:00:00';
139
+ sun.LightColor = new Gaea.Color(1, 226 / 255, 164 / 255, 1);
140
+ sun.LightEnergy = 3.5;
141
+ environment.BackgroundEnergy = 0.2;
142
+ environment.GlowBlendMode = 2;
143
+
144
+ fullScreenQuad.Rainy = false;
145
+ }
146
+ };
147
+
148
+ const onRainDirectionChange = () => {
149
+ if (!fullScreenQuad || !rainEnabled.value) return;
150
+ fullScreenQuad.WindDirection = rainDirection.value;
151
+ };
152
+
153
+ const onRainSpeedChange = () => {
154
+ if (!fullScreenQuad || !rainEnabled.value) return;
155
+ fullScreenQuad.Speed = rainSpeed.value;
156
+ };
157
+
158
+ const onRainCountChange = () => {
159
+ if (!fullScreenQuad || !rainEnabled.value) return;
160
+ fullScreenQuad.RainyCount = rainCount.value;
161
+ };
162
+
163
+ // ---- 关闭 ----
164
+ const handleClose = () => {
165
+ emit('close');
166
+ };
167
+
168
+ // ---- 台风标注(打点模式) ----
169
+ const typhoonLoading = ref(false);
170
+ const typhoonPoints = ref([]);
171
+ const typhoonPicking = ref(false);
172
+ let typhoonCanvasHandler = null;
173
+
174
+ // 点击地图获取经纬度(同 FlyAlongRoute)
175
+ const getTyphoonLatLng = (event) => {
176
+ try {
177
+ if (!window.Gaea || !window.Gaea.GaeaMath) return null;
178
+ const result = window.Gaea.GaeaMath.ProjectPosition(
179
+ new window.Gaea.Vector2(event.offsetX, event.offsetY)
180
+ );
181
+ const lonlat = window.Gaea.GaeaMath.CartesianToSphericalDeg(result);
182
+ return {
183
+ lat: lonlat.x,
184
+ lng: lonlat.y,
185
+ alt: lonlat.z,
186
+ };
187
+ } catch (e) {
188
+ console.warn('台风点位拾取失败:', e);
189
+ return null;
190
+ }
191
+ };
192
+
193
+ const toggleTyphoonPickMode = () => {
194
+ if (!typhoonPicking.value) {
195
+ typhoonPicking.value = true;
196
+ const canvas = document.getElementById('canvas');
197
+ if (!canvas) return;
198
+ typhoonCanvasHandler = (event) => {
199
+ const pos = getTyphoonLatLng(event);
200
+ if (pos) {
201
+ typhoonPoints.value.push({
202
+ lat: +pos.lat.toFixed(6),
203
+ lng: +pos.lng.toFixed(6),
204
+ alt: +pos.alt.toFixed(1) || 0,
205
+ });
206
+ }
207
+ };
208
+ canvas.addEventListener('mousedown', typhoonCanvasHandler, false);
209
+ } else {
210
+ removeTyphoonCanvasListener();
211
+ typhoonPicking.value = false;
212
+ }
213
+ };
214
+
215
+ const removeTyphoonCanvasListener = () => {
216
+ if (typhoonCanvasHandler) {
217
+ const canvas = document.getElementById('canvas');
218
+ if (canvas) {
219
+ canvas.removeEventListener('mousedown', typhoonCanvasHandler, false);
220
+ }
221
+ typhoonCanvasHandler = null;
222
+ }
223
+ };
224
+
225
+ const removeTyphoonPoint = (idx) => {
226
+ typhoonPoints.value.splice(idx, 1);
227
+ };
228
+
229
+ const clearTyphoonPoints = () => {
230
+ typhoonPoints.value = [];
231
+ };
232
+
233
+ const loadTyphoonRoute = async () => {
234
+ if (typhoonPoints.value.length === 0) return;
235
+ // 关闭拾取
236
+ removeTyphoonCanvasListener();
237
+ typhoonPicking.value = false;
238
+ typhoonLoading.value = true;
239
+ try {
240
+ await APP.addPointIcons(typhoonPoints.value);
241
+ } catch (error) {
242
+ console.error('加载台风路线错误:', error);
243
+ } finally {
244
+ typhoonLoading.value = false;
245
+ }
246
+ };
247
+
248
+ const clearAllTyphoon = () => {
249
+ clearTyphoonPoints();
250
+ removeTyphoonCanvasListener();
251
+ typhoonPicking.value = false;
252
+ if (APP && APP.clearPointIcons) {
253
+ APP.clearPointIcons();
254
+ }
255
+ };
256
+
257
+ onBeforeUnmount(() => {
258
+ removeTyphoonCanvasListener();
259
+ });
260
+ </script>
261
+
262
+ <style lang="less" scoped>
263
+ .weather-panel {
264
+ position: absolute;
265
+ top: 100px;
266
+ left: 600px;
267
+ z-index: 2000;
268
+ width: 300px;
269
+ background: rgba(15, 23, 42, 0.95);
270
+ border-radius: 12px;
271
+ border: 1px solid rgba(59, 130, 246, 0.3);
272
+ overflow: hidden;
273
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
274
+ }
275
+
276
+ .panel-header {
277
+ display: flex;
278
+ justify-content: space-between;
279
+ align-items: center;
280
+ padding: 14px 16px;
281
+ background: rgba(30, 41, 59, 1);
282
+ border-bottom: 1px solid rgba(59, 130, 246, 0.2);
283
+ }
284
+
285
+ .panel-title {
286
+ font-size: 15px;
287
+ font-weight: 600;
288
+ color: #f1f5f9;
289
+ }
290
+
291
+ .close-btn {
292
+ width: 24px;
293
+ height: 24px;
294
+ border: none;
295
+ background: rgba(239, 68, 68, 0.2);
296
+ color: #ef4444;
297
+ border-radius: 4px;
298
+ cursor: pointer;
299
+ font-size: 16px;
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: center;
303
+ }
304
+
305
+ .close-btn:hover {
306
+ background: rgba(239, 68, 68, 0.3);
307
+ }
308
+
309
+ .panel-body {
310
+ padding: 16px;
311
+ }
312
+
313
+ .weather-section {
314
+ margin-bottom: 16px;
315
+ padding: 12px;
316
+ background: rgba(30, 41, 59, 0.3);
317
+ border-radius: 8px;
318
+
319
+ &:last-child {
320
+ margin-bottom: 0;
321
+ }
322
+ }
323
+
324
+ .weather-header {
325
+ display: flex;
326
+ justify-content: space-between;
327
+ align-items: center;
328
+ margin-bottom: 8px;
329
+ }
330
+
331
+ .weather-label {
332
+ font-size: 14px;
333
+ font-weight: 500;
334
+ color: #f1f5f9;
335
+ }
336
+
337
+ .weather-label i {
338
+ margin-right: 6px;
339
+ }
340
+
341
+ /* 开关样式 */
342
+ .switch {
343
+ position: relative;
344
+ display: inline-block;
345
+ width: 44px;
346
+ height: 24px;
347
+ }
348
+
349
+ .switch input {
350
+ opacity: 0;
351
+ width: 0;
352
+ height: 0;
353
+ }
354
+
355
+ .slider {
356
+ position: absolute;
357
+ cursor: pointer;
358
+ top: 0;
359
+ left: 0;
360
+ right: 0;
361
+ bottom: 0;
362
+ background-color: rgba(71, 85, 105, 0.4);
363
+ transition: 0.3s;
364
+ border-radius: 24px;
365
+ }
366
+
367
+ .slider:before {
368
+ position: absolute;
369
+ content: "";
370
+ height: 18px;
371
+ width: 18px;
372
+ left: 3px;
373
+ bottom: 3px;
374
+ background-color: #94a3b8;
375
+ transition: 0.3s;
376
+ border-radius: 50%;
377
+ }
378
+
379
+ input:checked + .slider {
380
+ background-color: #3b82f6;
381
+ }
382
+
383
+ input:checked + .slider:before {
384
+ transform: translateX(20px);
385
+ background-color: #fff;
386
+ }
387
+
388
+ input:disabled + .slider {
389
+ opacity: 0.3;
390
+ cursor: not-allowed;
391
+ }
392
+
393
+ /* 滑块行 */
394
+ .weather-sliders {
395
+ margin-top: 8px;
396
+ }
397
+
398
+ .slider-row {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 8px;
402
+ margin-bottom: 8px;
403
+
404
+ label {
405
+ width: 36px;
406
+ font-size: 12px;
407
+ color: #94a3b8;
408
+ flex-shrink: 0;
409
+ }
410
+
411
+ input[type="range"] {
412
+ flex: 1;
413
+ height: 4px;
414
+ -webkit-appearance: none;
415
+ appearance: none;
416
+ background: rgba(71, 85, 105, 0.4);
417
+ border-radius: 2px;
418
+ outline: none;
419
+ cursor: pointer;
420
+
421
+ &::-webkit-slider-thumb {
422
+ -webkit-appearance: none;
423
+ width: 14px;
424
+ height: 14px;
425
+ border-radius: 50%;
426
+ background: #3b82f6;
427
+ cursor: pointer;
428
+ }
429
+ }
430
+
431
+ .val {
432
+ width: 44px;
433
+ font-size: 11px;
434
+ color: #cbd5e1;
435
+ text-align: right;
436
+ flex-shrink: 0;
437
+ }
438
+ }
439
+
440
+ .btn-typhoon-load {
441
+ flex: 2;
442
+ padding: 8px 10px;
443
+ border: none;
444
+ border-radius: 6px;
445
+ font-size: 13px;
446
+ font-weight: 500;
447
+ cursor: pointer;
448
+ background: linear-gradient(135deg, #f97316, #ea580c);
449
+ color: #fff;
450
+ transition: all 0.2s;
451
+ }
452
+
453
+ .btn-typhoon-load:hover:not(:disabled) {
454
+ background: linear-gradient(135deg, #ea580c, #c2410c);
455
+ }
456
+
457
+ .btn-typhoon-load:disabled {
458
+ opacity: 0.5;
459
+ cursor: not-allowed;
460
+ }
461
+
462
+ .btn-typhoon-clear {
463
+ flex: 1;
464
+ padding: 8px 10px;
465
+ border: none;
466
+ border-radius: 6px;
467
+ font-size: 13px;
468
+ font-weight: 500;
469
+ cursor: pointer;
470
+ background: rgba(239, 68, 68, 0.2);
471
+ color: #ef4444;
472
+ border: 1px solid rgba(239, 68, 68, 0.3);
473
+ transition: all 0.2s;
474
+ }
475
+
476
+ .btn-typhoon-clear:hover {
477
+ background: rgba(239, 68, 68, 0.3);
478
+ }
479
+
480
+ /* 地图拾取按钮 */
481
+ .pick-row {
482
+ margin-bottom: 8px;
483
+ }
484
+
485
+ .btn-picking {
486
+ background: rgba(245, 158, 11, 0.2);
487
+ color: #f59e0b;
488
+ border: 1px solid rgba(245, 158, 11, 0.3);
489
+ }
490
+
491
+ .btn-picking:hover {
492
+ background: rgba(245, 158, 11, 0.3);
493
+ }
494
+
495
+ .btn-picking-active {
496
+ background: rgba(34, 197, 94, 0.3);
497
+ color: #22c55e;
498
+ border: 1px solid rgba(34, 197, 94, 0.5);
499
+ animation: pulse 1.5s ease-in-out infinite;
500
+ }
501
+
502
+ @keyframes pulse {
503
+ 0%, 100% { opacity: 1; }
504
+ 50% { opacity: 0.6; }
505
+ }
506
+
507
+ /* 台风点位列表 */
508
+ .typhoon-point-list {
509
+ margin-bottom: 8px;
510
+ }
511
+
512
+ .typhoon-point-header {
513
+ display: flex;
514
+ justify-content: space-between;
515
+ align-items: center;
516
+ margin-bottom: 6px;
517
+ font-size: 12px;
518
+ color: #94a3b8;
519
+ }
520
+
521
+ .btn-clear-small {
522
+ padding: 2px 8px;
523
+ border: none;
524
+ background: rgba(239, 68, 68, 0.15);
525
+ color: #ef4444;
526
+ border-radius: 4px;
527
+ cursor: pointer;
528
+ font-size: 11px;
529
+ }
530
+
531
+ .btn-clear-small:hover {
532
+ background: rgba(239, 68, 68, 0.3);
533
+ }
534
+
535
+ .typhoon-point-items {
536
+ max-height: 150px;
537
+ overflow-y: auto;
538
+ }
539
+
540
+ .typhoon-point-item {
541
+ display: flex;
542
+ align-items: center;
543
+ gap: 6px;
544
+ padding: 6px 8px;
545
+ background: rgba(30, 41, 59, 0.6);
546
+ border-radius: 6px;
547
+ margin-bottom: 4px;
548
+ }
549
+
550
+ .tp-index {
551
+ width: 20px;
552
+ height: 20px;
553
+ border-radius: 50%;
554
+ background: #f97316;
555
+ color: #fff;
556
+ font-size: 11px;
557
+ display: flex;
558
+ align-items: center;
559
+ justify-content: center;
560
+ flex-shrink: 0;
561
+ }
562
+
563
+ .tp-coords {
564
+ flex: 1;
565
+ font-size: 11px;
566
+ color: #cbd5e1;
567
+ white-space: nowrap;
568
+ overflow: hidden;
569
+ text-overflow: ellipsis;
570
+ font-family: monospace;
571
+ }
572
+
573
+ .tp-delete {
574
+ width: 20px;
575
+ height: 20px;
576
+ border: none;
577
+ background: rgba(239, 68, 68, 0.15);
578
+ color: #ef4444;
579
+ border-radius: 4px;
580
+ cursor: pointer;
581
+ font-size: 12px;
582
+ flex-shrink: 0;
583
+ }
584
+
585
+ .tp-delete:hover {
586
+ background: rgba(239, 68, 68, 0.3);
587
+ }
588
+
589
+ .typhoon-btns {
590
+ display: flex;
591
+ gap: 8px;
592
+ }
593
+ </style>
@@ -0,0 +1,2 @@
1
+ export { default as WeatherControl } from './WeatherControl.vue';
2
+ export { default as FlyAlongRoute } from './FlyAlongRoute.vue';
package/src/config.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Gaea 3D Viewer 配置模块
3
+ * 使用者可通过 setConfig() 在应用初始化时注入配置
4
+ */
5
+
6
+ const defaultConfig = {
7
+ // Gaea 服务地址
8
+ serveUrl: 'http://localhost:8088/gaeaExplorerServer/',
9
+ // 静态资源 Base URL(如 http://localhost:9012)
10
+ serveLocal: '',
11
+ // 初始相机定位 { x: lat, y: lng, z: alt }
12
+ defaultPosition: { x: 27.380913, y: 120.449982, z: 80000 },
13
+ // 地形 TMS 服务
14
+ terrainList: {},
15
+ // 开发模式
16
+ developmentMode: true,
17
+ };
18
+
19
+ let currentConfig = { ...defaultConfig };
20
+
21
+ /**
22
+ * 设置全局配置(在初始化 Gaea 之前调用)
23
+ * @param {Object} config - 配置对象
24
+ * @param {string} config.serveUrl - Gaea 服务 URL
25
+ * @param {string} config.serveLocal - 静态资源 Base URL(必传)
26
+ * @param {Object} config.defaultPosition - 初始定位
27
+ * @param {Object} config.terrainList - 地形图层
28
+ */
29
+ export function setConfig(config) {
30
+ currentConfig = { ...currentConfig, ...config };
31
+ }
32
+
33
+ /**
34
+ * 获取当前配置
35
+ */
36
+ export function getConfig() {
37
+ return { ...currentConfig };
38
+ }
39
+
40
+ export default currentConfig;
package/src/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * gaea-3dview 主入口
3
+ */
4
+
5
+ // 配置
6
+ export { setConfig, getConfig } from './config.js';
7
+ export { default as config } from './config.js';
8
+
9
+ // 核心类
10
+ export { GaeaApp } from './GaeaMethods.js';
11
+ export { default } from './GaeaMethods.js';
12
+
13
+ // 工具函数
14
+ export { screenToLatLng, loadResource } from './utils.js';
package/src/utils.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 工具函数
3
+ */
4
+
5
+ /** 从URL加载资源到Gaea */
6
+ export function loadResource(url, name) {
7
+ return new Promise((res, rej) => {
8
+ if (!window.Gaea || !window.Gaea.GaeaResourceLoader) {
9
+ rej(new Error('Gaea 未初始化'));
10
+ return;
11
+ }
12
+ window.Gaea.GaeaResourceLoader.Instance.LoadResourceFromUrl1(
13
+ url, name,
14
+ (code, msg) => {
15
+ if (code === 200) res(true);
16
+ else rej(new Error(msg));
17
+ }
18
+ );
19
+ });
20
+ }
21
+
22
+ /** 屏幕坐标 → 经纬度 (lat: x, lng: y) */
23
+ export function screenToLatLng(event) {
24
+ if (!window.Gaea || !window.Gaea.GaeaMath) return null;
25
+ const result = window.Gaea.GaeaMath.ProjectPosition(
26
+ new window.Gaea.Vector2(event.offsetX, event.offsetY)
27
+ );
28
+ const lonlat = window.Gaea.GaeaMath.CartesianToSphericalDeg(result);
29
+ return { lat: lonlat.x, lng: lonlat.y, alt: lonlat.z };
30
+ }