Skip to content

visionOS 版は .tortoise のビューワーにする(現実の平面をキャンバスに、🐢 を走らせる) #53

Description

@temoki

進捗(2026-08-18) — ブランチ visionos-viewer、Phase 0〜3 すべて完了(すべて実機確認済み)。PR は #54。
3D の 🐢 には TortoiseGraphics2 2.1.0(temoki/TortoiseGraphics2#46 / #47)が要った。
「注意」の 2 件は両方とも片付いた。 プライバシーポリシーは第 6 節を追加済み、公開タイミングは #55 で pages.yml が手動実行のみになったので、マージしてもサイトはデプロイされない。マージ前の判断は残っていない。
詳細は各フェーズのコメント(Phase 0 / Phase 0 追記 / Phase 1 / Phase 2 / 配置のやりなおし)。

背景

visionOS 対応は #11 で一度「iPad アプリ相当が動く」ところまで作り、PR #52 に退避した。
動くには動くが、それが visionOS である理由がどこにもない。同じことは Apple の
iPad 互換モードが最初からやっている。そして退避した残課題

  • メインウィンドウからファイルを選び直せない(dismiss() はシステムの「書類」
    ボタンを見せるだけで、openDocument / newDocument は visionOS で unavailable)
  • 表示の切り替えで前の View が背景に残る(実機のみ・アプリ全域)

は、どちらも DocumentGroup と編集 UI に由来する。回避策を探し続けるより、
その機能を持たない設計にした方が早い。

一方で、visionOS でしかできないことが一つある。**タートルグラフィックスの
タートルは、元々は床を走って紙にペンで描く実物のロボットだった。**画面の中の
三角形の方が後から出てきた代用品で、机の上を 🐢 が這って線を引くのは 3D 化の
こじつけではなく原点回帰にあたる。

そこで visionOS 版は iPad/macOS 版の移植をやめ、.tortoise のビューワーとして
作り直す
。

仕様案

全体像

visionOS 版は「編集しないアプリ」。3 つの面で構成する。

面 中身
ImmersiveSpace(mixed) 現実の平面に貼りついたキャンバス。その上を 🐢 が動いて描く
ウィンドウ 1 ブロック(読み取り専用)。再生位置に合わせてハイライト
ウィンドウ 2 生成された Swift コード(Phase 3、任意)

iPad ではキャンバス/コードをトグルで切り替えているが、visionOS には面が
いくらでもある。ブロックと絵を同時に見せられるのは iPad 版より良い版であって、
機能を削った版ではない。ここが visionOS 版の存在理由になる。

ドキュメントの入口 — DocumentGroup は使わない

.fileImporter を持つ普通の WindowGroup にする。read-only なら
DocumentGroup(viewing:) という手もあるが、#52 で踏んだ「書類選択に戻れない」
問題は viewing でも同じはずなので採らない。素の fileImporter なら 1 タップで
開き直せて、あの残課題がそのまま消える。

サンプル作品をアプリに同梱する。 現状ドキュメントは iCloud ではなく
「On My iPad」のローカルフォルダに保存されるので、Vision Pro には AirDrop 等で
手で持っていくことになる。同梱しないと初回起動が空のビューワーになる。
appstore/screenshot-sources/ に既に 2 つある。

描画をどう 3D にするか — Phase 0 で実機を見て決める

  • (a) テクスチャ方式 — 既存の 2D 描画をテクスチャ(TextureResource.DrawableQueue)
    にして平面エンティティに貼る。既存資産をほぼ全部再利用でき、安い。
    拡大したときの解像度が頭打ちになる。
  • (b) メッシュ方式 — ストロークをリボン/チューブのジオメトリにする
    (LowLevelMesh 等)。拡大に強く「ペンを上げたら浮く」表現もできるが、
    fill の三角形分割まで自前になる。

判断材料は「テクスチャ方式で見られる絵になるか」「10,000 コマンドで毎フレーム
更新が保つか」の 2 点だけ。まずアプリ側で試作する。 upstream-first からすると
RealityKit レンダラは TortoiseGraphics2 の話に見えるが、スケールとアンカーは
アプリ固有なので、一般化できると分かってから upstream に上げる。

ハイライトは「できれば」ではなく中核

これが無いと visionOS 版は「絵が出てくるのを眺めるもの」でしかない。ブロックと
絵の対応こそがこのアプリの教育的な核で、それを一番よく見せられるのが visionOS。

実装は既存の不変条件にただ乗りする。RunnerModel.currentBlockID は
expandedBlockIDs[player.currentCommandIndex] で、これは Tortoise.apply が
入力ストリームを index 対応でそのまま記録することに依存している(Kit の
往復テストで固定済み)。

設計上の制約: CommandPlayer はタイムラインとして残し、差し替えるのは
描画側だけ
にする。そうすればハイライトは自動的についてくる。新レンダラが
独自のタイムラインを持つと、この対応が壊れる。

スケールと単位

キャンバスはポイント、机はメートル。描画のバウンディングボックス
(DrawingBounds.compute は public)を、検出した平面上の一定サイズ(60cm 角程度)に
収める写像を 1 箇所に置く。PNG 書き出しの exportFrameSize と同じ考え方。
この写像は UI に依存しないので Kit 側に置いてテストする。

ターゲット構成 — 増やさない

App ターゲット 1 本のまま #if os(visionOS) で Scene ツリーだけ差し替える。
別ターゲットにすると Support/Info.plist の UTType 宣言が二重になり、署名も増える。
TortoiseBlocksApp は既に #if os(iOS) で LaunchScene を出し分けている。
バンドル ID が同じなら universal purchase も自然に効く。

やること

Phase 0 の結論が出るまで Phase 1 以降の見積もりは意味を持たない。

Phase 0 — 実機スパイク(捨てる前提)

  • ImmersiveSpace(.mixed) + ARKit PlaneDetectionProvider で平面を取る
  • 既存の 2D 描画をテクスチャで机に貼る(上の (a)) → 不要。ViewAttachmentComponent で既存の TortoiseCanvas をそのまま置ける
  • 既存スプライトを 🐢 として立てる(紙に寝ている)
  • 10,000 コマンドの作品で性能を測る → 問題なし
  • → (a) でも (b) でもない。スケールは定数ではなくジェスチャー(0.2〜2m)

Phase 1 — ビューワーの骨格

  • WindowGroup + .fileImporter、サンプル同梱(SampleBlocks を流用、追加リソースなし)
  • ImmersiveSpace に絵、再生コントロール(既存の PlaybackControls をそのまま再利用)
  • NSWorldSensingUsageDescription と、平面が見つからない場合の代替表示(15 秒で諦めて目の前に浮かべる)

Phase 2 — ブロックの窓(ここまでで初めて「このアプリ」になる)

  • 読み取り専用のワークスペース表示(D&D・編集・削除なし)
  • 再生進捗に追従するハイライト(再生位置まで自動スクロールする)

Phase 3 — 仕上げ

  • 3D の 🐢(USDZ)に差し替え
  • コードの窓 — ブロックの窓とほぼ同じ構造
  • SVG / PNG 書き出し → 作ったうえで外した。 安く動いたが、絵を変えられないビューワーが 2 つ目の成果物を書き出すのは、絵を作る場所(iPad / Mac)の仕事。リモコンのボタンも 6 個になっていた

3 つは互いに独立しているので着手順は自由。アセット待ちの 🐢 を後回しにするのが動かしやすい。

受け入れ条件

Phase 0:

  • 実機で、机の上に置いた絵が「線として読める」品質かどうかを目視で判定できている → 読める
  • 10,000 コマンドの作品でフレームレートが実用範囲か分かっている → 問題なし
  • (a)/(b) の選択とスケールが決まり、この issue に追記されている → どちらでもない/サイズはジェスチャー

Phase 1–3:

  • .tortoise を開くと、現実の平面の上に絵が現れ、🐢 が動いて描く
  • ファイルを開き直すのに 1 タップで済む(visionOS 対応(退避・1.1.0 には入れない) #11 #52 の残課題が再現しない)
  • ブロックの窓が再生位置に追従してハイライトする
  • 編集操作は一切露出していない(保存もしないので、ドキュメントが汚れることがない)
  • 既存 2 プラットフォームのビルド・Kit テスト(119 件)・lint が緑のまま

注意(先に潰しておくもの)

プライバシーポリシーが 1 行だけ嘘になる。(NSWorldSensingUsageDescription はブランチで追加済み。site/privacy.html と App Store の質問票はまだ) 平面検出には
NSWorldSensingUsageDescription の許諾が要る。カメラ映像がアプリに渡るわけでは
ないが、site/privacy.html は現在「位置情報、連絡先、写真、カメラ、マイクへの
アクセス」なしと書いており、App Store のプライバシー質問票も同じ申告になっている。
「端末内で完結し、送信も保存もしない」と書き足せば済む話だが、忘れると審査で
止まる種類のもの
。release skill の管轄。

3D の 🐢 アセットが要る。 現状は 23×32px の 2D 画像。USDZ の用意はアイコンと
同じく手作業になるので、Phase 1 は既存スプライトのビルボードで逃げて、
アセット待ちで止まらないようにする。

検証は実機でしかできない。 シミュレータに現実の平面はない。CI はビルドまで、
目視は毎回ヘッドセット。座標写像のように Kit へ落とせるロジックは落としておく。

対象年齢が噛み合わない。 Vision Pro は Apple 自身が 13 歳以上を想定している。
visionOS 版の位置づけは「大人が見せる/教室で見せる」で、site が「保護者と先生に
向けて書く」方針なのとは合う。ストア文言を iOS からそのままコピーしない理由になる。

PR #52 の扱い

この issue は #52 を置き換えるが、#52 の成果のうちプラットフォーム基盤は
そのまま活きる
。

活きるもの:

  • project.pbxproj の SUPPORTED_PLATFORMS / XROS_DEPLOYMENT_TARGET /
    TARGETED_DEVICE_FAMILY = "2,7"
  • 署名スコープの修正(ad-hoc を [sdk=macosx*] に限定。これは実機デバッグを
    塞いでいたバグ修正なので、この issue と切り離して main に入れてよい
    )
  • CI の visionOS ビルド、fastlane の platform :visionos、metadata_check.rb の
    3840×2160
  • ストア文言・LP の visionOS 記述(出荷するまで公開しないこと。文面は 3 つの
    リストで共有されているので、公開すると iOS/macOS のリストまで Vision Pro 対応を
    名乗ってしまう)

捨てるもの:

  • 3 ペインのカラム幅調整、documentBrowserToolbar(削除済み)、コードペインの白紙化
    (コードの窓を作るなら Phase 3 で参照する)

引き継ぐ知見:

  • hoverEffect と draggable を同じ View に付けると visionOS で segfault する
    (効果を ButtonStyle 側に 1 段下げると通る)
  • ToolbarSpacer は visionOS に無い
  • 表示切り替えの残像は実機のみ・アプリ全域。ビューワーでも踏む可能性があるので
    Phase 1 で再現するか確認し、するなら Feedback Assistant へ

Refs #11, #52

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestneeds design実装前に設計コメントが必要。進め方は CLAUDE.md の Issue Workflow を参照platform: visionOSvisionOS 固有priority: low優先度: 低

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions