@yangyongtao/gaea 1.1.11 → 1.1.13
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/CHANGELOG.md +5 -0
- package/README.md +178 -52
- package/package.json +5 -3
- package/scripts/postinstall.mjs +18 -107
- package/src/GaeaMethods.js +127 -38
- package/src/components/VectorPolygonControl.vue +396 -0
- package/src/components/index.cjs +6 -0
- package/src/components/index.js +1 -0
- package/src/index.cjs +11 -0
- package/src/plugin.cjs +6 -0
- package/USAGE.md +0 -200
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
# @yangyongtao/gaea
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
基于 Godot WASM 引擎的 Vue 3 三维可视化组件库。提供影像/地形/贴地线加载、相机视角控制、图层树、天气控制、沿线飞行、断面游览、室内游览等能力,并内置 Vite 插件自动注入引擎资源。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> 完整可运行示例参考消费者项目的 `src/App.vue`,本文所有片段均以其为参考。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 安装
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
12
|
npm install @yangyongtao/gaea
|
|
9
13
|
```
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
peerDependencies(按需安装):`vue ^3.4`、`element-plus ^2.6`(可选)、`pinia ^2.1`(可选)。
|
|
16
|
+
|
|
17
|
+
> ⚠️ 引擎大资源文件(`Gaea.pck` / `Gaea.wasm` / `1111.res` / `gltf` 等)**不随 npm 包分发**,需手动放置,详见第 8 节。
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Vite 配置
|
|
22
|
+
|
|
23
|
+
引入并注册 `gaeaVitePlugin`,它会自动注入 `Gaea.js`、提供引擎静态资源、设置 SharedArrayBuffer 所需的跨域隔离响应头。
|
|
14
24
|
|
|
15
25
|
```js
|
|
26
|
+
// vite.config.js
|
|
16
27
|
import { defineConfig } from 'vite';
|
|
17
28
|
import vue from '@vitejs/plugin-vue';
|
|
18
29
|
import { gaeaVitePlugin } from '@yangyongtao/gaea/plugin';
|
|
@@ -24,95 +35,210 @@ export default defineConfig({
|
|
|
24
35
|
});
|
|
25
36
|
```
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. 快速开始
|
|
41
|
+
|
|
42
|
+
页面里需要一个 `id="canvas"` 的画布。调用 `setConfig(...)` 后 `new GaeaApp()` 即可自动完成「引擎启动 → 环境初始化 → 图层加载 → 模型加载」。
|
|
28
43
|
|
|
29
44
|
```vue
|
|
30
45
|
<template>
|
|
31
46
|
<div id="gaea-app">
|
|
47
|
+
<div class="toolbar">
|
|
48
|
+
<button @click="showWeather = !showWeather">天气控制</button>
|
|
49
|
+
<button @click="showFly = !showFly">沿线飞行</button>
|
|
50
|
+
<button @click="showLayerTree = !showLayerTree">图层控制</button>
|
|
51
|
+
<WaterGateTour />
|
|
52
|
+
</div>
|
|
32
53
|
|
|
33
|
-
<div class="canvas-area">
|
|
54
|
+
<div class="canvas-area" id="canvas-container">
|
|
34
55
|
<canvas id="canvas"></canvas>
|
|
35
56
|
</div>
|
|
57
|
+
|
|
58
|
+
<WeatherControl v-if="showWeather" :appInstance="app" @close="showWeather = false" />
|
|
59
|
+
<FlyAlongRoute v-if="showFly" :appInstance="app" @close="showFly = false" />
|
|
60
|
+
<LayerTree v-if="showLayerTree" :appInstance="app" @close="showLayerTree = false" @node-click="onLayerNodeClick" />
|
|
61
|
+
<HydrologicStationDialog />
|
|
62
|
+
<SecTourView />
|
|
36
63
|
</div>
|
|
37
64
|
</template>
|
|
38
65
|
|
|
39
66
|
<script setup>
|
|
40
67
|
import { ref } from 'vue';
|
|
41
68
|
import { GaeaApp, setConfig } from '@yangyongtao/gaea';
|
|
69
|
+
import { WeatherControl, FlyAlongRoute, HydrologicStationDialog, SecTourView, WaterGateTour } from '@yangyongtao/gaea/components';
|
|
70
|
+
import LayerTree from '@yangyongtao/gaea/components/LayerTree.vue';
|
|
42
71
|
|
|
43
|
-
// 配置
|
|
44
72
|
setConfig({
|
|
45
|
-
serveLocal: `${window.location.origin}/static
|
|
73
|
+
serveLocal: `${window.location.origin}/static`,
|
|
46
74
|
serveUrl: 'http://192.168.3.151:8088/gaeaExplorerServer/',
|
|
47
|
-
|
|
48
|
-
{ layerName: 'earthland', forMat: 'image/png', min: 0, max: 18, transparency: 0.3 },
|
|
49
|
-
{ layerName: '苍南L19', forMat: 'image/png', min: 5, max: 17 },
|
|
50
|
-
],
|
|
51
|
-
useTianditu: true,
|
|
52
|
-
tiandituToken: '4ebb3872e465273416314341a99caea6',
|
|
75
|
+
serviceMode: 'platform',
|
|
53
76
|
defaultCameraPosition: '27.3680928977018,120.414139621887,0,109379.690132486,0,0',
|
|
54
77
|
});
|
|
55
|
-
|
|
78
|
+
|
|
56
79
|
const app = new GaeaApp();
|
|
57
|
-
window.APP = app;
|
|
80
|
+
window.APP = app; // 组件内部通过 window.APP 兜底获取实例
|
|
58
81
|
|
|
59
82
|
const showWeather = ref(false);
|
|
60
83
|
const showFly = ref(false);
|
|
84
|
+
const showLayerTree = ref(false);
|
|
85
|
+
|
|
86
|
+
function onLayerNodeClick({ node }) {
|
|
87
|
+
app.flyToNode(node);
|
|
88
|
+
}
|
|
61
89
|
</script>
|
|
62
90
|
```
|
|
63
91
|
|
|
64
|
-
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. setConfig 配置项
|
|
65
95
|
|
|
66
|
-
|
|
96
|
+
| 参数 | 类型 | 默认值 | 说明 |
|
|
97
|
+
|------|------|--------|------|
|
|
98
|
+
| `serveLocal` | string | `''` | 引擎本地静态资源地址(AssetHost),一般为 `${origin}/static` |
|
|
99
|
+
| `serveUrl` | string | `''` | 服务平台地址(WMTS 影像 / 地形 / 贴地线均基于此) |
|
|
100
|
+
| `serviceMode` | `'platform' \| 'online'` | `'platform'` | **服务模式**:见下方说明 |
|
|
101
|
+
| `tiandituToken` | string | — | 在线模式的天地图 token |
|
|
102
|
+
| `tiandituLevelRange` | [number,number] | `[0,18]` | 天地图层级范围 |
|
|
103
|
+
| `defaultLayers` | Array | 见下 | 服务平台模式下加载的影像图层 |
|
|
104
|
+
| `defaultTerrains` | Array | 见下 | 服务平台模式下加载的地形服务 |
|
|
105
|
+
| `defaultGroundLines` | Array | 见下 | 服务平台模式下加载的贴地线 |
|
|
106
|
+
| `defaultCameraPosition` | string | — | 初始相机视角:`纬度,经度,高程,相机高,俯仰,朝向` |
|
|
107
|
+
| `layerCategories` | Array | 内置 | 图层树分类数据(接口数据会合并进来) |
|
|
108
|
+
| `dataHubApiUrl` | string | `''` | 图层树数据接口地址,为空则只用默认数据 |
|
|
109
|
+
| `onProgress` | function | — | 引擎加载进度回调 `(cur, total) => {}` |
|
|
110
|
+
|
|
111
|
+
### 4.1 serviceMode —— 服务模式(区分「服务平台」与「在线」)
|
|
112
|
+
|
|
113
|
+
- `'platform'`(服务平台):影像走 `serveUrl` 的 WMTS,并加载 `defaultTerrains` 地形、`defaultGroundLines` 贴地线。
|
|
114
|
+
- `'online'`(在线):**仅加载天地图影像**,不加载地形和贴地线(在线模式目前只有天地图一种影像来源)。
|
|
67
115
|
|
|
68
116
|
```js
|
|
69
|
-
|
|
117
|
+
setConfig({ serviceMode: 'platform' }); // 内网/服务平台
|
|
118
|
+
// 或
|
|
119
|
+
setConfig({ serviceMode: 'online', tiandituToken: '你的token' }); // 在线(天地图)
|
|
120
|
+
```
|
|
70
121
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
### 4.2 defaultLayers —— 影像图层(服务平台模式)
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
defaultLayers: [
|
|
126
|
+
{ layerName: 'earthland', forMat: 'image/png', min: 0, max: 18, transparency: 0.3 },
|
|
127
|
+
{ layerName: '苍南L19', forMat: 'image/png', min: 5, max: 17 },
|
|
128
|
+
]
|
|
129
|
+
```
|
|
130
|
+
- `layerName`:WMTS 服务里的图层名(Title)
|
|
131
|
+
- `forMat`:影像格式,如 `image/png`、`image/jpeg`
|
|
132
|
+
- `min` / `max`:显示层级范围
|
|
133
|
+
- `transparency`:透明度(可选)
|
|
134
|
+
|
|
135
|
+
### 4.3 defaultTerrains —— 地形服务(服务平台模式)
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
defaultTerrains: [
|
|
139
|
+
{ name: '苍南县地形', url: 'htc/service/tms/1.0.0/苍南县地形@EPSG%3A4326@terrain/' },
|
|
140
|
+
]
|
|
78
141
|
```
|
|
142
|
+
- `name`:地形名称
|
|
143
|
+
- `url`:相对 `serveUrl` 的路径(也支持传完整 `http(s)://` 地址)
|
|
144
|
+
- 空数组 `[]` 则不加载地形
|
|
145
|
+
|
|
146
|
+
### 4.4 defaultGroundLines —— 贴地线(服务平台模式)
|
|
79
147
|
|
|
80
|
-
|
|
148
|
+
依赖 `serveUrl` 的 WMTS 矢量线服务(自动通过 `getAllServe` 获取服务列表后按名匹配)。
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
defaultGroundLines: [
|
|
152
|
+
{ name: '苍南县边界', range: [0, 9], color: [255, 255, 0], index: 100 },
|
|
153
|
+
{ name: '苍南县镇边界', range: [9, 15], color: [0, 255, 0], index: 80 },
|
|
154
|
+
{ name: '苍南县村边界', range: [15, 19], color: [255, 255, 255], index: 60 },
|
|
155
|
+
{ name: '沿浦海塘工程范围线', range: [0, 21], color: [0, 255, 255], index: 120 },
|
|
156
|
+
]
|
|
157
|
+
```
|
|
158
|
+
- `name`:矢量线服务名(Title)
|
|
159
|
+
- `range`:显示层级范围 `[min, max]`
|
|
160
|
+
- `color`:线颜色 `[r, g, b]`(0-255)
|
|
161
|
+
- `index`:渲染优先级
|
|
162
|
+
- 每项可加 `isInitLoad: false` 跳过该条
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 5. 组件说明
|
|
167
|
+
|
|
168
|
+
组件从 `@yangyongtao/gaea/components` 导入;`LayerTree` 需按路径单独导入。多数交互组件接收 `:appInstance="app"`,未传时内部回退到 `window.APP`。
|
|
169
|
+
|
|
170
|
+
| 组件 | 说明 | 主要事件 |
|
|
171
|
+
|------|------|----------|
|
|
172
|
+
| `WeatherControl` | 天气/台风控制面板(含台风路线拾取与播放) | `@close` |
|
|
173
|
+
| `FlyAlongRoute` | 沿线飞行:地图拾取点位,可设飞行高度/时长 | `@close` |
|
|
174
|
+
| `LayerTree` | 图层树,点击节点飞行定位 | `@close`、`@node-click` |
|
|
175
|
+
| `HydrologicStationDialog` | 水文测站弹窗,自监听 `gaea-icon-click` 事件 | — |
|
|
176
|
+
| `SecTourView` | 断面游览滚动条,模型锁定后显示 | — |
|
|
177
|
+
| `WaterGateTour` | 室内游览按钮(下拉选择水闸模型) | — |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 6. 常用实例方法(window.APP / app)
|
|
81
182
|
|
|
82
183
|
| 方法 | 说明 |
|
|
83
184
|
|------|------|
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
185
|
+
| `SetPosition('纬度,经度,高程,相机高,俯仰,朝向')` | 定位到指定视角 |
|
|
186
|
+
| `flyToNode({ position:{x,y,z} })` | 飞到指定位置 |
|
|
187
|
+
| `flyAlongPath(lineData, options)` | 沿线飞行,`lineData=[{x:纬度,y:经度,z:高度}]`,`options={duration,xrotation,lockEye,...}` |
|
|
188
|
+
| `stopFlyAlongPath()` | 停止沿线飞行 |
|
|
189
|
+
| `cameraReset()` | 相机复位到 `defaultCameraPosition` |
|
|
190
|
+
| `addInitTerrainServer(terrains?)` | 手动加载地形(不传则用 `defaultTerrains`) |
|
|
191
|
+
| `addGroundLines(lines?)` | 手动加载贴地线(不传则用 `defaultGroundLines`) |
|
|
192
|
+
| `addTyphoonRoute(pointList, options)` | 加载台风路线并播放动画 |
|
|
193
|
+
| `clearTyphoonRoute()` | 清除台风路线 |
|
|
194
|
+
| `getAllServe()` | 获取 WMTS 服务图层列表 |
|
|
195
|
+
| `addLayer({ layerName, forMat, min, max, transparency })` | 添加影像图层 |
|
|
196
|
+
| `favorite()` / `favoritePosition(uri)` | 记录 / 恢复视角 |
|
|
91
197
|
|
|
92
|
-
|
|
198
|
+
---
|
|
93
199
|
|
|
94
|
-
|
|
95
|
-
<script setup>
|
|
96
|
-
import { WeatherControl, FlyAlongRoute } from '@yangyongtao/gaea/components';
|
|
200
|
+
## 7. 注意事项
|
|
97
201
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
202
|
+
- 页面必须存在 `id="canvas"` 的 `<canvas>`,引擎会挂载到它上面。
|
|
203
|
+
- 需要跨域隔离(COOP/COEP)才能启用 SharedArrayBuffer,`gaeaVitePlugin` 已自动处理;首次进入若未隔离会自动刷新一次。
|
|
204
|
+
- 服务平台模式下地形/贴地线依赖 `serveUrl` 对应服务,若服务器无对应服务名,控制台会打印「未找到/加载失败」警告,不影响其它功能。
|
|
205
|
+
- 大体积引擎资源不随 npm 包分发,需手动放置,详见第 8 节。
|
|
101
206
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 8. 大资源文件的放置
|
|
210
|
+
|
|
211
|
+
引擎运行需要的大文件体积过大,**不包含在 npm 包内,也不会自动下载**(`postinstall` 仅做检测与提示)。请通过内网传输或手动拷贝的方式,将它们放到消费者项目里 gaea 包的 `public` 目录下。
|
|
212
|
+
|
|
213
|
+
### 8.1 需要放置的文件
|
|
214
|
+
|
|
215
|
+
放置目标目录:`node_modules/@yangyongtao/gaea/public/`
|
|
216
|
+
|
|
217
|
+
| 文件 / 目录 | 目标位置 | 参考大小 | 说明 |
|
|
218
|
+
|------|------|------|------|
|
|
219
|
+
| `Gaea.pck` | `public/Gaea.pck` | ~450 MB | 引擎主资源包 |
|
|
220
|
+
| `Gaea.wasm` | `public/Gaea.wasm` | ~16 MB | 引擎 WASM |
|
|
221
|
+
| `1111.res` | `public/static/1111.res` | **~34.6 MB** | 点图标/纹理资源包(务必用完整版,见下方警告) |
|
|
222
|
+
| `gltf/` | `public/static/gltf/` | 若干 | 断面/水闸等 GLTF 模型 |
|
|
223
|
+
|
|
224
|
+
> ⚠️ **重要**:`1111.res` 必须使用 **完整版(约 34.6 MB)**。早期存在一个残缺的 6.5MB 版本,会导致点图标纹理为空、加载台风路线时引擎崩溃(`SimpleFeatureRenderer.CombineSymbolMeshs` 空引用)。放置后可用文件大小快速校验。
|
|
225
|
+
|
|
226
|
+
### 8.2 校验是否就位
|
|
227
|
+
|
|
228
|
+
```powershell
|
|
229
|
+
# Windows PowerShell
|
|
230
|
+
Get-Item node_modules/@yangyongtao/gaea/public/Gaea.pck | Select-Object Length
|
|
231
|
+
Get-Item node_modules/@yangyongtao/gaea/public/static/1111.res | Select-Object Length
|
|
106
232
|
```
|
|
233
|
+
`Gaea.pck` 约 450MB、`1111.res` 约 34.6MB 即为正确。
|
|
234
|
+
|
|
235
|
+
### 8.3 注意事项
|
|
107
236
|
|
|
108
|
-
|
|
237
|
+
- 更换 `1111.res` 后,若浏览器曾加载过旧的残缺资源,引擎会把资源按名缓存进持久化虚拟文件系统,需**强制刷新(Ctrl+Shift+R)**一次;本库已用版本化资源名规避该缓存问题。
|
|
238
|
+
- 若不放置这些文件,`npm install` 后控制台会打印提示,且页面无法正常渲染三维场景。
|
|
239
|
+
- CI 等无需下载/提示的环境,可设置环境变量 `GAEA_SKIP_DOWNLOAD=1` 跳过 postinstall 提示。
|
|
109
240
|
|
|
110
|
-
|
|
111
|
-
|------|------|------|
|
|
112
|
-
| `GaeaApp`, `setConfig`, `getConfig` | `@yangyongtao/gaea` | 核心功能 |
|
|
113
|
-
| `loadResource`, `screenToLatLng` | `@yangyongtao/gaea` | 工具函数 |
|
|
114
|
-
| `WeatherControl`, `FlyAlongRoute` | `@yangyongtao/gaea/components` | Vue 组件 |
|
|
115
|
-
| `gaeaVitePlugin`, `GaeaPlugin` | `@yangyongtao/gaea/plugin` | Vite 插件 / v-drag 指令 |
|
|
241
|
+
---
|
|
116
242
|
|
|
117
243
|
## License
|
|
118
244
|
|
package/package.json
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yangyongtao/gaea",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.13",
|
|
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
|
-
"main": "src/index.
|
|
6
|
+
"main": "src/index.cjs",
|
|
7
7
|
"module": "src/index.js",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"import": "./src/index.js",
|
|
11
|
+
"require": "./src/index.cjs",
|
|
11
12
|
"default": "./src/index.js"
|
|
12
13
|
},
|
|
13
14
|
"./components": {
|
|
14
15
|
"import": "./src/components/index.js",
|
|
16
|
+
"require": "./src/components/index.cjs",
|
|
15
17
|
"default": "./src/components/index.js"
|
|
16
18
|
},
|
|
17
19
|
"./components/*": "./src/components/*",
|
|
18
20
|
"./plugin": {
|
|
19
21
|
"import": "./src/plugin.js",
|
|
22
|
+
"require": "./src/plugin.cjs",
|
|
20
23
|
"default": "./src/plugin.js"
|
|
21
24
|
}
|
|
22
25
|
},
|
|
@@ -24,7 +27,6 @@
|
|
|
24
27
|
"src/",
|
|
25
28
|
"scripts/",
|
|
26
29
|
"README.md",
|
|
27
|
-
"USAGE.md",
|
|
28
30
|
"CHANGELOG.md",
|
|
29
31
|
"public/*.js",
|
|
30
32
|
"public/*.html",
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,128 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Post-install script
|
|
2
|
+
* Post-install script.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 说明:引擎与大资源文件(Gaea.pck / Gaea.wasm / static/1111.res / static/gltf/ 等)
|
|
5
|
+
* 体积较大,不随 npm 包分发,也不再自动从 GitHub 下载。
|
|
6
|
+
* 请通过其它方式(内网传输 / 手动拷贝)将这些文件放置到消费者项目的 public/static 目录,
|
|
7
|
+
* 详见 README.md 第 8 节「大资源文件的放置」。
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
* gaea-assets-v{version}.zip (包含 Gaea.pck, Gaea.wasm, static/gltf/, static/1111.res)
|
|
8
|
-
*
|
|
9
|
-
* Zip 内部结构:
|
|
10
|
-
* Gaea.pck
|
|
11
|
-
* Gaea.wasm
|
|
12
|
-
* static/
|
|
13
|
-
* gltf/ (所有 .gltf 文件)
|
|
14
|
-
* 1111.res
|
|
9
|
+
* 如需强制跳过本脚本的任何提示:设置环境变量 GAEA_SKIP_DOWNLOAD=1
|
|
15
10
|
*/
|
|
16
11
|
|
|
17
12
|
import fs from 'fs';
|
|
18
13
|
import path from 'path';
|
|
19
14
|
import { fileURLToPath } from 'url';
|
|
20
|
-
import https from 'https';
|
|
21
15
|
|
|
22
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
17
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
24
18
|
const PUBLIC_DIR = path.join(PACKAGE_ROOT, 'public');
|
|
25
|
-
const PKG = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
26
|
-
const VERSION = PKG.version;
|
|
27
|
-
|
|
28
|
-
const ASSETS_VERSION = '1.1.1'; // 大文件资源版本,不随 npm 版本变动
|
|
29
|
-
const ZIP_NAME = `gaea-assets-v${ASSETS_VERSION}.zip`;
|
|
30
|
-
const GITHUB_RELEASES = `https://github.com/smallflypig/Gaea/releases/download/v${ASSETS_VERSION}`;
|
|
31
|
-
const ZIP_URL = `${GITHUB_RELEASES}/${ZIP_NAME}`;
|
|
32
|
-
|
|
33
|
-
// ── helpers ──────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
function download(url, dest) {
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
const file = fs.createWriteStream(dest);
|
|
38
|
-
https.get(url, (res) => {
|
|
39
|
-
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
40
|
-
file.close();
|
|
41
|
-
try { fs.unlinkSync(dest); } catch {}
|
|
42
|
-
return download(res.headers.location, dest).then(resolve, reject);
|
|
43
|
-
}
|
|
44
|
-
if (res.statusCode !== 200) {
|
|
45
|
-
file.close();
|
|
46
|
-
try { fs.unlinkSync(dest); } catch {}
|
|
47
|
-
reject(new Error(`HTTP ${res.statusCode}`));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const total = parseInt(res.headers['content-length'], 10) || 0;
|
|
52
|
-
let downloaded = 0;
|
|
53
|
-
|
|
54
|
-
res.on('data', (chunk) => {
|
|
55
|
-
downloaded += chunk.length;
|
|
56
|
-
if (total > 0 && downloaded % (50 * 1024) < chunk.length) {
|
|
57
|
-
const pct = Math.round((downloaded / total) * 100);
|
|
58
|
-
process.stdout.write(` ${pct}%\r`);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
res.pipe(file);
|
|
63
|
-
|
|
64
|
-
file.on('finish', () => {
|
|
65
|
-
file.close();
|
|
66
|
-
process.stdout.write('\r');
|
|
67
|
-
resolve();
|
|
68
|
-
});
|
|
69
|
-
}).on('error', (err) => {
|
|
70
|
-
file.close();
|
|
71
|
-
try { fs.unlinkSync(dest); } catch {}
|
|
72
|
-
reject(err);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ── main ─────────────────────────────────────────────────────────────
|
|
78
19
|
|
|
79
20
|
if (process.env.GAEA_SKIP_DOWNLOAD === '1') {
|
|
80
|
-
console.log('[gaea] GAEA_SKIP_DOWNLOAD=1, skipping.');
|
|
81
21
|
process.exit(0);
|
|
82
22
|
}
|
|
83
23
|
|
|
84
|
-
//
|
|
24
|
+
// 检测关键资源是否已就位(Gaea.pck 通常 >100MB)
|
|
85
25
|
const MARKER = path.join(PUBLIC_DIR, 'Gaea.pck');
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
26
|
+
const ready = fs.existsSync(MARKER) && fs.statSync(MARKER).size > 100 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
if (ready) {
|
|
29
|
+
console.log('[gaea] 引擎资源已就位。');
|
|
30
|
+
} else {
|
|
31
|
+
console.log(
|
|
32
|
+
'\n[gaea] 未检测到引擎大资源文件(Gaea.pck / Gaea.wasm / static/1111.res / static/gltf 等)。\n' +
|
|
33
|
+
'[gaea] 这些文件不随 npm 包分发,请通过内网/手动方式放置到项目静态资源目录后再运行。\n' +
|
|
34
|
+
'[gaea] 放置方法详见 README.md 第 8 节。\n'
|
|
35
|
+
);
|
|
89
36
|
}
|
|
90
37
|
|
|
91
|
-
|
|
92
|
-
console.log(`[gaea] ${ZIP_URL}`);
|
|
93
|
-
|
|
94
|
-
const tmpZip = path.join(PUBLIC_DIR, ZIP_NAME);
|
|
95
|
-
|
|
96
|
-
(async () => {
|
|
97
|
-
try {
|
|
98
|
-
// 1. Download zip
|
|
99
|
-
await download(ZIP_URL, tmpZip);
|
|
100
|
-
|
|
101
|
-
// 2. Extract
|
|
102
|
-
console.log('[gaea] Extracting...');
|
|
103
|
-
const { execSync } = await import('child_process');
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
execSync(`powershell -Command "Expand-Archive -Path '${tmpZip}' -DestinationPath '${PUBLIC_DIR}' -Force"`, {
|
|
107
|
-
stdio: 'pipe',
|
|
108
|
-
});
|
|
109
|
-
} catch {
|
|
110
|
-
// Fallback: try unzip
|
|
111
|
-
execSync(`unzip -o "${tmpZip}" -d "${PUBLIC_DIR}"`, { stdio: 'pipe' });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 3. Cleanup
|
|
115
|
-
fs.unlinkSync(tmpZip);
|
|
38
|
+
process.exit(0);
|
|
116
39
|
|
|
117
|
-
console.log('[gaea] Assets installed successfully.');
|
|
118
|
-
} catch (e) {
|
|
119
|
-
console.error(`\n[gaea] Download failed: ${e.message}`);
|
|
120
|
-
console.error(
|
|
121
|
-
`[gaea] Please download manually: ${ZIP_URL}\n` +
|
|
122
|
-
` Extract to: ${PUBLIC_DIR}\n` +
|
|
123
|
-
` Or set GAEA_SKIP_DOWNLOAD=1 to skip.`
|
|
124
|
-
);
|
|
125
|
-
try { fs.unlinkSync(tmpZip); } catch {}
|
|
126
|
-
process.exit(0);
|
|
127
|
-
}
|
|
128
|
-
})();
|
package/src/GaeaMethods.js
CHANGED
|
@@ -274,6 +274,7 @@ export class GaeaApp {
|
|
|
274
274
|
this._screenIconList = [];
|
|
275
275
|
this._secIsClickArr = [];
|
|
276
276
|
this._secScreenIconKeyValue = {};
|
|
277
|
+
this._groundPolygonElements = [];
|
|
277
278
|
this.tweenGroup = new TWEEN.Group();
|
|
278
279
|
this._tourConfig = {
|
|
279
280
|
'沿浦水闸': { direction: [-0.5, 0.3, 0.3], positionList: [
|
|
@@ -335,47 +336,59 @@ export class GaeaApp {
|
|
|
335
336
|
await this._initLayers();
|
|
336
337
|
console.log('[GaeaApp] 图层加载完成');
|
|
337
338
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
339
|
+
// 影像/地形就绪即可显示地图,隐藏 loading,避免用户等待模型/接口
|
|
340
|
+
hideLoading();
|
|
341
|
+
this._addResetButton();
|
|
342
|
+
console.log('[GaeaApp] 地图已就绪,模型与图层树数据后台加载中...');
|
|
343
|
+
|
|
344
|
+
// 剩余重资源(模型位置、GLTF、图层树接口、屏幕图标)后台异步加载,不阻塞地图显示
|
|
345
|
+
this._loadRestAsync();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** 后台异步加载:模型位置调整 → GLTF → 图层树数据 → 屏幕图标 */
|
|
349
|
+
async _loadRestAsync() {
|
|
350
|
+
try {
|
|
351
|
+
// 5. 调整模型位置
|
|
352
|
+
console.log('[GaeaApp] 开始调整模型位置...');
|
|
353
|
+
const models = [
|
|
354
|
+
{ nodeName: '沿浦水闸', position: '-2876603.788, 4892658.352, 2900208.622', rotation: '-37.24, 162.42, -17.17' },
|
|
355
|
+
{ nodeName: '下在水闸', position: '-2875959.024, 4893109.058, 2900101.919', rotation: '-23.42, -165.20, -34.52' },
|
|
356
|
+
{ nodeName: '联盟水闸', position: '-2878251.995, 4893011.199, 2897998.160', rotation: '-23.43, -165.98, -33.40' },
|
|
357
|
+
{ nodeName: '岭尾新闸', position: '-2877382.212, 4892920.226, 2898965.484', rotation: '7.79, 36.98, 40.83' },
|
|
358
|
+
{ nodeName: '岭尾水闸', position: '-2877582.576, 4892520.352, 2899499.184', rotation: '-41.04, 126.37, 6.29' },
|
|
359
|
+
];
|
|
360
|
+
for (const model of models) {
|
|
361
|
+
try {
|
|
362
|
+
const node = Gaea.World.Instance.GetNode(model.nodeName);
|
|
363
|
+
if (!node) { console.warn(`[GaeaApp] 未找到节点: ${model.nodeName}`); continue; }
|
|
364
|
+
const inst = Gaea.MeshInstance.ConvertBy(node);
|
|
365
|
+
const pos = model.position.split(',').map(v => parseFloat(v.trim()));
|
|
366
|
+
const rot = model.rotation.split(',').map(v => parseFloat(v.trim()));
|
|
367
|
+
if (inst.Translation) inst.Translation = new Gaea.Vector3(pos[0], pos[1], pos[2]);
|
|
368
|
+
if (inst.RotationDegrees) inst.RotationDegrees = new Gaea.Vector3(rot[0], rot[1], rot[2]);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
console.warn(`[GaeaApp] 调整模型 ${model.nodeName} 失败:`, e.message);
|
|
371
|
+
}
|
|
359
372
|
}
|
|
360
|
-
|
|
361
|
-
console.log('[GaeaApp] 所有模型位置调整完成');
|
|
373
|
+
console.log('[GaeaApp] 所有模型位置调整完成');
|
|
362
374
|
|
|
363
|
-
|
|
364
|
-
|
|
375
|
+
// 6. 加载 GLTF 模型
|
|
376
|
+
await this._loadGltfModels();
|
|
365
377
|
|
|
366
|
-
|
|
367
|
-
|
|
378
|
+
// 7. 获取图层树数据(有接口走接口,没接口用默认)
|
|
379
|
+
await this._fetchLayerTreeData();
|
|
368
380
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
381
|
+
// 同步 layerCategories(修复数据更新未同步的问题)
|
|
382
|
+
this.layerCategories = this.config.layerCategories || [];
|
|
383
|
+
console.log('[GaeaApp] layerCategories 更新后:', this.layerCategories.length);
|
|
372
384
|
|
|
373
|
-
|
|
374
|
-
|
|
385
|
+
// 8. 初始化屏幕图标
|
|
386
|
+
this._initScreenIcons();
|
|
375
387
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
388
|
+
console.log('[GaeaApp] 后台资源加载完成');
|
|
389
|
+
} catch (e) {
|
|
390
|
+
console.error('[GaeaApp] 后台资源加载出错:', e);
|
|
391
|
+
}
|
|
379
392
|
}
|
|
380
393
|
|
|
381
394
|
_waitForCanvas() {
|
|
@@ -570,13 +583,14 @@ export class GaeaApp {
|
|
|
570
583
|
}
|
|
571
584
|
|
|
572
585
|
console.log('[GaeaApp] 开始加载 GLTF 模型...');
|
|
573
|
-
|
|
586
|
+
// 并行加载所有模型(原串行逐个 await 会累加耗时)
|
|
587
|
+
await Promise.all(gltfList.map(async (item) => {
|
|
574
588
|
item.url = encodeURI(item.url);
|
|
575
589
|
try {
|
|
576
590
|
const el = await this.AddGltf(item);
|
|
577
591
|
if (el) this._gltfElements[item.name] = el;
|
|
578
592
|
} catch (e) { console.warn(`[GaeaApp] 模型 ${item.name} 加载失败:`, e.message); }
|
|
579
|
-
}
|
|
593
|
+
}));
|
|
580
594
|
console.log('[GaeaApp] GLTF 模型加载完成');
|
|
581
595
|
}
|
|
582
596
|
|
|
@@ -619,7 +633,7 @@ export class GaeaApp {
|
|
|
619
633
|
});
|
|
620
634
|
|
|
621
635
|
Gaea.World.Instance.RenderableObjectList.AddLast(element);
|
|
622
|
-
setTimeout(() => res(element),
|
|
636
|
+
setTimeout(() => res(element), 8000);
|
|
623
637
|
});
|
|
624
638
|
};
|
|
625
639
|
|
|
@@ -948,6 +962,81 @@ export class GaeaApp {
|
|
|
948
962
|
}
|
|
949
963
|
}
|
|
950
964
|
|
|
965
|
+
// ---- 贴地面(矢量面绘制) ----
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* 根据坐标数组添加贴地面
|
|
969
|
+
* @param {Array} coords - 坐标数组,格式 [[lng, lat], [lng, lat], ...]
|
|
970
|
+
* @param {string} name - 面名称
|
|
971
|
+
* @param {string} colorHex - 颜色 hex 值,如 '#3498db'
|
|
972
|
+
* @returns {object|null} 创建的 PolygonGeometryElement,失败返回 null
|
|
973
|
+
*/
|
|
974
|
+
addGroundPolygonByCoords(coords, name, colorHex) {
|
|
975
|
+
if (!coords || coords.length < 3) {
|
|
976
|
+
console.warn(`坐标点不足,跳过: ${name}`);
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
// 检查是否已存在同名面
|
|
980
|
+
if (this._groundPolygonElements.find(p => p.name === name)) {
|
|
981
|
+
console.warn(`贴地面 "${name}" 已存在,跳过`);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const positions = coords.map(c => new Gaea.Vector3(c[1], c[0], 50));
|
|
986
|
+
const geometry = Gaea.Geometry.Create1(Gaea.GeometryType.Polygon, positions);
|
|
987
|
+
|
|
988
|
+
const res = Gaea.GaeaResourceLoader.Instance.LoadLoaclResource('res://assets/shaders/polygonClampGround.shader');
|
|
989
|
+
const shader = Gaea.Shader.ConvertBy(res);
|
|
990
|
+
const material = new Gaea.ShaderMaterial();
|
|
991
|
+
material.Shader = shader;
|
|
992
|
+
|
|
993
|
+
const hex = colorHex.replace('#', '');
|
|
994
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
|
995
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
|
996
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
|
997
|
+
Gaea.World.Instance.SetMtlColor(material, 'albedo', new Gaea.Color(r, g, b, 0.5));
|
|
998
|
+
|
|
999
|
+
const symbol = new Gaea.GroundPolygonSymbol();
|
|
1000
|
+
symbol.PrismHeight = 9999;
|
|
1001
|
+
symbol.GroundPolygonMtl = material;
|
|
1002
|
+
|
|
1003
|
+
const polygonElement = new Gaea.PolygonGeometryElement();
|
|
1004
|
+
polygonElement.Symbol = symbol;
|
|
1005
|
+
polygonElement.Shape = geometry;
|
|
1006
|
+
polygonElement.EnablePick = true;
|
|
1007
|
+
polygonElement.EnableEdit = true;
|
|
1008
|
+
polygonElement.NeedIntersectWithMesh = false;
|
|
1009
|
+
polygonElement.EnableVirtualPoint = true;
|
|
1010
|
+
polygonElement.Name = name;
|
|
1011
|
+
|
|
1012
|
+
Gaea.World.Instance.RenderableObjectList.AddLast(polygonElement);
|
|
1013
|
+
this._groundPolygonElements.push({ name, color: colorHex, element: polygonElement });
|
|
1014
|
+
return polygonElement;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** 移除指定贴地面 */
|
|
1018
|
+
removeGroundPolygon(element) {
|
|
1019
|
+
try {
|
|
1020
|
+
Gaea.World.Instance.RenderableObjectList.Remove(element);
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
console.error('移除贴地面失败:', err);
|
|
1023
|
+
}
|
|
1024
|
+
const idx = this._groundPolygonElements.findIndex(p => p.element === element);
|
|
1025
|
+
if (idx >= 0) this._groundPolygonElements.splice(idx, 1);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** 清除所有贴地面 */
|
|
1029
|
+
clearAllGroundPolygons() {
|
|
1030
|
+
this._groundPolygonElements.forEach(item => {
|
|
1031
|
+
try {
|
|
1032
|
+
Gaea.World.Instance.RenderableObjectList.Remove(item.element);
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
console.error('移除贴地面失败:', err);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
this._groundPolygonElements = [];
|
|
1038
|
+
}
|
|
1039
|
+
|
|
951
1040
|
/** 定位到指定视角(经纬度高程 + 相机参数) */
|
|
952
1041
|
SetPosition(numStr) {
|
|
953
1042
|
const arr = numStr.split(',').map(s => Number(s.trim()));
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vector-polygon-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="section">
|
|
11
|
+
<div class="section-header">
|
|
12
|
+
<span class="section-label"><i class="fas fa-palette"></i> 面颜色</span>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="color-row">
|
|
15
|
+
<div
|
|
16
|
+
v-for="c in presetColors"
|
|
17
|
+
:key="c"
|
|
18
|
+
class="color-block"
|
|
19
|
+
:class="{ active: polygonColor === c }"
|
|
20
|
+
:style="{ backgroundColor: c }"
|
|
21
|
+
@click="polygonColor = c"
|
|
22
|
+
></div>
|
|
23
|
+
<div class="color-custom">
|
|
24
|
+
<input type="color" v-model="polygonColor" class="color-picker" />
|
|
25
|
+
<span class="color-hex">{{ polygonColor }}</span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- 上传GeoJSON -->
|
|
31
|
+
<div class="section">
|
|
32
|
+
<div class="section-header">
|
|
33
|
+
<span class="section-label"><i class="fas fa-file-upload"></i> 上传GeoJSON</span>
|
|
34
|
+
</div>
|
|
35
|
+
<div
|
|
36
|
+
class="upload-area"
|
|
37
|
+
:class="{ 'drag-over': isDragOver }"
|
|
38
|
+
@click="triggerFileInput"
|
|
39
|
+
@dragover.prevent="isDragOver = true"
|
|
40
|
+
@dragleave.prevent="isDragOver = false"
|
|
41
|
+
@drop.prevent="handleFileDrop"
|
|
42
|
+
>
|
|
43
|
+
<i class="fas fa-cloud-upload-alt upload-icon"></i>
|
|
44
|
+
<span class="upload-text">点击或拖拽.geojson文件到此处</span>
|
|
45
|
+
<input
|
|
46
|
+
ref="fileInput"
|
|
47
|
+
type="file"
|
|
48
|
+
accept=".geojson,.json"
|
|
49
|
+
multiple
|
|
50
|
+
style="display: none"
|
|
51
|
+
@change="handleFileSelect"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
<div v-if="uploadingFile" class="upload-status">
|
|
55
|
+
<i class="fas fa-spinner fa-spin"></i> 正在处理: {{ uploadingFile }}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- 已加载的面列表 -->
|
|
60
|
+
<div class="section" v-if="polygonList.length > 0">
|
|
61
|
+
<div class="section-header">
|
|
62
|
+
<span class="section-label"><i class="fas fa-list"></i> 已加载矢量面 ({{ polygonList.length }})</span>
|
|
63
|
+
<button class="btn-clear-small" @click="clearAll">清空全部</button>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="polygon-items">
|
|
66
|
+
<div v-for="(item, i) in polygonList" :key="i" class="polygon-item">
|
|
67
|
+
<span
|
|
68
|
+
class="polygon-color-dot"
|
|
69
|
+
:style="{ backgroundColor: item.color }"
|
|
70
|
+
></span>
|
|
71
|
+
<span class="polygon-name" :title="item.name">{{ item.name }}</span>
|
|
72
|
+
<button class="polygon-delete" @click="removePolygon(i)" title="删除">
|
|
73
|
+
<i class="fas fa-trash-alt"></i>
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
|
|
82
|
+
<script setup>
|
|
83
|
+
import { ref } from 'vue';
|
|
84
|
+
import { GaeaApp } from '../GaeaMethods.js';
|
|
85
|
+
import vDrag from '../directives/vDrag.js';
|
|
86
|
+
|
|
87
|
+
const props = defineProps({
|
|
88
|
+
appInstance: { type: Object, default: null }
|
|
89
|
+
});
|
|
90
|
+
const emit = defineEmits(['close']);
|
|
91
|
+
|
|
92
|
+
const APP = props.appInstance || (window.APP || new GaeaApp());
|
|
93
|
+
|
|
94
|
+
const presetColors = [
|
|
95
|
+
'#e74c3c', '#e67e22', '#f1c40f', '#2ecc71',
|
|
96
|
+
'#1abc9c', '#3498db', '#9b59b6', '#34495e',
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const polygonColor = ref('#3498db');
|
|
100
|
+
|
|
101
|
+
const fileInput = ref(null);
|
|
102
|
+
const isDragOver = ref(false);
|
|
103
|
+
const uploadingFile = ref('');
|
|
104
|
+
const polygonList = ref([]);
|
|
105
|
+
|
|
106
|
+
const triggerFileInput = () => {
|
|
107
|
+
fileInput.value?.click();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleFileDrop = (e) => {
|
|
111
|
+
isDragOver.value = false;
|
|
112
|
+
const files = e.dataTransfer.files;
|
|
113
|
+
if (files.length > 0) processFiles(files);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleFileSelect = (e) => {
|
|
117
|
+
const files = e.target.files;
|
|
118
|
+
if (files.length > 0) processFiles(files);
|
|
119
|
+
e.target.value = '';
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const processFiles = async (files) => {
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
if (!file.name.endsWith('.geojson') && !file.name.endsWith('.json')) continue;
|
|
125
|
+
|
|
126
|
+
uploadingFile.value = file.name;
|
|
127
|
+
try {
|
|
128
|
+
const text = await file.text();
|
|
129
|
+
const geojson = JSON.parse(text);
|
|
130
|
+
const polygons = extractPolygons(geojson, file.name.replace(/\.(geojson|json)$/i, ''));
|
|
131
|
+
for (const poly of polygons) {
|
|
132
|
+
const el = APP.addGroundPolygonByCoords(poly.coords, poly.name, polygonColor.value);
|
|
133
|
+
if (el) {
|
|
134
|
+
polygonList.value.push({ name: poly.name, color: polygonColor.value, element: el });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(`解析GeoJSON文件失败: ${file.name}`, err);
|
|
139
|
+
alert(`解析文件 "${file.name}" 失败: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
uploadingFile.value = '';
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const extractPolygons = (geojson, defaultName) => {
|
|
146
|
+
const results = [];
|
|
147
|
+
|
|
148
|
+
const processGeometry = (geom, name) => {
|
|
149
|
+
if (!geom) return;
|
|
150
|
+
if (geom.type === 'Polygon') {
|
|
151
|
+
results.push({ name, coords: geom.coordinates[0] });
|
|
152
|
+
} else if (geom.type === 'MultiPolygon') {
|
|
153
|
+
geom.coordinates.forEach((poly, idx) => {
|
|
154
|
+
results.push({ name: `${name}_${idx + 1}`, coords: poly[0] });
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (geojson.type === 'FeatureCollection') {
|
|
160
|
+
geojson.features.forEach((feature, idx) => {
|
|
161
|
+
const featureName = feature.properties?.name || feature.properties?.Name || `${defaultName}_${idx + 1}`;
|
|
162
|
+
processGeometry(feature.geometry, featureName);
|
|
163
|
+
});
|
|
164
|
+
} else if (geojson.type === 'Feature') {
|
|
165
|
+
const featureName = geojson.properties?.name || geojson.properties?.Name || defaultName;
|
|
166
|
+
processGeometry(geojson.geometry, featureName);
|
|
167
|
+
} else if (geojson.type === 'Polygon') {
|
|
168
|
+
results.push({ name: defaultName, coords: geojson.coordinates[0] });
|
|
169
|
+
} else if (geojson.type === 'MultiPolygon') {
|
|
170
|
+
geojson.coordinates.forEach((poly, idx) => {
|
|
171
|
+
results.push({ name: `${defaultName}_${idx + 1}`, coords: poly[0] });
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const removePolygon = (index) => {
|
|
179
|
+
const item = polygonList.value[index];
|
|
180
|
+
if (!item) return;
|
|
181
|
+
APP.removeGroundPolygon(item.element);
|
|
182
|
+
polygonList.value.splice(index, 1);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const clearAll = () => {
|
|
186
|
+
APP.clearAllGroundPolygons();
|
|
187
|
+
polygonList.value = [];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleClose = () => {
|
|
191
|
+
emit('close');
|
|
192
|
+
};
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<style lang="less" scoped>
|
|
196
|
+
.vector-polygon-panel {
|
|
197
|
+
position: absolute;
|
|
198
|
+
top: 100px;
|
|
199
|
+
left: 600px;
|
|
200
|
+
width: 360px;
|
|
201
|
+
background: rgba(30, 41, 59, 0.95);
|
|
202
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
203
|
+
border-radius: 10px;
|
|
204
|
+
color: #e2e8f0;
|
|
205
|
+
font-size: 13px;
|
|
206
|
+
z-index: 10000;
|
|
207
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
208
|
+
backdrop-filter: blur(8px);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.panel-header {
|
|
212
|
+
display: flex;
|
|
213
|
+
justify-content: space-between;
|
|
214
|
+
align-items: center;
|
|
215
|
+
padding: 10px 14px;
|
|
216
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.panel-title {
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
font-size: 15px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.close-btn {
|
|
225
|
+
background: none;
|
|
226
|
+
border: none;
|
|
227
|
+
color: #94a3b8;
|
|
228
|
+
font-size: 18px;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
padding: 0 4px;
|
|
231
|
+
&:hover { color: #fff; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.panel-body {
|
|
235
|
+
padding: 12px 14px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.section {
|
|
239
|
+
margin-bottom: 16px;
|
|
240
|
+
&:last-child { margin-bottom: 0; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.section-header {
|
|
244
|
+
display: flex;
|
|
245
|
+
justify-content: space-between;
|
|
246
|
+
align-items: center;
|
|
247
|
+
margin-bottom: 8px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.section-label {
|
|
251
|
+
font-weight: 500;
|
|
252
|
+
font-size: 13px;
|
|
253
|
+
color: #cbd5e1;
|
|
254
|
+
i { margin-right: 6px; color: #3b82f6; }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.btn-clear-small {
|
|
258
|
+
background: rgba(239, 68, 68, 0.15);
|
|
259
|
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
260
|
+
color: #ef4444;
|
|
261
|
+
font-size: 11px;
|
|
262
|
+
padding: 3px 8px;
|
|
263
|
+
border-radius: 4px;
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
&:hover { background: rgba(239, 68, 68, 0.3); }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.color-row {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
gap: 8px;
|
|
272
|
+
flex-wrap: wrap;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.color-block {
|
|
276
|
+
width: 26px;
|
|
277
|
+
height: 26px;
|
|
278
|
+
border-radius: 4px;
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
border: 2px solid transparent;
|
|
281
|
+
transition: all 0.15s;
|
|
282
|
+
&:hover { transform: scale(1.1); }
|
|
283
|
+
&.active {
|
|
284
|
+
border-color: #fff;
|
|
285
|
+
box-shadow: 0 0 8px rgba(59, 130, 246, 0.5);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.color-custom {
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: 6px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.color-picker {
|
|
296
|
+
width: 28px;
|
|
297
|
+
height: 28px;
|
|
298
|
+
border: none;
|
|
299
|
+
border-radius: 4px;
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
padding: 0;
|
|
302
|
+
background: transparent;
|
|
303
|
+
&::-webkit-color-swatch-wrapper { padding: 0; }
|
|
304
|
+
&::-webkit-color-swatch {
|
|
305
|
+
border-radius: 4px;
|
|
306
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.color-hex {
|
|
311
|
+
font-size: 11px;
|
|
312
|
+
color: #94a3b8;
|
|
313
|
+
font-family: monospace;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.upload-area {
|
|
317
|
+
border: 2px dashed rgba(148, 163, 184, 0.3);
|
|
318
|
+
border-radius: 8px;
|
|
319
|
+
padding: 20px;
|
|
320
|
+
text-align: center;
|
|
321
|
+
cursor: pointer;
|
|
322
|
+
transition: all 0.2s;
|
|
323
|
+
&:hover {
|
|
324
|
+
border-color: rgba(59, 130, 246, 0.5);
|
|
325
|
+
background: rgba(59, 130, 246, 0.05);
|
|
326
|
+
}
|
|
327
|
+
&.drag-over {
|
|
328
|
+
border-color: #3b82f6;
|
|
329
|
+
background: rgba(59, 130, 246, 0.1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.upload-icon {
|
|
334
|
+
font-size: 28px;
|
|
335
|
+
color: #64748b;
|
|
336
|
+
display: block;
|
|
337
|
+
margin-bottom: 8px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.upload-text {
|
|
341
|
+
font-size: 12px;
|
|
342
|
+
color: #94a3b8;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.upload-status {
|
|
346
|
+
margin-top: 8px;
|
|
347
|
+
font-size: 12px;
|
|
348
|
+
color: #3b82f6;
|
|
349
|
+
i { margin-right: 4px; }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.polygon-items {
|
|
353
|
+
max-height: 200px;
|
|
354
|
+
overflow-y: auto;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.polygon-item {
|
|
358
|
+
display: flex;
|
|
359
|
+
align-items: center;
|
|
360
|
+
padding: 6px 8px;
|
|
361
|
+
margin-bottom: 4px;
|
|
362
|
+
background: rgba(255, 255, 255, 0.04);
|
|
363
|
+
border-radius: 6px;
|
|
364
|
+
&:hover { background: rgba(255, 255, 255, 0.08); }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.polygon-color-dot {
|
|
368
|
+
width: 12px;
|
|
369
|
+
height: 12px;
|
|
370
|
+
border-radius: 3px;
|
|
371
|
+
flex-shrink: 0;
|
|
372
|
+
margin-right: 8px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.polygon-name {
|
|
376
|
+
flex: 1;
|
|
377
|
+
font-size: 12px;
|
|
378
|
+
overflow: hidden;
|
|
379
|
+
text-overflow: ellipsis;
|
|
380
|
+
white-space: nowrap;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.polygon-delete {
|
|
384
|
+
background: none;
|
|
385
|
+
border: none;
|
|
386
|
+
color: #64748b;
|
|
387
|
+
cursor: pointer;
|
|
388
|
+
padding: 2px 6px;
|
|
389
|
+
font-size: 12px;
|
|
390
|
+
border-radius: 4px;
|
|
391
|
+
&:hover {
|
|
392
|
+
color: #ef4444;
|
|
393
|
+
background: rgba(239, 68, 68, 0.1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
</style>
|
package/src/components/index.js
CHANGED
|
@@ -5,3 +5,4 @@ export { default as CommonDialog } from './CommonDialog.vue';
|
|
|
5
5
|
export { default as HydrologicStationDialog } from './HydrologicStationDialog.vue';
|
|
6
6
|
export { default as SecTourView } from './SecTourView.vue';
|
|
7
7
|
export { default as WaterGateTour } from './WaterGateTour.vue';
|
|
8
|
+
export { default as VectorPolygonControl } from './VectorPolygonControl.vue';
|
package/src/index.cjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @yangyongtao/gaea - CJS 入口
|
|
3
|
+
*
|
|
4
|
+
* CommonJS 用法:
|
|
5
|
+
* const gaea = await import('@yangyongtao/gaea');
|
|
6
|
+
* 或
|
|
7
|
+
* require('@yangyongtao/gaea').then(({ GaeaApp, setConfig }) => { ... })
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
module.exports = import('./index.js').then(m => m);
|
package/src/plugin.cjs
ADDED
package/USAGE.md
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# @yangyongtao/gaea 使用文档
|
|
2
|
-
|
|
3
|
-
基于 Godot WASM 引擎的 Vue 3 三维可视化组件库。提供影像/地形/贴地线加载、相机视角控制、图层树、天气控制、沿线飞行、断面游览、室内游览等能力,并内置 Vite 插件自动注入引擎资源。
|
|
4
|
-
|
|
5
|
-
> 完整可运行示例见 `gaea-ground-test/src/App.vue`,本文所有片段均以其为参考。
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1. 安装
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npm install @yangyongtao/gaea
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
peerDependencies(按需安装):`vue ^3.4`、`element-plus ^2.6`(可选)、`pinia ^2.1`(可选)。
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 2. Vite 配置
|
|
20
|
-
|
|
21
|
-
引入并注册 `gaeaVitePlugin`,它会自动注入 `Gaea.js`、提供引擎静态资源、设置 SharedArrayBuffer 所需的跨域隔离响应头。
|
|
22
|
-
|
|
23
|
-
```js
|
|
24
|
-
// vite.config.js
|
|
25
|
-
import { defineConfig } from 'vite';
|
|
26
|
-
import vue from '@vitejs/plugin-vue';
|
|
27
|
-
import { gaeaVitePlugin } from '@yangyongtao/gaea/plugin';
|
|
28
|
-
|
|
29
|
-
export default defineConfig({
|
|
30
|
-
plugins: [vue(), gaeaVitePlugin()],
|
|
31
|
-
resolve: { preserveSymlinks: true },
|
|
32
|
-
optimizeDeps: { exclude: ['@yangyongtao/gaea'] },
|
|
33
|
-
});
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## 3. 快速开始
|
|
39
|
-
|
|
40
|
-
页面里需要一个 `id="canvas"` 的画布。调用 `setConfig(...)` 后 `new GaeaApp()` 即可自动完成「引擎启动 → 环境初始化 → 图层加载 → 模型加载」。
|
|
41
|
-
|
|
42
|
-
```vue
|
|
43
|
-
<template>
|
|
44
|
-
<div id="gaea-app">
|
|
45
|
-
<div class="toolbar">
|
|
46
|
-
<button @click="showWeather = !showWeather">天气控制</button>
|
|
47
|
-
<button @click="showFly = !showFly">沿线飞行</button>
|
|
48
|
-
<button @click="showLayerTree = !showLayerTree">图层控制</button>
|
|
49
|
-
<WaterGateTour />
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<div class="canvas-area" id="canvas-container">
|
|
53
|
-
<canvas id="canvas"></canvas>
|
|
54
|
-
</div>
|
|
55
|
-
|
|
56
|
-
<WeatherControl v-if="showWeather" :appInstance="app" @close="showWeather = false" />
|
|
57
|
-
<FlyAlongRoute v-if="showFly" :appInstance="app" @close="showFly = false" />
|
|
58
|
-
<LayerTree v-if="showLayerTree" :appInstance="app" @close="showLayerTree = false" @node-click="onLayerNodeClick" />
|
|
59
|
-
<HydrologicStationDialog />
|
|
60
|
-
<SecTourView />
|
|
61
|
-
</div>
|
|
62
|
-
</template>
|
|
63
|
-
|
|
64
|
-
<script setup>
|
|
65
|
-
import { ref } from 'vue';
|
|
66
|
-
import { GaeaApp, setConfig } from '@yangyongtao/gaea';
|
|
67
|
-
import { WeatherControl, FlyAlongRoute, HydrologicStationDialog, SecTourView, WaterGateTour } from '@yangyongtao/gaea/components';
|
|
68
|
-
import LayerTree from '@yangyongtao/gaea/components/LayerTree.vue';
|
|
69
|
-
|
|
70
|
-
setConfig({
|
|
71
|
-
serveLocal: `${window.location.origin}/static`,
|
|
72
|
-
serveUrl: 'http://192.168.3.151:8088/gaeaExplorerServer/',
|
|
73
|
-
serviceMode: 'platform',
|
|
74
|
-
defaultCameraPosition: '27.3680928977018,120.414139621887,0,109379.690132486,0,0',
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const app = new GaeaApp();
|
|
78
|
-
window.APP = app; // 组件内部通过 window.APP 兜底获取实例
|
|
79
|
-
|
|
80
|
-
const showWeather = ref(false);
|
|
81
|
-
const showFly = ref(false);
|
|
82
|
-
const showLayerTree = ref(false);
|
|
83
|
-
|
|
84
|
-
function onLayerNodeClick({ node }) {
|
|
85
|
-
app.flyToNode(node);
|
|
86
|
-
}
|
|
87
|
-
</script>
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## 4. setConfig 配置项
|
|
93
|
-
|
|
94
|
-
| 参数 | 类型 | 默认值 | 说明 |
|
|
95
|
-
|------|------|--------|------|
|
|
96
|
-
| `serveLocal` | string | `''` | 引擎本地静态资源地址(AssetHost),一般为 `${origin}/static` |
|
|
97
|
-
| `serveUrl` | string | `''` | 服务平台地址(WMTS 影像 / 地形 / 贴地线均基于此) |
|
|
98
|
-
| `serviceMode` | `'platform' \| 'online'` | `'platform'` | **服务模式**:见下方说明 |
|
|
99
|
-
| `tiandituToken` | string | — | 在线模式的天地图 token |
|
|
100
|
-
| `tiandituLevelRange` | [number,number] | `[0,18]` | 天地图层级范围 |
|
|
101
|
-
| `defaultLayers` | Array | 见下 | 服务平台模式下加载的影像图层 |
|
|
102
|
-
| `defaultTerrains` | Array | 见下 | 服务平台模式下加载的地形服务 |
|
|
103
|
-
| `defaultGroundLines` | Array | 见下 | 服务平台模式下加载的贴地线 |
|
|
104
|
-
| `defaultCameraPosition` | string | — | 初始相机视角:`纬度,经度,高程,相机高,俯仰,朝向` |
|
|
105
|
-
| `layerCategories` | Array | 内置 | 图层树分类数据(接口数据会合并进来) |
|
|
106
|
-
| `dataHubApiUrl` | string | `''` | 图层树数据接口地址,为空则只用默认数据 |
|
|
107
|
-
| `onProgress` | function | — | 引擎加载进度回调 `(cur, total) => {}` |
|
|
108
|
-
|
|
109
|
-
### 4.1 serviceMode —— 服务模式(区分「服务平台」与「在线」)
|
|
110
|
-
|
|
111
|
-
- `'platform'`(服务平台):影像走 `serveUrl` 的 WMTS,并加载 `defaultTerrains` 地形、`defaultGroundLines` 贴地线。
|
|
112
|
-
- `'online'`(在线):**仅加载天地图影像**,不加载地形和贴地线(在线模式目前只有天地图一种影像来源)。
|
|
113
|
-
|
|
114
|
-
```js
|
|
115
|
-
setConfig({ serviceMode: 'platform' }); // 内网/服务平台
|
|
116
|
-
// 或
|
|
117
|
-
setConfig({ serviceMode: 'online', tiandituToken: '你的token' }); // 在线(天地图)
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### 4.2 defaultLayers —— 影像图层(服务平台模式)
|
|
121
|
-
|
|
122
|
-
```js
|
|
123
|
-
defaultLayers: [
|
|
124
|
-
{ layerName: 'earthland', forMat: 'image/png', min: 0, max: 18, transparency: 0.3 },
|
|
125
|
-
{ layerName: '苍南L19', forMat: 'image/png', min: 5, max: 17 },
|
|
126
|
-
]
|
|
127
|
-
```
|
|
128
|
-
- `layerName`:WMTS 服务里的图层名(Title)
|
|
129
|
-
- `forMat`:影像格式,如 `image/png`、`image/jpeg`
|
|
130
|
-
- `min` / `max`:显示层级范围
|
|
131
|
-
- `transparency`:透明度(可选)
|
|
132
|
-
|
|
133
|
-
### 4.3 defaultTerrains —— 地形服务(服务平台模式)
|
|
134
|
-
|
|
135
|
-
```js
|
|
136
|
-
defaultTerrains: [
|
|
137
|
-
{ name: '苍南县地形', url: 'htc/service/tms/1.0.0/苍南县地形@EPSG%3A4326@terrain/' },
|
|
138
|
-
]
|
|
139
|
-
```
|
|
140
|
-
- `name`:地形名称
|
|
141
|
-
- `url`:相对 `serveUrl` 的路径(也支持传完整 `http(s)://` 地址)
|
|
142
|
-
- 空数组 `[]` 则不加载地形
|
|
143
|
-
|
|
144
|
-
### 4.4 defaultGroundLines —— 贴地线(服务平台模式)
|
|
145
|
-
|
|
146
|
-
依赖 `serveUrl` 的 WMTS 矢量线服务(自动通过 `getAllServe` 获取服务列表后按名匹配)。
|
|
147
|
-
|
|
148
|
-
```js
|
|
149
|
-
defaultGroundLines: [
|
|
150
|
-
{ name: '苍南县边界', range: [0, 9], color: [255, 255, 0], index: 100 },
|
|
151
|
-
{ name: '苍南县镇边界', range: [9, 15], color: [0, 255, 0], index: 80 },
|
|
152
|
-
{ name: '苍南县村边界', range: [15, 19], color: [255, 255, 255], index: 60 },
|
|
153
|
-
{ name: '沿浦海塘工程范围线', range: [0, 21], color: [0, 255, 255], index: 120 },
|
|
154
|
-
]
|
|
155
|
-
```
|
|
156
|
-
- `name`:矢量线服务名(Title)
|
|
157
|
-
- `range`:显示层级范围 `[min, max]`
|
|
158
|
-
- `color`:线颜色 `[r, g, b]`(0-255)
|
|
159
|
-
- `index`:渲染优先级
|
|
160
|
-
- 每项可加 `isInitLoad: false` 跳过该条
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## 5. 组件说明
|
|
165
|
-
|
|
166
|
-
组件从 `@yangyongtao/gaea/components` 导入;`LayerTree` 需按路径单独导入。多数交互组件接收 `:appInstance="app"`,未传时内部回退到 `window.APP`。
|
|
167
|
-
|
|
168
|
-
| 组件 | 说明 | 主要事件 |
|
|
169
|
-
|------|------|----------|
|
|
170
|
-
| `WeatherControl` | 天气/台风控制面板(含台风路线拾取与播放) | `@close` |
|
|
171
|
-
| `FlyAlongRoute` | 沿线飞行:地图拾取点位,可设飞行高度/时长 | `@close` |
|
|
172
|
-
| `LayerTree` | 图层树,点击节点飞行定位 | `@close`、`@node-click` |
|
|
173
|
-
| `HydrologicStationDialog` | 水文测站弹窗,自监听 `gaea-icon-click` 事件 | — |
|
|
174
|
-
| `SecTourView` | 断面游览滚动条,模型锁定后显示 | — |
|
|
175
|
-
| `WaterGateTour` | 室内游览按钮(下拉选择水闸模型) | — |
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## 6. 常用实例方法(window.APP / app)
|
|
180
|
-
|
|
181
|
-
| 方法 | 说明 |
|
|
182
|
-
|------|------|
|
|
183
|
-
| `SetPosition('纬度,经度,高程,相机高,俯仰,朝向')` | 定位到指定视角 |
|
|
184
|
-
| `flyToNode({ position:{x,y,z} })` | 飞到指定位置 |
|
|
185
|
-
| `flyAlongPath(lineData, options)` | 沿线飞行,`lineData=[{x:纬度,y:经度,z:高度}]`,`options={duration,xrotation,lockEye,...}` |
|
|
186
|
-
| `stopFlyAlongPath()` | 停止沿线飞行 |
|
|
187
|
-
| `cameraReset()` | 相机复位到 `defaultCameraPosition` |
|
|
188
|
-
| `addInitTerrainServer(terrains?)` | 手动加载地形(不传则用 `defaultTerrains`) |
|
|
189
|
-
| `addGroundLines(lines?)` | 手动加载贴地线(不传则用 `defaultGroundLines`) |
|
|
190
|
-
| `addTyphoonRoute(pointList, options)` | 加载台风路线并播放动画 |
|
|
191
|
-
| `clearTyphoonRoute()` | 清除台风路线 |
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
## 7. 注意事项
|
|
196
|
-
|
|
197
|
-
- 页面必须存在 `id="canvas"` 的 `<canvas>`,引擎会挂载到它上面。
|
|
198
|
-
- 需要跨域隔离(COOP/COEP)才能启用 SharedArrayBuffer,`gaeaVitePlugin` 已自动处理;首次进入若未隔离会自动刷新一次。
|
|
199
|
-
- 服务平台模式下地形/贴地线依赖 `serveUrl` 对应服务,若服务器无对应服务名,控制台会打印「未找到/加载失败」警告,不影响其它功能。
|
|
200
|
-
- 大体积引擎资源(`Gaea.pck` / `Gaea.wasm` / `1111.res` 等)通过 postinstall 脚本下载到本地静态目录,请确保下载源可访问。
|