提交室内导览交互与讲解优化

This commit is contained in:
lyf
2026-06-14 23:48:13 +08:00
parent a7c1879f60
commit feb7310a46
33 changed files with 3257 additions and 361 deletions

View 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`

View 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
View 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 };

View 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

File diff suppressed because one or more lines are too long

View 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"
}

View 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。

View 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`) 校验失败 |