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

検索したい単語を入力

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

インターフェース

エディタ(Editor.tsx)は、4 つの領域を持つ 1 枚のフルスクリーンシェルです。ヘッダー、左ツールバー、中央のキャンバスビューポート、右サイドバーで構成されます。シェルが保持するのは汎用的な状態だけです。ドキュメント(undo/redo 履歴を含む)、カメラ、現在の選択、そしてどのツールがアクティブか、という状態です。ドメイン固有の要素はすべて、拡張レジストリが公開するデータからレンダリングされます。そのためこのページでは、レジストリが登録済みになった状態での UI の挙動を説明します。

ヘッダー

components/header.tsx は最上部のバーです。左から右へ、次の要素が並びます。

  • アプリ名。

  • 保存状態チップ — ドキュメントがローカルに保存されているか、まだ未保存か、保存に失敗したかを知らせる小さなピル。

  • ズーム操作のまとまり。ズームアウト / ズームインボタン(25% 刻み)、現在のズーム率(物理的な mm ではなく「フィット」スケールに対する相対値)、そしてパネル全体が収まるようにカメラを再センタリング・再スケールする Fit ボタン。

  • undo / redo ボタン。undo/redo できる対象がないときは無効化されます。

  • 検索可能なキーボードショートカットオーバーレイを開く ? ボタン。

  • パネル設定 JSON ファイルをインポートする ⬆ JSON ボタン(ラウンドトリップ → インポートを参照)。

  • 現在のドキュメントを発注可能なパネル設定 JSON としてダウンロードする ⬇ JSON ボタン(後述の ダウンロードトリガー を参照)。

  • 現在のドキュメントのアートワークを Gerber ファイル一式の zip としてダウンロードする ⬇ Gerber ボタン。アートワークだけである旨の確認を必ず挟みます(後述の Gerber エクスポート を参照)。

  • 確認のうえでデフォルトのスターター文書にリセットする New panel ボタン(オートセーブと新規パネル → 新規パネルを参照)。

アプリ全体はコマンドレジストリによっても駆動されています。上記のショートカットはすべて、ヘッダーにボタンすら持たないもの(クリップボード、整列/分布、コマンドパレット自体)も含めて、同じ commands.ts の一覧を経由します。

ブラウザズームガード

browser-zoom-guard.ts は Editor のライフタイムを通じてインストールされ(useEffect(() => installBrowserZoomGuard(), []))、ブラウザ自身のネイティブなズームジェスチャーを抑止します。⌘/Ctrl + ホイール(ピンチズームは wheel イベントとして届きます)、⌘/Ctrl + +/=/-/0、そして Safari のトラックパッドイベント gesturestart/gesturechange/gestureend に対して preventDefault() します。修飾キーなしの素のホイールやキー入力はそのまま通されます——これはブラウザ自身のページズームだけをブロックするもので、エディタのキャンバス内ズーム(ヘッダーのボタンやキャンバス自身のホイールハンドラ)を妨げることはありません。

これが存在する理由は、ブラウザでズームされたビューポートは、アプリに伝えられるカーソル位置と実際の画面位置をずれさせてしまい、このドラッグの多いエディタのあらゆるドラッグハンドル・リサイズハンドル・クリックターゲットを狂わせてしまうためです。このガードの移植元であるリファレンスアプリとは異なり、zpd には代わりに刻むべき別個の表示スケール設定がありません——このガードは純粋な抑止だけで、代わりに何かをディスパッチすることはありません。

左ツールバー

components/toolbar.tsx は完全にデータ駆動です。ツールレジストリの各エントリを登録順に 1 つずつボタンとしてレンダリングし、続いて区切り線を挟んで 追加アクションレジストリの各エントリを 1 つずつボタンにします。ツールボタンをクリックすると ctx.setActiveTool(tool.id) が、追加アクションのボタンをクリックすると action.run(ctx) が呼ばれます。どちらのリストも手作業で管理するものではありません。tools/add-actions/ に新しいファイルを置くだけで、自動的にここへ現れます。組み込みのツール一式については ツール を参照してください。

キャンバスビューポートとルーラー

components/canvas-viewport.tsx は、単一の <canvas> を包む、純粋に表示だけを担うラッパーです。containerRefResizeObserver で計測し、リサイズ時にカメラが再フィットできるようにする)を公開し、ポインタイベントを Editor シェルへ転送します。<canvas> 自体には touch-action: none が指定されており、タッチデバイスでドラッグがスクロールやピンチのジェスチャーとして横取りされないようにしています。実際の描画はすべて Editor の再描画エフェクトで行われます。レンダリングとカメラ を参照してください。

ビューポートは、固定された ルーラーフレームcomponents/ruler.tsx)の内側に収まります。上辺には 20px のミリメートルルーラーの帯、左辺にはそれと対になる帯、そして両者が交わる左上には小さな mm のコーナーボックスが置かれます。これらの帯は、カメラがパンやズームをするたびに目盛りとラベルを描き直しますが、レイアウト上は決して動きません。動くのはキャンバスの内容だけで、帯自体はその下で静止しています。目盛りの計算については レンダリングとカメラ → mm ルーラー を参照してください。

右サイドバー

components/sidebar.tsx は、折りたたみ可能なカードパネル を縦に積み重ねたものです。上の 5 つはスクロールする内側の列を共有し(overflow-y-auto overscroll-contain なので、リストが下端まで来ても背後のキャンバスがスクロールすることはありません)、Help パネルはその下に、スクロールしないフッターとして固定されます。上のスタックがあふれても常に見えたままです。

各パネルは CollapsibleSectioncomponents/collapsible-section.tsx)です。枠線付きのカードで、そのヘッダーがボタンになっており、本体の開閉を切り替えます(ボタンに aria-expanded/ のインジケーター付き)。開閉の状態はセッション単位で、意図的に永続化されません。並べ替えやアニメーションもありません。

  • View — スタックの一番上に置かれた、表示専用の 2 つのチェックボックスです。Show content outside the panel(デフォルトは オン)は、パネルの端からはみ出したレイヤーの内容を低い不透明度でゴースト描画します。これにより、一部がパネル外にドラッグされたレイヤーも、選択ハンドルだけが何もないワークスペースの上に浮くのではなく、見えたまま・つかめたままになります。この減光には意味があります。パネルの端より外側の領域は製造時に物理的に切り落とされるため、「これは製造されない」ことを表します(レンダリングとカメラ → パネル外のゴーストパスを参照)。Show guides(デフォルトは オン)は、ルーラーガイドを表示し、ルーラーからのドラッグでガイドの作成・移動・削除ができるようにします(ガイドを参照)。どちらも純粋な表示状態であり、ドキュメントやエクスポートされる JSON を変更することはありません。

  • Panel — 実在する Eurorack の HP サイズ(@zpd/corePANEL_SIZES)を並べた <select>。各項目は {hp}HP — {widthMm}×{heightMm}mm とラベル付けされます。変更するとドキュメントに新しい panelHp がコミットされ、カメラが再フィットされます。

  • Layerscomponents/layer-list.tsx。上から Silkscreen(白)、Solder mask(黒)、Copper(gold/HASL)の 3 つの固定マテリアルセクションで構成されます。物理的・保存上の順序は逆の Copper → Solder mask → Silkscreen です。固定ヘッダーはセッション中だけ折りたため、永続化され undo 可能な表示/非表示を切り替えられますが、選択、名前変更、削除、ネスト、並べ替えはできません。内部の通常行は選択、名前変更、並べ替え/ドラッグ、表示/非表示、削除、グループ化が可能です。別のマテリアルセクションへ移動すると実効的な製造マテリアルが変わり、旧来のオブジェクトごとの色は互換用データであって編集可能な仕上げではありません。

  • Align & Distributecomponents/align-panel.tsx。6 つの整列ボタン、2 つの分布ボタン、そして selection/panel の基準トグルです。現在の選択が操作の最小レイヤー数を満たすまでは無効化されます。整列と分布を参照してください。

  • Propertiescomponents/inspector-host.tsx。選択中のレイヤーの型に登録された インスペクタ をレンダリングします。何も選択されていないときはプレースホルダーを表示します。カードのタイトルには、選択中のレイヤーの型も表示されます(例: Properties — shape)。

  • Help(フッター) — components/help-panel.tsx。サイドバーの一番下に固定され、初期状態では折りたたまれています。現在 アクティブな ツールを説明します。ツール名、キーボードショートカットのバッジ、そしてツールの description テキストです(ツール拡張アーキテクチャ を参照)。表示は activeToolId に追従するため、Space を押している間の一時的なパンのオーバーライドで内容が変わることはありません。

アプリ全体でのテキスト選択

エディタシェルのルート要素には select-none が指定されているため、クロムやキャンバスのどこをクリック&ドラッグしても、ネイティブのテキスト選択が始まることはありません。これにより、レイヤーの移動・パン・ペンパスの描画といったドラッグのジェスチャーが、操作の途中で UI のラベルを誤ってハイライトしてしまうのを防ぎます。編集用のフィールドは select-text で明示的に選択を有効化しているため(レイヤー名の変更用入力、インスペクタの数値/テキストフィールド、テキストインスペクタの <textarea>)、その中身は通常どおり選択・編集できます。

ダウンロードトリガー

ヘッダーの ⬇ JSON ボタンは download.tsxdownloadPanelConfig(ctx.doc) を呼び出します。この関数は次の処理を行います。

  1. @zpd/coreserializePanelConfig(製造側のリーダーが期待する正規の形)でドキュメントをシリアライズし、JSON.stringify します。この処理は DOM に依存しない純粋な panelConfigJson() ヘルパーとして切り出されており、実際の Blob やアンカーなしで出力文字列そのものをユニットテストできます。

  2. 文字列を Blob に包んでオブジェクト URL を作成し、一時的な <a download> のクリックによって zpd-panel-{panelHp}hp.json という名前でブラウザのダウンロードを発火させます。

  3. オブジェクト URL の破棄は即座にではなく、遅延した tick(setTimeout(..., 0))で行います。クリックと同じ tick で破棄すると、ブラウザによっては blob を読み終える前にダウンロードが中断されることがあるためです。

これはあくまでエディタ側のトリガーです。エクスポートされる JSON の形と、それが下流でどう利用されるかについては、エクスポートのセクションで解説します。

Gerber エクスポート

ヘッダーの ⬇ Gerber ボタン(title と aria-label はどちらも Download Gerber export (.zip))と、コマンドパレットの Download Gerber (.zip) コマンド(id は file-download-gerber。chord を持たないためパレットからしか到達できません——ショートカットとコマンドパレットを参照)は、どちらも download.tsxexportGerberZip(ctx.doc) という同じ入り口を呼びます。⬇ JSON がその場でファイルを書き出すのとは違い、こちらは必ず確認ダイアログを 1 枚挟んでから走る非同期のフローです。

Gerber パイプライン本体——IR ビルダー、ジオメトリ抽出器、ブーリアンカーネル、RS-274X ライター——は、静的 import ではなく遅延 import の裏に置かれています。このボタンはヘッダーにあり、アプリのエントリから到達可能です。静的に import すると、エクスポートを押したときにしか動かない機能のために、パイプライン全体が毎回のページロードでパースされることになってしまいます。確認ゲートに出す文言だけは依存を持たない専用モジュール(gerber/artwork-only-statement.ts)に切り出されているので、この遅延読み込みを待たずに表示できます。

  1. 確認ゲート — まだ何も生成しないうちに、確認ダイアログが開きます。

    Export Gerber (.zip) This export contains artwork only — copper, solder mask, silkscreen, and the board outline. It contains no drill file and no mounting-hole geometry. It is artwork for an already-specified Takazudo blank panel, not a standalone orderable board. Panel: <hp>HP, <幅> × <高さ> mm. [Cancel] [Export .zip]

    ダイアログの本文は、この zip に入るのが銅・ソルダーマスク・シルクスクリーン・基板外形の アートワークだけ であることを伝えます。ドリルファイルも取り付け穴のジオメトリも含まないため、すでに仕様の決まっている Takazudo のブランクパネル用のアートワークであって、単体で発注できる基板データではありません。文末には現在のパネル仕様が続きます。開いた時点でフォーカスが当たっているのは Cancel のほうで、ここでキャンセルするとファイルは 1 つも作られません。

  2. 成功Gerber export downloaded の成功トーストが出て、zpd-panel-<hp>hp-gerber.zip のダウンロードが始まります。zip の中身についてはラウンドトリップ → Gerber エクスポートはラウンドトリップしないを参照してください。

  3. 拒否 — ビルドが 1 つでも問題を見つけた場合は、後述の拒否ダイアログが開き、ファイルは作られません。

  4. パイプラインの例外 — 拒否コードでは表現できない失敗——path-boolopentype.js の遅延インポート、フォントの取得などが落ちるケース——は Gerber export failed のエラートーストになります。2 つの入り口はどちらも戻り値を待たない void exportGerberZip(...) の呼び出しなので、ここで catch しなければ、ダイアログもトーストも出ない本当にサイレントな失敗になってしまいます。「黙って失敗しない」という原則は、想定内の拒否だけでなく想定外のクラッシュにも同じように適用されています。

拒否ダイアログは選択を求めるものではなく、確認を求めるものです。 タイトルは Gerber export blocked — N issue(s) で、danger: true が指定されているため確認ボタンは赤で表示されます。

Gerber export blocked — 2 issues [Cancel] [OK]

本文には、見つかった理由がすべて 1 つのリストにまとめて並びます。各理由の下には、原因になったレイヤーの名前が入れ子で列挙されます。

  • This text layer uses a font with no local file to outline.

    • Label

  • A raster image cannot be manufactured on the panel. Trace it to vector layers, or hide it, before exporting.

    • Photo

unlisted-panel-hp のようなドキュメント単位の拒否には、原因になったレイヤーがないので名前が付きません。ボタンは CancelOK の 2 つですが、どちらを押してもダイアログを閉じる以外には何も起きません。拒否されたときの扱いは、エクスポータの拘束的なルールとして決まっているからです。拒否はエクスポートを中止し、ファイルを 1 つも生成しません。 問題のあるレイヤーを飛ばして残りだけ出力することも、コンソール警告で済ませて黙って続行することもありません。

拒否のきっかけになるコードは次のとおりです。ダイアログのメッセージ 列はダイアログがそのまま表示する文字列、補足 列はこのページ独自の説明です。

コードダイアログのメッセージ補足
unlisted-panel-hpThis panel HP has no entry in the blank-panel spec table, so its width is only an approximation — not an order-ready dimension.ドキュメント単位の拒否なので、レイヤー名は付きません。
image-layer-presentA raster image cannot be manufactured on the panel. Trace it to vector layers, or hide it, before exporting.
non-curated-fontThis text layer uses a font with no local file to outline.
missing-glyphThis text layer contains text the export cannot resolve to an outline in the resolved font subset.
unknown-pattern-idThis pattern layer names a generator that is not registered.
complexity-overrunThis design is too dense to export: the boolean pipeline would have to process more geometry than the export can handle.
unsupported-layer-typeNo geometry extractor is registered for this layer type, so its artwork cannot be exported.扱えないレイヤータイプを黙って落とす代わりに、拒否として表に出すために用意されています。組み込みのレイヤータイプにはいずれも抽出器があるため、通常の操作でこれに当たることはありません。
pattern-union-unreliableThe boolean union this export depends on is measured to corrupt this pattern generator — a #206/path-bool backend defect tracked in #218, not a caller bug. Refusing rather than shipping fabrication data already known to be wrong.レイヤー名は <レイヤー名> (pattern "<patternType>") の形で表示されます。追跡先は issue #218 です。

非表示のレイヤーは抽出そのものに到達しないため、決して拒否の原因になりません。 image-layer-present のメッセージが「ベクターレイヤーへトレースするか、非表示にする」の 2 択を案内しているのはこのためです。非表示にしたレイヤーはエクスポートから丸ごと外れます。

Note

このページ全体で参照しているドキュメントモデル、undo/redo 履歴、レイヤーの型(DocStateLayerPATTERN_GENERATORSserializePanelConfig など)は @zpd/core@zpd/patterns にあり、ドキュメントモデルのセクションで解説します。