npm package: @hvsp/viewer
Description: 本库为高性能组态/可视化渲染引擎，支持自定义交互、标签、缩放等功能。
Latest version: 0.0.10

# HVSP Viewer 使用说明

本库为高性能组态/可视化渲染引擎，支持自定义交互、标签、缩放等功能。

## 快速开始

### 1. 安装依赖

安装库
1. 在package.json 手动添加 "@hvsp/viewer": "file:C:\\Users\\admin\\Desktop\\viewer"(本地的路径),
2. 如果提示 ogl 和 pako 缺失，则需要手动安装一下
```bash
npm install ogl
npm install pako
```
## Viewer API 详细说明

### 构造函数

```ts
new Viewer(container: HTMLElement, options: ViewerOptions)
```

**参数说明：**
- `container`: 容器 DOM 元素
- `options`: 配置选项

**ViewerOptions 接口：**
```ts
interface ViewerOptions {
    res?: string | ArrayBuffer;           // 资源文件路径或二进制数据
    backgroundColor?: [number, number, number];  // 背景颜色 [r, g, b]，范围 0-1
    zoomable?: boolean;                   // 是否使用内置的缩放
    movable?: boolean;                    // 是否使用内置的平移
    onPick?: (e: MouseEvent, actorId: string | null) => void;  // 点击拾取回调
    onTransformChange?: () => void;       // 视图变换回调
}
```

### 核心方法

#### 资源加载
```ts
loadRes(url: string | ArrayBuffer): Promise<void>
```
- 加载 HVSP 资源文件
- 支持 URL 字符串或 ArrayBuffer 数据
- 返回 Promise，加载完成后 resolve

#### 坐标转换
```ts
screenToWorld(x: number, y: number): { x: number, y: number }
```
- 屏幕坐标转世界坐标
- 输入：屏幕像素坐标
- 输出：世界坐标

```ts
worldToScreen(x: number, y: number): { x: number, y: number }
```
- 世界坐标转屏幕坐标
- 输入：世界坐标
- 输出：屏幕像素坐标

#### Actor 状态管理
```ts
setActorState(actorId: string, state: ActorState): void
```
- 设置指定 Actor 的状态
- `ActorState` 枚举值：`Stop(0)默认`, `Offline(1)变灰色`, `Running(2)绿色闪烁`, `Warning(3)红色`

```ts
getActorState(actorId: string): ActorState
```
- 获取指定 Actor 的当前状态

```ts
getAllActorIds(): string[]
```
- 获取所有 Actor 的 ID 列表

#### 视图控制
```ts
setZoom(zoom: number, e: { clientX: number, clientY: number }): void
```
- 设置缩放级别
- `zoom`: 缩放值（0.1-10 范围）
- `e`: 鼠标事件，用于确定缩放锚点

```ts
getZoom(): number
```
- 获取当前缩放级别

```ts
setPan(pan: { x: number, y: number }): void
```
- 设置平移位置
- `pan`: 世界坐标系中的平移偏移

```ts
getPan(): { x: number, y: number }
```
- 获取当前平移位置

#### 样式设置
```ts
setBackgroundColor(color: [number, number, number]): void
```
- 设置背景颜色
- `color`: RGB 颜色值，范围 0-1

#### DOM 访问
```ts
getParentDom(): HTMLElement
```
- 获取容器 DOM 元素

```ts
getCanvas(): HTMLCanvasElement
```
- 获取渲染画布元素

#### 资源管理
```ts
destroy(): void
```
- 销毁 Viewer 实例，释放所有资源

## 完整示例：Flowmap 标签系统

以下示例演示如何创建支持中心点定位和缩放的标签系统：

```ts
import { ActorState, Viewer } from "@hvsp/viewer";

// 创建 Viewer 容器
document.body.style.margin = '0';
const viewerDom = document.createElement("div");
viewerDom.style.width = "100vw";
viewerDom.style.height = "100vh";
document.body.appendChild(viewerDom);

// 初始化 Viewer
const viewer = new Viewer(viewerDom, {
    res: 'assets/hvsp/组态.bin',
    backgroundColor: [1, 1, 1],
    zoomable: false,      // 禁用内置缩放，使用自定义缩放
    movable: false,       // 禁用内置平移，使用自定义平移
    onPick: (e, actorId) => {
        console.log('点击到 Actor:', actorId);
        if (actorId) {
            viewer.setActorState(actorId, ActorState.Running);
        }
    },
    onTransformChange: () => {
        // 视图变换时更新所有标签
        objects.forEach(o => o.update());
    }
});

// 标签对象类
class WorldObject {
    label: HTMLDivElement;
    worldPos: { x: number, y: number };
    screenPos: { x: number, y: number };
    
    constructor(public name: string, public viewer: Viewer) {
        // 创建标签 DOM 元素
        this.label = document.createElement("div");
        this.label.style.position = "absolute";
        this.label.style.left = "0";
        this.label.style.top = "0";
        this.label.style.color = "white";
        this.label.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
        this.label.style.padding = "5px";
        this.label.style.borderRadius = "5px";
        this.label.style.zIndex = "1000";
        this.label.style.pointerEvents = "none";
        this.label.style.userSelect = "none";
        this.label.style.fontSize = "12px";
        this.label.style.fontWeight = "bold";
        // 关键：使用 transform 实现中心点定位
        this.label.style.transform = "translate(-50%, -50%)";
        this.label.innerText = this.name;
        document.body.appendChild(this.label);
    }
    
    update() {
        // 世界坐标转屏幕坐标
        this.screenPos = this.viewer.worldToScreen(this.worldPos.x, this.worldPos.y);
        
        // 直接使用屏幕像素坐标
        this.label.style.left = `${this.screenPos.x}px`;
        this.label.style.top = `${this.screenPos.y}px`;
        
        // 根据当前缩放调整标签大小
        let curZoom = this.viewer.getZoom();
        this.label.style.transform = `translate(-50%, -50%) scale(${curZoom})`;
    }
    
    setScreenPos(x: number, y: number) {
        this.screenPos = { x, y };
        // 屏幕坐标转世界坐标
        this.worldPos = this.viewer.screenToWorld(x, y);
        this.label.style.left = `${x}px`;
        this.label.style.top = `${y}px`;
        let curZoom = this.viewer.getZoom();
        this.label.style.transform = `translate(-50%, -50%) scale(${curZoom})`;
    }
    
    setLabel(text: string) {
        this.label.innerText = text;
    }
}

// 标签管理
let objects: WorldObject[] = [];

// 自定义缩放控制
viewerDom.addEventListener("wheel", (e) => {
    let zoom = viewer.getZoom();
    viewer.setZoom(zoom - e.deltaY * 0.001, { clientX: e.clientX, clientY: e.clientY });
});

// 自定义平移控制
let startPos = { x: 0, y: 0 };
let startViewPos = viewer.getPan();
let isDragging = false;
let distance = 0;

viewerDom.addEventListener("mousedown", (e) => {
    isDragging = true;
    startPos = { x: e.clientX, y: e.clientY };
    startViewPos = viewer.getPan();
    distance = 0;
});

viewerDom.addEventListener("mousemove", (e) => {
    if (!isDragging) return;
    
    // 计算世界坐标偏移
    let prepos = viewer.screenToWorld(startPos.x, startPos.y);
    let curpos = viewer.screenToWorld(e.clientX, e.clientY);
    let delta = { x: curpos.x - prepos.x, y: curpos.y - prepos.y };
    
    // 更新平移位置
    viewer.setPan({ x: startViewPos.x - delta.x, y: startViewPos.y - delta.y });
    distance += Math.sqrt(delta.x * delta.x + delta.y * delta.y);
});

viewerDom.addEventListener("mouseup", (e) => {
    isDragging = false;
    // 短距离点击添加标签，长距离拖拽平移
    if (distance < 2) {
        let object = new WorldObject("test", viewer);
        object.setScreenPos(e.clientX, e.clientY);
        objects.push(object);
    }
});
```

## 关键实现要点

### 1. 中心点定位
使用 CSS `transform: translate(-50%, -50%)` 让标签以中心点定位，而不是左上角。

### 2. 坐标系统
- **世界坐标**: 组态图的原始坐标系
- **屏幕坐标**: 像素坐标，直接对应屏幕位置

### 3. 缩放同步
标签大小通过 `scale(${curZoom})` 与视图缩放同步，保持视觉一致性。

### 4. 事件处理
- 短距离点击：添加标签
- 长距离拖拽：平移视图
- 滚轮：缩放视图

### 5. 性能优化
- 使用 `onTransformChange` 回调统一更新所有标签
- 避免频繁的 DOM 操作