Skip to content

レンダラが 🐢 を自分で描けるようにする - #47

Merged
temoki merged 1 commit into
mainfrom
draw-your-own-tortoise
Aug 17, 2026
Merged

レンダラが 🐢 を自分で描けるようにする#47
temoki merged 1 commit into
mainfrom
draw-your-own-tortoise

Conversation

@temoki

@temoki temoki commented Aug 16, 2026

Copy link
Copy Markdown
Owner

概要

Fixes #46

「線はライブラリが描き、タートルは呼び出し側が描く」ための3つを追加します。互いに単独では意味をなさず、3つ揃って初めて成立するものです。

用途は Canvas が届かない場所にタートルを置くこと — SwiftUI のオーバーレイ、別エンジンのスプライト、イマーシブ空間で現実の机の上に立つ 3D モデル。

追加 何のため
TortoiseSprite.hidden (UI) canvas がカーソルを描かない
TortoisePlayer.currentTortoiseState (UI) いまタートルがどこにいるか
ViewportMode.transform(...) / TortoiseSprite.halfExtent を public に その位置を view 上のどこに置くか

それぞれの理由

TortoiseSprite.hiddenhideTortoise() では代用できない

自前のカーソルを描くと、canvas が描く三角形/画像と二重になります。hideTortoise() は一見これに使えそうですが別のものです。あれはコマンドなので、シリアライズされて SVG にも PNG にもサムネイルにも保存ファイルにも付いて回ります。ここで要るのは「この view では描かない」という表示側の指定で、描画そのものを変えてはいけません。

halfExtent は 0 です。描かないスプライトのために .autoFit が余白を取る理由がないためで、結果として描画は端から端まで広がります。余白が要る場合は呼び出し側の .padding() なりフレームなりで取ります(この点は DocC と CHANGELOG に明記しました)。

currentTortoiseState — コマンド単位では「歩く」が再現できない

TortoisePlayer が公開していたのは currentCommandIndexisFinished だけで、これはコマンド単位、毎秒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 の引数からは progressnext も消えているので、別の補間をしようにも材料がありません。

動くのは位置と向きだけです。ペンはコマンドの瞬間に下りるものなので、isPenDown や色を補間すると、そのプログラムが一度も取らなかった状態を発明してしまいます。向きは短い方の弧を通り(350° → 10° は 20° 進む)、progress は 0...1 にクランプするので、遅れて発火したタイマーがタートルを目的地の先へ運ぶことはありません。

挙動の変更

ありません。 TortoiseSprite に case が増えるので、利用側で網羅的に switch している場合にアーム追加が要る — ソースレベルの影響はそれだけです。

実際の利用側で確認済み

TortoiseBlocksvisionos-viewer をこのブランチに pin してビルド・実行しています。USDZ のタートルが visionOS ランタイムで実際にロードされること、200×200 の描画が紙の内側の四隅に小数4桁まで一致して写ることを確認しました。残りの見た目の確認はヘッドセットの担当です。

テスト

12件追加(合計142件):

  • 補間 (Core): 中間点、両端が厳密に元の状態と一致すること、範囲外 progress のクランプ、0°をまたぐ短い弧、向きの正規化、位置と向き以外が動かないこと
  • currentTortoiseState (UI): 未 attach では nil、コマンド途中では「コミット済みの状態」ではないこと、canvas がレンダラに渡す値と一致すること、静止時はコミット済みの状態そのものになること
  • .hidden (UI): halfExtent が 0 で .autoFit の inset も 0 になること、等価性

ゴールデン画像

  • SVG: 無変更(SVG はもともとタートルを描きません)
  • Canvas PNG: 既存は無変更.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 セクションを新設)
  • ゴールデン画像: 既存の出力は変わらず全件通過。新規1枚のみ記録し目視確認済み

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

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.76%. Comparing base (8304101) to head (7756fd1).

Files with missing lines Patch % Lines
Sources/TortoiseUI/CanvasRenderer.swift 80.00% 1 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@temoki temoki changed the title Let a renderer draw the tortoise itself (#46) レンダラが 🐢 を自分で描けるようにする Aug 17, 2026
@temoki
temoki merged commit 83941fe into main Aug 17, 2026
6 checks passed
@temoki
temoki deleted the draw-your-own-tortoise branch August 17, 2026 04:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

レンダラが 🐢 を自分で描けるようにしたい(Canvas の外にタートルを置く)

1 participant