Sausage Erectos X Y軸は下、Z軸は手前 ── 2Dと3Dの座標系が噛み合わない地獄
前回、私はAIに「もういい」と言い放った。
しかし本当の地獄は、AIを変えた後に始まった。
問題の根源は、AIの能力ではなかった。2Dと3Dの座標系──この、コンピュータサイエンスの教科書なら3ページで終わる話が、実装では底なしの沼だったのだ。
2つの世界の「上」は違う
Paper.js の世界:Y軸は下を向く
Paper.jsは2Dベクターグラフィックスライブラリだ。HTMLのCanvas APIと同じく、座標系は左上が原点、Y軸が下方向に伸びる。
(0,0) ────→ X
│
│
↓
Y
これはWebの世界では当たり前だ。CSSもSVGもこの座標系を使う。「下に行くほどYが増える」──直感に反するが、Web開発者には常識とされている。
Three.js の世界:Y軸は上、Z軸が手前
Three.jsは3Dライブラリだ。座標系は右手系。Y軸が上方向、Z軸がカメラに向かって手前方向に伸びる。
Y (上)
│
│
│
└──── X (右)
/
/
Z (手前)
数学やOpenGLの世界では標準的な座標系だ。
問題:2つの座標系を同時に扱う
Sausage Erectosは、Three.jsで3Dモデルを表示しながら、Paper.jsで2Dの展開図(レーザー加工用のDXF図面)を同時生成する。
つまり、同じ「ピクトグラムの肩の位置」を:
- Three.js側では Y=上、Z=手前 で計算し
- Paper.js側では Y=下 で計算する
この変換を、アプリの全ての関節・全てのパーツ・全てのフレームで正しく行う必要がある。
角丸が逆になる ── 「たった1つのバグ」の正体
フットボードの要件
フットボード(台座)は単純な長方形ではない。
「接続面(ピクトグラムと繋がる側)は直角、外側は角丸」
これだけの要件だ。図にするとこうなる:
┌────────────────────────┐
│ │ ← 接続面:直角
│ │
│ │
╰────────────────────────╯ ← 外側:角丸
AIが何度も間違えた理由
この「上が直角、下が角丸」を実装する時、座標系の変換が絡む。
Paper.jsでは「下」はYが大きい方向。Three.jsでは「下」はYが小さい方向。AIがコードを書く時、無意識に座標系を混同する。
結果:
╭────────────────────────╮ ← ここが角丸になってしまう
│ │
│ │
│ │
└────────────────────────┘ ← ここが直角になってしまう
逆だ。
修正を指示する。AIは「修正しました」と返す。コードを見ると、確かに変更されている。しかし実行すると──逆だ。
AIは「2DのY座標を反転する」コードを書いた。正しい。しかしその反転を2回適用していた。反転の反転は元に戻る。つまり直っていない。
人間も間違える
公平を期すために言えば、私も間違えた。
「ここのYを反転して」とAIに指示した箇所が、実はThree.js側の座標で、Paper.js側ではなかった。結果、正しかったものが壊れた。
座標系の問題は、人間もAIも等しく騙す。これは能力の問題ではなく、認知の問題だ。
解法:座標変換を「1箇所」に閉じ込める
アンチパターン:変換を散らばらせる
最初の実装では、座標変換のコードがアプリ全体に散らばっていた。
// SceneManager.js
const y3d = -paperY; // Paper→Three変換
// GeometryBuilder.js
const paperY = -y3d; // Three→Paper変換
// Exporter.js
const dxfY = canvasHeight - paperY; // Paper→DXF変換
各ファイルが独自に変換を行い、しかもその変換ロジックが微妙に異なる。バグが入り込む隙間だらけだ。
パターン:変換関数を1つにまとめる
解決策は単純だった。座標変換を担う関数を1つだけ作り、全てのコードがそれを通す。
// coordinateUtils.js
export function paperToThree(paperPoint) {
return { x: paperPoint.x, y: -paperPoint.y, z: 0 };
}
export function threeToPaper(threePoint) {
return { x: threePoint.x, y: -threePoint.y };
}
当たり前の設計パターンだ。しかし「当たり前」が最初からできていれば、深夜にパンクロックを歌う必要はなかった。
教訓:「たった2つの事実」を壁に貼れ
最終的に、私はモニターの横にポストイットを貼った。
Paper.js: Y軸は下 Three.js: Y軸は上、Z軸は手前
たった2行。しかしこの2行を常に意識するだけで、バグは劇的に減った。
AIにも毎回のプロンプトの冒頭にこの2行を書くようにした。すると、AIの座標系関連のミスも激減した。
赤いボードと青いボード ── 複雑さの罠
最初の設計:2枚のボード
初期設計では、台座は2枚のボードで構成されていた。
- 青いボード(ベース):固定長。ピクトグラムの足元に配置。
- 赤いボード(拡張用):重心に応じて伸縮。青いボードの先端に接続。
接続面は直角、先端は角丸。2枚のボードそれぞれに角丸処理が必要で、しかもそれぞれの座標系変換も必要。複雑さは2倍ではなく、4倍になった。
削ぎ落とす勇気
最終的に、赤いボードは廃止された。
青いボード1枚が直接伸縮する。接続面は直角、先端は角丸。変換ロジックは1箇所。
この「削ぎ落とし」は、座標系バグとの格闘の末に辿り着いた結論だ。複雑な設計を維持するコストが、得られるメリットを遥かに上回っていた。
複雑さは、理由がない限り罪だ。
「見えないバグ」を見えるようにする
スクリーンショット駆動デバッグ
座標系のバグには、ログ出力では気づけないものがある。数値は正しく見えるのに、画面では反転している。
最終的に私が採用したのは「スクリーンショット駆動デバッグ」だ。
- 変更を加える
- 3Dビューと2Dビューの両方をスクリーンショットに撮る
- AIに両方の画像を送り、「この2つの表示は一致しているか?」と問う
AIは画像の差異を人間より正確に検出できる。コードの生成では間違えるAIが、画像の比較では驚くほど正確だった。
適材適所。AIの「得意なこと」と「苦手なこと」を見極めるのも、AI時代のスキルだ。
次回予告:台座が無限に伸びていく
座標系の問題は解決した。しかし、もっと恐ろしいバグが待っていた。
台座を伸ばす → 重くなる → 重心が変わる → さらに台座を伸ばす → さらに重くなる → …
無限ループ。フィギュアが上空に逃げていく。
次回は、この「自己参照バグ」と、それを解決した**Payload CoM(ペイロード重心)**という概念について書く。
第3回「台座が無限に伸びていく ── 重心計算の自己参照バグとPayload CoMの発明」── 近日公開。