Skip to content

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

Description

@temoki

進捗(2026-08-18) — ブランチ visionos-viewerPhase 0〜3 すべて完了(すべて実機確認済み)。PR は #54
3D の 🐢 には TortoiseGraphics2 2.1.0(temoki/TortoiseGraphics2#46 / #47)が要った。
「注意」の 2 件は両方とも片付いた。 プライバシーポリシーは第 6 節を追加済み、公開タイミングは #55pages.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.pbxprojSUPPORTED_PLATFORMS / XROS_DEPLOYMENT_TARGET /
    TARGETED_DEVICE_FAMILY = "2,7"
  • 署名スコープの修正(ad-hoc を [sdk=macosx*] に限定。これは実機デバッグを
    塞いでいたバグ修正なので、この issue と切り離して main に入れてよい
  • CI の visionOS ビルド、fastlane の platform :visionosmetadata_check.rb
    3840×2160
  • ストア文言・LP の visionOS 記述(出荷するまで公開しないこと。文面は 3 つの
    リストで共有されているので、公開すると iOS/macOS のリストまで Vision Pro 対応を
    名乗ってしまう)

捨てるもの:

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

引き継ぐ知見:

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

Refs #11, #52

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