本文へ移動
ドキュメント

クイックスタート

公式の React と TypeScript スターターで公開シーンを表示し、Team データに接続する前に Five SDK の操作を確認します。

このページの内容
公式スターターv1.3.0

ローカルで動かす

React + TypeScript の完全なプロジェクトです。Node 24 が必要です。

実際のシーンを表示し、表示モードを切り替えます。アカウントやキーは不要です。

ターミナルを操作できる AI コーディングツールに貼り付けてください。アプリの起動と確認を依頼するプロンプトです。

Markdown を表示

実際の公開シーンを使い、React + TypeScript のプロジェクトを起動します。スターターは Five SDK 6.9.2、Three.js 0.117.1、React 18.3.1 と依存関係のロックファイルを固定しています。

開始方法を選ぶ

上の「スターター用プロンプトをコピー」を使い、ファイル作成とターミナル操作ができる AI コーディングツールに貼り付けてください。コピー前に全文を確認できます。プロンプトには公式のバージョン付きアーカイブ、チェックサム、確認手順が含まれています。プロンプトとスターター内の操作ラベルは英語です。

手動で設定する場合は同じ ZIP を新しいディレクトリに展開し、展開した realsee-react-five ディレクトリでターミナルを開きます。同梱のロックファイルを保持してください。

必要なもの

  • Node 24 と npm。
  • WebGL に対応したブラウザー。
  • シーンの画像とモデルにアクセスできるネットワーク。
  • 公開シーンにはアカウントやキーは不要です。

Playground ではホスト済みの公開シーンを試せます。公開サンプルと Playground のデモにはログイン、Developer app、OpenAPI のアクセス権限は不要です。

インストール・確認・起動

npm ci
npm run check
npm run build
npm run dev

ターミナルに表示されたアドレスを開きます。開発用フロントエンドの既定値は http://127.0.0.1:5173、ローカルの Node API はポート 4101 です。

ローカルで本番ビルドを動かす場合は、npm run build の後に npm start を実行し、表示されたアドレスを使います。停止するには Ctrl+C を押します。

実際の表示を確認する

Ready が表示されたら、ドラッグして見回し、床を選んで移動し、Panorama と Floorplan を切り替えます。Reset view で開始時のカメラに戻ります。

ビューアーには、その Work が参照する画像とモデルへのネットワークアクセスが必要です。

公開 Work は @realsee/open-works@0.1.1 のデータです。このパッケージは Five SDK の peer dependency ではありません。Five SDK は three@^0.117.1 を指定し、このスターターは互換性のある 0.117.1 に固定しています。

SDK のサンプルを試す

スターターで Open SDK examples を選ぶか、ローカル URL に ?sdk-examples=1 を付けます。この独立したページでは公開シーンと、次のガイドと共通のソースを使います。

SDK サンプルはアプリの認証情報を交換せず、Team へのアクセスも検証しません。

自分のデータに接続する

Team API クイックスタート に進みます。npm run configure -- --device を実行し、表示された確認リンクを開きます。ログインして所有元の Team と有効なアプリを選び、Authorize agent を自分で選択してください。補助ツールがローカルサーバーの .env を設定します。デバイス認証を利用できない場合は .env をローカルで設定します。秘密情報をプロンプトやブラウザー環境変数に入れないでください。

ローカルサーバーはトークンと一覧の応答を確認し、有効期間の短い選択情報を発行して、選択した Work を取得します。ブラウザーはその Work を解析して表示します。認可済みの一覧が空でも API の正常な結果です。シーンの表示成功とは区別してください。

問題が起きた場合

段階対応
インストールNode 24 とレジストリーへのアクセスを確認します。ロックファイルを保持し、依存関係の競合を隠すために --force を使わないでください。
ローカルサーバー表示されたポートを使うか、ローカル環境で未使用のポートを設定します。
WebGL・シーンのアセットブラウザーの対応状況とアセットへの接続を確認します。読み込み中やエラーの状態を表示したままにしてください。
Team APIローカルサーバーが報告した段階に沿って確認します。公開デモの成功だけでは認証情報を検証できません。

既存のアプリへの組み込み前に React ライフサイクル、Team データを使う環境の公開前に 本番環境のチェックリスト を確認してください。

動作するビューアーのコードを読む

既定のビューアーは React のライフサイクルフックと実際の SDK を使います。変更する準備ができたら全文を開いてください。以下のコードはドキュメントのビルド時にダウンロード用プロジェクトから読み込まれます。

src/Viewer.tsx の全体を表示
import { useEffect, useRef, useState } from "react";
import type { Five as FiveInstance, Mode, State } from "@realsee/five";
import { requestApi } from "./api";

export type Scene =
  | { kind: "demo"; label: string }
  | { kind: "team"; label: string; handle: string };
export type SceneStatus = {
  state: "idle" | "loading" | "ready" | "error";
  message: string;
  stage: "demo" | "work" | "scene";
};

export function Viewer({
  scene,
  status,
  onStatus,
}: {
  scene: Scene | null;
  status: SceneStatus;
  onStatus: (value: SceneStatus) => void;
}) {
  const host = useRef<HTMLDivElement>(null);
  const viewer = useRef<FiveInstance | null>(null);
  const initial = useRef<State | null>(null);
  const [currentMode, setCurrentMode] = useState<Mode | null>(null);
  const [modeChanging, setModeChanging] = useState(false);
  const [modeError, setModeError] = useState("");

  useEffect(() => {
    if (!scene || !host.current) return;
    const controller = new AbortController();
    let instance: FiveInstance | null = null;
    let resize: ResizeObserver | null = null;
    let stopped = false;
    let stopModeListener: (() => void) | undefined;
    const layer = document.createElement("div");
    layer.className = "canvas-layer";
    host.current.append(layer);

    async function load() {
      onStatus({
        state: "loading",
        stage: scene!.kind === "demo" ? "demo" : "work",
        message:
          scene!.kind === "demo"
            ? "Loading the public sample…"
            : "Checking your selection and fetching Work…",
      });
      try {
        const [{ Five, parseWork }, work] = await Promise.all([
          import("@realsee/five"),
          scene!.kind === "demo"
            ? import(
                "@realsee/open-works/virtual/816lPVZQkQDF5XOpPo/work.json"
              ).then((module) => module.default)
            : requestApi<{ work: unknown }>("/api/team/work", {
                method: "POST",
                body: JSON.stringify({
                  sceneHandle: (scene as Extract<Scene, { kind: "team" }>)
                    .handle,
                }),
                signal: controller.signal,
              }).then((data) => data.work),
        ]);
        if (stopped) return;
        onStatus({
          state: "loading",
          stage: "scene",
          message: "Rendering the spatial scene…",
        });
        instance = new Five({ imageOptions: { size: 512 } });
        viewer.current = instance;
        stopModeListener = instance.on("modeChange", (mode) => {
          if (!stopped) setCurrentMode(mode);
        });
        instance.appendTo(layer);
        await instance.load(
          parseWork(work as Parameters<typeof parseWork>[0]),
          "initial"
        );
        if (stopped) return;
        initial.current = {
          ...instance.state,
          offset: instance.state.offset.clone(),
        };
        setCurrentMode(instance.state.mode);
        setModeError("");
        setModeChanging(false);
        instance.refresh();
        resize = new ResizeObserver(() => instance?.refresh());
        resize.observe(layer);
        onStatus({
          state: "ready",
          stage: "scene",
          message:
            "Scene ready. Drag to look around and select a floor point to move.",
        });
      } catch (error) {
        if (stopped) return;
        instance?.dispose();
        instance = null;
        viewer.current = null;
        layer.replaceChildren();
        const detail =
          error && typeof error === "object" && "detail" in error
            ? error.detail
            : undefined;
        const stage =
          detail && typeof detail === "object" && "stage" in detail
            ? detail.stage
            : "scene";
        onStatus({
          state: "error",
          stage: stage === "work" || stage === "selection" ? "work" : "scene",
          message:
            error instanceof Error && detail
              ? error.message
              : "The scene could not render. Check WebGL and access to the scene assets, then reload the scene. API success and scene rendering are separate checks.",
        });
      }
    }
    void load();
    return () => {
      stopped = true;
      controller.abort();
      resize?.disconnect();
      stopModeListener?.();
      if (viewer.current === instance) viewer.current = null;
      initial.current = null;
      instance?.dispose();
      layer.remove();
    };
  }, [scene, onStatus]);

  async function toggleMode() {
    const instance = viewer.current;
    if (!instance || modeChanging) return;
    setModeChanging(true);
    setModeError("");
    try {
      const nextMode =
        instance.state.mode === "Panorama" ? "Floorplan" : "Panorama";
      await instance.changeMode(nextMode);
    } catch {
      if (viewer.current === instance)
        setModeError(
          "This scene could not switch modes. Some Work data has no floorplan model. The current view is still available."
        );
    } finally {
      if (viewer.current === instance) setModeChanging(false);
    }
  }

  return (
    <section className="viewer-card" aria-label="Spatial scene viewer">
      <div className="viewer-heading">
        <span className="eyebrow">FIVE SDK · LIVE CANVAS</span>
        <span
          className={`scene-badge ${
            status.state === "ready" ? "is-ready" : ""
          }`}
        >
          <span className="status-dot" />
          {status.state === "ready"
            ? "Ready"
            : status.state === "loading"
            ? "Loading"
            : scene
            ? "Needs attention"
            : "No scene selected"}
        </span>
      </div>
      <div
        ref={host}
        className="viewer"
        role="region"
        aria-label={scene?.label || "Select a Team VR to render"}
        aria-busy={status.state === "loading"}
        tabIndex={0}
        onKeyDown={(event) => {
          if (
            event.target === event.currentTarget &&
            event.key.toLowerCase() === "r" &&
            initial.current
          )
            viewer.current?.setState(initial.current, true);
        }}
      >
        {status.state !== "ready" && (
          <div
            className="viewer-overlay"
            role={status.state === "error" ? "alert" : "status"}
          >
            <div>
              <div
                className={`spatial-mark ${
                  status.state === "loading" ? "is-loading" : ""
                }`}
                aria-hidden="true"
              >
                ⌘
              </div>
              <p>
                {scene
                  ? status.message
                  : "Select a VR from your Team directory."}
              </p>
              {!scene && (
                <span className="viewer-hint">
                  Your public demo is one tab away.
                </span>
              )}
            </div>
          </div>
        )}
      </div>
      <div className="viewer-footer">
        <span>
          {scene?.label || "Team scene"}{" "}
          <span className="subtle">
            ·{" "}
            {scene?.kind === "demo"
              ? "Public sample Work"
              : "Your configured Team"}
          </span>
        </span>
        <div className="viewer-actions">
          <button
            className="text-button"
            disabled={status.state !== "ready" || modeChanging}
            onClick={() => void toggleMode()}
          >
            {modeChanging
              ? "Switching…"
              : currentMode === "Panorama"
              ? "Switch to floorplan"
              : "Switch to panorama"}
          </button>
          <button
            className="text-button"
            disabled={status.state !== "ready" || modeChanging}
            onClick={() => {
              if (initial.current)
                viewer.current?.setState(initial.current, true);
            }}
          >
            Reset view <span aria-hidden="true">↗</span>
          </button>
        </div>
      </div>
      {modeError && (
        <p className="viewer-mode-error" role="alert">
          {modeError}
        </p>
      )}
    </section>
  );
}