| name | node-graph-reference |
| description | node-graph(ノードエディタ)関連のシステム開発、不具合修正、機能追加を行う際の必須リファレンス。nodeEditor/ディレクトリ配下のコードを変更・追加・デバッグする場合、ノードやエッジの新規タイプ追加、レシピエッジの修正、保存/読み込み処理の変更、PropertiesPanel関連の修正、コンテキストメニューやツールバーの変更時に使用する。「ノードグラフ」「ノードエディタ」「nodeEditor」「エッジ」「レシピエッジ」「ノード追加」に関連するタスクで自動トリガーする。 |
Node Graph Reference
設計思想
三つのデータソースと「保存時全再構築」
このシステムには3つの独立したデータソースがある:
- React Flow状態(
NodeEditorContextのuseReducer) — キャンバス上のノード位置・エッジ接続のランタイム表現
- nodeGraph.v1.json — グラフの永続化ファイル。ノードのID・位置・masterGuid、エッジのID・接続・edgeTypeのみ保存
- マスタデータ(
jsonData: Column[]) — items.json, blocks.json等。App.tsxが所有。ノードエディタはprops経由で参照・変更
核心原則: 保存時に全て再構築する。 research依存関係、clearedActions、spatialUnlockの結果は全てexportToMaster()内で毎回ゼロから計算される。中間状態をキャッシュしない。これにより、保存すれば必ず整合性のとれたデータが得られる。
表示名は永続化しない
ノードの表示名(displayName)やエッジのレシピラベルはnodeGraph.v1.jsonに保存しない。毎レンダリングでresolveDisplayNames()とresolveEdgeRecipeLabels()がjsonDataから解決する。マスタデータ側で名前が変わっても、グラフは常に最新の名前を表示する。
スキーマ構造のハードコード禁止
最重要ルール。 スキーマのフィールド名(itemName, blockGuid等)を直接参照してはならない。全てSchemaMetaを介して動的に解決する。
const name = record.itemName;
const name = meta.nameField ? record[meta.nameField] : null;
既知の違反箇所: spatialUnlock.tsのgetItemGuidForNodeがrecord?.itemGuidをハードコードしている。新しいコードでこのパターンを踏襲しない。
レシピはエッジが所有する
レシピレコード(craftRecipes, machineRecipes)のライフサイクルはエッジが管理する。エッジ作成時にレシピを生成、エッジ削除時に他のエッジから参照されていなければレシピも削除(recipeCleanup.ts)。
変更時に必ず意識すること
1. 削除は二重に走る
React FlowのdeleteKeyCode="Delete"とカスタムuseDeleteHandlerが両方同じキー押下を処理する。React FlowはonEdgesChange(remove)を発火し、useDeleteHandlerはdeleteSelected()を呼ぶ。レシピクリーンアップも二箇所で走る。setJsonDataのfunctional updaterにより実害はないが、この二重構造を理解せずに片方だけ変更すると不整合が生じる。
2. エッジ編集時のレシピ孤立
handleEdgeEditConfirmはエッジのrecipeRefsを新しい参照で置換するが、古いレシピレコードのクリーンアップは行わない。エッジ削除時のみクリーンアップが走る。エッジ編集で参照を差し替えると、旧レシピがマスタデータに残る可能性がある。
3. dependency全削除でstaleデータが残る
patchResearchColumnはif (dependencies)で分岐しており、researchノードから全dependencyエッジを削除するとdependencyMap.get(guid)がundefinedになり、古いprevResearchNodeGuidsがクリアされない。
4. onMarkDirtyの過剰発火
onNodesChangeはノードの選択やドラッグでもonMarkDirty()を呼ぶ。つまりノードをクリックしただけでApp側が「未保存変更あり」と認識する。レイアウト変更とデータ変更を区別していない。
5. 保存パイプラインの順序依存
exportToMaster()内の処理順序は重要:
validateGraph() — 孤立エッジ除去。必ず最初。 これがないと存在しないノードへのエッジが依存関係マップに入る
calculateUnlockedItems() — 空間位置からresearch→item割当
buildResearchDependencyMap() — cleanedEdgesから依存関係構築
patchResearchColumn() — research masterデータ書き込み
buildNodeGraphFile() — グラフファイルシリアライズ
validateGraphを後に回すと、孤立エッジが依存関係データを汚染する。
6. 保存失敗時の状態不整合
useNodeExportでsetJsonData(updatedColumns)がawait onRequestSave()の前に呼ばれる。保存が失敗してもReact状態は既に更新済みで、ディスクのデータとReact状態が乖離する。
落とし穴集
SchemaMeta
- ルートに複数のarray propertyがあるスキーマでは、最初のarrayが選ばれる。意図しないarrayがdataArrayPathになる可能性
guidFieldはtype: "uuid" && autoGenerated: trueのフィールドのみ検出。手動入力UUIDスキーマではnullになり、レコード検索が全て失敗する
nameFieldは"name"を含むstringフィールドを優先、なければ最初のstringフィールド。"title"等は検出されない場合がある
空間アンロック
- item/blockがresearchノードと完全に同じ座標にある場合、どのゾーンにも属さず割当されない
- 等距離に複数researchがある場合、nodes配列の順序(追加順)で決まる。視覚的に近いノードが選ばれるとは限らない
getItemGuidForNodeがrecord?.itemGuidをハードコードしている(前述の違反)
React Flow統合
nodeTypesとedgeTypesはNodeCanvas.tsxでモジュールスコープ定数として定義。インラインで定義すると無限レンダリングになる。新しいノード/エッジタイプ追加時もこのパターンに従う
defaultViewportは初回マウント時のみ適用。コンポーネントがマウント済みの状態でLOAD_GRAPHしても、React Flowのビューポートは更新されない
- エッジIDに
Date.now()を使用。同一ミリ秒で2つのエッジ作成するとID衝突する(ノードはカウンター付きで安全)
パフォーマンス
resolveDisplayNamesはuseMemo([state.nodes, ...])で呼ばれるが、ノードドラッグでstate.nodesの参照が変わるため、ドラッグ中に毎フレーム全ノードの名前解決が走る。内部はO(N*M)のlinear scan
詳細リファレンス