Ver código fonte

refactor[krpano]: webvr

chenlei 1 ano atrás
pai
commit
010a3714ee

+ 76 - 16
packages/krpano/src/KrpanoActionProxy.ts

@@ -1,3 +1,4 @@
+import { PromiseQueue } from "./PromiseQueue";
 import { ViewProps } from "./components";
 import {
   NativeKrpanoRendererObject,
@@ -6,12 +7,6 @@ import {
 } from "./types";
 import { buildKrpanoAction, buildKrpanoTagSetterActions } from "./utils";
 
-declare global {
-  interface Window {
-    ReactKrpanoActionProxy?: KrpanoActionProxy;
-  }
-}
-
 export type HandlerFunc = (renderer: KrpanoActionProxy) => void;
 
 interface EventHandler {
@@ -24,6 +19,7 @@ export class KrpanoActionProxy {
   name: string;
   krpanoRenderer?: NativeKrpanoRendererObject;
   eventHandlers: EventHandler[] = [];
+  dynamicTagWaitQueue: PromiseQueue<any>;
 
   constructor(
     krpanoRenderer?: NativeKrpanoRendererObject,
@@ -31,8 +27,22 @@ export class KrpanoActionProxy {
   ) {
     this.krpanoRenderer = krpanoRenderer;
     this.name = name;
+
+    // krpano 1.19 版本不支持动态插入 include,只能在文本中插入后重新加载
+    this.dynamicTagWaitQueue = new PromiseQueue();
   }
 
+  waitIncludeLoad() {
+    return this.syncTagsLoaded
+      ? Promise.resolve()
+      : this.dynamicTagWaitQueue.push();
+  }
+
+  /**
+   * 执行 Javascript 函数
+   * @param action 动作
+   * @param nexttick 是否在下一个渲染帧后执行
+   */
   call(action: string, nexttick = false): void {
     const actionStr = nexttick ? `nexttick(${action})` : action;
 
@@ -43,24 +53,25 @@ export class KrpanoActionProxy {
     this.call(buildKrpanoAction("set", name, ...params));
   }
 
-  setTag(
-    tag:
-      | "scene"
-      | "hotspot"
-      | "layer"
-      | "view"
-      | "events"
-      | "autorotate"
-      | "plugin",
+  /**
+   * 动态添加标签
+   * @param tag 标签
+   * @param name 名称
+   * @param attrs 属性
+   */
+  async setTag(
+    tag: "scene" | "hotspot" | "layer" | "view" | "events" | "autorotate",
     name: string | null,
     attrs: Record<string, any>
-  ): void {
+  ) {
     let nexttick = false;
 
     if (tag === "hotspot" || tag === "events") {
       nexttick = true;
     }
 
+    await this.waitIncludeLoad();
+
     this.call(
       buildKrpanoTagSetterActions(name ? `${tag}[${name}]` : tag, attrs),
       nexttick
@@ -71,6 +82,10 @@ export class KrpanoActionProxy {
     return this.krpanoRenderer?.get(name);
   }
 
+  /**
+   * 删除场景
+   * @param name 场景名称
+   */
   removeScene(name: string): void {
     if (
       this.get("scene") &&
@@ -198,4 +213,49 @@ export class KrpanoActionProxy {
   removeHotspot(name: string): void {
     this.call(buildKrpanoAction("removehotspot", name), true);
   }
+
+  syncTagsLoaded = false;
+  syncTagStack: {
+    tagName: string;
+    attribute: Record<string, unknown>;
+  }[] = [];
+
+  pushSyncTag(tagName: string, attribute: Record<string, unknown>) {
+    this.syncTagStack.unshift({
+      tagName,
+      attribute,
+    });
+  }
+
+  async createSyncTags() {
+    const xmlDoc = await this.getXMLContent();
+    const krpanoElement = xmlDoc.querySelector("krpano");
+
+    while (this.syncTagStack.length) {
+      const tag = this.syncTagStack.pop()!;
+      const element = xmlDoc.createElement(tag.tagName);
+
+      for (const key in tag.attribute) {
+        element.setAttribute(key, tag.attribute[key] as string);
+      }
+
+      krpanoElement?.insertBefore(element, null);
+    }
+
+    return xmlDoc;
+  }
+
+  private async getXMLContent() {
+    let contentText = "";
+    const xml = this?.get("xml");
+    const parser = new DOMParser();
+
+    if (xml.content) {
+      contentText = xml.content;
+    } else if (xml.url) {
+      contentText = await fetch(xml.url).then((res) => res.text());
+    }
+
+    return parser.parseFromString(contentText, "text/xml");
+  }
 }

+ 33 - 0
packages/krpano/src/PromiseQueue.ts

@@ -0,0 +1,33 @@
+export interface PromiseQueueItem<T> {
+  resolve(value: T): void;
+  reject(error: Error): void;
+}
+
+export class PromiseQueue<T> {
+  private queue: Array<PromiseQueueItem<T>> = [];
+
+  /**
+   * 返回一个 promise 用于等待
+   */
+  push = () => {
+    return new Promise<T>((resolve, reject) => {
+      this.queue.push({ resolve, reject });
+    });
+  };
+
+  /**
+   * 清空队列, 正常结束
+   */
+  flushResolve = (value: T) => {
+    this.queue.forEach((item) => item.resolve(value));
+    this.queue = [];
+  };
+
+  /**
+   * 清空队列, 移除结束
+   */
+  flushReject = (error: Error) => {
+    this.queue.forEach((item) => item.reject(error));
+    this.queue = [];
+  };
+}

+ 1 - 1
packages/krpano/src/components/HotSpot.tsx

@@ -65,8 +65,8 @@ export const HotSpot: FC<HotspotProps> = ({ name, children, ...rest }) => {
       }
       return {};
     });
-    renderer?.bindEvents(EventSelector, eventsObj as any);
 
+    renderer?.bindEvents(EventSelector, eventsObj as any);
     renderer?.addHotspot(name, {});
 
     return () => {

+ 21 - 0
packages/krpano/src/components/Include.tsx

@@ -0,0 +1,21 @@
+import { FC, useContext, useEffect } from "react";
+import { KrpanoRendererContext } from "../contexts";
+
+export interface IncludeProps {
+  url: string;
+}
+
+/**
+ * 注意:不支持动态插入
+ */
+export const Include: FC<IncludeProps> = ({ url }) => {
+  const renderer = useContext(KrpanoRendererContext);
+
+  useEffect(() => {
+    if (!renderer) return;
+
+    renderer.pushSyncTag("include", { url });
+  }, [renderer]);
+
+  return <></>;
+};

+ 38 - 10
packages/krpano/src/components/Krpano.tsx

@@ -1,13 +1,15 @@
-import React, { useCallback, useMemo, useState } from "react";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
 import { KrpanoActionProxy } from "../KrpanoActionProxy";
 import { useEventCallback } from "../hooks";
 import { IKrpanoConfig, NativeKrpanoRendererObject } from "../types";
 import { useKrpano } from "../hooks/useKrpano";
 import { CurrentSceneContext, KrpanoRendererContext } from "../contexts";
+import { buildKrpanoAction } from "../utils";
 import { WebVR } from "./WebVR";
 
 export interface KrpanoProps extends Omit<IKrpanoConfig, "onready" | "target"> {
   className?: string;
+  style?: React.CSSProperties;
   children?: React.ReactNode;
   currentScene?: string;
   target?: string;
@@ -20,6 +22,7 @@ export interface KrpanoProps extends Omit<IKrpanoConfig, "onready" | "target"> {
 
 export const Krpano: React.FC<KrpanoProps> = ({
   className,
+  style,
   children,
   currentScene,
   target = "krpano",
@@ -28,6 +31,7 @@ export const Krpano: React.FC<KrpanoProps> = ({
   ...rest
 }) => {
   const [renderer, setRenderer] = useState<KrpanoActionProxy | null>(null);
+  const restRef = useEventCallback(rest);
   const onReadyRef = useEventCallback(onReady);
   const onReadyCallback = useCallback(
     (obj: NativeKrpanoRendererObject) => {
@@ -45,25 +49,49 @@ export const Krpano: React.FC<KrpanoProps> = ({
     () => ({
       target,
       onready: onReadyCallback,
-      ...rest,
+      ...restRef.current,
     }),
-    [target, rest, onReadyCallback]
+    [target, restRef, onReadyCallback]
   );
 
   useKrpano(krpanoConfig);
 
+  useEffect(() => {
+    if (!renderer) return;
+
+    const reloadXML = async () => {
+      if (renderer.syncTagStack.length) {
+        // 如果有同步标签(include、plugin),则重新加载
+        const updateXmlString = new XMLSerializer().serializeToString(
+          await renderer.createSyncTags()
+        );
+
+        renderer.call(buildKrpanoAction("loadxml", updateXmlString));
+      }
+
+      renderer.syncTagsLoaded = true;
+      renderer.dynamicTagWaitQueue.flushResolve(true);
+    };
+
+    reloadXML();
+  }, [renderer]);
+
+  useEffect(() => {
+    if (!renderer || !currentScene) return;
+
+    renderer.waitIncludeLoad().then(() => {
+      renderer.loadScene(currentScene);
+    });
+  }, [renderer, currentScene]);
+
   return (
     <KrpanoRendererContext.Provider value={renderer}>
       <CurrentSceneContext.Provider value={currentScene || null}>
-        <div
-          id={target}
-          className={className}
-          style={className ? {} : { width: "100vw", height: "100vh" }}
-        >
+        {webvrUrl && <WebVR url={webvrUrl} />}
+
+        <div id={target} className={className} style={style}>
           {renderer ? children : null}
         </div>
-
-        {webvrUrl && <WebVR url={webvrUrl} />}
       </CurrentSceneContext.Provider>
     </KrpanoRendererContext.Provider>
   );

+ 21 - 0
packages/krpano/src/components/Plugin.tsx

@@ -0,0 +1,21 @@
+import { FC, useContext, useEffect } from "react";
+import { KrpanoRendererContext } from "../contexts";
+
+export interface PluginProps {
+  attribute: Record<string, unknown>;
+}
+
+/**
+ * 注意:不支持动态插入
+ */
+export const Plugin: FC<PluginProps> = ({ attribute }) => {
+  const renderer = useContext(KrpanoRendererContext);
+
+  useEffect(() => {
+    if (!renderer) return;
+
+    renderer.pushSyncTag("plugin", attribute);
+  }, [renderer]);
+
+  return <></>;
+};

+ 0 - 6
packages/krpano/src/components/Scene.tsx

@@ -95,11 +95,5 @@ export const Scene: React.FC<SceneProps> = ({
     };
   }, [renderer, name, images, imageTagAttributes, content, previewUrl]);
 
-  useEffect(() => {
-    if (currentScene === name) {
-      renderer?.loadScene(name);
-    }
-  }, [name, renderer, currentScene]);
-
   return <div className="scene">{currentScene === name ? children : null}</div>;
 };

+ 12 - 19
packages/krpano/src/components/WebVR/index.tsx

@@ -1,32 +1,25 @@
-import { FC, useContext, useEffect } from "react";
-import { KrpanoRendererContext } from "../../contexts";
-import { buildKrpanoAction } from "../../utils";
+import { FC } from "react";
+import { Include, Plugin } from "..";
 
 export interface WebVRProps {
   url: string;
 }
 
-const PLUGIN_CONFIG = {
+const WEBVR_CONFIG = {
+  name: "WebVR",
   keep: true,
   devices: "html5",
-  multireslock: {
-    desktop: true,
-    "mobile.or.tablet": false,
-  },
+  "multireslock.desktop": true,
+  "multireslock.mobile.or.tablet": false,
   mobilevr_support: true,
   mobilevr_fake_support: true,
 };
 
 export const WebVR: FC<WebVRProps> = ({ url }) => {
-  const renderer = useContext(KrpanoRendererContext);
-
-  useEffect(() => {
-    if (url && renderer) {
-      renderer.call(buildKrpanoAction("loadpano", url, "null", "MERGE"));
-
-      renderer?.setTag("plugin", "WebVR", PLUGIN_CONFIG);
-    }
-  }, [renderer]);
-
-  return <></>;
+  return (
+    <>
+      <Include url={url} />
+      <Plugin attribute={WEBVR_CONFIG} />
+    </>
+  );
 };

+ 2 - 0
packages/krpano/src/components/index.ts

@@ -4,3 +4,5 @@ export * from "./View";
 export * from "./HotSpot";
 export * from "./Autorotate";
 export * from "./Events";
+export * from "./Include";
+export * from "./Plugin";

+ 3 - 2
packages/krpano/src/hooks/useKrpano.ts

@@ -11,8 +11,9 @@ export const useKrpano = (config: IKrpanoConfig): void => {
       mobilescale: 1,
     };
     const embedpano = () => {
-      if (typeof window.embedpano === "function")
-        window.embedpano(Object.assign({}, defaultConfig, config));
+      if (typeof window.embedpano === "function") {
+        window.embedpano({ ...defaultConfig, ...config });
+      }
     };
 
     if (typeof window.embedpano === "function") {

+ 7 - 6
packages/krpano/src/types.ts

@@ -1,5 +1,12 @@
 import { KrpanoActionProxy } from "./KrpanoActionProxy";
 
+declare global {
+  interface Window {
+    embedpano?: (config: IKrpanoConfig) => void;
+    ReactKrpanoActionProxy?: KrpanoActionProxy;
+  }
+}
+
 export interface NativeKrpanoRendererObject {
   get(key: string): any;
   call(action: string): void;
@@ -67,9 +74,3 @@ export enum ZOOM_ACTION {
   IN = "in",
   OUT = "out",
 }
-
-declare global {
-  interface Window {
-    embedpano?: (config: IKrpanoConfig) => void;
-  }
-}

+ 4 - 0
packages/krpano/src/utils.ts

@@ -1,7 +1,11 @@
 import escapeHTML from "escape-html";
 import { XMLMeta } from "./types";
 
+/**
+ * @see https://krpano.com/docu/actions/?version=119pr13
+ */
 type FuncName =
+  | "addplugin"
   | "set"
   | "loadxml"
   | "loadscene"