zudo-panel-designer docs
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

ジオメトリと編集の契約

bbox の計算、ヒットテストのルール、レイヤーリスト操作、パスのジオメトリ、リサイズの制御、グリッドスナップ。

bbox の計算

packages/core/src/bbox.ts には、他のジオメトリモジュール間で共有される矩形/bbox の計算処理が置かれています。ドキュメント空間の単位はミリメートルであり、回転は常に矩形自身の中心を軸とした時計回りの度数です——これは @zpd/core 全体で用いられているのと同じ規約です。

Function目的
rectCenter(rect)矩形の中心点。
rectCorners(rect)4 つの角を [Pt, Pt, Pt, Pt] として返す。
boundsOfPoints(points)点の集合を囲む軸並行の境界。
rotatedRectAABB(rect, rotationDeg)矩形の 4 つの角を自身の中心を軸に回転させ、その min/max を取った回転後のバウンディングボックス。rotationDeg0/undefined の場合は高速に何もしない no-op となる。結果は常に新しいコピーであり、入力の矩形を参照として返すことはない。
unionBbox(a, b)ab の両方を含む最小の矩形。
mergeBboxes(rects)リストに対して unionBbox を畳み込んだもの——選択範囲全体を囲む bbox。

ヒットテスト

packages/core/src/hit-test.ts は、すでに投影されたフラット 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.6TEXT_LINE_HEIGHT_FACTOR = 1.2)を用いる。クリックで選択するには十分だが、レイアウトの正確な根拠にはならない。
pathフラット化した塗り領域に対する even-odd(偶奇)の点内包判定(closed のときのみ)に加え、余裕を持たせたつかみ幅の下限(Math.max(layer.strokeWidth, 1.5) / 2MIN_STROKE_GRAB_MM = 1.5)でのストローク距離判定。これにより、細い線でもクリックしやすいままになる。
pattern移動可能な xysize 正方形への点内包判定。pattern のフォールバック段階でのみ考慮される。

パターンはアートワークのクリックを奪わない

pattern 正方形はキャンバスから選択できますが、視覚的に上にあっても pattern 以外のヒットが常に先です。大きな背景 pattern の下にある アートワークへ引き続きアクセスできます。

素の Node/Vitest にはブラウザの Path2D が存在しないため、パスのヒットテストは isPointInPath/isPointInStroke を使いません——ベジェをポリライン(折れ線)にフラット化し(下記参照)、独自に even-odd のレイキャスティングと点から線分への距離計算を行います。これにより @zpd/core は依存関係を持たず、ブラウザの外でもテスト可能なままになっています。

レイヤーリスト操作

packages/core/src/layer-ops.ts は投影 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/core/src/path-geometry.ts には、ベジェパスのヘルパーが置かれています。いずれも純粋な TypeScript で、ブラウザのランタイム依存はありません。

  • PathPointLike — アンカー(xy)と、オプションの絶対座標ハンドル 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/core/src/resize.ts は、軸並行・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) は、ドラッグの差分をハンドルの軸に沿って適用します。矩形が minSizeDEFAULT_MIN_SIZE_MM = 1 mm)より小さくなったり反転したりしないようクランプされます。各ハンドルは反対側の辺を固定したまま動きます——例えば 'e' をドラッグすると西の辺が固定され、'w' をドラッグすると東の辺が固定されます。

グリッドスナップ

packages/core/src/snap.ts は、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) は、点の xy の両方に snapToGrid を適用します。