This is the full developer documentation for FloatSoda # FloatSoda ドキュメント > FloatSoda のドキュメントは、読む人と目的ごとのタブに分かれています。初めて触るなら、チュートリアルで最初のオーバーレイを作るところから始めてください。 FloatSoda のドキュメントは、読む人と目的ごとのタブに分かれています。初めて触るなら、チュートリアルで最初のオーバーレイを作るところから始めてください。 ## ページ一覧 | ページ | 内容 | 対象読者 | | :-- | :-- | :-- | | [チュートリアル](/tutorials/) | 手を動かして、最初のオーバーレイを作る | 利用者 | | [ガイド](/guide/) | 探す・採用する・準備する・作る・困ったとき・配る・メンテナンスするの順に、目的の手順と仕組みを引く | 利用者 | | [リファレンス](/reference/) | 作例、制約、動作環境、用語、部品を引く | 利用者 | | [コントリビューター](/contributor/) | FloatSoda を一緒に育てるための理解・準備・貢献の手順 | コントリビュータ | # コントリビューター > FloatSoda を一緒に育てるための理解・準備・貢献の手順 ## FloatSoda に関わる人へ FloatSoda を一緒に育ててもらうための案内です。コントリビューター向けの docs は、関わり始めてから提案するまでの順に、6つのグループに分かれています。コードを書かなくても、ドキュメント、動作検証、提案、エージェント向けの skill という入口があります。どのグループから読んでもかまいません。迷ったら「1. はじめに」から読んでください。 ## 1. はじめに 関わってよいのか、何ができるのかを知るグループです。FloatSoda が大事にしていることを読むと、方針に共感できるかを確かめられます。どんな貢献が助かるかを読むと、自分の得意なことの入口が見つかります。自分のツールで不具合を踏んだ人は、切り分けて報告するページへ進みます。 - [FloatSoda が大事にしていること](/contributor/1-get-involved/values/) - [どんな貢献が助かるか](/contributor/1-get-involved/ways-to-help/) - [不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/) ## 2. 理解する 全体の仕組みと、そう作った理由を知るグループです。手を動かして学ぶ入口と、読んで学ぶ入口があります。 - [描画が届くまで(4段、手を動かして学ぶ)](/contributor/2-understand/tutorial-render-path/) - [1フレームが SteamVR に届くまで(読んで学ぶ)](/contributor/2-understand/frame-to-steamvr/) - [決定の記録(ADR)の一覧](/contributor/2-understand/adr-index/) 層ごとの解説は [フレームワーク層](/contributor/2-understand/framework-layer/)(入力は [入力が届くまでを追う](/contributor/2-understand/framework-input/))、[描画層](/contributor/2-understand/rendering-layer/)、[エンジン層](/contributor/2-understand/engine-layer/)、[OVR 層](/contributor/2-understand/ovr-layer/)、[UI 層](/contributor/2-understand/ui-layer/) にあります。層ごとの実験は [フレームワーク層](/contributor/2-understand/experiment-framework/)、[描画層](/contributor/2-understand/experiment-rendering/)、[OVR 層](/contributor/2-understand/experiment-ovr/) にあります。 ## 3. 準備する 手元でビルドとテストを動かし、規約の場所を知るグループです。足りない準備のページだけ開けば足ります。 - [開発環境を用意する](/contributor/3-prepare/dev-environment/) - [テストの分け方](/contributor/3-prepare/test-strategy/) - [エージェントと一緒に作業する](/contributor/3-prepare/work-with-agents/) - [Issue・PR・マイルストーンの読み方](/contributor/3-prepare/read-github/) - [コントリビューターの規約](/contributor/3-prepare/conventions/) ## 4. 書く やりたい貢献が決まっている人が、規約に沿って形にするグループです。規約の正本は [CONTRIBUTING.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md) にあります。 - [Flutter から移植する](/contributor/4-write/port-from-flutter/) - [skill を足す・直す](/contributor/4-write/skills/) ## 5. 出す 変更ができて、PR を出してレビューを通すグループです。 - [レビューの指摘を受けて直す](/contributor/5-submit/address-review/) - [CI が落ちたとき](/contributor/5-submit/ci-failures/) 設計の変更を提案したい人は、次の「6. 提案する」へ進みます。 ## 6. 提案する 新しい機能や設計の変更を提案し、決まったことを記録するグループです。提案が通る前なら「提案を書く」、通った後なら「ADR を書く」を開きます。 - [提案を書く](/contributor/6-propose/write-proposal/) - [決定を ADR に書く](/contributor/6-propose/write-adr/) # 不具合を切り分けて報告する > この手順を終えると、必須の欄が埋まった Issue が1件立ちます。手順は5つです。切り分けないまま報告すると、FloatSoda の問題か、使い方か、環境かが分からず、直す人が再現に時間を使います。報告の形をそろえると、直るまでが早くなります。 ## この手順でできること この手順を終えると、必須の欄が埋まった Issue が1件立ちます。手順は5つです。切り分けないまま報告すると、FloatSoda の問題か、使い方か、環境かが分からず、直す人が再現に時間を使います。報告の形をそろえると、直るまでが早くなります。 自分のツールで FloatSoda を使っていて、おかしな挙動に出会った人が対象です。実機で動かした結果を報告したい人は、[動作検証の結果を報告する場合](#動作検証の結果を報告する場合)に進みます。 ## 前提条件 - GitHub のアカウントを用意しておくこと - 使っている FloatSoda のバージョン(NuGet パッケージのバージョン、または試したコミットの SHA)を確かめておくこと - 不具合が出るコード、または再現の手順を用意しておくこと 再現できない場合は、[うまくいかないとき](#うまくいかないとき)に進みます。 ## 手順1: 利用者向けの切り分けを済ませる 使い方の誤りや環境の問題は、先に利用者向けのガイドで切り分けます。 1. [反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)を読み、当てはまる原因を確かめる 2. [利用者の環境で動かないとき](/guide/7-maintain/user-environment-issues/)を読み、当てはまる原因を確かめる **できた?** 使い方の誤りや環境の問題では、症状の説明がつかない。
できなかったら リンク先の原因に当てはまったときは、そこに書かれた直し方を試します。直ったときは Issue を出す必要はありません。
## 手順2: 最小の再現に絞る 不具合が出るコードから、再現に関係しない部分を外して、小さなコードにします。外しても同じ症状が出るなら、外した部分は再現に要らないので、外したままにします。症状が出なくなったら、外した部分が原因に関わっています。 1. ウィジェットツリーから、子や兄弟のウィジェットを1つ外す 2. 不具合がまだ出るかを確かめる 3. 出なくなったら元に戻して、別の部分を外す 4. 外せる部分がなくなるまで繰り返す Issue テンプレートの「最小再現コード」の欄には、次の形のコードを貼ります。 ```csharp app.CreateWindow(new DashboardWindow { Title = "Repro", Child = new Padding { Spacing = EdgeInsets.All(8), Child = new Text("hello"), }, }); ``` この例は、`app` の作成を省いた断片です。実際に貼るコードは、`app` の作成まで含めて、貼るだけで動く形にします。 **できた?** 貼り付けるだけで動くコードで、不具合が再現する。
できなかったら コードでは再現できない場合は、Issue の同じ欄に再現の手順を文章で書きます。
## 手順3: Flutter でも起きるかを確かめる(Flutter 由来の場合) 不具合が Flutter 由来のウィジェットに関わるときは、Flutter でも同じことが起きるかを確かめます。FloatSoda は Flutter のウィジェットを移植しているので、起きた挙動は次の3つのどれかに分かれます。 - FloatSoda のバグ - 意図された Flutter の仕様 - FloatSoda 固有の意図的な差異 Flutter でも同じことが起きた場合は、意図された仕様と決めつけず、Flutter の公式の説明やテストも確かめます。 この欄は任意です。分かる範囲で書き、分からなければ空欄にして出します。Flutter の元の実装を引く手順は、[開発環境を用意する](/contributor/3-prepare/dev-environment/)にあります。 ## 手順4: バージョンと環境を集める Issue に書く値を、次の表のとおりに書き出します。 | 値 | 調べ方 | | :-- | :-- | | FloatSoda のバージョン | NuGet パッケージのバージョン、または試したコミットの SHA | | .NET SDK のバージョン | `dotnet --version` の出力 | | OS | 例: Windows 11 26200 | | GPU とドライバ、HMD、SteamVR のバージョン | 表示や描画の問題のときだけ | ## 手順5: Issue を出す [Bug report のテンプレート](https://github.com/sumx21t-3310/FloatSoda/issues/new?template=bug_report.yml)を開いて、欄を埋めて出します。必須の欄だけ埋めれば足ります。 必須の欄と任意の欄は、テンプレートの [.github/ISSUE_TEMPLATE/bug_report.yml](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.github/ISSUE_TEMPLATE/bug_report.yml) に書かれています。「最小再現コード / 再現手順」には手順2で絞ったコードを、バージョンと OS の欄には手順4の値を、「Flutter との対応」には手順3で分かったことを書きます。 例外が出たときは、手元のパスや個人情報を伏せたうえで、「例外 / スタックトレース」の欄に全文を貼ります。 タイトルには、何をする Issue かを書きます。種別や領域はラベルで表すので、`feat(Widget):` のような prefix は付けません。 Issue は公開されます。出す前に、手元のパスや個人情報が貼り付けに入っていないかを確かめます。 **できた?** Issue が立ち、`bug` のラベルが付いている。
できなかったら ラベルが付かなかった場合は、テンプレートを使わずに作った可能性があります。作成済みの Issue のコメントで、`bug` ラベルの付与を頼みます。同じ報告を作り直すと、重複した Issue ができます。
自分で直したい場合は、ブランチ名と PR の範囲を [コントリビューターの規約](/contributor/3-prepare/conventions/)で確かめます。 ## 動作検証の結果を報告する場合 実機で試した結果は、サンプルの `checks/results/` に jsonl で足す PR を出して報告します。「落ちた」「落ちなかった」と、HMD、GPU、プラットフォーム、モード、日付を同じ形で残します。ここでの「落ちなかった」は、作例の確かめる点をすべて通ったことを指します。落ちなかった記録も、その環境で動いた証拠になります。 ファイル名と1行の形式は、CONTRIBUTING.md の [サンプルを追加する場合の規約](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md)に定められています。このページには写していないので、PR を出す前に開いてください。項目ごとの手順は、[`floatsoda-device-test-run`](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-run/SKILL.md) に書かれています。 ## うまくいかないとき 再現できないとき、または FloatSoda の問題か判断できないときの行き先は、次のとおりです。 | 状況 | 行き先 | | :-- | :-- | | 使い方の誤りかもしれない | 手順1の2ページ | | 再現しないが、おかしな挙動を見た | 見た状況と環境を書いて Issue を出す | | 判断がつかない | 判断がつかないことを Issue に書いて出す | # FloatSoda が大事にしていること > FloatSoda は、SteamVR のオーバーレイ UI を、Flutter のような宣言的 UI として .NET 10 / C# 14 で書けるフレームワークです。大事にしていることは2つあります。Flutter の考え方を写すことと、UI をすべて C# のコードで書けることです。 ## FloatSoda は何を大事にして作られているか FloatSoda は、SteamVR のオーバーレイ UI を、Flutter のような宣言的 UI として .NET 10 / C# 14 で書けるフレームワークです。大事にしていることは2つあります。Flutter の考え方を写すことと、UI をすべて C# のコードで書けることです。 この2つは、レビューで何を見るか、どんな変更を受け入れるかに直接効きます。先に知っておくと、関わるかどうかを決めやすくなり、PR が差し戻される理由も予想できます。 次は「Flutter の考え方を写す」です。 ## Flutter の考え方を写す FloatSoda は、Flutter の Widget・Element・RenderObject という三つのツリーと、ウィジェットの語彙、そこから観測できる挙動を写しています。Flutter 由来の Widget と RenderObject は、明示された理由がない限り、Flutter と観測できる挙動を変えません。「実装しやすい」「こちらの方が自然」だけを理由にした独自仕様は受け入れません。 理由は、FloatSoda を使う人も、その人の代わりにコードを書く LLM も、Flutter の挙動を期待して書くからです。Flutter との違いは、バグでも意図した差異でも、利用者にとっては同じ手間になります。 写さないものもあります。C# や .NET として自然な表現への置き換え(`event`、`init` / `required`、`record`)は、挙動の差異に数えません。Semantics のように、移植しないと明示した機能もあります。 Flutter との差異を残すときは、今ある差異の台帳([known-divergences.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/references/known-divergences.md))に記録します。決定の記録(ADR)の置き場所と番号の付け方は、決まっていません。 三つのツリーが C# でどう写されているかは、[1フレームが SteamVR に届くまで](/contributor/2-understand/frame-to-steamvr/)で読めます。 次は「UI をすべて C# のコードで書く」です。 ## UI をすべて C# のコードで書く FloatSoda の UI は、シーンもプレハブも持たず、すべて C# のコードで決まります。コードに書けない状態(外部のアセットやファイル)を持ち込む変更は、トレードオフを検討してから入れます。 コードだけで UI が決まると、diff、レビュー、grep、LLM による生成がそのまま使えます。Unity の uGUI では、シーンとプレハブが text として扱いにくく、この点が使いにくさの原因でした。 想定している作り手は3種類です。 - LLM にコードを書かせて、自分用のツールを作る VRChatter - Booth でオーバーレイを売る、Unity に慣れたクリエイター - uGUI を避けたいエンジニア 1番目の作り手が中心なので、public API の合否基準は「更新後の docs だけを読んだ LLM が、その API を正しく使えるか」で決まります。 次は「この方針が決めている、関わり方の線引き」です。 ## この方針が決めている、関わり方の線引き 2つの方針から、関わり方の線が引かれます。現段階では、コアに受け入れるのは Flutter の `basic.dart` 相当のプリミティブ層のウィジェットだけです。`Button` や `Card` のような装飾済みの複合ウィジェットは、コアではなく、別パッケージやユーザーランドでの実装を検討してください。コードに書けない状態を持ち込む変更は、前の節のとおり、トレードオフを検討してから判断します。 ウィジェットの受け入れ範囲の正本は、CONTRIBUTING.md の「受け入れ範囲」です。このページには範囲を写していないので、変更を考えるときは [CONTRIBUTING.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md) を開いてください。 ## 次に読むページ 方針を読んだあとの行き先は、目的で分かれます。 - どんな貢献が助かるかを知る: [どんな貢献が助かるか](/contributor/1-get-involved/ways-to-help/) - 手元で動かす: [開発環境を用意する](/contributor/3-prepare/dev-environment/) - 仕組みを知る: [1フレームが SteamVR に届くまで](/contributor/2-understand/frame-to-steamvr/) # どんな貢献が助かるか > 貢献は5種類あります。ドキュメントを書く、動作検証をする、コードを作る・直す、機能を提案する、エージェント向けの skill を書く、の5つです。コードを書かなくても関われます。どの種類から始めてもかまいません。 ## どの貢献から始めてもよい 貢献は5種類あります。ドキュメントを書く、動作検証をする、コードを作る・直す、機能を提案する、エージェント向けの skill を書く、の5つです。コードを書かなくても関われます。どの種類から始めてもかまいません。 いま何が足りないかは、このページには書きません。状況は変わるので、GitHub の Issue のラベルで探せるようにしています。手が足りない Issue には `help-wanted`、始めやすい Issue には `good-first-issue` が付いています。探し方は [Issue・PR・マイルストーンの読み方](/contributor/3-prepare/read-github/)にあります。 気になる種類の節へ進んでください。 ## ドキュメントを書く 分かりにくかった箇所や、直したい箇所を `docs/` 配下のファイルで直す貢献です。公開サイトは `docs/` からビルドされるので、直すのは `docs/` 配下のファイルです。 要る条件は次のとおりです。 - Git と GitHub の基本操作ができること - 書く規約を読んでおくこと 規約の場所は [コントリビューターの規約](/contributor/3-prepare/conventions/)にあります。 ## 動作検証をする 自分の環境で動かして、結果を報告する貢献です。「落ちた」「落ちなかった」の報告がいちばん助かります。SteamVR は環境で挙動が大きく変わるためです。ここでの「落ちなかった」は、作例の確かめる点をすべて通ったことを指します。異常終了しなかっただけの報告とは区別します。落ちなかった報告は、その環境で作例が動いた記録になります。 要る条件は次のとおりです。 - HMD と SteamVR のある環境を用意すること 報告には、HMD、GPU、プラットフォーム、モード、日付を添えます。 報告の形は [不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/)にあります。 ## コードを作る・直す ウィジェットなどのコードを足したり、バグを直したりする貢献です。現段階でコアに受け入れるのは、プリミティブ層のウィジェットです。public API の追加・変更や、observable behavior の変更は、着手前に Issue を立てます。 要る条件は次のとおりです。 - 手元でビルドとテストを通せる環境を用意すること 次のページから始めます。 - [開発環境を用意する](/contributor/3-prepare/dev-environment/) - [Flutter から移植する](/contributor/4-write/port-from-flutter/) ## 機能を提案する 作ってほしい機能や、設計の変更の案を出す貢献です。提案は Issue で始まります。public API を追加する場合は、受け入れ条件があります。 書き方は [提案を書く](/contributor/6-propose/write-proposal/)にあります。 ## エージェント向けの skill を書く 繰り返す作業を、エージェントに任せられる形にする貢献です。リポジトリの skill の正本は `.agents/skills/` にあります。 足す・直す手順は [skill を足す・直す](/contributor/4-write/skills/)にあります。 ## 次に読むページ 選んだ種類ごとの行き先は、各節の末尾に書いたとおりです。手が足りない Issue を探すときは、[Issue・PR・マイルストーンの読み方](/contributor/3-prepare/read-github/)を開きます。出す前の規約は [コントリビューターの規約](/contributor/3-prepare/conventions/)にあります。 # 決定の記録(ADR)の一覧 > このページは、「なぜこの形に作ったのか」「Flutter とどこをわざと変えたのか」を調べる人が、記録を探すための入口です。記録の書き方は、このページには置きません。書き方は 決定を ADR に書く にあります。 ## ADR とは何か、台帳とは何か このページは、「なぜこの形に作ったのか」「Flutter とどこをわざと変えたのか」を調べる人が、記録を探すための入口です。記録の書き方は、このページには置きません。書き方は [決定を ADR に書く](/contributor/6-propose/write-adr/) にあります。 FloatSoda には、決定を残す記録が2種類あります。 - ADR: 設計や規約を決めた理由と、選ばなかった案を残す記録 - 台帳: Flutter との差異を項目ごとに残す記録 台帳のファイルは [known-divergences.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/references/known-divergences.md) です。意図した差異の一次情報として扱うのは、`deliberate` の項目だけです。 調べたいことで見分けます。「なぜこう作ったのか」は ADR、「Flutter とどこが違うのか」は台帳です。 ## ステータスと種別の意味 一覧の列で使う言葉を、先に定義します。 ADR のステータスは、次の5つを候補としています。正式に採用するかは未決です。 | ステータス | 意味 | | :-- | :-- | | `proposed` | 提案中。承認されるまで、決定として扱わない | | `accepted` | 承認済み。決定として扱う | | `superseded` | 後の決定に置き換わった | | `rejected` | 却下された | | `deprecated` | 決定は残るが、もう使わない | 一覧の種別は、次の2つです。 | 種別 | 内容 | | :-- | :-- | | 設計判断 | 設計や構成の決定。理由と別案を残す | | Flutter との差異 | Flutter と挙動を変えた決定。台帳のラベルも示す | 台帳のラベルは、次の4つです。意図した差異の一次情報になるのは、`deliberate` だけです。 | ラベル | 意味 | | :-- | :-- | | `deliberate` | 意図した差異。設計判断として確定している。レビューは、これを一次情報として扱う | | `not ported` | 未移植。Issue を立てて追う | | `port mistake` | 実装されているが誤り。直す対象 | | `unlabelled` | まだ判断が下っていない。意図が確定していないので、一次情報として扱わない | ## 決定の一覧 ADR の置き場所と番号の付け方は、決まっていません。一覧は、置き場所と番号が決まってから、1件につき1行で作ります。一覧の列は、番号、題名、種別、ステータス、決定日、関係するページです(追跡: #219)。それまでの Flutter との差異は、次の節の台帳で探します。 該当する決定が見つからない場合は、まだ決定されていないか、記録されていません。決めたい内容があれば、[提案を書く](/contributor/6-propose/write-proposal/) に進みます。 ## Flutter との差異の台帳 Flutter のコードを移植している人、移植されたコードをレビューしている人は、自分が扱うウィジェットに関わる差異がないかを、台帳で探します。層の解説の「Flutter と違うところ」([フレームワーク層](/contributor/2-understand/framework-layer/))からも、台帳の項目を参照しています。 台帳の項目は、次の欄を持ちます。 | 欄 | 内容 | | :-- | :-- | | FloatSoda | FloatSoda の挙動。ソースの場所つき | | Flutter | Flutter の挙動。対応するファイルつき | | Why | 差異が必要な理由 | | Test | 差異を固定するテスト | | Docs | 利用者向けの docs | | Observation | 差異が観測できる場所(`HEADLESS` か `VR`) | | Label | 上の4つのラベル | 意図した差異には、次の5項目の記録が必須です(REVIEW.md の「Flutter parity」)。Flutter の挙動、FloatSoda の挙動、差異が必要な理由、差異を固定するテスト、利用者に影響する場合のドキュメントです。レビューは、台帳の項目の番号(たとえば 2 番)で、項目を参照します。台帳の項目を移すときは、番号を変えません。 台帳に項目がなく、新しく差異を作りたいときは、[Flutter から移植する](/contributor/4-write/port-from-flutter/) と [決定を ADR に書く](/contributor/6-propose/write-adr/) に進みます。 ## 決定を足すとき 新しい決定を記録したいときは、[決定を ADR に書く](/contributor/6-propose/write-adr/) に進みます。 # エンジン層 > エンジン層(FloatSoda.Engine)は、Layer ツリーを GL テクスチャに描き、SteamVR のオーバーレイまたはデスクトップのウィンドウに出す層です。参照するプロジェクトは Abstractions、OVR、Rendering の3つで、フレームワーク(src/FloatSoda)は参照しません。… ## エンジン層は、描画スレッドと GL の描画先を受け持つ エンジン層(`FloatSoda.Engine`)は、Layer ツリーを GL テクスチャに描き、SteamVR のオーバーレイまたはデスクトップのウィンドウに出す層です。参照するプロジェクトは Abstractions、OVR、Rendering の3つで、フレームワーク(`src/FloatSoda`)は参照しません。そのため、フレームワークなしでも組めます。実際に組む手順は [描画が届くまで(4段)](/contributor/2-understand/tutorial-render-path/) の3段目にあります。 型の担当は次のとおりです。 | 型 | 担当 | | :-- | :-- | | `RenderPostTaskRunner` | 描画専用のスレッドを持ち、投入されたタスクを順に実行する | | `GLView` | GLFW のウィンドウ、GL のコンテキスト、Skia の描画先(Surface)と GL テクスチャを持つ | | `Renderer` | Layer ツリーを `GLView` の Surface に描く | | `OverlayWindow`、`DesktopWindow` | 描いた結果の出し先。どちらも `IEngineWindow` の実装 | | `FramePacer` | ループの周期をそろえる | 次の節から、この順に1つずつ説明します。 ## 描画スレッドは、キューのタスクを実行する `RenderPostTaskRunner` は、専用スレッドを1本だけ持ちます。スレッドはバックグラウンドスレッドで、名前は作る側が渡します。フレームワークは `RenderThread` という名前で作ります。`GLView` の説明が、OpenGL リソースの作成・使用・破棄を、リソースを所有するスレッドで行うよう求めています。 - GL を使う処理は、描画スレッドで実行すること ほかのスレッドから GL を使いたいときは、処理を `PostTask(Action)` に渡して、描画スレッドで実行させます。`PostTask` は `ConcurrentQueue` に積むだけなので、どのスレッドからでも呼べます。次の形は、フレームワークが `GLView` と `Renderer` を作る箇所と同じ生成処理だけの抜粋です。使い終わった `Renderer` と `GLView` は `Dispose` で破棄し、その呼び出しも描画スレッドで行います。 ```csharp renderThread.PostTask(() => { var renderer = new Renderer { GLView = new GLView(size) }; // renderer を使う処理は、すべてこの描画スレッドの上で行う }); ``` 描画スレッドのループは、1周ごとに次の順で動きます。 1. `PreUpdate`: キューに積まれたタスクをすべて実行する 2. `Update`: `GLFW.PollEvents` でウィンドウのイベントを処理する 3. `PostUpdate`: 既定では何もしない 4. `FramePacer` で次の周期まで待つ ループに入る前に `GLFW.Init()` を、抜けたあとに `GLFW.Terminate()` を、どちらも描画スレッド上で呼びます。1つのタスクが例外を投げても、ほかのタスクとスレッドは動き続けます。例外はログに記録されます。ここで拾わないと、ループの外まで例外が抜けて `OnStop` が走り、以降の描画がすべて止まるためです。 ほかに、`IOTaskRunner` という専用スレッドもあります。利用者のコード向けで、投入された処理を投入順に1件ずつ実行します。フレームの描画には使いません。 ## GLView は、GL テクスチャを描画先の Surface にする `GLView` は、GL のテクスチャを描画先とする Skia の `SKSurface` を作り、持ち続けます。持ち物は次のとおりです。 - GLFW のウィンドウ - GL のコンテキストと、Skia の `GRContext` - 描画結果を保持する GL の 2D テクスチャ(`TextureHandle`) - テクスチャを描画先とする `SKSurface`(`Surface`) ウィンドウは、既定では画面に表示されません。表示の有無は、コンストラクターの `visible` 引数で指定します。 - 表示の有無は、作成時に決めること(作成後は変更できない) `Surface` は、原点の指定を `GRSurfaceOrigin.BottomLeft` にして作ります。この向きは、最後の節で説明する入力の座標に関わります。 `Resize` は、既存の `Surface` とテクスチャを破棄して作り直します。サイズが同じなら何もしません。幅か高さが 0 以下なら `ArgumentOutOfRangeException` です。作り直すとテクスチャのハンドルが変わるので、ハンドルを使う処理は、`Resize` のあとに `Renderer.GetTextureHandle()` で取り直す必要があります。`OverlayWindow.Present` は、描くたびに取り直しています。 ## Renderer と Present が、Layer を描いて SteamVR に渡す `Renderer.Render(ILayer)` は、次の3段で Layer ツリーを描きます。 1. `GLView.Clear()` で、描画先を透明色でクリアする 2. `LayerRenderer.Render` で、`Layout` のあとに `Paint` を実行する(詳細は [描画層](/contributor/2-understand/rendering-layer/)) 3. `GLView.Flush()` で、Skia と GL に保留されたコマンドを送る 描いた結果を出す入口は、`IEngineWindow`(Abstractions)の `Present(ILayer)` です。`RenderPostTaskRunner.PostRender(window, layer)` は、`window.Present(layer)` を描画スレッドで呼ぶタスクを投入します。呼ぶ側が知っている型は `IEngineWindow` だけなので、ウィンドウの種類が変わっても入口は変わりません。 - `Present` と `Resize` は、描画スレッドで呼ぶこと `OverlayWindow` と `DesktopWindow` は、`Present` の後半が違います。 | 型 | `Present` の後半 | `Resize` | | :-- | :-- | :-- | | `OverlayWindow` | GL テクスチャのハンドルを `Texture_t` に包み、`Overlay.Texture.FromTexture_t` で OpenVR に渡す | テクスチャを作り直し、オーバーレイの幅(ピクセル幅 ÷ `Dpm`)を更新する | | `DesktopWindow` | テクスチャを既定のフレームバッファに転送し、`SwapBuffers` を呼ぶ | テクスチャを作り直し、GLFW のウィンドウサイズを変える | `OverlayWindow` を作るとき、オーバーレイがダッシュボードなら、入力方式をマウスにして `OverlayMouseRawPointerSource` を作ります。ほかのオーバーレイでは、`PointerSource` は `null` です。OpenVR 側の型は [OVR 層](/contributor/2-understand/ovr-layer/) で説明しています。 ## なぜ Layer を複製して描画スレッドに渡すのか メインスレッドは、Layer ツリーの原本を作り直し続けます。`PaintingContext.RepaintCompositedChild` は、再描画のたびに既存の `TransformLayer` を再利用し、`Children` を消してから子を描き直します。`WidgetBinding.DrawFrame` は、この原本を `Clone()` して `PostRender` に渡します。原本を直接渡すと、描画スレッドが読んでいる最中に、メインスレッドが次のフレームで `Children` を消す可能性があるためです。これはコードの構造から読んだ推定で、複製を省いて実際に描画が崩れるかは再現していません。AGENTS.md も、データ競合を避けるために描画スレッドへ渡す前に複製する、と説明しています。 複製しても共有されるものがあります。それが `SKPicture` の参照です。何を共有して何を共有しないかは、[描画層](/contributor/2-understand/rendering-layer/) の「Clone は何を共有し、何を共有しないか」にあります。複製を省く変更を入れるときは、メインスレッドの次のフレームと描画スレッドの `Present` が、同じ Layer を同時に読み書きしないかを確かめます。 ## フレーム間隔と入力源 `FramePacer` は、目標のフレームレート(既定は 60)から1フレームの時間を求め、前回からの経過時間との差だけ待ちます。待ち方は2段です。残りが長いあいだは `Sleep` で CPU を空け、最後の1ミリ秒ほどは `Thread.Yield` で回って待ちます。 ポインターの入力源は2つあります。どちらも `RawPointerEvent` を発火し、フレームワーク側の `PointerController` が受け取ります。 | 型 | 変換元 | イベントを発火するスレッド | | :-- | :-- | :-- | | `OverlayMouseRawPointerSource` | OpenVR のマウスイベント(ダッシュボードのレーザーポインター) | `PollEvents` を呼んだスレッド | | `GLFWRawPointerSource` | GLFW のカーソルとボタンのコールバック | GLFW を動かしている描画スレッド | `OverlayMouseRawPointerSource` は、OpenVR のマウス座標をそのまま使い、y 座標を反転しません。クラスのコメントは、y 座標を反転せずに左上原点の入力として扱う理由を、実測で確認済みと説明しています。この文書の筆者は再現していません。y を反転すると、入力の位置が上下に鏡写しになります。画像は正しいのに押した位置だけが上下逆になる不具合では、y の反転や、`GRSurfaceOrigin` を変える変更を疑ってください。 ポインターの入力が、ヒットテストとジェスチャを経てウィジェットに届くまでは [入力が届くまでを追う](/contributor/2-understand/framework-input/) にあります。1フレーム全体の流れは [1フレームが SteamVR に届くまで](/contributor/2-understand/frame-to-steamvr/) にまとめています。 # 実験: フレームワーク層 > SetState で画面の一部の色を変えたとき、どの再描画境界が描画待ちリストに登録されるかを、ログで確かめる実験です。最終画像の部分再描画を調べるものではありません。フレームワーク層 を読んだあとに進みます。HMD も SteamVR も使いません。 ## 問い: 一部の色を変えると、どこが描き直されるか `SetState` で画面の一部の色を変えたとき、どの再描画境界が描画待ちリストに登録されるかを、ログで確かめる実験です。最終画像の部分再描画を調べるものではありません。[フレームワーク層](/contributor/2-understand/framework-layer/) を読んだあとに進みます。HMD も SteamVR も使いません。 確かめる対象は `RenderPipeline.NodesNeedingPaint` です。次の `FlushPaint` で描き直す RenderObject の待ちリストで、空ならその時点で描き直すものはありません。このリストを、4つのタイミングで出力します。 1. 初回フレームのあと 2. `SetState` の直後 3. `BuildScope` のあと 4. `FlushPaint` のあと 先に予想を立てます。2 と 3 で、リストに何が入るかを考えてください。2つの四角を縦に並べて、片方の色だけを変えます。それぞれを `RepaintBoundary` で包みます。 ## 作例を動かす 作例は [samples/FloatSoda.Samples.ExperimentFramework](https://github.com/sumx21t-3310/FloatSoda/tree/v0.5.0/samples/FloatSoda.Samples.ExperimentFramework) にあります。コードの全文は、このページに載せず、作例のファイルを読みます。作例は、パイプラインを自分で組み、`BuildScope`、`FlushLayout`、`FlushPaint` を順に呼びます。2つの四角を `Column` に並べ、上の四角の色を `SetState` で切り替えます。それぞれの四角は `RepaintBoundary` で包みます。 リポジトリのルートで、次のコマンドを実行します。.NET の SDK のバージョンなど、開発の準備は [開発環境を用意する](/contributor/3-prepare/dev-environment/) にあります。 ```powershell dotnet run --project samples/FloatSoda.Samples.ExperimentFramework ``` **できた?** 「初回フレーム後」「SetState 直後」「BuildScope 後」「FlushPaint 後」の4つの行が出た。
できなかったら SDK が見つからない、またはビルドで例外が出る場合は、[開発環境を用意する](/contributor/3-prepare/dev-environment/) で SDK と clone の状態を確かめます。コンパイルが通らない場合は、公開 API が変わった可能性があります。作例が使っている型(`RenderPipeline`、`BuildOwner`、`RenderObjectToWidgetAdapter`、`StatefulWidget`、`State`)の名前と使い方を、[フレームワーク層](/contributor/2-understand/framework-layer/) と、`src/FloatSoda` の現在のソースで照らします。
## ログを読む 出力は次の4行になります。 ```text 初回フレーム後: NodesNeedingPaint = [] SetState 直後: NodesNeedingPaint = [] BuildScope 後: NodesNeedingPaint = [RenderRepaintBoundary@depth2] FlushPaint 後: NodesNeedingPaint = [] ``` 見る場所は、リストの中身です。 - `SetState` 直後: 空 - `BuildScope` 後: `RenderRepaintBoundary` が1件 - `FlushPaint` 後: 空 `SetState` が行うのは再ビルドの予約までなので、直後は RenderObject がまだ変わらず、リストは空です。`BuildScope` の後に境界が1件入るのは、色を変えた四角の `RenderColoredBox` が `MarkNeedsPaint` を呼び、直近の境界で要求が止まるためです。`@depth2` は木の深さで、`RenderView`(0)、`Column` の `RenderFlex`(1)、境界(2)の順です。 このログで確認できるのは、深さ2の境界が1件だけ登録されたことです。2つの境界は型も深さも同じなので、どちらの境界かはこの出力から区別できません。 **できた?** 「`BuildScope` のあとだけ、深さ2の `RenderRepaintBoundary` が1件入った」と言える。
できなかったら `BuildScope` のあとのリストが空の場合は、作例の中で `Toggle()`、`BuildScope()`、各行を出す処理の呼び出し順を確かめます。`[RenderRepaintBoundary@depth2]` ではなく `[RenderView@depth0]` が出た場合は、実行コマンドに `no-boundary` を付けていないかを確かめます。
## 壊してみる: RepaintBoundary を外す 境界を外すと、結果がどう変わるかを予想してから実行します。引数 `no-boundary` を付けると、作例は四角を `RepaintBoundary` で包まずに、そのまま並べます。 ```powershell dotnet run --project samples/FloatSoda.Samples.ExperimentFramework -- no-boundary ``` **できた?** 「`BuildScope` 後」の行が `[RenderView@depth0]` になった。描き直しの範囲が、境界から根の `RenderView` まで広がっている。
できなかったら 行が `[RenderRepaintBoundary@depth2]` のままなら、引数が渡っていません。`--` の位置と綴りを確かめます。
## なぜそうなるか この例では、`RenderColoredBox.Color` の値が変わると、`MarkNeedsPaint` が呼ばれます。`MarkNeedsPaint` は、自分が再描画境界ならそこで止まって `NodesNeedingPaint` に登録し、そうでなければ親へ伝わります。`RepaintBoundary` があると、色を変えた `RenderColoredBox` から親へ伝わった要求が、最初の境界で止まります。境界がなければ、要求は根の `RenderView` まで伝わります。`RenderView` も再描画境界なので、そこで登録されます。 つまり、描き直す範囲は、変えたノードの祖先をたどって、最初に見つかる境界までです。自分の変更で境界をどこに置くかは、この範囲の広さで考えます。範囲が根まで広がる場所に、頻繁に変わる部品を置かないようにします。伝播の細部は、[フレームワーク層](/contributor/2-understand/framework-layer/) の「レイアウトと描画は、境界までを描き直す」にあります。 ## 次に読むもの - [フレームワーク層](/contributor/2-understand/framework-layer/): ツリーの役割と、差分更新の仕組み - [実験: 描画層](/contributor/2-understand/experiment-rendering/): Layer の合成を png で確かめる - [1フレームが SteamVR に届くまで](/contributor/2-understand/frame-to-steamvr/): 描き直した結果が、SteamVR に届くまで # 実験: OVR 層 > FloatSoda.OVR だけを参照するコンソールアプリで、オーバーレイを出し、レーザーポインターの入力を受けられるかを、HMD の中で確かめる実験です。OVR 層 を読んだあとに進みます。SteamVR と HMD が要ります。 ## 問い: OVR 層だけで、オーバーレイと入力が扱えるか `FloatSoda.OVR` だけを参照するコンソールアプリで、オーバーレイを出し、レーザーポインターの入力を受けられるかを、HMD の中で確かめる実験です。[OVR 層](/contributor/2-understand/ovr-layer/) を読んだあとに進みます。SteamVR と HMD が要ります。 完成形は次のとおりです。 - HMD の 1 m 前に付いたオーバーレイに、画像が出る - レーザーポインターを当てると、コントローラーが振動する - ホバー、クリック、移動のイベントが、コンソールに出る 先に予想を立てます。FloatSoda のほかの層なしで、ここまで動くかを考えてください。 ## 用意するもの SteamVR と HMD を使える状態にします。FloatSoda を clone したフォルダーで、オーバーレイに出す画像 `layer_tree_output.png` を作ります。サンプルの `PaintingSample` を1回動かすと、デスクトップに `layer_tree_output.png` ができます。 ```powershell dotnet run --project samples/FloatSoda.Samples.PaintingSample ``` **できた?** デスクトップに `layer_tree_output.png` ができた。
できなかったら ビルドに失敗する場合は、[開発環境を用意する(コントリビューター)](/contributor/3-prepare/dev-environment/) で SDK と clone の状態を確かめます。HMD を持っていない場合は、この実験を飛ばして [実験: 描画層](/contributor/2-understand/experiment-rendering/) に進みます。
## サンプルを動かす 実験には、[samples/FloatSoda.Samples.PrimitiveOverlay](https://github.com/sumx21t-3310/FloatSoda/tree/v0.5.0/samples/FloatSoda.Samples.PrimitiveOverlay) を使います。プロジェクトが参照するのは `FloatSoda.OVR` だけです。`Program.cs` の流れは次のとおりです。 1. `OVRApplication` を作って、SteamVR に接続し、`Identify()` を呼ぶ 2. `VRSystemEventDispatcher` に、終了を知らせる `VREvent_Quit` の処理を登録する 3. `OverlayIdentity` と `DeviceTrackedOverlay` を作り、位置を `(0, 0, -1)` にする 4. デスクトップの `layer_tree_output.png` を `Texture.FromFile` で読み、`Visibility.Show()` で出す 5. `Input.Method` をマウスにして、`MakeOverlaysInteractiveIfVisible` を有効にする 6. `FocusEnter`、`FocusLeave`、`MouseButtonDown`、`MouseButtonUp`、`MouseMove` の処理を登録する 7. ループでホバー状態の変化の表示と、2つのディスパッチャーの `PollEvents` を行い、各周の終わりに `1000 / 72` ミリ秒待機する 画像のパスは、第1引数で渡せます。省略すると、デスクトップの `layer_tree_output.png` を使います。SteamVR を起動してから、サンプルを動かします。 ```powershell dotnet run --project samples/FloatSoda.Samples.PrimitiveOverlay ``` **できた?** 初期化エラーで終了せず、動き続けている。
できなかったら `Init_HmdNotFound` の `VRInitializeException` で止まる場合は、SteamVR の状態と HMD の接続を確かめます。`NullReferenceException` で止まる場合は、原因が確かめられていません。スタックトレースの全文を添えて、[OVR 層](/contributor/2-understand/ovr-layer/) の「つまずきやすい点」に従って報告します。`OpenVR.IsHmdPresent()` が `True` でも、初期化が `Init_HmdNotFound` で失敗する場合があります。詳細は [OVR 層](/contributor/2-understand/ovr-layer/) の「つまずきやすい点」にあります。デスクトップに png がない場合は、前の手順に戻って `PaintingSample` を動かします。
## HMD の中で操作する ソースの読みでは、オーバーレイは HMD の正面、1 m 先に付きます。実機での見え方は未確認です。`DeviceTrackedOverlay` の追従先は、既定で HMD で、位置は追従先から見た `(0, 0, -1)` です。頭を動かしても、同じ位置に付いてきます。 正面の 1 m 先でオーバーレイを探し、レーザーを当てて、クリックします。 **できた?** 画像が見え、ホバーとクリックで振動した。
できなかったら オーバーレイが見えない場合は、位置(`Transform.Position`)と、追従先のデバイス(`Transform.Target`)を確かめます。ホバーしない場合は、`Input.Method` がマウスになっているか、`MakeOverlaysInteractiveIfVisible` が有効かを確かめます。
## ログと振動を読む コンソールには、イベントに応じて次の行が出ます。 | 出力 | イベント | | :-- | :-- | | `クリック: (x, y) ボタン: Left` | `MouseButtonDown`。座標とボタンを出す | | `クリック離した` | `MouseButtonUp` | | `移動: (x, y)` | `MouseMove`。座標を出す | | `hovered` | ホバーが始まったときに出る | | `unhovered` | ホバーが終わったときに出る | 振動は、`FocusEnter` で 25 ミリ秒、`FocusLeave` で振幅を下げた 25 ミリ秒、`MouseButtonDown` で 25 ミリ秒(周波数 1500、振幅 0.5)に設定されています。 見る点は、クリックした位置の座標と、イベントの順序です。 **できた?** クリックした位置の座標が出た。
できなかったら 座標が出ない場合は、`overlay.EventDispatcher.PollEvents()` がループの中で呼ばれているかを確かめます。イベントは、`PollEvents` を呼んだスレッドで、ハンドラーが呼ばれます。
## なぜそうなるか OVR 層だけで、画像の提出と入力の受信ができます。画像は `Texture.FromFile` が OpenVR にファイルのパスを渡し、OpenVR が画像ファイルを読みます。入力は `VREvent_*` のイベントとして、`PollEvents` を呼んだときに受け取ります。 OVR 層が持たないのは、描画とレイアウトです。描画層が Layer ツリーを絵にし、エンジン層が GL テクスチャとして OpenVR に提出し、フレームワーク層がウィジェットから Layer ツリーを作ります。ポインターの扱いも上の層が足します。エンジン層は、OpenVR のマウスイベントを `OverlayMouseRawPointerSource` で包んで、フレームワークへ渡します。OVR 層の型の詳細は [OVR 層](/contributor/2-understand/ovr-layer/) にあります。 上の層が足すものは、[描画が届くまで(4段)](/contributor/2-understand/tutorial-render-path/) で、層を1つずつ足して確かめられます。 ## 次に読むもの - [OVR 層](/contributor/2-understand/ovr-layer/): OpenVR を包む型と、つまずきやすい点 - [描画が届くまで(4段)](/contributor/2-understand/tutorial-render-path/): 層を足して、描画が届くまでを作る - [実験: 描画層](/contributor/2-understand/experiment-rendering/): HMD なしで確かめられる実験 # 実験: 描画層 > 同じ絵を OpacityLayer、ClipRectLayer、TransformLayer で1つずつ包み、png を並べて見比べる実験です。描画層 の Layer の種類の表を読んだあとに進みます。HMD も SteamVR も使いません。 ## 問い: 合成の Layer は、同じ絵に何をするか 同じ絵を `OpacityLayer`、`ClipRectLayer`、`TransformLayer` で1つずつ包み、png を並べて見比べる実験です。[描画層](/contributor/2-understand/rendering-layer/) の Layer の種類の表を読んだあとに進みます。HMD も SteamVR も使いません。 絵は、青い矩形と橙の矩形が重なった1枚です。包まない絵と合わせて、png が4枚できます。 | ファイル | 包み方 | | :-- | :-- | | `plain.png` | 包まない | | `opacity.png` | `OpacityLayer`(`Alpha` は 128) | | `clip.png` | `ClipRectLayer`(左上の 200×150 だけを残す) | | `transform.png` | `TransformLayer`(中心まわりに 15 度回す) | 先に予想を立てます。3つの Layer が、それぞれ絵をどう変えるかを考えてください。特に、青と橙が重なる部分が、`OpacityLayer` でどう見えるかを予想します。 ## 4通りに包んで png に出す 作例は [samples/FloatSoda.Samples.ExperimentRendering](https://github.com/sumx21t-3310/FloatSoda/tree/v0.5.0/samples/FloatSoda.Samples.ExperimentRendering) にあります。コードの全文は、このページに載せず、作例のファイルを読みます。作例は、同じ絵を4通りに包み、`LayerBitmapRenderer.Render` で `SKBitmap` にして、png で保存します。あわせて、3つの点の色を出力します。 Layer ツリーを組んで png にする基本は、[samples/FloatSoda.Samples.PaintingSample](https://github.com/sumx21t-3310/FloatSoda/tree/v0.5.0/samples/FloatSoda.Samples.PaintingSample) のコードで読めます。この実験は、その流れに、絵を包む Layer を1枚足したものです。 リポジトリのルートで、次のコマンドを実行します。.NET の SDK のバージョンなど、開発の準備は [開発環境を用意する](/contributor/3-prepare/dev-environment/) にあります。 ```powershell dotnet run --project samples/FloatSoda.Samples.ExperimentRendering -- <出力先フォルダー> ``` 出力先フォルダーは省略できます。省略すると、カレントディレクトリに png を保存します。 **できた?** `plain.png`、`opacity.png`、`clip.png`、`transform.png` の4枚ができた。最後の行に出る保存先のフォルダーに、4枚がある。
できなかったら SDK が見つからない、またはビルドで例外が出る場合は、[開発環境を用意する](/contributor/3-prepare/dev-environment/) で SDK と clone の状態を確かめます。png が空になる、サイズが 0 になる場合は、作例の中で `LayerBitmapRenderer.Render` に渡した大きさと、ルートに子を入れたかを確かめます。仕組みは [描画層](/contributor/2-understand/rendering-layer/) の「CPU の Bitmap に描く」にあります。
## png を見比べる 画像ビューアーで4枚を並べて開き、違いを言葉にします。背景は透明なので、ビューアーによって白や黒に見えます。 見る点は3つです。 - 青と橙が重なる部分の濃さ - 左上の 200×150 より外に、絵が残っているか - 絵全体が傾いているか コンソールの出力は、4枚の点の色を数値で示します(このあとに、保存先の行が続きます)。`#80ffa500` は、先頭の 2 桁(`80`)が不透明度で、残りが色です。 ```text plain.png: 青(50,50)=#ff6495ed 橙(300,230)=#ffffa500 重なり(150,120)=#ffffa500 opacity.png: 青(50,50)=#806495ed 橙(300,230)=#80ffa500 重なり(150,120)=#80ffa500 clip.png: 青(50,50)=#ff6495ed 橙(300,230)=#00000000 重なり(150,120)=#ffffa500 transform.png: 青(50,50)=#ff6495ed 橙(300,230)=#ffffa500 重なり(150,120)=#ffffa500 ``` `opacity.png` は、3つの点がどれも不透明度 `80`(10進で 128)です。重なりの点も `80` のままで、重なると濃くなる、ということはありません。`clip.png` は、橙の点(300,230)が透明(`00000000`)になり、(50,50)と(150,120)は残ります。`transform.png` は、3つの点の値が `plain.png` と同じです。回転は、点の値では分かりません。画像を開いて、絵全体が回っていることを目で確かめます。 **できた?** 「まとめた絵に不透明度を掛けると、この例の重なりは濃くならない」「指定した矩形の外は描かれない」「指定した回転が絵全体に掛かる」と言える。
できなかったら `opacity.png` の重なりの不透明度が `80` より大きい場合は、作例の中で `OpacityLayer` が包んでいる対象が、2つの矩形をまとめた `PictureLayer` になっているかを確かめます。矩形ごとに別の `PictureLayer` にして、それぞれを `OpacityLayer` で包むと、重なりは濃くなります。
## なぜそうなるか `OpacityLayer` は、`SaveLayer` で子を一時レイヤーにまとめて描きます。`Restore` のときに、`Alpha` を掛けて合成します。2つの矩形は、先に1枚の絵として描かれるので、重なりの部分は橙で上書きされたままになります。そのあとで、絵全体が半透明になります。クリップと変換は、`Save` と `Restore` の組で子を包みます。クリップは `ClipRect` で描画範囲を絞り、変換は `Concat` で座標系を変えてから、子を描きます。どちらも `Restore` で状態を戻すので、後ろに並ぶ Layer へは影響しません。 自分で Layer を足すときは、`Paint` が何を `Save` して何を `Restore` するかを先に決めます。詳細は、[描画層](/contributor/2-understand/rendering-layer/) の「Layer の種類と、それぞれの役割」にあります。 ## 次に読むもの - [描画層](/contributor/2-understand/rendering-layer/): Layer の種類と、`Clone` の共有範囲 - [描画が届くまで(4段)](/contributor/2-understand/tutorial-render-path/): 2段目で、自分で組んだ Layer ツリーを png に出す - [実験: フレームワーク層](/contributor/2-understand/experiment-framework/): `SetState` のあとに描き直される範囲を確かめる # 1フレームが SteamVR に届くまで > 1フレームの描画は、2つのスレッドの仕事です。メインスレッドが、ウィジェットのビルドから Layer ツリーの複製までを行い、描画スレッドが、複製を GL で描いて SteamVR のオーバーレイに提出します。変更したい処理がどの層のどの段にあるかは、この流れで見つけます。 ## 1フレームを担う2つのスレッド 1フレームの描画は、2つのスレッドの仕事です。メインスレッドが、ウィジェットのビルドから Layer ツリーの複製までを行い、描画スレッドが、複製を GL で描いて SteamVR のオーバーレイに提出します。変更したい処理がどの層のどの段にあるかは、この流れで見つけます。 | スレッド | 名前 | 仕事 | | :-- | :-- | :-- | | メインスレッド | `FloatSodaMainThread`(Windows では STA) | OpenVR のイベント、入力、ビルド、レイアウト、描画命令の記録(Layer ツリーの更新)、Layer の複製 | | 描画スレッド | `RenderThread` | GL のコンテキストを持ち、Layer ツリーをテクスチャに描いてオーバーレイに渡す | ほかに I/O スレッド(`IOTaskRunner`)があります。利用者のコード向けに、処理を投入順に1件ずつ実行するスレッドで、フレームの流れには入りません。 ```text メインスレッド 描画スレッド タスクの実行 OpenVR のイベント、アクション入力 BeginFrame 入力の処理 ビルド → レイアウト → 描画命令の記録 Layer の複製 ── PostRender ──────────────▶ PreUpdate: 投入されたタスクの実行 待機 (Present: Renderer.Render → OpenVR へ提出) Update: GLFW のイベント処理 待機 ``` 層は、プロジェクトごとに分かれ、参照の向きは csproj が決めます。 - `FloatSoda.OVR`: ほかの FloatSoda のプロジェクトを参照しない - `FloatSoda.Rendering`: ほかの FloatSoda のプロジェクトを参照しない - `FloatSoda.Abstractions`: `FloatSoda.Rendering` を参照する - `FloatSoda.Engine`: Abstractions、OVR、Rendering を参照する - `FloatSoda`(フレームワーク): Abstractions、Engine、Rendering を参照する - `FloatSoda.UI`: `FloatSoda` を参照する - `FloatSoda.UI.Cream`、`FloatSoda.UI.FizzyPop`: `FloatSoda.UI` を参照する メインループと描画ループの周期は、どちらも `FloatSodaOptions.TargetFrameRate`(既定は 60)から作った `FramePacer` が決めます。 ## メインスレッド: ビルドから Layer の複製まで メインループは、1周ごとに次の順で動きます。 1. 保留中のタスクを実行する(`CreateWindow` や、ホットリロードの再ビルドの予約) 2. OpenVR のシステムイベントをポーリングし、アクション入力を更新する 3. ウィンドウごとの `WidgetBinding.BeginFrame` を呼ぶ 4. `FramePacer.WaitForNextFrame` で次の周期まで待つ `BeginFrame` は、ポインターイベントの処理、登録済みのフレームコールバックの実行、`DrawFrame` の順に動きます。`DrawFrame` の中の順序は、次のとおりです。各段の詳細は [フレームワーク層](/contributor/2-understand/framework-layer/) にあります。 | 段 | 内容 | | :-- | :-- | | ビルド | `BuildOwner.BuildScope` が、dirty な Element を再構築し、RenderObject のプロパティを更新する | | レイアウト | `RenderPipeline.FlushLayout` が、dirty なノードを `Depth` の昇順で再レイアウトする。続けて、ホバー状態を更新し、サイズが変わっていればリサイズを描画スレッドに投入する | | 描画命令の記録 | `RenderPipeline.FlushPaint` が、dirty なノードを `Depth` の昇順で再描画し、描画命令を記録して Layer ツリーを更新する。最終画像を描くのは描画スレッド | | 複製と投入 | `RenderView.Layer` を `Clone()` し、`RenderPostTaskRunner.PostRender` で描画スレッドに渡す | すべてのフレームで、レイアウトと描画が走るわけではありません。ビルドは毎フレーム確かめますが、レイアウトからあとは、`NeedsVisualUpdate` が立っているフレームだけです。ウィンドウがまだ作られていないフレームも、レイアウトからあとは飛ばします。 ## 描画スレッド: Layer を描いて SteamVR に渡す 描画スレッドは `PostTaskRunner` のループで動き、1周ごとに、投入されたタスクの実行、`GLFW.PollEvents`、待機の順に進みます。`PostRender` が投入するタスクは、`window.Present(layer)` を呼びます。 `OverlayWindow.Present` は、`Renderer.Render(layer)` で Layer ツリーを GL テクスチャに描きます。`Render` は、`GLView.Clear()`、`LayerRenderer.Render`(`Layout` のあとに `Paint`)、`GLView.Flush()` の順に動きます。そのあと、GL テクスチャのハンドルを `Texture_t` に包み、`Overlay.Texture.FromTexture_t` で OpenVR に渡します。GL テクスチャが OpenVR に渡るのは、この呼び出しです。デスクトップウィンドウ(`DesktopWindow`)は、同じ `Present` の流れを、テクスチャを既定のフレームバッファに転送して、`SwapBuffers` で終えます。 ウィンドウの作成にも、2つのスレッドが関わります。`FloatSodaApp.CreateWindow` は、作成の処理をメインスレッドのタスクとして積みます。`WidgetBinding.EnsureInitialized` は、`GLView` と `Renderer` と、エンジン層のウィンドウの作成を、描画スレッドのタスクとして投入します。そのため、ウィンドウは描画スレッドで非同期に作られ、メインスレッドから見えるのは、作成が終わったフレームからです。 エンジン層の詳細は [エンジン層](/contributor/2-understand/engine-layer/)、Layer の描画は [描画層](/contributor/2-understand/rendering-layer/)、OpenVR の型は [OVR 層](/contributor/2-understand/ovr-layer/) にあります。 ## なぜ Layer を複製して渡すのか 複製は、描画中の Layer を、次のフレームの変更から守るためにあります。メインスレッドは、再描画のたびに、同じ Layer のインスタンスの子を消して作り直します。描画スレッドが原本を読んでいる最中に、次のフレームの描画が始まると、読んでいる Layer が書き換わる可能性があります。これはコードの構造から読んだ推定で、複製を省いて実際に描画が崩れるかは再現していません。そこで、描画スレッドには、複製したツリーを渡します。AGENTS.md も、描画スレッドへ渡す前に複製して、データ競合を避けると説明しています。 複製が共有するのは、`SKPicture` のような描画リソースの参照です。ツリーの構造は共有しません。共有の範囲は [描画層](/contributor/2-understand/rendering-layer/) の「Clone は何を共有し、何を共有しないか」にあります。複製を省く変更は、メインスレッドの次のフレームと、描画スレッドの `Present` が、同じ Layer を同時に読み書きしないかを確かめます。 ## リサイズは同じ流れの別の枝 ウィンドウの大きさは、既定ではウィジェットのレイアウト結果が決めます。`DrawFrame` は、レイアウトのあとに、サイズが前回投入した値と違うかを調べます。違えば、`PostTask` で `window.Resize(newSize)` を描画スレッドに投げます。この投入は、描画の投入より先なので、同じキューの順で、リサイズのあとに描画が走ります。 `OverlayWindow.Resize` は、`GLView` の描画先 `Surface` と GL テクスチャを作り直して、オーバーレイの幅(ピクセル幅 ÷ `Dpm`)を更新します。入力を受けるオーバーレイでは、マウスのスケールもピクセルサイズに合わせます。サイズを変える処理は、描画スレッドで走ります。 ## 入力は逆向きに流れる ダッシュボードのオーバーレイでは、コントローラーのレーザーが、次の順でボタンに届きます。 1. OpenVR のマウスイベントを、`OverlayWindow.PollInputEvents` がポーリングする 2. `OverlayMouseRawPointerSource` が、`RawPointerEvent` に変換して発火する 3. `PointerController` が、キューに積む。`Flush` で取り出し、`PointerEvent` に変換する 4. `WidgetBinding.HandlePointerEvent` が、ヒットテストを行う 5. ヒットパスの末尾で、ジェスチャ認識器にイベントが届く 1 の `PollInputEvents` も、3 の `Flush` も、メインスレッドで `BeginFrame` から呼ばれます。つまり、これらの段は、すべてメインスレッドで動きます。デスクトップウィンドウでは、GLFW のコールバックが描画スレッドで `RawPointerEvent` を発火しますが、キューに積むだけで、変換とそれ以降はメインスレッドの `Flush` が行います。 フレームワークの標準のポインター入力が接続されるのは、オーバーレイのうち、ダッシュボードだけです。ほかのオーバーレイでは、`PointerSource` が `null` です。コントローラーのレイで当たりを判定する `ControllerPointerSystem` は、どこからも呼ばれていません(追跡: [#182](https://github.com/sumx21t-3310/FloatSoda/issues/182))。 コントローラーのボタンを読むアクション入力は、別の流れです。メインループが、1周ごとに `VRInputUpdater.Update` で更新します。 ウィジェット側の入力は [入力が届くまでを追う](/contributor/2-understand/framework-input/)、OpenVR 側は [OVR 層](/contributor/2-understand/ovr-layer/) にあります。 ## 各層の解説へ 変更したい層の解説に進みます。 - [フレームワーク層](/contributor/2-understand/framework-layer/): Widget、Element、RenderObject と、差分更新 - [入力が届くまでを追う](/contributor/2-understand/framework-input/): ヒットテスト、ジェスチャ、ポインター - [描画層](/contributor/2-understand/rendering-layer/): Layer ツリーと、`Clone` の共有範囲 - [エンジン層](/contributor/2-understand/engine-layer/): 描画スレッド、GL の描画先、ウィンドウ - [OVR 層](/contributor/2-understand/ovr-layer/): OpenVR の包み方と、つまずきやすい点 - [UI 層](/contributor/2-understand/ui-layer/): UI を3層に分けた理由 手を動かして確かめるときは、[描画が届くまで(4段)](/contributor/2-understand/tutorial-render-path/) と、各層の実験([フレームワーク層](/contributor/2-understand/experiment-framework/)、[描画層](/contributor/2-understand/experiment-rendering/)、[OVR 層](/contributor/2-understand/experiment-ovr/))を使います。 # 入力が届くまでを追う > 入力は、フレームの先頭でウィジェットに届きます。WidgetBinding.BeginFrame は、DrawFrame の前に FlushPointerEvents を呼びます。流れは次のとおりです。 ## 入力は、ヒットテストからジェスチャへ流れる 入力は、フレームの先頭でウィジェットに届きます。`WidgetBinding.BeginFrame` は、`DrawFrame` の前に `FlushPointerEvents` を呼びます。流れは次のとおりです。 1. `OverlayWindow.PollInputEvents` が、オーバーレイ宛の OpenVR のイベントをポーリングする。入力源が `RawPointerEvent` を発火し、`PointerController` のキューに積む。デスクトップウィンドウでは、GLFW のコールバックが描画スレッドで同じキューに積む 2. `PointerController.Flush` が、キューを取り出して `PointerEvent` に変換する。変換は `Flush` を呼んだスレッド(メインスレッド)で行う 3. `HandlePointerEvent` が、`RenderView.HitTest` でヒットしたノードのパス(ヒットパス)を求める。パスの末尾に `WidgetBinding` 自身を加える 4. ヒットパスの各ノードへイベントを配る。押下中は、押した時点のヒットパスへ、移動と離す操作を配り続ける 5. パスの末尾の `WidgetBinding.HandleEvent` が、`PointerRouter` でジェスチャ認識器へイベントを届ける。押下では `GestureArena` の参加受付を締め切り、離す操作では未確定のアリーナの既定勝者を確定する 認識器は `src/FloatSoda/Gesture/` にあります。ウィジェットとして使うのは `GestureDetector` や `Listener` です。 オーバーレイのうち、ポインターが届くのはダッシュボードだけです。エンジン層の `OverlayWindow` が入力源を作るのが、ダッシュボードのときだけだからです。ほかのオーバーレイへの接続は、Issue で追跡しています(追跡: [#182](https://github.com/sumx21t-3310/FloatSoda/issues/182))。 OpenVR のイベントがポインター入力になるまでは、[エンジン層](/contributor/2-understand/engine-layer/) と [OVR 層](/contributor/2-understand/ovr-layer/) の担当です。ウィジェット側の仕事は、`PointerEvent` を受け取るところから始まります。 ## 関連するページ - [フレームワーク層](/contributor/2-understand/framework-layer/): Widget、Element、RenderObject と、差分更新 - [エンジン層](/contributor/2-understand/engine-layer/): ポインターの入力源と、y 座標の向き - [1フレームが SteamVR に届くまで](/contributor/2-understand/frame-to-steamvr/): 入力が逆向きに流れる段の全体 # フレームワーク層 > フレームワーク層(src/FloatSoda)は、Flutter の三つのツリー(Widget、Element、RenderObject)を写し、描画の結果として Layer ツリーを作ります。どの変更をどのツリーに入れるかは、次の役割で見分けます。 ## フレームワーク層が持つ4つのツリー フレームワーク層(`src/FloatSoda`)は、Flutter の三つのツリー(Widget、Element、RenderObject)を写し、描画の結果として Layer ツリーを作ります。どの変更をどのツリーに入れるかは、次の役割で見分けます。 | ツリー | 役割 | 置き場所 | | :-- | :-- | :-- | | Widget | UI の設計図。不変の `record` | `src/FloatSoda/Widgets/` | | Element | Widget と RenderObject の橋渡し。可変で、ビルドの状態を持つ | `src/FloatSoda/Elements/` | | RenderObject | レイアウトと描画の記録。サイズを決め、描画命令を記録する | `src/FloatSoda/RenderObjects/` | | Layer | 合成の結果。描画層が持つ(→ [描画層](/contributor/2-understand/rendering-layer/)) | `src/FloatSoda.Rendering/Layers/` | 流れは次のとおりです。Widget から Element を作り、RenderObject を持つウィジェットの Element が RenderObject を作って更新し、RenderObject の描画が Layer ツリーになります。 ```text Widget ──CreateElement──▶ Element ──作成・更新──▶ RenderObject ──Paint──▶ Layer ``` ウィンドウごとに1つの `WidgetBinding` があり、このツリーの更新を進めます。`WidgetBinding` は、`BuildOwner`(再ビルドの予約)、`RenderPipeline`(レイアウトと描画の予約)、ジェスチャの調停の部品を持ちます。 ## Widget から Element を作り、更新する `Widget` は不変の `abstract record` で、`CreateElement()` と任意の `Key` を持ちます。ビルドで新しい Widget が来るたびに、`Element.UpdateChild` が、既存の子 Element をどう扱うかを次の順で決めます。 1. 新しい Widget が `null` なら、子を取り除く 2. 子 Element がまだなければ、Widget から Element を作る 3. 古い Widget と新しい Widget が `record` として等しければ、何もせずに終える 4. `Widget.CanUpdate` が `true` なら、子 Element を再利用して `Update` する 5. それ以外は、古い子を外して、新しい Element を作り直す `CanUpdate` が `true` になる条件は、実行時の型が同じで、`Key` が等しいことです。つまり、自分の書いたウィジェットが、同じ位置で同じ型のまま `Key` も変わらなければ、Element は再利用されます。`StatefulWidget` の `State` も、その Element の中で保たれます。型か `Key` が変わると、作り直しになり、`State` は破棄されます。 ## 変更が dirty として登録され、ビルドされる `State.SetState` は、渡された処理を実行してから `Element.MarkNeedsBuild()` を呼びます。`MarkNeedsBuild` は、Element を dirty にして、`BuildOwner` の待ちリストに登録します。登録されたことは、`WidgetBinding.EnsureVisualUpdate` で「次のフレームで視覚の更新が要る」と記録されます。 `BuildOwner.BuildScope` は、`DrawFrame` のたびに呼ばれ、待ちリストの Element を `Depth` の昇順で再ビルドします。親が先に再ビルドされます。再ビルドの途中で増えた dirty も、同じ呼び出しの中で処理されます。つまり、`SetState` を呼んだあとの最初の `DrawFrame` で、再ビルドが走ります。 ## レイアウトと描画は、境界までを描き直す 再ビルドは、`UpdateRenderObject` で RenderObject のプロパティを書き換えます。プロパティの設定が値の変化を検知すると、レイアウトか描画の再実行を要求します。たとえば `RenderColoredBox.Color` は、値が変わったときだけ `MarkNeedsPaint()` を呼びます。 ```csharp public Color Color { get; set { if (field == value) return; field = value; MarkNeedsPaint(); } } = new(0, 0, 0); ``` 再実行の要求は、境界で止まります。 | 呼び出し | 伝わり方 | 登録先 | | :-- | :-- | :-- | | `MarkNeedsLayout` | 最も近い再レイアウト境界まで、親へ伝わる | `RenderPipeline.NodesNeedingLayout` | | `MarkNeedsPaint` | 自分が再描画境界なら、そこで止まる。そうでなければ親へ伝わる | `RenderPipeline.NodesNeedingPaint` | 登録されるのは、伝わった先の境界のノードです。どちらも、登録と同時に `RequestVisualUpdate` で視覚の更新を予約します。 視覚の更新が必要なフレーム(`NeedsVisualUpdate` が立っているフレーム)では、`RenderPipeline` が2段で登録済みのノードを処理します。 1. `FlushLayout`: リストが空になるまで、登録されたノードを `Depth` の昇順で再レイアウトする 2. `FlushPaint`: 開始時点のリストを取り出して空にし、取り出したノードを `Depth` の昇順で再描画する `FlushLayout` は、処理中に増えたノードも同じ呼び出しで処理します。`FlushPaint` の処理中に増えたノードは、次のフレームで処理します。 再描画では、`PaintingContext.RepaintCompositedChild` が、境界ノードの `Layer`(`TransformLayer`)を再利用します。子の Layer を消してから、その境界から下だけを描き直します。`RepaintBoundary` ウィジェットは、`IsRepaintBoundary` が真の `RenderRepaintBoundary` を作ります。`RenderView` も再描画境界です。そのため、`RepaintBoundary` を足すと、描き直しの範囲が、その境界の中に狭まります。 値が変わっていないのに `MarkNeedsPaint` や `MarkNeedsLayout` を呼ぶと、無駄な描き直しになります。値が変わったときだけ dirty になるよう、プロパティの setter が値を比べます。`UpdateRenderObject` から値を設定するときは、この setter を通すこと(→ [REVIEW.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/REVIEW.md) の「差分更新」)。 `SetState` のあとに、どのノードが `NodesNeedingPaint` に入るかは、[実験: フレームワーク層](/contributor/2-understand/experiment-framework/) で、ログで確かめられます。 ## 入力の流れは別のページにある ポインター入力が、ヒットテストとジェスチャ認識器を経てウィジェットに届くまでは、[入力が届くまでを追う](/contributor/2-understand/framework-input/) に書いています。 ## Flutter と違うところ Flutter のコードを移植するときは、次の3つの前提を持ち込めません。 | 違い | 内容 | 台帳または記録 | | :-- | :-- | :-- | | 非活性の Element のプールがない | `Deactivate` は終端で、unmount と同じ扱いになる。Flutter が同一フレーム内で再活性化できる Element も再利用せず、`State` を破棄する。親をまたぐ移動に使う `GlobalKey` は未実装 | 台帳の 2 番と 3 番。[REVIEW.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/REVIEW.md) の「ツリーの所有権とライフサイクル」 | | Semantics を移植しない | スクリーンリーダー向けの情報の仕組みを持たない | [REVIEW.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/REVIEW.md) の「Flutter parity」、[制限事項](/reference/limitations/) | | Widget が `record` で、値が同じなら更新を飛ばす | 同じ位置で型と `Key` が同じ Widget を、Flutter は同一性で比べる。そのため新しいインスタンスは `didUpdateWidget` に届く。FloatSoda は値で比べ、同じ値なら `Update` が走らず、子も再ビルドされない | 台帳の 1 番 | 台帳は、Flutter との差異を項目ごとに残すファイルです([known-divergences.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/references/known-divergences.md))。各項目のラベルと、決定の記録の一覧は [決定の記録(ADR)の一覧](/contributor/2-understand/adr-index/) にあります。移植するウィジェットに関わる差異があるかは、移植の前に台帳で探します。 ## 実験で確かめる ここまでの内容は、[実験: フレームワーク層](/contributor/2-understand/experiment-framework/) で動かして確かめられます。ほかの層は、[描画層](/contributor/2-understand/rendering-layer/) と [エンジン層](/contributor/2-understand/engine-layer/) に続きます。1フレームの流れ全体は [1フレームが SteamVR に届くまで](/contributor/2-understand/frame-to-steamvr/) にあります。 # OVR 層 > OVR 層(FloatSoda.OVR)は、OpenVR の初期化、オーバーレイ、イベント、アクション入力を、C# の型で包む層です。ほかの FloatSoda のプロジェクトを参照せず、ネイティブの openvrapi.dll を同梱します。上の層(エンジン層、フレームワーク層)は、この層の型を通して OpenVR… ## OVR 層は、OpenVR の生のバインディングを包む OVR 層(`FloatSoda.OVR`)は、OpenVR の初期化、オーバーレイ、イベント、アクション入力を、C# の型で包む層です。ほかの FloatSoda のプロジェクトを参照せず、ネイティブの `openvr_api.dll` を同梱します。上の層(エンジン層、フレームワーク層)は、この層の型を通して OpenVR を使います。 ```text openvr_api.cs、openvr_api.dll OpenVR の生のバインディングと、ネイティブライブラリ ▲ OVRApplication、オーバーレイの型、イベントディスパッチャー、InputAction C# の型で包んだ層 ▲ エンジン層、フレームワーク層 ``` 直したい箇所は、この図で探します。OpenVR の呼び出し自体を直したいときは、2段目の型を見ます。生のバインディングは、次の節のとおり、直さずに読みます。 ## OpenVR の資料の読み方 OpenVR の挙動は、FloatSoda のコードからは変えられません。調べるときの入口は、OpenVR の Wiki()です。C# の呼び出し署名は、同梱の `src/FloatSoda.OVR/openvr_api.cs` で確認します。挙動が資料と食い違うときは、資料とバインディングと実行環境のバージョンを記録し、食い違いを未解決として報告します。 `openvr_api.cs` は、Valve が自動生成したバインディングです。ファイルの冒頭に、編集しないよう書かれています。名前空間は `FloatSoda.OVR` です。 資料を読むときは、次の順に進めます。 1. 調べたい API を `openvr_api.cs` で探す 2. 同じ名前の項目を Wiki で探して、説明を読む 3. 署名に食い違いがあれば、`openvr_api.cs` の署名に従う 入力方式を例にすると、該当するのは `OpenVR.Overlay.SetOverlayInputMethod` です。FloatSoda では `OverlayInput.Method` が包んでいます。 ## アプリケーションとオーバーレイ `OVRApplication` は、作ると `OpenVR.Init` を呼びます。失敗は例外になります(`VRInitializeException` など)。種類を決める `OVRAppInfo` の `Type` の既定は `Overlay` です。`Dispose` で、`OpenVR.Shutdown` を呼びます。 `AppKey` は、SteamVR がアプリケーションを識別するキーです。型の説明が求める形は、ASCII の小文字、数字、ドット、アンダースコアだけで、127 文字までです。この規則は、`Value` の `init` アクセサーが検証します。つまり、`with` 式や初期化子で `Value` を設定すると、大文字を含む値は `FormatException`、128 文字以上は `ArgumentOutOfRangeException` になります。一方で、`new AppKey("...")` のコンストラクターは、検証を通りません。コンストラクターに渡す値は、規則に合う値にすること。 オーバーレイは、`IOverlay` の型で表します。種類は3つです。 | 型 | 位置の決まり方 | 追加で持つもの | | :-- | :-- | :-- | | `DashboardOverlay` | SteamVR のダッシュボードが管理する。位置を持たない | `Thumbnail`(ダッシュボードのサムネイル) | | `DeviceTrackedOverlay` | 追跡デバイスからの相対位置。既定の追従先は HMD | `Visibility`、`Intersection`、`Transform`(`Target` で追従先を選ぶ) | | `WorldSpaceOverlay` | 追跡空間の絶対位置。`Origin` の既定は立った状態の空間 | `Visibility`、`Intersection`、`Transform` | どの種類も、機能を小さな部品に分けて持ちます。`Opacity`、`WidthInMeters`、`Curvature`、`Texture`、`State`、`Vibration`、`Input`、`EventDispatcher` です。位置を動かせる2種類は、さらに `Visibility`、`Intersection`、`Transform` を持ちます。 `OverlayIdentity` は、作るときに `OpenVR.Overlay.CreateOverlay` を呼び、`Dispose` で `DestroyOverlay` を呼びます。`OverlayTexture` は、絵を2通りで設定します。 | 呼び出し | 渡すもの | 使いどころ | | :-- | :-- | :-- | | `FromFile` | 画像ファイルのパス。OpenVR がファイルを読む | 画像を出すだけのとき | | `FromTexture_t` | OpenVR のテクスチャ記述(`Texture_t`)。GL テクスチャのハンドルなどを包む | 描画した結果を出すとき(エンジン層がこちらを使う) | どの型で作るかは、自分のオーバーレイが、ダッシュボードのタブか、頭やコントローラーに付くものか、空間に固定するものかで選びます。 ## イベントは、ポーリングしたスレッドで呼ばれる OpenVR のイベントは、`VREventDispatcher` に `Register` した処理が、`PollEvents` を呼んだスレッドで呼ばれます。`PollEvents` を呼ぶまで、ハンドラーは動きません。 ディスパッチャーは、見る先が違う2つがあります。 | 型 | ポーリング先 | 受け取るイベントの例 | | :-- | :-- | :-- | | `VRSystemEventDispatcher` | プロセス全体のシステムのイベントの待ち行列 | `VREvent_Quit`、デバイスの接続 | | `OverlayEventDispatcher` | そのオーバーレイ宛のイベントの待ち行列 | `VREvent_FocusEnter`、`VREvent_MouseMove`、`VREvent_MouseButtonDown` | イベントを配るとき、ハンドラーの一覧は複製されます。そのため、ハンドラーの中で `Register` や `Unregister` を呼べます。 ハンドラーの中で触る状態が、どのスレッドのものかを確かめます。FloatSoda は、システムのイベントも、入力を持つオーバーレイのイベントも、メインスレッドで `PollEvents` を呼びます。ハンドラーはメインスレッドで動くので、描画スレッドの状態に触れる処理は、描画スレッドへ渡します(→ [エンジン層](/contributor/2-understand/engine-layer/))。 ## アクション入力とマニフェスト コントローラーのボタンは、アクション入力で読みます。`InputAction` が1つの入力を表し、`T` に使えるのは `bool`、`float`、`System.Numerics.Vector2` だけです。ほかの型は `NotSupportedException` で拒否されます。`InputActionMap` が複数のアクションを束ね、`Enabled` でまとめて有効にしたり無効にしたりします。 `VRInputUpdater` が、マニフェストを SteamVR に登録し、毎周 `Update` でアクションの値とイベント(`OnPerformed`、`OnReleased`)を更新します。入力の定義は C# のコードだけで書きます。マニフェストの JSON は、FloatSoda が自動で作ります。 ## 2種類のマニフェストを比べる SteamVR にはマニフェストが2種類あり、FloatSoda での扱いが違います。アクションマニフェストは自動で作られますが、アプリケーションマニフェストは作られません。 | 観点 | アクションマニフェスト | アプリケーションマニフェスト | | :-- | :-- | :-- | | 何のためか | 入力アクションの名前、型、既定の割り当てを SteamVR に伝える | プロセスを `app_key` に結びつけ、`AutoLaunch` やシーン遷移などの連携を使えるようにする | | だれが作るか | FloatSoda が起動のたびに自動で作る。入力の定義が1つ以上あるときだけ | 利用者。FloatSoda は作らない | | どこに置くか | 一時ディレクトリ配下の `%TEMP%/FloatSoda/{AppKey}/input` | 利用者が決める絶対パス | | いつ登録するか | 起動時に `VRInputUpdater` が `IVRInput.SetActionManifestPath` で登録する | `OVRApplications.AddManifest(path, temporal)` を呼んだとき | | 使えないと何が起きるか | 警告にして続行する。オーバーレイの表示は続き、アクション入力だけ使えない | `OVRApplication.Identify()` が `UnknownApplication` で失敗し、警告にして続行する。マニフェスト連携の機能だけ動かない | アプリケーションマニフェストは、`OVRApplication.Identify()` がプロセスと `app_key` を結びつけるときに要ります。フレームワークの `FloatSodaApp` は起動時に `Identify()` を呼びますが、AppKey が登録されていなくても止まりません。OVR 層を単独で使う場合は、`Identify()` の呼び出しと失敗時の処理を、呼び出し側が決めます。`AutoLaunch` やシーン遷移を使うアプリは、`AddManifest` で登録します。 足りない側は、症状で見分けます。入力が動かなければアクションマニフェストを、`AutoLaunch` などが動かなければアプリケーションマニフェストを確かめます。 ## つまずきやすい点 OpenVR まわりで詰まりやすい点を、症状、原因、取る行動の順に並べます。 ### マニフェストが2種類あり、FloatSoda での扱いが違う - 症状: `AutoLaunch` やシーン遷移が動かない - 原因: アクションマニフェストは自動で登録されるが、アプリケーションマニフェストは自動では登録されない - 取る行動: `OVRApplications.AddManifest` でアプリケーションマニフェストを登録する。詳細は前の節の比較表にある ### `OpenVR.IsHmdPresent()` が `True` でも、初期化が失敗する - 症状: `OVRApplication` を作ると、`Init_HmdNotFound` の `VRInitializeException` になる - 原因: `IsHmdPresent()` が `True` でも起きる。条件は分かっていない - 取る行動: 初期化の成否は、`IsHmdPresent()` ではなく、`OVRApplication` の例外で判断する。SteamVR の状態と HMD の接続を確かめる - 確かめた程度: 1台の環境で再現した。どの状態で起きるかは分かっていない ### 初期化が通ったあと、`OverlayIdentity` の作成で `NullReferenceException` になる - 症状: `OVRApplication` の作成は通り、`new OverlayIdentity(...)` で `NullReferenceException` になる。スタックトレースは `OverlayIdentity` のコンストラクターの `OpenVR.Overlay.CreateOverlay` の行を指す - 原因: 分かっていない。スタックトレースから、`OpenVR.Overlay` が `null` だった可能性を推定している - 取る行動: 起きたときは、SteamVR の起動状態とスタックトレースの全文を添えて、Issue に報告する(→ [不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/)) - 確かめた程度: 一度だけ再現した。同じ操作で起きないときもあった ### `AppKey` の検証が、コンストラクターでは働かない - 症状: 大文字を含む `AppKey` を作っても、例外にならない - 原因: 検証は `Value` の `init` アクセサーにあり、`new AppKey("...")` のコンストラクターは通らない - 取る行動: 小文字、数字、ドット、アンダースコアだけで、127 文字までの値を渡す - 確かめた程度: 実行して確かめた。大文字、200 文字、空文字の3通りで、例外にならなかった ### ポインター入力が届くのは、ダッシュボードのオーバーレイだけ - 症状: ダッシュボード以外のオーバーレイで、`GestureDetector` のコールバックが呼ばれない - 原因: エンジン層の `OverlayWindow` が入力源を作るのは、ダッシュボードのときだけ。`ControllerPointerSystem` は、どこからも呼ばれていない - 取る行動: ダッシュボード以外へのポインター入力は、Issue で追跡している(追跡: [#182](https://github.com/sumx21t-3310/FloatSoda/issues/182)) - 確かめた程度: コードから読んだ。実機では確かめていない `ControllerPointerSystem` は、`MakeOverlaysInteractiveIfVisible` を使わずに、コントローラーのレイとオーバーレイの交差を毎フレーム計算します。SteamVR の入力の横取りが起きないので、VRChat の AFK 判定を避けられる、とクラスの説明に書かれています。これはクラスの説明にあるだけで、VRChat では確かめていません。 OVR 層だけでオーバーレイを出して入力を受ける実験は、[実験: OVR 層](/contributor/2-understand/experiment-ovr/) にあります。 # 描画層 > Layer ツリーは、記録済みの描画(PictureLayer)と、描画をまとめて加工する合成の操作(不透明度、変換、クリップ)を、木にしたものです。描画層(FloatSoda.Rendering)は、この木と、木を描く LayerRenderer を持ちます。参照するのは SkiaSharp だけで、FloatSo… ## Layer ツリーが表すもの Layer ツリーは、記録済みの描画(`PictureLayer`)と、描画をまとめて加工する合成の操作(不透明度、変換、クリップ)を、木にしたものです。描画層(`FloatSoda.Rendering`)は、この木と、木を描く `LayerRenderer` を持ちます。参照するのは SkiaSharp だけで、FloatSoda のほかのプロジェクトは参照しません。 木の葉は `PictureLayer` で、記録済みの `SKPicture` を再生します。枝は `ContainerLayer` とその派生型で、子を順に描きます。合成の操作は、枝として子を包みます。 ```text ContainerLayer ルート。子を順に描く ├─ PictureLayer 葉。記録済みの絵を再生する └─ OpacityLayer 枝。子をまとめて半透明にする └─ PictureLayer ``` ## Layer の種類と、それぞれの役割 `ILayer` は、`PaintBounds`、`Layout`、`Paint`、`Clone` を持ちます。`PictureLayer` 以外の種類は、すべて `ContainerLayer` の派生です。 | 名前 | 持つ設定 | `Paint` がすること | | :-- | :-- | :-- | | `ContainerLayer` | `Children` | 子を一覧の順に描く | | `PictureLayer` | `Picture`(`SKPicture?`) | `Picture` を `Playback` で再生する。`null` なら何も描かない | | `OpacityLayer` | `Alpha`(`byte`。既定は 255) | `SaveLayer` で子をまとめて描き、`Restore` で `Alpha` を掛けて合成する | | `TransformLayer` | `Transform`(`SKMatrix`) | `Save`、`Concat`、子の描画、`Restore` の順に行う | | `ClipRectLayer` | `ClipRect`(`SKRect`)、`ClipBehavior` | `Save`、`ClipRect`、子の描画、`Restore` の順に行う | | `ClipRoundRectLayer` | `ClipRect`(`SKRoundRect`)、`ClipBehavior` | `Save`、`ClipRoundRect`、子の描画、`Restore` の順に行う | | `ClipPathLayer` | `ClipPath`(`SKPath`)、`ClipBehavior` | `Save`、`ClipPath`、子の描画、`Restore` の順に行う | `ClipBehavior` の既定は `Clip.Antialias` です。3つのクリップは、`ClipRectLayer.cs` に定義されています。 表のうち、変換とクリップの Layer は `Save` と `Restore` の組で、`OpacityLayer` は `SaveLayer` と `Restore` の組で、子を包みます。`ContainerLayer` と `PictureLayer` は、`Paint` でこの組を持ちません。そのため、変換・クリップ・不透明度の Layer が変えたキャンバスの状態は、後ろに並ぶ Layer へ引き継がれません。新しい Layer を足すときも、`Paint` で変えたキャンバスの状態は `Restore` で戻します。実際に3つの Layer が同じ絵へ何をするかは、[実験: 描画層](/contributor/2-understand/experiment-rendering/) で確かめられます。 ## Layout のあとに Paint を実行する `Layout` は、その Layer と子の描画境界(`PaintBounds`)を計算します。`PictureLayer` は `Picture` の `CullRect`、`ContainerLayer` は子の境界をすべて含む外接矩形、`TransformLayer` は変換後の矩形です。`Paint` は、計算済みの内容をキャンバスに合成します。`ILayer` の契約は次のとおりです。 - `Layout` で境界を確定してから、同じコンテキストで `Paint` を呼ぶこと `LayerRenderer.Render(root, context)` が、この2つを順に呼びます。描画先のクリア、フラッシュ、`SKSurface` の所有権は、呼び出し側の仕事です。`LayerContext` が持つのは、描画先の `SKCanvas` だけです。 ## Clone は何を共有し、何を共有しないか `Clone` は、子を再帰的に複製して、独立したツリーを作ります。描画リソースの所有権は移さず、元と共有する場合があります。 | 区分 | 内容 | | :-- | :-- | | 共有される | `PictureLayer` の `SKPicture` の参照 | | 共有される | `ClipRoundRectLayer` の `SKRoundRect` と、`ClipPathLayer` の `SKPath` の参照 | | 共有されない | 木の構造(`Children` の一覧と、各 Layer のインスタンス) | | 共有されない | 値で持つ合成の設定(`Alpha`、`Transform`、`ClipRectLayer` の `ClipRect`、`ClipBehavior`)。値のコピーになる | | 共有されない | `PaintBounds`。複製したツリーに `Layout` を呼んだときに計算される | 自分で Layer を足すときは、`Clone` が何をコピーして何を参照するかを決めます。記録済みの絵のような描画リソースは参照にして、設定の値はコピーにします。参照にする場合は、所有者と寿命を決めます。 `SKPicture` の所有権は、複製先に移りません。複製を描いているあいだは、元の持ち主が `SKPicture` を有効な状態に保つ必要があります。複製した側で破棄すると、同じ `SKPicture` を使う元のツリーも描けなくなります。 - 共有する `SKPicture`、`SKRoundRect`、`SKPath` は、描画が完了するまで変更・破棄しないこと 複製を描画スレッドへ渡す理由は、[エンジン層](/contributor/2-understand/engine-layer/) の「なぜ Layer を複製して描画スレッドに渡すのか」にあります。 ## CPU の Bitmap に描く GL を使わずに Layer ツリーを確かめるには、`LayerBitmapRenderer` を使います。`Render(root, imageSize)` は、ツリーに `Layout` を呼んでから、透明で初期化した新しい `SKBitmap` に描いて返します。返された `SKBitmap` は、呼び出し側が破棄します。`root` が `null` なら `ArgumentNullException`、`imageSize` の幅か高さが 0 以下なら `ArgumentOutOfRangeException` です。 `SKBitmap` を png にするには、拡張メソッド `Save` を使います。既定の形式は png、品質は 100 です。保存先に同名のファイルがあれば上書きし、親のディレクトリは作りません。 ```csharp using var bitmap = new LayerBitmapRenderer().Render(root, new SKSizeI(400, 300)); bitmap.Save("output.png"); ``` `LayerBitmapRenderer` と `Save` は `FloatSoda.Rendering` にあります。RenderObject ツリーや Widget ツリーを描くヘッドレスの描画器は、`FloatSoda.Testing` の `RenderObjectBitmapRenderer` と `WidgetBitmapRenderer` です。 手を動かして確かめる続きは、[実験: 描画層](/contributor/2-understand/experiment-rendering/) と、[描画が届くまで(4段)](/contributor/2-understand/tutorial-render-path/) の2段目にあります。 # 描画が届くまで(4段) > 層を1つずつ足しながら、描画が SteamVR のオーバーレイに届くまでを、手を動かして確かめます。OVR 層だけでオーバーレイを出すところから始め、描画層、エンジン層、フレームワーク層の順に足します。OVR 層と描画層は互いを参照しないので、それぞれを試してから、両方を使うエンジン層へ進みます。最後に、フレームワー… ## このチュートリアルで作るもの 層を1つずつ足しながら、描画が SteamVR のオーバーレイに届くまでを、手を動かして確かめます。OVR 層だけでオーバーレイを出すところから始め、描画層、エンジン層、フレームワーク層の順に足します。OVR 層と描画層は互いを参照しないので、それぞれを試してから、両方を使うエンジン層へ進みます。最後に、フレームワーク層で同じ描画の流れを組みます。 | 段 | 足す層 | 目に見える結果 | | :-- | :-- | :-- | | 1 | OVR 層 | HMD の 1 m 前のオーバーレイに、png の絵が出る | | 2 | 描画層 | 自分で組んだ Layer ツリーが、png の画像になる | | 3 | エンジン層 | 同じ Layer ツリーが、GL テクスチャ経由でオーバーレイに出る | | 4 | フレームワーク層 | ウィジェットで書いた色の四角が、ダッシュボードに出る | 最後の段で、フレームワークの `FloatSodaApp` が、段3で自分が書いた仕事と同じことをしているのが分かります。 ```text 段1 OVR OpenVR を呼ぶ 参照: なし 段2 Rendering Layer ツリーを絵にする 参照: なし(SkiaSharp のみ) 段3 Engine GL テクスチャで SteamVR に出す 参照: Abstractions、OVR、Rendering 段4 FloatSoda ウィジェットから Layer ツリーを作る 参照: Abstractions、Engine、Rendering ``` 各段の終わりに、確認の項目と、選択式のクイズが1問あります。仕組みの説明は、層ごとの解説に分けています。 ## 始める前に用意するもの 次の3つを用意します。 - .NET の SDK - FloatSoda を clone したフォルダー - SteamVR と HMD(段2だけは、HMD なしで進められる) 4段のコードは、1つの作例 [samples/FloatSoda.Samples.RenderPath](https://github.com/sumx21t-3310/FloatSoda/tree/v0.5.0/samples/FloatSoda.Samples.RenderPath) にまとまっています。コードの全文は、このページに載せません。作例のファイルを読みながら進めます。リポジトリのルートで、`--step N` を付けて段を選びます。準備の詳細は [開発環境を用意する](/contributor/3-prepare/dev-environment/) にあります。SteamVR が起動できない、HMD がつながらない場合の直し方は、ここでは扱いません。 ## 1. OVR 層だけでオーバーレイを出す 段1は、FloatSoda のほかの層を使わず、`FloatSoda.OVR` だけで、png の画像をオーバーレイとして HMD の前に出します。出す png は段2が作るので、先に次のコマンドを1回動かします。png は、カレントディレクトリの `my_layer_tree.png` に保存されます。段2の内容は、次の節で説明します。 ```powershell dotnet run --project samples/FloatSoda.Samples.RenderPath -- --step 2 ``` SteamVR を起動してから、動かします。HMD の中で、オーバーレイを探します。 ```powershell dotnet run --project samples/FloatSoda.Samples.RenderPath -- --step 1 ``` 別の png を出したいときは、`-- --step 1 ` の形でパスを渡します。省略すると、カレントディレクトリの `my_layer_tree.png` を使います。 作例は、`DeviceTrackedOverlay` を HMD から 1 m 前の位置に置き、`overlay.Texture.FromFile` で png を渡します。 **できた?** HMD の 1 m 前に付いたオーバーレイに、段2で作った png の絵が出た。
できなかったら `Init_HmdNotFound` の例外が出る場合や、オーバーレイが見つからない場合は、SteamVR の状態と HMD の接続を確かめます。原因の見分け方は、[OVR 層](/contributor/2-understand/ovr-layer/) の「つまずきやすい点」にあります。段2の png がない場合は、段2に戻ります。
**クイズ** 段1のオーバーレイの絵は、だれが描いたでしょうか。 - (a) Skia - (b) OpenVR が png を読んで表示した - (c) OpenGL
答え (b) です。`overlay.Texture.FromFile` が、OpenVR の `SetOverlayFromFile` を呼びます。FloatSoda は画像のパスを渡すだけで、描いているのは OpenVR です。
OVR 層の型の詳細は、[OVR 層](/contributor/2-understand/ovr-layer/) にあります。 ## 2. Layer ツリーを組んで png に描く 段2は、HMD も SteamVR も使わずに、自分で組んだ Layer ツリーを png にします。`FloatSoda.Rendering` だけを使います。Layer ツリーを組んで png にする基本は、[samples/FloatSoda.Samples.PaintingSample](https://github.com/sumx21t-3310/FloatSoda/tree/v0.5.0/samples/FloatSoda.Samples.PaintingSample) のコードで読めます。このページでは、同じ説明を繰り返しません。段2の作例は、その流れを最小にしたものです。 ```powershell dotnet run --project samples/FloatSoda.Samples.RenderPath -- --step 2 ``` コンソールに、png の保存先のパスが出ます。保存先を変えたいときは、`-- --step 2 ` の形で渡します。省略すると、カレントディレクトリの `my_layer_tree.png` です。保存される絵は、白っぽい地に緑の四角です。 **できた?** 保存先のパスが出て、その png を画像ビューアーで開くと、白っぽい地に緑の四角が見えた。
できなかったら png が空になる、真っ黒になる場合は、作例の中で `PictureLayer` に `Picture` を設定したか、描画の範囲(`BeginRecording` に渡した矩形)がサイズと合っているかを確かめます。仕組みは [描画層](/contributor/2-understand/rendering-layer/) の「CPU の Bitmap に描く」にあります。
**クイズ** `LayerBitmapRenderer.Render` が、`root` に対して最初に呼ぶのは、どれでしょうか。 - (a) `Paint` - (b) `Layout` - (c) `Clone`
答え (b) です。内部の `LayerRenderer.Render` が、`root.Layout(context)` を呼んでから `root.Paint(context)` を呼びます。
Layer の種類と `Clone` の共有範囲は、[描画層](/contributor/2-understand/rendering-layer/) にあります。合成の Layer を同じ絵に使って見比べる実験は、[実験: 描画層](/contributor/2-understand/experiment-rendering/) です。 ## 3. Layer ツリーをエンジン層に差し込んで SteamVR に出す 段3は、段2で組んだ Layer ツリーを、png ではなく GL テクスチャとしてオーバーレイに出します。段1の `FromFile` の代わりに、エンジン層の `RenderPostTaskRunner`、`GLView`、`Renderer`、`OverlayWindow` を使います。`FloatSoda.Engine` は、フレームワーク(`FloatSoda`)を参照しないので、フレームワークなしで書けます。 ```powershell dotnet run --project samples/FloatSoda.Samples.RenderPath -- --step 3 ``` 作例のコードの要点は3つです。 - `RenderPostTaskRunner` が、描画スレッドを作る。`Start` のあと、`PostTask` に渡した処理が、描画スレッドで実行される - `GLView`、`Renderer`、`OverlayWindow` は、描画スレッドの中で作る。`OverlayWindow` は、`Present` のたびに GL テクスチャを OpenVR に渡す - `FramePacer(60)` が、描画ループの周期を 60 回/秒にそろえる 作例は、GL のリソースの後始末を省いた最小のコードです。 `OverlayWindow` は、エンジン層の型です。フレームワーク(`FloatSoda`)にも、同じ名前の `FloatSoda.Widgets.OverlayWindow` があります。同じプロジェクトに両方を入れるときは、フレームワークと同じように、`using EngineOverlayWindow = FloatSoda.Engine.OverlayWindow;` と別名を付けます。 **できた?** 段2と同じ絵が、`FromFile` を呼ばずに、オーバーレイに出た。
できなかったら 絵が出ない、例外が出る場合は、`GLView` を描画スレッド以外で作っていないかを、作例のコードで確かめます。GL を使う処理は、`PostTask` に入れます。スレッドの制約は、[エンジン層](/contributor/2-understand/engine-layer/) の「描画スレッドは、キューのタスクを実行する」にあります。
**クイズ** GL テクスチャのハンドルを `Texture_t` に包んで、オーバーレイに渡すのは、どの型でしょうか。 - (a) `GLView` - (b) `Renderer` - (c) `OverlayWindow`
答え (c) です。`OverlayWindow.Present` が、`Renderer.Render` のあとに、テクスチャのハンドルを `Texture_t` に包み、`Overlay.Texture.FromTexture_t` で OpenVR に渡します。
エンジン層の詳細は、[エンジン層](/contributor/2-understand/engine-layer/) にあります。 ## 4. ウィジェットで描く 段4は、段3で手書きしたものを、フレームワークに任せます。ウィジェットで色の四角を書き、ダッシュボードに出します。 SteamVR を起動してから、動かします。HMD の中でダッシュボードを開き、`RenderPath` のタブを選びます。 ```powershell dotnet run --project samples/FloatSoda.Samples.RenderPath -- --step 4 ``` 段3で書いた処理は、フレームワークの次の箇所が受け持っています。 | 段3で自分が書いたもの | フレームワークの担当 | | :-- | :-- | | `OVRApplication` を作る | `FloatSodaApp` の起動処理 | | `OverlayIdentity` と `DeviceTrackedOverlay` を作る | `CreateWindow` が作成を予約し、初期化処理が描画スレッドでダッシュボード用のオーバーレイを作る | | `RenderPostTaskRunner` と `FramePacer` を作って `Start` する | `AddFloatSoda` が登録し、`FloatSodaApp` が持って開始する | | `PostTask` で `GLView`、`Renderer`、`OverlayWindow` を作る | `WidgetBinding.EnsureInitialized` | | Layer ツリーを手で組む | `BuildOwner` がビルドを進め、`RenderPipeline` がレイアウトと描画を処理して Layer ツリーを更新する | | `window.Resize(size)` | `WidgetBinding.DrawFrame` が、レイアウト結果のサイズが変わったときに投入する | | `PostRender(window, layer)` | `WidgetBinding.DrawFrame` が、`Layer` を複製して `PostRender` に渡す | | Enter キーで待ち、`Stop` を呼ぶ | ホスト(`host.RunAsync`)と、`FloatSodaApp` の終了処理 | **できた?** ダッシュボードに、色の四角が出た。
できなかったら 起動で例外が出る場合は、利用者向けのガイドの [エラーを読んで直す](/guide/5-troubleshoot/read-errors/) を確かめます。四角が出ない場合は、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/) を確かめます。
**クイズ** 段3で自分が書いた `PostRender` の呼び出しを、フレームワークでは、何が呼んでいるでしょうか。 - (a) `FloatSodaApp.CreateWindow` - (b) `WidgetBinding.DrawFrame` - (c) `FramePacer`
答え (b) です。`WidgetBinding.DrawFrame` が、`RenderView.Layer` を複製して、`PostRender` で描画スレッドに渡します。
フレームワークの詳細は、[フレームワーク層](/contributor/2-understand/framework-layer/) にあります。 ## 次に読むページ 目的に合わせて、続きを選びます。 - 層をまたぐ流れを読む: [1フレームが SteamVR に届くまで](/contributor/2-understand/frame-to-steamvr/) - 層ごとの解説を読む: [OVR 層](/contributor/2-understand/ovr-layer/)、[描画層](/contributor/2-understand/rendering-layer/)、[エンジン層](/contributor/2-understand/engine-layer/)、[フレームワーク層](/contributor/2-understand/framework-layer/)、[UI 層](/contributor/2-understand/ui-layer/) - 実験で確かめる: [実験: OVR 層](/contributor/2-understand/experiment-ovr/)、[実験: 描画層](/contributor/2-understand/experiment-rendering/)、[実験: フレームワーク層](/contributor/2-understand/experiment-framework/) - 開発の準備に進む: [開発環境を用意する(コントリビューター)](/contributor/3-prepare/dev-environment/) # UI 層(3層に分けた理由) > FloatSoda は、UI の部品(ボタンなど)を、コア、ヘッドレス、デザインシステムの3層に分けます。置き場所のプロジェクトは、コア(FloatSoda)、ヘッドレスの振る舞い(FloatSoda.UI)、デザインシステム(FloatSoda.UI.Cream と FloatSoda.UI.FizzyPop)の4… ## UI を3層に分ける理由 FloatSoda は、UI の部品(ボタンなど)を、コア、ヘッドレス、デザインシステムの3層に分けます。置き場所のプロジェクトは、コア(`FloatSoda`)、ヘッドレスの振る舞い(`FloatSoda.UI`)、デザインシステム(`FloatSoda.UI.Cream` と `FloatSoda.UI.FizzyPop`)の4つです。分ける理由は、見た目と振る舞いが一体になることを避けるためです。 一体になると、別の見た目の部品を作りたいときに、振る舞いのコードを複製することになります。プロジェクトの設計方針は、Flutter で起きた「Material ロックイン」を例にしています。振る舞いの層が独立せず、見た目と振る舞いが `material` パッケージに一体化したため、別の見た目の `cupertino` が振る舞いを複製することになった、という問題です。 ## 3層の役割と、置き場所の基準 新しい部品を足すときは、置く層を、次の表で仮に決めます。 | 層 | プロジェクト | 置くもの | 置かないもの | | :-- | :-- | :-- | :-- | | コア | `FloatSoda` | Skia や RenderObject の型に触れるウィジェット。見た目の方針を持たない合成ウィジェット(`Center`、`Container` など) | インタラクションの状態機械、具体的な見た目 | | ヘッドレス | `FloatSoda.UI` | インタラクションの状態機械(`ButtonBase`、`InteractionState`)。見た目を受け取る型付きの入口 | 色、余白、角丸 | | デザインシステム | `FloatSoda.UI.Cream`、`FloatSoda.UI.FizzyPop` | ヘッドレスの状態から見た目への対応。スタイルの `record`(`ButtonStyle`)、テーマ | 振る舞いの状態機械 | プロジェクトの参照は、次の向きです。 ```text FloatSoda ◀── FloatSoda.UI ◀── FloatSoda.UI.Cream ▲ └────────── FloatSoda.UI.FizzyPop ``` 2つのデザインシステムは、お互いを参照しません。デザインシステムは、`FloatSoda.UI` を通して、コアのウィジェットも使えます。 ## 2つの規約と、判定基準 置き場所は、次の2つの規約と、1つの判定基準で確定します。 1. 振る舞いと状態機械は `FloatSoda.UI` に置き、デザインシステムの `State` には、ヘッドレスの状態から見た目への対応だけを書くこと 2. `FloatSoda.UI` のウィジェットは、デザインシステムの `InheritedWidget` がなくても動くこと 規約1は、Flutter の `material` に振る舞いが入った轍を踏まないためです。規約2のため、ヘッドレス層はデザインシステムのテーマに依存させません。デザインシステム側のテーマの `Of()` は、祖先にテーマが見つからないときに `null` を返し、部品は既定のスタイルに戻ります。`FloatSoda.UI.Cream` の `Button` は、スタイルが未指定なら祖先の `CreamTheme` を、それもなければ `ButtonStyle.Default` を使います。 判定基準は、「2つ目のデザインシステムが、1つ目のコードをコピーせずに、同じ部品を作れるか」です。Cream と FizzyPop を最初から並べて作るのは、この基準を常に確かめるためです。ヘッドレス層の API に片方のデザインシステムの都合が漏れると、もう片方が作れなくなるので、漏れに気づけます。 基準を満たさない場合は、必要な状態や拡張点がヘッドレス層に足りないか、片方のデザインシステムの都合がヘッドレス層の API に漏れていないかを確かめます。置き場所と API の形を見直します。 ## 見た目を型付きのビルダーで渡す ヘッドレス層は、見た目を `Builder` で受け取ります。`ButtonBase` は、`required Func Builder` を持ちます。`InteractionState` は、`IsPressed`、`IsHovered`、`IsFocused`、`IsDisabled` を持つ `readonly record struct` で、インタラクションの状態を型で公開します。デザインシステムは、`Builder` の中で、この状態を見た目に対応づけます。 次は、`FloatSoda.UI.Cream` の `Button` の処理の概略です。`Builder` が `Widget` を返す部分を省略しているため、このままではコンパイルできません。 ```csharp new ButtonBase { OnPressed = OnPressed, IsDisabled = IsDisabled, Builder = (ctx, state) => { var style = Style ?? CreamTheme.Of(ctx)?.ButtonStyle ?? ButtonStyle.Default; var background = state switch { { IsDisabled: true } => style.DisabledBackgroundColor, { IsPressed: true } => style.PressedBackgroundColor, _ => style.BackgroundColor }; // background で ColoredBox を作り、Child を包んで返す } }; ``` 押下やホバーの状態を入力イベントから更新する処理は、ヘッドレス層に置きます。`Builder` は、渡された `state` に応じて色を選びます。自分の部品の `Builder` の形を設計するときは、デザインシステムが必要とする状態を、`InteractionState` に足すかどうかを先に決めます。 この方式は、Avalonia のルックレスコントロール(状態を疑似クラス、見た目の注入を `PART_` のテンプレートパーツで行う契約)を、型付きにした形です。`InteractionState` のコメントが、この対応を述べています。文字列の疑似クラスの代わりに型付きの `InteractionState` を、名前で探す `PART_` の代わりに型付きの `Builder` を使います。 ## 実装の進み具合の追い方 このページは、実装の進み具合を書きません。進み具合は、次の Issue で追います。 | Issue | 追うもの | | :-- | :-- | | [#102](https://github.com/sumx21t-3310/FloatSoda/issues/102) | `ButtonBase` の押下・ホバー状態(`GestureDetector` との接続) | | [#78](https://github.com/sumx21t-3310/FloatSoda/issues/78) | Cream の `Button` と、テーマ | | [#100](https://github.com/sumx21t-3310/FloatSoda/issues/100) | FizzyPop の `Button` と、テーマ | | [#38](https://github.com/sumx21t-3310/FloatSoda/issues/38) | 背景ブラー | | [#182](https://github.com/sumx21t-3310/FloatSoda/issues/182) | ダッシュボード以外のオーバーレイへのポインター入力 | 3つのプロジェクトを NuGet で配布しているかは、各プロジェクトの `.csproj` の `IsPackable` で確かめます。 # コントリビューターの規約 > 規約の正本は、リポジトリ直下の2つのファイルにあります。このページは規約を写さず、場面ごとに読む節の場所だけを案内します。 ## この一覧の使い方 規約の正本は、リポジトリ直下の2つのファイルにあります。このページは規約を写さず、場面ごとに読む節の場所だけを案内します。 | ファイル | 決めていること | | :-- | :-- | | [CONTRIBUTING.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md) | 人間とエージェントに共通する開発・コントリビューション規約 | | [REVIEW.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/REVIEW.md) | コードレビューの判断基準と、見る項目の優先度 | どちらのファイルも、本文中の「必須」は MUST、「推奨」は SHOULD の強さで読みます。API 設計原則と Flutter parity の判断基準、エージェントの入口、リリース手順の正本は、それぞれ別のファイルです。置き場所は CONTRIBUTING.md の冒頭の表にあります。 自分の場面に合う見出しへ進んでください。 ## 書き始める前に読むもの 書き始める前に、変更が Issue を要するか、ブランチ名と PR の範囲をどうするか、変更が受け入れ範囲に入るかを確かめます。 | 確かめること | 正本の節 | | :-- | :-- | | 着手前に Issue が要る変更か | CONTRIBUTING.md の「Issue が必須な変更」 | | Issue のタイトルとラベル | CONTRIBUTING.md の「タイトルとラベル」 | | ブランチ名 | CONTRIBUTING.md の「ブランチ名」 | | PR に含めてよい変更の範囲 | CONTRIBUTING.md の「PR のスコープ」 | | コアに受け入れる変更か | CONTRIBUTING.md の「受け入れ範囲」 | Flutter 由来の部品を作る場合は、[Flutter から移植する](/contributor/4-write/port-from-flutter/)も読みます。 ## 書いている間に引くもの 書いている最中の疑問は、次の表の節で引きます。 | 疑問 | 正本の節 | | :-- | :-- | | テスト名の付け方 | CONTRIBUTING.md の「テストの命名」 | | 何を検証するか | CONTRIBUTING.md の「テスト観点」(置き場所は [テストの分け方](/contributor/3-prepare/test-strategy/)) | | namespace とディレクトリの対応 | CONTRIBUTING.md の「namespace とディレクトリ」 | | コードの書き方 | CONTRIBUTING.md の「コーディング規約」 | | public API の追加 | CONTRIBUTING.md の「新しい public API を追加する場合の受け入れ条件」 | | サンプルの追加 | CONTRIBUTING.md の「サンプルを追加する場合の規約」 | | skill の追加・変更 | CONTRIBUTING.md の「エージェント向け skill を追加・変更する場合」 | | 文書の更新と文体 | CONTRIBUTING.md の「ドキュメントを更新する場合の注意」と「ドキュメントのトンマナ」 | ## 出す前に読むもの 変更を出す前に、CONTRIBUTING.md の「PRを出す前のチェックリスト」を上から確かめます。 - PR を出す前に、CI と同じ手順を手元で通すこと(手順は [開発環境を用意する](/contributor/3-prepare/dev-environment/)にあります) PR の本文は、[PULL_REQUEST_TEMPLATE.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.github/PULL_REQUEST_TEMPLATE.md) の欄に沿って書きます。 描画に関わる変更には、範囲を決める予定の規約があります。範囲の一覧は CONTRIBUTING.md に置く方針ですが、まだ載っていません。載るまでは、変更が描画に関わるかどうかを Issue で確かめます。 PR を出したあとは、[レビューの指摘を受けて直す](/contributor/5-submit/address-review/)と [CI が落ちたとき](/contributor/5-submit/ci-failures/)を読みます。 ## レビューされる側とする側が読むもの PR を出した人も、レビューを頼まれた人も、REVIEW.md で何が見られるかを確かめます。見る順番に並べた章が、次のとおりです。 | 知りたいこと | 正本の章 | | :-- | :-- | | 指摘の重さの順 | REVIEW.md の「1. 重要度の順序」 | | どんな指摘を出すか | REVIEW.md の「2. finding の基準」 | | 判断の根拠にする資料の順 | REVIEW.md の「3. 仕様の一次情報と優先順位」 | | FloatSoda 固有の不変条件 | REVIEW.md の「4. FloatSoda 固有の不変条件」 | | Flutter との差異の扱い | REVIEW.md の「6. Flutter parity」 | | 自動レビューと人のレビューの分担 | REVIEW.md の「9. レビューの層と分担」 | 指摘の受け方は [レビューの指摘を受けて直す](/contributor/5-submit/address-review/)にあります。 ## 提案するときに読むもの 新しい機能や設計の変更を提案する前に、案が受け入れ範囲に入るかを確かめます。範囲は CONTRIBUTING.md の「受け入れ範囲」、public API の受け入れ条件は同じファイルの「新しい public API を追加する場合の受け入れ条件」にあります。 提案の書き方は [提案を書く](/contributor/6-propose/write-proposal/)にあります。 ## そのほかの規約 CONTRIBUTING.md と REVIEW.md のほかに、次の規約がリポジトリの直下にあります。どれも公開サイトには載りません。 | 規約 | 中身 | | :-- | :-- | | [API_DESIGN.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/API_DESIGN.md) | API 設計の原則と Flutter parity。差異を残すときの判断基準 | | [LOCALIZATION.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/LOCALIZATION.md) | 日本語を既定の言語にする方針 | | [DOCUMENTATION_COMMENTS.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/DOCUMENTATION_COMMENTS.md) | XML ドキュメントコメントの書き方 | | [WRITING_DOCUMENTATION.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/WRITING_DOCUMENTATION.md) | docs の書き方と、変更ごとに直す docs を決める Documentation Impact Matrix | # 開発環境を用意する(コントリビューター) > この手順を終えると、手元で dotnet build と、CI と同じ手順のテストが通ります。移植をする人は、Flutter の元の実装をローカルのクローンから引けるようになります。手順は5つで、手順4は移植をする人だけ、手順5は任意です。 ## この手順でできること この手順を終えると、手元で `dotnet build` と、CI と同じ手順のテストが通ります。移植をする人は、Flutter の元の実装をローカルのクローンから引けるようになります。手順は5つで、手順4は移植をする人だけ、手順5は任意です。 環境がそろっていないと、CI と同じ結果が手元で出ず、PR を出してから落ちます。移植では Flutter の実装を見ながら書くので、引ける状態を先に作ります。 ## 前提条件 - Windows の PC を用意すること(CI は `windows-latest` で動く) - `git` を使えること この手順で実行するビルドとテストに、SteamVR は要りません。SteamVR が要るのは、サンプルの実行と実機の確認です。実機の確認は [テストの分け方](/contributor/3-prepare/test-strategy/)で区別しています。 ## 手順1: SDK を入れる 1. [.NET の公式ダウンロードページ](https://dotnet.microsoft.com/download)から、.NET 10 SDK を入れる 2. 次のコマンドで版を確かめる ```bash dotnet --version ``` **できた?** `dotnet --version` が 10 で始まる版を返す。
できなかったら 9 以下が返るときは、`dotnet --list-sdks` で .NET 10 SDK の有無を確かめます。入っていなければ入れ直します。入っていれば、`dotnet` が選ぶ版(PATH の順序や `global.json`)を確かめます。
## 手順2: クローンしてビルドする リポジトリをクローンして、リポジトリ直下でビルドします。 ```bash git clone https://github.com/sumx21t-3310/FloatSoda.git cd FloatSoda dotnet build ``` **できた?** `dotnet build` がエラーなしで終わる。
できなかったら エラーメッセージの全文を読みます。SDK の版が合わないエラーなら、手順1に戻ります。それ以外のエラーで、自分の変更を加える前に出るときは、[不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/)に進みます。
## 手順3: CI と同じ手順でテストする PR を出す前に、CI と同じ手順を手元で通すことが規約です。CI の定義は [ci.yml](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.github/workflows/ci.yml) の Build と Test のステップで、次のコマンドと同じです。食い違うときは ci.yml に合わせます。次のコマンドを順に実行します。 ```bash dotnet build --configuration Release dotnet test tests/FloatSoda.Rendering.Test --configuration Release --no-build dotnet test tests/FloatSoda.Test --configuration Release --no-build ``` 特定のテストだけを動かすには、`--filter` を使います。 ```bash dotnet test tests/FloatSoda.Test --filter "FullyQualifiedName~AlignmentTest.ComputeOffset" ``` `--filter` に一致するテストがないと、テストを1件も実行しないまま終わります。出力の合格件数が1以上であることを確かめます。 規約の全体は [コントリビューターの規約](/contributor/3-prepare/conventions/)から案内しています。 **できた?** 2つのテストプロジェクトが、どちらも成功で終わる。
できなかったら テストが落ちたときは、落ちたテストの名前とメッセージを確かめます。自分の変更を加える前に落ちるなら、環境の問題か FloatSoda の問題です。[不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/)に進みます。
移植をする人は手順4へ、それ以外の人は手順5へ進みます。 ## 手順4: Flutter の元の実装を引ける状態にする(移植をする人) Flutter の参照クローンの作り方は、[flutter/flutter](https://github.com/flutter/flutter) のリポジトリを参照してください。 **できた?** flutter/flutter のクローンが手元にあり、移植するウィジェットのファイルを開ける。
できなかったら ウィジェット名が Flutter のクローンに見つからないときは、FloatSoda 独自のウィジェットの可能性があります。移植の手順は [Flutter から移植する](/contributor/4-write/port-from-flutter/)にあります。
## 手順5: Rider などの道具を足す(任意) JetBrains Rider が入っていて、その MCP サーバーが使えるなら、C# のシンボル解決、リネーム、namespace の移動に使うと確実です。文字列置換は、ジェネリック制約、XML ドキュメントの `cref`、`nameof` を見落とします。 Rider がなくても作業は進められます。`dotnet` CLI と検索で続けます。道具を入れるかどうかは、自分で決めます。エージェントは、ソフトウェアを入れる前に、入れるものと理由を人間に確認します。 次は [コントリビューターの規約](/contributor/3-prepare/conventions/)です。 ## うまくいかないとき ビルドやテストで出たエラーは、症状で行き先が分かれます。 | 症状 | 行き先 | | :-- | :-- | | SDK の版が合わない | 手順1 | | 自分の変更を加える前にテストが落ちる | [不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/) | | 自分の変更を加えたらテストが落ちる | 落ちたテストの名前とメッセージを読み、変更を見直す | # Issue・PR・マイルストーンの読み方 > この手順を終えると、手伝える Issue を1件見つけられます。FloatSoda の今の状況は、この docs には書きません。件数や一覧を docs に書くと古くなるためです。状況は GitHub の Issue、PR、マイルストーンに書かれているので、読み方をここで説明します。手順は5つです。 ## この手順でできること この手順を終えると、手伝える Issue を1件見つけられます。FloatSoda の今の状況は、この docs には書きません。件数や一覧を docs に書くと古くなるためです。状況は GitHub の Issue、PR、マイルストーンに書かれているので、読み方をここで説明します。手順は5つです。 GitHub のアカウントがあり、Issue と PR を開ける人が対象です。ラベルとマイルストーンの意味は知らなくてかまいません。 ## 手順1: ラベルから Issue の種類を読む FloatSoda の Issue には、3つの軸のラベルが付きます。 | 軸 | 意味 | 付き方 | | :-- | :-- | :-- | | 種別 | Issue の主目的 | 原則1つ | | 領域(`area:*`) | 関わるプロジェクトや機能 | 複数付くことがある | | 補助属性 | `breaking-change`、`flutter-divergence`、`needs-device-test`、`good-first-issue`、`help-wanted` | 必要なものだけ | 種別は、`bug` や `feature` のように、Issue の主目的を表します。ラベルの一覧と説明の正本は、[リポジトリの Labels ページ](https://github.com/sumx21t-3310/FloatSoda/labels)です。このページには写しません。 Issue の一覧でラベルを選ぶと、Issue を絞り込めます。 **できた?** 種別と領域で絞った Issue の一覧が出る。
できなかったら 一覧が空のときは、ラベルを1つ外して絞り込み直します。領域ラベルの名前は、Labels ページで確かめます。
## 手順2: マイルストーンから Phase の範囲を読む FloatSoda の開発は Phase 単位で進みます。Phase は、フレームワークとして何ができる段階かを表す到達点です。NuGet のバージョン番号とは対応しません。同じ Phase の間に、複数のバージョンが公開されることがあります。 各 Phase の範囲は、[GitHub のマイルストーン](https://github.com/sumx21t-3310/FloatSoda/milestones)の Issue にあります。Phase の一覧と大まかな内容は、[README.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/README.md) の「開発ステータス」にあります。Phase の完了条件や進み具合を束ねる Issue には、`tracking` のラベルが付きます。 関心のある Phase のマイルストーンを開いて、Issue を見ます。 **できた?** 「この Issue はどの Phase の範囲か」を言える。
できなかったら Issue にマイルストーンが付いていないときは、その Issue はどの Phase にも割り当てられていません。Issue のコメントで、関係する Phase を尋ねます。
## 手順3: Issue と PR の議論を追う 1つの Issue を、上のコメントから順に読みます。Issue から PR へは、PR の本文にある `Closes #NN` の形の関連づけで、たどれます。PR の本文は、[PR テンプレート](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.github/PULL_REQUEST_TEMPLATE.md)の欄(変更の概要、関連 Issue、テスト、残した作業など)で書かれています。欄を順に読むと、何を、なぜ変えたかが分かります。 議論は古くなります。古い Issue のコメントだけを根拠に、決まったことと見なさないでください。新しいコメントと、マージされた PR の本文で確かめます。閉じただけの PR は、採用されなかった案のこともあります。 **できた?** その Issue がいま何を待っているか(設計判断、実装、確認)を言える。
できなかったら 待っているものが読み取れないときは、Issue に、読み取れなかった点を質問として書きます。
## 手順4: 決まったことを探す 「これはもう決まったことか」を確かめるには、次の順に探します。 1. 関係する Issue のコメント 2. 決定の記録(ADR)。ADR の置き場所と番号の付け方は決まっていない。Flutter との差異は、今ある台帳 [known-divergences.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/references/known-divergences.md) で探す。一覧の入口は [決定の記録(ADR)の一覧](/contributor/2-understand/adr-index/)にある 3. 規約。[CONTRIBUTING.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md) と [REVIEW.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/REVIEW.md) リリースごとの変更は、[CHANGELOG.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CHANGELOG.md) にあります。 見つからないときは、提案として出します。書き方は [提案を書く](/contributor/6-propose/write-proposal/)にあります。 ## 手順5: 手伝える Issue を見つける 手が足りない Issue には `help-wanted`、始めやすい Issue には `good-first-issue` が付きます。得意なこと(ドキュメント、動作検証、コード)に合う種別や領域のラベルと組み合わせて、Issue を絞り込みます。 1件選んだら、作業を始める前に、Issue のコメントで担当したい旨を伝えます。ほかの人の作業と重ならずに済みます。 次の行き先は、貢献の種類によって分かれます。 - 貢献の種類を選び直す: [どんな貢献が助かるか](/contributor/1-get-involved/ways-to-help/) - 書く前に規約を読む: [コントリビューターの規約](/contributor/3-prepare/conventions/) ## うまくいかないとき 絞り込んでも Issue が見つからないときは、2つの行き先があります。 | 状況 | 行き先 | | :-- | :-- | | 手伝えることを提案したい | 提案として Issue を立てる([提案を書く](/contributor/6-propose/write-proposal/)) | | 質問がある | Issue に質問として書く | # テストの分け方(HMD が要る範囲) > FloatSoda のテストは、どこまで結合して動かすかで、単体・結合・描画・実機の4つの範囲に分かれます。範囲によって、動かす手段、速さ、HMD が要るかが決まります。 ## 範囲で分けると、何が決まるか FloatSoda のテストは、どこまで結合して動かすかで、単体・結合・描画・実機の4つの範囲に分かれます。範囲によって、動かす手段、速さ、HMD が要るかが決まります。 範囲を決めずにテストを書くと、HMD が要らない確認を実機に回して遅くしたり、実機でしか壊れないものを xunit で通ったことにしたりします。このページを読むと、確かめたいことを置く範囲を自分で選べます。置き場所の決め方は、最後の節にあります。 ## HMD なしで確かめられる3つの範囲 単体、結合、描画の3つは、`dotnet test` で動き、HMD も SteamVR も要りません。 | 範囲 | 対象 | 受け持つテストプロジェクト | | :-- | :-- | :-- | | 単体 | ジオメトリ型、RenderObject 1個、Widget 1個 | `tests/FloatSoda.Test`。Layer ツリーは `tests/FloatSoda.Rendering.Test` | | 結合 | `WidgetBinding` から `RenderPipeline` までを通したフロー。差分更新、入力の配送 | `tests/FloatSoda.Test` | | 描画 | ビットマップに描画した画素 | `tests/FloatSoda.Test`(`src/FloatSoda.Testing/` のビットマップ描画を使う) | CI が動かすテストプロジェクトは、`FloatSoda.Rendering.Test` と `FloatSoda.Test` の2つです。CI は SteamVR を使いません。 テスト名は `MemberName_条件_期待結果` の形で付けます。命名の規約は CONTRIBUTING.md の「テストの命名」にあります。何を検証するかの観点は、同じファイルの「テスト観点」が正本です。このページには写しません。 ## HMD が要る範囲 実機の範囲は、SteamVR が動いているときにしか観測できないものに使います。たとえば、オーバーレイのハンドル、コントローラーの座標、描画スレッドの GL コンテキストです。 実機の確認は、サンプルの `checks/items.jsonl` に書いた項目を、HMD を着けて1つずつ判定する形で行います。項目の生成は [`floatsoda-device-test-gen`](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/SKILL.md)、実行と yes/no の記録は [`floatsoda-device-test-run`](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-run/SKILL.md) が担います。 HMD を持っていない人は、実機の確認を、持っている人に頼る形になります。自分の変更が実機を要するのに HMD がないときは、PR に「実機で未確認」と書いて、確認を頼みます。出す前に確かめることは、[コントリビューターの規約](/contributor/3-prepare/conventions/)から案内しています。 動作検証の結果の報告は、[不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/)にあります。 CONTRIBUTING.md と skill には、実機のシナリオを置く `tests/FloatSoda.DeviceTest` という名前が出てきます。このディレクトリは、いまのリポジトリにはありません。 ## 期待値の決め方 範囲を選んだら、期待値をどう定義するかを選びます。4つあります。 | 決め方 | 期待値の定義 | 向いている対象 | | :-- | :-- | :-- | | 例示 | 入力に対する正確な出力を書く | サイズ計算、イベントの順序、画素の色 | | 不変条件 | 常に成り立つ性質を書く | ツリーの親子関係と深さ、dirty list の整合 | | ゴールデン | 保存した出力と比べる | 描画結果の意図しない変化 | | 差分 | Flutter の挙動を正解にして比べる | Flutter 由来の Widget / RenderObject | 差分が使えるのは、Flutter から観測できる挙動に限ります。性能や並行性は、Flutter が正解になりません。 Flutter 由来の部品では、Flutter の実装の分岐と公式テストを確かめて、検証するケースを決めます。この規約は、CONTRIBUTING.md の「テスト観点」にあります。 ## どこにテストを置くか決める 置き場所は、2つの基準で1つに決めます。 1. 同じ不具合を、より小さな範囲で観測できるなら、大きな範囲は使わない。実機は、実機でしか観測できないものにだけ使う 2. 選んだ範囲で期待値を定義できないなら、期待値の決め方を選び直す。それでも定義できない範囲は使わない。たとえば、性能は Flutter が正解にならないので、差分以外の決め方を選ぶ 画素の色なら、描画の範囲で例示を使います。ツリーの整合なら、単体か結合の範囲で不変条件を使います。オーバーレイのハンドルは、実機の範囲です。 docs と API の分かりやすさは、このページの範囲には入れません。LLM に docs だけを渡して実装させるジュニアコーダーテストで確かめます。CI には入れません。手順は [`floatsoda-junior-coder-test`](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-junior-coder-test/SKILL.md) にあります。 ## 次に読むページ 目的に合うほうへ進みます。 - テストの書き方の規約を引く: [コントリビューターの規約](/contributor/3-prepare/conventions/) - Flutter のテストを写す: [Flutter から移植する](/contributor/4-write/port-from-flutter/) # エージェントと一緒に作業する > この手順を終えると、Claude Code や Codex のようなコーディングエージェントが、FloatSoda の規約を読んで作業する準備が整います。エージェントに任せる範囲と、人間が確かめる箇所も決まります。手順は4つです。 ## この手順でできること この手順を終えると、Claude Code や Codex のようなコーディングエージェントが、FloatSoda の規約を読んで作業する準備が整います。エージェントに任せる範囲と、人間が確かめる箇所も決まります。手順は4つです。 エージェントが規約を読まないまま作業すると、ブランチ名、PR の範囲、日本語での出力といった決まりから外れた PR ができます。入口を先に教えておくと、差し戻しが減ります。このページの対象は「リポジトリでエージェントを使う」ことです。エージェント自体を作る手順は扱いません。 ## 前提条件 - 手元でビルドとテストが通っていること 通っていなければ、先に [開発環境を用意する](/contributor/3-prepare/dev-environment/)を済ませます。 ## 手順1: エージェントの入口ファイルを確かめる このリポジトリの共通指示は、リポジトリ直下の `AGENTS.md` にあります。Codex などの `AGENTS.md` を読むツールは、そのまま読みます。Claude Code は、`CLAUDE.md` の1行 `@AGENTS.md` を通して読みます。 プロジェクトの指示を足すときは、`AGENTS.md` に書きます。`CLAUDE.md` に書いた指示は、ほかのエージェントに届きません。 エージェントが `AGENTS.md` を読む状態で起動します。 **できた?** エージェントが読み込んだ指示の表示に、`AGENTS.md` が含まれている。表示のないツールでは、出力言語を尋ねて日本語と答えれば、補助の確認になる。
できなかったら 指示の表示に `AGENTS.md` がないとき、または日本語で答えないときは、エージェントが `AGENTS.md` を読めていない可能性があります。作業ディレクトリがリポジトリ直下か、`CLAUDE.md` が `@AGENTS.md` を含んでいるかを確かめます。
## 手順2: 規約の正本を確かめる 規約の正本は、開発規約の [CONTRIBUTING.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md) と、レビューの基準の [REVIEW.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/REVIEW.md) です。`AGENTS.md` は規約を写さず、この2つを参照しています。人間が変更しても、エージェントが変更しても、規約は同じです。 エージェントに、作業の前に2つを読ませます。どの節を読むかは [コントリビューターの規約](/contributor/3-prepare/conventions/)にあります。 ## 手順3: skill の位置を確かめる リポジトリの skill の正本は `.agents/skills/` です。`.claude/skills/` は、Claude Code が `.agents/skills/` を読めないために置いた派生のスタブです。frontmatter と正本へのポインタしか持たないので、直接編集しません。 どの skill が何に使えるかは、`.agents/skills/` の各 `SKILL.md` の description に書かれています。実機テストの生成と実行、ジュニアコーダーテスト、リリース、OpenVR API の参照などがあります。一覧は `.agents/skills/` のディレクトリで確かめます。 skill を足す・直す手順は、[skill を足す・直す](/contributor/4-write/skills/)にあります。 ## 手順4: 任せる範囲と、人間が確かめる箇所を決める エージェントに任せてよい範囲は、作業の種類で3段階に分かれます。 | 段階 | 作業 | 進め方 | | :-- | :-- | :-- | | 任せない | Flutter にない API の実装 | 人間が実装し、DevEx(使い心地)と設計を確かめる | | 部分的に任せる | バグの修正 | 最初は調査だけを頼む。結果を読んでから、実装まで頼むかを決める | | 任せてよい | Flutter のウィジェットの移植 | 全面的に任せてよい。Flutter のテストも移植する | 詳細は CONTRIBUTING.md の「[コーディングエージェントに任せる範囲](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md)」にあります。 実機の確認は、エージェントではなく人間が判定します。エージェントは実機テストの運転と記録を担います。PR の本文のうち、エージェントが書いた箇所は、人間が内容を確かめます。 レビューは三層で行われます。自動(CodeRabbit)、人間・エージェントによる PR レビュー、フェーズ末の Codex による監査です。層の分担は、REVIEW.md の「9. レビューの層と分担」にあります。 C# の解析と変更には、Rider の MCP が使えるなら使います。使えなければ、`dotnet` CLI と検索で続けます。ソフトウェアをエージェントが勝手に入れることはありません。 **できた?** 「エージェントがやること」と「自分が確かめること」を、1行ずつ言える。
できなかったら 決められないときは、最初に任せる作業を調査に絞ります。調査の結果を読んでから、実装を依頼するかを決めます。
作業の進め方は [コントリビューターの規約](/contributor/3-prepare/conventions/)に、PR を出したあとの対応は [レビューの指摘を受けて直す](/contributor/5-submit/address-review/)にあります。 ## うまくいかないとき エージェントが規約から外れた変更をしたときは、症状ごとに規約で確かめて、エージェントにやり直させます。 | 症状 | 確かめる規約 | | :-- | :-- | | ブランチ名に `codex/`、`claude/`、`agent/` のようなツール名が入った | CONTRIBUTING.md の「ブランチ名」に従って切り直す | | 無関係な変更が混ざった | CONTRIBUTING.md の「PR のスコープ」に従って別の PR に分ける | | `.claude/skills/` を直接編集した | CONTRIBUTING.md の「エージェント向け skill を追加・変更する場合」に従って `.agents/skills/` 側で直す | `git push --force` のように履歴を書き換える操作や、取り消せない操作は、実行する前に人間が確かめます。`main` へは直接 push せず、PR 経由にします(CONTRIBUTING.md の「ブランチ・PRフロー」)。 # Flutter から移植する > Flutter の Widget や RenderObject を FloatSoda に移植し、Flutter の観測できる挙動と合った状態で PR に出せます。挙動が合わない箇所を残す場合は、理由とテストつきで記録されている状態になります。手順は5つです。 ## この手順でできること Flutter の Widget や RenderObject を FloatSoda に移植し、Flutter の観測できる挙動と合った状態で PR に出せます。挙動が合わない箇所を残す場合は、理由とテストつきで記録されている状態になります。手順は5つです。 1. Flutter の元の実装を見つける 2. どこまでそろえるかを決める 3. 差異を残すなら、記録する 4. Flutter のテストを FloatSoda のテストに写す 5. C# として自然な書き方に置き換える 先に、移植したい部品が Flutter 由来かを確かめてください。Flutter に対応する概念がない部品は、このページの対象外です。 ## 前提条件 次の2つがそろっていれば、手順1から始められます。 - 開発環境と Flutter の参照クローンを用意しておくこと([開発環境を用意する](/contributor/3-prepare/dev-environment/)) - 移植の Issue を立てておくこと([コントリビューターの規約](/contributor/3-prepare/conventions/)) Flutter との差異の判断は、Issue が必須な変更に含まれます。 ## 手順1: Flutter の元の実装を見つける 移植したい部品について、対応する Widget と、関係する Element・RenderObject の定義位置を、Flutter の版とあわせて控えます。部品専用の Element や RenderObject がないときは、継承元や委譲先の定義位置を控えます。 Flutter の元の実装は、ローカルの参照クローンで引きます(CONTRIBUTING.md の「テスト観点」では `~/code_reading/flutter_reference`)。Widget は `packages/flutter/lib/src/widgets/`、RenderObject は `packages/flutter/lib/src/rendering/` にあります。Widget / Element / RenderObject の位置は、参照クローンでクラス名を検索して特定します。`flutter-widget-source` skill でも特定できますが、この skill は `.agents/skills/` にありません。 版は、参照クローンで `git rev-parse HEAD` を実行して commit を控えます。 **できた?** Widget、Element、RenderObject の定義位置(専用の定義がなければ継承元や委譲先)と、Flutter の commit を書き出せます。 ## 手順2: どこまでそろえるかを決める Flutter 由来の Widget と RenderObject は、明示された理由がない限り、観測できる挙動に差異を作りません。そろえる対象は次のとおりです(REVIEW.md の「6. Flutter parity」)。 - プロパティの意味 - 既定値 - レイアウト - 描画とクリップ - ヒットテスト - 子の扱い - Widget の更新 - 不正・退化入力の扱い - dirty の条件 - Element と State のライフサイクル 「実装しやすい」「こちらの方が安全」「こちらの方が自然」だけは、差異の理由になりません。Flutter を期待して書く利用者と LLM が、その差異で困るためです。 仕様が食い違うときは、次の順に従います(REVIEW.md の「3. 仕様の一次情報と優先順位」)。 1. FloatSoda で明示された差異・設計判断 2. Flutter の仕様・実装・公式テスト 3. 既存の FloatSoda 実装(根拠になりません) Semantics のように、すでに移植しないと決めたものは、parity を理由に破りません。 **できた?** 「差異なし」か「差異あり、理由は〜」を一文で言えます。差異なしなら手順4へ、差異ありなら手順3へ進みます。 Flutter 由来かどうかの判断が割れるときは、実装を始める前に Issue で基準を明示してください。 ## 手順3: 差異を残すなら、記録する 差異を残すときは、次の5項目を記録します(REVIEW.md の「6. Flutter parity」)。 | 項目 | 書くこと | | :-- | :-- | | Flutter の挙動 | Flutter が何をするか。対応するファイルつき | | FloatSoda の挙動 | FloatSoda が何をするか。`src/` のファイルと行つき | | 差異が必要な理由 | 手順2の「理由にならないもの」以外の理由 | | 差異を固定するテスト | 差異を戻すと失敗するテストのファイルとメソッド名 | | 利用者向けのドキュメント | 利用者から観測できる場合の、docs やサンプルの `## Flutterとの違い` 節 | 差異を固定するテストがないと、次の移植で差異が戻されます。テストは省かないでください。 記録先は、差異の台帳 [known-divergences.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/references/known-divergences.md) です。エントリの形と、`deliberate` / `not ported` / `port mistake` / `unlabelled` のラベルの意味は、台帳の冒頭にあります。台帳を ADR にまとめる移行と、ADR の置き場所と番号の付け方は、決まっていません。決まるまでは、台帳へ追記してください。設計判断として ADR も書く場合は [決定を ADR に書く](/contributor/6-propose/write-adr/) を読んでください。 台帳の `Observation` 欄は、差異が HMD なしで観測できるか(`HEADLESS`)、SteamVR が要るか(`VR`)を示します。HMD が要る範囲の分け方は [テストの分け方](/contributor/3-prepare/test-strategy/) にあります。 **できた?** 5項目がすべて埋まり、差異を戻すと落ちるテストがあります。 ## 手順4: Flutter のテストを FloatSoda のテストに写す Flutter 由来の Widget と RenderObject のテストは、FloatSoda 独自にケースを想像するだけで済ませません(CONTRIBUTING.md の「テスト観点」)。Flutter の実装の分岐、公式テスト、API contract を確かめ、対応する重要なケースを FloatSoda 側でも検証します。Flutter が明示的にテストしている退化ケースは、FloatSoda でも壊れます。 Flutter の公式テストは、参照クローンの `packages/flutter/test/widgets/` と `packages/flutter/test/rendering/` にあります。FloatSoda 側のテストは、`tests/FloatSoda.Test` の `Widgets/` と `RenderObjects/` に置きます。 写したテストの名前は `対象メンバー名_条件_期待結果` の形にし、条件と期待結果を日本語で書きます(CONTRIBUTING.md の「テストの命名」)。バグ修正の場合は、修正を戻すと落ちるテストを足します。 **できた?** Flutter のテストのケースと、FloatSoda のテストの対応を表にできます。 ## 手順5: C# として自然な書き方に置き換える 次の置き換えは、差異に数えません(REVIEW.md の「6. Flutter parity」)。 - `event` - `init` / `required` - `record` / `record struct` - .NET 標準機構の利用 迷ったら、利用者から見た挙動が変わるかで切り分けます。変わらなければ置き換えです。変わるなら、手順2に戻って差異として扱います。 **できた?** 置き換えの前後で、観測できる挙動が同じです。 ## PR に書くこと PR の本文は、[PR テンプレート](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.github/PULL_REQUEST_TEMPLATE.md)の「Flutter reference」欄に次の4つを書きます。PR から、Flutter の版、参照した実装、参照したテストを追跡できるようにするためです。 - Flutter version / commit - Referenced source - Referenced tests - Behavioral differences from Flutter(差異がなければ「なし(parity)」) 出す前のチェックは [コントリビューターの規約](/contributor/3-prepare/conventions/) にあります。出したあとの指摘への対応は [レビューの指摘を受けて直す](/contributor/5-submit/address-review/) に進んでください。 ## うまくいかないとき
Flutter とどちらを正にするか迷う 手順2に戻り、優先順位の表で確かめます。FloatSoda で明示された差異がなければ、Flutter が正です。既存の FloatSoda 実装は根拠になりません。
差異の記録の書き方が分からない 手順3の5項目と、台帳の既存エントリを見比べてください。
判断が割れるときは、Issue で相談してください。 # skill を足す・直す > エージェント向けの skill を、正しい場所と名前で足せます。手順が終わると、.agents/skills//SKILL.md に正本があり、.claude/skills//SKILL.md に正本へのポインタだけのスタブがある状態になります。 ## この手順でできること エージェント向けの skill を、正しい場所と名前で足せます。手順が終わると、`.agents/skills//SKILL.md` に正本があり、`.claude/skills//SKILL.md` に正本へのポインタだけのスタブがある状態になります。 正本を1か所に決めているのは、コピーが2つあると片方だけ更新されてずれるためです。skill の唯一の一次情報は `.agents/skills/` で、追加も変更も必ずこちらから行います(CONTRIBUTING.md の「エージェント向け skill を追加・変更する場合」)。`.claude/skills/` は、`.agents/skills/` を読まない Claude Code のための派生物です。直接編集しません。 手順は5つです。 1. 名前を決める 2. 正本を書く 3. Claude Code 用のスタブを置く 4. 言語を確かめる 5. PR を出す 自分の作業が skill に向くかは、エージェントに繰り返し渡す手順書にしたいかで判断してください。 ## 前提条件 リポジトリをクローンしていれば始められます。雛形にする skill を、`.agents/skills/` から1つ選んでください。たとえば `floatsoda-release` は、正本が `.agents/skills/floatsoda-release/`、スタブが `.claude/skills/floatsoda-release/SKILL.md` にあります。 エージェントと一緒に作業する場合は、[エージェントと一緒に作業する](/contributor/3-prepare/work-with-agents/) も読んでください。 ## 手順1: 名前を決める 名前は `--` の順にします。例は `floatsoda-device-test-gen` と `floatsoda-device-test-run` です。広い語を先に、動詞を最後に置くと、対になる skill が一覧で隣り合います。 skill のディレクトリはネストできません。Claude Code は `.claude/skills/*/SKILL.md` の1階層だけを探すためです。分類は、ディレクトリではなく名前の接頭辞で表します。 **できた?** 名前が `floatsoda-` で始まり、最後が動詞です。 ## 手順2: 正本を書く `.agents/skills//SKILL.md` に本体を書きます。参照ファイルは `.agents/skills//references/` に置きます。 frontmatter の `description` は、エージェントが skill を使うかを判定する材料です。日本語で書き、トリガーのキーフレーズは日本語と英語の両方を含めます(例: 「実機テスト」と "device test")。 **できた?** `.agents/skills//SKILL.md` があり、frontmatter が入っています。 ## 手順3: Claude Code 用のスタブを置く `.claude/skills//SKILL.md` に、正本と同じ frontmatter と、正本へのポインタだけを置きます。既存のスタブ(たとえば `.claude/skills/floatsoda-release/SKILL.md`)を雛形にしてください。 スタブの本文に手順を足したくなったら、足す先は正本です。スタブは固有の手順を持ちません。 **できた?** スタブの frontmatter が正本と一致し、本文は正本へのポインタだけです。 ## 手順4: 言語を確かめる エージェント向けファイルの言語は、次のとおりです(CONTRIBUTING.md の「エージェント向けファイルの言語」)。 - `AGENTS.md` を英語で書くこと - skill の本文と `references/` を日本語で書くこと - 識別子、型名、パス、コマンド、コードを英語のままにすること **できた?** 本文が日本語で、識別子とコマンドが原文のままです。 ## 手順5: PR を出す 今回対象の skill の追加・変更と、それに伴うスタブや参照ファイルの更新だけを PR に入れます。ブランチ名の規約は [コントリビューターの規約](/contributor/3-prepare/conventions/) から案内しています。出したあとの指摘への対応は [レビューの指摘を受けて直す](/contributor/5-submit/address-review/) に進んでください。 ## 既存の skill を直す場合 直す場所は `.agents/skills/` の正本です。frontmatter を変えたときは、スタブの frontmatter も同じに直します。 **できた?** 正本とスタブの frontmatter が一致しています。 ## うまくいかないとき
skill が発火しない 正本の `description` のトリガーのキーフレーズを確かめてください。日本語と英語の両方が入っているかを見ます。
正本とスタブがずれた 正本に合わせて、スタブを直してください。
# レビューの指摘を受けて直す > PR についたレビューの指摘を、重さで見分けて、直すものと返すものに分けられます。手順が終わると、blocking の指摘が残っておらず、すべての指摘に「直した」または理由つきで「直さない」という返事がある状態になります。手順は4つです。 ## この手順でできること PR についたレビューの指摘を、重さで見分けて、直すものと返すものに分けられます。手順が終わると、blocking の指摘が残っておらず、すべての指摘に「直した」または理由つきで「直さない」という返事がある状態になります。手順は4つです。 1. 指摘を重要度で分ける 2. blocking の指摘を直す 3. 非 blocking とスコープ外の指摘に返事をする 4. 直した差分を確かめて、返事を書く 指摘の重さが分からないまま全部を直すと、PR が膨らみます。重い指摘を見落とすこともあります。そのため、先に重さの基準を使って分けます。基準の正本は [REVIEW.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/REVIEW.md) です。このページは使い方だけを書きます。 ## 前提条件 次の2つがそろっていれば、手順1から始められます。 - 手元でビルドとテストが通っていること([開発環境を用意する](/contributor/3-prepare/dev-environment/)) - レビューの基準の正本 REVIEW.md の場所を確かめておくこと([コントリビューターの規約](/contributor/3-prepare/conventions/) に案内があります) ## 手順1: 指摘を重要度で分ける 指摘を「blocking」と「非 blocking」に分けます。非 blocking のうち、PR の範囲外のものは「別 Issue 向け」とします。 重要度は8段あり、上ほど重くなります。順序と各段の内容は、REVIEW.md の「1. 重要度の順序」を開いて確かめます。このページには写しません。 blocking になるのは、重要度が上位5段(1〜5)で、concrete failure mode を説明できる指摘です。concrete failure mode とは、どの入力や状態で何が壊れるかのことです。下位3段(6〜8)は、原則として非 blocking の提案です。concrete failure mode を書けない指摘は、書けるようになるまで格下げされます(REVIEW.md の「2. finding の基準」)。 スコープ外の改善要求は、「別 Issue 向け」と明示した非 blocking として伝えられます。 自動レビュー(CodeRabbit)は blocking の判断をしません。`.coderabbit.yaml` で `request_changes_workflow: false` にしてあり、blocking の判断は人間・エージェントによる PR レビューが行います(REVIEW.md の「9. レビューの層と分担」)。 **できた?** すべての指摘が、blocking か非 blocking のどちらかに入っています。範囲外の指摘には、「別 Issue 向け」と書いてあります。 ## 手順2: blocking の指摘を直す 直す範囲は、指摘に絞ります。無関係なリファクタリング、リネーム、クリーンアップ、依存の追加・削除は、同じ PR に混ぜません([コントリビューターの規約](/contributor/3-prepare/conventions/) の「PR のスコープ」)。 バグ修正には regression test が要ります。判定基準は、修正を戻すとテストが落ちるかです(REVIEW.md の「5. テスト」)。 **できた?** blocking の指摘ごとに、直した内容と、解消を確かめた根拠(再現コードか回帰テストの結果)を言えます。 **できた?** 手元で CI と同じ手順が通ります([CI が落ちたとき](/contributor/5-submit/ci-failures/) の手順2にコマンドがあります)。 指摘に同意できないときは、返事の前に、concrete failure mode が本当にあるかを再現コードで確かめます。既存のコードがそうなっているという理由だけでは、正しい仕様とは判断されません。仕様が食い違うときは、FloatSoda で明示された差異・設計判断、Flutter の仕様、の順に確かめます。既存の実装は根拠になりません(REVIEW.md の「3. 仕様の一次情報と優先順位」)。 ## 手順3: 非 blocking とスコープ外の指摘に返事をする 残りの指摘に、1つずつ返事を書きます。 - 非 blocking: 直す、または直さない理由を書く - 別 Issue 向け: Issue を立ててリンクを書く、または立てない理由を書く **できた?** 返事がない指摘が残っていません。 ## 手順4: 直した差分を確かめて、返事を書く 差分が、指摘への対応だけになっているかを確かめます。そのうえで、PR 本文の「Tests」と「Scope / follow-up」の欄を、今の差分に合わせて更新します。最後に、PR のコメントへ、直した旨と変更点を書きます。 **できた?** PR 本文が今の差分と合っています。 CI が落ちたときは [CI が落ちたとき](/contributor/5-submit/ci-failures/) に進んでください。 ## うまくいかないとき
指摘の意味が分からない 指摘の根拠を、PR のコメントで尋ねます。どの入力や状態で何が壊れるか、を聞いてください。
指摘に同意できない 手順2の再現コードで確かめます。確かめたうえでも判断が割れるときは、PR のコメントで相談してください。
# CI が落ちたとき > PR の CI が落ちたときに、落ちたステップを見つけ、手元で同じ失敗を再現して直せます。手順が終わると、手元で CI と同じコマンドが通り、CI も通る状態になります。手順は4つです。 ## この手順でできること PR の CI が落ちたときに、落ちたステップを見つけ、手元で同じ失敗を再現して直せます。手順が終わると、手元で CI と同じコマンドが通り、CI も通る状態になります。手順は4つです。 1. 落ちたステップを特定する 2. 手元で同じコマンドを実行する 3. 原因を切り分ける 4. 直して確かめる コード側の CI は [ci.yml](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.github/workflows/ci.yml) です。動く契機と実行環境は ci.yml で確かめます。ステップは次のとおりです。この表と ci.yml が食い違うときは、ci.yml に合わせます。 | ステップ | コマンド | | :-- | :-- | | Build | `dotnet build --configuration Release` | | Test (FloatSoda.Rendering.Test) | `dotnet test tests/FloatSoda.Rendering.Test --configuration Release --no-build` | | Test (FloatSoda.Test) | `dotnet test tests/FloatSoda.Test --configuration Release --no-build` | CI は SteamVR を使いません。実機でしか分からない失敗は、CI には出ません。CI が通っても、実機が要る変更は別に実機で確かめる必要があります。 ## 前提条件 - 手元のクローンでビルドとテストを実行できること 実行できなければ、先に [開発環境を用意する](/contributor/3-prepare/dev-environment/)を済ませてください。 ## 手順1: 落ちたステップを特定する PR の Checks から CI のログを開き、落ちたステップの名前と、エラーの最初の行を控えます。 **できた?** 「ビルドで落ちた」「どのテストで落ちた」を言えます。 ## 手順2: 手元で同じコマンドを実行する 上の表のうち、落ちたステップのコマンドを、そのまま手元で実行します。PR を出す前に手元で通すのは規約です([コントリビューターの規約](/contributor/3-prepare/conventions/) から案内している CONTRIBUTING.md の「ブランチ・PRフロー」)。 Debug ではなく Release で実行してください。CI が Release 構成で動くためです。テストのステップは `--no-build` を付けるので、先に `dotnet build --configuration Release` を実行しておきます。 **できた?** 手元でも同じ失敗が出ます。再現できたら手順3へ、再現できなければ「うまくいかないとき」へ進みます。 ## 手順3: 原因を切り分ける 再現した失敗を、自分の変更、既存の問題、環境のどれかに分けます。 - ビルドの失敗: ログの `error` の行にある診断 ID と位置を読みます。コードスタイルの診断(`IDE0161` など)は `.editorconfig` で `warning` にしてあり、警告だけではビルドは失敗しません。警告が出ていれば、合わせて直します。namespace とディレクトリの対応は診断では見つからないので、CONTRIBUTING.md の「namespace とディレクトリ」で確かめてください - テストの失敗: どのテストかを特定し、期待値と実際の値を見ます **できた?** 原因を一文で言えます。 ## 手順4: 直して確かめる 直す範囲は、原因に絞ります。無関係な変更は混ぜません。バグなら、修正を戻すと落ちるテストを足します。 手元で CI と同じコマンドを通してから push してください。 **できた?** 手元でコマンドがすべて通り、CI も通ります。 既存のテストが落ちたときは、仕様と実装のどちらが期待値に合っていないかを調べます。通すためだけにテストを消さないでください。直せないときは、理由を PR に書きます。 ## docs サイトの検査が落ちたとき `docs/` か `website/` を触った PR でサイトの検査が落ちたときは、[website/README.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/website/README.md) を開いてください。 ## うまくいかないとき 手元で再現しない失敗は、環境の差を疑います。失敗した CI のログで、実際の OS、.NET SDK の版、ビルド構成(Release か Debug か)を確かめ、手元の値と1つずつ比べて揃えてください。 揃えても再現しないときは、CI のログを PR に貼って相談してください。 # 決定を ADR に書く > 議論で決まった内容を、あとから「なぜそう決めたか」を読める形で ADR(Architecture Decision Record、設計の決定の記録)に書けます。手順が終わると、ADR が1本、置き場所にある状態になります。ADR には、背景、決定、理由、影響が書かれます。Flutter との差異なら、差異の記録に必要… ## この手順でできること 議論で決まった内容を、あとから「なぜそう決めたか」を読める形で ADR(Architecture Decision Record、設計の決定の記録)に書けます。手順が終わると、ADR が1本、置き場所にある状態になります。ADR には、背景、決定、理由、影響が書かれます。Flutter との差異なら、差異の記録に必要な5項目も書かれます。 決定の理由が議論のコメントにしかないと、あとから「なぜこうなっているか」を追えません。同じ議論も繰り返されます。ADR は、その理由を repo に残すための記録です。ADR の正本は repo に置きます。 手順は5つです。 1. ADR に書く決定かを確かめる 2. 番号と置き場所を決める 3. 背景・決定・理由・影響を書く 4. Flutter との差異なら、5項目を足す 5. PR で出す 議論の結論が出ていない場合は、先に [提案を書く](/contributor/6-propose/write-proposal/) へ戻ってください。 ## 前提条件 次の2つがそろっていれば、手順1から始められます。 - 決まった内容と、議論の Issue を確認しておくこと - 提案の書き方と規約を読んでおくこと([提案を書く](/contributor/6-propose/write-proposal/)、[コントリビューターの規約](/contributor/3-prepare/conventions/)) ## 手順1: ADR に書く決定かを確かめる 次のような決定は、ADR に書きます。 - 設計判断 - 依存やプロジェクト構成の決定 - Flutter との差異 - public API の方針 typo の修正のように、設計判断のない変更は書きません。迷ったら書いてください。Issue が必須な変更([CONTRIBUTING.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md) の「Issue が必須な変更」)のうち、設計判断を伴うものが目安です。 **できた?** 「書く」「書かない」のどちらかを、理由とあわせて言えます。書くなら手順2へ進みます。 ## 手順2: 番号と置き場所を決める ADR の置き場所と番号の付け方は、まだ決まっていません。決まるまでは、ADR を足す前に、議論の Issue で置き場所をたずねてください。 今ある Flutter との差異は、差異の台帳 [known-divergences.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/references/known-divergences.md) に記録されています。入口は [決定の記録(ADR)の一覧](/contributor/2-understand/adr-index/) です。 **できた?** 置き場所と番号が、Issue で確認できています。 ## 手順3: 背景・決定・理由・影響を書く ADR には、次の4つの欄を書きます。 | 欄 | 書くこと | | :-- | :-- | | 背景 | 決める前の状況 | | 決定 | 選んだもの | | 理由 | 選ばなかった案と、選んだ理由 | | 影響 | 決めたことで生じる制約や代償 | 議論の記録を見ながら、欄ごとに書きます。 **できた?** 理由の欄に、実際に検討した案と選んだ理由が書かれています。別の案を検討していない場合は、その旨が書かれています。 ## 手順4: Flutter との差異なら、5項目を足す Flutter と違う設計の決定には、記録必須の5項目を書きます。5項目の内容は、REVIEW.md の「6. Flutter parity」にあります。このページには写しません。5項目を背景・決定・理由・影響のどの欄に書くかは、決めていません。 差異を固定するテストがない差異は、次の移植で戻されます。テストの名前を必ず書いてください。利用者に見える差異は、利用者向けの docs にも書きます。 差異の台帳 [known-divergences.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/references/known-divergences.md) に同じ差異のエントリがあるときは、重複させないでください。どちらを正にするかを、Issue でオーナーに確かめます。台帳を ADR にまとめる移行は、まだ決まっていません。 **できた?** 5項目がすべて埋まり、テストが実在します。 ## 手順5: PR で出す PR には ADR の追加だけを入れます。関連する Issue は、PR 本文の「Related Issue」欄に `#NN` の形で結び付けます。この PR だけで Issue の完了条件を満たすときは、`Closes #NN` と書きます。出したあとの指摘への対応は [レビューの指摘を受けて直す](/contributor/5-submit/address-review/) に進んでください。 ## ADR を読むとき 「なぜこう作ったのか」を調べるときは、ADR の4つの欄を上から読みます。背景で当時の状況を、決定で選んだものを、理由で選ばなかった案を確かめます。ADR の置き場所と番号の付け方は、決まっていません。Flutter との差異は、差異の台帳 [known-divergences.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.agents/skills/floatsoda-device-test-gen/references/known-divergences.md) で探します。入口は [決定の記録(ADR)の一覧](/contributor/2-understand/adr-index/) にあります。 ## うまくいかないとき
何が決定か分からない、または決定がまだない [提案を書く](/contributor/6-propose/write-proposal/) に戻り、議論の結論を Issue にまとめてください。
# 提案を書く > 新しい機能や設計の変更を、議論に入れる形で Issue に書けます。手順が終わると、提案の Issue が1件立っている状態になります。Issue には、提案が受け入れ範囲に入ること、どの利用者のどんな困りごとを解くか、Flutter と違うならその理由が書かれています。 ## この手順でできること 新しい機能や設計の変更を、議論に入れる形で Issue に書けます。手順が終わると、提案の Issue が1件立っている状態になります。Issue には、提案が受け入れ範囲に入ること、どの利用者のどんな困りごとを解くか、Flutter と違うならその理由が書かれています。 受け入れ範囲の外の提案や、理由の書かれていない提案は、議論に入る前に止まります。必要な情報を先に書くと、議論を設計の中身に使えます。 手順は5つです。 1. 似た議論を探す 2. 受け入れ範囲に入るか確かめる 3. 利用者のペルソナに照らして、困りごとを書く 4. Flutter と違う設計なら、差異の5項目を書く 5. Issue を出す 提案が通ったあとに決定を記録する手順は、[決定を ADR に書く](/contributor/6-propose/write-adr/) にあります。 ## 前提条件 - GitHub のアカウントを用意しておくこと - [FloatSoda が大事にしていること](/contributor/1-get-involved/values/)を読んでおくことが望ましい - [Issue・PR・マイルストーンの読み方](/contributor/3-prepare/read-github/)を読んでおくことが望ましい 後ろの2つを読んでおくと、手順1から迷わず進められます。 ## 手順1: 似た議論を探す 案のキーワードで、Issue と PR を検索します。閉じた Issue も見てください。古い Issue だけを根拠に決めないでください。Issue が書かれた時点の前提が、今も成り立つとは限りません。 **できた?** 似た議論が「ある」か「ない」かを言えます。ない場合と、あっても新しい論点がある場合は、手順2へ進みます。 ## 手順2: 受け入れ範囲に入るか確かめる 現段階のコアが受け入れるのは、Flutter の `widgets/basic.dart` に相当するプリミティブ層のウィジェットです。単一の RenderObject への薄いラッパーで、状態やビジネスロジックを持たないものを指します。`Button` や `Card` のような装飾済み・複合ウィジェットや、Material / Cupertino 相当のコンポーネントは受け入れません。範囲外の提案は、コア以外の別パッケージやユーザーランドでの実装を検討してください。 新しい public API は、さらに受け入れ条件があります。合否基準は、更新後の docs だけを読んだ LLM が、その API を正しく使えるかです。 範囲の正本は CONTRIBUTING.md の「受け入れ範囲」と「新しい public API を追加する場合の受け入れ条件」です([コントリビューターの規約](/contributor/3-prepare/conventions/) から案内しています)。 **できた?** 「入る」「入らない」「迷う」のどれかを言えます。迷うときは、Issue で相談してください。 ## 手順3: 利用者のペルソナに照らして、困りごとを書く FloatSoda が想定する作り手は3種類です([AGENTS.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/AGENTS.md) の「Target Users」)。 | ペルソナ | 特徴 | | :-- | :-- | | LLM にコードを書かせる VRChatter | 自分ではほとんどコードを書かず、LLM に docs と API を読ませる | | Booth でオーバーレイを売るクリエイター | Unity のプログラマー。uGUI しか知らず、宣言的 UI は初めて | | uGUI を避けたいエンジニア | シーンやプレハブが diff やレビューに向かないことが悩み | 提案は、3種類のどれの困りごとを解くかで説明します。書くのは次の3点です。 - だれの - どんな困りごとを - この提案がどう解くか 外部のアセットや設定ファイルのように、コードに書けない状態を入れる提案は、トレードオフを検討してから書いてください。UI のすべてを C# のコードに書けることが、FloatSoda の見せ場だからです。 **できた?** ペルソナを1つ挙げて、その人の困りごとを一文で言えます。Flutter 由来の部品で、Flutter と違う設計にするなら手順4へ、そうでなければ手順5へ進みます。 ## 手順4: Flutter と違う設計なら、差異の5項目を書く Flutter と違う設計にするときは、記録必須の5項目を、分かる範囲で書きます(REVIEW.md の「6. Flutter parity」)。 - Flutter の挙動 - FloatSoda の挙動 - 差異が必要な理由 - 差異を固定するテスト - 利用者に影響する場合のドキュメント 提案の段階でまだ存在しない項目(差異を固定するテスト、docs の更新)は「計画」と書きます。名前やリンクは、実在するものにだけ付けます。 「実装しやすい」「こちらの方が安全」「こちらの方が自然」だけでは、差異の理由になりません。Flutter の元の実装の読み方は [Flutter から移植する](/contributor/4-write/port-from-flutter/) にあります。提案が通ったら、決定を [決定を ADR に書く](/contributor/6-propose/write-adr/) で記録します。 **できた?** 差異が必要な理由を、制約や利用者の困りごとと結び付けて説明できます。 ## 手順5: Issue を出す 機能要望のテンプレート [.github/ISSUE_TEMPLATE/feature_request.yml](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/.github/ISSUE_TEMPLATE/feature_request.yml) に書いて出します。欄の一覧と必須の別は、テンプレートを開いて確かめます。「動機 / ユースケース」には手順3の3点を、「対応する Flutter API」には手順4の Flutter 側の情報を書きます。 種別ラベルは、機能要望のテンプレートが `feature` を付けます。Flutter と違う設計なら、`flutter-divergence` も必要です。ラベルを付ける権限がない場合は、付けたいラベルを Issue に書いて頼みます。public API や observable behavior を変える提案は、breaking change の有無も Issue に書いてください。Alpha であることは、判定を省略する理由になりません。 **できた?** Issue が立ち、種別ラベルが1つ付いています。 議論で決まったら、[決定を ADR に書く](/contributor/6-propose/write-adr/) に進んでください。 ## うまくいかないとき
範囲外と言われた コア以外の別パッケージやユーザーランドでの実装を検討してください。
理由が足りないと言われた 手順3と手順4に戻り、困りごとの3点と、差異の5項目を書き足してください。
# ガイド > 探す・採用する・準備する・作る・困ったとき・配る・メンテナンスするの順に、目的の手順と仕組みを引く ガイドは、FloatSoda を知ってから、作ったツールを配って保守するまでを案内するタブです。段階ごとに7つのグループがあるので、自分の段階に近いグループのページを選んでください。 ## ガイドの読み方 グループは時間の順に並んでいます。「1. 探す」から「7. メンテナンスする」までのうち、自分の段階に近いグループの節へ進んでください。 各グループの中は、手順を渡すページ(How-to)を先に、考え方を説明するページ(解説)を後に並べています。部品の一覧や制約の一覧のような調べ物は、ガイドの外のリファレンスにあります。ガイドからは、必要な場面でリンクします。最初の1本を手を動かして作るチュートリアルも別のタブにあります。 ## 1. 探す FloatSoda をまだ知らない、または名前だけ知っている段階です。自分の目的に合うかを確かめます。 - [FloatSoda とは](/guide/1-discover/what-is-floatsoda/): FloatSoda が何で、どんな人が何を作れるか - [オーバーレイとは](/guide/1-discover/what-is-an-overlay/): SteamVR のオーバーレイが何で、ゲームの画面とどう違うか 作例を見たいときは、リファレンスの[作例のカタログ](/reference/sample-catalog/)を開いてください。確かめたら、次は「2. 採用する」です。 ## 2. 採用する 使うかどうかを決める段階です。 - [ほかの作り方と比べる](/guide/2-adopt/compare-approaches/): uGUI で作る場合や OpenVR を直接呼ぶ場合と比べて、何が楽になり、何を引き受けるか 作りたいものが今できるかは、リファレンスの[できないこと・まだ使えないもの](/reference/limitations/)で確かめられます。使うと決めたら「3. 準備する」へ進みます。 ## 3. 準備する 使うと決めて、書き始める前に整える段階です。自分の経験に合うページを選んでください。 | 自分の状況 | 開くページ | | :-- | :-- | | PC に .NET を入れて、動かせる状態にしたい | [開発環境を用意する](/guide/3-prepare/set-up-environment/) | | コードは AI に書かせたい | [AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/) | | VRChat と組み合わせたい | [VRChat と組み合わせるときに確かめること](/guide/3-prepare/vrchat-checklist/) | | Unity の uGUI で UI を作ってきた | [uGUI から来た人へ](/guide/3-prepare/from-ugui/) | | C# がほぼ初めてで、AI が書いたコードを読みたい | [C# がほぼ初めての人へ](/guide/3-prepare/csharp-primer/) | Flutter から来た人は、リファレンスの[Flutter の読み替え表](/reference/flutter-mapping/)が近道です。用語は[用語集](/reference/glossary/)、対応する環境は[動作環境](/reference/requirements/)にあります。準備ができたら、チュートリアル[最初のオーバーレイを作る](/tutorials/first-overlay/)で最初の1本を作り、次に「4. 作る」へ進みます。 ## 4. 作る 最初の1本を終えて、作りたいものがある段階です。作る順(見た目、動き、操作、確かめ、起動)に並べています。 **画面を組み立てる** - [見た目を整える](/guide/4-build/screen/style-appearance/) - [アニメーションを付ける](/guide/4-build/screen/add-animation/) - [ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/) - [自分の部品を作る](/guide/4-build/screen/make-your-own-widget/) - [表情切り替えパネルを作る](/guide/4-build/screen/expression-switcher/) **入力を受け取り、確かめる** - [コントローラーの入力を受け取る](/guide/4-build/input/controller-input/) - [HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/) **仕組みを知り、足す** - [設定やログ、裏で動く処理を足す](/guide/4-build/mechanics/use-generic-host/) - [状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/) - [オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/) - [FloatSoda の仕組み(使う人向け)](/guide/4-build/mechanics/how-floatsoda-works/) - [起動と Generic Host](/guide/4-build/mechanics/startup-and-host/) 使いたい部品は、リファレンスの[部品を目的から探す](/reference/find-widgets/)で探せます。詰まったら「5. 困ったとき」、できたら「6. 配る」へ進みます。 ## 5. 困ったとき エラーが出る、反応しない、思った大きさにならない、重い、といった症状が出た段階です。症状に合うページを選んでください。 - [エラーを読んで直す](/guide/5-troubleshoot/read-errors/) - [反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/) - [負荷を抑える](/guide/5-troubleshoot/reduce-load/) - [レイアウトの考え方](/guide/5-troubleshoot/layout-model/): 思った大きさや位置にならないとき Flutter の書き方で動かないときは、[Flutter の読み替え表](/reference/flutter-mapping/)を開いてください。ここで解けなければ、「docs で見つからないとき」へ進みます。 ## 6. 配る 動くツールができて、人に渡す、売る、自動で起動する段階です。 - [exe にして配る](/guide/6-distribute/publish-exe/) - [Booth で売る準備](/guide/6-distribute/booth-release/) - [SteamVR と一緒に起動する](/guide/6-distribute/launch-with-steamvr/) 配ったあとは「7. メンテナンスする」へ進みます。 ## 7. メンテナンスする 配った相手の環境で動かない、または久しぶりに自分のツールを触る段階です。 - [利用者の環境で動かないとき](/guide/7-maintain/user-environment-issues/) - [画面と状態の分け方](/guide/7-maintain/separate-ui-and-state/): 直しやすい形に整えるとき 久しぶりに触るときは、チュートリアルで手順を、リファレンスで API と制約を確かめてください。FloatSoda の版を上げるときは、[CHANGELOG.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CHANGELOG.md) も読んでください。直した版を届ける手順は、[exe にして配る](/guide/6-distribute/publish-exe/)の後半にあります。 ## docs で見つからないとき docs を読んでも解けない問題は、次の2か所に持ち込めます。 - GitHub の Issue: バグの報告と機能の要望を受け付けています。使い方や設計の相談も Issue に書いてください。GitHub の Discussions は有効にしていません。 - AI への質問: 公開サイトの `llms.txt`(索引)か `llms-full.txt`(全文)を AI に渡して聞けます。渡し方は[AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/)にあります。 Issue には、FloatSoda のバージョン、`dotnet --version` の出力、OS、再現するコードか手順を書いてください。表示の問題では、GPU、HMD、SteamVR のバージョンも添えると原因をたどれます。Issue の書式は、バグ報告と機能要望の2つのテンプレートで選べます。Issue を出す前に確かめたい決まりは、[CONTRIBUTING.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CONTRIBUTING.md)にあります。 # オーバーレイとは > オーバーレイは、SteamVR が VR の景色の上に重ねて出す画面です。この考え方を知ると、「ゲームの中にしか出せない」という思い込みを避けて、自分のツールをどこに出すかを決められます。 オーバーレイは、SteamVR が VR の景色の上に重ねて出す画面です。この考え方を知ると、「ゲームの中にしか出せない」という思い込みを避けて、自分のツールをどこに出すかを決められます。 ## オーバーレイは、VR の景色の上に重ねる画面 SteamVR のダッシュボードを開くと、メニューやパネルが景色の手前に浮かびます。あの画面が、オーバーレイの身近な例です。手首に付くメニューや、空中に浮かぶ時計のような画面も、同じ仕組みで出せます。 つまり、オーバーレイは「VR のシーンとは別の層に出る画面」です。VRChat のようなゲームの画面は、シーンを描くアプリが出します。オーバーレイは、その上に別の層として重なります。 ## ゲームとは別のアプリが出す FloatSoda では、ゲームとは別のアプリからオーバーレイを出します。ゲームを改造しなくても、自分で作ったアプリから画面を重ねられます。VRChat にワールドを作った人でなくても、VRChat の上に自分のツールを出せます。 オーバーレイのアプリは、ゲームと同時に動きます。ゲームとオーバーレイで同じコントローラーのボタンを使うと、入力が競合することがあります。ボタンやスティックの入力がどちらに届くかは、SteamVR のアクションセットの優先度で決まります。 ## 置き場所は3種類 FloatSoda では、オーバーレイの置き場所を、ウィンドウの種類で選びます。選べるのは3種類です。 | 置き場所 | FloatSoda のウィンドウ | 出る場所 | ポインタで押せるか | | :-- | :-- | :-- | :-- | | SteamVR のダッシュボード | `DashboardWindow` | ダッシュボードを開いたときの画面 | 押せる | | ワールド座標に固定 | `WorldSpaceWindow` | 部屋のある位置(メートルで指定) | 押せない(表示専用) | | トラッキングデバイスに追従 | `DeviceTrackedWindow` | HMD やコントローラーのそば | 押せない(表示専用) | ポインタ(コントローラーから出るレーザー)で押せるのは、ダッシュボードだけです。ワールド座標固定と追従のオーバーレイに `GestureDetector` を置いても、コンパイルは通りますが、タップのコールバックは呼ばれません。時刻や FPS を出す表示専用の画面は、どの置き場所でも作れます。 - ボタンを押す画面は、`DashboardWindow` に置くこと(追跡: #182) 置き場所の選び方は、[オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/)で詳しく説明します。ほかにできないことは、リファレンスの[できないこと・まだ使えないもの](/reference/limitations/)にまとめています。 ## FloatSoda が担うこと FloatSoda は、オーバーレイを出すための面倒な部分を引き受けます。引き受けるのは、画面の描画、置き場所の管理、ポインタ入力の受け取りの3つです。描いた画像は、OpenVR のオーバーレイのテクスチャとして SteamVR に渡されます。 自分で書くのは、画面の中身です。ウィンドウの種類を選び、`Child` に部品を入れ子にして画面を組み立てます。 オーバーレイを出すアプリは、シーンを描くアプリではありません。そのため FloatSoda は、SteamVR の描画の同期を使わず、目標のフレームレートで自分で更新を刻みます。この刻みも FloatSoda が担うので、利用者のコードには出てきません。 ほかの作り方との違いは[ほかの作り方と比べる](/guide/2-adopt/compare-approaches/)で、手元で書き始める準備は[開発環境を用意する](/guide/3-prepare/set-up-environment/)で説明しています。 # FloatSoda とは > FloatSoda は、SteamVR のオーバーレイを C# のコードで作る UI フレームワークです。Flutter のように、部品を入れ子にして画面を書きます。対応する .NET の版は、リファレンスの動作環境で確かめられます。VRChat 向けの自分用ツールや、Booth で売るオーバーレイを、コードだけで形… FloatSoda は、SteamVR のオーバーレイを C# のコードで作る UI フレームワークです。Flutter のように、部品を入れ子にして画面を書きます。対応する .NET の版は、リファレンスの[動作環境](/reference/requirements/)で確かめられます。VRChat 向けの自分用ツールや、Booth で売るオーバーレイを、コードだけで形にしたい人に向いています。このページは、FloatSoda が自分の目的に合うかを判断するための説明です。 オーバーレイという言葉が初めてなら、先に[オーバーレイとは](/guide/1-discover/what-is-an-overlay/)を読んでください。 ## FloatSoda は、VR のオーバーレイを C# のコードで作る道具 FloatSoda で書いた画面は、SkiaSharp で描かれ、OpenGL を経て、OpenVR のオーバーレイのテクスチャとして SteamVR に渡されます。画面の中身は、ダッシュボード、ワールド座標に固定、デバイスに追従の3種類のどれでも、同じ書き方で組めます。ポインタで押せるのはダッシュボードだけです(追跡: #182)。 「こんな画面を VR の中に出したい」と思ったときに、描画や置き場所の管理を自分で書かずに、画面の中身だけを書けるようにするのが目的です。 ## UI がすべてコードであること FloatSoda の UI は、すべて C# のコードで決まります。シーンもプレハブも持ちません。そのため、変更の差分を読む、レビューする、検索する、AI に生成させる、といった作業がコードの扱いのままできます。 書き方は宣言的です。画面を「どんな部品がどう入れ子になっているか」で書きます。たとえば、100 × 100 の色の四角を出す画面は、次のとおりです。 ```csharp Widget root = new SizedBox { Width = 100, Height = 100, Child = new ColoredBox { Color = Colors.Tomato } }; ``` `SizedBox` が大きさを決め、`ColoredBox` が色を塗ります。`SizedBox` の `Child` に `ColoredBox` を入れる、という入れ子です。Unity の経験がある人は、シーンの Hierarchy に当たるものがコードの入れ子になった、と読んでください。Flutter や React を触ってきた人には、見慣れた形です。 入れ子を重ねた書き方は、C# の読み方を[C# がほぼ初めての人へ](/guide/3-prepare/csharp-primer/)で、動くコードをチュートリアル[最初のオーバーレイを作る](/tutorials/first-overlay/)で説明しています。 ほかの作り方との違いは、[ほかの作り方と比べる](/guide/2-adopt/compare-approaches/)で説明しています。 ## 向いている人 FloatSoda が想定する作り手は、3タイプです。 | タイプ | FloatSoda で得るもの | 次に開くページ | | :-- | :-- | :-- | | AI に書かせて、自分用のツールを作る VRChatter | AI が誤用しにくいように設計した API と docs | [AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/) | | Booth でオーバーレイを売るクリエイター(Unity の経験者) | Unity の語彙で説明される考え方と、exe で配る流れ | [uGUI から来た人へ](/guide/3-prepare/from-ugui/) | | uGUI を避けたいエンジニア | シーンもプレハブもない、テキストで読める UI | [ほかの作り方と比べる](/guide/2-adopt/compare-approaches/) | ## 作れるものの例 想定している例は、次の4つです。 - 時刻や FPS を出す、表示専用の HUD - FaceEmo の表情を OSC で切り替えるパネル - 写真アルバム - フレンドのログインを知らせる通知 どの例が今の FloatSoda で作れるかは、リファレンスの[できないこと・まだ使えないもの](/reference/limitations/)で確かめてください。たとえば、写真アルバムに欲しいスクロールの部品(追跡: #69)や、通知に欲しいトーストの機能(追跡: #123)は、まだ使えません。動く作例は、[作例のカタログ](/reference/sample-catalog/)で探せます。表情切り替えは、[表情切り替えパネルを作る](/guide/4-build/screen/expression-switcher/)で扱います。 ## 開発段階 FloatSoda は Alpha の段階にあり、API は予告なく変わります。変更の内容は、リリースごとに CHANGELOG に書かれます。使うバージョンを決めたら、更新の前に [CHANGELOG.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CHANGELOG.md) で、今の版から更新先の版までの変更を読んでください。 ライセンスは MIT です。使うかどうかを決めるときは、[ほかの作り方と比べる](/guide/2-adopt/compare-approaches/)で引き受けることを確かめてください。 # ほかの作り方と比べる > SteamVR のオーバーレイを作る方法は、FloatSoda のほかに、Unity の uGUI で UI を作り、オーバーレイとして表示する方法と、OpenVR を直接呼ぶ方法があります。このページは、3つの作り方を同じ軸で並べ、FloatSoda にすると何が楽になり、何を引き受けるかを示します。比べる範囲は、… SteamVR のオーバーレイを作る方法は、FloatSoda のほかに、Unity の uGUI で UI を作り、オーバーレイとして表示する方法と、OpenVR を直接呼ぶ方法があります。このページは、3つの作り方を同じ軸で並べ、FloatSoda にすると何が楽になり、何を引き受けるかを示します。比べる範囲は、リポジトリで確かめられる事実に限ります。ほかのオーバーレイ作成ツールとの比較は、確かめられる資料がないので載せていません。 ## 比べる3つの作り方 比べるのは、次の3つです。 - **uGUI で UI を作る**: Unity の uGUI で UI を作り、オーバーレイとして表示する方法。UI は Unity のシーンとプレハブで組む。 - **OpenVR を直接呼ぶ**: ウィジェットの層を使わず、`FloatSoda.OVR` の API で、自分で用意した画像をオーバーレイに出す方法。 - **FloatSoda**: C# のコードで UI を宣言し、描画、レイアウト、入力、差分更新を任せる方法。 軸は、作る手間、変更の追いやすさと AI の書きやすさ、置き場所と入力、配る手間の4つです。自分が重く見る軸の節から読んでください。 ## 作る手間 FloatSoda は、レイアウト、描画、ポインタ入力、差分更新を肩代わりします。画面を作る人は、部品を入れ子にして書き、値を変えたいときは `State` の中で `SetState` を呼びます。数字とボタンの画面は、[最初のオーバーレイを作る](/tutorials/first-overlay/)のコードの量で見積もれます。 OpenVR を直接呼ぶ方法では、ウィジェットの層がありません。`samples/FloatSoda.Samples.PrimitiveOverlay` は、PNG のファイルをテクスチャにしてオーバーレイを出しています。画像の描画、レイアウト、押された位置の判定は、自分で書きます。 uGUI で UI を作る方法の手間は、リポジトリに比べる材料がないので、ここでは書きません。 ## 変更の追いやすさと、AI の書きやすさ FloatSoda の UI がすべて C# のコードであることは、[FloatSoda とは](/guide/1-discover/what-is-floatsoda/)で説明しています。 uGUI で UI を作る方法では、UI をシーンとプレハブの YAML と GUID の参照で持ちます。この形式は差分が読みにくく、AI が生成しにくい、というのが FloatSoda の想定する読み手の困りごとです。OpenVR を直接呼ぶ方法は、UI を持たないので、画面に当たる部分は自分の描画のコードになります。 自分の開発の形(1人、チーム、AI 任せ)と照らして、重い軸を選んでください。AI に書かせる手順は、[AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/)で説明しています。 ## 置き場所と入力 FloatSoda は、ウィンドウの種類を選ぶだけで、置き場所が切り替わります。ダッシュボードなら `DashboardWindow`、ワールド座標に固定なら `WorldSpaceWindow`、デバイスに追従なら `DeviceTrackedWindow` です。 押す操作には制約があります。ポインタが届くのはダッシュボードのオーバーレイだけで、ワールド座標固定と追従の画面は表示専用です(追跡: #182)。OpenVR を直接呼ぶ方法では、イベントを自分で受け取るので、置き場所ごとの入力の扱いも自分で書きます。 制約の詳しい内容は、リファレンスの[できないこと・まだ使えないもの](/reference/limitations/)にあります。 ## 配る手間 FloatSoda で作ったアプリは .NET 上で動き、Unity のランタイムに依存しません。配り方は、[exe にして配る](/guide/6-distribute/publish-exe/)で説明しています。動作環境は、リファレンスの[動作環境](/reference/requirements/)にあります。 uGUI で UI を作る方法で配る手間との違いは、確かめられていないので書きません。 ## FloatSoda にして引き受けること FloatSoda を選ぶと、次の3つを引き受けます。 - Alpha の段階で API が予告なく変わるため、更新の前に CHANGELOG で変更内容を読むこと - スクロールの部品(`ListView`、`GridView`、`SingleChildScrollView`)が公開 API にない(追跡: #69) - `Button` などの UI コンポーネントがなく、ボタンは `GestureDetector` で組み立てる(追跡: #102) ほかの制約は、[できないこと・まだ使えないもの](/reference/limitations/)に集めています。引き受けられると判断したら、[開発環境を用意する](/guide/3-prepare/set-up-environment/)へ進んでください。 # C# がほぼ初めての人へ > FloatSoda のコードを読むのに要る C# の書き方は、オブジェクト初期化子と init、record と with、ラムダの3つです。このページは、この3つだけを FloatSoda の実際のコードで説明し、AI が書いたコードを読んで直せるようにします。変数、関数、if は分かっている前提で書いています。 FloatSoda のコードを読むのに要る C# の書き方は、オブジェクト初期化子と `init`、`record` と `with`、ラムダの3つです。このページは、この3つだけを FloatSoda の実際のコードで説明し、AI が書いたコードを読んで直せるようにします。変数、関数、`if` は分かっている前提で書いています。 型システム、LINQ、非同期処理(`async` / `await`)、ジェネリクスは、このページでは扱いません。必要になったときに、Microsoft Learn の該当ページを引いてください。分からない言葉は、リファレンスの[用語集](/reference/glossary/)でも引けます。 ## このページで分かるようになること AI が書いた FloatSoda のコードには、次の3つの形がたくさん出てきます。 | コードの形 | 読めるようになること | 節 | | :-- | :-- | :-- | | `new Align { Child = ... }` | 作りながら値を設定する書き方 | 値を設定しながら作る書き方 | | `style with { FontSize = 60 }` | 一部だけ変えたコピーを作る書き方 | 値の入れ物と複製 | | `OnTap = () => ...` | 押されたときの処理を渡す書き方 | 押されたときの処理を渡す書き方 | 自分が止まった形の節へ進んでください。 ## 値を設定しながら作る書き方(オブジェクト初期化子と `init`) `new 型 { 名前 = 値 }` の形は、部品を作りながら、プロパティに値を入れる書き方です。この形をオブジェクト初期化子と呼びます。FloatSoda のほとんどのコードは、この形でできています。 README の最小のコードを分解して読みます。 ```csharp Widget root = new Align { Child = new SizedBox { Width = 100, Height = 100, Child = new ColoredBox { Color = Colors.Tomato } } }; ``` `new Align { ... }` で `Align` という部品を作ります。`{ }` の中の `Child = ...` は、`Align` の `Child` に値を入れる指定です。`Child` には別の部品が入るので、`new SizedBox { ... }` が入れ子になります。`SizedBox` の `Width = 100` と `Height = 100` は大きさの指定で、その `Child` に `ColoredBox` が入ります。 つまり、外側が親、`Child` に入っている内側が子です。この例では、`Align` の子が `SizedBox`、`SizedBox` の子が `ColoredBox` です。 FloatSoda の部品のプロパティは、`init` で宣言されています。`init` のプロパティは、作るとき(`{ }` の中)にだけ値を設定でき、作ったあとに別の値を代入し直せません。ただし、プロパティに入れたリストのような、中身を書き換えられる型の中身までは、固定されません。画面を変えたいときは、値を書き換えるのではなく、新しい部品を作り直します。その仕組みは[状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/)で説明しています。 `required` が付いたプロパティは、作るとき必ず設定します。たとえば、ウィンドウの `Title` は `required` なので、`new DashboardWindow { Child = root }` のように書くと、ビルドでエラーになります。 **できた?** チュートリアルのコードを見て、入れ子になった部品のどれが親でどれが子かを言える。
できなかったら 括弧の対応が分からないときは、`{` と `}` の組を探して、1つの `new` が閉じる位置を見てください。「部品」という言葉が分からないときは、用語集の[ウィジェット](/reference/glossary/)を引いてください。書き方そのものの詳しい説明は、Microsoft Learn の[オブジェクト初期化子とコレクション初期化子](https://learn.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers)と、[init キーワード](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/init)にあります。
## 値の入れ物と複製(`record` と `with`) `record` は、値を入れておく型を宣言する書き方です。FloatSoda の部品と設定は、`record` で宣言されています。`record` の値どうしは、プロパティの値を1つずつ比べて、すべて等しければ等しいものとして扱われます。`List` のように、中身が同じでも別のインスタンスなら等しくならない型のプロパティは、同じインスタンスのときだけ等しくなります。 `with` は、`record` の値から、一部だけ変えたコピーを作る書き方です。元の値は変わりません。 ```csharp var options = new FloatSodaOptions(); var fast = options with { TargetFrameRate = 90 }; ``` `options` の `TargetFrameRate` は既定の 60 のままで、`fast` は 90 になります。`TextStyle` のように、書式をまとめた `record` でも同じです。 ```csharp var title = new TextStyle { FontSize = 60, FontWeight = 700 }; var subtitle = title with { FontSize = 30 }; ``` `subtitle` は、`title` の `FontWeight` を引き継いで、`FontSize` だけを 30 にしたコピーです。 **できた?** `with` を使ったあと、元の値が変わらず、新しい値ができると言える。
できなかったら `record` の詳しい説明は、Microsoft Learn の[record 型](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/record)にあります。
## 押されたときの処理を渡す書き方(ラムダ) `() => ...` は、名前のない小さな関数を、その場で書く書き方です。ラムダと呼びます。FloatSoda では、ボタンが押されたときの処理を渡すときに使います。 `samples/FloatSoda.Samples.OverlayApp/CounterWidget.cs` に、次の1行があります。 ```csharp OnTap = () => SetState(() => _count = 0), ``` `OnTap` は、`GestureDetector` のプロパティで、タップされたときに実行する処理を受け取ります。右辺の `() => SetState(...)` が、その処理です。`()` は引数がないことを表し、`=>` の右が関数の中身です。この1行は、「タップされたら、`SetState` を呼んで、`_count` を 0 にする」という意味です。内側の `() => _count = 0` も、同じラムダの形です。 FloatSoda では、イベントの処理を受け取るプロパティの名前は `On` で始まります。型は、引数がなければ `Action?`、引数が1つあれば `Action?` です。`?` が付いた型は、`null`(何も渡さない状態)を許します。イベントのプロパティは `null` が既定なので、使わない処理は書かずに済みます。 **できた?** 内側のラムダ `() => _count = 0` が、`_count` を 0 にする処理だと言える。
できなかったら `OnTap` に書いた処理が実行されないときは、そのウィンドウがダッシュボードかを確かめてください。ワールド座標固定と追従のオーバーレイでは、`OnTap` は呼ばれません。切り分けは[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)にあります。ラムダの詳しい説明は、Microsoft Learn の[ラムダ式](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/lambda-expressions)にあります。
## これより先は この3つ以外で分からない書き方に出会ったら、次の3つの調べ先があります。 - リファレンスの[用語集](/reference/glossary/)で、言葉の意味を引く。 - Microsoft Learn で、C# の書き方を引く。 - AI に、分からない行を貼って「この行を説明して」と頼む。 読めるようになったら、チュートリアル[最初のオーバーレイを作る](/tutorials/first-overlay/)で、実際にコードを書いてください。 # uGUI から来た人へ > uGUI は「オブジェクトを置いて、スクリプトから書き換える」作り方で、FloatSoda は「状態を変えて、画面を作り直す」作り方です。このページは、uGUI の語彙で FloatSoda の考え方を説明し、最初のコードを書くときの食い違いを減らします。シーン、プレハブ、Inspector、Update を知ってい… uGUI は「オブジェクトを置いて、スクリプトから書き換える」作り方で、FloatSoda は「状態を変えて、画面を作り直す」作り方です。このページは、uGUI の語彙で FloatSoda の考え方を説明し、最初のコードを書くときの食い違いを減らします。シーン、プレハブ、Inspector、`Update` を知っていて、C# の基本が書ける人を読み手にしています。 ## uGUI と FloatSoda の考え方の違い 違いの要点は1つです。uGUI では画面のオブジェクトを探して値を書き換えますが、FloatSoda では `SetState` の中で状態の値を変えると、画面が作り直されます。 FloatSoda にシーンとプレハブはありません。UI は C# のコードで宣言します。この違いから、このページの対応表と、書き換えの癖の直し方が出てきます。 ## 置いて書き換えるから、状態を変えて作り直すへ 「数字が増えるボタン」を2つの作り方で書いて、違いを見ます。 uGUI では、シーンにテキストを置き、スクリプトからそのテキストを書き換えます。 ```csharp // uGUI: 置いたテキストを、スクリプトから書き換える public Text label; int count; public void OnClick() { count++; label.text = count.ToString(); } ``` FloatSoda では、数字を状態として持ち、状態を変えます。画面を書き換える対象を探す必要はありません。次のコードは、ウィジェット定義の抜粋です。`using` とアプリの起動処理は省略しています。押して動かすには、`DashboardWindow` の `Child` に `new CounterWidget()` を置きます。 ```csharp public record CounterWidget : StatefulWidget { public override State CreateState() => new CounterState(); } public class CounterState : State { private int _count; public override Widget Build(IBuildContext context) { return new GestureDetector { OnTap = () => SetState(() => _count++), Child = new Text(_count.ToString()) }; } } ``` `SetState` の中で `_count` を変えると、`Build` がもう一度呼ばれ、新しい数字の画面ができます。`label.text = ...` のように、画面の部品へ手を伸ばす行は、FloatSoda のコードには出てきません。 **できた?** 「`SetState` の中で、状態を持つクラスの値を変える」と言える。
できなかったら `Build` が呼ばれるタイミングが分からないときは、[状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/)を読んでください。`record` や `=>` の書き方が分からないときは、[C# がほぼ初めての人へ](/guide/3-prepare/csharp-primer/)を読んでください。
## 概念の対応表 uGUI で使ってきた概念が、FloatSoda でどうなるかを表にしました。 | uGUI | FloatSoda | 違う点 | | :-- | :-- | :-- | | シーン、プレハブ | なし | UI の構造を C# のコードで宣言する。シーンやプレハブに当たる外部ファイルはない | | Hierarchy | コードの上のウィジェットの入れ子 | 親子関係は `Child` と `Children` で書く | | Inspector で設定する値 | ウィジェットのプロパティ | `new SizedBox { Width = 100, Height = 100 }` のように、コードで設定する | | Canvas | ウィンドウ(`DashboardWindow`、`WorldSpaceWindow`、`DeviceTrackedWindow`) | 置き場所を、ウィンドウの種類で選ぶ | | LayoutGroup | `Row`、`Column`、`Wrap` など | 親が制約を渡し、子が大きさを返す | | Button | `GestureDetector` で組み立てる | `Button` は提供していない | `Button` は今は使えません(追跡: #102)。見た目をつけたボタンは、[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)で作ります。対応する部品は、リファレンスの[部品を目的から探す](/reference/find-widgets/)で探せます。 ## 入力と時間の対応 uGUI の EventSystem に当たるのは、ウィジェットの入力の受け取り口です。押す、離す、ドラッグするといった操作は、`GestureDetector` と `Listener` が受け取ります。ポインタが届くのは、ダッシュボードのオーバーレイだけです(追跡: #182)。 コントローラーのボタンやスティックの入力は、`InputActionMap` と `InputAction` で受け取ります。Unity の Input System の ActionMap、Action、DefaultBinding に近い語彙です。受け取り方は、[コントローラーの入力を受け取る](/guide/4-build/input/controller-input/)で説明しています。 `Update` に当たる毎フレームの処理は、`AnimationController` と `WidgetTicker` が時間を進めます。書き方は、[アニメーションを付ける](/guide/4-build/screen/add-animation/)で説明しています。 ## 落とし穴 uGUI の癖で、最初のコードに出やすい食い違いは3つあります。次の3つを守ってください。 - 画面の部品を直接書き換えず、`SetState` の中で状態を変えること - `Button` を探さず、`GestureDetector` でボタンを組み立てること - 押せる画面は `DashboardWindow` に置くこと(追跡: #182) コンパイルは通り、例外も出ないのに押しても動かない、というのが、3つ目を守らなかったときの症状です。ここまで読んだら、チュートリアル[最初のオーバーレイを作る](/tutorials/first-overlay/)を始めてください。 # 開発環境を用意する > この手順で、自分の PC に .NET の SDK を入れ、FloatSoda を足したコンソールアプリを作って、SteamVR のダッシュボードに色の四角を出せます。手順は5つです。終わると、FloatSoda を足し、ダッシュボードに色の四角を出すコンソールアプリが手元にそろいます。チュートリアル最初のオーバーレ… この手順で、自分の PC に .NET の SDK を入れ、FloatSoda を足したコンソールアプリを作って、SteamVR のダッシュボードに色の四角を出せます。手順は5つです。終わると、FloatSoda を足し、ダッシュボードに色の四角を出すコンソールアプリが手元にそろいます。チュートリアル[最初のオーバーレイを作る](/tutorials/first-overlay/)の最初に要る「空のコンソールアプリ」は、手順2と手順3で作るものと同じです。 エディター(IDE)は指定しません。好きなものを使ってください。必要な設定も、この手順にはありません。 ## 始める前に確かめること 次の2つが必要です。 - SteamVR が PC に入っていること(Steam から入れる) - VR の機材が PC につながっていること 動作環境の全体は、リファレンスの[動作環境](/reference/requirements/)にあります。SteamVR が入っていない、または OS が対応していないときは、ここでは進めず、動作環境を確かめてから戻ってください。 HMD を被らずに確かめたいときは、[HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/)を読んでください。 ## 手順1: .NET の SDK を入れる .NET 10 の SDK を、公式の配布元()から入れる。 環境変数と PATH の設定は、インストーラーが行います。 入れたら、ターミナルで次を実行する。 ```powershell dotnet --version ``` **できた?** `10.` で始まる版が表示された。
できなかったら 「コマンドが見つかりません」と出たときは、ターミナルを開き直してください。インストーラーが設定した PATH は、開き直したターミナルから有効になります。それでも出ないときは、SDK(ランタイムだけではない)を入れたかを確かめてください。`10.` より前の版が表示されたときは、`dotnet --list-sdks` で一覧を見て、10 系の SDK が入っているかを確かめてください。
## 手順2: コンソールアプリを作る 作業用のフォルダーで、次を実行する。 ```powershell dotnet new console -n MyOverlayApp cd MyOverlayApp ``` **できた?** `MyOverlayApp` フォルダーができ、その中に `Program.cs` と `MyOverlayApp.csproj` がある。 ## 手順3: FloatSoda を足す `MyOverlayApp` フォルダーで、NuGet から FloatSoda を足す。 ```powershell dotnet add package FloatSoda ``` **できた?** `MyOverlayApp.csproj` に `FloatSoda` の `PackageReference` が入った。
できなかったら 「パッケージが見つかりません」と出たときは、ネットワークと NuGet のパッケージソース(`dotnet nuget list source` で確かめられる)への接続を確かめてください。FloatSoda のリポジトリを取得して使うときは、`dotnet add reference <リポジトリ>/src/FloatSoda/FloatSoda.csproj` で、プロジェクト参照として足せます。この場合、使うのは取得したリポジトリの版の API です。公開されている版と API が違うことがあるので、チュートリアルのコードがビルドできないときは、[CHANGELOG.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CHANGELOG.md) で変更を確かめてください。
## 手順4: ビルドして確かめる 次を実行する。 ```powershell dotnet build ``` **できた?** 「ビルドに成功しました」と出た。
できなかったら 警告ではなく、エラーが出たときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)を読んでください。
## 手順5: SteamVR で色の四角を出す SteamVR を起動してから、チュートリアル[最初のオーバーレイを作る](/tutorials/first-overlay/)の[「1. オーバーレイを SteamVR に出す」](/tutorials/first-overlay/#1-オーバーレイを-steamvr-に出す)にあるコードを、このアプリの `Program.cs` に貼る。コードの意味は、チュートリアルで説明している。 SteamVR を起動したまま、次を実行する。 ```powershell dotnet run ``` SteamVR のダッシュボードを開く。 **できた?** ダッシュボードに、青い四角が出た。
できなかったら 四角が出ないときは、まず SteamVR が起動しているかを確かめてください。FloatSoda は、SteamVR が起動していない状態では動きません。アプリが終了したり例外が出たりしたときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)を、起動しているのに出ないときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)を読んでください。環境の要件は、リファレンスの[動作環境](/reference/requirements/)にあります。
最後まで進めたら、チュートリアル[最初のオーバーレイを作る](/tutorials/first-overlay/)へ進んでください。リポジトリを取得した人は、リポジトリのルートに移動して `dotnet run --project samples/FloatSoda.Samples.GettingStarted` を実行すると、色の四角を出す作例も動かせます。 # VRChat と組み合わせるときに確かめること > VRChat と連携するツール(表情切り替えパネルなど)が動かないとき、原因が FloatSoda 側か VRChat 側かを分けられると、次に調べる箇所を絞れます。このページは、確かめる項目を VRChat 側、FloatSoda 側、つなぐ側の順に並べ、1つずつ「確かめた」と答えられるようにします。 VRChat と連携するツール(表情切り替えパネルなど)が動かないとき、原因が FloatSoda 側か VRChat 側かを分けられると、次に調べる箇所を絞れます。このページは、確かめる項目を VRChat 側、FloatSoda 側、つなぐ側の順に並べ、1つずつ「確かめた」と答えられるようにします。 FloatSoda 本体は OSC に依存していません。VRChat と OSC で通信するコードは、利用者が(または AI が)追加します。そのため、このページは外部ツールの使い方を書きません。VRChat の設定画面の操作、FaceEmo の使い方、OSC の通信の仕様は、それぞれの公式資料で確かめてください。通信のコードは、[AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/)の手順で頼めます。AI が書いた API 名と設定値は、公式資料と照らしてください。 ## このページで確かめること 確かめる項目は、次の3つのグループに分かれます。 | グループ | 確かめること | 項目の数 | | :-- | :-- | :-- | | VRChat 側 | OSC の有効化、パラメーター名と型、ポート | 3 | | FloatSoda 側 | SteamVR の起動、オーバーレイが出る、押せる | 3 | | つなぐ側 | 送信のコードが動く、VRChat が受け取る | 2(切り分けの順) | VRChat 側から確かめてください。FloatSoda 側は、VRChat が無くても確かめられますが、まとめて確かめたほうが早く終わります。 ## VRChat 側で確かめること VRChat 側の項目は3つです。確かめたら、パラメーター名と型は控えて、AI に渡します。 1. **OSC が有効になっている** 2. **アバターのパラメーター名と型が分かっている** 3. **ポートが分かっている** OSC は、VRChat の設定で有効にします。有効にする場所と手順は、VRChat の公式資料にあります。パラメーターは、切り替えたい表情について、名前と型(bool、int、float)を調べます。調べ方も公式資料にあります。ポートは、VRChat が OSC を受け取るポートと、送ってくるポートを確かめます。 公式資料は、VRChat Creators の OSC の概要()から辿れます。 **できた?** 3つの項目のどれにも「確かめた」と答えられる。
できなかったら 公式資料を読んでも分からないときは、VRChat のコミュニティか、公式のサポートに聞いてください。どちらも FloatSoda の外にあります。
## FloatSoda 側で確かめること FloatSoda 側の項目も3つです。ツールが作成中でも確かめられます。 1. **SteamVR が起動している** 2. **ダッシュボードにオーバーレイが出る** 3. **ボタンを押すと反応する** FloatSoda のアプリは、SteamVR が起動していないと動きません。ダッシュボードのオーバーレイは、SteamVR のダッシュボードを開いて、自分のツールの画面を探します。ポインタ(レーザー)で押せるのは、ダッシュボードに出ているオーバーレイだけです。ワールド座標固定や追従のオーバーレイは、押しても反応しません(追跡: #182)。 VRChat とツールは同時に動きます。ダッシュボードのボタンをポインタ(レーザー)で押す操作は、オーバーレイのマウスのイベントとして届くので、VRChat の入力とは競合しません。競合するのは、`InputAction` で登録したコントローラーのボタンやスティックの入力です。こちらは、SteamVR のアクションセットの優先度で、どちらに届くかが決まります。 **できた?** 3つの項目のどれにも「確かめた」と答えられる。
できなかったら オーバーレイが出ない、押しても反応しないときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)を読んでください。押せない置き場所の制約は、リファレンスの[できないこと・まだ使えないもの](/reference/limitations/)にあります。
## つながらないときの切り分け ボタンを押しても VRChat のアバターが変わらないときは、次の順に確かめます。確かめられなかった段から、先に調べます。 1. **オーバーレイは反応しているか。** ボタンを押して、画面が変わる(数字が増える、色が変わる)なら、FloatSoda 側は動いています。反応しないなら、「FloatSoda 側で確かめること」に戻ります。 2. **送信のコードは動いているか。** 押したときに OSC を送るコードが実行されているかを、ログで確かめます。 3. **VRChat は受け取っているか。** VRChat が OSC を受け取っているかの確かめ方は、VRChat の公式資料の [OSC Debugging](https://docs.vrchat.com/docs/osc-debugging) にあります。OSC の有効化と、送り先のポートは [OSC Overview](https://docs.vrchat.com/docs/osc-overview) で確かめます。 コードを直すときは、確かめられなかった段と、そのときの様子を AI に伝えてください。 # AI に FloatSoda を書かせる > この手順で、AI に FloatSoda の資料を渡してコードを書かせ、出来上がったコードで SteamVR のダッシュボードにオーバーレイを出し、FloatSoda で起きやすい食い違いを点検できます。コードを読めなくても進められます。手順は5つです。 この手順で、AI に FloatSoda の資料を渡してコードを書かせ、出来上がったコードで SteamVR のダッシュボードにオーバーレイを出し、FloatSoda で起きやすい食い違いを点検できます。コードを読めなくても進められます。手順は5つです。 AI は、Flutter の知識で書いた「動かないコード」を出すことがあります。FloatSoda は Flutter に似ていますが、名前や引数が違う部品があり、まだない部品もあります。資料を渡すこと、ビルドと実行で確かめること、よくある食い違いを検索で点検することが、この事故を減らします。 始める前に、[開発環境を用意する](/guide/3-prepare/set-up-environment/)を済ませて、FloatSoda を足したコンソールアプリを用意してください。この手順では、そのアプリの `Program.cs` を書き換えます。 ## 手順1: AI に資料を渡す FloatSoda の公開サイトは、AI 向けに2つのファイルを配信しています。AI には、このどちらかを渡します。 | ファイル | 中身 | 向く場面 | | :-- | :-- | :-- | | `llms.txt` () | 索引 | AI が自分でページを開ける(コーディングエージェントなど) | | `llms-full.txt` () | 全文 | チャットに貼る、またはファイルとして取り込む | 渡し方は、URL を貼るか、ファイルを取り込みます。URL を開けない AI には、ファイルの本文を貼ってください。あわせて、リファレンスの[できないこと・まだ使えないもの](/reference/limitations/)を読むように伝えてください。作例の README にも、ウィジェットごとの使い方と「Flutterとの違い」があります。AI が部品を迷うときは、`samples/` の該当する作例の `README.md` を渡せます。 **できた?** AI に使う API と制約を挙げさせ、その内容が、渡した資料の本文にある。 ## 手順2: 作りたいものを頼む 作りたいものを、次の3つを入れて頼む。 - 何を出すか(例: 現在の時刻、表情を切り替えるボタン) - どこに置くか(ダッシュボード、ワールド座標に固定、コントローラーに追従) - 押す操作が要るか 押す操作が要る画面は、ダッシュボードに置くよう伝えてください。ワールド座標固定と追従のオーバーレイは、ポインタで押せません(追跡: #182)。 頼み方の例です。 ```text FloatSoda の資料を読んだうえで、SteamVR のダッシュボードに、現在の時刻を毎秒更新して出す オーバーレイを作ってください。押す操作は要りません。定期更新と SetState の呼び出し条件は、 資料で確かめてください。資料にない API は推測せず、確認が必要な点を示してください。 Program.cs 全体を、そのまま貼れる形で出してください。 ``` **できた?** コードが `Program.cs` に貼れる形で返ってきた。 ## 手順3: ビルドする 返ってきたコードを `Program.cs` に貼り、次を実行する。 ```powershell dotnet build ``` エラーが出たら、エラーの文をそのまま AI に貼って、直させてください。存在しない名前を AI が書いたときは、ここでコンパイルエラーになります。 **できた?** 「ビルドに成功しました」と出た。
できなかったら AI に何度か直させても消えないエラーは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)を読んでください。`using` のエラーが出るときは、参照している FloatSoda の版と、`using` の綴りを確かめてください。FloatSoda は、リリースで名前空間を移すことがあります。たとえば、`Text` と `RichText` は 0.4.0 で `FloatSoda.Widgets.Components` から `FloatSoda.Widgets` に移りました。AI が古い `using` を書いている可能性があります。
## 手順4: 動かす SteamVR を起動してから、次を実行する。 ```powershell dotnet run ``` SteamVR のダッシュボードを開く。 **できた?** ダッシュボードにオーバーレイが出て、頼んだ動き(例の依頼なら、時刻が毎秒変わる)が見えた。
できなかったら アプリが終了する、例外が出るときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)を読んでください。起動しているのに出ない、押しても反応しないときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)を読んでください。
## 手順5: 点検する 動いたコードでも、FloatSoda で起きやすい食い違いが残っていることがあります。次の語を、コードの中で検索してください。見つかった箇所は、表の「起きること」に当たるかを確かめます。当たるときは、その行の「AI への指示の例」を AI に伝えて、直させます。たとえば `WorldSpaceWindow` や `Task.Run` は、それ自体は正しい使い方があるので、語が見つかっただけでは直しません。 | 検索する語 | 起きること | AI への指示の例 | | :-- | :-- | :-- | | `Button` | `Button` などの UI コンポーネントは提供していない | `Button` を使わず、`GestureDetector` で組み立てて | | `WorldSpaceWindow` または `DeviceTrackedWindow` の中の `GestureDetector` | 押しても `OnTap` が呼ばれない | 押す画面は `DashboardWindow` に置いて | | `ListView`、`GridView`、`SingleChildScrollView` | 公開されていない。使えない | スクロールが使えない前提で、表示する件数を絞り、`Stack` と `Positioned` などで表示範囲に収まるように並べて。元の要件がそれで満たせないときは、その点を教えて | | `ClipRRect`、`ClipPath` | FloatSoda では名前が違う | `ClipRRect` は `ClipRoundRect`、`ClipPath` は `ClipCustomPath` に対応する。名前だけでなく、引数とプロパティも、[Flutter の読み替え表](/reference/flutter-mapping/)と作例の README で確かめて書き直して | | `MouseRegion` | FloatSoda では名前が違う | `MouseRegion` は `PointerRegion` に対応する。名前だけでなく、引数とコールバックも、読み替え表と作例の README で確かめて書き直して | | `margin`、`Margin`、`spacing`、`Spacing` | `Container` に `margin` はなく、`Row` や `Column` に `spacing` はない(`Wrap` の `Spacing` は実在する) | 見つかった型に、そのプロパティがあるかを確かめ、なければ `Padding` と `SizedBox` で余白と間隔を作って | | `SetState` を呼ぶ `Task.Run` や `Timer` の中身 | メインスレッド(フレームを処理するスレッド)以外から呼ぶと、画面の更新が失われる、またはクラッシュする(追跡: #150、#281) | 定期的な更新は、メインスレッドから呼ばれる `WidgetTicker` の中で `SetState` を呼んで。[チュートリアル](/tutorials/first-overlay/#2-数字を表示して書き換える)の書き方に合わせて | `Size` を指定していないウィンドウは、起動後に中身が大きくなっても、SteamVR 上の表示範囲が最初の大きさに固定されます。描かれた内容は正常で、増えた部分が表示されません。中身の大きさが変わる画面は、ウィンドウに `Size` を指定するよう AI に伝えてください(追跡: #285)。 この表は語の一覧だけで、制約の詳しい内容は[できないこと・まだ使えないもの](/reference/limitations/)、Flutter の名前の違いは[Flutter の読み替え表](/reference/flutter-mapping/)にあります。 **できた?** 表の語が見つかった箇所のすべてで、「起きること」に当たるかを確かめ、当たるものを直した。
できなかったら 「起きること」に当たる箇所は、表の指示を AI に渡して直させ、手順3に戻ってください。直し方の根拠は、上の2つのリファレンスにあります。
動いて点検も済んだら、次は「4. 作る」です。最初の1本をまだ作っていないときは、チュートリアル[最初のオーバーレイを作る](/tutorials/first-overlay/)を先に読んでください。見た目の整え方は、[見た目を整える](/guide/4-build/screen/style-appearance/)から始められます。 # コントローラーの入力を受け取る > コントローラーの入力は、InputAction で宣言します。宣言したアクションを AddFloatSoda に登録すると、ボタン、トリガー、スティックの入力をコードで受け取れます。画面を押す操作とは別の入力なので、ショートカットや「掴む」操作のように、画面の外から使う物に向いています。この手順を終えると、トリ… ## この手順でできること コントローラーの入力は、`InputAction` で宣言します。宣言したアクションを `AddFloatSoda` に登録すると、ボタン、トリガー、スティックの入力をコードで受け取れます。画面を押す操作とは別の入力なので、ショートカットや「掴む」操作のように、画面の外から使う物に向いています。この手順を終えると、トリガーを引いたときにコンソールへ表示が出て、トリガーの引き量を毎フレーム読めるようになります。 `T` には、受け取りたい入力に合わせて次の3つの型から1つを選びます。 | `T` | 入力の種類 | 受け取れる値 | | :-- | :-- | :-- | | `bool` | ボタン | 押している間は `true` | | `float` | トリガーの引き量 | 引き量の値 | | `Vector2` | スティック、トラックパッド | X と Y の値 | 画面上のボタンをコントローラーのレーザーで押す操作は、この手順の対象外です。その場合は[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)に進みます。 ## 前提条件 - [最初のオーバーレイを作る](/tutorials/first-overlay/)を終え、起動のコードが動くこと - SteamVR を起動し、HMD を接続していること - ソースの先頭に `using System.Numerics;`、`using FloatSoda.OVR;`、`using FloatSoda.OVR.Input;` があること ## 手順1: アクションを宣言する 使いたい入力ごとに、`InputAction` を1つ作ります。必須の指定は `Name` だけです。コントローラーのどの入力に割り当てたいかは、`SuggestedPath` で提案します。 ```csharp var grab = new InputAction { Name = "grab", SuggestedPath = "/user/hand/right/input/trigger/click", }; var pull = new InputAction { Name = "pull", SuggestedPath = "/user/hand/right/input/trigger", }; var move = new InputAction { Name = "move", SuggestedPath = "/user/hand/left/input/thumbstick", }; ``` `SuggestedPath` は、すべてのコントローラー種別に同じパスで提案されます。`/user/hand/left/input/thumbstick` のように、コントローラーによって名前が違う入力は、種別ごとに指定できる `DefaultBindings` を使います(例は次のコードにあります)。 `SuggestedPath` の書き方は、型ごとに次のとおりです。 | `T` | 書き方の例 | 末尾の扱い | | :-- | :-- | :-- | | `bool` | `/user/hand/right/input/trigger/click` | 末尾が `click` か `touch` のとき、その動作を使う。省略すると `click` | | `float` | `/user/hand/right/input/trigger` | 末尾が `pull` のとき、その動作を使う。省略すると `pull` | | `Vector2` | `/user/hand/left/input/thumbstick` | 末尾が `position` のとき、その動作を使う。省略すると `position` | コントローラーの種類ごとに割り当てを変えたいときは、`SuggestedPath` の代わりに `DefaultBindings` を使います。`ControllerType` には `Index`、`ViveWand`、`OculusTouch` があります。 次の例は、手順1の最初のコードにある `grab` の宣言を置き換えます。`DefaultBindings` を1つでも書くと、`SuggestedPath` はどのコントローラーにも使われません。使うコントローラーの種類は、すべて書きます。 ```csharp var grab = new InputAction { Name = "grab", DefaultBindings = [ new DefaultBinding { Controller = ControllerType.Index, Path = "/user/hand/right/input/trigger/click" }, new DefaultBinding { Controller = ControllerType.ViveWand, Path = "/user/hand/right/input/trigger/click" }, new DefaultBinding { Controller = ControllerType.OculusTouch, Path = "/user/hand/right/input/trigger/click" }, ], }; ``` **できた?** コンパイルが通れば、手順1は完了です。
できなかったら `T` に `bool`、`float`、`Vector2` 以外の型を渡すと、実行時に `TypeInitializationException` が出ます。原因は、その `InnerException` の `NotSupportedException` です。メッセージは次の形です。 ```text InputAction は未対応です。bool / float / Vector2 を使用してください。 ``` `Name` を書き忘れると、コンパイルが通りません。
## 手順2: AddFloatSoda に登録する 宣言したアクションは、`InputActionMap` にまとめて `FloatSodaOptions.InputActionMaps` に渡します。`AppKey` も、アプリごとに別の値を指定します。 起動のコードにある `AddFloatSoda` の呼び出しは、次のコードに置き換えます。アクションの宣言は、この呼び出しより前に置きます。 ```csharp using FloatSoda; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddFloatSoda(new FloatSodaOptions { AppKey = new AppKey("my.first.overlay"), InputActionMaps = [new InputActionMap { Name = "main", Actions = [grab, pull, move] }], }); ``` アクションを1つ以上登録すると、SteamVR に渡すアクションマニフェストが自動で作られて登録されます。JSON ファイルを手で書く必要はありません。マニフェストは、一時フォルダー配下の `FloatSoda//input` に書き出されます。`AppKey` が違うアプリどうしは、書き出し先が分かれます。 **できた?** 起動したとき、コンソールに「アクション入力の初期化に失敗しました」という警告が出なければ、手順2は完了です。登録後に入力が届くかどうかは、手順3で確かめます。
できなかったら 入力の初期化に失敗しても、オーバーレイの表示は続きます。警告が出た場合は、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)で、メッセージの続きから原因を切り分けます。
## 手順3: 入力を受け取る 入力の受け取り方は3つあります。イベントは、`host.RunAsync()` を呼ぶ前に購読します。 | 受け取り方 | 呼ばれる、または読める場面 | 向いている使い方 | | :-- | :-- | :-- | | `OnPerformed` | `bool` では押した瞬間。`float` と `Vector2` では値が変わったフレーム | 押した瞬間に1回だけ動かす | | `OnReleased` | `bool` で離した瞬間。`float` と `Vector2` では呼ばれない | 離したときに終了処理をする | | `Value` | いつでも読める。毎フレーム更新される | 引き量やスティックの傾きを、画面に反映し続ける | ```csharp grab.OnPerformed += _ => Console.WriteLine("grab: pressed"); grab.OnReleased += () => Console.WriteLine("grab: released"); pull.OnPerformed += value => Console.WriteLine($"pull: {value}"); ``` `Value` は、たとえば `pull.Value` のように、必要な場所で読みます。`Value` を読むだけでは、画面の再構築は予約されません。画面に反映するときは、メインスレッドで `SetState()` を呼びます。 `IsActive` は、アクションが現在いずれかの入力に割り当てられているかを示します。 以下の確認は、例のとおりアクションをトリガーに割り当てた場合のものです。割り当てを変えた場合は、その入力を操作します。 **できた?** トリガーを引くと、コンソールに `grab: pressed` が出て、離すと `grab: released` が出れば、手順3は完了です。 ## 手順4: SteamVR のバインディングを確かめる 実際にどの入力がアクションに結び付くかは、SteamVR が決めます。`SuggestedPath` と `DefaultBindings` は、アプリから出す割り当ての提案です。利用者は、SteamVR のバインディングの画面で割り当てを変えられます。 そのため、コードは「どのボタンか」を前提にせず、アクションの名前だけを頼りに書きます。割り当てが変わっても、同じコードのまま動きます。 バインディングの画面の開き方と、割り当てがどの順で決まるか(利用者の割り当て、アプリの提案、ドライバーの既定)は、OpenVR の公式資料にあります。 - [SteamVR Input](https://github.com/ValveSoftware/openvr/wiki/SteamVR-Input) - [Input Profiles](https://github.com/ValveSoftware/openvr/wiki/Input-Profiles) **できた?** 自分のアクション(`grab`、`pull`、`move`)が、SteamVR のバインディングの画面に出ていれば、手順4は完了です。 ## 結果を確かめる HMD を被り、コントローラーのトリガーを操作して(割り当てを変えた場合は、その入力を操作して)、コンソールで次の3点を見ます。 - トリガーを引いたときに `grab: pressed` が出る - トリガーを離したときに `grab: released` が出る - トリガーをゆっくり引くと、`pull:` の値が少しずつ変わる 自分の物を作る場合は、次に[表情切り替えパネルを作る](/guide/4-build/screen/expression-switcher/)に進みます。作例は[作例のカタログ](/reference/sample-catalog/)にもあります。 ## うまくいかないとき 入力が来ない、または起動時に警告が出たときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)で切り分けます。 # HMD を被らずに確かめる > 見た目を確かめるために HMD を被る回数は、デスクトップの窓に出す方法か、画像にして見る方法で減らせます。窓に出す方法は、画面をモニターで見ながらマウスで操作でき、マウスの操作に対する動きも確かめられます。画像にする方法は、ウィジェットを PNG ファイルに書き出すので、SteamVR も HMD も要りません。た… ## どちらの方法を使うか 見た目を確かめるために HMD を被る回数は、デスクトップの窓に出す方法か、画像にして見る方法で減らせます。窓に出す方法は、画面をモニターで見ながらマウスで操作でき、マウスの操作に対する動きも確かめられます。画像にする方法は、ウィジェットを PNG ファイルに書き出すので、SteamVR も HMD も要りません。ただし、書き出すのは描画を1回実行した時点の見た目だけで、動きや操作は確かめられません。 | 方法 | できること | SteamVR と HMD | | :-- | :-- | :-- | | 方法1: デスクトップの窓に出す | 画面をモニターで見る。マウスで押す | 起動と接続が要る | | 方法2: 画像にして比べる | ウィジェットのある時点の見た目を PNG にして、保存した画像を開いて見る。動きと操作は確かめられない | 要らない | どちらの方法も、HMD の中で動くことの確認の代わりにはなりません。最後は HMD で確かめます。 ## 前提条件 - 方法1: `DashboardWindow` で出している起動のコードがあること - 方法1: SteamVR を起動し、HMD を接続していること - 方法2: テストしたいウィジェットがあること - 方法2: プロジェクトに `FloatSoda.Testing` パッケージを足していること 方法1でも HMD の接続が要る理由は、FloatSoda が表示先によらず、起動時に OpenVR を初期化するためです。HMD が未接続だと `Init_HmdNotFound` で起動に失敗します(追跡: #140)。方法1で省けるのは、HMD を頭に付ける手間だけです。 ## 方法1: デスクトップの窓に出す 起動のコードで `DashboardWindow` を渡している箇所を、`DesktopWindow` に替えます。`DesktopWindow` は、開発機のデスクトップ上に見える窓として画面を出します。 ```csharp var useDesktop = args.Contains("--desktop"); var hostArgs = args.Where(argument => argument != "--desktop").ToArray(); var builder = Host.CreateApplicationBuilder(hostArgs); builder.Services.AddFloatSoda(); using var host = builder.Build(); var app = host.Services.GetRequiredService(); app.CreateWindow(useDesktop ? new DesktopWindow { Title = "MyOverlay", Child = root } : new DashboardWindow { Dpm = new Dpm(100), Title = "MyOverlay", Child = root }); await host.RunAsync(); ``` `Dpm` は `FloatSoda.Abstractions.Geometries` の名前空間にあります。`--desktop` を付けて起動すると窓に出て、付けないと従来どおりダッシュボードに出ます。`--desktop` はこのアプリで処理する引数なので、`Host.CreateApplicationBuilder` に渡す引数から取り除きます。このコードは、[GestureDetector のサンプル](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GestureDetector/Program.cs)と同じ形です。 ```powershell dotnet run -- --desktop ``` **できた?** デスクトップに窓が出て、マウスで操作できれば、方法1は完了です。
できなかったら 窓が出ない、または起動時に例外が出るときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)で原因を切り分けます。HMD を接続していない場合は、前提条件のとおり `Init_HmdNotFound` で失敗します。
デスクトップで動いても、ダッシュボードで動く証明にはなりません。ウィンドウの種類ごとに、ポインターの座標が届く仕組みが違うためです(追跡: #182)。ボタンを押せる画面は、最後に HMD のダッシュボードでも確かめます。 ## 方法2: 画像にして比べる `WidgetBitmapRenderer.Render` にウィジェットと画像のサイズを渡すと、ビルド、レイアウト、ペイントを1回実行して、結果を `SKBitmap` で返します。この方法は SteamVR を使いません。 ```csharp using FloatSoda.Testing; using SkiaSharp; var renderer = new WidgetBitmapRenderer(); using var bitmap = renderer.Render(root, new SKSizeI(400, 300)); using var image = SKImage.FromBitmap(bitmap); using var data = image.Encode(SKEncodedImageFormat.Png, 100); File.WriteAllBytes("widget.png", data.ToArray()); ``` `root` には、HMD の中に出したい画面のウィジェットを渡します。`Render` は1回しか実行しないので、`Image` ウィジェットのように、画像を非同期で読み込む部品は、読み込み前の状態で描かれます。画像まで描きたいときは、`Image` に渡すものと同じプロバイダーを変数に持ち、`Render` の前に `await provider.ResolveAsync()` を実行します。返されたハンドルは、描画が終わるまで手放さないでください。次の例は、`using var` で、スコープの終わりまでハンドルを保持します。 ```csharp using FloatSoda.Core.Providers; var provider = new FileImageProvider("photo.png"); Widget imageRoot = new Image { Provider = provider }; using var handle = await provider.ResolveAsync(); using var bitmap = renderer.Render(imageRoot, new SKSizeI(400, 300)); ``` `FloatSoda.Samples.PaintingSample` も、ウィジェットのツリーを PNG に書き出します。コードは[PaintingSample の Program.cs](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PaintingSample/Program.cs)にあります。 画像を期待する画像と自動で比べる方法は、この手順では扱いません。書き出した PNG を開いて、目で見て確かめます。 **できた?** `widget.png` が保存され、開くと、400 × 300 ピクセルの画像の中で、配置、色、文字が意図どおりになっていれば、方法2は完了です。HMD の中での実寸と操作は、HMD で別に確かめます。
できなかったら 画像の中の配置や大きさが思ったとおりでないときは、[レイアウトの考え方](/guide/5-troubleshoot/layout-model/)で、大きさの決まり方を確かめます。例外が出るときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)に進みます。
## 結果を確かめる 窓または画像で、次の3点を見ます。 - 配置: 部品が思った場所に、思った大きさで並んでいる - 色: 背景と文字の色が指定どおりに出ている - 文字: 文字が切れずに、読める大きさで出ている HMD の中で最後に確かめる項目は、押せるかどうかと、実寸の大きさです。見た目の指定は[見た目を整える](/guide/4-build/screen/style-appearance/)、押せる部品は[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)で扱います。 ## うまくいかないとき 窓が出ない、または起動で例外が出るときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)で原因を切り分けます。 # FloatSoda の仕組み(使う人向け) > 書いたウィジェットは、作り直し、レイアウト、描画の記録の順に処理され、別のスレッドで描かれた絵が、テクスチャとして SteamVR に渡されます。この流れを知っておくと、遅い、描き直されない、スレッドの問題が出た、といった症状から、原因の当たりをつけられます。 ## 画面に出るまでの流れ 書いたウィジェットは、作り直し、レイアウト、描画の記録の順に処理され、別のスレッドで描かれた絵が、テクスチャとして SteamVR に渡されます。この流れを知っておくと、遅い、描き直されない、スレッドの問題が出た、といった症状から、原因の当たりをつけられます。 次の図は、1フレームで起きる処理を上から下の順に並べたものです。 ```text メインスレッド ウィジェットの作り直し 変わった部分だけ ↓ レイアウト 大きさと位置を決める ↓ ペイント 描く内容を「レイヤー」に記録する ↓ レイヤーを複製して渡す レンダースレッド Skia で描画 レイヤーを絵にする ↓ OpenGL のテクスチャ ↓ SteamVR のオーバーレイ ``` 内部は、3つのツリー(ウィジェットと Element、RenderObject、レイヤー)で組み立てられています。ウィジェットのツリーが作り直されると、RenderObject のツリーがレイアウトと描画の記録を行い、その結果がレイヤーのツリーになります。使う側が書くのはウィジェットだけで、残りの2つは FloatSoda が動かします。 ## 1フレームの中で、メインスレッドがすること メインスレッドは、毎フレーム、次の4つをこの順に行います。 1. `CreateWindow` で予約したウィンドウの作成など、保留中のタスクを実行する 2. SteamVR のイベントを処理する 3. [コントローラーの入力を受け取る](/guide/4-build/input/controller-input/)で登録したアクションの値とイベントを更新する 4. 各ウィンドウの描画を準備する(ウィジェットの作り直し、レイアウト、ペイント) 4つを終えたら、次のフレームの時刻まで待ちます。目標のフレームレートは、既定で 60 です。`FloatSodaOptions.TargetFrameRate` で変えられます([設定やログ、裏で動く処理を足す](/guide/4-build/mechanics/use-generic-host/))。 ## 描画は別のスレッドで行われる ここでは、ウィジェットの処理を行うメインスレッドと、描画を行うレンダースレッドを説明します。`Build()` はメインスレッドで実行されます。`SetState()` も、メインスレッドから呼びます。レンダースレッドは、Skia による描画と、OpenGL と SteamVR とのやり取りを受け持ちます。 メインスレッドは、ペイントで作ったレイヤーのツリーを複製して、レンダースレッドに渡します。複製して渡すのは、メインスレッドが次のフレームでレイヤーを書き換えているあいだに、レンダースレッドが同じレイヤーを読んで、データがぶつかるのを避けるためです。 自分のコードが動くスレッドは、書く場所で決まります。ウィジェットの `Build()` とアクションのイベントはメインスレッドで動きます。`BackgroundService` のような裏の処理は、別のスレッドで動きます。 ## 変わった部分だけが処理される 作り直し、レイアウト、ペイントは、毎フレーム全体をやり直すわけではありません。それぞれ更新が必要になった部分だけが処理されます。作り直しの対象は、`SetState()` などで変更を知らせたウィジェットです。変わった部分がないフレームでは、レイアウトとペイントを行いません。 そのため、`SetState()` を呼ぶこと自体を避ける必要はありません。値が変わったら呼ぶ、という書き方で足ります。作り直しの考え方は、[状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/)にあります。 ## 止まったり終わったりするとき 保留中のタスクの実行、イベントの処理、メインスレッドでの描画の準備(作り直し、レイアウト、ペイント)のどれかで例外が出ると、メインループが止まります。そのとき、`タスクエラー`、`イベント処理エラー`、`描画エラー` のいずれかで始まるメッセージが、エラーのログに出ます。メインループが止まると、FloatSoda のランタイムも終了処理に入り、アプリが終わります。 SteamVR が終了を通知したときも、アプリは終わります。SteamVR を閉じると、FloatSoda のアプリも一緒に終了します。 アプリが止まった、または画面が出ないときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)で、ログのメッセージから原因を切り分けます。 ## もっと深く知りたいとき 各ツリーの内部と、差分更新の詳しい動きは、コントリビューター向けの解説にあります。1フレームが SteamVR に届くまでの流れは[1フレームが SteamVR に届くまで](/contributor/2-understand/frame-to-steamvr/)、ウィジェットのツリーは[フレームワーク層](/contributor/2-understand/framework-layer/)で読めます。 # オーバーレイの種類と選び方 > FloatSoda のオーバーレイは、DashboardWindow、WorldSpaceWindow、DeviceTrackedWindow の3種類です。出る場所と、コントローラーで押せるかどうかが違います。押して操作する物はダッシュボード、表示だけでよい物は空間に固定するか、コントローラーに追従させます。 ## オーバーレイは3種類あり、出る場所と入力の届き方が違う FloatSoda のオーバーレイは、`DashboardWindow`、`WorldSpaceWindow`、`DeviceTrackedWindow` の3種類です。出る場所と、コントローラーで押せるかどうかが違います。押して操作する物はダッシュボード、表示だけでよい物は空間に固定するか、コントローラーに追従させます。 | 種類 | 出る場所 | 押して操作できるか | 位置の決め方 | | :-- | :-- | :-- | :-- | | `DashboardWindow` | SteamVR のダッシュボード | できる | SteamVR が管理する | | `WorldSpaceWindow` | ワールドの決まった座標 | できない(表示専用) | `Position` と `Rotation` | | `DeviceTrackedWindow` | コントローラーや HMD のそば | できない(表示専用) | `Target`、`Offset`、`Rotation` | 作る前に種類を選ぶ理由は、選び間違えると、ボタンが押せない物ができてしまうためです。表示専用になる理由は、ポインターの座標が、ダッシュボードのオーバーレイにしか届かないことにあります(追跡: #182)。 1つのアプリは、複数のウィンドウを同時に作れます。種類を混ぜてもかまいません。複数のウィンドウを作るときの条件は、次のとおりです。 - 同じアプリで作るウィンドウには、それぞれ異なる `Title` を指定すること ## ダッシュボード: 操作できる `DashboardWindow` は、SteamVR のダッシュボードにタブとして出ます。`Title` が、そのタブの名前になります。ポインターの座標が届くので、`GestureDetector` で組み立てたボタンを押せます。 ```csharp app.CreateWindow(new DashboardWindow { Title = "Panel", Child = panel }); ``` `DashboardWindow` の表示サイズは SteamVR が管理するため、`Dpm` の指定が効かない場合があります。 操作する物は、この種類で作ります。押せる部品の組み立て方は、[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)にあります。 ## 空間に固定: 表示専用 `WorldSpaceWindow` は、ワールドの座標に固定されます。座標は、Standing の追跡座標系で、プレイエリアの床の中央が原点、前方が -Z 方向、単位はメートルです。`Position` を省略すると、中央から前方 1m、高さ 1m に出ます。 ```csharp app.CreateWindow(new WorldSpaceWindow { Title = "Clock", Position = new Vector3(0f, 1.5f, -1f), Child = clock, }); ``` この種類は表示専用です。コントローラーのレーザー操作を受け取れないので、`GestureDetector` のコールバックは呼ばれません(追跡: #182)。時計や字幕のように、見るだけの物に向いています。 ## 手首などに追従: 表示専用 `DeviceTrackedWindow` は、`Target` に指定したデバイスに追従します。`Target` には `TrackedDevice.LeftController`、`TrackedDevice.RightController`、`TrackedDevice.HMD` を指定できます。`Offset` でデバイスからの位置を、`Rotation` で向きを決めます。`TrackedDevice` は `FloatSoda.OVR.Overlay` の名前空間にあり、`Vector3` は `System.Numerics` のものです。 ```csharp app.CreateWindow(new DeviceTrackedWindow { Title = "Left Hand", Target = TrackedDevice.LeftController, Offset = new Vector3(-0.1f, 0, 0.2f), Child = clock, }); ``` この種類も表示専用です(追跡: #182)。手元に常に出しておきたい時計のような物に向いています。 ## 大きさの決まり方 `WorldSpaceWindow` と `DeviceTrackedWindow` の実際の大きさは、`Dpm` と、中身のピクセル数で決まります。`DashboardWindow` の表示サイズは、前の節のとおり SteamVR が管理します。`Dpm` は 1m あたりのピクセル数で、既定は 4000 です。幅 1000px のウィジェットは、実寸で 25cm になります。 `Size` を指定しないウィンドウは、中身のレイアウト結果にサイズが追従します。`Size` を指定すると、その大きさで中身をレイアウトします。`Dpm` は `FloatSoda.Abstractions.Geometries`、`SKSize` は `SkiaSharp` の名前空間にあります。 ```csharp app.CreateWindow(new WorldSpaceWindow { Title = "Fixed", Dpm = new Dpm(1000), Size = new SKSize(800, 600), Child = panel, }); ``` `Size` を指定しないウィンドウには、既知の問題があります。起動したあとに中身の大きさが変わっても、SteamVR 上の表示が、最初の大きさのまま更新されません。中身の大きさが変わるウィンドウには、`Size` を指定します(追跡: #285)。 ## 自分の物に合う種類の選び方 押して操作する物は、`DashboardWindow` を選びます。見るだけの物は、空間のどこかに置くなら `WorldSpaceWindow`、手元に付けたいなら `DeviceTrackedWindow` を選びます。 `WorldSpaceWindow` と `DeviceTrackedWindow` では、コントローラーのレーザー操作を受け取れません(追跡: #182)。できないことの一覧は、[できないこと・まだ使えないもの](/reference/limitations/)にあります。 選んだ種類で最初の画面を作るときは、[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)に進みます。開発中の見た目の確認には、HMD を被らずに窓へ出せる `DesktopWindow` を使えます([HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/))。 # 起動のコードは何をしているの? > チュートリアルで貼った起動のコードは、ホストを作る、FloatSoda を登録する、ウィンドウを予約する、動き始める、終わる、の5つをしています。各行の意味が分かると、AI が書き換えたコードを点検でき、起動に失敗したときも原因を探せます。 ## 起動のコードは、5つのことをしている チュートリアルで貼った起動のコードは、ホストを作る、FloatSoda を登録する、ウィンドウを予約する、動き始める、終わる、の5つをしています。各行の意味が分かると、AI が書き換えたコードを点検でき、起動に失敗したときも原因を探せます。 次のコードの番号が、5つのうち最初の4つに対応します。5つ目の「終わる」は、コードに書かれた行ではなく、`RunAsync` の中で起きます。このコードは、`AddFloatSoda()` を設定なしで呼ぶ最小の形です。アプリごとに別の `AppKey` を指定する形は、「ホストの準備」の節で説明します。 ```csharp var builder = Host.CreateApplicationBuilder(args); // 1. ホストを作る builder.Services.AddFloatSoda(); // 2. FloatSoda を登録する using var host = builder.Build(); var app = host.Services.GetRequiredService(); // 3. ウィンドウを作る FloatSodaApp を取り出す app.CreateWindow(new DashboardWindow // 3. ウィンドウを予約する { Title = "MyDashboard", Child = root, }); await host.RunAsync(); // 4. 動き始める ``` ## ホストの準備: Host.CreateApplicationBuilder と AddFloatSoda Generic Host は、設定、ログ、サービスの登録(依存性注入)、起動と終了の管理をまとめて持つ、.NET の標準の土台です。FloatSoda は、独自の起動の仕組みを作らず、この土台の上で動きます。詳しい説明は、.NET の公式資料の[.NET Generic Host](https://learn.microsoft.com/dotnet/core/extensions/generic-host)にあります。 `Host.CreateApplicationBuilder(args)` は、その土台の builder を作ります。次の `AddFloatSoda()` が登録するもののうち、起動のコードを読むために知っておきたいものを挙げます。内部の登録のすべてを示すものではありません。 - FloatSoda の設定(`FloatSodaOptions`) - SteamVR がアプリを見分ける `AppKey` を持つ、アプリの情報(`OVRAppInfo`) - フレームの間隔を管理する仕組み(`IFramePacer`) - `FloatSodaApp` - ホストが動き始めたときに FloatSoda を動かす役の `FloatSodaHostedService` 設定を変えたいときは、`AddFloatSoda(new FloatSodaOptions { ... })` の形で渡します。`AppKey` の既定は `FloatSoda` で、アプリごとに別の値を指定します。 ## アプリの取り出しと、ウィンドウの予約 `host.Services.GetRequiredService()` は、登録済みの `FloatSodaApp` を取り出します。ウィンドウを作る操作 `CreateWindow` が、この `FloatSodaApp` にあるためです。 `CreateWindow` は、ウィンドウの作成を予約します。その場では作りません。メインループが動き始めて、最初の1周を回るときに、予約が実行されます。そのため、`RunAsync` の前に `CreateWindow` を呼んでも、ウィンドウは動き始めたあとに出ます。 `builder.Build()` の時点では、SteamVR にはつながりません。つなぐのは、`RunAsync` で動き始めたあとです。 ## RunAsync で動き始める `host.RunAsync()` を呼ぶと、ホストが `FloatSodaHostedService` を始めます。FloatSoda は、次の順に動き始めます。 1. FloatSoda 専用のメインスレッドを作る。Windows では STA として作る 2. そのスレッドで OpenVR を初期化する 3. 初期化に成功したら、ホストへ「起動が済んだ」と知らせる 4. メインループを回し続ける メインループの1周で行うことは、[FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/)にあります。 ## 起動に失敗するとき 起動の失敗は、止まる場合と続く場合に分かれます。 | 何が失敗したか | 起動はどうなるか | 見える表示 | | :-- | :-- | :-- | | OpenVR の初期化 | 止まる。`RunAsync` が例外で終わる | 「OpenVRの初期化に失敗しました」のログ | | アプリの識別(SteamVR への登録) | 続く。警告だけが出る | 「SteamVRへのアプリケーション識別に失敗しました」のログ | | コントローラー入力の初期化 | 続く。オーバーレイは表示される | 「アクション入力の初期化に失敗しました」のログ | HMD を接続していない、または SteamVR を起動していないと、OpenVR の初期化で止まります。止まったときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)で、メッセージから原因を切り分けます。 ## 終わるとき 通常の終了は、2つの場面で始まります。SteamVR が終了を知らせたときと、ホストが止まるときです。メインループが例外で止まったときも終わります([FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/)を参照)。SteamVR を閉じると、FloatSoda のアプリも終わります。ホストが止まるのは、コンソールで Ctrl+C を押したときなどです。 メインループが止まれば、ホストも止まって、`RunAsync` が終わります。 ホストの設定、ログ、裏で動く処理を足したいときは、[設定やログ、裏で動く処理を足す](/guide/4-build/mechanics/use-generic-host/)に進みます。 # 値が変わると、画面が変わる仕組み > FloatSoda では、変数の値を書き換えるだけでは、画面の再構築は予約されず、画面は変わりません。画面を変えるには、SetState() を呼びます。この違いを知らないと、AI が書いたコードで「押しても数字が増えない」ときに、直し方が分かりません。 ## 値を直接書き換えても、画面は変わらない FloatSoda では、変数の値を書き換えるだけでは、画面の再構築は予約されず、画面は変わりません。画面を変えるには、`SetState()` を呼びます。この違いを知らないと、AI が書いたコードで「押しても数字が増えない」ときに、直し方が分かりません。 uGUI では、画面の部品が実体として残っていて、`text.text = "..."` のように値を直接書き換えます。FloatSoda では、ウィジェットは「こういう画面にしたい」を書いた設計図で、直接は書き換えません。値が変わったら、新しい設計図を作り直して、変わった部分だけを画面に反映します。uGUI から来た人向けの対応は、[uGUI から来た人へ](/guide/3-prepare/from-ugui/)にあります。 次のコードは、押しても画面が変わらない例です。`_count` は増えますが、作り直しが予約されないので、`Build()` は呼ばれません。 ```csharp OnTap = () => _count++, ``` `SetState()` で包むと、画面が変わります。 ```csharp OnTap = () => SetState(() => _count++), ``` ## ウィジェットは作り直される設計図で、状態は State が持つ ウィジェットは不変です。一度作ったウィジェットのプロパティは、あとから書き換えられません。変わる値は、ウィジェットではなく `State` が持ちます。 置き場所は、値が変わるかどうかで決めます。 | 値の性質 | 置き場所 | 例 | | :-- | :-- | :-- | | ウィジェットを作るときに、外から渡す | ウィジェットのプロパティ | ボタンのラベル、色 | | 画面の中で変わる | `State` のフィールド | 押した回数、オンかオフか | 渡す値を変えるときは、新しいウィジェットを作ります。 `State` を持つウィジェットは、`StatefulWidget` を継承します。`State` を継承したクラスに、変わる値と `Build()` を書きます。 ```csharp public record Counter : StatefulWidget { public override State CreateState() => new CounterState(); } public class CounterState : State { private int _count; public override Widget Build(IBuildContext context) => new GestureDetector { OnTap = () => SetState(() => _count++), Child = new Text($"押した回数: {_count}"), }; } ``` `Build()` は、作り直しのたびに呼ばれ、新しいウィジェットを返します。それでも `_count` が消えないのは、`State` が作り直しをまたいで残るためです。 ## SetState を呼ぶと、次のフレームで何が起きるか `SetState()` は、渡された処理を実行して、このウィジェットに再構築を要求します。作り直しは、その場では行われません。予約されたあとの再構築の処理で、変更のあったウィジェットが、親から順に作り直されます。 ```text SetState(...) 状態を変えて、作り直しを予約する ↓ 再構築の処理 Build() が呼ばれる 親から順に、新しいウィジェットを作る ↓ 前のウィジェットと比べる ↓ 変わった部分だけ、レイアウトと描画内容の記録をやり直す ``` そのため、`SetState()` を呼んだ直後の行では、まだ画面は変わっていません。 `SetState()` を呼ぶ条件は、次のとおりです。 - FloatSoda のメインスレッドから呼ぶこと 裏で動く処理から画面を変えたいときは、[設定やログ、裏で動く処理を足す](/guide/4-build/mechanics/use-generic-host/)の方法を使います。 作り直しのとき、古いウィジェットと新しいウィジェットが同じ型で、同じ `Key` なら、対応する要素が使い回されます。`State` もそのまま残ります。型か `Key` が違うと、作り直しではなく入れ替えになり、`State` は捨てられます。 ## 作り直しても、変わった部分だけが描き直される 作り直しは、毎フレームの全描画ではありません。`SetState()` を呼んだウィジェットを作り直し、その結果が変わった部分だけが、レイアウトと描画内容の記録をやり直します。再構築、レイアウト、描画内容の記録は、更新が必要な部分で行います。 このため、値が変わったら `SetState()` を呼ぶ、という書き方で足ります。ツリーの内部の動きは、コントリビューター向けの[フレームワーク層](/contributor/2-understand/framework-layer/)にあります。処理の全体の流れは、[FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/)で読めます。 ## SetState を使わない更新がある 値の変化が描画だけで済むときは、`SetState()` を使わずに更新できます。`FadeTransition` は、`AnimationController` の値を受け取り、`Build()` を呼ばずに、描画だけを更新します。ふわっと出す、点滅させる、といった動きに向いています。作り方は、[アニメーションを付ける](/guide/4-build/screen/add-animation/)にあります。 祖先の値が変わったときに、その値に依存する子孫を作り直す `InheritedWidget` もあります。子孫は、`DependOnInheritedWidgetOfExactType()` で値を参照すると、依存先として登録されます。テーマや設定のように、離れた部品に同じ値を渡す場面で使います。 ## 作り直しが期待どおりに起きないとき 画面が変わらない、または意図しないときに状態が初期値へ戻るときは、次の2つの原因を確かめます。 | 症状 | よくある原因 | 直し方 | | :-- | :-- | :-- | | 値は変わったのに、画面が変わらない | `SetState()` を呼んでいない | 値を変える処理を `SetState(() => ...)` で包む | | 並べ替えたら、値が別の項目に移った | 並ぶ子に `Key` を付けていない | 子ごとに `Key` を付ける。同じ親の子どうしで重複せず、並べ替えの前後で変わらない値を使う。たとえば `Key = new ValueKey(name)` | ```csharp names.Select(name => (Widget)new Counter { Key = new ValueKey(name) }).ToList() ``` 反応しない原因の切り分けは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)で扱います。 画面と状態を、ツール全体でどう分けるかは、[画面と状態の分け方](/guide/7-maintain/separate-ui-and-state/)に進みます。 # 設定やログ、裏で動く処理を足す > FloatSoda の起動には、.NET の Generic Host が使われています。そのため、設定ファイル、ログ、サービスの登録、裏で動く処理は、FloatSoda 用に作り直さず、.NET の標準の書き方でそのまま足せます。たとえば、設定ファイルの値で動きを変え、裏で動く処理の結果を画面に出す、といった物を形… ## この手順でできること FloatSoda の起動には、.NET の Generic Host が使われています。そのため、設定ファイル、ログ、サービスの登録、裏で動く処理は、FloatSoda 用に作り直さず、.NET の標準の書き方でそのまま足せます。たとえば、設定ファイルの値で動きを変え、裏で動く処理の結果を画面に出す、といった物を形にできます。 この手順では、次の4つを順に足します。 - 設定: 設定ファイルの値を読む - ログ: コンソールにログを出す - サービス: 自分のクラスを登録して、画面から取り出す - 裏で動く処理: `BackgroundService` を足して、結果を画面に出す 終わると、設定の値がコンソールに出て、ログが流れ、サービスの値と裏の処理の結果が画面に出ます。足したいものだけを選んで読んでもかまいませんが、手順3は手順4の前提です。 手順を足し合わせるときの条件は、次のとおりです。 - 設定の読み込み、ログの設定、サービスの登録は、すべて `builder.Build()` より前に置くこと - `builder.Build()` は1回だけ呼ぶこと ## 前提条件 - [最初のオーバーレイを作る](/tutorials/first-overlay/)を終え、`Host.CreateApplicationBuilder` を使う起動のコードが動くこと - 起動のコードの各行の意味が分かっていること - 起動のコードを編集し、ビルド結果と実行時のログを確認できること 起動のコードの意味が分からない場合は、[起動のコードは何をしているの?](/guide/4-build/mechanics/startup-and-host/)を先に読みます。 ## 手順1: 設定の値を読む `builder.Configuration` から値を読み、`FloatSodaOptions` に渡します。次の例は、フレームレートの目標値を設定ファイルで決めます。値が無いときは 60 にします。 まず、プロジェクトに `appsettings.json` を作り、ビルドしたときに出力先へコピーされるようにします。 ```json { "FloatSoda": { "TargetFrameRate": 90 } } ``` ```xml ``` 次に、起動のコードで値を読みます。 ```csharp var builder = Host.CreateApplicationBuilder(args); var frameRate = builder.Configuration.GetValue("FloatSoda:TargetFrameRate", 60); Console.WriteLine($"TargetFrameRate: {frameRate}"); builder.Services.AddFloatSoda(new FloatSodaOptions { TargetFrameRate = frameRate }); ``` `TargetFrameRate` に 0 以下の値を渡すと、`AddFloatSoda` が `ArgumentOutOfRangeException` を出します。 **できた?** 起動したとき、コンソールに `TargetFrameRate: 90` と出れば、手順1は完了です。`appsettings.json` の値を変えて再起動すると、表示も変わります。
できなかったら 表示が `TargetFrameRate: 60` のままのときは、`appsettings.json` が出力先にコピーされているかを確かめます。出力先のフォルダーに `appsettings.json` が無ければ、上の `CopyToOutputDirectory` の指定を見直します。Host は `appsettings.json` を作業フォルダー(コンテンツルート)から読むため、実際に起動したフォルダーに `appsettings.json` があるかも確かめます。
## 手順2: ログを出す `builder.Logging` の設定は、FloatSoda 自身のログにもそのまま効きます。自分のログは、`ILogger` を取り出して書きます。 ```csharp using Microsoft.Extensions.Logging; builder.Logging.SetMinimumLevel(LogLevel.Debug); using var host = builder.Build(); var logger = host.Services.GetRequiredService>(); logger.LogInformation("起動します"); ``` **できた?** コンソールに `起動します` が出て、ウィンドウを作ったあとに、`を作成しました` の形のログが続けば、手順2は完了です。後者は FloatSoda 自身が出すログで、`<Title>` にはウィンドウの `Title` が入ります。 <details> <summary>できなかったら</summary> ログが何も出ないときは、`SetMinimumLevel` の指定と、`appsettings.json` の `Logging` の指定を見直します。`LogLevel.None` は、ログを出さない指定です。 </details> ## 手順3: サービスを登録して、画面で使う 自分のクラスは、`builder.Services` に登録します。画面のウィジェットから取り出すには、ウィンドウの `Child` を `ServiceProvider` ウィジェットで包んで、`IServiceProvider` を渡します。 ```csharp public class GreetingService { public string Message => "こんにちは"; } ``` ```csharp builder.Services.AddSingleton<GreetingService>(); ``` 画面側のウィジェットでは、`context.GetRequiredService<T>()` で取り出します。 ```csharp public record GreetingView : StatelessWidget { public override Widget Build(IBuildContext context) { var greeting = context.GetRequiredService<GreetingService>(); return new Text(greeting.Message); } } ``` 最後に、ウィンドウを作るところで `ServiceProvider` を挟みます。 ```csharp var app = host.Services.GetRequiredService<FloatSodaApp>(); app.CreateWindow(new DashboardWindow { Dpm = new Dpm(1000), Title = "Greeting", Child = new FloatSoda.Widgets.ServiceProvider { Services = host.Services, Child = new GreetingView(), }, }); ``` ここで `FloatSoda.Widgets.ServiceProvider` と、名前空間つきで書く理由は、`Microsoft.Extensions.DependencyInjection` にも同じ名前の `ServiceProvider` があり、`ServiceProvider` だけでは、どちらか決まらずコンパイルが通らないためです。 **できた?** ダッシュボードのタブ `Greeting` に、`こんにちは` が出れば、手順3は完了です。 <details> <summary>できなかったら</summary> `ServiceScopeが祖先に見つかりません。` というメッセージで例外が出るときは、`GetRequiredService<T>()` を呼ぶウィジェットが、`ServiceProvider` ウィジェットの子孫に置かれているかを確かめます。メッセージにある `ServiceScope` は、この `ServiceProvider` を指しています(文言と型名が食い違っています)。サービスが登録されていないという例外のときは、`builder.Services.Add...` の行があるかを確かめます。 </details> ## 手順4: 裏で動く処理を足す 裏で動く処理は、`BackgroundService` を継承したクラスを作り、`AddHostedService` で足します。FloatSoda 自身も `IHostedService` として登録されているので、同じ仕組みの上で並んで動きます。 裏の処理から画面に値を渡すには、`TaskBuilder<T>` を使います。`TaskBuilder<T>` は、`Task` が終わるのを待って、表示を切り替えるウィジェットです。次の例では、サービスが結果の入れ物(`TaskCompletionSource`)を持ち、裏の処理が3秒後に結果を入れます。 ```csharp public class StatusService { private readonly TaskCompletionSource<string> _ready = new(); public Task<string> Ready => _ready.Task; public void SetReady(string text) => _ready.TrySetResult(text); } public class Worker(StatusService status, ILogger<Worker> logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("裏の処理を始めます"); await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken); status.SetReady("完了しました"); } } ``` ```csharp builder.Services.AddSingleton<StatusService>(); builder.Services.AddHostedService<Worker>(); ``` 画面側は、サービスの `Ready` を `TaskBuilder<string>` に渡します。 ```csharp public record StatusView : StatelessWidget { public override Widget Build(IBuildContext context) { var status = context.GetRequiredService<StatusService>(); return new TaskBuilder<string> { Task = status.Ready, Builder = (_, snapshot) => new Text(snapshot.HasData ? snapshot.Data! : "読み込み中"), }; } } ``` `StatusView` は、手順3と同じように、`ServiceProvider` の子孫に置きます。 別のスレッドから `SetState()` を呼ぶ書き方は避けます。この手順の方法なら、`Task` の完了は、ウィジェットのツリーを処理するメインスレッドに戻されて、`TaskBuilder<T>` が受け取ります。この手順で扱うのは、1回きりの結果の反映です。値を何度も更新し続ける方法は、ここでは扱いません。 **できた?** `Worker` の処理が終わるまでは `読み込み中`、終わったあとは `完了しました` と表示されれば、手順4は完了です。`Worker` は、動き出してから3秒後に処理を終えます。画面が出る前に処理が終わっていれば、`読み込み中` を経ずに `完了しました` が出ることがあります。 <details> <summary>できなかったら</summary> `読み込み中` のまま変わらないときは、`Worker` が起動しているかを、ログで確かめます。`裏の処理を始めます` が出ていなければ、`AddHostedService<Worker>()` の行があるかを見直します。 </details> ## 結果を確かめる 起動して、次の4点を見ます。 - 設定: コンソールに、`appsettings.json` で決めた値が出る - ログ: 自分のログと、FloatSoda のログが、同じ設定で出る - サービス: 画面に、登録したサービスの値が出る - 裏の処理: 画面の表示が、処理の終わりに合わせて切り替わる 作ったものを配るときは、[exe にして配る](/guide/6-distribute/publish-exe/)に進みます。 ## うまくいかないとき 起動で例外が出る、または値が画面に出ないときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)で原因を切り分けます。 # アニメーションを付ける > 画面の一部を、ふわっと出したり点滅させたりするには、AnimationController と FadeTransition で、子の不透明度を時間で変えます。フレームごとに SetState() を呼ぶ必要はありません。この手順を終えると、HMD の中で四角がフェードインとフェードアウトを繰り返します。片道は 1.… ## この手順でできること 画面の一部を、ふわっと出したり点滅させたりするには、`AnimationController` と `FadeTransition` で、子の不透明度を時間で変えます。フレームごとに `SetState()` を呼ぶ必要はありません。この手順を終えると、HMD の中で四角がフェードインとフェードアウトを繰り返します。片道は 1.5 秒です。 この手順で扱うのは、不透明度を変えるアニメーションです。移動や拡大に使える専用のウィジェットは、まだありません(追跡: #219)。目的が移動や拡大の場合は、[できないこと・まだ使えないもの](/reference/limitations/)で、今の範囲を確かめます。 ## 前提条件 - `StatefulWidget` と `SetState()` で値を変えたことがあること。なければ、[最初のオーバーレイを作る](/tutorials/first-overlay/)に戻る - 動かしたい部分が、ウィジェットとして書けていること ## 手順1: アニメーションを動かす State を作る アニメーションを動かす `State` は、`State<T>` の代わりに `TickerProviderState<T>` を継承します。`TickerProviderState<T>` は、フレームごとの時刻の知らせを、アニメーションに渡す役目を持ちます。 ```csharp public record BlinkBox : StatefulWidget<BlinkBox> { public override State<BlinkBox> CreateState() => new BlinkBoxState(); } public class BlinkBoxState : TickerProviderState<BlinkBox> { public override Widget Build(IBuildContext context) => new Align { Alignment = Alignment.Center, Child = new SizedBox { Width = 200, Height = 200, Child = new ColoredBox { Color = Colors.MediumSlateBlue }, }, }; } ``` `using` には、`FloatSoda.Animation`、`FloatSoda.Elements`、`FloatSoda.Geometrics`、`FloatSoda.Widgets`、`FloatSoda.Widgets.Animation`、`FloatSoda.Widgets.Layout`、`FloatSoda.Widgets.Paint` を足します。 起動のコードで、ウィンドウの `Child` を `new BlinkBox()` に置き換えます。 **できた?** コンパイルが通れば、手順1は完了です。 ## 手順2: AnimationController を作って動かす `AnimationController` は、`InitState()` で作ります。`Vsync` には `this` を、`Duration` には片道にかける時間を渡します。作ったあとに `Forward()` を呼ぶと、動き始めます。 ```csharp private AnimationController? _controller; public override void InitState() { _controller = new AnimationController { Vsync = this, Duration = TimeSpan.FromSeconds(1.5), }; _controller.Forward(); } ``` 変化の速さを変えたいときは、`Curve` に `Curves.EaseInOut` のような値を渡します。次のコードは、`InitState()` にある `_controller` を作る部分を置き換えます。 ```csharp _controller = new AnimationController { Vsync = this, Duration = TimeSpan.FromSeconds(1.5), Curve = Curves.EaseInOut, }; ``` `AnimationController` の解放の条件は、次のとおりです。 - 作成した `AnimationController` は、`State` の `Dispose()` で解放すること `State` の `Dispose()` に書くと、ウィジェットが画面から外れるときに解放されます。 ```csharp public override void Dispose() { _controller?.Dispose(); base.Dispose(); } ``` **できた?** 実行して、例外が出なければ、手順2は完了です。この段階では画面は変わりません。コントローラーが動いているかは、手順3のフェードインで確かめます。 <details> <summary>できなかったら</summary> `Vsync` や `Duration` を書き忘れると、コンパイルが通りません。どちらも必須です。例外が出るときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)で原因を切り分けます。 </details> ## 手順3: FadeTransition で子に反映する 動かしたい部分を `FadeTransition` で包み、`Opacity` にコントローラーを渡します。`FadeTransition` は、`SetState()` なしで、描画だけを更新します。 ```csharp public override Widget Build(IBuildContext context) => new FadeTransition { Opacity = _controller!, Child = new Align { Alignment = Alignment.Center, Child = new SizedBox { Width = 200, Height = 200, Child = new ColoredBox { Color = Colors.MediumSlateBlue }, }, }, }; ``` 不透明度を変えない固定値だけでよい場合は、`Opacity` ウィジェットを使います。 **できた?** HMD の中で、四角が1.5秒かけてフェードインすれば、手順3は完了です。フェードインのあとは、そのまま止まります。 ## 手順4: 往復させる 往復させるには、コントローラーの `StatusChanged` で、終わりに着いたら戻し、始めに戻ったら進め直します。`Completed` なら `Reverse()`、`Dismissed` なら `Forward()` を呼びます。 次のコードは、手順2の `InitState()` にある `_controller.Forward();` の1行を置き換えます。 ```csharp _controller.StatusChanged += status => { switch (status) { case AnimationStatus.Completed: _controller.Reverse(); break; case AnimationStatus.Dismissed: _controller.Forward(); break; } }; _controller.Forward(); ``` `StatusChanged` の購読は、`Forward()` を呼ぶ前に書きます。書き終えた `InitState()` と `Dispose()` は、[サンプルの PulseWidget.cs](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverlayApp/PulseWidget.cs) と同じ形です。 **できた?** フェードインとフェードアウトが、止まらずに続けば、手順4は完了です。 ## 結果を確かめる HMD で、次の2点を見ます。 - 四角が、薄くなったり濃くなったりを、繰り返している - しばらく見ていても、途中で止まらない HMD を被らずに確かめたいときは、[HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/)に進みます。作った部品を押せるようにするには、[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)に進みます。 ## うまくいかないとき 動かない、または途中で止まるときは、症状に合うページに進みます。画面が反応しない場合は[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)、動作が重い場合は[負荷を抑える](/guide/5-troubleshoot/reduce-load/)です。 # ボタンやトグルを組み立てる > 今、コントローラーで押せるのは、ダッシュボードのオーバーレイだけです。WorldSpaceWindow と DeviceTrackedWindow は表示専用で、GestureDetector のコールバックが呼ばれません(追跡: #182)。ボタンやトグルを組み立てるときは、DashboardWindow に出しま… ## ボタンが効くオーバーレイの種類 今、コントローラーで押せるのは、ダッシュボードのオーバーレイだけです。`WorldSpaceWindow` と `DeviceTrackedWindow` は表示専用で、`GestureDetector` のコールバックが呼ばれません(追跡: #182)。ボタンやトグルを組み立てるときは、`DashboardWindow` に出します。 `Button` ウィジェットは、まだありません(追跡: #219)。この手順では、`GestureDetector` と見た目のウィジェットを組み合わせて、押せるボタンと、押すたびに切り替わるトグルを作ります。 自分のオーバーレイの種類は、`CreateWindow` に渡している型で確かめます。ダッシュボード以外に出したい場合は、[オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/)で、種類ごとの違いを確かめます。 ## 前提条件 - [最初のオーバーレイを作る](/tutorials/first-overlay/)を終え、ダッシュボードにオーバーレイが出ること - `SetState()` で値を変えると画面が変わることを知っていること - ソースの先頭に、`FloatSoda.Geometrics`、`FloatSoda.Widgets.Gesture`、`FloatSoda.Widgets.Layout`、`FloatSoda.Widgets.Paint`、`FloatSoda.Painting` の `using` があること `SetState()` の仕組みが分からない場合は、[値が変わると、画面が変わる仕組み](/guide/4-build/mechanics/state-and-rebuild/)を先に読みます。 ## 手順1: 押せるボタンを作る 見た目の四角を `GestureDetector` で包み、`OnTap` で `SetState()` を呼びます。`OnTap` は、押して離した時点で呼ばれます。押しただけでは呼ばれません。 ```csharp public record CountButton : StatefulWidget<CountButton> { public override State<CountButton> CreateState() => new CountButtonState(); } public class CountButtonState : State<CountButton> { private int _count; public override Widget Build(IBuildContext context) => new GestureDetector { OnTap = () => SetState(() => _count++), Child = new ClipRoundRect { BorderRadius = BorderRadius.All(Radius.Circular(15)), Child = new SizedBox { Width = 300, Height = 100, Child = new ColoredBox { Color = Colors.CornflowerBlue, Child = new Center { Child = new Text($"押した回数: {_count}") { Style = new TextStyle { Color = Colors.WhiteSmoke, FontSize = 40, FontWeight = 700 }, }, }, }, }, }, }; } ``` 起動のコードで、`DashboardWindow` の `Child` を `new CountButton()` に置き換えます。 **できた?** ダッシュボードで、コントローラーのレーザーを四角に当てて押し、離したときに押した回数が増えれば、手順1は完了です。 <details> <summary>できなかったら</summary> 押しても反応しないときは、ウィンドウが `DashboardWindow` であることを確かめます。`WorldSpaceWindow` と `DeviceTrackedWindow` では、コントローラーのレーザー操作を受け取れません(追跡: #182)。それでも直らないときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)に進みます。 </details> ## 手順2: 押すたびに切り替わるトグルを作る トグルは、ボタンと同じ仕組みで作れます。`bool` の状態を `State` に持ち、`OnTap` で反転させ、状態に合わせて見た目を変えます。 ```csharp public record ToggleButton : StatefulWidget<ToggleButton> { public override State<ToggleButton> CreateState() => new ToggleButtonState(); } public class ToggleButtonState : State<ToggleButton> { private bool _isOn; public override Widget Build(IBuildContext context) => new GestureDetector { OnTap = () => SetState(() => _isOn = !_isOn), Child = new ClipRoundRect { BorderRadius = BorderRadius.All(Radius.Circular(15)), Child = new SizedBox { Width = 300, Height = 100, Child = new ColoredBox { Color = _isOn ? Colors.SeaGreen : Colors.Gray, Child = new Center { Child = new Text(_isOn ? "ON" : "OFF") { Style = new TextStyle { Color = Colors.White, FontSize = 40, FontWeight = 700 }, }, }, }, }, }, }; } ``` 手順1の確認のあとは、`DashboardWindow` の `Child` を `new ToggleButton()` に置き換えます。 **できた?** 押すたびに、緑の `ON` と灰色の `OFF` が切り替われば、手順2は完了です。 ## 手順3: 押せる範囲を決める `GestureDetector` の `Behaviour` の既定は `HitTestBehaviour.DeferToChild` で、子が押された位置を受け取れるときだけ反応します。`ColoredBox` と `Text` は、自分の範囲全体で押された位置を受け取ります。`Padding` や `SizedBox` は受け取らないので、これらの余白だけが残る位置は、押しても反応しません。余白を含む領域全体で反応させるには、`Behaviour = HitTestBehaviour.Opaque` を指定します。 ```csharp new GestureDetector { Behaviour = HitTestBehaviour.Opaque, OnTap = () => SetState(() => _count++), Child = new Padding { Spacing = EdgeInsets.All(40), Child = new Text($"押した回数: {_count}"), }, } ``` `HitTestBehaviour` は、`FloatSoda.Gesture` の名前空間にあります。 **できた?** 文字の周りの余白を押しても、押した回数が増えれば、手順3は完了です。 ボタンを押したまま動かすドラッグは、`OnPanUpdate` で受け取ります。コードは[サンプルの DragBoxWidget.cs](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverlayApp/DragBoxWidget.cs) にあります。ホバーや生のポインターイベントが要るときは、`Listener` と `PointerRegion` を使います。 ## 結果を確かめる ダッシュボードのタブを開き、コントローラーで次の3点を試します。 - ボタンを押すと、値が変わる - トグルを押すたびに、見た目が切り替わる - 余白を含む範囲を押しても、反応する HMD を被らずに確かめたいときは、[HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/)に進みます。ただし、デスクトップで動いても、ダッシュボードで動く確認の代わりにはなりません(追跡: #182)。最後は、ダッシュボードで押して確かめます。 組み立てた部品を使った、通しの例は[表情切り替えパネルを作る](/guide/4-build/screen/expression-switcher/)です。 ## うまくいかないとき 押しても反応しないときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)で原因を切り分けます。 # 表情切り替えパネルを作る > VRChat の表情を、HMD の中のパネルから切り替えたいときは、ボタンを並べたパネルを FloatSoda で作り、押したボタンの番号を OSC の送信処理へ渡します。この手順は、チュートリアルのあとに、自分の物を最後まで作る通しの例です。作例は、FaceEmo で作った表情を OSC で切り替えるパネルです。 ## この手順でできること VRChat の表情を、HMD の中のパネルから切り替えたいときは、ボタンを並べたパネルを FloatSoda で作り、押したボタンの番号を OSC の送信処理へ渡します。この手順は、チュートリアルのあとに、自分の物を最後まで作る通しの例です。作例は、FaceEmo で作った表情を OSC で切り替えるパネルです。 手順は3つです。 1. 表情のボタンを並べたパネルを作る 2. 押したときに、ボタンの番号を渡す接続口を作る 3. OSC の送信を AI に頼んで、接続口につなぐ ダッシュボードのタブに、表情の名前を書いたボタンが格子状に並びます。ボタンを押すと、押したボタンの番号が OSC の送信処理に渡ります。OSC の送信そのものは、FloatSoda の外の話で、FloatSoda は OSC のライブラリに依存していません。この手順では、AI に書いてもらう前提で、渡す値と呼ぶ位置までを作ります。 ## 前提条件 - [最初のオーバーレイを作る](/tutorials/first-overlay/)を終え、ボタンを押すと数字が増えるオーバーレイが動くこと - VRChat の OSC を有効にしていること。有効にする方法は、VRChat の公式資料の[OSC Overview](https://docs.vrchat.com/docs/osc-overview)を見る - 対象のアバターで、使う表情と OSC のパラメーターの設定を確認できていること - コードを AI に書いてもらえる環境があること。AI に FloatSoda の資料を渡す方法は、[AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/)にある ## 手順1: 表情のボタンを並べたパネルを作る ボタンは、[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)の手順1と同じ形で作ります。表情の名前は、1か所の配列にまとめます。ボタンは `Wrap` に入れると、横に並べて、幅に収まらなければ次の行に折り返します。 ```csharp public record ExpressionPanel : StatelessWidget { public static readonly string[] Names = ["Neutral", "Smile", "Angry", "Sad", "Surprised", "Wink"]; public required Action<int> OnSelected { get; init; } public override Widget Build(IBuildContext context) { var buttons = new List<Widget>(); for (var i = 0; i < Names.Length; i++) { var index = i; buttons.Add(new GestureDetector { OnTap = () => OnSelected(index), Child = new ClipRoundRect { BorderRadius = BorderRadius.All(Radius.Circular(15)), Child = new SizedBox { Width = 240, Height = 100, Child = new ColoredBox { Color = Colors.CornflowerBlue, Child = new Center { Child = new Text(Names[index]) { Style = new TextStyle { Color = Colors.White, FontSize = 36, FontWeight = 700 }, }, }, }, }, }, }); } return new Align { Alignment = Alignment.Center, Child = new SizedBox { Width = 780, Child = new Wrap { Spacing = 20, RunSpacing = 20, Children = buttons, }, }, }; } } ``` ループの中で `var index = i;` と書き写すのは、各ボタンが、自分の番号を覚えておくためです。 起動のコードでは、このパネルをダッシュボードのウィンドウに入れます。`Dpm` は `FloatSoda.Abstractions.Geometries` の名前空間にあります。 ```csharp app.CreateWindow(new DashboardWindow { Dpm = new Dpm(1000), Title = "Expression", Child = new ExpressionPanel { OnSelected = index => Console.WriteLine($"expression: {index}") }, }); ``` **できた?** ダッシュボードに、`Expression` のタブが出て、6つのボタンが3つずつ2行で並べば、手順1は完了です。 <details> <summary>できなかったら</summary> ボタンが1列に並ぶ、または並ばないときは、[レイアウトの考え方](/guide/5-troubleshoot/layout-model/)で、大きさの決まり方を確かめます。ボタンを押しても反応しないときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)に進みます。 </details> ## 手順2: 押したときの処理を接続する 接続口は、`ExpressionPanel` の `OnSelected` です。ボタンを押すと、`OnSelected` に、押したボタンの番号(`int`)が渡されます。番号は 0 から始まり、`Names` の並びと同じです。 手順1の起動のコードは、すでに `OnSelected` に、番号をコンソールへ出す処理を渡しています。送信の処理を別のクラスにしたいときは、`builder.Services` に登録して、起動のコードから呼ぶ形にもできます。登録の方法は、[設定やログ、裏で動く処理を足す](/guide/4-build/mechanics/use-generic-host/)にあります。 **できた?** ボタンを押すと、コンソールに `expression: 0` のように、押した番号が出れば、手順2は完了です。 <details> <summary>できなかったら</summary> 番号が出ないときは、ボタンへの入力、`OnSelected` の接続、コンソールの出力先を順に確かめます。入力は、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)に進みます。 </details> ## 手順3: OSC の送信を AI に頼む OSC の送信は、AI に書いてもらいます。頼むときは、次の6点を伝えます。 | 伝えること | 内容 | | :-- | :-- | | 渡される値 | `Action<int>` で渡る、0 から始まるパネル上の番号 | | 送信先のホストとポート | VRChat の OSC の受信先。VRChat の公式資料で確かめた値を渡す | | OSC メッセージのアドレス | 表情を切り替えるパラメーターのアドレス。FaceEmo の資料で確かめた値を渡す | | 値の型 | 送る値の型(int、float、bool のどれか)。FaceEmo の資料で確かめた型を渡す | | 番号から送る値への対応 | パネル上の番号と、送る値の対応表。パネルの番号を、そのまま送る値にできるとは限らない | | 呼ぶ位置 | `ExpressionPanel` の `OnSelected` に渡す関数。`ExpressionPanel` 自体は書き換えない | 頼み方の例は、次のとおりです。 ```text C# で、int 型の表情の番号を受け取って、VRChat に OSC で送る関数 SendExpression(int index) を書いてください。 送信先のホストとポートは <VRChat の資料で確かめた値>、OSC メッセージのアドレスは <FaceEmo の資料で確かめたアドレス>、値の型は <確かめた型> です。 番号から送る値への対応は <対応表> です。 OSC のライブラリは NuGet から入れてください。 ``` できあがったコードは、`OnSelected` に渡します。 ```csharp Child = new ExpressionPanel { OnSelected = SendExpression }, ``` できあがったコードを受け取ったら、次の4点を点検します。 - 送る値が、対応表のとおりに、手順2でコンソールに出た番号から変換されている - 送信先のホストとポートが、VRChat の公式資料と同じ - OSC メッセージのアドレスと値の型が、FaceEmo の資料と同じ - `ExpressionPanel` の中身を、AI が書き換えていない OSC のポートや受信の設定は、FloatSoda の外の話です。直し方はここに書かず、VRChat の公式資料に任せます。 **できた?** ボタンを押すと、VRChat の中で表情が切り替われば、手順3は完了です。 ## 結果を確かめる VRChat を起動し、HMD の中でダッシュボードを開いて、`Expression` のタブのボタンを押します。ボタンごとに、期待した表情へ切り替わるかを見ます。 できあがったツールを配るときは、[exe にして配る](/guide/6-distribute/publish-exe/)に進みます。SteamVR と一緒に起動したいときは、[SteamVR と一緒に起動する](/guide/6-distribute/launch-with-steamvr/)に進みます。 ## うまくいかないとき 押しても表情が変わらないときは、次の順で切り分けます。 1. 手順2のログで、ボタンから番号が渡ることを確認する。出なければ、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)に進む 2. 送信の関数が呼ばれたことと、送信処理の成否を、コンソールのログで確認する。ログがなければ、AI に書いてもらった送信の関数に、ログを出す1行を足してもらう。ログには、実際に確かめた段階だけを出す 3. OSC が送られているのに VRChat が反応しない場合は、FloatSoda の外の話なので、VRChat の公式資料を確かめる # 自分の部品を作る > 画面のコードで同じ組み合わせを何度も書いているなら、その組み合わせに名前を付けて、自分の部品にまとめられます。Unity でよく使う形をプレハブにしておくのと同じ使い方を、C# のコードだけで書けます。このページでは、ラベルの付いた札を例に、状態を持たない部品、外から値を渡す部品、押すと開閉する部品の順に作ります。 画面のコードで同じ組み合わせを何度も書いているなら、その組み合わせに名前を付けて、自分の部品にまとめられます。Unity でよく使う形をプレハブにしておくのと同じ使い方を、C# のコードだけで書けます。このページでは、ラベルの付いた札を例に、状態を持たない部品、外から値を渡す部品、押すと開閉する部品の順に作ります。 ## どちらで作るかを決める 自分の部品は、`StatelessWidget` か `StatefulWidget<T>` を継承して作ります。どちらにするかは、部品の中で値が変わるかどうかで決めます。 | 部品の見た目を決めるもの | 継承する型 | 例 | | :-- | :-- | :-- | | 外から渡された値だけ | `StatelessWidget` | ラベルと色の付いた札 | | 部品の中で変わる値(押すと開く、数が増える) | `StatefulWidget<T>` | 押すたびに開閉する札 | 迷ったら `StatelessWidget` で作り始め、部品の中で値を変えたくなったら `StatefulWidget<T>` に書き換えます。値が変わると画面が変わる仕組みは、[値が変わると、画面が変わる仕組み](/guide/4-build/mechanics/state-and-rebuild/)で説明しています。 このページで作るのは、今ある部品を組み合わせた部品です。自分で絵を描く部品は、Flutter の `CustomPaint` に当たる仕組みがまだないため、作れません(追跡: #171)。できないことの一覧は、リファレンスの[できないこと・まだ使えないもの](/reference/limitations/)にあります。 ## 前提条件 次の2つを済ませてください。 - [最初のオーバーレイを作る](/tutorials/first-overlay/)を終え、ダッシュボードにオーバーレイを出せること - 部品を置くファイルを、1つの部品につき1つ作ること(例: `NameTag.cs`) このページの例は、プロジェクトの名前空間を `MyOverlay` としています。自分のプロジェクトの名前空間に読み替えてください。部品のファイルの先頭には、次の `using` が要ります。 ```csharp using FloatSoda.Elements; using FloatSoda.Geometrics; using FloatSoda.Painting; using FloatSoda.Widgets; using FloatSoda.Widgets.Layout; using FloatSoda.Widgets.Paint; ``` 押す操作を受け取る部品(手順3から)は、`using FloatSoda.Widgets.Gesture;` も足します。 ## 手順1: 状態を持たない部品を作る 何度も書いている組み合わせを、`StatelessWidget` を継承した `record` に移します。組み合わせは、`Build` から返します。 次の例は、背景の色と余白と文字を組み合わせた札です。 ```csharp namespace MyOverlay; public sealed record NameTag : StatelessWidget { public override Widget Build(IBuildContext context) { return new ColoredBox { Color = Colors.SeaGreen, Child = new Padding { Spacing = EdgeInsets.All(16), Child = new Text("VRChat") { Style = new TextStyle { Color = Colors.White, FontSize = 40 } } } }; } } ``` 元の画面では、組み合わせを書いていた場所を `new NameTag()` に置き換えます。 ```csharp app.CreateWindow(new DashboardWindow { Title = "MyOverlay", Child = new NameTag(), }); ``` **できた?** 部品に置き換える前と同じ見た目で、ダッシュボードに札が出る。 <details> <summary>できなかったら</summary> 何も出ないときや、見た目が変わったときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/#文字や画像が出ない)の「文字や画像が出ない」を開きます。 </details> ## 手順2: 外から値を渡す 手順1の札は、文字と色が決め打ちです。部品を使う側から値を渡せるように、値を `init` のプロパティにします。必ず渡してほしい値には `required` を付けます。 ```csharp public sealed record NameTag : StatelessWidget { /// <summary>札に出す文字。</summary> public required string Label { get; init; } /// <summary>札の背景の色。</summary> public Color Color { get; init; } = Colors.SeaGreen; public override Widget Build(IBuildContext context) { return new ColoredBox { Color = Color, Child = new Padding { Spacing = EdgeInsets.All(16), Child = new Text(Label) { Style = new TextStyle { Color = Colors.White, FontSize = 40 } } } }; } } ``` 使う側は、オブジェクト初期化子で値を渡します。FloatSoda の部品は、どれもコンストラクタの引数ではなく、プロパティで値を受け取ります。自分の部品も同じ形にしておくと、FloatSoda の部品と同じ書き方で並べられます。 ```csharp new Row { Children = { new NameTag { Label = "VRChat" }, new NameTag { Label = "Booth", Color = Colors.Tomato }, }, } ``` `List<T>` のような参照型の値をプロパティで渡すときは、値を変えるたびに新しいリストを作って渡します。部品は `record` なので、作り直しのときに前と同じ値の部品は、等しいとみなされて作り直されません。リストは中身ではなく参照で比べられるため、同じリストの中身だけを書き換えて渡すと、部品の見た目は変わりません。 ```csharp // 変わらない: 同じリストの中身だけを書き換えている SetState(() => _items.Add("Booth")); // 変わる: 新しいリストを作って渡している SetState(() => _items = [.. _items, "Booth"]); ``` **できた?** 文字と色の違う札が、2つ並んで出る。 ## 手順3: 状態を持つ部品を作る 押すと開閉する札のように、部品の中で値が変わる部品は、`StatefulWidget<T>` で作ります。`StatefulWidget<T>` を継承した `record` には外から渡す値を置き、`State<T>` を継承した `class` には部品の中で変わる値を置きます。状態の中から自分のプロパティを読むときは、`Widget` を使います。 ```csharp public sealed record FoldableTag : StatefulWidget<FoldableTag> { /// <summary>札に出す見出し。</summary> public required string Title { get; init; } /// <summary>開いたときに出す説明。</summary> public required string Detail { get; init; } public override State<FoldableTag> CreateState() => new FoldableTagState(); } public sealed class FoldableTagState : State<FoldableTag> { private bool _open; public override Widget Build(IBuildContext context) { var children = new List<Widget> { new NameTag { Label = Widget!.Title }, }; if (_open) { children.Add(new Text(Widget.Detail) { Style = new TextStyle { Color = Colors.White, FontSize = 30 } }); } return new GestureDetector { OnTap = () => SetState(() => _open = !_open), Child = new Column { Children = children }, }; } } ``` 値を変えるときは、`SetState()` の中で変えます。`SetState()` の外で変えた値は、画面に出ません。 状態を作ったときに一度だけ行う準備は `InitState()` に、部品が画面から外れるときの後片付けは `Dispose()` に書きます。どちらも `State<T>` を継承した `class` で上書きします。 押せるのは、ダッシュボードに出したオーバーレイだけです(追跡: #182)。押せる範囲の決め方は、[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)で説明しています。 **できた?** 札を押すたびに、説明が出たり隠れたりする。 <details> <summary>できなかったら</summary> 押しても変わらないときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/#数字や文字が変わらない)の「数字や文字が変わらない」を開きます。 </details> ## 手順4: 押されたことを外へ知らせる 部品の中で起きたことを、部品を使う側で受け取るには、`On` で始まる名前のプロパティを持たせます。値を渡さないときは `Action?`、値を渡すときは `Action<T>?` にします。 手順3の `FoldableTag` に、開閉を知らせるプロパティを足します。開いたら `true`、閉じたら `false` を渡します。 ```csharp public sealed record FoldableTag : StatefulWidget<FoldableTag> { public required string Title { get; init; } public required string Detail { get; init; } /// <summary>開いたり閉じたりしたときに呼ばれる。開いたら true を渡す。</summary> public Action<bool>? OnToggled { get; init; } public override State<FoldableTag> CreateState() => new FoldableTagState(); } ``` 状態の側では、押したときに値を変えたあとで、`Widget!.OnToggled` を呼びます。使う側が何も渡していないときに備えて、`?.Invoke` で呼びます。 ```csharp private void Toggle() { SetState(() => _open = !_open); Widget!.OnToggled?.Invoke(_open); } ``` `Build` の `GestureDetector` は、`OnTap = Toggle` に書き換えます。使う側では、受け取った値で自分の状態を変えます。次の例は、札を開いた回数を数える画面です。 ```csharp public sealed record TagScreen : StatefulWidget<TagScreen> { public override State<TagScreen> CreateState() => new TagScreenState(); } public sealed class TagScreenState : State<TagScreen> { private int _opened; public override Widget Build(IBuildContext context) { return new Column { Children = { new FoldableTag { Title = "FaceEmo", Detail = "表情を切り替える", OnToggled = open => { if (open) SetState(() => _opened++); }, }, new Text($"開いた回数: {_opened}") { Style = new TextStyle { Color = Colors.White, FontSize = 30 } }, }, }; } } ``` **できた?** 札を開くたびに、「開いた回数」の数字が1つ増える。 ## 結果を確かめる 作った部品について、次の3点を確かめます。 1. 別の画面でも、同じ部品を `new NameTag { ... }` のように呼んで使える 2. 値の違う部品を並べると、それぞれの値で出る 3. 部品を押したことが、`On` で始まるプロパティを通して、使う側に届く 見た目だけなら、HMD を被らずに確かめられます。方法は、[HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/)にあります。 ## うまくいかないとき 部品にしたら動かなくなったときは、症状に合わせて次を確かめます。 | 症状 | 確かめること | | :-- | :-- | | 値を渡しても見た目が変わらない | 前と同じ値を渡していないか。`List<T>` なら、新しいリストを作って渡しているか(手順2) | | 同じ種類の部品を並べ替えると、開いた状態が別の部品に移る | 部品に `Key` を付けて、どの部品かを区別できるようにする | | 押しても反応しない | [反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/#ボタンを押しても反応しない)の「ボタンを押しても反応しない」 | 自分で絵を描く部品を作りたいときは、`CustomPaint` に当たる仕組みがまだないため、作れません(追跡: #171)。ここまでで解決しないときは、ガイドの[docs で見つからないとき](/guide/#docs-で見つからないとき)を開いてください。 # 見た目を整える > 文字の色や大きさ、背景、角丸、余白は、すべて C# のコードで指定します。見た目は、C# のウィジェットで指定します。シーンやプレハブを編集する手順は要りません。AI に頼むときも、自分で直すときも、同じコードの同じ場所を直せば、見た目が変わります。 ## この手順でできること 文字の色や大きさ、背景、角丸、余白は、すべて C# のコードで指定します。見た目は、C# のウィジェットで指定します。シーンやプレハブを編集する手順は要りません。AI に頼むときも、自分で直すときも、同じコードの同じ場所を直せば、見た目が変わります。 この手順では、4つを順に足して、1枚のカードを作ります。終わると、HMD の中に、角が丸く、白い縁取りの付いた青紫色のカードが出ます。カードの中には、大きな太字のタイトルと、説明の文字が並びます。 1. 文字の色と大きさを指定する 2. 背景色を付けて、角を丸める 3. 余白を取る 4. 画面全体の文字書式を1か所で決める ## 前提条件 - [最初のオーバーレイを作る](/tutorials/first-overlay/)を終え、ウィジェットを `Align` や `SizedBox` で組めること - ソースの先頭に、`FloatSoda.Painting`、`FloatSoda.Geometrics`、`FloatSoda.Widgets`、`FloatSoda.Widgets.Layout`、`FloatSoda.Widgets.Paint` の `using` があること ## 手順1: 文字の色と大きさを指定する 文字は `Text` ウィジェットで出します。色、大きさ、太さは、`Style` に渡す `TextStyle` で指定します。 ```csharp new Text("タイトル") { Style = new TextStyle { Color = Colors.WhiteSmoke, FontSize = 56, FontWeight = 700, }, } ``` `FontWeight` は数値で、太字は 700 です。指定を省くと、大きさは 30、色は黒になります。色は `Colors.WhiteSmoke` のような名前のほか、`new Color(124, 205, 255)` のように、赤、緑、青の値でも指定できます。ほかの書き方は[色の指定](#色の指定)にまとめています。 日本語の文字が出ないときは、`Font` にフォントの名前を指定します。`SystemFontProvider` は、`FloatSoda.Core.Providers` の名前空間にあります。 ```csharp Style = new TextStyle { Font = new SystemFontProvider("Meiryo") } ``` **できた?** HMD の中で、文字の色と大きさが変わっていれば、手順1は完了です。 ## 手順2: 背景色を付けて、角を丸める 背景と角丸は、`DecoratedBox` と `BoxDecoration` でまとめて指定します。`BoxDecoration` には、背景の色(`Color`)、角の丸み(`BorderRadius`)、縁取り(`Border`)を渡せます。 ```csharp new DecoratedBox { Decoration = new BoxDecoration { Color = Colors.SlateBlue, BorderRadius = BorderRadius.All(Radius.Circular(24)), Border = Border.All(new BorderSide { Color = Colors.White, Width = 4 }), }, Child = new Text("タイトル"), } ``` 角丸が要らず、単色の背景だけでよいときは、`ColoredBox { Color = ... }` が短く書けます。子の中身ごと角丸で切り抜きたいときは、`ClipRoundRect` を使います。 **できた?** 文字の後ろに色が付き、角が丸くなっていれば、手順2は完了です。 ## 手順3: 余白を取る 文字と背景の縁のあいだの余白は、`Padding` と `EdgeInsets` で取ります。次のコードの `decoration` には、手順2で作った `BoxDecoration` を渡します。`Padding` の余白は、`Spacing` に渡します。`DecoratedBox` の `Child` を `Padding` で包みます。 ```csharp new DecoratedBox { Decoration = decoration, Child = new Padding { Spacing = EdgeInsets.Symmetric(vertical: 24, horizontal: 48), Child = new Text("タイトル"), }, } ``` 四方向に同じ余白を取るなら `EdgeInsets.All(24)`、縦と横で変えるなら `EdgeInsets.Symmetric(vertical: 24, horizontal: 48)` です。 **できた?** 文字が背景の縁に張り付かず、余白が空いていれば、手順3は完了です。 ## 手順4: 画面全体の文字書式を1か所で決める `DefaultTextStyle` で画面の外側を包むと、子孫の `Text` すべてに、同じ文字書式が渡ります。文字が複数ある画面で、色や大きさを1か所で揃えられます。次のコードの `card` は、手順3までに組んだカードのウィジェットです。 ```csharp new DefaultTextStyle { Style = new TextStyle { Color = Colors.WhiteSmoke, FontSize = 36 }, Child = card, } ``` `Text` の `Style` を個別に指定すると、その指定が優先されます。優先順位は、`Text` に書いた指定、`DefaultTextStyle`、既定値の順です。指定は項目ごとに引き継がれるので、タイトルだけ大きくしたいときは、そのタイトルの `Text` にだけ `FontSize = 56` を書きます。色は `DefaultTextStyle` の値になります。 ここまでを合わせたカード全体は、次のとおりです。 ```csharp public record Card : StatelessWidget { public override Widget Build(IBuildContext context) => new DefaultTextStyle { Style = new TextStyle { Color = Colors.WhiteSmoke, FontSize = 36 }, Child = new Align { Alignment = Alignment.Center, Child = new DecoratedBox { Decoration = new BoxDecoration { Color = Colors.SlateBlue, BorderRadius = BorderRadius.All(Radius.Circular(24)), Border = Border.All(new BorderSide { Color = Colors.White, Width = 4 }), }, Child = new Padding { Spacing = EdgeInsets.Symmetric(vertical: 24, horizontal: 48), Child = new Column { Children = { new Text("タイトル") { Style = new TextStyle { FontSize = 56, FontWeight = 700 } }, new Text("説明の文章"), }, }, }, }, }, }; } ``` 組み込みの、見た目をまとめて切り替える仕組み(テーマやデザインシステム)は、今はありません(追跡: #219)。この手順では、色や文字書式を、使う場所のコードで指定します。 起動のコードで、ウィンドウの `Child` を `new Card()` に置き換えます。 **できた?** 複数の文字の書式が、1か所の指定で揃っていれば、手順4は完了です。 ## 結果を確かめる HMD で、画面全体が指定どおりかを見ます。 - カードの背景色、角丸、縁取りが指定どおりに出ている - タイトルが説明より大きく、太く出ている - 文字が背景の縁に張り付いていない HMD を被らずに確かめたいときは、[HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/)に進みます。見た目を整えたら、次は[アニメーションを付ける](/guide/4-build/screen/add-animation/)に進みます。 ## うまくいかないとき 見た目が指定と違う、または文字が出ないときは、症状に合うページに進みます。大きさや位置が思ったとおりでない場合は[レイアウトの考え方](/guide/5-troubleshoot/layout-model/)、例外が出る場合は[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)です。 ## 色の指定 色は、`FloatSoda.Geometrics` の名前空間にある `Color` 型で指定します。`ColoredBox`、`Container`、`TextStyle`、`BoxDecoration`、`BorderSide` の `Color` は、どれもこの型です。作り方は次のとおりで、どれで作っても同じ `Color` 型の値になります。 | 書き方 | 例 | 向いている場面 | | :-- | :-- | :-- | | `Colors.名前` | `Colors.Tomato` | CSS の名前付きの色で指定する。`Colors.Transparent` は透明な黒 | | `new Color(r, g, b)` | `new Color(124, 205, 255)` | 赤、緑、青を、それぞれ 0〜255 の整数で指定する | | `new Color(r, g, b, a)` | `new Color(124, 205, 255, 128)` | 4つ目に不透明度を 0〜255 で足す | | `Color.FromArgb(0xAARRGGBB)` | `Color.FromArgb(0xFF2196F3)` | 16進数の整数で指定する。Flutter の `Color(0xFF2196F3)` と同じ並び | | `Color.FromArgb(a, r, g, b)` | `Color.FromArgb(255, 33, 150, 243)` | 不透明度を先頭にして、成分を 0〜255 で渡す | | `Color.FromRgbo(r, g, b, opacity)` | `Color.FromRgbo(255, 0, 0, 0.5)` | 不透明度を 0.0〜1.0 の小数で指定する。範囲の外か `NaN` を渡すと `ArgumentOutOfRangeException` | | `Color.Parse("#RRGGBB")` | `Color.Parse("#2196F3")` | ウェブカラーの文字列で指定する。`#RGB`、`#RGBA`、`#RRGGBB`、`#RRGGBBAA` を受け付け、`#` は省略できる。大文字と小文字は区別しない | 不透明度 `Alpha` は、0 で完全な透明、255 で完全な不透明です。`new Color(r, g, b)` のように不透明度を省くと、255 になります。作った色の不透明度だけを変えるときは、`color with { Alpha = 128 }` と書きます。 16進数で不透明度を書くときは、並び順に気を付けます。`Color.Parse` の8桁は、不透明度が末尾の `#RRGGBBAA` です。`Color.FromArgb` の整数は、不透明度が先頭の `0xAARRGGBB` です。たとえば、`Color.Parse("#FF000080")` と `Color.FromArgb(0x80FF0000)` は、どちらも同じ半透明の赤になります。 `new Color()` と `default(Color)` は、全成分が 0 の透明な黒です。この色で塗っても、画面には何も見えません。`new Color(...)` で作るときは、赤、緑、青の3つを必ず渡します。 # レイアウトの考え方 > 子の大きさは、子が自分で決めるのではなく、親が渡す制約の範囲で決まります。たとえば SizedBox で幅 100 を指定しても、親が「幅は 400 ちょうど」と決めていれば、子の幅は 400 になります。 ## 大きさは、親の制約と子の希望で決まる 子の大きさは、子が自分で決めるのではなく、親が渡す制約の範囲で決まります。たとえば `SizedBox` で幅 100 を指定しても、親が「幅は 400 ちょうど」と決めていれば、子の幅は 400 になります。 uGUI の `RectTransform` は、アンカーとオフセットで「親のどこに置くか」を子が指定します。FloatSoda は逆向きです。親が「この範囲の大きさなら使ってよい」と伝え、子がその範囲の中で希望の大きさを返します。思った大きさにならないときは、自分の画面の親子関係を上から順にたどり、どの親がどんな制約を渡しているかを見直してください。 ## 制約は上から下へ、サイズは下から上へ流れる レイアウトは、制約が親から子へ下り、サイズが子から親へ上る、という 1 つの規則で行われます。親は子に制約を渡します。子はその範囲で大きさを決め、親に返します。親は返ってきた大きさをもとに、子を配置します。 制約は `BoxConstraints` で表します。最小の幅と高さ、最大の幅と高さの 4 つの値を持ちます。 | 制約の形 | 最小と最大 | 子に起きること | | :-- | :-- | :-- | | 厳密(tight) | 最小と最大が同じ | 子の大きさが 1 つに決まる | | ゆるい(loose) | 最小が 0、最大が上限 | 0 から上限までの好きな大きさを選べる | | 上限なし | 最大が無限大 | 上限がなく、子が希望の大きさを選ぶ。大きさの決まり方はウィジェットによって異なる | この規則は `README.md` のレンダリングライフサイクル図の「制約↓ サイズ↑」と同じものです。以降の節は、この規則で説明できる典型の現象です。 ## SizedBox を指定したのに、大きさが変わらない `SizedBox` は、指定した幅や高さを、子の制約に反映するウィジェットです。省略した軸は固定しません。ただし、親の制約が厳しい(tight)と、`SizedBox` が渡そうとした値は親の制約の範囲に収められます。 たとえばウィンドウに `Size` を指定すると、ルートの子は、その大きさちょうどの制約を受け取ります。ここに `SizedBox` を直接置くと、幅と高さの指定は効きません。 ```csharp app.CreateWindow(new DashboardWindow { Title = "Layout", Size = new SKSize(400, 300), Child = new SizedBox { Width = 100, Height = 100, Child = new ColoredBox { Color = Colors.Tomato }, }, }); ``` この例の四角は 100 x 100 ではなく、400 x 300 で描かれます。直し方は、`Align` か `Center` で包んで、制約をゆるめることです。 ```csharp Child = new Center { Child = new SizedBox { Width = 100, Height = 100, Child = new ColoredBox { Color = Colors.Tomato }, }, }, ``` `Center` は子に「0 から 400 x 300 までの好きな大きさでよい」という制約を渡すので、`SizedBox` の 100 x 100 がそのまま通ります。`SizedBox` の挙動は、サンプル [FloatSoda.Samples.SizedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.SizedBox/README.md) でも確かめられます。 ## Align で包んだら、中央に寄る `Align` は、子を `Alignment` で指定した位置に置くウィジェットです。親から受け取った領域いっぱいに広がり、その中に子を配置します。`Center` は `Alignment.Center` を固定した `Align` です。 前の節で `Center` を足すと四角が中央に寄ったのは、この動きの結果です。`Align` が 400 x 300 の領域いっぱいに広がり、100 x 100 の子を中央に置いています。 親の最大の幅や高さが無限大の軸では、`Align` は広がる先がないので、その軸だけ子の大きさに合わせます。もう一方の軸は、その軸の制約に従います。`Size` を省略したウィンドウのルートに `Align` を置くと、この状態になります。位置の指定は `Alignment.TopLeft` などの 9 つのプリセットか、`new Alignment(0.5f, -0.25f)` のような任意の値で行います。 ## Row と Column で、余りの領域を分ける `Row` と `Column`(まとめて `Flex`)は、複数の子を 1 列に並べます。並べる方向を主軸、それと直交する方向を交差軸と呼びます。`Row` の主軸は水平、`Column` の主軸は垂直です。 並べ方は次の順で決まります。 1. `Expanded` / `Flexible` で包んでいない子(`Spacer` を除く)を、先に自分の希望の大きさで置く 2. 主軸方向に残った領域を、`Expanded` / `Flexible` / `Spacer` の `Flex` の比率で分ける `Expanded` は割り当てられた領域を使い切るように子を引き伸ばし、`Flexible` は割り当てられた領域を上限として子に大きさを選ばせます。`Spacer` は何も描かない `Expanded` で、子の間の空白に使います。 子の大きさの合計が主軸の最大を超えると、領域からはみ出します。はみ出すときは、固定の幅や高さの子を小さくします。主軸の最大が有限であれば、`Expanded` / `Flexible` で包んで、残りの領域に収める方法もあります。`Expanded` の使い方は、サンプル [FloatSoda.Samples.Expanded](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Expanded/README.md) にあります。 `Stack` は並べずに重ねるウィジェットで、`Positioned` で、`Stack` の各辺からの距離として子の位置を指定します。子へ渡す制約を作り替えたいときは、`ConstrainedBox`、`LimitedBox`、`ConstraintsTransformBox`、`UnconstrainedBox` を使います。 ## ウィンドウの大きさは、Size を指定するかで変わる ウィンドウの大きさは、`Size` を指定するかどうかで決まり方が変わります。 | `Size` | ルートの子が受け取る制約 | ウィンドウの大きさ | | :-- | :-- | :-- | | 指定する | その大きさちょうど(tight) | `Size` のとおり | | 省略する | 上限なし | 子のレイアウト結果に追従する | 画面の大きさを自分で決めたいときは、`Size` を指定します。中身に合わせたいときは、省略します。 長さの単位はピクセルです。実際の大きさは `Dpm`(1 メートルあたりのピクセル数)で決まり、既定は 4000 です。1 ピクセルが 0.25 mm なので、幅 1000 ピクセルのウィジェットは実寸 25 cm になります。`Dpm` は `DashboardWindow` では表示サイズに影響しない場合があります。ダッシュボードのオーバーレイの大きさは SteamVR が管理するためです。 `Size` を省略したウィンドウには、既知の問題があります。起動後に中身の大きさが変わっても、SteamVR 上の表示は最初の大きさのままになります(追跡: #285)。中身の大きさが変わるウィンドウには `Size` を指定してください。 ## Flutter を知っている人へ 制約が下り、サイズが上るという基本の規則は Flutter と同じです。個々のウィジェットで Flutter と挙動が違う箇所は、リファレンスの [Flutter の読み替え表](/reference/flutter-mapping/) にまとめています。 症状が残るときは、ほかのページで切り分けます。エラーが出ずに動かないときは [反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)、例外が出るときは [エラーを読んで直す](/guide/5-troubleshoot/read-errors/) を読んでください。どちらでも解けないときの行き先は、ガイドの [docs で見つからないとき](/guide/#docs-で見つからないとき) にあります。 # 反応しないときに切り分ける > このページは、エラーが出ないのに期待どおりに動かないときの切り分け手順です。症状ごとに、FloatSoda の制約によるものか、書き間違いかを分けて直します。 ## 最初に、症状を1つ選ぶ このページは、エラーが出ないのに期待どおりに動かないときの切り分け手順です。症状ごとに、FloatSoda の制約によるものか、書き間違いかを分けて直します。 最初に、次の 2 点を確かめてください。 - 動かしている窓の種類(`DashboardWindow`、`WorldSpaceWindow`、`DeviceTrackedWindow`) - コンソールのログに、エラーや警告が出ていないか ログに例外やエラーが出ているときは、このページではなく [エラーを読んで直す](/guide/5-troubleshoot/read-errors/) へ進みます。出ていなければ、症状に合う節を選びます。 | 症状 | 進む先 | | :-- | :-- | | ボタンを押しても何も起きない | [ボタンを押しても反応しない](#ボタンを押しても反応しない) | | 値は変わっているのに、数字や文字が変わらない | [数字や文字が変わらない](#数字や文字が変わらない) | | 文字や画像が出ない | [文字や画像が出ない](#文字や画像が出ない) | | 中身が変わっても、窓の大きさが変わらない | [表示の大きさが更新されない](#表示の大きさが更新されない) | | `WorldSpaceWindow` や `DeviceTrackedWindow` で押せない | [ほかの種類のオーバーレイで試した場合](#ほかの種類のオーバーレイで試した場合) | ## ボタンを押しても反応しない 押しても何も起きないときは、次の順に確かめます。 1. 窓の種類が `DashboardWindow` であることを確かめる 2. 押す範囲に、ヒットテストに応じる子があることを確かめる 3. 上に重なって入力を塞ぐウィジェットがないことを確かめる 4. `OnTap` が、離した時点で呼ばれる前提で動いていることを確かめる 各項目の理由は次のとおりです。 - 窓の種類: ポインタの座標が届くのは `DashboardWindow` だけです。`WorldSpaceWindow` と `DeviceTrackedWindow` では、`GestureDetector` のコールバックが呼ばれません(追跡: #182)。デスクトップの窓で反応しても、ダッシュボードで反応する証明にはなりません。 - 押す範囲: `Behaviour` の既定は `HitTestBehaviour.DeferToChild` で、子が押された位置を受け取れる範囲でだけ反応します。`ColoredBox` と `Text` は自分の範囲全体で受け取りますが、`Padding` や `SizedBox` の余白は受け取りません。空白を含む領域全体で反応させるには、`Behaviour = HitTestBehaviour.Opaque` を指定します。 - 重なり: `Stack` などで重ねたウィジェットの上側に、`AbsorbPointer` や `Opaque` の `GestureDetector` があると、下側に入力が届きません。入力を素通りさせたい上側のウィジェットは、`IgnorePointer` で包みます。 - `OnTap`: 押した時点ではなく、離した時点でタップが成立して呼ばれます。押したまま 18 ピクセル(`GestureConstants.TouchSlop`)を超えて動かすと、タップは成立せず、`OnTap` は呼ばれません。そのとき `OnPanUpdate` などを指定していれば、ドラッグとして扱われます。 ```csharp new GestureDetector { Behaviour = HitTestBehaviour.Opaque, OnTap = () => SetState(() => _count++), Child = new ColoredBox { Color = Colors.CornflowerBlue, Child = new SizedBox { Width = 220, Height = 64 }, }, } ``` `Button` などの UI コンポーネントは、まだ提供していません(追跡: #126)。ボタンは `GestureDetector` で組み立てます(組み立て方は [ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/))。 **できた?** ボタンを押すと反応する。 <details> <summary>できなかったら</summary> ダッシュボード以外で押したい場合は、直せません。ポインタ座標の接続が未対応の制約です(追跡: #182)。操作が要る画面を `DashboardWindow` に置いてください。ほかの制約は、リファレンスの [できないこと・まだ使えないもの](/reference/limitations/) にあります。 上の 4 点を確かめて必要な修正をしても反応しないときは、[それでも直らないとき](#それでも直らないとき) へ進みます。 </details> ## 数字や文字が変わらない 値は変わっているのに画面が変わらないときは、まず、値を書き換えるときに `SetState()` を使っているかを確かめます。ウィジェットは、作り直されて初めて新しい値を画面に反映します。`SetState()` は、値の書き換えと作り直しの依頼を一度に行います。 確かめ方は、値を書き換えている行を探すことです。`State` のフィールドを `_count++` のように直接書き換えているだけなら、画面は変わりません。その行を `SetState()` の中に入れます。 ```csharp // 画面が変わらない書き方 _count++; // 画面が変わる書き方 SetState(() => _count++); ``` 仕組みの詳細は [状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/) にあります。 **できた?** 値が変わると画面が変わる。 <details> <summary>できなかったら</summary> `SetState()` を呼んでいるのに変わらないときは、値を書き換えた先が、画面に表示している値と同じ場所かを確かめます。ウィジェットは、作り直しで新しいインスタンスに置き換わることがあり、そのときウィジェットのフィールドに持たせた値は引き継がれません。変わる値は `State` に持たせます。それでも変わらないときは、[それでも直らないとき](#それでも直らないとき) へ進みます。 </details> ## 文字や画像が出ない 文字や画像が出ないときは、次の順に確かめます。 1. **文字の色が背景と同じではないか。** `Text` の既定の色は黒です(`TextStyle` の `Color` を省略したとき)。背景も暗い色だと、文字が見えません。`Color` を明るい色にして試します。 2. **文字の大きさが小さすぎないか。** `TextStyle` の `FontSize` の既定は 30 です。0 以下の値を入れると、描かれないのではなく `ArgumentOutOfRangeException` が出ます。 3. **ウィンドウの `Title` が重複していないか。** 同じ `Title` で 2 つのウィンドウを作ると、後から作ったウィンドウは描画されません。`Title` はウィンドウごとに別の値にします。 4. **画像は、読み込みが終わるまで何も出ないことを踏まえているか。** `Image` の読み込みは非同期です。`LoadingBuilder` と `ErrorBuilder` を指定しなければ、読み込み中も、読み込みに失敗したときも、何も表示されません。失敗してもアプリは停止しません。`ErrorBuilder` を指定すると、失敗に気づけます。 5. **画像とアイコンに、`FloatSoda.Widgets.Paint` 名前空間の `Image` と `Icon` を使っているか。** 画像は `Image` に `FileImageProvider` を渡して表示します。 `ErrorBuilder` には、エラーを受け取ってウィジェットを返す関数(`Func<IBuildContext, Exception, Widget>`)を渡します。次の例は、読み込みに失敗したときに、エラーの文を `Text` で出します。 ```csharp new Image { Provider = new FileImageProvider("photo.png"), ErrorBuilder = (context, error) => new Text($"読み込めません: {error.Message}"), } ``` **できた?** 文字や画像が出る。 <details> <summary>できなかったら</summary> 色、大きさ、`Title` の重複、画像の読み込みの 4 つが当てはまらないときは、[それでも直らないとき](#それでも直らないとき) へ進みます。文字の色や大きさの指定方法は [見た目を整える](/guide/4-build/screen/style-appearance/) にあります。 </details> ## 表示の大きさが更新されない 起動後に中身の大きさが変わっても、SteamVR 上の窓の大きさが最初のままのときは、まず `Size` を省略していないかを確かめます。`Size` を省略したウィンドウは、最初のレイアウト結果の大きさで表示され、その後に中身が大きくなっても更新されません(追跡: #285)。 中身の大きさが変わるウィンドウには、`Size` を指定します。 ```csharp app.CreateWindow(new DashboardWindow { Title = "MyDashboard", Size = new SKSize(600, 400), Child = root, }); ``` 大きさの決まり方の詳細は、[レイアウトの考え方](/guide/5-troubleshoot/layout-model/#ウィンドウの大きさはsize-を指定するかで変わる) にあります。 **できた?** 表示の大きさが指定どおりになる。 <details> <summary>できなかったら</summary> `Size` を指定しても大きさが変わらないときは、ダッシュボードのオーバーレイの大きさを SteamVR が管理していることが原因の可能性があります。その場合は、[オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/) で `DashboardWindow` の大きさの扱いを確かめます。それでも直らないときは、[それでも直らないとき](#それでも直らないとき) へ進みます。 </details> ## ほかの種類のオーバーレイで試した場合 `WorldSpaceWindow` や `DeviceTrackedWindow` で押せないのは、書き間違いではなく制約です。この 2 種類では、ポインタの座標を使うウィジェット(`GestureDetector` など)が動きません(追跡: #182)。コントローラーのボタンで操作するなら、アクション入力を使えます。操作が要る画面は `DashboardWindow` に置き、ダッシュボードに替えて試してください。3 種類の違いは [オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/) にあります。 ## それでも直らないとき 直らないときの行き先は、ガイドの [docs で見つからないとき](/guide/#docs-で見つからないとき) にあります。聞くときは、次の情報を添えると、原因を絞りやすくなります。 - 症状(何をしたら、何が起きなかったか) - 窓の種類(`DashboardWindow` など) - 再現するコード(貼り付けるだけで動く最小のもの) - FloatSoda のバージョン # エラーを読んで直す > このページは、起動したときや実行中に、例外やエラーのログが出たときの手順です。出ているメッセージから原因を探し、直す操作を 1 つ行います。 ## 最初に、エラーのメッセージを控える このページは、起動したときや実行中に、例外やエラーのログが出たときの手順です。出ているメッセージから原因を探し、直す操作を 1 つ行います。 最初に、コンソールに出ているメッセージを控えます。控えるのは次の 3 つです。 - 例外の型(`VRInitializeException` や `InvalidOperationException` など) - メッセージ全文 - ログの先頭の語(`OpenVRの初期化に失敗しました`、`描画エラー` など) 控えたら、次の表で当たる節を探します。 | 出ているもの | 進む先 | | :-- | :-- | | 起動の直後に `VRInitializeException` が出て、アプリが終わる | [起動で例外が出る: SteamVR または HMD につながらない](#起動で例外が出る-steamvr-または-hmd-につながらない) | | 起動時に警告が出るが、オーバーレイは表示される | [起動で警告が出るが、表示はされる](#起動で警告が出るが表示はされる) | | 動いていたアプリが、ログを出して終わる | [動いていたアプリが途中で止まる](#動いていたアプリが途中で止まる) | | `ArgumentOutOfRangeException`、`TypeInitializationException`、`FormatException` | [渡した値が不正だと言われる](#渡した値が不正だと言われる) | | `InvalidOperationException`(`…が祖先に見つかりません`) | [ウィンドウやサービスが祖先に見つからないと言われる](#ウィンドウやサービスが祖先に見つからないと言われる) | AI に貼って聞くときは、メッセージ全文に加えて、FloatSoda のバージョンと、動かした窓の種類(`DashboardWindow` など)を添えると、原因を絞りやすくなります。 エラーが出ずに動かないときは、このページではなく [反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/) と [レイアウトの考え方](/guide/5-troubleshoot/layout-model/) を読んでください。 ## 起動で例外が出る: SteamVR または HMD につながらない 起動の直後に `VRInitializeException` が出るのは、OpenVR(SteamVR の API)の初期化に失敗したときです。例外のメッセージの末尾の丸かっこに、失敗の種類が入ります。コンソールには、次のようなログも出ます。 ```text OpenVRの初期化に失敗しました: HMDが見つからないか、接続されていません。 (Init_HmdNotFound) ``` メッセージの文言は、実行環境の言語(UI カルチャ)で日本語と英語が切り替わります。末尾の丸かっこにある名前(`Init_`、`Driver_`、`IPC_`、`Compositor_` などで始まる)は、言語によらず同じです。主な失敗の種類は次のとおりです。 | 末尾の名前 | メッセージ(日本語) | 原因と直し方 | | :-- | :-- | :-- | | `Init_HmdNotFound` | HMDが見つからないか、接続されていません。 | HMD を PC に接続し、SteamVR に HMD が認識されていることを確かめてから、起動し直す | | `Init_InstallationNotFound` | OpenVRのインストールが見つかりません。 | SteamVR をインストールする | | `Init_VRClientDLLNotFound` | vrclient.dllが見つかりません。 | SteamVR のインストール状態を確かめ、壊れていれば再インストールする | `--desktop` でデスクトップの窓に表示する場合でも、HMD の接続と SteamVR の起動は必要です。HMD を接続していない PC では、`Init_HmdNotFound` で起動に失敗します(追跡: #140)。この制約は、アプリの起動に関するものです。HMD を接続した PC で、HMD を被らずに画面を確かめる方法は [HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/) にあります。 SteamVR を先に起動してから、アプリを起動し直してください。 **できた?** アプリが起動し、ダッシュボードにオーバーレイが出る。 <details> <summary>できなかったら</summary> 上の表にない失敗の種類が出たときは、末尾の名前とメッセージ全文を控えて、[それでも直らないとき](#それでも直らないとき) へ進みます。SteamVR 自体が壊れている場合は、FloatSoda の外の問題です。直し方はこのページに書かず、SteamVR の公式の資料を参照してください。 </details> ## 起動で警告が出るが、表示はされる 次の 2 つの警告は、出ても起動が続き、オーバーレイは表示されます。 | ログ | 動かなくなる機能 | 直すかどうか | | :-- | :-- | :-- | | `SteamVRへのアプリケーション識別に失敗しました: …` | SteamVR への自動起動の登録など、アプリのマニフェストに関わる機能 | その機能を使う場合だけ直す | | `アクション入力の初期化に失敗しました: …` | コントローラーのボタンやスティックの入力 | コントローラー入力を使う場合だけ直す | 1 つ目は、SteamVR がアプリを識別できないときに出ます。主な原因は、`AppKey` が SteamVR に登録されていないことです。警告の後ろに出るメッセージで、原因を確かめます。自動起動を使う場合は [SteamVR と一緒に起動する](/guide/6-distribute/launch-with-steamvr/) の手順でマニフェストを登録します。2 つ目は、アクション入力の初期化で入力まわりの例外が出たときに出ます。バインディングの問題が候補です。確かめ方は [コントローラーの入力を受け取る](/guide/4-build/input/controller-input/) にあります。 **できた?** 警告の意味が分かり、自分が使う機能に関わるかどうかを判断できた。 ## 動いていたアプリが途中で止まる 次のログを出してアプリが終わったときは、メインループの中で例外が出ています。例外が出るとログを出してループが止まり、アプリが終了します。ログの先頭の語で、どこで起きたかが分かります。 | ログの先頭の語 | 起きた場所 | | :-- | :-- | | `タスクエラー` | メインループに積まれた処理 | | `イベント処理エラー` | SteamVR のイベントとコントローラー入力の処理 | | `描画エラー` | ウィジェットの作り直しとレイアウト、描画 | ログの続きに、例外の型、メッセージ、スタックトレースが出ます。スタックトレースを上から読み、自分のコードのファイル名が出てくる最初の行を探します。その行は、例外につながった呼び出し位置の候補です。例外のメッセージと、その行から呼んでいる処理を合わせて確かめます。`FloatSoda.` で始まる行しかないときは、自分のコードの呼び出しが原因とは限りません。 **できた?** 自分のコードのどの部分で例外が出たか分かる。 <details> <summary>できなかったら</summary> 自分のコードの行が見つからないときは、メッセージ全文とスタックトレースを控えて、[それでも直らないとき](#それでも直らないとき) へ進みます。 </details> ## 渡した値が不正だと言われる 設定や入力に許されない値を渡すと、次の例外が出ます。 | 例外 | 原因 | 直し方 | | :-- | :-- | :-- | | `ArgumentOutOfRangeException`(`TargetFrameRateは1以上である必要があります。`) | `FloatSodaOptions.TargetFrameRate` が 0 以下 | 1 以上の値にする。既定は 60 | | `TypeInitializationException`(`InnerException` が `NotSupportedException`: `InputAction<…> は未対応です。bool / float / Vector2 を使用してください。`) | `InputAction<T>` の `T` が対応外 | `T` を `bool`、`float`、`Vector2` のどれかにする | | `FormatException` | `with` 式で `AppKey` の `Value` に、空の値か使えない文字を含む値を入れた(`new AppKey("...")` の時点では検証されない) | 小文字の英字、数字、ドット、アンダースコアだけで書く | | `ArgumentOutOfRangeException`(`AppKey` の値) | `with` 式で `AppKey` の `Value` に 128 文字以上の値を入れた(`new AppKey("...")` の時点では検証されない) | 127 文字以下にする | ```csharp builder.Services.AddFloatSoda(new FloatSodaOptions { AppKey = new AppKey("example.my_overlay"), TargetFrameRate = 60, }); ``` **できた?** 例外が出なくなる。 ## ウィンドウやサービスが祖先に見つからないと言われる `InvalidOperationException` で「…が祖先に見つかりません」と言われるのは、探しているウィジェットが、画面の上位にないときです。メッセージと原因の対応は次のとおりです。 | メッセージ | 原因 | 直し方 | | :-- | :-- | :-- | | `WindowWidgetが祖先に見つかりません。` | `WindowWidget.Of(context)` を、ウィンドウに包まれていない場所で呼んだ | `CreateWindow` に渡したウィンドウ(`DashboardWindow` など)の子孫のウィジェットで呼ぶ | | `ServiceScopeが祖先に見つかりません。` | `context.GetRequiredService<T>()` を、`ServiceProvider` に包まれていない場所で呼んだ | 画面のルートを `ServiceProvider` で包む | 登録していないサービスを `GetRequiredService<T>()` で取り出したときも、`InvalidOperationException` になります。この場合は、`builder.Services` にそのサービスを登録します。包み方と登録の手順は [Generic Host の仕組みを使う](/guide/4-build/mechanics/use-generic-host/) にあります。 **できた?** 例外が出なくなる。 ## それでも直らないとき 直らないときの行き先は、ガイドの [docs で見つからないとき](/guide/#docs-で見つからないとき) にあります。聞くときは、次の情報を添えてください。 - メッセージ全文(例外の型とスタックトレースを含む) - FloatSoda のバージョン - 動かした窓の種類 - 再現する手順またはコード # 負荷を抑える > このページは、オーバーレイを出すと VR 全体が重くなる、またはオーバーレイの動きだけがカクつくときに、負荷の原因を見つけて減らす手順です。VR の中で動くので、オーバーレイの負荷は、VR 体験全体の快適さに響きます。 ## 最初に、重さの種類を見分ける このページは、オーバーレイを出すと VR 全体が重くなる、またはオーバーレイの動きだけがカクつくときに、負荷の原因を見つけて減らす手順です。VR の中で動くので、オーバーレイの負荷は、VR 体験全体の快適さに響きます。 最初に、重さの種類を見分けます。同じ VR アプリ・同じ操作で、オーバーレイを起動した状態と終了した状態を比べてください。 | 観点 | 当てはまるとき | | :-- | :-- | | オーバーレイを終了しても、VR が重い | VRChat や GPU のドライバーなど、FloatSoda の外に原因がある。このページでは直せない | | オーバーレイを出したときだけ、VR が重い、またはオーバーレイの動きだけがカクつく | FloatSoda のアプリ側で調整できる。手順 1 へ進む | 調整できる箇所は 3 つです。フレームレートの設定、再描画の範囲、更新の頻度です。 ## 手順1: 負荷を測る 変える前の値を控えます。値を控えておくと、調整のたびに、効いたかどうかを数字で比べられます。 測る値は、フレームレートと GPU の使用率の2つです。FloatSoda に計測の機能はないので、SteamVR と Windows の表示を使います。SteamVR でフレームの時間を表示する方法は、Valve の [SteamVR の資料](https://developer.valvesoftware.com/wiki/SteamVR)にあります。 毎回同じ操作(たとえば、ダッシュボードを開いて 30 秒眺める)で測ってください。 **できた?** 変える前の値が手元にある。 ## 手順2: フレームレートの設定を下げる 目標のフレームレートは `FloatSodaOptions.TargetFrameRate` で決まり、既定は 60 です。メインループと描画ループの両方がこの値に合わせて動くので、下げると処理の回数が減ります。 ```csharp builder.Services.AddFloatSoda(new FloatSodaOptions { TargetFrameRate = 30, }); ``` 1 以上の整数を指定します。0 以下にすると、起動時に `ArgumentOutOfRangeException` になります。値を下げると、アニメーションの描き換え回数も減るので、動きは粗くなります。 値を下げて起動し、手順 1 と同じ操作で測り直します。 **できた?** 値の変化が数字で見える。 ## 手順3: 再描画の範囲を絞る FloatSoda は、変わった部分だけを作り直し、そのレイアウトと描画をやり直します。変更のないフレームでは、レイアウトと描画を行いません。範囲を絞ると、やり直す処理が減ります。絞る方法は 2 つあります。 1 つ目は、`RepaintBoundary` で囲むことです。`RepaintBoundary` で囲んだ子は、独立した描画の層に分けられます。囲んだ中だけが変わっても、外側は描き直されません。省けるのは描画だけで、ウィジェットの作り直しとレイアウトは省けません。 ```csharp new Column { Children = { new Text("見出し(変わらない)"), new RepaintBoundary { Child = new CounterWidget(), // 頻繁に変わる部分 }, }, } ``` 2 つ目は、`SetState()` を呼ぶ `State` を小さく分けることです。`SetState()` は、その `State` を持つウィジェットの下を作り直します。頻繁に変わる値は、その値を表示する小さなウィジェットの `State` に持たせると、作り直す範囲が狭くなります。画面全体のルートで `SetState()` を呼ぶと、画面全体が作り直されます。仕組みは [状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/) にあります。 頻繁に変わる部分を囲み、測り直します。 **できた?** 測り直した値が下がり、見た目と操作感が変わっていない。 ## 手順4: 更新の頻度を減らす 更新の頻度を減らす方法は 3 つあります。該当するものを直します。 - タイマーで `SetState()` を呼んでいる場合は、必要な表示頻度に合わせて、更新の間隔を長くすることが望ましい - 不透明度を動かすために `SetState()` を毎フレーム呼んでいる場合は、`FadeTransition` に替えること - 固定の `SizedBox` などで足りる場合は、`IntrinsicWidth` と `IntrinsicHeight` を避けることが望ましい 理由は次のとおりです。画面に見える変化が必要な頻度より短い間隔で更新していると、見えない更新に処理を使います。`FadeTransition` は、アニメーションの値を再描画だけで反映するので、1 フレームごとの `SetState()` が要りません。`IntrinsicWidth` と `IntrinsicHeight` は子の自然な大きさを調べるためにコストが高く、多用すると負荷が上がります。 あわせて、次の 2 点も負荷に関わります。 - 画面のピクセル数が大きいほど、描画する面積が増えます。ピクセル数は `Size` で決まり(省略するとレイアウト結果の大きさになります)、VR 内の実寸は `Dpm` で決まります。`DashboardWindow` では `Dpm` が表示サイズに影響しない場合があります([レイアウトの考え方](/guide/5-troubleshoot/layout-model/#ウィンドウの大きさはsize-を指定するかで変わる))。必要以上に大きな画面にしないことが、負荷を抑える手段になります。 - 複数のウィンドウを同時に出すと、ウィンドウごとにレイアウトと描画の処理が行われます。使っていないウィンドウは作らないでください。 **できた?** 測り直した値が下がる。 ## 結果を確かめる 手順 1 で控えた値と、調整後の値を、同じ操作で測って比べます。調整は 1 つずつ行い、そのたびに測ります。フレームレートが上がった、または GPU の使用率が下がり、動きと操作感が許容できれば、調整は効いています。効果がない、または操作感が悪化した変更は、元に戻します。 ## それでも直らないとき 直らないときの行き先は、ガイドの [docs で見つからないとき](/guide/#docs-で見つからないとき) にあります。聞くときは、次の情報を添えてください。 - 測った値(調整前と調整後、測った道具) - ウィンドウの数と種類 - 画面の大きさ(`Size` と `Dpm`) - `TargetFrameRate` の値 # Booth で売る準備 > オーバーレイを Booth で売るときに、同梱するものと商品ページに書く動作環境をそろえます。終わると、購入者が説明書だけで起動できる状態になり、動かないという問い合わせを減らせます。手順は 4 つです。 ## この手順でできること オーバーレイを Booth で売るときに、同梱するものと商品ページに書く動作環境をそろえます。終わると、購入者が説明書だけで起動できる状態になり、動かないという問い合わせを減らせます。手順は 4 つです。 1. 同梱するものを決める 2. 購入者向けの説明書を書く 3. 動作環境を書く 4. 配布物を確かめる Booth の出品画面の操作は、このページでは扱いません。Booth の公式の資料を参照してください。 ## 前提条件 次の 2 つを済ませてください。 - [exe にして配る](/guide/6-distribute/publish-exe/) の手順 1 から 3 を終えていること - zip にまとめた配布物があること ## 手順1: 同梱するものを決める zip に入れるものは、次の 3 種類です。 | 同梱するもの | 要る理由 | | :-- | :-- | | `.exe` と必要なファイル | 購入者の PC で動かすため。`dotnet publish` の出力をそのまま入れる | | 説明書(テキストファイル) | 起動の順番と操作を伝え、問い合わせを減らすため | | ライセンス表記 | 再配布するライブラリのライセンス条件を守るため | ライセンス表記は、FloatSoda のリポジトリにある [LICENSE](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/LICENSE) と [THIRD_PARTY_NOTICES.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/THIRD_PARTY_NOTICES.md) を入れます。FloatSoda のライセンスは MIT で、MIT ライセンスは、配布物に著作権表示と許諾表示を含めることを条件にしています。`THIRD_PARTY_NOTICES.md` には、Flutter、OpenVR SDK、OVRSharp のライセンスの全文が載っています。 配布物に含まれる、ほかのライブラリ(SkiaSharp など)のライセンスは、各ライブラリの配布元で確かめ、条件が求める表示や文書を同梱してください。`LICENSE` と `THIRD_PARTY_NOTICES.md` だけで足りるとは限りません。法的な判断は、このページでは行いません。 **できた?** 同梱物の一覧が手元にある。 ## 手順2: 購入者向けの説明書を書く 説明書は、初めて見る人が、説明書だけで起動できるように書きます。書く項目は次のとおりです。 1. 動作環境(SteamVR が要ること、OS、HMD) 2. 起動の順番(SteamVR を先に起動し、そのあとに `.exe` を動かす) 3. オーバーレイの出し方(商品で使うウィンドウの種類に合わせた表示手順。ダッシュボードなら、ダッシュボードを開いてタブを選ぶ) 4. 操作の方法(ポインタで押す操作に対応するのが、ダッシュボードのオーバーレイだけであること) 5. 動かないときの連絡先 6. 動作確認した版(商品の版、同梱した FloatSoda の版、動作確認した SteamVR の版) ポインタで押す操作に対応しているのは、ダッシュボードのオーバーレイだけです。`WorldSpaceWindow` と `DeviceTrackedWindow` はポインタで押せないので、これらを使う商品では、「ポインタでは押せない」と書きます(追跡: #182)。コントローラーのボタンで操作できる商品なら、その操作方法も書きます。 FloatSoda は Alpha の段階で、API が変わることがあります。動作確認した版を書いておくと、購入者が、問い合わせのときに版を伝えられます。 SteamVR の起動と同時にオーバーレイを自動で起動させる設定を商品に含める場合は、[SteamVR と一緒に起動する](/guide/6-distribute/launch-with-steamvr/) の手順が要ります。マニフェストの自動生成は検討中で、今は手動です。 説明書の雛形は、このページには載せません。項目の一覧が、このページでの正です。 **できた?** 初めて見る人が、説明書だけで起動できる。 ## 手順3: 動作環境を書く 商品ページには、購入者の PC に要るものを書きます。動作環境の一覧は、リファレンスの [動作環境](/reference/requirements/) と共有しています。商品ページには、そこに載っている実行する PC の項目を写し、商品独自の条件を加えます。実際に確かめた環境と、動作を保証しない環境は分けて書きます。 書く項目は次の 3 点です。 - SteamVR が要ることと、起動してから使うことを書くこと - ポインタで押せるオーバーレイの種類を書くこと(手順 2 の 4 と同じ内容にする) - .NET のランタイムが要るかどうかを書くこと(ランタイムを同梱した配布物なら、要らないと書く) **できた?** 商品ページに動作環境が書かれている。 ## 手順4: 配布物を確かめる 出品する前に、別の PC で、説明書だけを見て起動します。確かめる観点は次のとおりです。 - zip を展開して、`.exe` を起動できる - 説明書の手順どおりに、オーバーレイが出る - 説明書にある操作ができる 自分の PC の開発環境が残っていると、足りないファイルに気づけません。配布先と同じ条件の別の PC で確かめてください。ランタイムを同梱した配布物なら、.NET の SDK もランタイムも入っていない PC で起動できることも確かめます。ユーザーアカウントを分けただけでは、PC に入った SDK やランタイムは残ります。 **できた?** 説明書の手順どおりにオーバーレイが出る。 ## 結果を確かめる 出品の前に、次の 4 点を点検します。 - 同梱物の一覧が、zip の中身と一致している - 説明書に、手順 2 で決めた項目がすべて書かれている - 商品ページの動作環境が、説明書と同じ内容になっている - 別の PC での起動を確かめた 出品後に「動かない」という報告を受けたら、[利用者の環境で動かないとき](/guide/7-maintain/user-environment-issues/) で切り分けます。 ## うまくいかないとき 配布物が別の PC で動かないときは、[exe にして配る](/guide/6-distribute/publish-exe/) と [利用者の環境で動かないとき](/guide/7-maintain/user-environment-issues/) を読んで、切り分けます。 # SteamVR と一緒に起動する > アプリを SteamVR に登録して、SteamVR を起動すると、オーバーレイも自動で起動するようにします。SteamVR を起動するたびに、手で .exe を動かす手間がなくなります。手順は 3 つです。 ## この手順でできること アプリを SteamVR に登録して、SteamVR を起動すると、オーバーレイも自動で起動するようにします。SteamVR を起動するたびに、手で `.exe` を動かす手間がなくなります。手順は 3 つです。 1. アプリのマニフェストを用意する 2. SteamVR に登録する 3. 自動起動を有効にする 「一緒に起動する」は、SteamVR の自動起動の設定を指します。Windows のスタートアップへの登録は扱いません。 マニフェストの自動生成は検討中で、今は手動で用意します(追跡: #139)。マニフェストの書き方と、登録・自動起動の呼び出し方は、OpenVR の公式資料にあります。このページでは、FloatSoda から使う型と、手順の順番を案内します。 ## 前提条件 次の 2 つを済ませてください。 - [exe にして配る](/guide/6-distribute/publish-exe/) の手順 1 を終え、`publish` フォルダーに `.exe` があること - `AppKey` が、アプリごとに固有の値であること `AppKey` は、`FloatSodaOptions` で指定します。空にはできません。使える文字は、小文字の英字、数字、ドット、アンダースコアで、127 文字までです。 ```csharp builder.Services.AddFloatSoda(new FloatSodaOptions { AppKey = new AppKey("example.my_overlay"), }); ``` ## 手順1: アプリのマニフェストを用意する SteamVR にアプリを登録するには、アプリの情報(アプリのキー、`.exe` の場所など)を書いたマニフェストのファイルが要ります。マニフェストの書き方は、OpenVR の公式資料で確かめてください。 - [OpenVR API Documentation](https://github.com/ValveSoftware/openvr/wiki/API-Documentation) マニフェストに書くアプリのキーは、`FloatSodaOptions` の `AppKey` と同じ値にします。 ## 手順2: SteamVR に登録する マニフェストを SteamVR に登録します。FloatSoda からは、`FloatSoda.OVR` の `OVRApplications.AddManifest(path)` で登録できます。これは OpenVR の `IVRApplications` の `AddApplicationManifest` を呼ぶものです。登録の手順と、登録できたかの確かめ方は、OpenVR の公式資料で確かめてください。 - [OpenVR API Documentation](https://github.com/ValveSoftware/openvr/wiki/API-Documentation) ## 手順3: 自動起動を有効にする 登録したアプリの自動起動を有効にします。自動起動は、OpenVR の `IVRApplications` の `SetApplicationAutoLaunch` で切り替えます。FloatSoda では、`OVRApplication` の `AutoLaunch` がこの呼び出しを包んでいます。ただし、`FloatSodaApp` は内部で使う `OVRApplication` を外に出していないため、アプリの中からこのプロパティを切り替える手段は今はありません(追跡: #139)。呼び出し方は、OpenVR の公式資料で確かめてください。 - [OpenVR API Documentation](https://github.com/ValveSoftware/openvr/wiki/API-Documentation) ## 結果を確かめる アプリと SteamVR を終了し、アプリのプロセスが残っていないことを確かめます。そのあと SteamVR だけを起動します。アプリを手で動かさなくても、ダッシュボードにオーバーレイが出れば、設定は効いています。 配布先の PC で自動起動させるときも、手順 2 の登録の実行が、その PC で 1 回必要です。マニフェストを配布物に同梱し、説明書に登録の手順を書いてください([Booth で売る準備](/guide/6-distribute/booth-release/))。 配布先で動かないときは、[利用者の環境で動かないとき](/guide/7-maintain/user-environment-issues/) で切り分けます。 ## うまくいかないとき 自動で起動しないとき、または起動時に `SteamVRへのアプリケーション識別に失敗しました` という警告が出るときは、`AppKey` が SteamVR に登録されていない可能性があります。次の 4 点を確かめます。 - マニフェストに書いたアプリのキーが、`FloatSodaOptions` の `AppKey` と同じ値であること - 手順 2 の登録が成功していること - 自動起動が有効であること - マニフェストのファイルと `.exe` を、登録のあとに移動していないこと 警告が出たときは、警告の後ろに続くメッセージも確かめます。 この警告が出ても、オーバーレイの表示は続きます。止まるのは、自動起動など、マニフェストに関わる機能だけです。警告の詳細は [エラーを読んで直す](/guide/5-troubleshoot/read-errors/#起動で警告が出るが表示はされる) にあります。 なお、コントローラー入力のアクションマニフェストは、自動で生成されます。このページのアプリのマニフェストとは別のものです。 # exe にして配る > 自分の PC で動くオーバーレイを、.NET の SDK を入れていない PC でも動く形にして、zip で渡せます。修正版も、同じ手順で作り直して届けられます。手順は 4 つです。 ## この手順でできること 自分の PC で動くオーバーレイを、.NET の SDK を入れていない PC でも動く形にして、zip で渡せます。修正版も、同じ手順で作り直して届けられます。手順は 4 つです。 1. 配布用にビルドする 2. 開発環境のない PC で動かして確かめる 3. 渡す 4. 修正版を届ける 「配る」は、zip などのファイルとして渡すことを指します。Booth の商品ページの作り方は [Booth で売る準備](/guide/6-distribute/booth-release/) で扱います。 ## 前提条件 次の 3 つがそろっていることを確かめます。 - .NET 10 の SDK が開発用の PC に入っていること - 自分の PC で `dotnet run` して、オーバーレイの動作を確かめ済みであること - 配る相手の PC に SteamVR が入っていること 配る相手の PC の動作環境は、リファレンスの [動作環境](/reference/requirements/) にあります。また、SteamVR に登録するときの `AppKey` は、既定値のままにせず、アプリごとに固有の値にしておきます(`FloatSodaOptions.AppKey`。既定値はすべての FloatSoda アプリで同じです)。 ## 手順1: 配布用にビルドする プロジェクトのフォルダーで、次のコマンドを実行します。 ```powershell dotnet publish -c Release -r win-x64 --self-contained true -p:DebugType=none -p:DebugSymbols=false -o publish ``` `--self-contained true` は、.NET のランタイムを出力に含める指定です。相手の PC に .NET を入れてもらう必要がなくなります。`-r win-x64` は、64 ビットの Windows 向けに作る指定です。 出力先の `publish` フォルダーに、次のファイルが並びます。 - 自分のプロジェクト名の `.exe` と `.dll` - .NET のランタイム(`coreclr.dll`、`hostfxr.dll` など) - ネイティブライブラリ(`openvr_api.dll`、`glfw3.dll`、`libSkiaSharp.dll`、`libHarfBuzzSharp.dll`) 上のコマンドの `-p:DebugType=none` は、自分のプロジェクトの `.pdb` を作らない指定です。SkiaSharp と HarfBuzz のネイティブライブラリ(`libSkiaSharp.pdb`、`libHarfBuzzSharp.pdb`)には、デバッグ用の `.pdb` が付いてきて、出力全体の約半分の大きさを占めます。配るときは使わないので、削除します。 ```powershell Remove-Item publish\*.pdb ``` ランタイムを同梱せず、相手の PC に .NET 10 のランタイムを入れてもらう方法もあります。その場合は `--self-contained true` を `--self-contained false` に替えます。配布物は小さくなりますが、相手に .NET のインストールを頼むことになるため、この手順では同梱する方法を選びます。 **できた?** `publish` フォルダーに、`.exe` と必要なファイルが並んでいる。 ## 手順2: 開発環境のない PC で動かして確かめる 開発環境に依存しない別の PC で、`publish` フォルダーのコピーを動かします。ユーザーアカウントを分けただけでは、PC に入った SDK やランタイムは残ります。次の順に進めます。 1. SteamVR を起動する 2. `publish` フォルダーの `.exe` をダブルクリックする 3. アプリで使う窓の種類に合った場所に、オーバーレイが出ることを見る(`DashboardWindow` なら、ダッシュボードを開く) SteamVR を起動する前に `.exe` を動かすと、起動に失敗することがあります。必ず SteamVR を先に起動してください。 **できた?** ダッシュボードにオーバーレイが出る。 <details> <summary>できなかったら</summary> 動かないときの切り分けは、[利用者の環境で動かないとき](/guide/7-maintain/user-environment-issues/) にあります。コンソールにエラーが出ているときは、[エラーを読んで直す](/guide/5-troubleshoot/read-errors/) を読みます。 </details> ## 手順3: 渡す 動作を確かめた `publish` フォルダーを、zip にまとめます。 ```powershell Compress-Archive -Path publish\* -DestinationPath MyOverlay-1.0.0.zip ``` zip には、`.exe` と必要なファイルのほかに、配布物のライセンス条件が求める表示と文書([Booth で売る準備](/guide/6-distribute/booth-release/) の手順 1)を入れます。さらに、次の内容を書いた説明書(テキストファイル)を入れます。 - 起動の前に、SteamVR を起動すること - 起動したあとに、ダッシュボードにオーバーレイが出ること 説明書に書く項目の全体は、[Booth で売る準備](/guide/6-distribute/booth-release/) の手順 2 にあります。 **できた?** 別の場所で zip を展開しても、動く。 ## 手順4: 修正版を届ける 修正版は、手順 1 から 3 をもう一度行います。版を区別できるよう、zip の名前に版を入れます(例: `MyOverlay-1.0.1.zip`)。`AppKey` は、前の版と同じ値のままにします。SteamVR に登録済みの場合は、更新後も登録した起動先が有効かを確かめます。 届けるときに、利用者へ求める操作は 1 つです。アプリを終了してから、古いフォルダーを削除し、新しい zip を展開し直してもらいます。設定や保存データを同じフォルダーに置いている場合は、削除の前に退避してもらいます。 **できた?** 更新した版を展開したとき、前の版と同じように動く。 ## 結果を確かめる 渡した版を受け取った人に、次の 3 点を確かめてもらいます。 - SteamVR を起動してから `.exe` を動かすと、オーバーレイが出る - 説明書のとおりに操作できる - 版の名前が、渡した zip の名前と一致している 確かめたあとは、次のページに進みます。 - 売る場合: [Booth で売る準備](/guide/6-distribute/booth-release/) - SteamVR の起動と同時にオーバーレイを自動で起動させたい場合: [SteamVR と一緒に起動する](/guide/6-distribute/launch-with-steamvr/) ## うまくいかないとき 公開に失敗する、または相手の PC で動かないときは、[利用者の環境で動かないとき](/guide/7-maintain/user-environment-issues/) で切り分けます。 # 画面と状態の分け方 > ファイルが長くなったら、画面の組み立てと、状態や処理を分けます。変更する場所を追いやすくなります。分けないままだと、画面を直したつもりが通信の処理を壊す、といったことが起きます。 ## 画面と処理が混ざると、直すたびに壊れる ファイルが長くなったら、画面の組み立てと、状態や処理を分けます。変更する場所を追いやすくなります。分けないままだと、画面を直したつもりが通信の処理を壊す、といったことが起きます。 たとえば、次のような 1 つの `State` があるとします。 ```csharp public class PanelState : State<PanelWidget> { private int _count; private readonly HttpClient _http = new(); private Timer? _timer; public override void InitState() { _timer = new Timer(async _ => { var json = await _http.GetStringAsync("http://localhost:8080/count"); File.WriteAllText("count.json", json); SetState(() => _count = int.Parse(json)); }, null, 0, 5000); } // …ここに、100 行の画面の組み立て… } ``` この例は責務の混在を示す反例で、通信やタイマーの実装例ではありません。通信、保存、タイマー、画面が、1 つのクラスに入っています。AI に「ボタンの色を変えて」と頼んだとき、AI は通信の行も同じ場所で読むことになり、関係のない箇所を巻き込んで書き換える余地が生まれます。画面を確かめるにも、通信やタイマーが動く状態で確かめることになり、表示する値を固定して画面だけを確かめにくくなります。 FloatSoda は UI のすべてを C# のコードで書くので、分けたコードは diff で読めて、AI が直しやすくなります。分け方の考え方を、次の節から順に説明します。 ## 画面は「値を受け取って画面を返す」部分に絞る 画面のウィジェットは、受け取った値をどう描くかだけを持つ部品にします。この例のウィジェットは、`init` プロパティで値を受け取る `record` で、値を受け取って画面を返す設計図です。持つものは、表示する値と、押されたときに呼ぶ処理(`Action`)だけです。 ```csharp public record CounterView : StatelessWidget { public required int Count { get; init; } public required Action OnIncrement { get; init; } public override Widget Build(IBuildContext context) => new GestureDetector { Behaviour = HitTestBehaviour.Opaque, OnTap = OnIncrement, Child = new Text($"カウント: {Count}"), }; } ``` この `CounterView` は、通信も保存も知りません。何を表示し、押されたら何を呼ぶかを、外から渡されます。画面に置くものを決めるときは、「この行は、渡された値から画面を作るために要るか」を問います。要らない行は、次の節で外に出します。 ## 状態と処理は、画面の外に置く 状態と処理は、画面とは別のクラスに置きます。外に出すものの例は、通信、保存、タイマーです。 ```csharp public sealed class CounterModel : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; public int Count { get; private set; } public void Increment() { Count++; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); } } ``` `CounterModel` は、ウィジェットを 1 つも知りません。値が変わったら `PropertyChanged` で知らせるだけです。通信や保存を足すときも、このクラスの中に足します。画面へ渡す値と `Action` が変わらなければ、画面のファイルは変わりません。 ## 画面の外の状態を、画面から取り出す 外に置いた状態を画面から取り出す手段は、2 つあります。違いは、取り出すものの性質です。 | 手段 | 向いているもの | 値が変わったときの動き | | :-- | :-- | :-- | | `ServiceProvider` | アプリの間ずっと使うオブジェクト(通信、保存、状態を持つクラス) | 取り出しただけでは画面は変わらない。`ListenableBuilder` などで変更通知を受ける | | `InheritedWidget` | 画面の上から下へ渡す値(設定、見た目の指定など) | 値が変わると、その値に依存する子孫が作り直される | `ServiceProvider` は、画面のルートを包み、`IServiceProvider` を子孫へ渡します。子孫は `context.GetRequiredService<T>()` で取り出します。取り出した状態の変更は、`ListenableBuilder` で受け取ります。`ListenableBuilder` は、`INotifyPropertyChanged` の通知のたびに、自分自身を作り直し、`ChildBuilder` で新しいウィジェットを作ります。`ChildBuilder` が返したウィジェットから下は、通常の作り直しと同じ規則で更新されます。 ```csharp app.CreateWindow(new DashboardWindow { Title = "Counter", Child = new FloatSoda.Widgets.ServiceProvider { Services = host.Services, Child = new CounterScreen(), }, }); public record CounterScreen : StatelessWidget { public override Widget Build(IBuildContext context) { var model = context.GetRequiredService<CounterModel>(); return new ListenableBuilder { Listenable = model, ChildBuilder = _ => new CounterView { Count = model.Count, OnIncrement = model.Increment, }, }; } } ``` `CounterModel` は、起動のコードで `builder.Services.AddSingleton<CounterModel>()` として登録します。登録の手順は [Generic Host の仕組みを使う](/guide/4-build/mechanics/use-generic-host/) にあります。 `ListenableBuilder` は、変更通知を、ウィジェットがマウントされたスレッド(通常は FloatSoda のメインループ)から出すことを前提にしています。別のスレッドから `PropertyChanged` を起こすと、`InvalidOperationException` になります。バックグラウンドのスレッドで状態を更新するときは、メインループに処理を戻してから `PropertyChanged` を起こします。 `InheritedWidget` は、自作の `record` で作ります。子孫は `context.DependOnInheritedWidgetOfExactType<T>()` で取り出し、その値に依存します。`InheritedWidget` の中の値を書き換えても、通知は起きません。親が作り直されて、新しい `InheritedWidget` が渡され、`UpdateShouldNotify` が `true` を返したときだけ、依存した子孫が作り直されます。 ## 分けると、HMD を被らずに確かめられる 画面が「値を受け取って画面を返す」部品に絞られていると、状態を作らなくても、決めた値で画面を作れます。たとえば `new CounterView { Count = 42, OnIncrement = () => { } }` は、通信が動いていなくても組み立てられます。 組み立てた画面は、`WidgetBitmapRenderer` で画像にして確かめられます。HMD を被る必要はありません。手順は [HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/) にあります。値を変えた画像を何枚か書き出しておくと、AI に画面を直してもらったあとの見た目の変化を、画像で比べられます。 ## 分けすぎない ツールが小さいうちは、分けなくてかまいません。次の 3 つがすべて当てはまるなら、1 つの `State` に置いたままで足ります。 - 通信、保存、タイマーのどれも使わない - 画面のファイルが 1 つで、読み返して全体を追える - 同じ値を、複数の画面で使わない 1 つでも外れたら、状態を外に出す時期です。 分ける対象は、ツール全体の構造です。1 つの部品の中の状態、つまり `SetState()` の仕組みは [状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/) にあります。 FloatSoda は、状態管理の特定のライブラリを強制しません。上の例の `INotifyPropertyChanged` のほかに、自作のクラスや、使い慣れたライブラリを使えます。`FloatSoda.Hooks` は、今は使えません(追跡: #72)。 # 利用者の環境で動かないとき > 自分の PC では動くのに、購入者や友人の PC で動かないという報告を受けたときに、原因を「自分のコード」「相手の環境」「FloatSoda」のどれかに絞ります。相手の環境は見えないので、聞く情報を先に決めておくと、切り分けが進みます。FloatSoda の不具合だった場合は、集めた情報をそのまま Issue にで… ## この手順でできること 自分の PC では動くのに、購入者や友人の PC で動かないという報告を受けたときに、原因を「自分のコード」「相手の環境」「FloatSoda」のどれかに絞ります。相手の環境は見えないので、聞く情報を先に決めておくと、切り分けが進みます。FloatSoda の不具合だった場合は、集めた情報をそのまま Issue にできます。手順は 4 つです。 1. 相手に環境の情報を集めてもらう 2. 自分の PC で再現するかを確かめる 3. 症状から原因を絞る 4. FloatSoda の不具合なら Issue にする 自分の PC で動かないときは、このページではなく [エラーを読んで直す](/guide/5-troubleshoot/read-errors/) を読んでください。 ## 前提条件 次の 2 つがそろっていることを確かめます。 - 自分の PC では動くこと - 相手とやり取りできること - 相手に渡した配布物の版が分かること ## 手順1: 相手に環境の情報を集めてもらう 相手に、次の項目を聞きます。そのまま送れる形にしてあります。 ```text 動かないときの情報を教えてください。分かる範囲で大丈夫です。 1. OS(例: Windows 11) 2. GPU(例: NVIDIA GeForce RTX 3060) 3. HMD(例: Meta Quest 3、Valve Index) 4. SteamVR の版 5. 受け取ったファイルの版(zip の名前) 6. 動かそうとしたオーバーレイの種類(ダッシュボード / 空間に固定 / 手首などに追従) 7. 画面に出たメッセージ(コンソールの文字を、そのまま貼るか、スクリーンショットで) ``` コンソールにログを出す構成のアプリでは、メッセージは、`.exe` を動かしたときに開く黒い窓(コンソール)に出ます。コンソールを開かない構成のアプリでは、そのアプリで定めたログの取得手順を案内します。窓がすぐ閉じるときは、コマンドプロンプトから `.exe` を動かして、出力を残してもらいます。 この項目は、コントリビューター向けの動作検証の報告(落ちたか落ちなかったかと、環境)と同じ形です。そのため、FloatSoda の不具合だったときに、Issue の材料として使えます。FloatSoda のバージョンなど、足りない項目は、配布の記録で補います。 **できた?** 項目が埋まった返事がある。 ## 手順2: 自分の PC で再現するかを確かめる 相手と同じ版、同じ種類のオーバーレイで、自分の PC で動かします。相手に渡した配布物(zip)をそのまま展開して動かすのが確実です。 - 再現した: 自分の PC でも症状を調べられる。手順 3 へ進む - 再現しない: 環境、設定、操作の差を控えて、手順 3 へ進む **できた?** 再現した、または再現しなかったと言える。 ## 手順3: 症状から原因を絞る 症状ごとに、疑う箇所は次のとおりです。 | 症状 | 疑うもの | 確かめること | | :-- | :-- | :-- | | 起動しない、起動の直後に終わる | SteamVR が未起動、HMD が未接続、配布物のファイル不足 | コンソールのメッセージを [エラーを読んで直す](/guide/5-troubleshoot/read-errors/) で引く。配布物に `openvr_api.dll` などのネイティブライブラリがあるか([exe にして配る](/guide/6-distribute/publish-exe/)) | | `Init_HmdNotFound` と出る | HMD が未接続 | HMD の接続を頼む。デスクトップの窓でも HMD の接続が要る(追跡: #140) | | 起動するが、押せない | オーバーレイの種類 | ポインターで操作するなら、窓の種類を確かめる。`WorldSpaceWindow` と `DeviceTrackedWindow` は未対応(追跡: #182)。[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/) | | 表示の大きさが、自分の PC と違う | ピクセル数か、VR 内の実寸かの違い | `Size`、`Dpm`、窓の種類を確かめる。`Size` を省略すると、起動後の大きさの変化が反映されない(追跡: #285)。[レイアウトの考え方](/guide/5-troubleshoot/layout-model/) | HMD 未接続の PC では、デスクトップの窓でも起動に失敗します。相手が HMD なしで試している場合は、この点を最初に疑います。 原因は、次の 4 つのどれかに絞れます。 - 相手の環境: SteamVR の未起動、HMD の未接続 - 配布物の不足: ネイティブライブラリなどのファイル不足 - 自分のコード: 再現した上で、自分のコードを直すと解決する - FloatSoda の不具合の可能性: 上のどれでも説明できない。原因が判断できないときも、手順 4 で Issue に相談する **できた?** 原因が 4 つのどれかに絞れた。 ## 手順4: FloatSoda の不具合なら Issue にする FloatSoda の不具合だと考えられるとき、または原因が FloatSoda にあるのか判断できないときは、Issue にします。原因が相手の環境だった場合は、Issue にせず、相手に直し方を伝えます。 Issue には、Issue のバグ報告テンプレートを使い、テンプレートの必須の欄を埋めます。手順 1 で集めた項目を、対応する欄に写し、足りない項目を補います。FloatSoda のバージョンと .NET SDK のバージョンは、配布者の開発環境の値を書きます。相手の PC の値ではありません。 Issue は公開されます。貼り付ける前に、メッセージにパスや個人情報が入っていないかを確かめてください。Issue の出し方の全体は、コントリビューター向けの [不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/) にあります。 **できた?** Issue が立った、または相手の環境の問題として相手に伝えた。 ## 結果を確かめる 相手に、動いたかどうかを聞きます。直した版を届けるときは、[exe にして配る](/guide/6-distribute/publish-exe/) の手順 4 に従います。 ## うまくいかないとき 原因が絞れないときの行き先は、ガイドの [docs で見つからないとき](/guide/#docs-で見つからないとき) にあります。手順 1 の項目と、手順 2 で再現したかどうかを添えて聞いてください。 # リファレンス > 作例、制約、動作環境、用語、部品を引く リファレンスは、引くための一覧を集めたタブです。読み物ではないので、調べたい場面に合うページを、サイドバーから開いてください。手順や使い方の説明は、[ガイド](/guide/)にあります。 ## 型やプロパティを調べる 型やメンバーの正確な定義は、API リファレンスのタブに置きます。API リファレンスのタブが公開されるまでは、`src/` のソースにある XML ドキュメントコメントで引きます(追跡: #219)。 型の名前が分からないときは、先に[部品を目的から探す](/reference/find-widgets/)を開いてください。目的から部品の名前が分かれば、その名前で定義を引けます。 # 部品を目的から探す > 画面を組むときに使う部品の名前を、目的の言葉から引くための一覧です。部品の使い方はここには書いていません。部品の名前が分かったら、作例で動くコードを確かめられます。正確な定義は、API リファレンスのタブが公開されるまでは、src/ のソースにある XML ドキュメントコメントで引きます(追跡: #219)。 画面を組むときに使う部品の名前を、目的の言葉から引くための一覧です。部品の使い方はここには書いていません。部品の名前が分かったら、作例で動くコードを確かめられます。正確な定義は、API リファレンスのタブが公開されるまでは、`src/` のソースにある XML ドキュメントコメントで引きます(追跡: #219)。 ## この一覧の使い方 目的を「並べる」「大きさと余白を決める」「見た目を整える」「文字や画像を出す」「押す・反応する」「動かす」「出し分ける・値を伝える」「出す場所を決める」の8つに分けています。自分の目的に近い節へ進んでください。 各節の表は、部品、違いの1文、作例の3列です。部品の名前は、そのまま `src/` のソースで型名として引けます。 作例の列は、`samples/` にある作例の README へのリンクです。作例がない部品は「なし」と書いています。 この一覧は手で書いています。部品が増えたときは、`src/FloatSoda/Widgets/` の公開された型が正になります。 ## 並べる 横一列、縦一列、重ね、折り返しで並べます。 | 部品 | 違いの1文 | 作例 | | :-- | :-- | :-- | | `Row`、`Column`、`Flex` | 子を横または縦の一列に並べる(`Flex` は `Direction` で向きを選ぶ) | [Flex](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Flex/README.md) | | `Wrap` | 一列に収まらなくなったら、次の行へ折り返して並べる | [Wrap](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Wrap/README.md) | | `Stack`、`Positioned` | 子を重ね、`Positioned` で辺からの距離を指定する | [Stack](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Stack/README.md) | | `IndexedStack` | 子を全部レイアウトし、指定した1つだけを表示する | なし | | `Expanded` | 一列の余った領域を比率で分け、子を割り当てた領域いっぱいに広げる | [Expanded](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Expanded/README.md) | | `Flexible` | 一列の余った領域を比率で分け、割り当てた領域以下の大きさを子に選ばせる(`Fit` で `Expanded` と同じ動きにも切り替えられる) | [Expanded](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Expanded/README.md) | | `Spacer` | 一列の余った領域のうち、比率で決めた分を空白にする | [Expanded](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Expanded/README.md) | ## 大きさと余白を決める 部品の大きさ、余白、位置を決めます。思った大きさにならないときは、ガイドの[レイアウトの考え方](/guide/5-troubleshoot/layout-model/)を開いてください。 | 部品 | 違いの1文 | 作例 | | :-- | :-- | :-- | | `SizedBox` | 親の制約の範囲で、`Width` と `Height` に指定した大きさを子に求める | [SizedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.SizedBox/README.md) | | `Padding` | 子の周りに余白を取る | [Padding](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Padding/README.md) | | `Align`、`Center` | 子を、親の中の指定した位置に置く | [Align](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Align/README.md) | | `ConstrainedBox` | 親から受け取った制約に、追加の寸法の制約を重ねる | なし | | `LimitedBox` | 親の制約に上限がないときだけ、上限を与える | [LimitedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.LimitedBox/README.md) | | `FractionallySizedBox` | 親の制約の最大値に `WidthFactor`・`HeightFactor` を掛けた大きさを、子に求める | [FractionallySizedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.FractionallySizedBox/README.md) | | `AspectRatio` | 縦横比を保って、子の大きさを決める | [AspectRatio](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.AspectRatio/README.md) | | `FittedBox` | 子を拡大または縮小して、領域に収める | [FittedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.FittedBox/README.md) | | `OverflowBox` | 親とは別の最小・最大の制約を子に渡し、自身の領域からのはみ出しを許す | [OverflowBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverflowBox/README.md) | | `SizedOverflowBox` | 自身は `Size` で指定した大きさになり、子には親から受け取った元の制約を渡す | なし | | `ConstraintsTransformBox` | 親から受け取った制約を加工して、子に渡す | [ConstraintsTransformBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ConstraintsTransformBox/README.md) | | `UnconstrainedBox` | `ConstrainedAxis` で指定した軸以外の親の制約を外し、子を自然な大きさで置く | [ConstraintsTransformBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ConstraintsTransformBox/README.md) | | `IntrinsicWidth`、`IntrinsicHeight` | 子の自然な大きさに合わせて、幅または高さを決める(計測の負荷が高いので、多用しない) | なし | ## 見た目を整える 色、角丸、透明、回転、切り抜きを付けます。まとめて整える手順は、ガイドの[見た目を整える](/guide/4-build/screen/style-appearance/)にあります。 | 部品 | 違いの1文 | 作例 | | :-- | :-- | :-- | | `ColoredBox` | 領域を1色で塗りつぶす | [ColoredBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ColoredBox/README.md) | | `DecoratedBox` | 背景色、角丸、ボーダーを描く | [DecoratedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.DecoratedBox/README.md) | | `Container` | 配置・余白・装飾・大きさ・変換をまとめて指定する | [Container](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Container/README.md) | | `Opacity` | 子をまとめて半透明にする | [Opacity](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Opacity/README.md) | | `Transform` | 子を、2次元の行列で変形して描く | [Transform](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Transform/README.md) | | `RotatedBox` | 子を、レイアウトの大きさごと、90度単位で回転する | なし | | `ClipRect`、`ClipRoundRect`、`ClipOval`、`ClipCustomPath` | 子を、四角、角丸の四角、楕円、任意の形で切り抜く | [Clip](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Clip/README.md) | | `RepaintBoundary` | 子の再描画を、周りとは別の単位に分ける | なし | ## 文字や画像を出す 文字、画像、アイコンを出します。 | 部品 | 違いの1文 | 作例 | | :-- | :-- | :-- | | `Text` | 1つの書式で文字列を出す | [Text](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Text/README.md) | | `RichText` | 書式つきの文字列(`TextSpan`)を段落として出す | [Text](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Text/README.md) | | `Image` | 画像ファイルを読み込んで出す | [Image](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Image/README.md) | | `Icon` | アイコンフォントの字を、正方形の領域に出す | なし | `Text` には、折り返しと省略を指定する項目がありません。詳しくは、[できないこと・まだ使えないもの](/reference/limitations/#見た目と文字)を開いてください。 ## 押す・反応する ボタン、ホバー、ドラッグを作ります。ボタンの部品はありません。`GestureDetector` で押された操作を受け取り、見た目は他の部品で組み立てます。 | 部品 | 違いの1文 | 作例 | | :-- | :-- | :-- | | `GestureDetector` | タップとドラッグを検出する | [GestureDetector](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GestureDetector/README.md) | | `Listener` | 押した、離した、動いた、入った、出たという生のポインターイベントを受け取る | [Listener](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Listener/README.md) | | `PointerRegion` | ポインターが領域に入ったこと、出たことを受け取る | [PointerRegion](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PointerRegion/README.md) | | `AbsorbPointer` | 子を描いたまま、ポインター入力を自分で止めて子に届けない | なし | | `IgnorePointer` | 子を描いたまま、子の部分をポインター入力の対象から外す | なし | ポインターの座標が届くのは、ダッシュボードのオーバーレイだけです。詳しくは、[できないこと・まだ使えないもの](/reference/limitations/#置く場所と入力)を開いてください。ボタンの組み立て方は[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)、コントローラーのボタンやトリガーは[コントローラーの入力を受け取る](/guide/4-build/input/controller-input/)にあります。 ## 動かす 値が変わったら画面を変えます。時間で値を動かします。 | 部品 | 違いの1文 | 作例 | | :-- | :-- | :-- | | `StatefulWidget<T>`、`State<T>`、`SetState()` | `State<T>` が持つ値を `SetState()` に渡した処理で書き換え、画面を描き直す | [CounterWidget.cs](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverlayApp/CounterWidget.cs) | | `AnimationController` | 時間に合わせて、0 から 1 の値を動かす | [PulseWidget.cs](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverlayApp/PulseWidget.cs) | | `FadeTransition` | アニメーションの値で、子の透明度を動かす | [PulseWidget.cs](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverlayApp/PulseWidget.cs) | 使えるアニメーションの範囲は、[できないこと・まだ使えないもの](/reference/limitations/#動きと状態)にあります。仕組みは、ガイドの[状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/)と[アニメーションを付ける](/guide/4-build/screen/add-animation/)で説明しています。 ## 出し分ける・値を伝える 条件で出し分けます。離れた部品へ値を渡します。 | 部品 | 違いの1文 | 作例 | | :-- | :-- | :-- | | `Visibility` | 表示するかどうかで、子か代わりの部品のどちらか一方を出す | なし | | `Offstage` | 子をレイアウトしたまま、描画とポインター入力から外す | なし | | `TaskBuilder<T>` | `Task<T>` の進み具合(読み込み中、完了、失敗)で、出す部品を切り替える | なし | | `Builder` | 自分の位置の構築コンテキストで、部品を組み立てる | なし | | `InheritedWidget` | 子孫の部品が参照できる値を持ち、値が変わると参照している部品を組み立て直す | なし | | `DefaultTextStyle` | 木の下にある `Text` へ、文字の書式を伝える | [Text](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Text/README.md) | | `ListenableBuilder` | `INotifyPropertyChanged` の通知のたびに、`ChildBuilder` で部品を組み立て直す | なし | ## 出す場所を決める オーバーレイを SteamVR のどこに出すかを、ウィンドウの種類で決めます。`CreateWindow` に渡します。 | 部品 | 違いの1文 | 作例 | | :-- | :-- | :-- | | `DashboardWindow` | SteamVR のダッシュボードのタブに出し、ポインターの座標が届く | [GettingStarted](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GettingStarted/Program.cs) | | `WorldSpaceWindow` | 空間の決まった位置(メートル単位)に固定して出す | [OverlayApp](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverlayApp/Program.cs) | | `DeviceTrackedWindow` | HMD やコントローラーなどのデバイスに付いて動く | [OverlayApp](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverlayApp/Program.cs) | | `DesktopWindow` | PC のデスクトップのウィンドウに出す | [Flex](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Flex/Program.cs) | SteamVR 上のオーバーレイでポインターの座標が届くのは、ダッシュボードだけです。種類の選び方は、ガイドの[オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/)にあります。 ## 見つからないとき 目的に合う部品が見つからないときは、理由が3つ考えられます。 - まだない: スクロール系の部品や `Button` などは未公開。[できないこと・まだ使えないもの](/reference/limitations/)で確かめる - Flutter の名前で探している: 名前が変わった部品がある。[Flutter の読み替え表](/reference/flutter-mapping/)で、Flutter の名前から引く - 部品を組み合わせて作る: 表にない形の画面は、表の部品を入れ子にして作る。AI に書かせる手順は、ガイドの[AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/)にある 自分で新しい部品を作る場合は、`StatelessWidget`、`StatefulWidget<T>`、`LeafRenderObjectWidget<T>`、`ParentDataWidget<T>` を継承します。 # Flutter の読み替え表 > Flutter で知っている書き方が、FloatSoda でそのまま通るか、変わるか、まだないかを、このページで引けます。Flutter のウィジェットの名前から、該当する節の表を探してください。 Flutter で知っている書き方が、FloatSoda でそのまま通るか、変わるか、まだないかを、このページで引けます。Flutter のウィジェットの名前から、該当する節の表を探してください。 ## この表の使い方 区分は3つです。 - 同じ: 名前も使い方も同じで、挙動も同等 - 違う: 名前や書き方が違う、または見た目や反応が違う - まだない: Flutter にあって、FloatSoda にまだないもの、または実装しないもの FloatSoda は、Flutter の3つの木(ウィジェット、Element、RenderObject)に合わせて作ってあります。ウィジェットの名前、レイアウトの用語(`MainAxisAlignment`、`CrossAxisAlignment`)、`Child` と `Children` は、Flutter と同じ名前です。表の「詳細」の列は、各作例の README の「Flutterとの違い」へのリンクです。差異の詳しい説明は、その README に1か所だけ書いてあり、このページにはコピーしていません。 ## 同じように書けるもの 次のウィジェットは、作例の README で「同等」と確かめたものです。基本の挙動は Flutter と同じです。C# の書き方の違いと未対応の項目は、各行の補足で確かめてください。 | Flutter | FloatSoda | 補足 | 詳細 | | :-- | :-- | :-- | :-- | | [`Align`](https://api.flutter.dev/flutter/widgets/Align-class.html)、[`Center`](https://api.flutter.dev/flutter/widgets/Center-class.html) | `Align`、`Center` | `textDirection` による左右の切り替えがない | [Align](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Align/README.md) | | [`LimitedBox`](https://api.flutter.dev/flutter/widgets/LimitedBox-class.html) | `LimitedBox` | 差異なし | [LimitedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.LimitedBox/README.md) | | [`FractionallySizedBox`](https://api.flutter.dev/flutter/widgets/FractionallySizedBox-class.html) | `FractionallySizedBox` | `textDirection` による左右の切り替えがない | [FractionallySizedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.FractionallySizedBox/README.md) | | [`OverflowBox`](https://api.flutter.dev/flutter/widgets/OverflowBox-class.html) | `OverflowBox` | `textDirection` による左右の切り替えがない | [OverflowBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverflowBox/README.md) | | [`FittedBox`](https://api.flutter.dev/flutter/widgets/FittedBox-class.html) | `FittedBox` | `textDirection` による左右の切り替えがない | [FittedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.FittedBox/README.md) | | [`ConstraintsTransformBox`](https://api.flutter.dev/flutter/widgets/ConstraintsTransformBox-class.html)、[`UnconstrainedBox`](https://api.flutter.dev/flutter/widgets/UnconstrainedBox-class.html) | `ConstraintsTransformBox`、`UnconstrainedBox` | `textDirection` による左右の切り替えと、debug 時のはみ出し表示がない | [ConstraintsTransformBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ConstraintsTransformBox/README.md) | | [`Wrap`](https://api.flutter.dev/flutter/widgets/Wrap-class.html) | `Wrap` | `textDirection` と `clipBehavior` は未対応(既定の見た目は一致する) | [Wrap](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Wrap/README.md) | | [`ColoredBox`](https://api.flutter.dev/flutter/widgets/ColoredBox-class.html) | `ColoredBox` | `Color` は `init` プロパティで指定する(次の節) | [ColoredBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ColoredBox/README.md) | | [`StatefulWidget`](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html)、[`State`](https://api.flutter.dev/flutter/widgets/State-class.html)、`setState` | `StatefulWidget<T>`、`State<T>`、`SetState()` | 型を `StatefulWidget<T>` のようにジェネリックで書く。`SetState()` で再ビルドする仕組みは同じ | [状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/) | レイアウトの仕組み(制約が親から子へ下り、大きさが子から親へ上る)も、Flutter と同じです。 作例のないウィジェットは、Flutter と同じと確かめていません。`IndexedStack`、`ConstrainedBox`、`RotatedBox`、`Visibility`、`Offstage`、`IntrinsicWidth`、`IntrinsicHeight`、`RepaintBoundary`、`Builder`、`KeyedSubtree` などが当たります。 ## 名前や書き方が違うもの 名前や書き方が違うと、コンパイルが通りません。表のとおりに書き換えてください。 ### 名前が変わるもの | Flutter | FloatSoda | 理由 | 詳細 | | :-- | :-- | :-- | :-- | | `ClipRRect` | `ClipRoundRect` | 記録なし | [Clip](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Clip/README.md) | | `ClipPath` | `ClipCustomPath` | 記録なし | [Clip](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Clip/README.md) | | `MouseRegion` | `PointerRegion` | VR のレーザーもマウスも「ポインター」と呼ぶ | [PointerRegion](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PointerRegion/README.md) | | `Opacity` の `opacity` | `Opacity.Value` | C# ではメンバー名を型名と同じにできない | [Opacity](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Opacity/README.md) | | `AspectRatio` の `aspectRatio` | `AspectRatio.Ratio` | C# ではメンバー名を型名と同じにできない | [AspectRatio](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.AspectRatio/README.md) | | `Padding` の `padding` | `Padding.Spacing` | 記録なし | [Padding](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Padding/README.md) | | `behavior`(`HitTestBehavior`) | `Behaviour`(`HitTestBehaviour`) | 綴りが違う(Flutter は米国式の behavior) | [GestureDetector](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GestureDetector/README.md) | | `ConstrainedBox` の `constraints` | `ConstrainedBox.AdditionalConstraints` | 親の制約に追加で課す制約であることを名前で示す | [CHANGELOG.md](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/CHANGELOG.md) | ### 書き方が変わるもの | Flutter | FloatSoda | 理由 | 詳細 | | :-- | :-- | :-- | :-- | | 子と設定を、コンストラクタの引数で渡す | 子は `Child` か `Children`(`IList<Widget>`)、設定は各プロパティに、オブジェクト初期化子で渡す。名前付きコンストラクタは持たない(`Icon` のように、必須の値をコンストラクタ引数に取る型もある) | C# の流儀に合わせた方針 | [SizedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.SizedBox/README.md) | | 色は `Color(0xAARRGGBB)` や `Colors.*` | `Colors.*`、`new Color(r, g, b)`、`Color.FromArgb(0xAARRGGBB)`(どれも `FloatSoda.Geometrics`。成分は各 0〜255 の整数) | 色の型が違う。`Colors` は Material の段階色ではなく、CSS の色名を並べたもの | [ColoredBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ColoredBox/README.md) | | `FontWeight.w700` | `FontWeight = 700`(`int`) | 列挙型を持たない | [Text](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Text/README.md) | | `Matrix4` | `Matrix3x2`(2次元のアフィン変換だけ) | 3次元の変換を持たない | [Transform](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Transform/README.md) | | `addListener` | 通知元ごとの `event`(`AnimationController` は `Changed`、`INotifyPropertyChanged` は `PropertyChanged`。`+=` で登録し、`-=` で解除する) | C# の標準の仕組みを使う方針 | [API_DESIGN.md の 3.6](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/API_DESIGN.md) | | `SizedBox.square()`、`SizedBox.expand()` | `square()` は `Width` と `Height` に同じ値を指定して書く。`expand()` に当たる手段はない。`Width` と `Height` を省略すると、親の制約がそのまま子に渡るだけで、親の制約が緩いときは領域いっぱいに広がらない(`BoxConstraints.TightFor(null, null)`) | 名前付きコンストラクタを持たない | [SizedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.SizedBox/README.md) | | `Padding` の `EdgeInsets.only(left: 48)` | `new EdgeInsets(Left: 48)` | 名前付きコンストラクタを持たない | [Padding](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Padding/README.md) | ### 受け付ける項目が少ないもの Flutter のウィジェットが取る引数のうち、FloatSoda にないものがあります。書くとコンパイルが通りません。 | ウィジェット | FloatSoda にないもの | 詳細 | | :-- | :-- | :-- | | `GestureDetector` | ダブルタップ、長押し、水平ドラッグと垂直ドラッグ、スケール。タップと Pan だけを持つ | [GestureDetector](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GestureDetector/README.md) | | `BoxDecoration` | グラデーション、画像、影、円形(`shape`)。背景色、角丸、ボーダーだけを持つ | [DecoratedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.DecoratedBox/README.md) | | `Text` | `softWrap`、`overflow`、`maxLines`、`textAlign` | [Text](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Text/README.md) | | `Flex`、`Row`、`Column` | `spacing`、`clipBehavior`、`textBaseline`。間隔は `SizedBox` を挟んで作る | [Flex](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Flex/README.md) | | `Container` | `margin`、`constraints`、`foregroundDecoration`、`clipBehavior`。外側の余白は `Padding` で包んで作る | [Container](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Container/README.md) | | `Stack` | `clipBehavior`、`Positioned.fill`、`Positioned.fromRect` | [Stack](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Stack/README.md) | | `Opacity` | `alwaysIncludeSemantics` | [Opacity](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Opacity/README.md) | | `Transform` | `Transform.rotate`、`scale`、`translate`、`flip`、`filterQuality` | [Transform](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Transform/README.md) | | `PointerRegion` | `onHover`、`cursor` | [PointerRegion](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PointerRegion/README.md) | 文字方向(`textDirection`)による左右の切り替えは、どのウィジェットにもありません。 **できた?** 書き換えたあと、コンパイルが通った。 <details> <summary>できなかったら</summary> エラーの読み方は、ガイドの[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)にあります。C# の書き方が分からないときは、ガイドの[C# がほぼ初めての人へ](/guide/3-prepare/csharp-primer/)を開いてください。 </details> コンパイルが通ってから見た目や反応が違うときは、次の節を開きます。 ## 挙動が違うもの コンパイルは通るのに、見た目や反応が Flutter と違うものです。「追跡」の列に Issue がある行は、Flutter に揃える作業が進んでいます。追跡がない行は、Flutter に揃える作業が見つかっていない差異です。 | ウィジェット | 症状 | 直し方 | 追跡 | 詳細 | | :-- | :-- | :-- | :-- | :-- | | `Stack` | 範囲を超えた子も、切り取られずに見える | `Stack` を `ClipRect` で包む | #257 | [Stack](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Stack/README.md) | | `Container` | 子がないと、緩い制約の下で 0 に縮む | `Width` と `Height` を指定する | #268 | [Container](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Container/README.md) | | `Expanded` | `Flex` に 0 以下を指定すると例外になる。主軸の長さが決まらない場所の `Expanded` も、常に例外になる | `Flex` に 1 以上を指定する | なし | [Expanded](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Expanded/README.md) | 表のとおりに直しても思った見た目にならないときは、ガイドの[レイアウトの考え方](/guide/5-troubleshoot/layout-model/)を開きます。押しても反応しないときは、[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)です。 ## まだないもの・実装しないもの Flutter にあって FloatSoda で見つからないものは、次の2つのどちらかです。 | 区分 | 該当するもの | 追跡 | | :-- | :-- | :-- | | まだない | Material と Cupertino に当たるデザインシステム(`FloatSoda.UI` 系) | #126 | | まだない | スクロール系(`ListView`、`GridView`、`SingleChildScrollView`) | #69 | | まだない | `Tween`、`CurvedAnimation` | #137 | | まだない | `AnimatedContainer` などの暗黙的アニメーション | #163 | | まだない | `LayoutBuilder` | #174 | | まだない | `CustomPaint` | #171 | | まだない | `MediaQuery` に当たるもの(`ViewMetrics`) | #173 | | まだない | `Notification` | #175 | | 実装しない | `Semantics`(アクセシビリティ) | 追跡なし | `Navigator` と `Theme` に当たる型は、型名の検索では `src/` に見つかりませんでした。別名の部品は確かめていません。 ないものは、自分で組むか、実装を待つかを選べます。各項目の詳しい状況は、[できないこと・まだ使えないもの](/reference/limitations/)にあります。 # 用語集 > FloatSoda の docs や、AI が書いたコードに出てくる言葉の意味を、このページで引けます。言葉を4つの分類に分け、それぞれ意味、出てくる場面、詳しく知るページを並べています。 FloatSoda の docs や、AI が書いたコードに出てくる言葉の意味を、このページで引けます。言葉を4つの分類に分け、それぞれ意味、出てくる場面、詳しく知るページを並べています。 ## この用語集の使い方 分からない言葉は、検索で探します。検索は2つあり、探したいものによって使い分けます。 | 探したいもの | 使う検索 | 結果 | | :-- | :-- | :-- | | 用語集に載っている、言葉の意味 | ブラウザーのページ内検索(Ctrl+F) | 用語集の項目にそのまま移る | | その言葉が、docs のどのページで使われているか | サイトの検索(Ctrl+K。画面の上部の検索欄) | 言葉が出てくるページの一覧が出る。用語集の項目が上に来るとは限らない | サイトの検索は、公開サイトでだけ使えます。リポジトリの `docs/` を直接読んでいるときは、ページ内検索を使います。 見出しから、言葉の分類を探すこともできます。言葉は、各分類の中で、docs に出てくる順に並べています。 各項目は、1〜2文の意味にとどめています。詳しい説明は、右端の列のページにあります。意味を読んだら、元のページへ戻ってください。 ## FloatSoda と Flutter の言葉 FloatSoda は Flutter に合わせて作ってあるので、Flutter の言葉がそのまま出てきます。Flutter との対応は、[Flutter の読み替え表](/reference/flutter-mapping/)にあります。 | 言葉 | 意味 | 出てくる場面 | 詳しく | | :-- | :-- | :-- | :-- | | ウィジェット(Widget) | 画面の構成や設定を宣言する、`Widget` を継承した部品(作ったあとは書き換えず、変えるときは作り直す) | 画面のコードのほぼ全部 | [FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/) | | Element | ウィジェットの木を実際に管理する、書き換えられる木の節。使う側は意識しなくてよい | エラーメッセージ、仕組みの説明 | [FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/) | | RenderObject | 大きさの計算と描画を担い、ウィジェットに対応して作られる木の節 | エラーメッセージ、自作の部品 | [FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/) | | `StatelessWidget` | 更新する状態を持たず、`Build()` で画面を返すウィジェット | 自作の部品 | [状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/) | | `StatefulWidget<T>`、`State<T>` | ウィジェットの宣言と、更新する状態を保持するオブジェクト | カウンターなど、値が変わる画面 | [状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/) | | `SetState` | 状態を変更する処理を渡し、ウィジェットの再構築を要求する操作 | `OnTap` の中など | [状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/) | | `InheritedWidget` | 子孫のウィジェットから参照できる値を持つウィジェット | `DashboardWindow`、`DefaultTextStyle` | [FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/) | | `Key` | 同じウィジェットかどうかを見分ける印(`ValueKey<T>` と `UniqueKey` がある) | 子のリストを並べ替えるとき | [FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/) | | 制約(`BoxConstraints`) | 親が子に渡す幅と高さの上限と下限(制約が親から子へ下り、大きさが子から親へ上る) | 思った大きさにならないとき | [レイアウトの考え方](/guide/5-troubleshoot/layout-model/) | | 主軸と交差軸 | `Row` や `Column` で、並べる向きと、それに直角な向き | `MainAxisAlignment`、`CrossAxisAlignment` | [Flex の作例](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Flex/README.md) | | ポインター | レーザーポインターとマウスをまとめた呼び方 | `PointerRegion`、`Listener` | [PointerRegion の作例](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PointerRegion/README.md) | | ヒットテスト | ポインターの位置で、どのウィジェットが反応するかを決める処理 | 押しても反応しないとき | [GestureDetector の作例](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GestureDetector/README.md) | | ジェスチャー | ポインターの動きに付けた意味(タップとドラッグ) | `GestureDetector` | [GestureDetector の作例](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GestureDetector/README.md) | | `AnimationController` | 時間に合わせて、0 から 1 の値を動かす部品 | アニメーション | [アニメーションを付ける](/guide/4-build/screen/add-animation/) | | `WidgetTicker` | フレームごとに、開始からの経過時間を渡す時計 | アニメーション、チュートリアル | [アニメーションを付ける](/guide/4-build/screen/add-animation/) | | `Curve` | アニメーションの動きの緩急(`Curves` に定義済みのものがある) | アニメーション | [アニメーションを付ける](/guide/4-build/screen/add-animation/) | ## VR と SteamVR の言葉 SteamVR での表示場所、入力、大きさ、アプリの識別に使う言葉です。 | 言葉 | 意味 | 出てくる場面 | 詳しく | | :-- | :-- | :-- | :-- | | オーバーレイ | SteamVR が、ゲームなどの画面の上に重ねて出す画面 | FloatSoda が作るものすべて | [オーバーレイとは](/guide/1-discover/what-is-an-overlay/) | | ダッシュボード | SteamVR のメニューの画面で、ポインターの座標が届くオーバーレイの置き場所 | `DashboardWindow` | [オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/) | | ウィンドウ(`DashboardWindow`、`WorldSpaceWindow`、`DeviceTrackedWindow`) | `CreateWindow` に渡すオーバーレイの置き場所の指定で、ダッシュボード、空間の固定位置、デバイスへの追従から選ぶ | 起動のコード | [オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/) | | アクション入力 | コントローラーのボタンやスティックの入力で、SteamVR のバインディングにより割り当てを利用者が変えられる | `InputAction` | [コントローラーの入力を受け取る](/guide/4-build/input/controller-input/) | | `Dpm` | 1メートルあたりのドット数で、画像の大きさから実寸を決める(既定は 4000、1ピクセルが 0.25mm) | `Dpm = new Dpm(1000)` | [オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/) | | `AppKey` | SteamVR がアプリを見分ける名前 | `FloatSodaOptions` | [起動と Generic Host](/guide/4-build/mechanics/startup-and-host/) | | OpenVR、SteamVR | VR のランタイムである SteamVR と、そのアプリ向けの API である OpenVR | 動作環境、エラーメッセージ | [動作環境](/reference/requirements/) | ## 起動と .NET の言葉 チュートリアルの[最初のオーバーレイを作る](/tutorials/first-overlay/)にある、起動のコードに出てくる言葉です。 | 言葉 | 意味 | 出てくる場面 | 詳しく | | :-- | :-- | :-- | :-- | | Generic Host(`Microsoft.Extensions.Hosting`) | .NET の起動の仕組み。設定、ログ、依存性注入、バックグラウンド処理をまとめて持つ | `Host.CreateApplicationBuilder`、`host.RunAsync()` | [起動と Generic Host](/guide/4-build/mechanics/startup-and-host/) | | 依存性注入(DI) | 必要な部品を、自分で作らず、外から受け取る仕組み | `host.Services.GetRequiredService<FloatSodaApp>()` | [Generic Host の仕組みを使う](/guide/4-build/mechanics/use-generic-host/) | | `AddFloatSoda` | FloatSoda を Generic Host に登録する呼び出し | Generic Host のサービス登録 | [起動と Generic Host](/guide/4-build/mechanics/startup-and-host/) | | `FloatSodaApp` | `CreateWindow` でウィンドウを作る、FloatSoda の入口 | 起動のコード | [起動と Generic Host](/guide/4-build/mechanics/startup-and-host/) | | Skia(SkiaSharp) | 描画のライブラリ。画面のコードでは直接は触らないが、ウィンドウの大きさの `SKSize` のように、型の名前に出ることがある | `SKSize` のような `SK` で始まる型名 | [FloatSoda の仕組み](/guide/4-build/mechanics/how-floatsoda-works/) | ## C# の言葉 C# の言葉は、名前と意味だけを書いています。詳しい説明は、ガイドの[C# がほぼ初めての人へ](/guide/3-prepare/csharp-primer/)にあります。 | 言葉 | 意味 | | :-- | :-- | | オブジェクト初期化子 | `new SizedBox { Width = 100 }` のように、生成時のプロパティの値を波かっこの中で指定する書き方 | | `init` | 作るときだけ値を入れられ、そのあとは書き換えられないプロパティ | | `record` | 値の比較などのメンバーが自動で生成される型。ウィジェットの多くが `record` | | `with` | `record` を複製し、指定したプロパティだけを変えた新しい値を作る式(参照先のオブジェクトまでは複製しない) | | ラムダ式 | `() => SetState(...)` のように、名前を付けずに書く小さな処理 | | `event` | 通知を受け取る処理を登録・解除するための C# の仕組み。Flutter の `addListener` に近い | # できないこと・まだ使えないもの > FloatSoda で作りたいものが今できるかを、このページで確かめられます。使えないものを、置く場所と入力、見た目と文字、動きと状態、起動と終了、部品と UI コンポーネント、既知の不具合の順に並べています。 FloatSoda で作りたいものが今できるかを、このページで確かめられます。使えないものを、置く場所と入力、見た目と文字、動きと状態、起動と終了、部品と UI コンポーネント、既知の不具合の順に並べています。 ## このページの読み方 各項目は、制約の内容、起きる症状、今できる回避、追跡先の4つで書いています。解除の見込みは、追跡先の Issue で確かめてください。このページには、日付や「今は」を書いていません。 項目は3種類に分かれます。まだ実装していないもの、仕様として実装しないもの、既知の不具合です。対応方針と進捗は、追跡先の Issue で確かめてください。仕様として実装しないものは、最後の節に分けています。自分の作りたいものに関わる節へ進んでください。 ## 置く場所と入力 ポインタの座標が届くのは、ダッシュボードのオーバーレイだけです。ボタンを置く画面は、ダッシュボードに置きます。 | 制約の内容 | 起きる症状 | 今できる回避 | 追跡 | | :-- | :-- | :-- | :-- | | ポインタの座標が届くのは、`DashboardWindow` だけ | `WorldSpaceWindow` と `DeviceTrackedWindow` に `GestureDetector` を置いても、コンパイルは通り例外も出ないが、コールバックは呼ばれない | ポインター操作が要る画面は `DashboardWindow` に置き、コントローラーのボタンで操作するならアクション入力を使う | #182 | | `Size` を省略したウィンドウは、表示範囲が起動時の大きさに固定される | 起動後に中身が大きくなると、増えた部分が SteamVR 上に表示されない | 中身の大きさが変わるウィンドウに `Size` を指定する | #285 | | `Size` を省略したウィンドウは、ルートの制約に幅の上限がない | 途中の部品が幅の上限を渡さない場合、`Wrap` が折り返さず横一列に並ぶ | `Size` を指定する。または `Wrap` を、幅を決めた `SizedBox` で包む | 追跡先なし(仕様) | | 実行中にオーバーレイの種類を切り替えられない | `DashboardWindow` から `WorldSpaceWindow` への切り替えのような API がない | 起動時に、使う種類ごとにウィンドウを作る | #90 | 置き場所の選び方は、ガイドの[オーバーレイの種類と選び方](/guide/4-build/mechanics/overlay-types/)にあります。コントローラーの入力は[コントローラーの入力を受け取る](/guide/4-build/input/controller-input/)、押しても反応しないときの切り分けは[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)を開いてください。 ## 見た目と文字 スクロールする一覧と文字入力は、今のところ作れません。`Text` は、親から受けた幅に収まるように自動で折り返しますが、折り返しと省略の制御はできません。 | 制約の内容 | 起きる症状 | 今できる回避 | 追跡 | | :-- | :-- | :-- | :-- | | `ListView`、`GridView`、`SingleChildScrollView` が `internal` | 公開 API として呼べず、コンパイルできない | `Stack` と `Positioned` などで自分で並べ、表示する件数を絞る | #69 | | 文字入力(TextField)がない | 文字を打ち込む画面を作れない | なし | #107 | | `Text` に折り返しと省略(`softWrap`、`overflow`、`maxLines`)を指定する項目がない | 幅が有限なら自動で折り返すが、折り返さない指定、省略記号、行数の上限は指定できない(`RenderParagraph.PerformLayout` は親の `MaxWidth` で改行する) | 行を自分で決めたいときは、文字列を自分で分け、`Column` に複数の `Text` を並べる | 追跡先なし | 回避のコードは、ガイドの[AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/)で AI に書かせられます。 ## 動きと状態 アニメーションは `AnimationController`、`Curves`、`FadeTransition` で組めます。未実装のものは、次の表にまとめています。 | 制約の内容 | 起きる症状 | 今できる回避 | 追跡 | | :-- | :-- | :-- | :-- | | `Tween`、`CurvedAnimation`、暗黙的アニメーション(`AnimatedContainer` 系)、スプリングの物理シミュレーションがない | Flutter の同名の部品を書いても、コンパイルできない | `AnimationController` の値(0 から 1)を、自分の式で変換して使う | #137、#163、#164 | | バックグラウンドのスレッドから `SetState` を呼べない | 画面の更新が失われる、またはクラッシュする | フレームごとに呼ばれる `WidgetTicker` の中など、メインのスレッドから呼ぶ(例は、チュートリアルの[数字を表示して、書き換える](/tutorials/first-overlay/#2-数字を表示して書き換える)) | #150、#281 | アニメーションの付け方は、ガイドの[アニメーションを付ける](/guide/4-build/screen/add-animation/)にあります。 ## 起動と終了 SteamVR が起動していない状態でアプリを起動できません。ウィンドウ1つだけを閉じる API もありません。 | 制約の内容 | 起きる症状 | 今できる回避 | 追跡 | | :-- | :-- | :-- | :-- | | SteamVR の起動が前提。遅延接続と再接続を扱えない | SteamVR が起動していない状態では、アプリを起動できない(`--desktop` でも OpenVR を初期化するため同じ) | SteamVR を先に起動してから、アプリを起動する | #140、#233 | | ウィンドウを閉じる API がない | 作ったウィンドウを、アプリの実行中に消せない | なし(アプリごと終了する) | #218 | 起動の作りは、ガイドの[SteamVR と一緒に起動する](/guide/6-distribute/launch-with-steamvr/)で確かめられます。 ## 部品と UI コンポーネント `Button` のような UI コンポーネントは、NuGet に出していません。ボタンは、`GestureDetector` で自分で組み立てます。スライダー、ダイアログ、トースト(通知として出して消す部品)の組み立て方は、確かめていません。 | 制約の内容 | 起きる症状 | 今できる回避 | 追跡 | | :-- | :-- | :-- | :-- | | `FloatSoda.UI`、`FloatSoda.UI.Cream`、`FloatSoda.UI.FizzyPop` を NuGet に出していない(`IsPackable=false`) | `Button`、スライダー、ダイアログなどの部品がない | ボタンは `GestureDetector` と `ColoredBox` などで組み立てる(例は、チュートリアルの[ボタンを押して、数字を増やす](/tutorials/first-overlay/#3-ボタンを押して数字を増やす))が、スライダーとダイアログの組み立て方は未確認 | #126、#102、#78、#100 | | `FloatSoda.Hooks` を NuGet に出しておらず、`UseState` などの補助メソッドが `NotImplementedException` を投げる | Hooks を使う書き方は、実行時に例外になる | `StatefulWidget<T>` と `State<T>` で書く | #72 | | 通知として出して消す部品(トースト)がない | 組み込みの通知用の部品を使えない | なし | #123、#74 | ボタンの組み立て方は、ガイドの[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)にあります。 ## 既知の不具合 不具合は、[Issue](https://github.com/sumx21t-3310/FloatSoda/issues) で追跡しています。直ったかどうかは、Issue で確かめてください。 ここに載っていない症状は、ガイドの[反応しないときに切り分ける](/guide/5-troubleshoot/not-responding/)か[エラーを読んで直す](/guide/5-troubleshoot/read-errors/)を開きます。新しい不具合は、[Issue](https://github.com/sumx21t-3310/FloatSoda/issues)で知らせてください。 Flutter と挙動が違う箇所の一覧は、[Flutter の読み替え表](/reference/flutter-mapping/#挙動が違うもの)にあります。差異を解消する作業の追跡先は、#224 です。 ## 実装しないと決めたもの スクリーンリーダー向けの情報を扱う、Flutter の `Semantics` に当たる機能は、実装しません。VR のオーバーレイのテクスチャに、スクリーンリーダーへ接続する出力先がないためです。解除の条件は決まっていません。 # 動作環境 > FloatSoda で開発する PC と、作ったアプリを動かす PC に要るものを、このページで確かめられます。どちらの PC にも SteamVR が要ります。確かめていない項目は、最後の節に分けて書いています。 FloatSoda で開発する PC と、作ったアプリを動かす PC に要るものを、このページで確かめられます。どちらの PC にも SteamVR が要ります。確かめていない項目は、最後の節に分けて書いています。 ## 開発する PC に要るもの 開発する PC に要るものは、次の表にまとめています。FloatSoda の対象は .NET 10 で、言語は C# 14 です。 | 項目 | 必要な値 | 確かめ方 | | :-- | :-- | :-- | | .NET の SDK | 10 | `dotnet --version` の出力が `10.` で始まる | | SteamVR | 起動できる | Steam から SteamVR を起動し、ダッシュボードが開く | | HMD | SteamVR につないである | SteamVR の画面に HMD が認識されている | | OS | Windows(x64) | 同梱する OpenVR のネイティブ DLL の対象が x64 の Windows だけ(他の OS での動作は確かめていない) | **できた?** 表の全部の行に「ある」と答えられた。 <details> <summary>できなかったら</summary> 足りないものの入れ方は、ガイドの[開発環境を用意する](/guide/3-prepare/set-up-environment/)にあります。 </details> 開発に使うエディターや IDE は、決まっていません。C# が書ける環境であれば使えます。開発だけの人は、準備ができたらチュートリアルの[最初のオーバーレイを作る](/tutorials/first-overlay/)へ進めます。 ## 実行する PC に要るもの 作ったアプリを動かす PC には、SteamVR と HMD が要ります。.NET 10 のランタイムは、フレームワーク依存形式で配るときに要ります。 | 項目 | 必要な値 | 確かめ方 | | :-- | :-- | :-- | | SteamVR | アプリを起動する前に、起動してある | SteamVR のステータス画面が出ている | | HMD | SteamVR につないである | SteamVR の画面に HMD が認識されている | | OS | Windows(x64) | 開発する PC と同じ | | .NET のランタイム | 10 | `dotnet --list-runtimes` に `Microsoft.NETCore.App 10.` が出る。自己完結形式で配ったアプリには要らない | 配る人は、ガイドの[Booth で売る準備](/guide/6-distribute/booth-release/)で、商品ページに書く動作環境の書き方を確かめられます。配られたアプリが動かないときは、ガイドの[利用者の環境で動かないとき](/guide/7-maintain/user-environment-issues/)を開いてください。 ## SteamVR が要らない場面 [PaintingSample](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PaintingSample/Program.cs)は、`FloatSoda.Testing` でツリーを画像に書き出す作例で、SteamVR が要りません。この作例は `AddFloatSoda` も OpenVR も呼びません。 デスクトップのウィンドウに表示する `--desktop` 付きの作例も、SteamVR の起動が要ります。起動時に OpenVR を初期化するためです。この場合も HMD の接続が要ります。 HMD を被らずに確かめる方法は、ガイドの[HMD を被らずに確かめる](/guide/4-build/input/test-without-hmd/)にあります。 ## 依存するライブラリ FloatSoda が依存するライブラリとバージョンは、次のプロジェクトファイルで確かめてください。 - [src/FloatSoda/FloatSoda.csproj](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/src/FloatSoda/FloatSoda.csproj) - [src/FloatSoda.Engine/FloatSoda.Engine.csproj](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/src/FloatSoda.Engine/FloatSoda.Engine.csproj) OpenVR のネイティブ DLL(`openvr_api.dll`)は、`FloatSoda.OVR` のパッケージに含まれています。[src/FloatSoda.OVR/FloatSoda.OVR.csproj](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/src/FloatSoda.OVR/FloatSoda.OVR.csproj)で、`runtimes/win-x64/native/` に置く設定を確かめられます。 ## 確かめていないこと 次の項目は、動作を確かめていません。 - Windows 以外の OS で動くか(同梱の OpenVR のネイティブ DLL は x64 の Windows 用だけ) - 対応する HMD の機種(SteamVR が対応する機種であること以外は確かめていない) - GPU と OpenGL の最低バージョン - 自己完結形式で配ったアプリの実行要件 自分の環境が表にないときは、動いたかどうかを[Issue](https://github.com/sumx21t-3310/FloatSoda/issues)で知らせてください。書き方は、コントリビューター向けの[不具合を切り分けて報告する](/contributor/1-get-involved/report-bugs/)にあります。確かめられた項目は、このページの表へ移します。 # 作例のカタログ > samples/ には、動かして手本にできる作例が入っています。このページで、作りたいものに近い作例を探し、動かし方を確かめられます。作例は、アプリ全体の形を見るものと、ウィジェット1つ(または1グループ)を扱うカタログ型に分かれます。ウィジェットの作例は、レイアウト・描画・入力・ツリーの組み立ての4つで引けます。目… `samples/` には、動かして手本にできる作例が入っています。このページで、作りたいものに近い作例を探し、動かし方を確かめられます。作例は、アプリ全体の形を見るものと、ウィジェット1つ(または1グループ)を扱うカタログ型に分かれます。ウィジェットの作例は、レイアウト・描画・入力・ツリーの組み立ての4つで引けます。目的(時計を出したい、アルバムを作りたい)から引ける作例の整備は、追跡: #261 です。 ## 作例の動かし方 作例は [FloatSoda のリポジトリ](https://github.com/sumx21t-3310/FloatSoda)に入っています。リポジトリを clone し、SteamVR を起動してから、リポジトリのルートで次のコマンドを実行します。`<名前>` を、下の表の作例の名前(`FloatSoda.Samples.` に続く部分)に置き換えます。`PaintingSample` のように、画面を PNG に書き出すだけの作例は、SteamVR を起動しなくても動きます。SteamVR が要らない場面は、[動作環境](/reference/requirements/#steamvr-が要らない場面)で確かめられます。 ```powershell dotnet run --project samples/FloatSoda.Samples.<名前> ``` ウィジェットの作例には、`--desktop` を付けられます。ダッシュボードの代わりに、PC のデスクトップのウィンドウに表示します。HMD を被らずに見た目を確かめるときに使います。 ```powershell dotnet run --project samples/FloatSoda.Samples.Flex -- --desktop ``` `--desktop` を付けても、SteamVR の起動と HMD の接続は要ります。起動時に OpenVR を初期化するためです。 **できた?** ダッシュボード(`--desktop` なら PC のウィンドウ)に、作例の画面が出た。 <details> <summary>できなかったら</summary> 起動で例外が出るときは、ガイドの[起動で例外が出る: SteamVR または HMD につながらない](/guide/5-troubleshoot/read-errors/#起動で例外が出る-steamvr-または-hmd-につながらない)を開きます。PC に必要なものが足りないときは、[動作環境](/reference/requirements/)で確かめてください。 </details> ## 総合・最小・低レベルの作例 ウィジェット単体ではなく、まとまったアプリの形を見たいときは、次の4つから選びます。名前から各作例の README を開けます。README には、作例の位置づけと主な読者も書いてあります。 | 作例 | 示すもの | SteamVR | | :-- | :-- | :-- | | [FloatSoda.Samples.OverlayApp](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverlayApp/README.md) | レイアウト、時計、アニメーション、カウンター、ドラッグの総合デモ。3種類のオーバーレイを同時に作る | 要る | | [FloatSoda.Samples.GettingStarted](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GettingStarted/README.md) | 色の四角を1つ出す最小のアプリ。チュートリアルの段1に近い | 要る | | [FloatSoda.Samples.PrimitiveOverlay](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PrimitiveOverlay/README.md) | ウィジェットを使わず、`FloatSoda.OVR` の低レベル API だけでオーバーレイを出す。デスクトップにある `layer_tree_output.png` を貼るので、先に PaintingSample を実行しておく | 要る | | [FloatSoda.Samples.PaintingSample](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PaintingSample/README.md) | ウィジェット、RenderObject、レイヤーの3つのツリーを、PNG の画像に書き出す。デスクトップに保存する | 要らない | チュートリアルを先に通したいときは、[最初のオーバーレイを作る](/tutorials/first-overlay/)を開いてください。 ## ウィジェットの作例(レイアウト) 並べる、重ねる、余白を取る、大きさを決めるといった、レイアウトの目的で選びます。名前から各作例の README を開けます。 | 作例 | 扱うウィジェット | | :-- | :-- | | [Flex](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Flex/README.md) | `Flex`、`Row`、`Column` | | [Expanded](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Expanded/README.md) | `Expanded`、`Flexible`、`Spacer` | | [Wrap](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Wrap/README.md) | `Wrap` | | [Align](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Align/README.md) | `Align`、`Center` | | [Padding](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Padding/README.md) | `Padding` | | [Container](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Container/README.md) | `Container` | | [SizedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.SizedBox/README.md) | `SizedBox` | | [LimitedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.LimitedBox/README.md) | `LimitedBox` | | [ConstraintsTransformBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ConstraintsTransformBox/README.md) | `ConstraintsTransformBox`、`UnconstrainedBox` | | [Stack](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Stack/README.md) | `Stack`、`Positioned` | | [AspectRatio](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.AspectRatio/README.md) | `AspectRatio` | | [FittedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.FittedBox/README.md) | `FittedBox`、`BoxFit` | | [FractionallySizedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.FractionallySizedBox/README.md) | `FractionallySizedBox` | | [OverflowBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.OverflowBox/README.md) | `OverflowBox`、`SizedOverflowBox` | | [IntrinsicWidth](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.IntrinsicWidth/README.md) | `IntrinsicWidth`、`IntrinsicHeight` | | [Visibility](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Visibility/README.md) | `Visibility`、`Offstage`、`IndexedStack`、`RotatedBox` | 表は、`samples/` のディレクトリから手で写したものです。作例が増えたときは、`samples/` の一覧が正になります。部品を目的から探したいときは、[部品を目的から探す](/reference/find-widgets/)を開いてください。 ## ウィジェットの作例(描画) 色を塗る、文字や画像を出す、透明にする、切り抜くといった、見た目の目的で選びます。 | 作例 | 扱うウィジェット | | :-- | :-- | | [ColoredBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ColoredBox/README.md) | `ColoredBox` | | [DecoratedBox](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.DecoratedBox/README.md) | `DecoratedBox`、`BoxDecoration` | | [Opacity](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Opacity/README.md) | `Opacity` | | [Transform](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Transform/README.md) | `Transform` | | [Clip](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Clip/README.md) | `ClipRect`、`ClipRoundRect`、`ClipOval`、`ClipCustomPath` | | [Text](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Text/README.md) | `Text`、`RichText`、`TextSpan`、`TextStyle` | | [DefaultTextStyle](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.DefaultTextStyle/README.md) | `DefaultTextStyle` | | [Icon](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Icon/README.md) | `Icon`、`IconData` | | [Image](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Image/README.md) | `Image`、`FileImageProvider`、`BoxFit` | 見た目をまとめて整える手順は、ガイドの[見た目を整える](/guide/4-build/screen/style-appearance/)にあります。 ## ウィジェットの作例(入力) 押す、触れる、ドラッグするといった、操作の目的で選びます。 | 作例 | 扱うウィジェット | | :-- | :-- | | [GestureDetector](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.GestureDetector/README.md) | `GestureDetector`(タップとドラッグ) | | [Listener](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Listener/README.md) | `Listener`(生のポインターイベント) | | [PointerRegion](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.PointerRegion/README.md) | `PointerRegion`(ホバーの出入りと、`Listener`・`GestureDetector` を重ねたときのイベントの順序) | ポインタの座標が届くのは、ダッシュボードのオーバーレイだけです。詳しくは、[できないこと・まだ使えないもの](/reference/limitations/#置く場所と入力)を開いてください。ボタンの組み立て方は、ガイドの[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)にあります。 ## ウィジェットの作例(ツリーの組み立て) context を受け渡す、並べ替えても状態を保つ、モデルの変更で一部だけを作り直すといった、ツリーの組み立ての目的で選びます。 | 作例 | 扱うウィジェット | | :-- | :-- | | [Builder](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.Builder/README.md) | `Builder`、`KeyedSubtree`、`RepaintBoundary` | | [ListenableBuilder](https://github.com/sumx21t-3310/FloatSoda/blob/v0.5.0/samples/FloatSoda.Samples.ListenableBuilder/README.md) | `ListenableBuilder`(`INotifyPropertyChanged` のモデルで作り直す) | ## 目的から作例を探す 「時計を出したい」「画像を並べたい」のような、作りたいものの言葉から引ける作例は、今のところありません。目的別のミニアプリは、#261 で追っています。 近い作例がないときは、次の2つから選べます。 - 通しで1つのパネルを作る: ガイドの[表情切り替えパネルを作る](/guide/4-build/screen/expression-switcher/) - コードを AI に書かせる: ガイドの[AI に FloatSoda を書かせる](/guide/3-prepare/write-with-llm/) # チュートリアル > 手を動かして、最初のオーバーレイを作る ## このタブでできること このタブでは、手を動かして最初のオーバーレイを動かせます。SteamVR のダッシュボードに数字とボタンを出し、ボタンを押すと数字が増えるところまで作ります。「FloatSoda で何か動かしてみたい」と思ったら、まず最初のチュートリアルを通してください。 ## 最初の1本 [最初のオーバーレイを作る](/tutorials/first-overlay/)が、最初に通すチュートリアルです。完成すると、ダッシュボードに青い四角が出て、その中の数字をボタンで増やせます。 始める前に、SteamVR と .NET の SDK が要ります。要るものの一覧は、リファレンスの[動作環境](/reference/requirements/)にあります。準備の手順は、ガイドの[開発環境を用意する](/guide/3-prepare/set-up-environment/)です。 ## 終えたあと このチュートリアルを終えたら、目的に合う行き先を1つ選んでください。 - 通しで1つのパネルを作りたい: ガイドの[表情切り替えパネルを作る](/guide/4-build/screen/expression-switcher/) - 作りたいものに近い作例から始めたい: リファレンスの[作例のカタログ](/reference/sample-catalog/) - C# が初めて、または Flutter や uGUI の経験があって読み替えが要る: ガイドの[準備する](/guide/#3-準備する) ## ほかのタブ 調べたいことが決まっているときは、次のタブから探せます。 - [ガイド](/guide/): 探す、採用する、準備する、作る、困ったとき、配る、メンテナンスするの順に、目的の手順と仕組みを引く - [リファレンス](/reference/): 動作環境、できないこと、作例、部品の一覧、Flutter との違い、用語を引く - API リファレンス: 型やプロパティの正確な定義を引く - [コントリビューター](/contributor/): FloatSoda の開発に加わるための理解、準備、貢献の手順 # 最初のオーバーレイを作る > SteamVR のダッシュボードに、数字とボタンが出るオーバーレイを作ります。ボタンを押すと、数字が1つ増えます。 ## このチュートリアルで作るもの SteamVR のダッシュボードに、数字とボタンが出るオーバーレイを作ります。ボタンを押すと、数字が1つ増えます。 完成した画面は、青い四角の中に大きな数字と赤いボタンが縦に並んだものです。 作業は3段です。 1. 色の四角をダッシュボードに出す 2. 四角を数字に置き換え、数字を書き換える 3. ボタンを足して、押すと数字を増やす 各段の終わりに、HMD の中で確かめる点を1つ置いています。 ## 始める前に用意するもの 用意するものは次の3つです。 - .NET 10 の SDK(`dotnet --version` が `10.` で始まる) - SteamVR(起動できる状態で、HMD をつないである) - 空のコンソールアプリ 準備の手順は、ガイドの[開発環境を用意する](/guide/3-prepare/set-up-environment/)にあります。動作環境の一覧は、リファレンスの[動作環境](/reference/requirements/)です。足りないものがあれば、リンク先で用意してから戻ってください。 空のコンソールアプリは、次の2つのコマンドで作ります。1行ずつ実行します。 ```powershell dotnet new console -n FirstOverlay ``` ```powershell dotnet add FirstOverlay package FloatSoda ``` `FirstOverlay` フォルダーの中に `Program.cs` ができます。以降は、このファイル(`FirstOverlay/Program.cs`)の中身を段ごとに置き換えます。 ## 1. オーバーレイを SteamVR に出す 最初の段では、青い四角を1つ出します。`Program.cs` を次の内容に置き換えてください。 ```csharp using FloatSoda; using FloatSoda.Geometrics; using FloatSoda.Widgets; using FloatSoda.Widgets.Layout; using FloatSoda.Widgets.Paint; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddFloatSoda(); using var host = builder.Build(); var app = host.Services.GetRequiredService<FloatSodaApp>(); Widget root = new Align { Alignment = Alignment.Center, Child = new SizedBox { Width = 300, Height = 300, Child = new ColoredBox { Color = Colors.CornflowerBlue } } }; app.CreateWindow(new DashboardWindow { Title = "FirstOverlay", Child = root }); await host.RunAsync(); ``` 画面は `Align`、`SizedBox`、`ColoredBox` の3つを入れ子にして書きます。`Align` が中央に寄せ、`SizedBox` が 300 × 300 の大きさを決め、`ColoredBox` が色を塗ります。`DashboardWindow` は、この画面を SteamVR のダッシュボードに出す指定です。`Title` が、ダッシュボードのタブ名になります。 SteamVR を起動してから、`FirstOverlay` フォルダーを作った場所で、次のコマンドを実行します。 ```powershell dotnet run --project FirstOverlay ``` そのまま HMD を被り、SteamVR のダッシュボードを開きます。 **できた?** ダッシュボードの「FirstOverlay」のタブに、青い四角が出た。 <details> <summary>できなかったら</summary> 起動で例外が出たときは、メッセージを控えて、ガイドの[起動で例外が出る: SteamVR または HMD につながらない](/guide/5-troubleshoot/read-errors/#起動で例外が出る-steamvr-または-hmd-につながらない)を開きます。例外が出ないのにタブがないときは、同じページの[起動で警告が出るが、表示はされる](/guide/5-troubleshoot/read-errors/#起動で警告が出るが表示はされる)も確かめてください。 </details> `AddFloatSoda()` や `Host.CreateApplicationBuilder` の意味は、ガイドの[起動と Generic Host](/guide/4-build/mechanics/startup-and-host/)で説明しています。ここでは、この数行を毎回の書き出しとして、そのまま使ってください。 ## 2. 数字を表示して、書き換える 2段目では、四角を数字の表示に置き換え、1秒ごとに数字を増やします。画面が持つ値を書き換えて画面を描き直すには、`StatefulWidget<T>` と `State<T>` を使います。値を書き換える操作が `SetState()` です。 `Program.cs` を次の内容に置き換えてください。 ```csharp using FloatSoda; using FloatSoda.Animation; using FloatSoda.Elements; using FloatSoda.Geometrics; using FloatSoda.Painting; using FloatSoda.Widgets; using FloatSoda.Widgets.Layout; using FloatSoda.Widgets.Paint; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddFloatSoda(); using var host = builder.Build(); var app = host.Services.GetRequiredService<FloatSodaApp>(); app.CreateWindow(new DashboardWindow { Title = "FirstOverlay", Child = new CounterPanel() }); await host.RunAsync(); record CounterPanel : StatefulWidget<CounterPanel> { public override State<CounterPanel> CreateState() => new CounterPanelState(); } class CounterPanelState : TickerProviderState<CounterPanel> { private int _count; private WidgetTicker? _ticker; public override void InitState() { _ticker = CreateTicker(elapsed => { var seconds = (int)elapsed.TotalSeconds; if (seconds != _count) { SetState(() => _count = seconds); } }); _ticker.Start(); } public override void Dispose() { _ticker?.Dispose(); base.Dispose(); } public override Widget Build(IBuildContext context) { return new Align { Alignment = Alignment.Center, Child = new SizedBox { Width = 300, Height = 300, Child = new ColoredBox { Color = Colors.CornflowerBlue, Child = new Center { Child = new Text(_count.ToString()) { Style = new TextStyle { Color = Colors.White, FontSize = 120, FontWeight = 700 } } } } } }; } } ``` 段1のコードから、3か所が変わっています。 - 画面を `CounterPanel` というウィジェットに分けた。値を持つウィジェットは、`StatefulWidget<T>` を継承した型と、値を入れる `State<T>` の2つで書く - `Build()` が、`_count` の今の値で画面を返す。`SetState()` を呼ぶと、`_count` が書き換わり、`Build()` がもう一度呼ばれて画面が描き直される - 数字を増やすきっかけに、`WidgetTicker` を使った。`WidgetTicker` は、フレームごとに開始からの経過時間を渡す部品で、フレームを処理するメインのスレッドから呼ばれる きっかけを `System.Threading.Timer` にしないのは、別のスレッドから `SetState()` を呼ぶと、画面の更新が失われることがあるためです(追跡: #150)。`WidgetTicker` はフレームを処理するスレッドで呼ばれるので、このコードのままで `SetState()` を呼べます。 もう一度、SteamVR を起動してから `dotnet run --project FirstOverlay` を実行し、ダッシュボードを開きます。 **できた?** 青い四角の中央に数字が出て、1秒ごとに1つずつ増えていく。 <details> <summary>できなかったら</summary> 数字が 0 のまま変わらないときは、ガイドの[数字や文字が変わらない](/guide/5-troubleshoot/not-responding/#数字や文字が変わらない)を開きます。数字そのものが出ないときは、[文字や画像が出ない](/guide/5-troubleshoot/not-responding/#文字や画像が出ない)です。 </details> `SetState()` が画面を描き直す仕組みは、ガイドの[状態と作り直し](/guide/4-build/mechanics/state-and-rebuild/)で説明しています。 ## 3. ボタンを押して、数字を増やす 3段目では、数字の下にボタンを足し、押すと数字が1つ増えるようにします。`GestureDetector` で、押された操作を受け取る領域を作り、見た目は他のウィジェットで組み立てます。 `Program.cs` を次の内容に置き換えてください。1秒ごとに増やす `WidgetTicker` は外し、`TickerProviderState` を `State` に戻しています。 ```csharp using FloatSoda; using FloatSoda.Elements; using FloatSoda.Geometrics; using FloatSoda.Painting; using FloatSoda.Widgets; using FloatSoda.Widgets.Gesture; using FloatSoda.Widgets.Layout; using FloatSoda.Widgets.Paint; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddFloatSoda(); using var host = builder.Build(); var app = host.Services.GetRequiredService<FloatSodaApp>(); app.CreateWindow(new DashboardWindow { Title = "FirstOverlay", Child = new CounterPanel() }); await host.RunAsync(); record CounterPanel : StatefulWidget<CounterPanel> { public override State<CounterPanel> CreateState() => new CounterPanelState(); } class CounterPanelState : State<CounterPanel> { private int _count; public override Widget Build(IBuildContext context) { return new Align { Alignment = Alignment.Center, Child = new SizedBox { Width = 300, Height = 300, Child = new ColoredBox { Color = Colors.CornflowerBlue, Child = new Flex { Direction = Axis.Vertical, MainAxisAlignment = MainAxisAlignment.Center, CrossAxisAlignment = CrossAxisAlignment.Center, Children = { new Text(_count.ToString()) { Style = new TextStyle { Color = Colors.White, FontSize = 120, FontWeight = 700 } }, new GestureDetector { OnTap = () => SetState(() => _count++), Child = new SizedBox { Width = 200, Height = 80, Child = new ColoredBox { Color = Colors.Tomato, Child = new Center { Child = new Text("+1") { Style = new TextStyle { Color = Colors.White, FontSize = 50, FontWeight = 700 } } } } } } } } } } }; } } ``` 画面の組み立てとタップの処理の変更点は、次のとおりです。 - `Flex` に `Direction = Axis.Vertical` を指定して、数字とボタンを縦に並べた - 赤い四角を `GestureDetector` で包み、タップされたときに `OnTap` の処理で `SetState()` を呼んで数字を増やす 前の実行をターミナルの Ctrl+C で終了してから、SteamVR を起動した状態で `dotnet run --project FirstOverlay` を実行します。HMD を被り、ダッシュボードを開いて、コントローラーのレーザーで赤い四角を押します。 **できた?** 赤い四角を押すたびに、数字が1つ増える。 <details> <summary>できなかったら</summary> 押しても反応しないときは、ガイドの[ボタンを押しても反応しない](/guide/5-troubleshoot/not-responding/#ボタンを押しても反応しない)を開きます。`DashboardWindow` ではなく `WorldSpaceWindow` や `DeviceTrackedWindow` で試した人は、リファレンスの[できないこと・まだ使えないもの](/reference/limitations/#置く場所と入力)を確かめてください。SteamVR 上のオーバーレイでポインターの座標が届く範囲も、そのページにあります(追跡: #182)。 </details> ボタンの組み立て方は、ガイドの[ボタンやトグルを組み立てる](/guide/4-build/screen/build-buttons-toggles/)で詳しく説明しています。 ## 次に読むページ 動くオーバーレイができました。次に進むページは、目的で選べます。 - 通しで1つのパネルを作りたい: ガイドの[表情切り替えパネルを作る](/guide/4-build/screen/expression-switcher/) - 作りたいものに近い作例から始めたい: リファレンスの[作例のカタログ](/reference/sample-catalog/)