`@dage/krpano` 将常用的标签使用 `React` 进行了组件化封装（MVVM 真香），并在实例上补充了 `vtourskin` 的功能，支持类型推断，基本满足开发需求。

## 开发环境

`krpano 1.19-pr13`

## 示例

```tsx
import React, { useState, useMemo } from "react";
import {
  Autorotate,
  Krpano,
  Scene,
  View,
  HotSpot,
  Events,
  SceneProps,
  ROTATE_DIRECTION,
  ZOOM_ACTION,
} from "@dage/krpano";
import { MouseHoldView } from "./components";
import "./index.less";

interface ISceneProps extends Partial<SceneProps> {
  thumbUrl?: string;
}

const URL = "https://houseoss.4dkankan.com/project/leifeng-transfer";

export default () => {
  const [currentMenu, setCurrentMenu] = useState(0);
  const [currentScene, setCurrentScene] = useState("center1");
  const [autorotateEnable, setAutorotateEnable] = useState(false);

  const CENTER_SCENE_LIST = useMemo<ISceneProps[]>(
    () => [
      {
        name: "center1",
        thumbUrl: URL + "/panos/center1.tiles/thumb.jpg",
        previewUrl: URL + "/panos/center1.tiles/preview.jpg",
        imageTagAttributes: {
          type: "cube",
          tileSize: 512,
          multires: true,
        },
        images: [
          {
            tiledImageWidth: 2624,
            tiledImageHeight: 2624,
            url: URL + "/panos/center1.tiles/%s/l3/%v/l3_%s_%v_%h.jpg",
          },
          {
            tiledImageWidth: 1280,
            tiledImageHeight: 1280,
            url: URL + "/panos/center1.tiles/%s/l2/%v/l2_%s_%v_%h.jpg",
          },
          {
            tiledImageWidth: 640,
            tiledImageHeight: 640,
            url: URL + "/panos/center1.tiles/%s/l1/%v/l1_%s_%v_%h.jpg",
          },
        ],
        children: (
          <>
            <View
              hlookat={0}
              vlookat={0}
              fovType="MFOV"
              fov={120}
              maxPixelZoom={2}
              fovMin={70}
              fovMax={140}
              limitView="auto"
            />

            <HotSpot
              name="hotspot1"
              url={require("./hotspot/guide.png")}
              atv={5}
              scale={0.5}
              edge="top"
              distorted={true}
              onClick={() => setCurrentScene("center2")}
            />
          </>
        ),
      },
      {
        name: "center2",
        thumbUrl: URL + "/panos/center2.tiles/thumb.jpg",
        previewUrl: URL + "/panos/center2.tiles/preview.jpg",
        imageTagAttributes: {
          type: "cube",
          tileSize: 512,
          multires: true,
        },
        images: [
          {
            tiledImageWidth: 2624,
            tiledImageHeight: 2624,
            url: URL + "/panos/center2.tiles/%s/l3/%v/l3_%s_%v_%h.jpg",
          },
          {
            tiledImageWidth: 1280,
            tiledImageHeight: 1280,
            url: URL + "/panos/center2.tiles/%s/l2/%v/l2_%s_%v_%h.jpg",
          },
          {
            tiledImageWidth: 640,
            tiledImageHeight: 640,
            url: URL + "/panos/center2.tiles/%s/l1/%v/l1_%s_%v_%h.jpg",
          },
        ],
        children: (
          <View
            hlookat={0}
            vlookat={0}
            fovType="MFOV"
            fov={120}
            maxPixelZoom={2}
            fovMin={70}
            fovMax={140}
            limitView="auto"
          />
        ),
      },
    ],
    []
  );
  // 支持从 xml 中获取场景
  const PARTY_SCENE_LIST = useMemo<ISceneProps[]>(
    () => [
      {
        name: "scene_party",
        thumbUrl: URL + "/panos/party.tiles/thumb.jpg",
      },
    ],
    []
  );
  const MENUS = [
    {
      title: "长沙国防教育馆",
      scenes: CENTER_SCENE_LIST,
    },
    {
      title: "游客服务中心",
      scenes: PARTY_SCENE_LIST,
    },
  ];

  const handleSceneClick = (name: string) => {
    setCurrentScene(name);
  };

  const handleMenuClick = (idx: number) => {
    setCurrentMenu(idx);
  };

  const handleView = (direction: ROTATE_DIRECTION) => {
    window.ReactKrpanoActionProxy?.rotateView(direction);
  };

  const handleZoom = (action: ZOOM_ACTION) => {
    window.ReactKrpanoActionProxy?.zoomView(action);
  };

  const handleAutoRotateOneRound = () => {
    const count = window.ReactKrpanoActionProxy?.get("scene").count;
    const curIndex = window.ReactKrpanoActionProxy?.get(
      "scene[get(xml.scene)].index"
    );
    const nextScene = window.ReactKrpanoActionProxy?.get("scene").getItem(
      curIndex + 1 >= count ? 0 : curIndex + 1
    );
    setCurrentScene(nextScene.name);
  };

  return (
    <div className="demo">
      <Krpano
        className="krpano"
        xml="./party.xml"
        webvrUrl="./krpano/plugins/webvr.xml"
        currentScene={currentScene}
        passQueryParameters={true}
      >
        <Autorotate enabled={autorotateEnable} />
        <Events onAutoRotateOneRound={handleAutoRotateOneRound} />

        {[...CENTER_SCENE_LIST, ...PARTY_SCENE_LIST].map(
          (sc) => sc.imageTagAttributes && <Scene key={sc.name} {...sc} />
        )}
      </Krpano>

      <div className="scene-panel">
        <div className="scene-panel__menu">
          {MENUS.map((sc, idx) => (
            <div
              key={sc.title}
              className={`scene-panel__menu__item ${
                idx === currentMenu && "active"
              }`}
              onClick={handleMenuClick.bind(undefined, idx)}
            >
              {sc.title}
            </div>
          ))}
        </div>

        <div className="scene-panel__list">
          {MENUS[currentMenu].scenes.map((sc) => (
            <div
              key={sc.name}
              className={`scene-panel__list__item ${
                sc.name === currentScene ? "active" : ""
              }`}
              onClick={handleSceneClick.bind(undefined, sc.name)}
            >
              <img
                src={sc.thumbUrl || sc.images![0].url.replace("%s", "f")}
                alt={sc.name}
              />
            </div>
          ))}
        </div>
      </div>

      <div className="toolbar">
        <MouseHoldView
          onHold={handleView.bind(undefined, ROTATE_DIRECTION.LEFT)}
        >
          <button>←</button>
        </MouseHoldView>
        <MouseHoldView
          onHold={handleView.bind(undefined, ROTATE_DIRECTION.RIGHT)}
        >
          <button>→</button>
        </MouseHoldView>
        <MouseHoldView onHold={handleView.bind(undefined, ROTATE_DIRECTION.UP)}>
          <button>↑</button>
        </MouseHoldView>
        <MouseHoldView
          onHold={handleView.bind(undefined, ROTATE_DIRECTION.DOWN)}
        >
          <button>↓</button>
        </MouseHoldView>
        <MouseHoldView onHold={handleZoom.bind(undefined, ZOOM_ACTION.IN)}>
          <button>+</button>
        </MouseHoldView>
        <MouseHoldView onHold={handleZoom.bind(undefined, ZOOM_ACTION.OUT)}>
          <button>-</button>
        </MouseHoldView>
        <button
          className={`${autorotateEnable && "active-btn"}`}
          onClick={() => setAutorotateEnable(!autorotateEnable)}
        >
          360°
        </button>
        <button
          onClick={() => {
            window.ReactKrpanoActionProxy?.call("webvr.enterVR();");
          }}
        >
          vr
        </button>
      </div>
    </div>
  );
};
```

## API

### Krpano

<API hideTitle exports='["Krpano"]' src='@dage/krpano/index.d.ts'></API>

## window.ReactKrpanoActionProxy Methods

### call

执行 Javascript 函数

| Name     | Description              | Type      | Default      |
| -------- | ------------------------ | --------- | ------------ |
| action   | 动作                     | `string`  | `(required)` |
| nexttick | 是否在下一个渲染帧后执行 | `boolean` | `false`      |

### setTag

动态添加标签

| Name  | Description | Type                  | Default      |
| ----- | ----------- | --------------------- | ------------ |
| tag   | 标签        | `string`              | `(required)` |
| name  | 名称        | `string`              | `(required)` |
| attrs | 属性        | `Record<string, any>` | `(required)` |

### removeScene

删除场景

| Name | Description | Type     | Default      |
| ---- | ----------- | -------- | ------------ |
| name | 场景名称    | `string` | `(required)` |

### loadScene

加载场景

| Name | Description | Type     | Default      |
| ---- | ----------- | -------- | ------------ |
| name | 场景名称    | `string` | `(required)` |

### rotateView

旋转视图

| Name      | Description | Type               | Default      |
| --------- | ----------- | ------------------ | ------------ |
| direction | 方位        | `ROTATE_DIRECTION` | `(required)` |
| degrees   | 旋转度数    | `number`           | `10`         |

### zoomView

缩放视图

| Name   | Description | Type          | Default      |
| ------ | ----------- | ------------- | ------------ |
| action | 动作        | `ZOOM_ACTION` | `(required)` |
| num    | 缩放大小    | `number`      | `10`         |
