背景
TortoiseBlocks の visionOS 版(temoki/TortoiseBlocks#53)で、机の上に置いた紙の上を 3D モデルの 🐢 が歩くようにしたい。線は今まで通り TortoiseCanvas が ViewAttachmentComponent の中で描き、タートルだけを RealityKit のエンティティに差し替える。
つまり「描画はライブラリ、カーソルは呼び出し側」という構成で、これは visionOS 固有の話ではない。SwiftUI のオーバーレイ、別のゲームエンジンのスプライト、SpriteKit — どれも同じものを必要とする。
いま、これができない。3 つ足りない。
足りないもの
1. スプライトを描かない指定がない
TortoiseSprite は .triangle と .image だけ。自前のカーソルを描くと、canvas が描く三角形/画像と二重に出る。
hideTortoise() は別のもので、代わりにはならない。あれはコマンドなので、シリアライズされて SVG にも PNG にもサムネイルにも保存ファイルにも付いて回る。ここで欲しいのは「この view では描かない」という表示側の指定。
2. タートルの現在位置が取れない
TortoisePlayer が公開しているのは currentCommandIndex と isFinished だけ。これはコマンド単位、つまり毎秒 10 回程度しか動かない。
一方 canvas は animationProgress でコマンド間を補間してスプライトを描いている(CanvasModel は internal)。だから currentCommandIndex からカーソルを動かすと、線はなめらかに伸びていくのにタートルだけがワープする。「タートルが線を引きながら歩く」がこのライブラリの売りなので、これが取れないと自前描画は成立しない。
3. 座標のマッピングが取れない
ViewportMode.transform(...) と TortoiseSprite.halfExtent が internal。.autoFit は描画のバウンディングボックスに合わせて拡大・中央寄せするので、呼び出し側は同じ写像を自分で書き直すしかない。書いた日は一致していて、あとから黙ってずれる種類のコードになる。
提案
TortoiseSprite.hidden — canvas はアニメーションはそのまま、スプライトだけ描かない。halfExtent は 0(描かないものに余白を取る理由がない)ので .autoFit の inset も 0 になる。余白が要るなら呼び出し側で .padding()
TortoisePlayer.currentTortoiseState: TortoiseState? — いま canvas がスプライトを描いている、まさにその状態。canvas に attach されるまでは nil
ViewportMode.transform(canvasSize:viewSize:drawingBounds:spriteHalfExtent:) と TortoiseSprite.halfExtent を public に
補間そのものは TortoiseState.interpolated(toward:progress:) として Core に置き、CanvasRenderer と currentTortoiseState の両方がそこを通るようにする。そうすれば自前カーソルが組み込みスプライトから 1 コマンドの端数ぶんずれることが原理的に起きない。位置と向きだけを補間する(ペンはコマンドの瞬間に下りるもので、途中まで下りた状態は存在しない)。
受け入れ条件
背景
TortoiseBlocks の visionOS 版(temoki/TortoiseBlocks#53)で、机の上に置いた紙の上を 3D モデルの 🐢 が歩くようにしたい。線は今まで通り
TortoiseCanvasがViewAttachmentComponentの中で描き、タートルだけを RealityKit のエンティティに差し替える。つまり「描画はライブラリ、カーソルは呼び出し側」という構成で、これは visionOS 固有の話ではない。SwiftUI のオーバーレイ、別のゲームエンジンのスプライト、SpriteKit — どれも同じものを必要とする。
いま、これができない。3 つ足りない。
足りないもの
1. スプライトを描かない指定がない
TortoiseSpriteは.triangleと.imageだけ。自前のカーソルを描くと、canvas が描く三角形/画像と二重に出る。hideTortoise()は別のもので、代わりにはならない。あれはコマンドなので、シリアライズされて SVG にも PNG にもサムネイルにも保存ファイルにも付いて回る。ここで欲しいのは「この view では描かない」という表示側の指定。2. タートルの現在位置が取れない
TortoisePlayerが公開しているのはcurrentCommandIndexとisFinishedだけ。これはコマンド単位、つまり毎秒 10 回程度しか動かない。一方 canvas は
animationProgressでコマンド間を補間してスプライトを描いている(CanvasModelは internal)。だからcurrentCommandIndexからカーソルを動かすと、線はなめらかに伸びていくのにタートルだけがワープする。「タートルが線を引きながら歩く」がこのライブラリの売りなので、これが取れないと自前描画は成立しない。3. 座標のマッピングが取れない
ViewportMode.transform(...)とTortoiseSprite.halfExtentが internal。.autoFitは描画のバウンディングボックスに合わせて拡大・中央寄せするので、呼び出し側は同じ写像を自分で書き直すしかない。書いた日は一致していて、あとから黙ってずれる種類のコードになる。提案
TortoiseSprite.hidden— canvas はアニメーションはそのまま、スプライトだけ描かない。halfExtentは 0(描かないものに余白を取る理由がない)ので.autoFitの inset も 0 になる。余白が要るなら呼び出し側で.padding()TortoisePlayer.currentTortoiseState: TortoiseState?— いま canvas がスプライトを描いている、まさにその状態。canvas に attach されるまでは nilViewportMode.transform(canvasSize:viewSize:drawingBounds:spriteHalfExtent:)とTortoiseSprite.halfExtentを public に補間そのものは
TortoiseState.interpolated(toward:progress:)として Core に置き、CanvasRendererとcurrentTortoiseStateの両方がそこを通るようにする。そうすれば自前カーソルが組み込みスプライトから 1 コマンドの端数ぶんずれることが原理的に起きない。位置と向きだけを補間する(ペンはコマンドの瞬間に下りるもので、途中まで下りた状態は存在しない)。受け入れ条件
.hiddenで線は今まで通り描かれ、スプライトだけ消えるcurrentTortoiseStateがコマンド間で補間されている(コミット済みの状態ではない)currentTortoiseStateが canvas の描画位置と一致する