| 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に一本化されている(2026-07修正)
キーボード削除(Delete/Backspace)はReact FlowのdeleteKeyCodeのみが処理し、レシピクリーンアップはuseGraphChangeHandlersのonEdgesChange(remove)で走る。useDeleteHandlerのhandleDeleteSelectedはツールバー/コンテキストメニュー専用(keydownリスナーは削除済み)。エッジ編集ダイアログ表示中はdeletionDisabledでdeleteKeyCodeが無効化される。削除経路を変更する場合はこの一本化を崩さないこと。
2. エッジ編集時のレシピ孤立(2026-07修正済み)
handleEdgeEditConfirmは全edgeTypeでエッジを更新し(recipe⇔dependency/visualの型変更可)、旧recipeRefsのうちどのエッジからも参照されなくなったレシピはcleanupOrphanedRecipesAfterEdgeUpdate(recipeCleanup.ts)で削除される。注意: このヘルパーは更新後エッジ自身(同一ID)を「使用中」集計に含める設計。removeRecipesFromJsonData(削除用、同一IDをスキップする)と混同して使うと、維持中のレシピまで削除するデータ破壊になる。
3. dependency全削除のstaleデータ(2026-07修正済み)
patchResearchColumnはprevResearchNodeGuidsをdependencyMap.get(guid) ?? []で常に上書きする(重複GUID・自己ループも除去済み)。ただしキャンバス上にノードが存在するレコードのみが再計算対象であり、ノードを削除したresearchレコードの旧依存は凍結されたまま残る(未解決・設計判断待ち。docs/node-graph-audit-handover-2026-07.md参照)。
4. dirty判定の精度(2026-07修正済み)
選択のみの変更(type: 'select')とマウント時のdimensions計測(resizing/setAttributesなし)はdirtyにしない。position/remove/add/ユーザーリサイズは従来どおりdirty。SET_NODES/SET_EDGESアクションはmarkDirty?: booleanを持つ。
5. 保存パイプラインの順序依存
exportToMaster()内の処理順序は重要:
validateGraph() — 孤立エッジ除去。必ず最初。 これがないと存在しないノードへのエッジが依存関係マップに入る
calculateUnlockedItems() — 空間位置からresearch→item割当
buildResearchDependencyMap() — cleanedEdgesから依存関係構築
patchResearchColumn() — research masterデータ書き込み
buildNodeGraphFile() — グラフファイルシリアライズ
validateGraphを後に回すと、孤立エッジが依存関係データを汚染する。
6. 保存失敗時の状態不整合(2026-07修正済み)
useNodeExportはonRequestSave()の成功後にsetJsonDataとdirtyクリアを行い、isSavingRefで多重保存を防ぐ。この順序を崩すと保存失敗時に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はカウンター併用の
generateEdgeId()(useNodeOperations.ts)で生成する(2026-07修正済み。Date.now()単独のID生成を新規コードで復活させないこと)
- 壊れたnodeGraph.v1.json対策として、
validateAndMigrateがitem/block/researchノードのmasterGuidを検証し、不正ノードとそれに接続するエッジをロード時に除外する(2026-07追加)
パフォーマンス
resolveDisplayNamesはuseMemo([state.nodes, ...])で呼ばれるが、ノードドラッグでstate.nodesの参照が変わるため、ドラッグ中に毎フレーム全ノードの名前解決が走る。内部はO(N*M)のlinear scan
詳細リファレンス