キャンバス¶
Overview¶
キャンバスは、訪問者と運営者がブラウザの中で図を描くツールである。
構成図を描く draw.io のツールと、手描きの図を描く Excalidraw のツールの 2 つを、play.fukuemon.dev で提供する。
運営者は、キャンバスから書き出した SVG を記事に埋め込む。訪問者は、記事の図をキャンバスで開いて、描き変えられる。
全体の構成とホスト名は fukuemon.dev Design Doc に、プレイグラウンドのアプリの構成は プレイグラウンド にある。
方式は、次の ADR が決めている。
- ADR-0039: キャンバスは、構成図を draw.io の Embed Mode で、手描きの図を Excalidraw で、別のパスのツールとして提供する
- ADR-0040: 記事に埋め込むキャンバスの図は、元データ入りの SVG を
content/figures/に置いて画像の記法で指し、ビルドのときにインラインにし、キャンバスには図の名前を渡す - ADR-0043: キャンバスとスライドのエディタは、ツールごとに、名前を付けた複数の図とスライドを IndexedDB に保存する
Scope¶
持つものは次のとおり。
- 2 つのツールのパス、ディレクトリ、ページの構成
- IndexedDB に保存する図の形と、保存するモジュール
- 図の一覧の画面と、開く、名前を変える、消す、新しく作る操作
- 保存、ファイルの読み書き、SVG の書き出しの流れ
- 記事の図を置くディレクトリ、Markdown の書き方、ビルドの処理、図の URL
- 記事の図をキャンバスで開く流れ
持たないものは次のとおり。
- スライドのエディタの画面。保存するモジュールは、スライドのエディタと共有する。
- AI のエージェントからの操作の名前と入出力、スイッチと確認と操作の記録の画面。
- 図に入る第三者のアイコンと書体を、MIT License の対象から外すことを書いた文言。
- 画面のコンポーネントの見た目。packages/ui/DESIGN.md が持つ。
Design¶
ツールとパス¶
| パス | ツール | エディタ | cross-origin isolation のヘッダ |
|---|---|---|---|
/drawio/ |
draw.io のツール | embed.diagrams.net の iframe |
返さない |
/excalidraw/ |
Excalidraw のツール | バンドルした @excalidraw/excalidraw |
返さない |
- パス、
src/のディレクトリ、保存するデータのtoolの値、書き出すファイルの拡張子は、同じ語(drawio、excalidraw)を使う。 - 記事に、2 つのツールのページを埋め込まない。
- ページは、プレイグラウンドのほかのツールと同じく、iframe の中で開かれたかを判定する。iframe の中では、ページのヘッダを出さず、AI のエージェントからの操作を登録しない。
- ページが読むクエリは、次の 2 つ。
| クエリ | 値 | 動き |
|---|---|---|
doc |
保存した図の id |
保存した図を開く |
figure |
記事の図の名前 | 記事の図を読み込む。doc は読まない |
- クエリが無いときは、そのツールの図のうち、最後に更新した図を開く。図が 1 つも無いときは、空の図を出す。
docのidに当たる図が無いときと、idがほかのツールの図を指すときは、クエリが無いときと同じに扱う。- ページは、開いている図が変わるたびに、URL のクエリを、履歴を足さずに書き換える。再読み込みで、同じ図が開く。
- 保存した図を開いているときは、
docにそのidを書き、figureを消す。 - まだ保存していない空の図を開いているときは、
docとfigureを消す。 - 記事の図を開いていて、まだ保存していないときは、
figureを残す。
- 保存した図を開いているときは、
ディレクトリ¶
キャンバスのコードは、apps/playground の中に、プレイグラウンドのほかのツールと同じ階層で置く。
apps/playground/
drawio/index.html draw.io のツールのエントリーポイント
excalidraw/index.html Excalidraw のツールのエントリーポイント
src/
drawio/ draw.io のツールのページ。iframe との postMessage、XML の読み書き
excalidraw/ Excalidraw のツールのページ。scene の読み書き、書体の配信の設定
documents/ 図とスライドの保存、一覧のダイアログ、名前を変える操作と消す操作のコンポーネント
figures/ 記事の図の名前をクエリから読む処理、記事の図の読み込み
packages/
figures/src/ 図の名前の規則、ファイルの名前と URL のパスの形
図の名前の規則と、ファイルの名前と URL のパスの形は、packages/figures(@fukuemon/figures)に置く。
apps/web のビルドと、apps/playground の figures/ が、この package を使う。
ADR-0040: 記事に埋め込むキャンバスの図は、元データ入りの SVG を content/figures/ に置いて画像の記法で指し、ビルドのときにインラインにし、キャンバスには図の名前を渡す
次の図は、キャンバスのディレクトリと package の間で許す import の向きを示す。矢印の無い組は、import しない。
flowchart TB
drawio["src/drawio"]
excalidraw["src/excalidraw"]
documents["documents"]
figures["figures"]
ui["@fukuemon/ui"]
fp["@fukuemon/figures"]
drawio --> documents
drawio --> figures
drawio --> ui
excalidraw --> documents
excalidraw --> figures
excalidraw --> ui
documents --> ui
figures --> fp
documents/は、ツールの種類ごとの処理を持たない。dataと、ツールがDocumentContentに足した値を、解釈せずに、そのまま読み書きする。documents/は、ツールの種類に依らない、次の処理を、React の外のモジュールとして持つ。ツールのページは、エディタとのやり取り、中身が変わったかの比較、ファイルの形式の処理だけを持つ。- 開いている図の
idの状態と、URL のクエリとの同期 - 最初に編集したときに作るか、保存するかの判定
- まだ IndexedDB に書いていない中身を、図を切り替える前と、ページを閉じるときに書く処理
- 開いている図を消したときに、次に開く図を決める処理
- 新しく作るものの名前の連番
- 開いているものの中身を読み込む Promise。開くものが変わるたびに、新しい Promise に替える。最初の保存で
idが付いたときは、替えない - 読み込みの番号の Store。Promise を替えるたびに進める。スライドのエディタが、エディタのコンポーネントを作り直す
keyに使う - 最後の書き込みの結果の Store。ツールのページと、名前を変えるコンポーネントの書き込みの結果を持つ
- 開いている図の
- ツールのページは、まだ
documents/に渡していない中身を返す関数を、documents/に登録できる。documents/は、図を切り替える前と、ページを閉じるときに、この関数を呼び、返った中身を先に書く。 - ツールのページは、
figures/の読み込みと、自分の元データを取り出す処理をつないだ関数を作り、URL から消すクエリのキー(figure)と一緒に、documents/のモジュールに引数で渡す。- 関数は、クエリに
figureが無い、エラー、読めた(図の名前と中身)、の 3 つを返し分ける。 documents/は、最初にこの関数を呼び、figureが無いときだけ、docを読む。- この関数は、省略できる。スライドのエディタは、渡さない。
- クエリのキーは、
figures/が、@fukuemon/figuresの値を、そのまま公開する。
- 関数は、クエリに
documents/とfigures/は、互いを import しない。2 つをつなぐ処理は、ツールのページが持つ。documents/とworkspace/(訪問者のファイルの保存)は、互いを import しない。@fukuemon/figuresは、workspace のほかの package にも、React にも依存しない。SVG を読む処理、fetch、2 つのアプリの origin の値を持たない。@fukuemon/figuresを import するのは、apps/playgroundではfigures/だけである。documents/は、import しない。documents/とfigures/は、project/と、実行環境の package を import しない。Storeの型は、documents/が、@fukuemon/sql-runtimeのStoreと同じ形で定義する。形を変えるときは、両方を直す。documents/のコンポーネントは、画面に出す語(「図」か「スライド」)と、新しく作るものの名前の付け方を、props で受ける。@excalidraw/excalidrawを import するのは、src/excalidraw/だけである。src/drawio/とsrc/excalidraw/は、ツールのページのディレクトリである。ツールのページの間の import の規則は、context/codebase.md にある。
ページの構成¶
2 つのツールのページは、同じ構成にする。ヘッダと一覧のダイアログは、documents/ と @fukuemon/ui のコンポーネントで組む。
┌──────────────────────────────────────────────────────────────────────┐
│ draw.io [VPC の構成] [図の一覧] [新しい図] [ファイル ▾] 保存した 12:34 │
│ エディタは draw.io のサーバーから読み込みます。PDF への書き出しは、… │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ エディタ │
│ │
└──────────────────────────────────────────────────────────────────────┘
- ヘッダは、ツールの名前、開いている図の名前、「図の一覧」、「新しい図」、「ファイル」のメニュー、保存の状態を、この順に並べる。
mdのブレークポイントより狭い幅では、ヘッダを 2 行にする。1 行目に、ツールの名前と図の名前を置く。2 行目に、操作と保存の状態を置く。図の名前は、1 行に収まらないときに、末尾を省略する。- 図の名前を押すと、その場で名前を変えられる。まだ保存していない図の名前を変えたときは、その時点で、新しい図として保存する。
- 名前を変える入力は、
@fukuemon/uiの FileTree の名前の入力と同じ形にする。TextField と確定のボタンを form に入れ、Enter で確定し、Esc で取り消す。 - 空の名前と、空白だけの名前は、確定しない。ほかの図と同じ名前は、確定できる。
- 図の名前のボタンの読み上げの名前は、「名前を変える: <図の名前>」にする。
- 一覧のダイアログの中で名前を変えている間の Esc は、名前の変更だけを取り消し、ダイアログを閉じない。
- 名前を変える入力は、
- 「ファイル」のメニューは、ActionMenu で作る。開くボタンには、「ファイル」の文字を出す。
- 「ファイル」のメニューは、ファイルを開く、ファイルに保存、SVG を書き出す、の 3 つを持つ。
- 保存の状態は、最後に保存した時刻を、文で出す。時刻が変わっても、読み上げない。
- 保存できないときは、ヘッダの下に、Notice(
danger、isAlert)で、保存できないことと理由を出す。保存の容量が足りないときは、図を消すと容量が空くことを、文に入れる。 - 記事の図を読めないときと、開いたファイルが読めないときのエラーも、Notice(
danger、isAlert)で出す。記事の図を読めないときは、エディタの代わりに出す。 - draw.io のツールは、ヘッダの下に、次の 2 つを書いた 1 行の文を、いつも出す。閉じる操作は持たない。文は、Notice を使わず、補足の文の大きさと色(
text-caption、text-secondary)で書く。- エディタを、draw.io のサーバーから読み込むこと。
- PDF への書き出しが、draw.io のサーバーを通ること。
- エディタの明暗は、ページを開いたときの OS の設定で決める。draw.io には
darkのパラメータを、Excalidraw にはthemeを渡す。 - draw.io の iframe の URL には、
libs=aws4;gcp2を渡し、draw.io の中の保存と終了のボタンを出さない設定を渡す。 - Excalidraw は、Library のボタンを隠し、書体を自分のパスから配る。
- AI のエージェントからの操作のスイッチと、操作の記録の欄の位置は、AI からの操作の設計が持つ。
保存するデータ¶
図は、play.fukuemon.dev の IndexedDB の playground-documents のデータベースに、2 つの object store に分けて保存する。
ADR-0057: 図とスライドは、一覧に出す値と中身を 2 つの object store に分けて、訪問者のファイルと別のデータベースに保存する
| object store | キー | レコード | index |
|---|---|---|---|
documents |
id |
一覧に出す値(DocumentMeta) |
[tool, updatedAt] |
contents |
id |
図の中身(DocumentContent) |
無い |
type ToolKind = "drawio" | "excalidraw" | "slides";
type DocumentMeta = {
id: string;
tool: ToolKind;
name: string;
createdAt: number;
updatedAt: number;
source?: { kind: "figure"; name: string };
};
type DocumentContent = {
id: string;
data: string;
};
dataは、draw.io のツールでは図の XML、Excalidraw のツールでは.excalidrawの JSON(scene を、Excalidraw のファイルの形式で書いた JSON)である。.excalidrawの JSON は、貼った画像を中に持つ。sourceは、記事の図から作った図だけが持つ。- 時刻は、エポックからのミリ秒の数値である。
documents/ は、次の型でデータベースを公開する。
Store は、プレイグラウンドの NodeRuntime の Store と同じ形である。
type DocumentBody = Omit<DocumentContent, "id">;
type WriteResult = { ok: true } | { ok: false; reason: "unavailable" | "quota" | "missing" };
type DocumentStore = {
status: Store<{ kind: "ready" } | { kind: "unavailable" }>;
list: (tool: ToolKind) => Store<readonly DocumentMeta[]>;
read: (id: string) => Promise<{ meta: DocumentMeta; content: DocumentBody } | undefined>;
create: (input: {
tool: ToolKind;
name: string;
content: DocumentBody;
source?: DocumentMeta["source"];
}) => Promise<{ ok: true; meta: DocumentMeta } | { ok: false; reason: "unavailable" | "quota" }>;
save: (id: string, content: DocumentBody) => Promise<WriteResult>;
rename: (id: string, name: string) => Promise<WriteResult>;
remove: (id: string) => Promise<WriteResult>;
};
documents/は、DocumentContentのidのほかの値を、そのまま読み書きする。ツールがDocumentContentに足した値も、同じ関数で通る。- 一覧は、更新した時刻の新しい順に並ぶ。画面は、
useSyncExternalStoreで読む。documents/は、ツールごとに 1 つの Store を持ち、listは、同じツールに、いつも同じ Store を返す。 create、save、removeは、2 つの store を 1 つのトランザクションで書く。saveは、updatedAtを進める。renameは、updatedAtを進めない。- 書き込みの関数は、失敗しても Promise を失敗させず、理由を持つ値を返す。ページは、失敗のときに、保存できないことを保存の状態に出す。
unavailable: IndexedDB を読めなかった。statusがunavailableになり、その後は、ページを開いている間、documents/は IndexedDB に書き込まない。quota: 保存の容量が足りない。statusは変えない。次の書き込みは、もう一度試す。訪問者は、図を消して、容量を空けられる。missing:idに当たる図が、documentsに無い。ほかのタブが消した図である。documents/は、何も書かない。
- IndexedDB を読めないときは、一覧が空で、ページは空の図を出す。訪問者は、描いた図を、ファイルに保存できる。
- 2 つのタブで同じ図を開いたときは、後から保存したタブの中身が残る。
- 一覧は、ほかのタブの変更を、ページを開き直すまで出さない。
図の一覧¶
「図の一覧」のボタンは、ダイアログを開く。エディタは、読み込んだままにする。
┌────────────────────────────────────────────────┐
│ 図の一覧 ✕ │
│ 名前 更新 │
│ VPC の構成(開いている) 11/13 12:34 [⋯] │
│ 無題の図 2 11/12 09:10 [⋯] │
│ vpc(記事の図から) 11/10 21:02 [⋯] │
│ [新しい図] │
└────────────────────────────────────────────────┘
[⋯] のメニュー: 名前を変える、消す
- 一覧は、そのツールの図だけを出す。
- 一覧は、Dialog の中に、Table で作る。「新しい図」は、Dialog の
footerに置く。 - 行の名前を押すと、その図を開き、ダイアログを閉じる。
- 行の名前は、
?doc=<id>をhrefに持つ Link にする。ページは、押したときに、ページを読み込み直さずに図を切り替える。新しいタブで開く操作は、ブラウザに任せる。
- 行の名前は、
- 行の「⋯」のボタンの読み上げの名前は、
<図の名前> の操作にする。「消す」は、ActionMenu のisDangerの項目にする。 - 「新しい図」を押すと、まだ保存していない空の図を開き、ダイアログを閉じる。前に開いていた図は変えない。名前は、
無題の図に、ツールごとの連番を付ける(無題の図 2)。- 連番は、そのツールの図の名前のうち、
無題の図 <数>の形のものの、最も大きい数に 1 を足した数にする。その形の名前が無いときは、1 にする。
- 連番は、そのツールの図の名前のうち、
- 「消す」は、ConfirmDialog で確認を出し、訪問者が通したら消す。消した図は戻せない。
- 題は「「<図の名前>」を消す」、本文は「消した図は戻せません。」、確定のボタンは「消す」にする。
- 消した後は、一覧のダイアログを開いたままにし、フォーカスを、次の行の名前に移す。行が無いときは、「新しい図」に移す。
- ほかのタブが先に消していたとき(
missing)は、消せたときと同じに扱う。そのほかの失敗は、ConfirmDialog を閉じ、一覧の上に Notice(danger)で出す。
- 開いている図を消したときは、残りの図のうち、最後に更新した図を開く。残りが無いときは、空の図を出す。
- 開いている図の行には、開いていることを出す。
- 記事の図から作った図の行には、記事の図から作ったことを出す。
- 図を複製する操作は、持たない。
空の図は、最初に編集したときに、新しい図として保存する。編集しないで閉じた空の図は、一覧に残らない。
保存の流れ¶
次の図は、ページを開いてから、編集した図が保存されるまでの流れを示す。
sequenceDiagram
participant V as 訪問者
participant P as ツールのページ
participant D as documents(IndexedDB)
participant E as エディタ
V->>P: ページを開く
P->>D: 開く図を読む(doc の id か、最後に更新した図)
alt 図がある
D-->>P: 一覧に出す値と中身
else 図が無い、または IndexedDB を読めない
D-->>P: 無し
opt IndexedDB を読めない
P-->>V: 保存できないことを出す
end
end
P->>E: 中身(無いときは空の図)を渡す
V->>E: 図を編集する
E-->>P: 編集した後の中身
alt 保存した図を開いている
P->>D: save(id、中身)
else まだ保存していない図(空の図、記事の図)
P->>D: create(ツール、名前、中身、記事の図の名前)
P-->>V: URL の doc を、新しい id に書き換える
end
P-->>V: 保存した時刻を出す
- draw.io のツールは、iframe の初期化のメッセージを受けてから、
loadで図の XML を渡す。編集のたびに届くautosaveのイベントの XML を保存する。 - Excalidraw のツールは、
onChangeを受けてから 300 ミリ秒のあいだ次のonChangeが無いときに、.excalidrawの JSON を保存する。 - ページは、
embed.diagrams.netの origin のメッセージだけを受ける。 - ページは、図の中身が変わったときだけ保存する。選択、スクロール、拡大と縮小、エディタが読み込みの直後に出す通知では、保存しない。
- draw.io のツールは、届いた XML を、最後にエディタに渡したか保存した XML と比べる。
- Excalidraw のツールは、要素と、貼った画像が変わったかを比べる。
- 図を切り替えるときは、エディタを読み込み直さずに、次の図の中身を渡す。
- 図を切り替える前と、ページを閉じるとき(
pagehide)に、まだ IndexedDB に書いていない中身を、先に書く。エディタから届いた中身は、編集したときに開いていた図のidに保存する。 - エディタの外から図の中身を変えたとき(AI のエージェントからの操作)は、ページが、訪問者の編集と同じ流れで保存する。
ファイルの読み書きと書き出し¶
| 操作 | draw.io のツール | Excalidraw のツール |
|---|---|---|
| ファイルに保存 | 図の XML を、<図の名前>.drawio で保存する |
.excalidraw の JSON を、<図の名前>.excalidraw で保存する |
| SVG を書き出す | 元データ入りの SVG を、<図の名前>.drawio.svg で保存する |
元データ入りの SVG を、<図の名前>.excalidraw.svg で保存する |
| ファイルを開く | .drawio と、元データ入りの SVG を開く |
.excalidraw と、元データ入りの SVG を開く |
- 「ファイルを開く」は、開いたファイルを、新しい図として保存して開く。前に開いていた図は変えない。名前は、ファイルの名前から、拡張子(
.drawio.svg、.excalidraw.svg、.drawio、.excalidraw、.svg)を除いたものにする。 - 元データ入りの SVG を開いたときは、SVG から元データ(図の XML か、
.excalidrawの JSON)を取り出して、dataに保存する。 - 開いたファイルに元データが無いときと、そのツールの形式でないときは、エラーを出し、何も保存しない。
- 書き出す SVG は、背景を持たない。運営者は、書き出した SVG を、そのまま記事の図のディレクトリに置ける。
- draw.io のツールは、
exportのアクションにxmlsvgの形式を渡す。SVG は、明暗の両方の色を持つ。 - Excalidraw のツールは、scene を入れる設定で、エディタの明暗に依らず、ライトの配色で書き出す。
- draw.io のツールは、
- ファイルの読み書きは、訪問者のブラウザの中だけで行う。fukuemon.dev のサーバーに、図を送らない。
記事の図¶
運営者は、キャンバスから書き出した SVG を apps/web/src/content/figures/ に置き、記事の Markdown から、画像の記法で指す。
ADR-0040: 記事に埋め込むキャンバスの図は、元データ入りの SVG を content/figures/ に置いて画像の記法で指し、ビルドのときにインラインにし、キャンバスには図の名前を渡す
apps/web/src/content/figures/
vpc.drawio.svg draw.io のツールから書き出した図
request-flow.excalidraw.svg Excalidraw のツールから書き出した図

- ファイルの名前は、
<名前>.drawio.svgか<名前>.excalidraw.svgである。<名前>は、小文字の英数字とハイフンだけにする(^[a-z0-9][a-z0-9-]*$)。 - ビルドは、ファイルの名前の検証、出力するパスの組み立て、「キャンバスで開く」のリンクのパスとクエリの組み立てに、
@fukuemon/figuresを使う。play.fukuemon.devの origin は、ビルドが足す。 - 画像のパスは、記事のファイルからの相対パスで書く。ビルドは、記事のファイルの場所からパスを解決し、解決した先が
content/figures/の直下のファイルのときに、記事の図として扱う。
apps/web のビルドは、記事の図を指す画像を、次のとおり置き換える。
置き換える処理は、Markdown を HTML にする処理のプラグインとして、apps/web/src/lib/ に置く。
- 画像を、
<figure>に置き換える。中に、インラインの SVG、キャプション、「キャンバスで開く」のリンクを、この順に置く。 - 代替テキストは、SVG の読み上げの名前にする。画像の記法のタイトルは、キャプションにする。タイトルが無いときは、キャプションを出さない。
- 「キャンバスで開く」のリンクの先は、
https://play.fukuemon.dev/<ツール>/?figure=<名前>である。- リンクの読み上げの名前に、図の代替テキストを入れる(例: 「VPC の構成をキャンバスで開く」)。1 つの記事に複数の図があっても、リンクを見分けられるようにするためである。
- リンクは、同じタブで開く。
- 図が本文の幅に収まらないときは、縮めずに、横にスクロールする枠に入れる。図の中の文字が、小さくなりすぎないようにするためである。
- 枠のキーボードの操作は、
@fukuemon/uiの Table の、幅に収まらない表の枠と同じにする。Tab のキーで枠にフォーカスし、左右の矢印のキーで横にスクロールする。
- 枠のキーボードの操作は、
- Excalidraw の図で、運営者が選んだ色は、ダークでもライトの色のままである。運営者は、図を置くときに、ライトとダークの両方で読めることを確認する。
- インラインにする SVG は、色の値を、トークンの CSS 変数に置き換える。
- ADR-0040: 記事に埋め込むキャンバスの図は、元データ入りの SVG を
content/figures/に置いて画像の記法で指し、ビルドのときにインラインにし、キャンバスには図の名前を渡す - draw.io の SVG は、ルートの
color-schemeの指定を外し、背景と線の色の変数の値を、トークンの CSS 変数にする。 - Excalidraw の SVG は、線と文字のデフォルトの色を、トークンに置き換える。
- ADR-0040: 記事に埋め込むキャンバスの図は、元データ入りの SVG を
- インラインにする SVG からは、元データ(draw.io の
contentの属性、Excalidraw の scene)を外す。元データは、図の URL で配る SVG が持つ。 - ビルドは、次のときに失敗する。
- 代替テキストが空である。
- 指したファイルが無い。
- ファイルの名前が、規則に合わない。
- SVG の中の元データが、拡張子のツールと合わない。
- 1 つの記事に複数の図を入れて、SVG の中の
idが重なるときは、ビルドが、図ごとにidを付け替える。
ビルドは、置いた SVG を、変えずに https://fukuemon.dev/figures/<名前>.<ツール>.svg に出力する。
fukuemon.dev は、/figures/* に、play.fukuemon.dev を許す Access-Control-Allow-Origin を返す。ヘッダは、apps/web/public/_headers に書く。
記事の図をキャンバスで開く¶
次の図は、訪問者が記事の「キャンバスで開く」のリンクを押してから、図が保存されるまでの流れを示す。
sequenceDiagram
participant V as 訪問者
participant A as 記事のページ
participant P as ツールのページ
participant W as fukuemon.dev
participant D as documents(IndexedDB)
V->>A: 「キャンバスで開く」を押す
A->>P: /drawio/?figure=vpc を開く
alt 名前が規則に合わない
P-->>V: エラーと、プレイグラウンドの一覧へのリンクを出す
else 合う
P->>W: /figures/vpc.drawio.svg を fetch する
alt 読めない、元データが無い
P-->>V: エラーと、プレイグラウンドの一覧へのリンクを出す
else 読めた
W-->>P: 元データ入りの SVG
P-->>V: 図をエディタに出す。保存はしない
V->>P: 図を編集する
P->>D: create(ツール、名前、中身、記事の図の名前)
P-->>V: URL を doc の id に書き換える
end
end
figures/は、@fukuemon/figuresで、URL のクエリのfigureから図の名前を読み、検証する。名前とツールから URL のパスを組み立てて、fetchする。figures/は、図の名前と、SVG の文字列を返す。クエリにfigureが無いときは、無いことを返す。- SVG から元データを取り出して、そのツールの形式かを確認する処理は、ツールのページが持つ。「ファイルを開く」と同じ関数を使う。
- ツールのページは、
figures/に、自分のツール(drawioかexcalidraw)を渡す。図を描いたツールの型は、@fukuemon/figuresが持つ。figures/とdocuments/は、互いの型を使わない。@fukuemon/figuresのツールの型の値は、ToolKindからslidesを除いた値と同じ文字列である。2 つの型は互いを import しないので、ツールのページが、同じ文字列をfigures/とdocuments/の両方に渡す。
figures/は、名前が規則に合わないときは、fetchしない。- URL は、
<fukuemon.dev の origin>/figures/<名前>.<ツール>.svgである。パスは@fukuemon/figuresが組み立て、origin はfigures/が足す。ツールは、ページのパスで決まる。fukuemon.dev の origin は、プレイグラウンドのプロジェクト と同じ、ビルドのときの環境変数から読む。 fetchは、リダイレクトを辿らない。- 開いただけでは、IndexedDB に保存しない。最初に編集したときに、新しい図として保存する。同じリンクを開くたびに、一覧に図が増えないようにするためである。
- 新しい図の名前は、記事の図の名前にする。レコードの
sourceに、記事の図の名前を持つ。 - 保存してあった図は変えない。同じ記事の図から作った図が保存してあっても、リンクは、記事の図を開く。
- エラーのときは、保存してある図を開かない。リンクの先は、
https://fukuemon.dev/playgroundである。
Interface¶
置くファイル¶
apps/playground/drawio/、apps/playground/excalidraw/: ツールのエントリーポイント。apps/playground/src/drawio/、src/excalidraw/、src/documents/、src/figures/: キャンバスのコード。packages/figures/: 図の名前の規則と、ファイルの名前と URL のパスの形。apps/web/src/content/figures/<名前>.drawio.svg、<名前>.excalidraw.svg: 運営者が置く、記事の図。apps/web/src/lib/: 記事の図を指す画像を置き換えるプラグインと、図を/figures/に出力する処理。apps/web/public/_headers:/figures/*に返すAccess-Control-Allow-Origin。
読む値¶
- URL のクエリの
doc: 保存した図のid。 - URL のクエリの
figure: 記事の図の名前。 - IndexedDB の
playground-documents: object store のdocumentsとcontents。 https://fukuemon.dev/figures/<名前>.<ツール>.svg: 記事の図の、元データ入りの SVG。
動くチェック¶
- ツールのページのディレクトリの間の import を止めるチェックは、context/codebase.md にある。
- Oxlint の
no-restricted-imports:apps/playground/src/excalidraw/のほかからの、@excalidraw/excalidrawの import を止める。 - Oxlint の
no-restricted-imports:apps/playgroundの中の、src/figures/のほかからの、@fukuemon/figuresの import を止める。 - Oxlint の
no-restricted-imports:apps/playground/src/の、documents/とfigures/の間の import を止める。 apps/webのビルド: 図を指す画像の代替テキスト、ファイルの有無、ファイルの名前、拡張子と元データの一致を確認する。