@yangyongtao/gaea 1.1.20 → 1.1.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangyongtao/gaea",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
4
4
  "description": "Gaea 3D visualization component library - Vue 3 components based on Godot WASM engine. Includes WMTS layer loading, camera view control, Vite plugin auto-injection, etc.",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -46,9 +46,17 @@ import Gaea from '../public/index.js';
46
46
  transition: background 0.2s; backdrop-filter: blur(4px);
47
47
  }
48
48
  #gaea-reset-btn:hover { background: rgba(0,0,0,0.7); }
49
+ #gaea-layertree-btn {
50
+ position: fixed; top: 16px; right: 60px; z-index: 99990;
51
+ width: 36px; height: 36px; border-radius: 6px; border: none;
52
+ background: rgba(0,0,0,0.5); color: #fff; cursor: pointer;
53
+ font-size: 16px; display: flex; align-items: center; justify-content: center;
54
+ transition: background 0.2s; backdrop-filter: blur(4px);
55
+ }
56
+ #gaea-layertree-btn:hover { background: rgba(0,0,0,0.7); }
49
57
  /* 屏幕图标样式(对齐 ht_3dview) */
50
58
  .gaea-locationLineStyle {
51
- position: absolute; overflow: hidden; z-index: 999; pointer-events: auto;
59
+ position: absolute; overflow: hidden; z-index: 99; pointer-events: auto;
52
60
  user-select: none; -webkit-user-select: none;
53
61
  }
54
62
  .gaea-locationLineStyle .text-wrapper {
@@ -67,7 +75,7 @@ import Gaea from '../public/index.js';
67
75
  }
68
76
  .gaea-iconscreen {
69
77
  position: absolute; overflow: hidden; width: 169px; height: 55px;
70
- user-select: none; -webkit-user-select: none; z-index: 999; pointer-events: auto;
78
+ user-select: none; -webkit-user-select: none; z-index: 99; pointer-events: auto;
71
79
  }
72
80
  .gaea-iconscreen img {
73
81
  position: absolute; width: 100%; height: 100%;
@@ -146,12 +154,16 @@ const defaultConfig = {
146
154
  { name: '苍南县村边界', range: [15, 19], color: [255, 255, 255], index: 60 },
147
155
  { name: '沿浦海塘工程范围线', range: [0, 21], color: [0, 255, 255], index: 120 },
148
156
  ],
157
+ /** 默认加载的倾斜摄影 3D Tiles(空数组则不加载) */
158
+ default3DTiles: [],
149
159
  /** 默认相机视角 */
150
160
  defaultCameraPosition: '27.3680928977018,120.414139621887,0,109379.690132486,0,0',
151
161
  /** 引擎加载进度回调 */
152
162
  onProgress: null,
153
163
  /** DataHub API 地址(图层树数据接口),为空则只用默认数据 */
154
164
  dataHubApiUrl: '',
165
+ /** 是否显示图层树按钮(右上角重置按钮旁边) */
166
+ showLayerTreeBtn: false,
155
167
  /** 图层树分类数据(默认数据,接口数据会合并进来) */
156
168
  layerCategories: [
157
169
  { key: 'seawall', text: '海塘', visible: true, children: [
@@ -265,6 +277,8 @@ export function getConfig() { return _config; }
265
277
 
266
278
  // ==================== GaeaApp ====================
267
279
 
280
+ console.log('[GaeaMethods] 模块已加载 - setCategoryVisible 可用');
281
+
268
282
  export class GaeaApp {
269
283
  constructor(options = {}) {
270
284
  this._options = options;
@@ -272,6 +286,7 @@ export class GaeaApp {
272
286
  this.tLayers = [];
273
287
  this.layerCategories = this.config.layerCategories || [];
274
288
  this._screenIconList = [];
289
+ this._hiddenCategories = new Set();
275
290
  this._secIsClickArr = [];
276
291
  this._secScreenIconKeyValue = {};
277
292
  this._groundPolygonElements = [];
@@ -339,6 +354,7 @@ export class GaeaApp {
339
354
  // 影像/地形就绪即可显示地图,隐藏 loading,避免用户等待模型/接口
340
355
  hideLoading();
341
356
  this._addResetButton();
357
+ this._addLayerTreeButton();
342
358
  console.log('[GaeaApp] 地图已就绪,模型与图层树数据后台加载中...');
343
359
 
344
360
  // 剩余重资源(模型位置、GLTF、图层树接口、屏幕图标)后台异步加载,不阻塞地图显示
@@ -459,7 +475,7 @@ export class GaeaApp {
459
475
  const cfg = this.config;
460
476
 
461
477
  if (cfg.serviceMode === 'online') {
462
- // 在线模式:仅加载天地图影像,不加载地形/贴地线
478
+ // 在线模式:仅加载天地图影像,不加载地形/贴地线/3D Tiles
463
479
  this.addTianditu();
464
480
  } else {
465
481
  // 服务平台模式:WMTS 影像 + 贴地线 + 地形
@@ -474,6 +490,9 @@ export class GaeaApp {
474
490
  try { this.addGroundLines(); } catch (e) { console.error('加载贴地线失败:', e); }
475
491
  // 地形服务
476
492
  try { await this.addInitTerrainServer(); } catch (e) { console.error('加载地形失败:', e); }
493
+ // 倾斜摄影 3D Tiles(isInitLoad 为 true 的自动加载,依赖地形服务)
494
+ const tiles = cfg.default3DTiles || []
495
+ tiles.filter(item => item.isInitLoad).forEach(item => this.add3DTiles(item))
477
496
  }
478
497
 
479
498
  const pos = cfg.defaultCameraPosition;
@@ -486,19 +505,33 @@ export class GaeaApp {
486
505
 
487
506
  // ---- 天地图 ----
488
507
 
489
- /** 加载天地图影像 */
508
+ /** 加载天地图影像 + 注记 */
490
509
  addTianditu() {
491
510
  const cfg = this.config;
492
- const store = new Gaea.TDTWMTSImageStore();
493
- store.ServerUri = cfg.tiandituServerUri || 'https://t{0}.tianditu.gov.cn/img_c/wmts';
494
- store.FormatString = cfg.tiandituFormatString || 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&TILECOL={0}&TILEROW={1}&TILEMATRIX={2}&tk={3}';
495
- store.ImageExtension = cfg.tiandituImageExtension ?? Gaea.ImageExtensionEnum.JPEG;
496
- store.TK = cfg.tiandituToken;
497
- store.Name = '影像底图';
498
- store.LevelRange = new Gaea.Vector2(cfg.tiandituLevelRange[0], cfg.tiandituLevelRange[1]);
499
- Gaea.World.Instance.DefaultPyramidTileSet.AddImageStore(store);
511
+
512
+ // 影像底图(卫星图)
513
+ const imgStore = new Gaea.TDTWMTSImageStore();
514
+ imgStore.ServerUri = cfg.tiandituServerUri || 'https://t{0}.tianditu.gov.cn/img_c/wmts';
515
+ imgStore.FormatString = cfg.tiandituFormatString || 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&TILECOL={0}&TILEROW={1}&TILEMATRIX={2}&tk={3}';
516
+ imgStore.ImageExtension = cfg.tiandituImageExtension ?? Gaea.ImageExtensionEnum.JPEG;
517
+ imgStore.TK = cfg.tiandituToken;
518
+ imgStore.Name = '影像底图';
519
+ imgStore.LevelRange = new Gaea.Vector2(cfg.tiandituLevelRange[0], cfg.tiandituLevelRange[1]);
520
+ Gaea.World.Instance.DefaultPyramidTileSet.AddImageStore(imgStore);
500
521
  console.log('天地图影像加载成功');
501
- return store;
522
+
523
+ // 注记图层(道路/地名标注)
524
+ const labelStore = new Gaea.TDTWMTSImageStore();
525
+ labelStore.ServerUri = 'https://t{0}.tianditu.gov.cn/cia_c/wmts';
526
+ labelStore.FormatString = 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=c&FORMAT=image/png&TILECOL={0}&TILEROW={1}&TILEMATRIX={2}&tk={3}';
527
+ labelStore.ImageExtension = Gaea.ImageExtensionEnum.PNG;
528
+ labelStore.TK = cfg.tiandituToken;
529
+ labelStore.Name = '天地图注记';
530
+ labelStore.LevelRange = new Gaea.Vector2(cfg.tiandituLevelRange[0], cfg.tiandituLevelRange[1]);
531
+ Gaea.World.Instance.DefaultPyramidTileSet.AddImageStore(labelStore);
532
+ console.log('天地图注记加载成功');
533
+
534
+ return { imgStore, labelStore };
502
535
  }
503
536
 
504
537
  /** 添加右上角重置按钮 */
@@ -549,6 +582,19 @@ export class GaeaApp {
549
582
  document.body.appendChild(btn);
550
583
  }
551
584
 
585
+ /** 添加图层树按钮 */
586
+ _addLayerTreeButton() {
587
+ if (document.getElementById('gaea-layertree-btn')) return
588
+ const btn = document.createElement('button')
589
+ btn.id = 'gaea-layertree-btn'
590
+ btn.title = '图层控制'
591
+ btn.innerHTML = '☰'
592
+ btn.onclick = () => {
593
+ document.dispatchEvent(new CustomEvent('gaea-toggle-layertree'))
594
+ }
595
+ document.body.appendChild(btn)
596
+ }
597
+
552
598
  // ---- GLTF 模型 ----
553
599
 
554
600
  async _loadGltfModels() {
@@ -831,8 +877,14 @@ export class GaeaApp {
831
877
  console.warn(`[GaeaApp] 图标 ${child.text} 创建失败:`, e.message);
832
878
  }
833
879
  }
880
+ // 根据配置的 visible 决定初始显隐(不影响创建,只控制初始状态)
881
+ if (cat.visible === false) {
882
+ this.setCategoryVisible(cat.key, false);
883
+ }
834
884
  }
835
885
  console.log('[GaeaApp] 屏幕图标创建完成, 共:', count);
886
+ // 通知外部图标已就绪
887
+ document.dispatchEvent(new CustomEvent('gaea-icons-ready'));
836
888
  }
837
889
 
838
890
  // ---- 图层分类显隐控制 ----
@@ -843,8 +895,17 @@ export class GaeaApp {
843
895
  * @param {boolean} visible 是否可见
844
896
  */
845
897
  setCategoryVisible(categoryKey, visible) {
898
+ // 记录分类显隐状态
899
+ if (visible) {
900
+ this._hiddenCategories.delete(categoryKey)
901
+ } else {
902
+ this._hiddenCategories.add(categoryKey)
903
+ }
904
+ const els = document.querySelectorAll('.gaea-cat-' + categoryKey);
905
+ console.log('[GaeaApp] setCategoryVisible:', categoryKey, visible, '元素数:', els.length);
906
+ els.forEach(el => { el.style.display = visible ? '' : 'none'; });
907
+ // 同步引擎可见性
846
908
  const items = this._screenIconList.filter(item => item.category === categoryKey);
847
- if (!items.length) return;
848
909
  items.forEach(item => {
849
910
  if (visible) {
850
911
  item.element.UseAutoVisible();
@@ -857,14 +918,13 @@ export class GaeaApp {
857
918
  }
858
919
 
859
920
  /**
860
- * 获取某个分类的当前可见性(根据第一个图标判断)
921
+ * 获取某个分类的当前可见性(根据 DOM 元素判断)
861
922
  * @param {string} categoryKey
862
923
  * @returns {boolean}
863
924
  */
864
925
  getCategoryVisible(categoryKey) {
865
- const item = this._screenIconList.find(item => item.category === categoryKey);
866
- if (!item) return true;
867
- return item.element.Visible !== false;
926
+ const el = document.querySelector('.gaea-cat-' + categoryKey);
927
+ return el ? el.style.display !== 'none' : true;
868
928
  }
869
929
 
870
930
  // ---- 公开方法 ----
@@ -1086,6 +1146,68 @@ export class GaeaApp {
1086
1146
  this.SetPosition(`${x},${y},${z},${h},0,0`);
1087
1147
  }
1088
1148
 
1149
+ /**
1150
+ * 根据名称或 id 定位到海塘
1151
+ * @param {string} name - 海塘名称或 id
1152
+ * @param {string} [tilesetUrl] - 可选,倾斜摄影 3D Tiles 的 tileset.json 地址
1153
+ */
1154
+ flyToSeawall = (name, tilesetUrl) => {
1155
+ const cats = this.layerCategories || []
1156
+ for (const cat of cats) {
1157
+ if (!cat.children?.length) continue
1158
+ const found = cat.children.find(c => c.text === name || c.id === name)
1159
+ if (found) {
1160
+ this.flyToNode(found)
1161
+ if (tilesetUrl) {
1162
+ this.add3DTiles(tilesetUrl)
1163
+ }
1164
+ return
1165
+ }
1166
+ }
1167
+ console.warn(`[GaeaApp] 未找到海塘: ${name}`)
1168
+ };
1169
+
1170
+ /**
1171
+ * 加载倾斜摄影 3D Tiles
1172
+ * @param {Object} item - { jsonUrl: tileset.json地址, tilName?: 名称, height?: 高度偏移, isInitLoad?: 是否初始化加载 }
1173
+ */
1174
+ add3DTiles = (item) => {
1175
+ const tilesetUrl = typeof item === 'string' ? item : item.jsonUrl
1176
+ const name = typeof item === 'string' ? '' : (item.tilName || '')
1177
+ const height = typeof item === 'string' ? 0 : (item.height || 0)
1178
+ const tileset = new Gaea.Gaea3DTileset()
1179
+ tileset.Name = name
1180
+ tileset.TilesetUrl = tilesetUrl
1181
+ tileset.EnablePick = true
1182
+ tileset.UseLight = false
1183
+ tileset.SkipLevelOfDetail = false
1184
+ tileset.IsZUp = true
1185
+ tileset.RecalculateNormals = true
1186
+ tileset.BaseUrl = tilesetUrl.slice(0, tilesetUrl.lastIndexOf('/') + 1)
1187
+ tileset.InitTileSet(name, tilesetUrl)
1188
+ tileset.MaximumMemoryUsage = 512
1189
+ tileset.MaximumScreenSpaceError = 28
1190
+ if (height) {
1191
+ tileset.LocalOffset = new Gaea.Vector3(0, height, 0)
1192
+ }
1193
+ Gaea.World.Instance.RenderableObjectList.AddLast(tileset)
1194
+ console.log('[GaeaApp] 3D Tiles 已加载:', name || tilesetUrl, height ? `高度: ${height}` : '')
1195
+ return tileset
1196
+ };
1197
+
1198
+ /**
1199
+ * 根据经纬度飞到指定位置
1200
+ * @param {number} lat - 纬度
1201
+ * @param {number} lng - 经度
1202
+ * @param {number} [z=0] - 地面高程
1203
+ * @param {number} [alt=500] - 相机高度
1204
+ * @param {number} [pitch=0] - 俯仰角,默认 0(垂直俯视),负值向前倾斜
1205
+ * @param {number} [yaw=0] - 偏航角
1206
+ */
1207
+ flyToPosition = (lat, lng, z = 0, alt = 500, pitch = 0, yaw = 0) => {
1208
+ this.SetPosition(`${lat},${lng},${z},${alt},${pitch},${yaw}`)
1209
+ };
1210
+
1089
1211
  // ---- 沿线飞行 ----
1090
1212
 
1091
1213
  /**
@@ -1263,12 +1385,17 @@ export class GaeaApp {
1263
1385
 
1264
1386
  } // end else (externalDivId)
1265
1387
 
1388
+ // 打分类标记,用于按类别显隐
1389
+ if (category) {
1390
+ container.classList.add('gaea-cat-' + category);
1391
+ }
1392
+
1266
1393
  if (!externalDivId) {
1267
1394
  document.body.appendChild(container);
1268
1395
  }
1269
1396
 
1270
1397
  container.style.position = 'absolute';
1271
- container.style.zIndex = '9999';
1398
+ container.style.zIndex = '99';
1272
1399
  container.style.pointerEvents = 'auto';
1273
1400
  container.style.cursor = 'pointer';
1274
1401
 
@@ -1338,6 +1465,35 @@ export class GaeaApp {
1338
1465
  Gaea.World.Instance.DefaultCamera.GotoFavorite(uri);
1339
1466
  }
1340
1467
 
1468
+ /**
1469
+ * 添加点位标记到 3D 地图
1470
+ * @param {Object} opts
1471
+ * @param {string} opts.name - 唯一标识名
1472
+ * @param {string} [opts.text] - 显示文本
1473
+ * @param {number} opts.lat - 纬度
1474
+ * @param {number} opts.lng - 经度
1475
+ * @param {number} [opts.z=0] - 高度
1476
+ * @param {string} [opts.imgUrl] - 图标地址
1477
+ * @param {'textImage'|'styled'|'imageOnly'} [opts.method='textImage'] - 显示样式
1478
+ * @param {Array<number>} [opts.offset=[0,0]] - 屏幕偏移
1479
+ * @param {Function} [opts.onClick] - 点击回调
1480
+ * @param {string} [opts.category] - 分类标识(用于图层树显隐控制)
1481
+ * @returns {Gaea.ScreenGUIElement} screen 元素
1482
+ */
1483
+ addMarker({ name, text, lat, lng, z = 0, imgUrl, method = 'textImage', offset = [0, 0], onClick, category } = {}) {
1484
+ return this.addScreenIcon({
1485
+ name,
1486
+ text: text || name,
1487
+ imgUrl,
1488
+ position: { x: lat, y: lng, z },
1489
+ method,
1490
+ offset,
1491
+ onClick,
1492
+ category,
1493
+ type: 'marker'
1494
+ })
1495
+ }
1496
+
1341
1497
  // ---- 引擎动画方法 ----
1342
1498
 
1343
1499
  hiddenScreenWithClass(className) {
@@ -1853,6 +2009,30 @@ export class GaeaApp {
1853
2009
  this._typhoonElement = null;
1854
2010
  }
1855
2011
  };
2012
+
2013
+ /** 显示所有屏幕标记 */
2014
+ showAllMarkers = () => {
2015
+ this._screenIconList.forEach(item => {
2016
+ // 跳过被隐藏分类的标记
2017
+ if (this._hiddenCategories.has(item.category)) return
2018
+ item.element.UseAutoVisible()
2019
+ item.element.EnablePick = true
2020
+ // 同步显示 DOM 元素
2021
+ const el = document.getElementById(item.element.DivID)
2022
+ if (el) el.style.display = ''
2023
+ })
2024
+ };
2025
+
2026
+ /** 隐藏所有屏幕标记 */
2027
+ hideAllMarkers = () => {
2028
+ this._screenIconList.forEach(item => {
2029
+ item.element.ForceSetVisible(false)
2030
+ item.element.EnablePick = false
2031
+ // 同步隐藏 DOM 元素
2032
+ const el = document.getElementById(item.element.DivID)
2033
+ if (el) el.style.display = 'none'
2034
+ })
2035
+ };
1856
2036
  }
1857
2037
 
1858
2038
  export default GaeaApp;
@@ -18,14 +18,9 @@
18
18
  <span v-else>▾</span>
19
19
  </span>
20
20
  <!-- 分类复选框 -->
21
- <label class="category-checkbox" @click.stop>
22
- <input
23
- type="checkbox"
24
- :checked="categoryVisibility[rootNode.key] !== false"
25
- @change="onCategoryToggle(rootNode.key, $event.target.checked)"
26
- />
27
- <span class="checkmark"></span>
28
- </label>
21
+ <div class="category-checkbox" @click.stop="onCategoryToggle(rootNode.key, !categoryVisibility[rootNode.key])">
22
+ <span class="checkmark" :class="{ checked: categoryVisibility[rootNode.key] !== false }"></span>
23
+ </div>
29
24
  <span class="node-text" :class="{ 'expanded-yellow': rootNode.isExpanded }">{{ rootNode.text }}</span>
30
25
  </div>
31
26
 
@@ -63,12 +58,10 @@ function handleHideLayerTree() {
63
58
 
64
59
  onMounted(() => {
65
60
  const raw = props.appInstance?.layerCategories || [];
66
- categories.value = raw
67
- .filter(c => c.visible !== false)
68
- .map(c => ({ ...c, isExpanded: false }));
69
- // 初始化每个分类的可见性状态
61
+ categories.value = raw.map(c => ({ ...c, isExpanded: false }));
62
+ // 初始化每个分类的可见性状态:按配置的 visible 字段
70
63
  for (const cat of categories.value) {
71
- categoryVisibility[cat.key] = props.appInstance?.getCategoryVisible?.(cat.key) ?? true;
64
+ categoryVisibility[cat.key] = cat.visible !== false;
72
65
  }
73
66
  window.addEventListener('gaea-hide-layertree', handleHideLayerTree);
74
67
  });
@@ -78,6 +71,7 @@ onBeforeUnmount(() => {
78
71
  });
79
72
 
80
73
  function onCategoryToggle(categoryKey, checked) {
74
+ console.log('[LayerTree] onCategoryToggle:', categoryKey, checked, 'appInstance:', !!props.appInstance, 'setCategoryVisible:', !!props.appInstance?.setCategoryVisible);
81
75
  categoryVisibility[categoryKey] = checked;
82
76
  props.appInstance?.setCategoryVisible?.(categoryKey, checked);
83
77
  }
@@ -129,8 +123,8 @@ const filteredData = computed(() => {
129
123
  <style scoped>
130
124
  .layer-tree-dialog {
131
125
  position: fixed;
132
- top: 10px;
133
- left: 10px;
126
+ top: 120px;
127
+ right: 20px;
134
128
  width: 300px;
135
129
  background: var(--lt-bg);
136
130
  border-radius: var(--lt-border-radius);
@@ -312,7 +306,6 @@ const filteredData = computed(() => {
312
306
 
313
307
  /* ===== 分类复选框 ===== */
314
308
  .category-checkbox {
315
- position: relative;
316
309
  display: inline-flex;
317
310
  align-items: center;
318
311
  justify-content: center;
@@ -323,13 +316,6 @@ const filteredData = computed(() => {
323
316
  user-select: none;
324
317
  }
325
318
 
326
- .category-checkbox input[type="checkbox"] {
327
- position: absolute;
328
- opacity: 0;
329
- width: 0;
330
- height: 0;
331
- }
332
-
333
319
  .checkmark {
334
320
  width: 100%;
335
321
  height: 100%;
@@ -342,12 +328,12 @@ const filteredData = computed(() => {
342
328
  transition: all 0.15s ease;
343
329
  }
344
330
 
345
- .category-checkbox input:checked + .checkmark {
331
+ .checkmark.checked {
346
332
  background: var(--lt-checkbox-checked-bg);
347
333
  border-color: var(--lt-checkbox-checked-bg);
348
334
  }
349
335
 
350
- .category-checkbox input:checked + .checkmark::after {
336
+ .checkmark.checked::after {
351
337
  content: '';
352
338
  width: 4px;
353
339
  height: 8px;