ローカルで動かす
React + TypeScript の完全なプロジェクトです。Node 24 が必要です。
ターミナルを操作できる AI コーディングツールに貼り付けてください。アプリの起動と確認を依頼するプロンプトです。
実際の公開シーンを使い、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>
);
}