レンダラが 🐢 を自分で描けるようにする - #47
Merged
Merged
Conversation
Three additions that only make sense together, for putting the tortoise somewhere a Canvas cannot reach — an overlay, another engine, a 3-D model standing on a real table in an immersive space. TortoiseSprite.hidden stops the canvas drawing its own cursor. It is not hideTortoise(): that records a command, which serializes and travels into every renderer, while this is a property of one view. Its halfExtent is 0, so autoFit keeps no room for a sprite it will not draw. TortoisePlayer.currentTortoiseState is where the tortoise is right now, interpolated between commands. Without it a custom cursor can only step a whole command at a time, while the line it is supposedly drawing grows smoothly underneath — currentCommandIndex changes ten times a second, and the sprite moves every display frame. ViewportMode.transform and TortoiseSprite.halfExtent become public, so the cursor lands where the sprite would have. The alternative is for callers to reimplement autoFit, which agrees on the day it is written and drifts silently afterwards. The blend moves into TortoiseState.interpolated(toward:progress:), in Core beside applying(_:) and now the only implementation of it: drawTortoise takes an already-interpolated state, and the player returns the same value the canvas draws at, so the two cannot disagree. Only position and heading move — a pen goes down at a command, not across one.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #47 +/- ##
==========================================
+ Coverage 92.78% 93.76% +0.97%
==========================================
Files 16 16
Lines 943 946 +3
==========================================
+ Hits 875 887 +12
+ Misses 68 59 -9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
4 tasks
This was referenced Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
Fixes #46
「線はライブラリが描き、タートルは呼び出し側が描く」ための3つを追加します。互いに単独では意味をなさず、3つ揃って初めて成立するものです。
用途は
Canvasが届かない場所にタートルを置くこと — SwiftUI のオーバーレイ、別エンジンのスプライト、イマーシブ空間で現実の机の上に立つ 3D モデル。TortoiseSprite.hidden(UI)TortoisePlayer.currentTortoiseState(UI)ViewportMode.transform(...)/TortoiseSprite.halfExtentを public にそれぞれの理由
TortoiseSprite.hidden—hideTortoise()では代用できない自前のカーソルを描くと、canvas が描く三角形/画像と二重になります。
hideTortoise()は一見これに使えそうですが別のものです。あれはコマンドなので、シリアライズされて SVG にも PNG にもサムネイルにも保存ファイルにも付いて回ります。ここで要るのは「この view では描かない」という表示側の指定で、描画そのものを変えてはいけません。halfExtentは 0 です。描かないスプライトのために.autoFitが余白を取る理由がないためで、結果として描画は端から端まで広がります。余白が要る場合は呼び出し側の.padding()なりフレームなりで取ります(この点は DocC と CHANGELOG に明記しました)。currentTortoiseState— コマンド単位では「歩く」が再現できないTortoisePlayerが公開していたのはcurrentCommandIndexとisFinishedだけで、これはコマンド単位、毎秒10回程度しか動きません。一方 canvas はanimationProgressでコマンド間を補間してスプライトを描いています(CanvasModelは internal)。そのため
currentCommandIndexからカーソルを動かすと、線はなめらかに伸びていくのにタートルだけが1コマンドぶんワープするという絵になります。「タートルが線を引きながら歩く」がこのライブラリの見せどころなので、ここが取れないと自前描画は成立しません。毎表示フレーム変わる値なので、SwiftUI の
bodyから observe するものではありません(view がリフレッシュレートで再評価されます)。すでに毎フレーム動いている場所 — RealityKit の scene update、TimelineViewのクロージャ、CADisplayLink— から読むこと、UI は従来どおりcurrentCommandIndexで駆動すること、を DocC に書きました。transformの公開 — 書き直させると黙ってずれる.autoFitは描画のバウンディングボックスに合わせて拡大・中央寄せするので、呼び出し側が自前カーソルを正しい位置に置くには同じ写像が要ります。internal のままだと各自が再実装することになり、書いた日は一致していて、あとから黙ってずれる種類のコードが増えます。補間を1箇所に寄せた
補間そのものは
TortoiseState.interpolated(toward:progress:)として Core のapplying(_:)の隣に置き、唯一の実装にしました。CanvasRenderer.drawTortoiseは補間済みの状態を受け取るだけになり、currentTortoiseStateは canvas が描いているのと同じ値を返します。自前カーソルが組み込みスプライトから1コマンドの端数ぶんずれることが、原理的に起きません。drawTortoiseの引数からはprogressもnextも消えているので、別の補間をしようにも材料がありません。動くのは位置と向きだけです。ペンはコマンドの瞬間に下りるものなので、
isPenDownや色を補間すると、そのプログラムが一度も取らなかった状態を発明してしまいます。向きは短い方の弧を通り(350° → 10° は 20° 進む)、progressは 0...1 にクランプするので、遅れて発火したタイマーがタートルを目的地の先へ運ぶことはありません。挙動の変更
ありません。
TortoiseSpriteに case が増えるので、利用側で網羅的にswitchしている場合にアーム追加が要る — ソースレベルの影響はそれだけです。実際の利用側で確認済み
TortoiseBlocks の
visionos-viewerをこのブランチに pin してビルド・実行しています。USDZ のタートルが visionOS ランタイムで実際にロードされること、200×200 の描画が紙の内側の四隅に小数4桁まで一致して写ることを確認しました。残りの見た目の確認はヘッドセットの担当です。テスト
12件追加(合計142件):
progressのクランプ、0°をまたぐ短い弧、向きの正規化、位置と向き以外が動かないことcurrentTortoiseState(UI): 未 attach では nil、コマンド途中では「コミット済みの状態」ではないこと、canvas がレンダラに渡す値と一致すること、静止時はコミット済みの状態そのものになること.hidden(UI):halfExtentが 0 で.autoFitの inset も 0 になること、等価性ゴールデン画像
.hidden用にhiddenSprite.1.pngを1枚だけ新規記録し、目視確認済み(2本の線が描かれ、カーソルは無い)imageSprite.1.pngと画素比較すると、差があるのは 82×82px の1領域だけ(+559+159=スプライトの位置、40×40pt @2x にアンチエイリアスぶん)。線の画素は完全に一致しており、.hiddenが消すのはカーソルだけで描画には触れていないことの裏付けになりますチェックリスト
swift testがローカルで通る(142件・16スイート)xcrun swift-format lint --recursive --strict Sources Testsが通るCHANGELOG.mdを更新(2.1.0セクションを新設)