提交室内导览交互与讲解优化
This commit is contained in:
21
static/sgs-map-sdk/CHANGELOG.md
Normal file
21
static/sgs-map-sdk/CHANGELOG.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# SGS Map SDK Changelog
|
||||
|
||||
## [2.0.0] - 2026-06-12
|
||||
|
||||
### 🚀 新特性 (New Features)
|
||||
- **双域双向通信架构**:采用不可见 iframe 挂载基座,通过安全沙箱和 `postMessage` 握手进行通信,从源头上解决 WebGL Context 内存泄漏和生命周期挂起问题。
|
||||
- **动态点位系统 (Markers API)**:新增 `addMarker` / `removeMarker` / `clearMarkers` 接口,支持锚定三维节点(防模型漂移)和绝对坐标系定位。
|
||||
- **异步寻路确权系统**:`planRoute` 和 `changeFloor` 升级为基于 `requestId` 的 Promise 异步确权模式,支持 Timeout 拦截机制。
|
||||
- **高精模型与坐标系重构**:全面接入米级统一坐标系(废弃 CAD 毫米系),支持多楼层分层物理拆分加载。
|
||||
|
||||
### 🐞 修复与强化 (Fixes & Improvements)
|
||||
- `HELLLO` 与 `ENGINE_READY` 双向握手协议对齐。
|
||||
- `targetOrigin` 自动推导增强,严格拦截非法的跨域源消息,免疫跨站攻击。
|
||||
- SDK Iframe 生命周期的 `onerror` 捕捉增强,能正确派发 `loadError` 事件。
|
||||
- 补齐了丢失的 `floorChanged` 运行时事件透传。
|
||||
- 完善 `package.json` 工程化暴露,同时提供 IIFE 与 ESM 产物,开启源码映射(SourceMap)。
|
||||
- `addMarker` 接口强制引入强类型定义 `MarkerConfig`。
|
||||
|
||||
### 🗑️ 废弃与移除 (Deprecations)
|
||||
- 彻底移除半成品接口 `startNavigateAnim` / `stopNavigateAnim`。
|
||||
- 彻底移除废弃接口 `addBusinessMarker` / `removeBusinessMarker`,统一收编为 `addMarker`。
|
||||
54
static/sgs-map-sdk/README.md
Normal file
54
static/sgs-map-sdk/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# SGS Map SDK 交付包
|
||||
|
||||
> **版本**:V2.0.0
|
||||
> **定位**:深圳自然博物馆统一三维高精地图导航服务平台 H5 SDK
|
||||
|
||||
欢迎接入 SGS Map SDK!本 SDK 将复杂的 WebGL 三维渲染、GLB 模型管线与 NavMesh 物理寻路引擎封装在服务端基座中,业务端(H5 / 大屏 / 小程序)只需通过几行代码即可极速唤起 3D 地图。
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```text
|
||||
sgs-map-sdk-release/
|
||||
├── dist/ # SDK 代码产物包(核心)
|
||||
│ ├── index.global.js # 给大屏端或传统网页使用的 IIFE 格式(通过 <script> 引入)
|
||||
│ ├── index.global.js.map # IIFE source map
|
||||
│ ├── index.mjs # 给 Webpack / Vite / Next.js 等现代工程使用的 ESM 格式
|
||||
│ ├── index.mjs.map # ESM source map
|
||||
│ ├── index.d.ts # TypeScript 类型声明文件
|
||||
│ └── index.d.mts # TypeScript 模块声明文件
|
||||
├── example/
|
||||
│ └── index.html # 完整接入示例(带控制台、日志面板、与后端 API 联调)
|
||||
├── package.json # NPM 元信息
|
||||
├── sdk-quickstart.md # 【必读】10 分钟快速接入指南(含完整示例代码)
|
||||
├── sdk-protocol.md # 【选读】底层通信协议与安全沙箱规范说明
|
||||
└── README.md # 当前说明文档
|
||||
```
|
||||
|
||||
## 🚀 环境对接配置信息 (非常重要)
|
||||
|
||||
在下游业务端执行 `new SGSMapSDK({ ... })` 初始化时,必须要填入由服务端分配的**环境对接变量**:
|
||||
|
||||
1. **`sdkUrl`(渲染基座地址)**:
|
||||
- 释义:独立渲染基座的 URL 路径,SDK 会自动在您的页面中创建一个不可见的 iframe 或 Web-View 连接到这里。
|
||||
- **请联系地图管理平台管理员获取最新的正式/测试域名**,例如:`https://map.museum.com/h5-sdk`。
|
||||
|
||||
2. **`targetOrigin`(安全通信域)**:
|
||||
- 释义:这是指**地图渲染基座的来源域名**(即 `sdkUrl` 的 Origin)。为了安全,SDK 只接收来自该域名 iframe 的消息。
|
||||
- 配置要求:请传入您的地图基座部署域名,例如:`https://map.museum.com`。如果不传,SDK 会自动从 `sdkUrl` 参数推导。注意:**不要**填成您业务页面的域名(业务页面的域名是交给 h5-sdk 做白名单校验的)。
|
||||
|
||||
## 📦 独立渲染基座部署与版本关系
|
||||
|
||||
- **部署要求**:本 SDK 是“双域架构”。除了在您的业务前端引入 `sgs-map-sdk` 库以外,**SGS 统一地图管理平台必须在您的服务器或内网环境中完成独立部署**(基座项目为 `sgs-frontend-map`)。
|
||||
- **版本对应**:SDK 与基座严格遵守大版本一致原则。例如,SDK `V2.x.x` 必须对应基座引擎的 `V2.x.x`。如果强行跨版本混用,`HELLO` 协议握手将被拒绝并抛出 `ERR_NOT_READY`。
|
||||
|
||||
## 📚 如何开始?
|
||||
|
||||
- **如果您是普通前端业务开发 / 微信小程序开发**:
|
||||
直接打开 `sdk-quickstart.md`,复制里面的示例代码,10分钟即可完成接入。
|
||||
*(注:微信小程序团队完全无需引入 `dist` 下的代码,请仔细阅读文档中小程序 `<web-view>` 的原生接入方式)*
|
||||
|
||||
- **如果您想看一份能直接跑的完整示例**:
|
||||
打开 `example/index.html`,用本地静态服务器(如 `npx serve`)打开即可,里面包含楼层切换、聚焦、寻路、图钉、超时、销毁等全场景演示。需要先把基座 `sgs-frontend-map` 跑起来并把 `sdkUrl` 改成对应的本地地址。
|
||||
|
||||
- **如果您是对架构感兴趣的高级开发**:
|
||||
可以阅读 `sdk-protocol.md`,了解我们如何通过双域确权协议解决 `postMessage` 多实例安全问题,以及 WebGL 的安全释放机制。
|
||||
143
static/sgs-map-sdk/index.d.ts
vendored
Normal file
143
static/sgs-map-sdk/index.d.ts
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
declare class EventEmitter<Events extends Record<string, any>> {
|
||||
private events;
|
||||
constructor();
|
||||
on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void;
|
||||
off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void;
|
||||
emit<K extends keyof Events>(event: K, data: Events[K]): void;
|
||||
}
|
||||
|
||||
/** 统一位置描述符 */
|
||||
type SgsMapNode = {
|
||||
type: 'coord';
|
||||
floorId: number;
|
||||
x: number;
|
||||
z: number;
|
||||
y?: number;
|
||||
} | {
|
||||
type: 'poiId';
|
||||
value: number;
|
||||
} | {
|
||||
type: 'nodeName';
|
||||
value: string;
|
||||
};
|
||||
/** 单点吸附详情 */
|
||||
interface SnapDetail {
|
||||
original: {
|
||||
x: number;
|
||||
z: number;
|
||||
};
|
||||
nearest: {
|
||||
x: number;
|
||||
z: number;
|
||||
};
|
||||
distance: number;
|
||||
}
|
||||
/** 寻路结果 */
|
||||
interface RouteResult {
|
||||
success: boolean;
|
||||
distance: number;
|
||||
duration: number;
|
||||
pathPoints: Array<{
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}>;
|
||||
startSnap: SnapDetail;
|
||||
endSnap: SnapDetail;
|
||||
computeTimeMs: number;
|
||||
polyCount: number;
|
||||
warnings: string[];
|
||||
errorCode?: string;
|
||||
message?: string;
|
||||
}
|
||||
/** 寻路选项 */
|
||||
interface RouteOptions {
|
||||
mode?: 'walk' | 'accessible';
|
||||
}
|
||||
/** SDK 初始化配置 */
|
||||
interface SGSMapSDKOptions {
|
||||
container: string | HTMLElement;
|
||||
sdkUrl?: string;
|
||||
targetOrigin?: string;
|
||||
floorId?: number;
|
||||
timeout?: number;
|
||||
}
|
||||
/** SDK 事件映射 */
|
||||
interface SGSMapEvents {
|
||||
ready: {
|
||||
engineVersion: string;
|
||||
protocolVersion: number;
|
||||
};
|
||||
poiClick: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
floorId?: number;
|
||||
description?: string;
|
||||
};
|
||||
floorChanged: {
|
||||
floorId: number;
|
||||
};
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
loadError: {
|
||||
type: string;
|
||||
detail: string;
|
||||
};
|
||||
}
|
||||
/** SDK 错误码 */
|
||||
type SGSErrorCode = 'ERR_TIMEOUT' | 'ERR_NO_PATH' | 'ERR_FLOOR_NOT_FOUND' | 'ERR_NOT_READY' | 'ERR_POI_NOT_FOUND' | 'ERR_NETWORK' | 'ERR_IFRAME_DEAD' | 'ERR_UNAUTHORIZED' | 'ERR_UNSUPPORTED' | 'ERR_FAILED';
|
||||
/** 图钉配置参数 */
|
||||
interface MarkerConfig {
|
||||
id: string;
|
||||
name?: string;
|
||||
iconUrl?: string;
|
||||
anchorNodeName?: string;
|
||||
position?: [number, number, number] | {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
}
|
||||
|
||||
declare class SGSMapSDK extends EventEmitter<SGSMapEvents> {
|
||||
private container;
|
||||
private sdkUrl;
|
||||
private floorId;
|
||||
private targetOrigin;
|
||||
private iframe;
|
||||
private isReady;
|
||||
private defaultTimeout;
|
||||
private promiseQueue;
|
||||
private _messageListener;
|
||||
constructor(options: SGSMapSDKOptions);
|
||||
private _initIframeBridge;
|
||||
private _generateRequestId;
|
||||
private _postCommand;
|
||||
private _postCommandAsync;
|
||||
getIsReady(): boolean;
|
||||
getCurrentFloor(): number;
|
||||
focusTo(node: SgsMapNode | string): void;
|
||||
changeFloor(floorId: number, timeout?: number): Promise<{
|
||||
success: boolean;
|
||||
floorId: number;
|
||||
}>;
|
||||
addMarker(config: MarkerConfig): void;
|
||||
removeMarker(markerId: string): void;
|
||||
clearMarkers(): void;
|
||||
highlightPolygon(nodeName: string): void;
|
||||
planRoute(from: SgsMapNode | string, to: SgsMapNode | string, options?: any, timeout?: number): Promise<RouteResult>;
|
||||
clearRoute(): void;
|
||||
getState(): Promise<any>;
|
||||
resetView(): void;
|
||||
setViewMode(mode: '2d' | '3d'): void;
|
||||
toggleLayer(types: string[], visible: boolean, timeout?: number): Promise<{
|
||||
success: boolean;
|
||||
visibleTypes: string[];
|
||||
}>;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export { type MarkerConfig, type RouteOptions, type RouteResult, type SGSErrorCode, type SGSMapEvents, type SGSMapSDKOptions, type SgsMapNode, type SnapDetail, SGSMapSDK as default };
|
||||
2
static/sgs-map-sdk/index.global.js
Normal file
2
static/sgs-map-sdk/index.global.js
Normal file
@@ -0,0 +1,2 @@
|
||||
"use strict";(()=>{var m=class{constructor(){this.events={}}on(r,e){this.events[r]||(this.events[r]=[]),this.events[r].push(e)}off(r,e){this.events[r]&&(this.events[r]=this.events[r].filter(t=>t!==e))}emit(r,e){this.events[r]&&this.events[r].forEach(t=>{try{t(e)}catch(s){console.error(`[EventEmitter] Error emitting event ${String(r)}:`,s)}})}};var l=class extends m{constructor(e){super();this.iframe=null;this.isReady=!1;this.promiseQueue={};this._messageListener=null;let t=typeof e.container=="string"?document.getElementById(e.container):e.container;if(!t)throw new Error(`[SGSMapSDK] Cannot find container: ${e.container}`);this.container=t,this.sdkUrl=e.sdkUrl||"/h5-sdk",this.floorId=e.floorId||1,this.defaultTimeout=e.timeout||5e3,this.targetOrigin=e.targetOrigin,this.targetOrigin||(this.sdkUrl.startsWith("http")?this.targetOrigin=new URL(this.sdkUrl).origin:typeof window<"u"?this.targetOrigin=window.location.origin:this.targetOrigin="*"),this._initIframeBridge()}_initIframeBridge(){let e=document.createElement("iframe");e.src=`${this.sdkUrl}?floorId=${this.floorId}`,e.style.cssText="width:100%;height:100%;border:none;background:transparent;",this.container.innerHTML="",this.container.appendChild(e),this.iframe=e,this._messageListener=t=>{if(this.targetOrigin!=="*"&&t.origin!==this.targetOrigin){console.warn(`[SGSMapSDK] Origin mismatch blocked: ${t.origin} !== ${this.targetOrigin}`);return}if(!this.iframe||t.source!==this.iframe.contentWindow)return;let s=t.data;if(s){if(s.type==="SGS_MAP_EVENT"){let{action:i,payload:n}=s;i==="ON_POI_CLICK"?this.emit("poiClick",n):i==="ENGINE_READY"?(this.isReady=!0,this.emit("ready",n),console.log("[SGSMapSDK] Handshake complete: Engine Ready received \u2705")):i==="ON_FLOOR_CHANGED"&&this.emit("floorChanged",n)}if(s.type==="SGS_MAP_RESPONSE"){let{action:i,requestId:n,payload:a,error:o}=s,d=this.promiseQueue[n];if(d){if(a&&a.success!==!1)d.resolve(a);else{let f=o||{code:"ERR_FAILED",message:`${i} command failed`};d.reject(f)}delete this.promiseQueue[n]}}}},window.addEventListener("message",this._messageListener),e.onload=()=>{this.isReady||(this._postCommand("HELLO",{protocolVersion:2,sdkVersion:"2.0.0"}),console.log("[SGSMapSDK] Sent HELLO handshake command, waiting for ENGINE_READY..."))},e.onerror=t=>{console.error("[SGSMapSDK] Iframe load error",t),this.emit("loadError",{type:"iframe_error",detail:"Failed to load map engine iframe."})}}_generateRequestId(){return`req_sdk_${Date.now()}_${Math.floor(Math.random()*1e5)}`}_postCommand(e,t,s){if(!this.iframe||!this.iframe.contentWindow){console.error("[SGSMapSDK] SDK Iframe is not ready yet!");return}this.iframe.contentWindow.postMessage({type:"SGS_MAP_COMMAND",action:e,requestId:s,payload:t},this.targetOrigin)}_postCommandAsync(e,t,s=this.defaultTimeout){if(!this.isReady)return Promise.reject({code:"ERR_NOT_READY",message:'SGSMapSDK is not ready. Wait for the "ready" event before calling methods.'});let i=this._generateRequestId();return new Promise((n,a)=>{let o=setTimeout(()=>{this.promiseQueue[i]&&(this.promiseQueue[i].reject({code:"ERR_TIMEOUT",message:`Command "${e}" timed out after ${s}ms`}),delete this.promiseQueue[i])},s);this.promiseQueue[i]={resolve:d=>{clearTimeout(o),n(d)},reject:d=>{clearTimeout(o),a(d)}},this._postCommand(e,t,i)})}getIsReady(){return this.isReady}getCurrentFloor(){return this.floorId}focusTo(e){typeof e=="string"?this._postCommand("FOCUS_TO",{nodeName:e}):this._postCommand("FOCUS_TO",e)}changeFloor(e,t){return this._postCommandAsync("CHANGE_FLOOR",{floorId:e},t).then(s=>(this.floorId=e,s))}addMarker(e){this._postCommand("ADD_MARKER",e)}removeMarker(e){this._postCommand("REMOVE_MARKER",{markerId:e})}clearMarkers(){this._postCommand("CLEAR_MARKERS")}highlightPolygon(e){this._postCommand("HIGHLIGHT_POLYGON",{nodeName:e})}planRoute(e,t,s,i=5e3){let n=typeof e=="string"?e:void 0,a=typeof t=="string"?t:void 0,o={};return n?o.startNode=n:o.from=e,a?o.endNode=a:o.to=t,s&&(o.options=s),this._postCommandAsync("PLAN_ROUTE",o,i)}clearRoute(){this._postCommand("CLEAR_ROUTE")}getState(){return this._postCommandAsync("GET_STATE")}resetView(){this._postCommand("RESET_VIEW")}setViewMode(e){this._postCommand("SET_VIEW_MODE",{mode:e})}toggleLayer(e,t,s){return this._postCommandAsync("TOGGLE_LAYER",{types:e,visible:t},s)}destroy(){this.iframe&&(this.iframe.parentNode&&this.iframe.parentNode.removeChild(this.iframe),this.iframe=null),Object.keys(this.promiseQueue).forEach(e=>{let t=this.promiseQueue[e];t&&t.reject({code:"ERR_TIMEOUT",message:"SGSMapSDK has been destroyed. Pending promises were rejected."}),delete this.promiseQueue[e]}),this._messageListener&&(window.removeEventListener("message",this._messageListener),this._messageListener=null),this.isReady=!1,console.log("[SGSMapSDK] Destroyed successfully. Cleaned up iframe, listeners and pending queues.")}};typeof window<"u"&&(window.SGSMapSDK=l);})();
|
||||
//# sourceMappingURL=index.global.js.map
|
||||
1
static/sgs-map-sdk/index.global.js.map
Normal file
1
static/sgs-map-sdk/index.global.js.map
Normal file
File diff suppressed because one or more lines are too long
25
static/sgs-map-sdk/package.json
Normal file
25
static/sgs-map-sdk/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@sgs/map-sdk",
|
||||
"version": "2.0.0",
|
||||
"description": "SGS Map SDK",
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"sideEffects": true,
|
||||
"files": [
|
||||
"dist",
|
||||
"example",
|
||||
"README.md",
|
||||
"sdk-quickstart.md",
|
||||
"sdk-protocol.md"
|
||||
],
|
||||
"devDependencies": {},
|
||||
"author": "SGS Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
116
static/sgs-map-sdk/sdk-protocol.md
Normal file
116
static/sgs-map-sdk/sdk-protocol.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# SGS Map SDK 通信协议规格 (Protocol Specification)
|
||||
|
||||
> 版本: V2.0.0
|
||||
> 适用对象: SGS Map SDK 核心开发者、渲染基座 (h5-sdk) 开发者
|
||||
|
||||
本文档定义了外壳 SDK (`sgs-map-sdk.js`) 与 独立渲染基座 iframe (`h5-sdk/page.tsx`) 之间的 `postMessage` 通信契约。所有消息传递必须严格遵循此结构,以保障多域环境下的确权、安全过滤与异步生命周期追踪。
|
||||
|
||||
---
|
||||
|
||||
## 1. 消息模型抽象
|
||||
|
||||
双方通信产生的 JSON 对象统一分为三大类:
|
||||
|
||||
### 1.1 命令 (Command): SDK -> 基座
|
||||
外壳 SDK 向基座发起的主动操作。
|
||||
```json
|
||||
{
|
||||
"type": "SGS_MAP_COMMAND",
|
||||
"action": "PLAN_ROUTE", // [必填] 行为动词
|
||||
"requestId": "req_sdk_123_45", // [可选] 异步确权追踪信标
|
||||
"payload": { ... } // [可选] 负载参数
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 响应 (Response): 基座 -> SDK
|
||||
基座处理完 `Command` 后(同步或异步计算),向 SDK 携带结果的被动回执。
|
||||
```json
|
||||
{
|
||||
"type": "SGS_MAP_RESPONSE",
|
||||
"action": "PLAN_ROUTE", // [必填] 原样回显 Command 的 action
|
||||
"requestId": "req_sdk_123_45", // [必填] 原样回显 Command 的 requestId
|
||||
"payload": { "success": true }, // [可选] 成功时的返回值
|
||||
"error": { "code": "ERR_NO_PATH", "message": "无路径" } // [可选] 失败时的异常信息
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 事件 (Event): 基座 -> SDK
|
||||
基座中发生的不可预知行为(如鼠标点击)或系统状态变更,主动向外广播。
|
||||
```json
|
||||
{
|
||||
"type": "SGS_MAP_EVENT",
|
||||
"action": "ON_POI_CLICK", // [必填] 事件动词 (ON_开头)
|
||||
"payload": { "poiId": 1234 } // [可选] 事件负载
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 握手时序 (Handshake Sequence)
|
||||
|
||||
为了解决 iframe 初始化过程中的“假死”与状态竞态,必须采用基于事件的确权握手。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SDK as 外壳 SDK
|
||||
participant Iframe as 浏览器 DOM
|
||||
participant Base as 渲染基座 (h5-sdk)
|
||||
|
||||
SDK->>Iframe: 动态创建 iframe(src)
|
||||
Iframe-->>SDK: 触发 iframe.onload
|
||||
|
||||
Note over SDK,Base: 此时 HTML 刚加载,WebGL/GLB 模型并未解析完成
|
||||
|
||||
SDK->>Base: 发送 HELLO { protocolVersion: 2, sdkVersion: "2.0.0" }
|
||||
Base->>Base: 挂载 WebGL,拉取 GLB 及 POI 接口
|
||||
Base-->>SDK: 内部加载完毕,推送 ENGINE_READY 事件
|
||||
SDK->>SDK: 解锁 isReady = true,清空缓存命令队列
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 指令字典 (Command Action Dictionary)
|
||||
|
||||
基座通过严格的**白名单机制**过滤 `action`,任何未在下表中声明的指令都将被立刻退回 `ERR_UNSUPPORTED` 响应。
|
||||
|
||||
| Action | Payload 负载签名 | 成功 Response 负载 | 说明 |
|
||||
|--------|------------------|--------------------|------|
|
||||
| `HELLO` | `{ protocolVersion, sdkVersion }` | (无,以 `ENGINE_READY` 代替回执) | 初始心跳探测 |
|
||||
| `CHANGE_FLOOR` | `{ floorId: number }` | `{ success: true, floorId }` | 触发析构当前层并加载新层 |
|
||||
| `FOCUS_TO` | `SgsMapNode` | `{ success: true }` | 镜头平滑转移,改变相机视角 |
|
||||
| `PLAN_ROUTE` | `{ startNode?: SgsMapNode, endNode?: SgsMapNode, from?: SgsMapNode, to?: SgsMapNode, options? }` | `RouteResult` | 寻路计算。基座将转换为微服务调用 |
|
||||
| `CLEAR_ROUTE` | (空) | `{ success: true }` | 销毁当前的 NavMeshPathLayer |
|
||||
| `GET_STATE` | (空) | `{ floorId, isReady, ... }` | 查询内部状态机当前切片 |
|
||||
| `ADD_MARKER` | `MarkerConfig` | `{ success: true }` | 生成三维图钉并依附到骨骼节点 |
|
||||
| `REMOVE_MARKER`| `{ markerId: string }` | `{ success: true }` | 释放指定图钉的材质 |
|
||||
| `CLEAR_MARKERS`| (空) | `{ success: true }` | 卸载所有动态业务图钉 |
|
||||
| `HIGHLIGHT_POLYGON`| `{ nodeName: string }` | `{ success: true }` | 激活发光材质特效 |
|
||||
| `RESET_VIEW` | (空) | `{ success: true }` | 重置相机视角至初始状态 |
|
||||
| `SET_VIEW_MODE` | `{ mode: '2d' | '3d' }` | `{ success: true }` | 切换 2D/3D 视角模式 |
|
||||
| `TOGGLE_LAYER` | `{ types: string[], visible: boolean }` | `{ success: true, visibleTypes: string[] }` | 批量控制某类底座设施的显示隐藏 |
|
||||
|
||||
> **类型定义备注:**
|
||||
> `SgsMapNode` 及 `RouteResult` 的精确结构请参考 TypeScript 源码定义:`sdk-src/types.ts`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 事件广播列表 (Event Action List)
|
||||
|
||||
| 事件 Action | Payload 结构 | SDK 派发事件名 | 触发时机 |
|
||||
|-------------|--------------|----------------|----------|
|
||||
| `ENGINE_READY` | `{ engineVersion, protocolVersion, sdkVersion }` | `ready` | 基座完全挂载,WebGL 画布可渲染 |
|
||||
| `ON_POI_CLICK` | `{ id, name, type }` | `poiClick` | 用户在触摸屏或鼠标点击了高亮要素 |
|
||||
| `ON_FLOOR_CHANGED`| `{ floorId }` | `floorChanged` | 楼层完全切换完毕后自动抛出 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 安全沙箱规则 (Security & Sandboxing)
|
||||
|
||||
1. **Origin 发射校验 (SDK 侧)**
|
||||
- `sgs-map-sdk.js` 使用 `iframe.contentWindow.postMessage(msg, targetOrigin)` 发射指令。若 `targetOrigin` 错误,浏览器底层会进行跨域阻断。
|
||||
2. **Origin 监听校验 (SDK 侧)**
|
||||
- 收到事件时比对 `event.origin !== targetOrigin`,丢弃第三方注入的伪造消息。
|
||||
- 收到事件时比对 `event.source !== this.iframe.contentWindow`,丢弃宿主页面其他无关 iframe 的干扰。
|
||||
3. **安全内存释放 (GC)**
|
||||
- 断网或后服务宕机时,SDK 侧的 `Promise` 将在设定的 `timeout` (默认 5000ms) 到达后自动 `reject`。
|
||||
- SDK 实例调用 `destroy()` 后,会清除自身 DOM 及事件树,杜绝闭包引用与孤儿 WebGL Context。
|
||||
147
static/sgs-map-sdk/sdk-quickstart.md
Normal file
147
static/sgs-map-sdk/sdk-quickstart.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# SGS Map SDK 快速接入指南
|
||||
|
||||
> 版本: V2.0.0 | 支持终端: H5 浏览器、大屏终端、微信小程序
|
||||
|
||||
## 1. 引入方式
|
||||
|
||||
SDK 提供多种模块规范的产出文件,适配不同的应用场景:
|
||||
|
||||
### 1.1 Script 标签引入 (浏览器直连)
|
||||
将 `dist/index.global.js`(IIFE 格式)通过 `<script>` 标签引入,SDK 会挂载全局变量 `window.SGSMapSDK`。
|
||||
```html
|
||||
<script src="./dist/index.global.js"></script>
|
||||
<script>
|
||||
const sdk = new SGSMapSDK({ ... });
|
||||
</script>
|
||||
```
|
||||
|
||||
### 1.2 ESM Import (Webpack / Vite / Next.js)
|
||||
在现代前端工程中,直接使用 ESM 格式:
|
||||
```javascript
|
||||
import SGSMapSDK from 'path/to/sdk/index.mjs';
|
||||
|
||||
const sdk = new SGSMapSDK({ ... });
|
||||
```
|
||||
|
||||
### 1.3 微信小程序 WebView 接入
|
||||
在微信小程序中,无需引入外壳 SDK,直接通过 `web-view` 组件加载地图基座,并通过 URL 传递参数。
|
||||
```html
|
||||
<!-- 小程序 WXML -->
|
||||
<web-view src="https://map.museum.com/h5-sdk?floorId=1&mode=miniapp"></web-view>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 初始化与生命周期
|
||||
|
||||
### 2.1 创建实例
|
||||
```javascript
|
||||
const map = new SGSMapSDK({
|
||||
container: 'map-container', // 挂载的 DOM 容器 ID
|
||||
sdkUrl: 'https://map.museum.com/h5-sdk', // 渲染基座的 URL
|
||||
targetOrigin: 'https://map.museum.com', // [重要] 安全防范,配置只允许该域名通信
|
||||
floorId: 1, // 初始楼层 ID
|
||||
timeout: 5000 // API 调用的默认超时时间(毫秒)
|
||||
});
|
||||
```
|
||||
|
||||
### 2.2 等待就绪事件
|
||||
**重要:** SDK 的所有控制指令(如聚焦、寻路等)必须在 `ready` 事件触发后调用。
|
||||
```javascript
|
||||
map.on('ready', (info) => {
|
||||
console.log('地图引擎加载完成', info.engineVersion);
|
||||
// 现在可以安全调用 API 了
|
||||
});
|
||||
```
|
||||
|
||||
### 2.3 销毁实例
|
||||
当页面卸载时,为了避免内存泄漏,必须调用 `destroy()`。
|
||||
```javascript
|
||||
// 清除 DOM、注销所有监听事件并拒绝所有挂起的异步调用
|
||||
map.destroy();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 常用场景 API
|
||||
|
||||
### 3.1 楼层切换
|
||||
切换楼层是**异步**的,SDK 会在后台销毁旧楼层 WebGL 显存,并加载新楼层模型。
|
||||
```javascript
|
||||
map.changeFloor(3).then((res) => {
|
||||
console.log("成功切换至楼层: ", res.floorId);
|
||||
}).catch(err => {
|
||||
console.error("切换失败: ", err.message);
|
||||
});
|
||||
```
|
||||
|
||||
### 3.2 搜索与对焦
|
||||
当用户在搜索栏查找到具体要素后,使镜头平滑飞至要素上方:
|
||||
```javascript
|
||||
// 通过 POI ID 对焦(推荐)
|
||||
map.focusTo({ type: 'poiId', value: 10102 });
|
||||
|
||||
// 通过三维节点名称对焦
|
||||
map.focusTo({ type: 'nodeName', value: 'Hall_DomeTheater' });
|
||||
```
|
||||
|
||||
### 3.3 路线导航
|
||||
起点与终点均支持绝对坐标或 POI ID。
|
||||
```javascript
|
||||
map.planRoute(
|
||||
{ type: 'poiId', value: 8001 }, // 起点
|
||||
{ type: 'poiId', value: 9005 }, // 终点
|
||||
{ mode: 'walk' } // 选项:walk 或 accessible(无障碍)
|
||||
).then((result) => {
|
||||
if (result.success) {
|
||||
console.log(`路径规划成功:总距离 ${result.distance} 米,预计步行 ${result.duration} 秒`);
|
||||
// 基座会自动在 3D 场景中渲染青色流光折线
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err.code === 'ERR_NO_PATH') {
|
||||
alert("抱歉,无法找到可达的路线。");
|
||||
}
|
||||
});
|
||||
|
||||
// 清除路线
|
||||
map.clearRoute();
|
||||
```
|
||||
|
||||
### 3.4 响应 POI 点击
|
||||
游客在三维场景中点击任意可交互物体时,SDK 会派发 `poiClick` 事件。
|
||||
```javascript
|
||||
map.on('poiClick', (poi) => {
|
||||
console.log(`用户点击了:${poi.name} (ID: ${poi.poiId})`);
|
||||
// 业务侧可在此处弹出底部展品详情栏或抽屉
|
||||
});
|
||||
```
|
||||
|
||||
### 3.5 动态撒图钉 (Marker)
|
||||
```javascript
|
||||
// 添加一个悬浮图标
|
||||
map.addMarker({
|
||||
markerId: 'marker-user-1',
|
||||
anchorNodeName: 'Hall_Dinosaur',
|
||||
imageUrl: 'https://domain.com/user-avatar.png',
|
||||
width: 64,
|
||||
height: 64,
|
||||
yOffset: 2 // 悬浮于物体上方 2 米
|
||||
});
|
||||
|
||||
// 清除图钉
|
||||
map.removeMarker('marker-user-1');
|
||||
map.clearMarkers();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 错误处理
|
||||
|
||||
SDK 采用 Promise 风格的异步响应,所有 `catch` 捕获的错误都遵循 `{ code, message }` 格式。
|
||||
|
||||
| 错误码 | 触发场景 |
|
||||
|--------|----------|
|
||||
| `ERR_TIMEOUT` | 网络拥塞或基座无响应,超出了构造时设置的超时时间 |
|
||||
| `ERR_NOT_READY` | 在 `ready` 事件触发前,强行调用了 API |
|
||||
| `ERR_NO_PATH` | 起点与终点之间没有连通的 NavMesh 物理网格 |
|
||||
| `ERR_UNAUTHORIZED` | 通信双方跨域安全策略 (`targetOrigin`) 校验失败 |
|
||||
Reference in New Issue
Block a user