ジオメトリと編集の契約
bbox の計算、ヒットテストのルール、レイヤーリスト操作、パスのジオメトリ、リサイズの制御、グリッドスナップ。
bbox の計算
packages/ には、他のジオメトリモジュール間で共有される矩形/bbox の計算処理が置かれています。ドキュメント空間の単位はミリメートルであり、回転は常に矩形自身の中心を軸とした時計回りの度数です——これは @zpd/core 全体で用いられているのと同じ規約です。
| Function | 目的 |
|---|---|
rectCenter(rect) | 矩形の中心点。 |
rectCorners(rect) | 4 つの角を [Pt, Pt, Pt, Pt] として返す。 |
boundsOfPoints(points) | 点の集合を囲む軸並行の境界。 |
rotatedRectAABB(rect, rotationDeg) | 矩形の 4 つの角を自身の中心を軸に回転させ、その min/max を取った回転後のバウンディングボックス。rotationDeg が 0/undefined の場合は高速に何もしない no-op となる。結果は常に新しいコピーであり、入力の矩形を参照として返すことはない。 |
unionBbox(a, b) | a と b の両方を含む最小の矩形。 |
mergeBboxes(rects) | リストに対して unionBbox を畳み込んだもの——選択範囲全体を囲む bbox。 |
ヒットテスト
packages/ は、すでに投影されたフラット leaf に対する mm 空間のキャンバスヒットテストを実装します。hitTestDoc(doc, mmX, mmY) は最前面からの 2 段階で走査し、まず pattern 以外、どの通常アートワークにも 当たらない場合だけ pattern を調べます。非表示の祖先/コンテナは投影時に すでに畳み込まれています。
| Layer type | ヒットテスト |
|---|---|
shape | 回転した矩形への点内包判定。shape === 'ellipse' のときは楕円の分岐(正規化した楕円方程式)を用いる。 |
image | 回転した矩形への点内包判定(矩形のみ。楕円の分岐はなし)。 |
text | 推定した bbox に対する、回転矩形への点内包判定。@zpd/core にはキャンバス/DOM のフォントメトリクスがないため、estimateTextBbox は実際のグリフ計測ではなく、おおまかな等幅フォント寄りの推定(TEXT_CHAR_WIDTH_FACTOR = 0.6、TEXT_LINE_HEIGHT_FACTOR = 1.2)を用いる。クリックで選択するには十分だが、レイアウトの正確な根拠にはならない。 |
path | フラット化した塗り領域に対する even-odd(偶奇)の点内包判定(closed のときのみ)に加え、余裕を持たせたつかみ幅の下限(Math.、MIN_STROKE_GRAB_MM = 1.5)でのストローク距離判定。これにより、細い線でもクリックしやすいままになる。 |
pattern | 移動可能な x/y/size 正方形への点内包判定。pattern のフォールバック段階でのみ考慮される。 |
パターンはアートワークのクリックを奪わない
pattern 正方形はキャンバスから選択できますが、視覚的に上にあっても pattern 以外のヒットが常に先です。大きな背景 pattern の下にある アートワークへ引き続きアクセスできます。
素の Node/Vitest にはブラウザの Path2D が存在しないため、パスのヒットテストは isPointInPath/isPointInStroke を使いません——ベジェをポリライン(折れ線)にフラット化し(下記参照)、独自に even-odd のレイキャスティングと点から線分への距離計算を行います。これにより @zpd/core は依存関係を持たず、ブラウザの外でもテスト可能なままになっています。
レイヤーリスト操作
packages/ は投影 leaf 用の小さなフラット配列ヘルパーを 残しています。正式なドキュメント変更は group-ops.ts の通常ツリー/固定 スタックヘルパーを使います。すべて純粋かつイミュータブルで、意味上の no-op は入力と同一参照を返すため、幽霊のような履歴 エントリを避けられます。
| Function | 効果 |
|---|---|
insertPcbNode(stack, role, node, parentId?, index?) | 深さと id の安全性を守り、通常 leaf/group を 1 つの固定マテリアルへ挿入する。 |
movePcbNode(stack, id, role, parentId?, index?) | マテリアル間を含め、通常サブツリーを親変更または並べ替えする。 |
groupPcbNodes(stack, ids, name?) / ungroupPcbNode(...) | 互換な通常ルートをグループ化、または通常グループを 1 つ解除する。固定ルートは参加しない。 |
clonePcbNode(...) / deletePcbNodeById(...) | 新しい id で複製、または通常サブツリーを削除する。 |
updatePcbNodeById(...) / mapPcbLeavesById(...) | 通常ノード/leaf を更新し、実効 paint を所有マテリアルへ再正規化する。 |
togglePcbLayerHidden(stack, role) | 1 つの固定コンテナの永続化・undo 可能な表示状態を切り替える。 |
固定ルートには意図的に名前変更、削除、選択、並べ替えの操作がありません。 コンテナ間移動では null でない paint が移動先マテリアルへ正規化され、 拒否された挿入/移動は元のスタックを返します。
パスのジオメトリ
packages/ には、ベジェパスのヘルパーが置かれています。いずれも純粋な TypeScript で、ブラウザのランタイム依存はありません。
PathPointLike— アンカー(x、y)と、オプションの絶対座標ハンドルhin/hout(mm)。flattenSubpath(points, closed, segments = 24)— アンカーからアンカーへの各ベジェセグメントを走査し、ポリラインへとサンプリングする(DEFAULT_FLATTEN_SEGMENTS = 24、セグメントあたり 24 点)。これがブラウザ外でのヒットテストや bbox 計算を支えている。flattenPath(points, closed, extraSubpaths?)— プライマリのサブパスに加え、あらゆるextraSubpaths(常に閉じているものとして扱われる)をフラット化する。pathBbox(points, extraSubpaths?)— フラット化した曲線ではなく、生のアンカー+ハンドルに対する近似。安価で、選択の装飾や bbox 表示には十分だが、描画された曲線に対してピクセル単位で正確ではない。buildPath2D(...)— 実際のブラウザのPath2Dを構築する。アプリ/レンダリング用途専用で、Node ではグローバルのPath2Dがないためnullを返す。core 自身のヒットテスト/bbox ロジックとテストは、どこでも動作するflattenPathを代わりに用いる。translatePoints/translatePathLayer— サブパス(とそのハンドル)をdx/dyだけ移動する。movePathAnchor(points, index, x, y)— 1 つのアンカーを移動し、そのハンドルも一緒に移動させて、アンカーからのオフセットを保つ。movePathHandle(points, index, which, x, y, mirror)— 1 つのハンドルを移動する。mirror: trueの場合、反対側のハンドルをアンカーを軸に反射させ、曲線が滑らかなまま保たれる(一般的なベジェエディタの挙動)。
リサイズの制御
packages/ は、軸並行・8 ハンドルのリサイズ計算('n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw')を実装します。
function isResizable(rotation: number | undefined): boolean {
return !rotation;
}回転したレイヤーはステージ 1 ではリサイズできない
リサイズは回転していないレイヤーにのみ提供されます——回転した bbox のハンドルはその見た目上の辺と一致しないため、isResizable は、アプリの UI がリサイズハンドルを表示するかどうかを判定するための 1 行のガードです。
resizeRect(rect, handle, dx, dy, minSize = DEFAULT_MIN_SIZE_MM) は、ドラッグの差分をハンドルの軸に沿って適用します。矩形が minSize(DEFAULT_MIN_SIZE_MM = 1 mm)より小さくなったり反転したりしないようクランプされます。各ハンドルは反対側の辺を固定したまま動きます——例えば 'e' をドラッグすると西の辺が固定され、'w' をドラッグすると東の辺が固定されます。
グリッドスナップ
packages/ は、mm 座標をグリッドにスナップします。
const DEFAULT_SNAP_MM = 0.1;
function snapToGrid(value: number, gridMm: number = DEFAULT_SNAP_MM): number {
const snapped = Math.round(value / gridMm) * gridMm;
return Number(snapped.toFixed(6));
}デフォルトのグリッドは 0.1 mm です。固定小数桁の文字列(toFixed(6))を経由するラウンドトリップは意図的なもので、0.1 + 0.2 !== 0.3 のような浮動小数点のノイズが、スナップした mm 座標に漏れ込むのを防ぎます。snapPoint(pt, gridMm) は、点の x と y の両方に snapToGrid を適用します。