本書は、SystemVerilog のソースコードを入力として、モジュール間の接続関係を視覚的に表示・編集できる Web アプリケーション(以下、本アプリ)の開発仕様を定義する。
- SystemVerilog の大規模設計では、モジュール階層と信号接続が複雑化しやすく、コードのみでは全体像の把握が困難
- 既存の商用 EDA ツール(Vivado RTL Viewer、Sigasi 等)は高機能だが、ライセンスや導入コストの制約がある
- 軽量・OSS のみで構成され、Web ブラウザだけで動作するビューアが求められる
- SystemVerilog ファイル(複数)を解析し、モジュール定義・インスタンス・ポート接続を抽出する
- モジュールを矩形、ポートを入出力端子、接続を線として描画する
- 接続線がノードと重ならないよう、直角配線で自動的にノードを迂回する
- ユーザーがノードをドラッグして配置を整えられる(編集機能)。ドロップ時は接続線のみが自動再ルーティングされる
- レイアウト・配置情報を JSON で保存・復元できる
- §6.5 に定義する「レイアウト・ルーティング要求仕様」(R-A1〜R-A12、R-A14〜R-A25、F-17)を遵守する
- 論理合成や回路図(ゲートレベル)生成は対象外
- シミュレーション・タイミング解析は対象外
- UVM コンポーネントの構造表現・class ベースの動的構造は対象外(将来検討)
- SystemVerilog の
program/checkerブロックの構造表現は対象外 virtual interface経由の動的接続(class 内での参照)は対象外。本ツールは静的なinterfaceインスタンス接続のみ扱う
補足:
interface/modportの 静的な解析と描画 は v0.9 で正式対応した(§4.1 のデータモデル、§5.2 のパース手順、§6.5.4 R-A10/R-A11 を参照)。本仕様で対象外とするのは UVM や virtual interface 等の動的構造に限定される。
┌─────────────────────────────────────────────────────────────┐
│ ブラウザ (Frontend) │
│ ┌────────────────────┐ ┌────────────────────────────┐ │
│ │ ファイル選択 UI │ │ ダイアグラム表示 (JointJS) │ │
│ │ (input[type=file])│ │ - ELK.js でレイアウト+経路 │ │
│ └─────────┬──────────┘ │ - ポート付きノード │ │
│ │ │ - ドラッグ編集 │ │
│ ↓ └────────────────────────────┘ │
│ ┌────────────────────┐ ↑ │
│ │ JSON エクスポート/ │ │ │
│ │ インポート │ │ │
│ └────────────────────┘ │ │
└─────────────────────────────────────────┼───────────────────┘
│ (JSON: graph model)
│
┌─────────────────────────────────────────┼───────────────────┐
│ サーバ (Backend, Python/Node) │
│ ┌────────────────────┐ ┌────────────────────────────┐ │
│ │ ファイル受信 API │ → │ Verible 呼び出し │ │
│ │ (POST /parse) │ │ (verible-verilog-syntax │ │
│ └────────────────────┘ │ --export_json) │ │
│ └────────────┬───────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ CST → 中間モデル変換 │ │
│ │ (modules, ports, insts) │ │
│ └────────────┬───────────────┘ │
│ ↓ │
│ ┌────────────────────────────┐ │
│ │ JointJS 用 JSON 整形 │ │
│ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| レイヤ | 技術 | 用途 |
|---|---|---|
| SystemVerilog 解析 | Verible (verible-verilog-syntax --export_json) |
CST 抽出 |
| バックエンド | Python 3.11+ / FastAPI | API サーバ、Verible 呼び出し、CST 解析 |
| Verible Python ラッパー | verible_verilog_syntax.py(Verible 同梱) |
CST → Python オブジェクト変換 |
| フロントエンド | 素の TypeScript + Vite | フレームワーク非依存 |
| ダイアグラム描画 | JointJS Core (@joint/core, MPL-2.0) |
ノード・リンク・自動回避配線 |
| パッケージ管理 | uv / pnpm | Python / Node |
- 本アプリのソースコードは MIT または Apache-2.0 で公開
- 依存ライブラリの主要ライセンスは以下を許容
- Verible: Apache-2.0
- JointJS Core: MPL-2.0(改変時は当該ファイルのみ MPL のまま再頒布)
- FastAPI / Vite / TypeScript: MIT
| ID | 機能 | 優先度 |
|---|---|---|
| F-01 | SystemVerilog ファイルのアップロード(複数) | 必須 |
| F-02 | Verible によるパース、エラー表示 | 必須 |
| F-03 | モジュール・ポート・インスタンスの抽出 | 必須 |
| F-04 | トップモジュールの選択 | 必須 |
| F-05 | モジュール矩形の描画(インスタンス名 + モジュール名) | 必須 |
| F-06 | 入出力ポートの描画(左 = input、右 = output) | 必須 |
| F-07 | ポート間接続線の描画(ELK.js の orthogonal 経路を vertices として注入、線重複を抑制) | 必須 |
| F-08 | ノードのドラッグ移動 | 必須 |
| F-09 | キャンバスのパン・ズーム | 必須 |
| F-10 | レイアウトの JSON 保存・復元 | 必須 |
| F-11 | 信号名のラベル表示 | 必須 |
| F-12 | inout ポートの表現(中央配置 or 別色) |
任意 |
| F-13 | バス幅の表示([7:0] 等) |
任意 |
| F-14 | SVG / PNG エクスポート | 任意 |
| F-15 | 階層展開(インスタンス内部を別ビューで表示) | 任意 |
| F-16 | 自動初期レイアウト(ELK.js による)(§6.5 R-A6〜R-A12 を満たす) | 必須 |
| F-17 | ノードドロップ時の接続線のみの自動再ルーティング(§6.5 R-A5) ※手動「再ルーティング」ボタンは設けない |
必須 |
| F-18 | interface 宣言の解析と内部信号・modport 情報の抽出(§4.1 InterfaceDef) |
必須 |
| F-19 | interface 型ポート(axi_if.master my_bus)の認識と、interface インスタンス経由の接続解析 |
必須 |
| F-20 | 同一 interface インスタンスに属する信号群を 1 本のトランクリンクとして描画(色・太さで識別、ラベルに interface 名 + modport)。展開して個別信号表示への切替可能 | 必須 |
- ユーザーがブラウザで本アプリを開く
- 「ファイルを選択」ボタンから
.sv/.vファイルを 1〜複数選択 - 「解析」ボタンをクリック → サーバが Verible でパース
- 解析成功時、トップモジュール候補がドロップダウンに表示される
- ユーザーがトップモジュールを選択 → ダイアグラムが描画される
- ユーザーは必要に応じてノードをドラッグして配置を整える
- 「保存」ボタンでレイアウト JSON をダウンロード
- 次回以降「読み込み」ボタンで JSON を読み込み、レイアウト復元
Verible CST から抽出する中間表現を以下とする。
from dataclasses import dataclass, field
from typing import Literal
@dataclass
class Port:
name: str
# interface 型ポートの場合は "interface" を取りうる
direction: Literal["input", "output", "inout", "interface"]
width: int = 1 # ビット幅([7:0] なら 8)
msb: int | None = None # 例: 7
lsb: int | None = None # 例: 0
# interface 型ポート専用: 接続先 interface 名と modport 名
# 例: `axi_if.master my_bus` → interface_name='axi_if', modport='master'
interface_name: str | None = None
modport: str | None = None
@dataclass
class ModportSignal:
"""interface 内 modport で再定義された信号方向"""
name: str
direction: Literal["input", "output", "inout"]
@dataclass
class Modport:
"""interface 内の modport 定義"""
name: str # 例: "master" / "slave"
signals: list[ModportSignal]
@dataclass
class InterfaceSignal:
"""interface 本体で宣言される信号(modport で方向が決まる)"""
name: str
width: int = 1
msb: int | None = None
lsb: int | None = None
@dataclass
class InterfaceDef:
"""SystemVerilog interface 宣言"""
name: str # interface 名 例: "axi_if"
signals: list[InterfaceSignal] # interface 内信号
modports: list[Modport] = field(default_factory=list)
parameters: dict[str, str] = field(default_factory=dict)
@dataclass
class ModuleDef:
name: str # モジュール名
ports: list[Port]
parameters: dict[str, str]
@dataclass
class PortConnection:
port_name: str # 接続先モジュールのポート名
signal: str # 接続される信号名(または式)
# interface 接続の場合: 接続先 interface インスタンス名と modport
# 例: `.bus(axi_bus.master)` → interface_instance='axi_bus', modport='master'
interface_instance: str | None = None
modport: str | None = None
@dataclass
class InterfaceInstance:
"""親モジュール内での interface のインスタンス宣言
例: `axi_if my_bus();` → instance_name='my_bus', interface_name='axi_if'
"""
instance_name: str
interface_name: str
parameters: dict[str, str] = field(default_factory=dict)
@dataclass
class ModuleInstance:
instance_name: str # 例: "u_cpu"
module_name: str # 例: "cpu_top"
parameters: dict[str, str]
connections: list[PortConnection]
@dataclass
class ParsedDesign:
modules: dict[str, ModuleDef] # モジュール名 → 定義
interfaces: dict[str, InterfaceDef] # interface 名 → 定義
top_candidates: list[str] # トップモジュール候補
instances_by_parent: dict[str, list[ModuleInstance]]
# 親モジュール名 → インスタンス一覧
interface_instances_by_parent: dict[str, list[InterfaceInstance]]
# 親モジュール名 → interface インスタンス一覧サーバ → フロントエンドへ返す JSON のスキーマ。
{
"status": "ok",
"errors": [],
"design": {
"modules": {
"cpu_top": {
"name": "cpu_top",
"ports": [
{ "name": "clk", "direction": "input", "width": 1 },
{ "name": "rst_n", "direction": "input", "width": 1 },
{ "name": "data_o", "direction": "output", "width": 32, "msb": 31, "lsb": 0 }
],
"parameters": { "WIDTH": "32" }
}
},
"top_candidates": ["cpu_top"],
"instances_by_parent": {
"cpu_top": [
{
"instance_name": "u_alu",
"module_name": "alu",
"parameters": {},
"connections": [
{ "port_name": "a", "signal": "reg_a" },
{ "port_name": "b", "signal": "reg_b" },
{ "port_name": "y", "signal": "alu_out" }
]
}
]
}
}
}JointJS の graph.toJSON() 形式をそのまま採用する。アプリ独自のメタ情報は attrs._meta に格納する。
{
"version": "1.0",
"topModule": "cpu_top",
"graph": {
"cells": [
{
"type": "standard.Rectangle",
"id": "u_alu",
"position": { "x": 100, "y": 100 },
"size": { "width": 180, "height": 120 },
"attrs": {
"label": { "text": "u_alu\n(alu)" },
"_meta": { "moduleName": "alu", "instanceName": "u_alu" }
},
"ports": { "groups": { /* ... */ }, "items": [ /* ... */ ] }
},
{
"type": "standard.Link",
"source": { "id": "u_alu", "port": "port_y" },
"target": { "id": "u_reg", "port": "port_d" },
"router": { "name": "normal" },
"connector": { "name": "rounded" },
"vertices": [
{ "x": 320, "y": 150 },
{ "x": 320, "y": 280 },
{ "x": 480, "y": 280 }
],
"labels": [{ "attrs": { "text": { "text": "alu_out[31:0]" } } }]
}
]
}
}| メソッド | パス | 用途 |
|---|---|---|
POST |
/api/parse |
SV ファイルを受け取り、解析結果 JSON を返す |
GET |
/api/health |
ヘルスチェック |
リクエスト: multipart/form-data
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
files |
File[] | ○ | .sv / .v ファイル群 |
top |
string | × | トップモジュール名のヒント(指定なければ自動推定) |
レスポンス: application/json(セクション 4.2 のスキーマ)
エラー応答:
{
"status": "error",
"errors": [
{ "file": "cpu.sv", "line": 42, "column": 8, "message": "syntax error near 'endmodule'" }
]
}verible-verilog-syntax --export_json --printtree path/to/file.sv--export_json フラグを使用することで、CST を JSON 形式で取得できる。Verible には Python ラッパー verible_verilog_syntax.py が同梱されており、これを利用して CST を Python オブジェクトとして扱う。
Verible CST のノードタグから以下を抽出する。
| 抽出対象 | CST タグ |
|---|---|
| モジュール定義 | kModuleDeclaration |
| モジュール名 | kModuleHeader 直下の SymbolIdentifier |
| ポート宣言(ANSI スタイル) | kPortDeclaration 配下の kPortDirection + SymbolIdentifier + kPackedDimensions |
| interface 型ポート宣言 | kPortDeclaration で kPortDirection が無く、型部が kInterfacePortHeader(または interface_name.modport_name 形式の kHierarchyIdentifier)である場合 |
| インスタンス | kModuleInstantiation / kGateInstantiation |
| インスタンス名 | kGateInstance 直下の SymbolIdentifier |
| 名前付き接続 | kActualNamedPort (.port_name(signal) 形式) |
| 順序接続 | kActualPositionalPort |
| interface 接続式 | kActualNamedPort の引数が kHierarchyExtension(bus_inst.modport_name 形式) |
| interface 宣言 | kInterfaceDeclaration(F-18) |
| interface 名 | kInterfaceHeader 直下の SymbolIdentifier |
| interface 内信号宣言 | kInterfaceDeclaration 配下の kNetDeclaration / kDataDeclaration |
| modport 宣言 | kModportDeclaration |
| modport 名 | kModportDeclaration 直下の SymbolIdentifier |
| modport 内方向付き信号 | kModportSimplePortsDeclaration(input / output / inout キーワード + 信号名一覧) |
| interface インスタンス宣言 | モジュール本体の kDataDeclaration のうち、型部分が既知の interface 名と一致するもの。例: axi_if my_bus(); |
トップモジュール候補は「他のモジュールからインスタンス化されていないモジュール」とする。interface はトップ候補から除外する。
- 第1パス: ファイル全体を走査し、すべての
kInterfaceDeclarationをInterfaceDefとして収集。interfaces辞書に登録 - 第2パス: 各モジュールを走査
- 通常ポートを抽出
- 型部が既知 interface 名と一致するポートは
Port.direction = "interface"+interface_name+modportを設定 - モジュール本体内の
kDataDeclarationのうち、型が既知 interface 名のものはInterfaceInstanceとしてinterface_instances_by_parentに登録
- 第3パス: モジュールインスタンスの接続式を解析
- 接続式が
bus_inst.master形式なら、PortConnection.interface_instance = "bus_inst"、PortConnection.modport = "master"を設定 - 通常信号(単純識別子)は従来通り
PortConnection.signalのみ設定
- 接続式が
kInterfacePortHeaderが CST 上で明示的に取れない Verible バージョンの場合、型部の文字列に.が含まれかつ既知 interface 名と前方一致するものを interface 型ポートとみなすフォールバック実装を許容する- modport が未指定の interface 型ポート(
axi_if my_bus形式)はPort.modport = Noneとし、描画時は方向不明扱い(両方向の太線)とする
- パースエラー時は HTTP 200 で
status: "error"を返す(HTTP 4xx は使わない) - ファイル形式不正(
.sv/.v以外)は HTTP 400 - Verible 未インストール時は HTTP 500 で
{ "error": "verible not found" }
┌──────────────────────────────────────────────────────────┐
│ ヘッダ: アプリ名 [ファイル選択] [解析] [保存] [読込] │
├──────┬───────────────────────────────────────────────────┤
│ │ │
│ 左 │ ダイアグラム表示エリア │
│ ペイン│ (JointJS Paper) │
│ │ │
│ - モ │ │
│ ジュ │ │
│ ール │ │
│ 一覧 │ │
│ - ト │ │
│ ップ │ │
│ 選択 │ │
│ │ │
├──────┴───────────────────────────────────────────────────┤
│ ステータスバー: パース結果 / エラー / 選択中ノード情報 │
└──────────────────────────────────────────────────────────┘
- 形状: 角丸矩形(
standard.Rectangle) - サイズ: 幅 =
max(180, ポート最大ラベル幅 + 80)、高さ =max(80, max(入力数, 出力数) * 25 + 40) - 色: 種別ごとに固定パレット(後述)
- ラベル: 上段にインスタンス名、下段にモジュール名(小さく)
- 例:
┌────────────────┐ │ u_alu │ ← インスタンス名(大) │ (alu) │ ← モジュール名(小) │ ●a y● │ │ ●b │ └────────────────┘
input: 左辺に配置、青色(#8ECAE6)、magnet: 'passive'(リンク終点になれる)output: 右辺に配置、橙色(#FFB703)、magnet: true(リンク始点になれる)inout: 左辺または右辺(R-A3 により上下辺は不可)に配置、緑色(#95D5B2)。原則として、信号の駆動方向が読み取れない場合は左辺扱いとする- ポートサイズ: 半径 6px の円
- ラベル: ポート円の内側方向にテキスト、フォントサイズ 11px
- ルーティング: ELK.js の orthogonal エッジルーティング結果を JointJS の
verticesとして注入する方式(§6.5.4 参照)。これにより、各リンクの経路はレイアウト計算段階で他リンクとの重複を考慮した形で決定される - コネクタ:
rounded(角丸) - 線の太さ: バス幅で変える
- 1 bit: 1.5px
- 2〜8 bit: 2.5px
- 9 bit 以上: 3.5px
- 色:
- 通常:
#444 - クロック信号(
clk,clockを含む名前):#E63946 - リセット信号(
rst,resetを含む名前):#F4A261
- 通常:
- 中央にラベル: 信号名 + ビット幅(例:
data[31:0])
リンクのデフォルトは「ルーターを使わず、vertices をそのまま頂点列として描画」する設定とする。vertices の中身は ELK.js が計算した orthogonal 経路を §6.5.4 の applyElkRoutingToLinks() で注入する。
const paper = new joint.dia.Paper({
// ...
defaultRouter: { name: 'normal' }, // ELK の vertices をそのまま使う
defaultConnector: { name: 'rounded', args: { radius: 8 } },
defaultLink: () => new joint.shapes.standard.Link()
});補足: JointJS の
'normal'ルーターは「指定された頂点列を順につなぐだけ」の最も単純なルーターで、独自の経路探索は行わない。これにより ELK が決めた経路をそのまま使える。
ユーザーが新規にリンクを引いた際の検証ルール。
- 出力 → 入力 のみ許可
- 同一モジュール内の自己ループは禁止
- ポート以外の場所への接続禁止(
linkPinning: false)
validateConnection: (srcView, srcMag, tgtView, tgtMag, end, linkView) => {
if (srcView === tgtView) return false;
if (!srcMag || !tgtMag) return false;
const srcGroup = srcMag.getAttribute('port-group');
const tgtGroup = tgtMag.getAttribute('port-group');
return srcGroup === 'out' && tgtGroup === 'in';
}| 操作 | 効果 |
|---|---|
| ノードをドラッグ | ノードの移動。接続線は自動再ルーティング |
| キャンバスをドラッグ(空きスペース) | パン |
| マウスホイール | ズームイン・アウト |
| ノードをクリック | 選択状態(青枠ハイライト) |
| 出力ポートからドラッグ | 新規リンクの作成(編集モード時のみ) |
| リンク上で右クリック | コンテキストメニュー(削除のみ。Reroute 追加は F-17 により提供しない) |
Ctrl+S |
レイアウト JSON 保存 |
Ctrl+O |
レイアウト JSON 読み込み |
初期レイアウトおよび再レイアウト時に遵守すべき要求仕様を 5 つの観点で定義する。これらは F-16(自動初期レイアウト)および F-17(自動再ルーティング)を実現するための必須要件である。詳細は .aiprj/AI_PRJ_REQUIREMENTS.md §2 / §7 を参照。
| ID | 要求 |
|---|---|
| R-A5 | ノードを移動した時は 接続線のみを再レイアウト する。ノード位置は再計算しない:ドラッグされたノードはユーザーがドロップした位置にそのまま留まり、兄弟ノード・無関係なノードも位置を保つ。親コンテナのサイズのみ R-A4 / R-A21 により再計算される(左上は不変)。接続線の再ルーティングは R-A22 によりグラフ全体に対して行われる(差分再ルーティングではなく、全リンクを ELK で一括再計算)。 |
実装方針(JointJS):
element:pointerupイベントをハンドルし、ドロップノードに対して以下を順に実行するresizeContainerToFit()で親コンテナのサイズを子の bbox にフィット(R-A4, R-A21)rerouteAll()で グラフ全体のリンクを ELK 経由で再ルーティング(R-A22)
- 他のノードの
positionには触れない(子の位置は維持) - 50ms デバウンスを入れて連続ドラッグ時の計算負荷を抑える
- 詳細フローは §6.5.7 を参照
| ID | 要求 |
|---|---|
| F-17 | 手動の「再ルーティング」ボタンは設けない。再ルーティングはドロップ時に自動で行われ(R-A5)、専用ボタンは冗長のため廃止。 |
→ ツールバー仕様(§6.1)からは「再ルーティング」ボタンを除外する。
| ID | 要求 |
|---|---|
| R-A8 | 接続線が混雑する場合は、最初からノード間に十分な距離を確保する。混雑度(edges / nodes)に応じて ELK の spacing 系オプションを 最大 2.0 倍 までスケールする。 |
| R-A12 | 混雑していない設計でもノードの左右方向に十分な余裕を確保する。elk.layered.spacing.nodeNodeBetweenLayers の基準値を 120 → 180 へ引き上げ、R-A8 の倍率と 加算的に作用 させる。 |
spacing スケール計算式:
// R-A8 + R-A12 を加算的に適用
function computeSpacingMultiplier(edgeCount: number, nodeCount: number): number {
const density = nodeCount > 0 ? edgeCount / nodeCount : 0;
// 混雑度 0 で 1.0、混雑度 3.0 で 2.0 にクランプ
const congestionMultiplier = Math.min(1.0 + density / 3.0, 2.0);
return congestionMultiplier;
}
const baseLayerSpacing = 180; // R-A12: 旧 120 → 180
const baseNodeSpacing = 80;
const mult = computeSpacingMultiplier(edges.length, nodes.length);
const layoutOptions = {
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT',
'elk.spacing.nodeNode': String(baseNodeSpacing * mult),
'elk.layered.spacing.nodeNodeBetweenLayers': String(baseLayerSpacing * mult),
'elk.spacing.edgeNode': String(40 * mult),
'elk.spacing.edgeEdge': String(20 * mult)
};| ID | 要求 |
|---|---|
| R-A1 | 線同士の重複を禁止する。特に 縦線同士の重複は厳禁。同じ x 座標で複数のリンクが垂直走行している状態を発生させない。ELK の elk.spacing.edgeEdge および elk.layered.spacing.edgeEdgeBetweenLayers を十分大きく確保し、ELK の OrthogonalEdgeRouter に全エッジ同時計算で重複を抑制させる。手動 vertices 編集で重なりを残してはならない。 |
| R-A2 | 線はノードに重ならず、ノードの上を通過せず、必ず迂回 する。ソース・ターゲット以外のノードを完全に避けてルーティングする。トップ I/O ノード(io_*)もノードとして障害物扱いし、リンクが横切ることを禁止する。 |
| R-A3 | 線は必ず 左右方向でポートに接続 する。入出力ポートは WEST(左辺)または EAST(右辺)のみ に配置し、NORTH(上辺)・SOUTH(下辺)には絶対に配置しない。ELK では各ポートに elk.port.side: 'WEST' | 'EAST' を明示し、親ノードに elk.portConstraints: 'FIXED_SIDE' を設定して強制する。 |
| R-A4 | コンテナ内の子ノードを移動した場合、親コンテナは「現在の左上座標を原点として、子ノード群の bounding box にフィットするようリサイズ」する。左上は不変で、幅・高さは子ノード群の最右端・最下端 + パディングに合わせて 拡張・縮小の両方を行う。階層は再帰的に適用される。 |
| R-A9 | 上記の禁止事項を実際に満たすよう、ルーティング実装を継続的に最適化する(メタ要求)。違反ケースが見つかった場合は実装を更新する。 |
| R-A14 | 1-to-N のファンアウトネット(クロック・リセット等、1 ソース → N ターゲットの構造)では、ターゲット側で N 本の独立した垂直経路を発生させない。ELK の elk.hierarchyHandling: 'INCLUDE_CHILDREN' および elk.edgeRouting: 'ORTHOGONAL' に依存し、可能な限りトランク状の共通経路にまとめる。やむを得ず N 本の経路が必要な場合も、同一 x 座標に集中させない(R-A1 と整合)。 |
| R-A20 | 子ノードへのファンアウトの起点となるポート位置を、親ノード上で 複数本のリンクが集中しない位置に分散させる。例えば親の port_RST_N と port_CLK を縦に並べると、そこから出るリンクが同じ x で縦走行して R-A1 違反になる。ELK の port.index を活用し、ファンアウトの多いポートを別の辺に配置するか、辺の中で十分な間隔を空ける。 |
| R-A21 | ノードを移動(ドラッグ&ドロップ)した時、ドロップ時(pointerup)に親コンテナのサイズを子ノード群の bounding box にフィットさせて自動再計算する。ドラッグ中(pointermove)は親サイズを変更せず、リアルタイム描画コストを抑える。ドロップ直後に親サイズと位置を確定する。親コンテナがネストしている場合は内側から外側へ再帰的にフィットを伝播する。 |
| R-A22 | ノードを移動(ドラッグ&ドロップ)した時、ドロップ時に全体のリンクを再ルーティングする。移動ノードに接続するリンクだけでなく、グラフ内すべてのリンクを ELK の INCLUDE_CHILDREN + ORTHOGONAL で再計算し、applyElkRoutingToLinks() で vertices を全更新する。これにより、移動による波及効果(他のリンクが横切ることになる、混雑が変わる等)も解消される。 |
| R-A23 | interface 接続のトランク化(F-20): 同一 InterfaceInstance を介して接続される複数の信号(req / ack / data / valid 等)は、ダイアグラム上で 1 本のトランクリンクとして描画する。両端は「マスタモジュールの interface 型ポート」と「スレーブモジュールの interface 型ポート」となる。トランクリンクは通常リンクより 2 倍太く(例: 3.0px)、ラベルに interface_name.modport_name(例: axi_if.master)を表示する。 |
| R-A24 | interface トランクの色分け: 各 InterfaceInstance に決定的な色を割り当て(インスタンス名のハッシュから HSL の hue を算出)、同一バスに属するトランクは同じ色で描画する。これによりダイアグラム上で複数バスが交錯しても識別可能とする。クロック・リセットの特殊色(#E63946, #F4A261)とは別カラーパレットを使う。 |
| R-A25 | interface トランクの展開: ユーザーがトランクリンクをクリックした時、そのトランクを構成する個別信号リンクに展開する(再クリックで折りたたみ)。展開時は元のトランクを点線で残し、個別信号は薄い色の細線で描画。展開状態は _meta.expanded: true として JointJS リンクに保持し、保存形式 JSON にも反映する。展開・折りたたみは R-A22 の対象外(再ルーティングは走らせない、見た目のみ切替)。 |
| R-A15 | 親コンテナのポートが枠から外れることを禁止する。ELK では親ノードに elk.nodeSize.constraints: 'PORTS NODE_LABELS MINIMUM_SIZE' と elk.padding を指定し、子ノード+全ポート+ラベルを内包するように親サイズを自動拡張する。 |
| R-A16 | 親コンテナの入出力ポートが、親自身の中央から枠の上下辺を経由して子ノードへ降りる U 字経路を生成することを禁止する。これは R-A3 の WEST/EAST 強制で構造的に防止する。さらに elk.hierarchyHandling: 'INCLUDE_CHILDREN' を有効化し、親ポートから子ノードへの経路がコンテナ内部で直接結ばれるようにする。 |
| R-A17 | すべてのノードはサイズ 0×0 を持ってはならない。width >= 60、height >= 40 を最小値とし、ポート数・ラベル長から自動計算する。ELK には常に有効なサイズを与え、ELK 自身に elk.nodeSize.constraints: 'PORTS NODE_LABELS MINIMUM_SIZE' で最終サイズを決定させる。サイズ 0×0 のノードはレンダリング不能(ポートが定義されていても表示されない)であり、品質保証上の不具合として扱う。 |
| R-A18 | 親コンテナのサイズ・ポート位置は手動指定禁止。width / height、ポート args.x / args.y を JSON に直接書かない。すべて ELK の elk.nodeSize.constraints + elk.padding + elk.port.side + elk.port.index の組み合わせで自動算出させ、ELK の出力(children[].width/height、ports[].x/y)を JointJS に反映する。例外として、ELK 計算結果を保存形式 JSON に書き戻すことは許容するが、初回レイアウト時は必ず ELK から得る。 |
| R-A19 | トップレベル I/O 端子(io_* ノード)のポート配置は elk.port.side で明示する。慣例として、トップ入力端子は EAST 側に出力ポート(信号を右へ流すため)、トップ出力端子は WEST 側に入力ポート(信号を右から受けるため)を持つ。これらも R-A2 の障害物として扱われ、リンクが横切ることを禁止する。 |
ルーティング方針: ELK.js のエッジ経路を JointJS の vertices として注入する
JointJS の各種 router(manhattan, orthogonal 等)は リンクごとに独立して経路を計算するため、複数の線が同じ走行路を通る場合に重なって描画されるという制約がある。本仕様ではこれを避けるため、ELK.js のレイアウト計算と同時にエッジ経路も決定し、その結果(edges[].sections[].bendPoints)を JointJS のリンクに vertices として渡す。JointJS 側のルーターは 'normal'(頂点列をそのままつなぐだけ)に固定する。
この方式により、ELK の OrthogonalEdgeRouter が 全エッジを同時に考慮して経路を計算するため、線同士の重複が発生しにくくなる。
実装方針:
- R-A1(縦線重複の回避): ELK の
elk.spacing.edgeEdgeを spacing スケールに連動させ、密集設計で最大 40px まで広げる。さらにelk.layered.spacing.edgeEdgeBetweenLayersを 30px 以上に設定。ELK のOrthogonalEdgeRouterは全エッジを同時計算するため、これらの spacing が確保されていれば構造的に重複は避けられる。手動vertices編集後は重なりを残さないよう検証する - R-A2(ノード回避): ELK の
elk.edgeRouting: 'ORTHOGONAL'で直角かつノード回避の経路を取得。トップ I/O ノード(io_*)も ELK のchildrenに含めて障害物として認識させる - R-A3(左右接続のみ・厳格化): 各ポートに
elk.port.side: 'WEST' | 'EAST'を明示。親ノードにelk.portConstraints: 'FIXED_SIDE'。JointJS 側ではポートのposition: { name: 'left' | 'right' }のみ使用- input → WEST(左辺)、output → EAST(右辺)、inout → 信号方向のヒントに応じて WEST/EAST のいずれか(デフォルトは WEST)
NORTH/SOUTHは本仕様では使用しない
- R-A4(コンテナの左上不変リサイズ): 子ノード移動後、親要素の子 bounding box を再計算し、
sizeのみ更新するresizeContainerToFit()ヘルパーを実装。positionは固定、サイズは拡張・縮小の両方を行う。再帰的に上位コンテナへ伝播(R-A21) - R-A14(1-to-Nファンアウトのトランク化): ELK は
INCLUDE_CHILDRENモードとORTHOGONALルーティングの組み合わせで、共通ソースを持つエッジを自動的にトランク状にまとめる。明示的なトランク x の指定は行わず、ELK の出力をそのまま採用する - R-A15(親コンテナのポート枠外回避): 親ノードに以下を必須指定する
elk.nodeSize.constraints: 'PORTS NODE_LABELS MINIMUM_SIZE'elk.padding: '[top=30, left=30, bottom=30, right=30]'(子ノードとポートの間に最低 30px の余白を確保)elk.nodeSize.minimum: '[120, 80]'(最小サイズの保証)
- R-A16(親→子の U 字経路回避): 親ノードに
elk.hierarchyHandling: 'INCLUDE_CHILDREN'を指定する。これにより親のポートと子ノードのポートを結ぶエッジが、親の内部空間を経由して直接ルーティングされる - R-A17(ノードサイズの最小保証): 全モジュールノード生成時にポート数とラベル幅から最小サイズを計算するヘルパー
computeNodeSize()を実装。width = max(180, longestPortLabelWidth + 80)、height = max(80, max(inputs, outputs) * 26 + 40) - R-A18(親サイズの自動算出): 親ノードを ELK に渡す際は
width/heightを 最小ヒント値のみ(例width: 200, height: 120)とし、elk.nodeSize.constraintsに決定を委ねる。ELK 戻り値のwidth/heightを JointJS に反映する - R-A19(トップ I/O のポート定義):
io_*ノードを ELK のchildrenに追加する際、トップ入力端子にはelk.port.side: 'EAST'、トップ出力端子にはelk.port.side: 'WEST'を持つ単一ポートを定義する - R-A20(ファンアウト起点の分散): 親ノードに同一信号が多数の子へ分岐する場合、ELK の
elk.layered.spacing.nodeNodeBetweenLayersを大きめに確保し、各子のポート位置が縦方向に十分離れるようにする - R-A21(ドロップ時の親サイズ自動フィット):
element:pointerupイベントでresizeContainerToFit()を呼ぶ。ドラッグ中(pointermove)は何もしない。50ms デバウンスで連続ドラッグを抑制。親が親を持つネストの場合は内側から外側へ再帰的に伝播 - R-A22(全体再ルーティング): ドロップ時に
rerouteAll()を呼び、ELK にグラフ全体を渡してcrossingMinimization.semiInteractive: 'true'でノード位置を尊重したまま全エッジを再計算。重い処理になるため、デバウンスと将来的な Web Worker 化で対応。INHERITや指定なしの場合、親ポートを起点として一度親の外周(上辺・下辺)に出てから戻ってくる U 字経路が発生しうる - R-A23(interface トランク化): バックエンドから受け取った
ParsedDesign.interface_instances_by_parentを参照し、buildElkEdges()で同一interface_instanceを持つ複数のPortConnectionを 1 本の ELK エッジに集約する。集約したエッジには_meta.interfaceInstanceを持たせ、フロントエンドで太線描画する - R-A24(色割り当て):
assignInterfaceColor(instanceName)ヘルパーで決定的にHSL色を算出。hue = hashString(instanceName) % 360、saturation = 60%、lightness = 45%。同一インスタンス名は常に同色 - R-A25(展開・折りたたみ): トランクリンクの
pointerclickで展開状態をトグル。展開時は元トランクの opacity を下げ、InterfaceDef.signals × Modport.signalsから個別リンクを動的生成。折りたたみ時は個別リンクを破棄
// ELK の経路計算結果を JointJS リンクに注入する
function applyElkRoutingToLinks(
elkGraph: ElkNode, // ELK の layout() 戻り値
graph: joint.dia.Graph
): void {
// 再帰的に全エッジ(ネストした子ノード内のエッジも含む)を処理
function walk(node: ElkNode, parentOffset: { x: number; y: number }): void {
const absX = parentOffset.x + (node.x ?? 0);
const absY = parentOffset.y + (node.y ?? 0);
node.edges?.forEach(elkEdge => {
const link = graph.getCell(elkEdge.id) as joint.dia.Link | undefined;
if (!link) return;
const section = elkEdge.sections?.[0];
if (!section) return;
// bendPoints は親要素相対座標 → 絶対座標へ変換
const vertices = (section.bendPoints ?? []).map(p => ({
x: p.x + absX,
y: p.y + absY
}));
link.set('vertices', vertices);
link.router('normal');
link.connector('rounded', { radius: 8 });
});
node.children?.forEach(child => walk(child, { x: absX, y: absY }));
}
walk(elkGraph, { x: 0, y: 0 });
}注意: ELK の出力する
bendPointsは 親要素を原点とした相対座標 で表される。階層構造(childrenを持つノード)がある場合、子ノード内のエッジの座標は親のオフセットを加算して絶対座標に変換する必要がある。JointJS のverticesは絶対座標を期待するため、上記のように再帰的にオフセットを伝播させる。
| ID | 要求 |
|---|---|
| R-A6 | トップレベル入力端子は 最左列 に配置する。ELK 子ノードに elk.layered.layering.layerConstraint: FIRST を付与する。 |
| R-A7 | トップレベル出力端子は 最右列 に配置する。同上、layerConstraint: LAST。inout には制約を付けない。 |
| R-A10 | SystemVerilog interface の master 側 modport に接続するインスタンスは 左側 へ配置する。接続元の PortConnection.modport == "master"(または "mst"、"m" を含む類似名)で判定する。正式な modport 解析(§5.4.1)を用い、サフィックスヒューリスティックは補助的なフォールバックに限定する。 |
| R-A11 | 同様に slave 側 modport(PortConnection.modport == "slave" / "slv" / "s")に接続するインスタンスは 右側 に配置する。 |
実装方針(ELK.js への制約付与):
// R-A3: input → WEST、output → EAST、inout → WEST(デフォルト)
function portSideFor(direction: 'input' | 'output' | 'inout'): 'WEST' | 'EAST' {
if (direction === 'output') return 'EAST';
return 'WEST'; // input, inout
}
// R-A17: ノードサイズの最小保証(0×0禁止)
function computeNodeSize(moduleDef: ModuleDef): { width: number; height: number } {
const longestLabel = Math.max(
...moduleDef.ports.map(p => p.name.length + (p.width > 1 ? 5 : 0)),
moduleDef.name.length
);
const inputs = moduleDef.ports.filter(p => p.direction === 'input').length;
const outputs = moduleDef.ports.filter(p => p.direction === 'output').length;
return {
width: Math.max(180, longestLabel * 8 + 80),
height: Math.max(80, Math.max(inputs, outputs) * 26 + 40)
};
}
// R-A15, R-A18: 子ノードを持つ親コンテナの共通レイアウト制約
const CONTAINER_LAYOUT_OPTIONS = {
'elk.portConstraints': 'FIXED_SIDE', // R-A3
'elk.nodeSize.constraints': 'PORTS NODE_LABELS MINIMUM_SIZE', // R-A15, R-A18
'elk.padding': '[top=30, left=30, bottom=30, right=30]', // R-A15
'elk.nodeSize.minimum': '[200, 120]', // R-A18
'elk.hierarchyHandling': 'INCLUDE_CHILDREN' // R-A16, R-A14
};
// R-A19: トップ I/O ノードのレイアウトオプション
const TOP_IO_LAYOUT_OPTIONS = {
'elk.portConstraints': 'FIXED_SIDE',
'elk.nodeSize.constraints': 'PORTS NODE_LABELS MINIMUM_SIZE',
'elk.nodeSize.minimum': '[120, 40]'
};
function buildElkChildren(design: ParsedDesign, topModuleName: string): ElkNode[] {
const top = design.modules[topModuleName];
const instances = design.instances_by_parent[topModuleName] ?? [];
const children: ElkNode[] = [];
// R-A6, R-A19: トップレベル入力端子(io_*)を FIRST 層へ、EAST ポート1個を持つ
top.ports.filter(p => p.direction === 'input').forEach(p => {
children.push({
id: `io_${p.name}`,
width: 120, height: 40, // R-A17: 0×0禁止
layoutOptions: {
...TOP_IO_LAYOUT_OPTIONS,
'elk.layered.layering.layerConstraint': 'FIRST'
},
ports: [{
id: `io_${p.name}_out`,
layoutOptions: { 'elk.port.side': 'EAST' } // R-A19
}]
});
});
// R-A7, R-A19: トップレベル出力端子を LAST 層へ、WEST ポート1個を持つ
top.ports.filter(p => p.direction === 'output').forEach(p => {
children.push({
id: `io_${p.name}`,
width: 120, height: 40,
layoutOptions: {
...TOP_IO_LAYOUT_OPTIONS,
'elk.layered.layering.layerConstraint': 'LAST'
},
ports: [{
id: `io_${p.name}_in`,
layoutOptions: { 'elk.port.side': 'WEST' } // R-A19
}]
});
});
// inout は制約なし、ただし R-A3 により WEST/EAST のいずれか
top.ports.filter(p => p.direction === 'inout').forEach(p => {
children.push({
id: `io_${p.name}`,
width: 120, height: 40,
layoutOptions: TOP_IO_LAYOUT_OPTIONS,
ports: [{
id: `io_${p.name}_p`,
layoutOptions: { 'elk.port.side': 'WEST' } // R-A3 デフォルト
}]
});
});
// R-A10/R-A11: modport 名でインスタンス位置を寄せる(正式解析)
for (const inst of instances) {
// インスタンスの接続から modport 名を収集
const modports = inst.connections
.map(c => c.modport)
.filter((m): m is string => !!m)
.map(m => m.toLowerCase());
const isMaster = modports.some(m => /^(master|mst|m)$/.test(m));
const isSlave = modports.some(m => /^(slave|slv|s)$/.test(m));
// フォールバック: modport 情報が無い場合は信号名のサフィックス判定
const sigs = inst.connections.map(c => c.signal);
const isMasterBySuffix = !modports.length && sigs.some(s => /\.master$/.test(s));
const isSlaveBySuffix = !modports.length && sigs.some(s => /\.slave$/.test(s));
const constraintOption: Record<string, string> = {
'elk.portConstraints': 'FIXED_SIDE' // R-A3
};
if ((isMaster || isMasterBySuffix) && !(isSlave || isSlaveBySuffix)) {
constraintOption['elk.layered.layering.layerChoiceConstraint'] = '1';
} else if ((isSlave || isSlaveBySuffix) && !(isMaster || isMasterBySuffix)) {
constraintOption['elk.layered.layering.layerChoiceConstraint'] = '-2';
}
// インスタンスのモジュール定義から ELK ポート一覧を生成
const moduleDef = design.modules[inst.module_name];
const elkPorts = moduleDef.ports.map((p, idx) => ({
id: `${inst.instance_name}_${p.name}`,
layoutOptions: {
// interface 型ポートは方向不明扱い → WEST デフォルト
'elk.port.side': p.direction === 'interface' ? 'WEST' : portSideFor(p.direction),
'elk.port.index': String(idx) // R-A20: 順序明示
}
}));
// 子ノードを持つ場合(階層モジュール)は CONTAINER_LAYOUT_OPTIONS を併用
const hasChildren = (design.instances_by_parent[inst.module_name] ?? []).length > 0;
// R-A17: 必ず有効なサイズを与える(ELK が最終調整)
const { width: minW, height: minH } = computeNodeSize(moduleDef);
const layoutOptions = hasChildren
? { ...CONTAINER_LAYOUT_OPTIONS, ...constraintOption } // R-A15, R-A16, R-A18
: constraintOption;
children.push({
id: inst.instance_name,
width: minW, // R-A17: 0×0禁止
height: minH, // R-A17
layoutOptions,
ports: elkPorts,
// 階層がある場合は再帰的に子ノードを構築
...(hasChildren ? { children: buildElkChildren(design, inst.module_name) } : {})
});
}
return children;
}重要(R-A17 / R-A18): ELK に渡す
width/heightは 最小ヒント値 であり、ELK はelk.nodeSize.constraints: 'PORTS NODE_LABELS MINIMUM_SIZE'の指定により、子要素・ポート・ラベルを内包できるサイズに自動拡張する。サイズ 0×0 を渡すと、ELK が拡張する起点を失い、ポートが定義されていてもレンダリング不能なノードが生成される。すべてのノードに最小ヒントを設定すること。重要(R-A14 / R-A1): ELK の
INCLUDE_CHILDRENモードとORTHOGONALルーティング、十分なedgeEdgespacing を組み合わせることで、1-to-N ファンアウトは自動的にトランク状にまとめられる。妥当な spacing が確保されていない場合、ターゲットごとに独立した縦経路が生成され、同一 x 座標に集中して縦線重複となるため、§6.5.3 の spacing スケールを必ず適用する。
interface 接続は通常の信号エッジとは別に集約処理を行う。buildElkEdges() ヘルパーで以下のロジックを適用する:
type ElkEdgeWithMeta = ElkEdge & {
_meta?: {
interfaceInstance?: string; // 集約元の interface instance 名
interfaceName?: string; // axi_if 等
modport?: string; // master / slave
isInterfaceTrunk?: boolean; // トランクなら true
color?: string; // R-A24: 割当色
}
};
// R-A23: 同一 interface_instance を持つ複数 PortConnection を 1 本のエッジに集約
function buildElkEdges(design: ParsedDesign, topModuleName: string): ElkEdgeWithMeta[] {
const edges: ElkEdgeWithMeta[] = [];
const instances = design.instances_by_parent[topModuleName] ?? [];
// (sourceModule, targetModule, interfaceInstance) でグループ化
const trunkGroups = new Map<string, PortConnection[]>();
for (const inst of instances) {
for (const conn of inst.connections) {
if (conn.interface_instance) {
const key = `${inst.instance_name}|${conn.interface_instance}|${conn.modport ?? ''}`;
if (!trunkGroups.has(key)) trunkGroups.set(key, []);
trunkGroups.get(key)!.push(conn);
} else {
// 通常信号エッジは従来通り 1 本ずつ
edges.push(buildNormalEdge(inst, conn));
}
}
}
// R-A23: 各グループを 1 本のトランクエッジに集約
for (const [key, conns] of trunkGroups) {
const [instName, ifaceInst, modport] = key.split('|');
const ifaceName = lookupInterfaceName(design, topModuleName, ifaceInst);
edges.push({
id: `trunk_${instName}_${ifaceInst}`,
sources: [`${instName}_${conns[0].port_name}`],
targets: [resolveTrunkTarget(design, topModuleName, ifaceInst, modport)],
_meta: {
interfaceInstance: ifaceInst,
interfaceName: ifaceName,
modport: modport || undefined,
isInterfaceTrunk: true,
color: assignInterfaceColor(ifaceInst) // R-A24
}
});
}
return edges;
}
// R-A24: interface インスタンス名からHSL色を決定的に算出
function assignInterfaceColor(instanceName: string): string {
// FNV-1a ハッシュ
let hash = 2166136261;
for (let i = 0; i < instanceName.length; i++) {
hash ^= instanceName.charCodeAt(i);
hash = Math.imul(hash, 16777619);
}
const hue = Math.abs(hash) % 360;
// クロック(0±20)・リセット(30±20)の hue は避ける
const safeHue = (hue < 50) ? hue + 80 : hue;
return `hsl(${safeHue}, 60%, 45%)`;
}JointJS 側ではトランクエッジを以下のスタイルで描画する(F-20, R-A23):
// applyElkRoutingToLinks() 内のリンク生成部
if (elkEdge._meta?.isInterfaceTrunk) {
link.attr('line/stroke', elkEdge._meta.color); // R-A24
link.attr('line/strokeWidth', 3.0); // R-A23: 通常の2倍
link.label(0, {
attrs: {
text: { text: `${elkEdge._meta.interfaceName}.${elkEdge._meta.modport ?? '?'}` }
}
});
link.prop('_meta/isInterfaceTrunk', true);
link.prop('_meta/expanded', false); // R-A25 初期値
}ユーザーがトランクリンクをクリックすると、そのリンクを構成する個別信号リンクに展開する。
paper.on('link:pointerclick', (linkView) => {
const link = linkView.model;
if (!link.prop('_meta/isInterfaceTrunk')) return;
const expanded = link.prop('_meta/expanded');
if (expanded) {
collapseInterfaceTrunk(link, graph);
} else {
expandInterfaceTrunk(link, graph, design);
}
// R-A25: 展開・折りたたみは ELK 再ルーティングを走らせない(見た目のみ切替)
});
function expandInterfaceTrunk(
trunk: joint.dia.Link,
graph: joint.dia.Graph,
design: ParsedDesign
): void {
const meta = trunk.prop('_meta');
const iface = design.interfaces[meta.interfaceName];
const modport = iface.modports.find(m => m.name === meta.modport);
// トランクを薄い破線で残す
trunk.attr('line/strokeDasharray', '5,5');
trunk.attr('line/opacity', 0.3);
// 個別信号リンクを動的生成
const childLinks: joint.dia.Link[] = [];
for (const sig of (modport?.signals ?? iface.signals)) {
const child = new joint.shapes.standard.Link();
child.source(trunk.source());
child.target(trunk.target());
child.attr('line/stroke', meta.color);
child.attr('line/strokeWidth', 1.0);
child.attr('line/opacity', 0.7);
child.label(0, { attrs: { text: { text: sig.name } } });
child.prop('_meta/parentTrunk', trunk.id);
child.addTo(graph);
childLinks.push(child);
}
trunk.prop('_meta/expanded', true);
trunk.prop('_meta/childLinkIds', childLinks.map(l => l.id));
}
function collapseInterfaceTrunk(trunk: joint.dia.Link, graph: joint.dia.Graph): void {
const childIds = trunk.prop('_meta/childLinkIds') ?? [];
childIds.forEach((id: string) => graph.getCell(id)?.remove());
trunk.attr('line/strokeDasharray', 'none');
trunk.attr('line/opacity', 1.0);
trunk.prop('_meta/expanded', false);
trunk.prop('_meta/childLinkIds', []);
}§6.5.3〜§6.5.5 をまとめた初期レイアウトの呼び出しは以下のようになる。
import ELK from 'elkjs/lib/elk.bundled.js';
async function performInitialLayout(
design: ParsedDesign,
topModuleName: string,
graph: joint.dia.Graph
): Promise<void> {
const elk = new ELK();
const children = buildElkChildren(design, topModuleName); // R-A6, R-A7, R-A10, R-A11
const edges = buildElkEdges(design, topModuleName);
const mult = computeSpacingMultiplier(edges.length, children.length); // R-A8
const elkGraph: ElkNode = {
id: 'root',
layoutOptions: {
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT', // R-A3 の前提
'elk.edgeRouting': 'ORTHOGONAL', // 直角配線で経路計算(R-A2)
'elk.hierarchyHandling': 'INCLUDE_CHILDREN', // 階層を跨ぐエッジを正しく処理(R-A14, R-A16)
'elk.spacing.nodeNode': String(80 * mult),
'elk.layered.spacing.nodeNodeBetweenLayers': String(180 * mult), // R-A12, R-A20
'elk.spacing.edgeNode': String(40 * mult),
'elk.spacing.edgeEdge': String(40 * mult), // R-A1: 縦線重複回避を強化
'elk.layered.spacing.edgeEdgeBetweenLayers': String(30 * mult), // R-A1
'elk.layered.crossingMinimization.semiInteractive': 'true',
'elk.portConstraints': 'FIXED_SIDE' // R-A3
},
children,
edges
};
const layouted = await elk.layout(elkGraph);
// ELK 結果を JointJS に反映: ノード位置とサイズ(階層を再帰的に処理)
function applyNodePositions(node: ElkNode, parentOffset: { x: number; y: number }): void {
const absX = parentOffset.x + (node.x ?? 0);
const absY = parentOffset.y + (node.y ?? 0);
node.children?.forEach(c => {
const cell = graph.getCell(c.id);
if (cell) {
cell.position(absX + (c.x ?? 0), absY + (c.y ?? 0));
// R-A17, R-A18: ELK が計算した最終サイズを JointJS に必ず反映
// (これがないと最小ヒント値のままになる、または手動値が残る)
if (c.width && c.height) {
cell.resize(c.width, c.height);
} else {
console.warn(`Node ${c.id} has invalid size from ELK:`, c.width, c.height);
}
// R-A18: ELK が計算した最終ポート位置も反映する
c.ports?.forEach(p => {
if (p.x != null && p.y != null && cell.getPort) {
cell.portProp(p.id, 'args/x', p.x);
cell.portProp(p.id, 'args/y', p.y);
}
});
}
applyNodePositions(c, { x: absX, y: absY });
});
}
applyNodePositions(layouted, { x: 0, y: 0 });
// ELK 結果を JointJS に反映: エッジ経路を vertices として注入(R-A2)
applyElkRoutingToLinks(layouted, graph);
}ユーザーがノードをドラッグ
↓
element:pointerdown → ドラッグ開始位置を記録
↓
element:pointermove → ノードの位置のみ更新
(リンクは JointJS が vertices に基づき自動追従)
(親コンテナのサイズは変更しない: R-A21)
↓
element:pointerup(= ドロップ)
↓
[ドロップハンドラ(50msデバウンス後)]
1. ドロップされたノードの位置を確定 ← R-A5
(他ノードの位置・サイズには触らない)
2. 親コンテナのリサイズ(再帰的、内側から外側へ) ← R-A4, R-A21
- 親内の全子ノードの bounding box を計算
- 親の左上は不変、`size = (bbox.maxX - position.x + padding,
bbox.maxY - position.y + padding)`
- 拡張・縮小の両方を許容
- 親の親があれば、同じロジックで再帰的に適用
3. ELK にグラフ全体を渡して再レイアウト(ノード位置固定モード) ← R-A22
- 各ノードに `elk.position` で現在の絶対座標を指定
- `elk.layered.crossingMinimization.semiInteractive: 'true'`
- 位置は固定したまま、エッジ経路のみ ELK に再計算させる
4. applyElkRoutingToLinks() で **全リンク** の vertices を再注入 ← R-A22, F-17
実装(擬似コード):
import { debounce } from 'lodash';
// ドロップ時にのみ実行する重いフロー(50ms デバウンス)
const handleDrop = debounce(async (movedNode: joint.dia.Element) => {
// Step 2: 親コンテナを子の bbox にフィット(R-A4, R-A21)
resizeContainerToFit(movedNode);
// Step 3-4: ELK で全体再ルーティング(R-A22)
await rerouteAll(graph, paper);
}, 50);
paper.on('element:pointerup', (elementView) => {
handleDrop(elementView.model);
});
// R-A4, R-A21: 親コンテナを子ノード群の bbox にフィット
// 左上は不変、サイズのみ拡張・縮小、再帰的に上位へ伝播
function resizeContainerToFit(node: joint.dia.Element): void {
const parentId = node.get('parent');
if (!parentId) return;
const parent = graph.getCell(parentId) as joint.dia.Element;
if (!parent) return;
const children = parent.getEmbeddedCells().filter(c => c.isElement()) as joint.dia.Element[];
if (children.length === 0) return;
// 子ノード群の bounding box を計算(絶対座標)
let maxX = -Infinity, maxY = -Infinity;
for (const c of children) {
const bbox = c.getBBox();
maxX = Math.max(maxX, bbox.x + bbox.width);
maxY = Math.max(maxY, bbox.y + bbox.height);
}
// 親の左上は不変、サイズのみ調整(R-A4: 拡張も縮小も許容)
const PADDING = 30;
const parentPos = parent.position();
const minWidth = 200; // R-A17 最小値
const minHeight = 120;
parent.resize(
Math.max(minWidth, maxX - parentPos.x + PADDING),
Math.max(minHeight, maxY - parentPos.y + PADDING)
);
// 再帰的に上位コンテナへ伝播
resizeContainerToFit(parent);
}
// R-A22: グラフ全体を再ルーティング
async function rerouteAll(graph: joint.dia.Graph, paper: joint.dia.Paper): Promise<void> {
const elk = new ELK();
const elkGraph = buildElkGraphWithFixedPositions(graph); // 各ノードに elk.position を付与
const layouted = await elk.layout(elkGraph);
// ノード位置は反映しない(固定済み)、エッジ経路のみ反映
applyElkRoutingToLinks(layouted, graph);
}
// 現在のノード位置を保ったまま ELK 入力グラフを構築する
function buildElkGraphWithFixedPositions(graph: joint.dia.Graph): ElkNode {
function nodeToElk(cell: joint.dia.Element): ElkNode {
const pos = cell.position();
const size = cell.size();
const parentPos = cell.get('parent')
? (graph.getCell(cell.get('parent')) as joint.dia.Element).position()
: { x: 0, y: 0 };
return {
id: cell.id as string,
// 親相対座標で渡す(ELK の慣習)
x: pos.x - parentPos.x,
y: pos.y - parentPos.y,
width: size.width,
height: size.height,
layoutOptions: {
// ノード位置を固定するモード(R-A22)
'elk.layered.crossingMinimization.semiInteractive': 'true'
},
ports: cell.getPorts().map(p => ({
id: p.id,
layoutOptions: { 'elk.port.side': portSideFromGroup(p.group) }
})),
children: cell.getEmbeddedCells()
.filter(c => c.isElement())
.map(c => nodeToElk(c as joint.dia.Element))
};
}
const topElements = graph.getElements().filter(e => !e.get('parent'));
return {
id: 'root',
layoutOptions: {
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT',
'elk.edgeRouting': 'ORTHOGONAL',
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
'elk.layered.crossingMinimization.semiInteractive': 'true'
},
children: topElements.map(e => nodeToElk(e)),
edges: graph.getLinks().map(l => ({
id: l.id as string,
sources: [`${l.source().id}/${l.source().port}`],
targets: [`${l.target().id}/${l.target().port}`]
}))
};
}設計上のトレードオフ:
- ドラッグ中のリアルタイム親リサイズはしない(R-A21)。
pointermoveで毎フレーム計算するとリンクの追従描画と相まってフレームレートが落ちる。代わりにドラッグ中は親枠から子が一時的にはみ出して見えるが、ドロップ瞬間に正しいサイズに整う。- 全体再ルーティングを採用(R-A22)。差分再ルーティング(移動ノード接続リンクだけ)は軽いが、移動が他リンクを横切る・混雑が変わる等の波及効果に対応できない。50ms デバウンスと Web Worker 化で重さを緩和する。
各要求 ID には少なくとも 1 つのテストケースを用意する。詳細は §10.1 のテストフィクスチャに以下を追加する。
| 要求 ID | フィクスチャ / シナリオ |
|---|---|
| R-A1 | 1-to-N ファンアウト(クロック・リセット等)で生成された全リンクの vertices を走査し、同一 x 座標で重なる縦区間が無いことを検証(許容差 ±2px) |
| R-A2 | ソース・ターゲットの直線経路上に障害物ノードが存在する設計。トップ I/O ノード列(io_*)を跨ぐリンクを生成し、リンクの vertices と障害物ノードの bounding box の交差を検証 |
| R-A3 | 全ポートの elk.port.side が WEST または EAST であることを確認(NORTH/SOUTH が出現しない) |
| R-A4 | 子ノードを右下に移動して親コンテナの左上が動かないことを確認 |
| R-A5 | ノード A をドラッグ後、無関係なノード B / C の座標が変化しないことを確認 |
| R-A6 / R-A7 | トップレベル input / output 端子が最左 / 最右に配置されることを確認 |
| R-A8 | 高混雑設計(edges/nodes = 3.0)で spacing が 2.0 倍になることを確認 |
| R-A10 / R-A11 | .master / .slave サフィックスを含む信号名でインスタンスが寄ることを確認 |
| R-A12 | 低混雑設計でも nodeNodeBetweenLayers が 180 以上であることを確認 |
| R-A14 | 1-to-5 ファンアウト(io_pcie_clk から複数サブモジュール)で、ELK 出力に共通トランク区間が含まれることを確認(各リンクの vertices の前半に共通点が現れる) |
| R-A15 | 階層モジュール(子ノードを含むコンテナ)の全ポートが親の枠内 or 枠上に配置されていることを確認。枠外に飛び出さない |
| R-A16 | 親コンテナのポートから子ノードへのエッジが、親の上辺・下辺を経由する U 字経路にならないことを確認(bendPoints が親の bounding box の上下を跨がない) |
| R-A17 | 全ノード(トップ I/O・モジュール・サブモジュール)について width >= 60 && height >= 40 を検証。0×0 ノードがゼロ件であることを保証 |
| R-A18 | 保存形式 JSON 内の階層モジュール(embeds を持つノード)について、サイズと全ポート位置が ELK 出力に由来することを確認(手動指定マーカー _meta.manualSize: true が存在しない) |
| R-A19 | トップ I/O ノードに必ず 1 つのポートが定義され、その elk.port.side が input → EAST、output → WEST、inout → WEST となっていることを確認 |
| R-A20 | 親ノードの同一辺(WEST/EAST)に配置されたポート間の y 座標差が、elk.spacing.portPort 以上であることを確認 |
| R-A21 | 親コンテナ内の子ノードを右下に移動 → ドロップ後に親サイズが拡張されることを確認。逆に子を内側にまとめて移動 → ドロップ後に親サイズが縮小されることを確認。両ケースで親 position(左上)が不変であることを検証 |
| R-A22 | 1つのノードをドラッグ移動 → ドロップ後、移動に接続していないリンクの vertices も変化していることを確認(全体再ルーティングが実行された証拠)。例えばノードAを大きく移動した結果、関係ないリンクB-C の経路も最適化されている |
| F-18 | interface foo_if; logic a; logic b; modport master(input a, output b); endinterface を含むフィクスチャから InterfaceDef が抽出され、signals=[a,b]、modports=[master] を持つことを確認 |
| F-19 | モジュール宣言 module m(foo_if.master bus); のポートが Port(direction="interface", interface_name="foo_if", modport="master") として解析されることを確認 |
| F-20 / R-A23 | 同一 interface インスタンスに紐づく 3 信号(req/ack/data)の接続が、JointJS グラフ上で 1 本のトランクリンクとして描画されることを確認(Link の数が 3 ではなく 1) |
| R-A24 | 異なる 2 つの interface インスタンス(axi0, axi1)のトランクリンクに異なる色が割り当てられることを確認。同じ名前のインスタンスは常に同じ色 |
| R-A25 | トランクリンクをクリック → 個別信号リンクが展開されることを確認。再クリック → 折りたたみで元の状態に戻ることを確認。展開・折りたたみ時に rerouteAll() が呼ばれないことを確認(再ルーティングが起きない) |
sv-module-viewer/
├── README.md
├── docker-compose.yml # 開発用
├── backend/
│ ├── pyproject.toml
│ ├── uv.lock
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py # FastAPI エントリ
│ │ ├── api/
│ │ │ └── parse.py # POST /api/parse
│ │ ├── verible/
│ │ │ ├── runner.py # Verible 呼び出し
│ │ │ └── cst_visitor.py # CST → 中間モデル
│ │ └── models.py # dataclass 定義
│ └── tests/
│ ├── fixtures/
│ │ ├── simple.sv
│ │ └── cpu_top.sv
│ └── test_parse.py
├── frontend/
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── index.html
│ ├── src/
│ │ ├── main.ts # エントリ
│ │ ├── api/
│ │ │ └── client.ts # バックエンド呼び出し
│ │ ├── diagram/
│ │ │ ├── paper.ts # JointJS Paper 初期化
│ │ │ ├── nodes.ts # モジュールノード生成
│ │ │ ├── ports.ts # ポート定義(R-A3: 左右配置のみ)
│ │ │ ├── links.ts # リンク生成
│ │ │ ├── layout.ts # ELK.js 自動配置(R-A6〜R-A12)
│ │ │ ├── routing.ts # applyElkRoutingToLinks(ELK経路をvertices注入、R-A2)
│ │ │ ├── container.ts # resizeContainerToFit、親リサイズ伝播(R-A4, R-A21)
│ │ │ └── dragHandlers.ts # ドロップ時の親リサイズ+全体再ルーティング(R-A21, R-A22, F-17)
│ │ ├── ui/
│ │ │ ├── toolbar.ts
│ │ │ ├── sidebar.ts
│ │ │ └── status.ts
│ │ └── types.ts # 型定義(API レスポンス等)
│ └── tests/
└── docs/
├── architecture.md
└── api.md
目標: 1 つの SV ファイルから、モジュールを矩形 + ポートで表示し、接続線を引く。
- Verible 呼び出しと CST 取得
- 単純なモジュール(パラメータなし、ANSI スタイル)の解析
- JointJS による基本描画
- ELK.js による自動レイアウト + orthogonal エッジ経路計算(R-A2 の基本実装)
- ELK 経路を JointJS の vertices として注入する
applyElkRoutingToLinks()実装 - ポート左右配置(R-A3)
- 手動ドラッグ配置 + ドロップ時の接続線のみ自動再ルーティング(R-A5, F-17)
§6.5 の要求仕様を完全に満たす。
- トップ入出力端子のレイヤー制約(R-A6, R-A7)
- トップ I/O ノードの port.side 明示と障害物化(R-A19)
- ノードサイズ最小保証
computeNodeSize()実装(R-A17) - 親コンテナサイズの ELK 自動算出(R-A18)
- 階層モジュールの親コンテナサイズ自動拡張(R-A15)
- 親→子のU字経路回避(
hierarchyHandling: INCLUDE_CHILDREN)(R-A16) - 縦線重複の構造的回避(spacing 強化)(R-A1)
- 1-to-N ファンアウトのトランク化(R-A14)
- ファンアウト起点のポート位置分散(R-A20)
- 混雑度に応じた spacing スケール(R-A8, R-A12)
-
interface/modport正式解析(modport 名による位置寄せ、サフィックスはフォールバック)(R-A10, R-A11, F-18, F-19) - interface 接続のトランク化描画(R-A23, R-A24, F-20)
- interface トランクの展開・折りたたみ(R-A25)
- コンテナの左上不変リサイズ(拡張・縮小両対応)(R-A4)
- ドロップ時の親サイズ自動フィット
resizeContainerToFit()(R-A21) - ドロップ時の全体再ルーティング
rerouteAll()(R-A22) - 複数ファイル対応
- バス幅の表示
- レイアウト JSON 保存・復元
- パースエラー UI
- ノン-ANSI スタイル(旧形式)のポート宣言対応
- SVG / PNG エクスポート
- パラメータ表示
-
inoutの整然とした表現(左右辺いずれかに自動配置) - 階層展開(インスタンスをダブルクリックで内部を表示)
- interface インスタンスを独立ノードとして描画するオプション(現状はトランクのみ)
-
interfaceのパラメータ化対応(virtual interfaceは除く) - ルーティング違反ケースの収集と継続改善(R-A9)
-
virtual interface経由の動的接続(class 内参照) - UVM コンポーネントの構造表現
-
generateブロック展開 - 検索・フィルタ機能
- ダーク/ライトテーマ切替
- ブラウザ単体動作(WASM 版 Verible でフロントエンドのみで完結)
| 指標 | 目標値 |
|---|---|
| 100 モジュール程度のプロジェクトの解析時間 | 5 秒以内 |
| 50 ノード描画時のフレームレート | 30 fps 以上 |
| 初回ページロード | 3 秒以内(CDN 経由) |
- Chrome / Edge: 最新版およびその 1 つ前のメジャーバージョン
- Firefox: 最新版
- Safari: 最新版(macOS 14 以降)
- IE / レガシー Edge: 非対応
- アップロードされたファイルはサーバ側で一時ディレクトリに保存し、処理完了後に削除
- Verible はサンドボックスとして
subprocessで実行(タイムアウト 30 秒) - ファイルサイズ上限: 1 ファイル 10 MB、合計 50 MB
- CORS: 開発時のみ全許可、本番は明示的なオリジン指定
- Docker Compose で
docker compose upの 1 コマンドで起動可能とする - 各サービスを個別の Docker イメージで配布
sv-module-viewer-backend(Python + Verible 同梱)sv-module-viewer-frontend(nginx + 静的ファイル)
- pytest を使用
- テストフィクスチャ
simple.sv: 1 モジュール、ポート 2 本hierarchy.sv: 親 + 子モジュール 2 個bus_width.sv: 各種ビット幅のポートnon_ansi.sv: 旧形式のポート宣言syntax_error.sv: 故意の構文エラーinterface_basic.sv: interface 1 個 + modport (master/slave) + 接続するモジュール 2 個(F-18, F-19, R-A23)interface_multi.sv: 同一モジュール内に複数 interface インスタンス(色分け検証用、R-A24)interface_nested.sv: 階層モジュール内で interface 接続(R-A16 と R-A23 の整合検証)
- 各フィクスチャで
ParsedDesignの期待値を比較
- Vitest を使用
- JointJS の描画は jsdom 上で簡易検証(DOM 上の要素数、属性)
- E2E は Playwright で「ファイルアップロード → 描画 → ドラッグ → 保存」のシナリオ
実プロジェクト(例: PicoRV32、ibex 等のオープンソース RISC-V コア)を読み込ませ、目視で接続図の妥当性を確認する。
| リスク | 影響 | 対応 |
|---|---|---|
| Verible が一部 SV 構文に未対応 | 高 | 既知の未対応構文をドキュメント化、エラー時はメッセージ表示 |
| 大規模設計でリンク数が増えると重い | 中 | 仮想化(画面外ノードを非描画)、信号フィルタ機能を Phase 3 で導入 |
| ELK の orthogonal ルーティングが密集時に経路品質を落とす | 中 | spacing スケール(R-A8)で予め間隔を確保する。elk.spacing.edgeEdge を大きめに設定 |
| ELK のレイアウト計算が大規模設計で遅い | 中 | Web Worker で実行、計算中はローディング表示。再レイアウトはデバウンス(50ms) |
| Verible バイナリのビルド・配布 | 中 | 公式リリースバイナリをダウンロードして Docker イメージに同梱 |
| JointJS の MPL ライセンス遵守 | 低 | JointJS 由来ファイルを別ディレクトリに隔離、ライセンス表示を README に明記 |
| ELK の bendPoints が JointJS の vertices と整合しない座標系の問題 | 中 | ELK の出力座標は親要素相対のため、ネスト時にはオフセット加算が必要。applyElkRoutingToLinks() 内で再帰的に絶対座標化 |
| 階層モジュールで親コンテナのポートが枠外に出る | 中 | elk.nodeSize.constraints: 'PORTS NODE_LABELS MINIMUM_SIZE' + elk.padding + elk.nodeSize.minimum の3点セットで親サイズを自動拡張する(R-A15)。手動指定は禁止(R-A18) |
| 親ポートから子ノードへのエッジがU字経路になる | 中 | elk.hierarchyHandling: 'INCLUDE_CHILDREN' を全コンテナに適用し、ELKに階層を跨ぐエッジを直接ルーティングさせる(R-A16) |
| サブモジュールが size 0×0 でレンダリングされない | 高 | 全ノード生成時に computeNodeSize() で最小ヒント値を必ず付与(R-A17)。ELK 出力後に width && height を検証し、ゼロなら警告ログ |
| 1-to-N ファンアウト(clk/rst)で縦線が重なる | 中 | elk.spacing.edgeEdge を 40px 以上、elk.layered.spacing.edgeEdgeBetweenLayers を 30px 以上に設定。hierarchyHandling: INCLUDE_CHILDREN と組み合わせて構造的に回避(R-A1, R-A14) |
| 過去の保存 JSON が手動サイズ・手動ポート位置を含む | 低 | JSON 読み込み時に旧形式を検出し、ELK 再レイアウトでサイズ・ポート位置を再算出する移行パスを提供(R-A18) |
R-A10/R-A11 のヒューリスティック(.master / .slave サフィックス)が実プロジェクトの命名規則と合わない |
中 | 第一義的には modport 正式解析(§5.4.1)を使用。modport 情報が取れない場合はサフィックスパターンを設定ファイルで上書き可能にする |
Verible が kInterfacePortHeader を期待通りに出力しないバージョン差異 |
中 | §5.4.2 のフォールバック実装(型部の文字列マッチ)で対応。CI で Verible 複数バージョン(v0.0-3000 系以降)に対する回帰テストを実施 |
| interface トランク化(R-A23)で個別信号の流れが見えにくくなる | 低 | R-A25 のクリック展開機能でいつでも個別信号に切り替え可能。トランクラベルに modport 名を表示し、ユーザーが内容を推測しやすくする |
interface 内に複雑な式(関数呼び出し、generate ブロック)があると解析失敗する |
低 | Phase 2 では「信号宣言と modport 宣言のみ」を対象とし、それ以外は無視。完全対応は Phase 3 で検討 |
| ドロップ時の自動再ルーティング(R-A5, R-A22)がフレームレートを下げる | 中 | デバウンス(ドロップ後 50ms 待機)、ELK レイアウトを Web Worker で実行。それでも 500 ノード超で遅い場合は、R-A22 を「親コンテナ内のみ再ルーティング」に局所化する設定オプションを Phase 3 で提供 |
| 全体再ルーティング(R-A22)でユーザー意図と異なる経路に変わる | 中 | crossingMinimization.semiInteractive: 'true' でノード位置を尊重させる。それでもユーザーが手動編集した vertices が消えるため、「ユーザー編集中のリンクは保護」フラグを Phase 3 で検討 |
| 親リサイズ(R-A21)で過度に縮小されると見た目が崩れる | 低 | min(width, height) を nodeSize.minimum で保証(R-A17 と同じ最小値)。子が 0 個のとき縮小を防ぐ |
- 公式: https://github.com/chipsalliance/verible
verible-verilog-syntaxドキュメント: https://chipsalliance.github.io/verible/verilog_syntax.html- CST JSON エクスポート例:
verilog/tools/syntax/export_json_examples/ - Python ラッパー:
verible_verilog_syntax.py
- 公式: https://www.jointjs.com/
- ドキュメント: https://docs.jointjs.com/
- API リファレンス: https://docs.jointjs.com/api/
- routers: https://docs.jointjs.com/api/routers/
- ポート設定: https://docs.jointjs.com/api/dia/Element/
- GitHub: https://github.com/clientIO/joint
- ライセンス: MPL-2.0
- ELK.js: https://github.com/kieler/elkjs(EPL-2.0、自動レイアウト用)
- FastAPI: https://fastapi.tiangolo.com/
- Vite: https://vitejs.dev/
| 用語 | 意味 |
|---|---|
| CST | Concrete Syntax Tree。構文木のうち、ソース上の全トークンを保持するもの |
| AST | Abstract Syntax Tree。CST から記号などを省いたもの |
| ANSI スタイル | ポート宣言を module foo(input clk, output q); のように本体に書く形式 |
| 非 ANSI スタイル | 古い形式で、module foo(clk, q); input clk; output q; のように分離する形式 |
| インスタンス | cpu_top u_cpu(.clk(clk), ...); の u_cpu のような具体実体 |
| トップモジュール | 他から呼ばれない最上位モジュール |
| ELK / ELK.js | Eclipse Layout Kernel。グラフの自動レイアウトエンジン。本アプリではノード配置とエッジ経路計算の両方を担う |
| OrthogonalEdgeRouter | ELK が提供する直角エッジルーター。elk.edgeRouting: 'ORTHOGONAL' で有効化し、全エッジを同時に考慮して経路を計算する(線同士の重複を抑制) |
| bendPoints | ELK エッジ出力の中間頂点列。JointJS の vertices として注入する |
| sections | ELK エッジ出力の経路区間。通常は 1 区間で、startPoint / bendPoints / endPoint を持つ |
| elk.port.side | ELK でポートが配置される辺。WEST / EAST / NORTH / SOUTH のいずれか。本仕様では R-A3 により WEST / EAST のみ使用 |
| elk.port.index | 同一辺に複数ポートを置く際の順序。R-A20 のためにファンアウトの多いポートの間隔を確保する用途で使う |
| elk.portConstraints | ノードのポート配置制約。FIXED_SIDE を指定すると、各ポートの elk.port.side 指定が尊重される |
| elk.nodeSize.constraints | 親ノードのサイズ計算制約。PORTS NODE_LABELS MINIMUM_SIZE で子要素・ポート・ラベルを内包するサイズに自動拡張 |
| elk.hierarchyHandling | 階層を跨ぐエッジの扱い。INCLUDE_CHILDREN を指定すると親ポートと子ノードを直接結ぶエッジが内部で正しくルーティングされる(R-A16) |
| computeNodeSize() | モジュール定義からノードの最小ヒントサイズを算出するヘルパー関数(R-A17)。ELK は最終的にこのヒント以上のサイズに自動拡張する |
| resizeContainerToFit() | 親コンテナを子ノード群の bounding box にフィットさせるヘルパー(R-A4, R-A21)。左上は不変、サイズのみ拡張・縮小。再帰的に上位へ伝播 |
| rerouteAll() | グラフ全体のリンクを ELK 経由で再ルーティングするヘルパー(R-A22)。semiInteractive モードでノード位置を尊重したまま全エッジを再計算 |
| semiInteractive | ELK の crossingMinimization.semiInteractive: 'true' モード。各ノードに elk.position を与えると、その位置を可能な限り尊重しながらレイアウトを再計算する |
| トランク化 | 1-to-N ファンアウトで共通経路を共有する状態。ELK の INCLUDE_CHILDREN + ORTHOGONAL の組み合わせで自動的に実現される(R-A14) |
| interface(SV) | SystemVerilog の言語機能。モジュール間で共有される信号群と方向(modport)をひとまとめに定義する仕様。例: interface axi_if; logic valid; modport master(output valid); endinterface |
| modport | interface 内で定義する「視点」。同じ信号でも master と slave で input/output 方向が反転する。modport master(input ack, output req) のように方向を明示 |
| interface 型ポート | モジュールポートの型に interface 名(+ modport)を指定したもの。例: module m(axi_if.master bus) の bus |
| interface インスタンス | モジュール本体で axi_if my_bus(); のように宣言された interface の実体。複数モジュール間の接続媒体として機能する |
| interface トランク | 同一 interface インスタンスを介した複数信号の接続を、ダイアグラム上で 1 本の太いリンクとして集約描画したもの(R-A23, F-20) |
この仕様書は Phase 1 着手前のドラフト v0.9 である。各セクションは開発進行に応じて更新する。
改版履歴
- v0.9: SystemVerilog
interface/modportの正式対応を追加(Phase 4 から Phase 2 へ格上げ):- データモデル(§4.1)に
InterfaceDef/Modport/ModportSignal/InterfaceSignal/InterfaceInstance/ 拡張Port(direction=interface) / 拡張PortConnectionを追加 - §5.4 CST 抽出ルールに
kInterfaceDeclaration/kModportDeclaration/ interface 型ポート / interface 接続式の規定を追加 - §5.4.1 で 3 パスの解析処理順序を明文化、§5.4.2 でフォールバック実装を許容
- 機能 F-18(interface 解析)/F-19(interface 型ポート)/F-20(トランク描画)を追加
- R-A10/R-A11 を「サフィックスヒューリスティック」から「modport 名による正式判定 + サフィックスフォールバック」へ書き換え
- 新規 R-A23(interface トランク化描画)/R-A24(決定的色割り当て)/R-A25(クリック展開・折りたたみ)
- §6.5.5b に
buildElkEdges()/assignInterfaceColor()擬似実装、§6.5.5c に展開・折りたたみハンドラ - 非ゴールを再整理(
interfaceは対象内に格上げ、virtual interfaceと UVM のみ将来検討に残す) - テストフィクスチャに
interface_basic.sv/interface_multi.sv/interface_nested.svを追加、Phase 4 の T4-01 タスクを Phase 2 へ移動
- データモデル(§4.1)に
- v0.8: ノード移動時の挙動を具体化。R-A4 を「拡張のみ」から「拡張・縮小両対応(子bbox フィット)」に修正。新規追加: R-A21(ドロップ時の親サイズ自動フィット、ドラッグ中は不変)、R-A22(ドロップ時の全体再ルーティング)。§6.5.7 のフローを書き換え、
resizeContainerToFit()とrerouteAll()の擬似実装を追加。関連するテストケース・リスク・用語集を整合化 - v0.7: 実際の出力 JSON の分析結果を反映し、6 つの新規要求を追加:
- R-A1 復活(縦線重複禁止、ELK の edgeEdge spacing 強化で構造的に回避)
- R-A14 復活(1-to-N ファンアウトのトランク化)
- R-A17(ノードサイズ最小保証、0×0 禁止、
computeNodeSize()ヘルパー導入) - R-A18(親コンテナのサイズ・ポート位置は ELK 自動算出、手動指定禁止)
- R-A19(トップ I/O 端子のポート side 明示)
- R-A20(ファンアウト起点のポート位置分散)
- 関連する実装方針・ELK 設定・テストケース・リスク表・用語集を整合化
- v0.6: R-A3 を厳格化(WEST/EASTのみ、各ポートに
elk.port.side明示)。R-A15(親コンテナのポート枠外回避)と R-A16(親→子のU字経路回避)を新規追加。buildElkChildren()を階層対応に拡張し、CONTAINER_LAYOUT_OPTIONS(elk.nodeSize.constraints+padding+minimum+hierarchyHandling)を導入。applyElkRoutingToLinks()と ELK 結果反映を再帰化して階層エッジの絶対座標化に対応 - v0.5: ルーティング方式を「JointJS manhattan router」から「ELK.js の orthogonal エッジ経路を JointJS の vertices として注入」へ変更。これにより線同士の重複を構造的に抑制。関連する設定値・コード例・用語集・リスク表を更新
- v0.4: ルーティング仕様を manhattan router 一本に最終集約。R-A1(縦線重複禁止)を削除し独自チャネル割当アルゴリズムを廃止。関連するリスク・テストケース・実装方針・コードコメントを整合化
- v0.3: ルーティングを manhattan router 一本化。R-A13(直線ショートカット)と R-A14(中央値トランク)を削除。リスク表のフォールバック記述(metro 代替・折れ線フォールバック)を削除
- v0.2: §6.5 レイアウト・ルーティング要求仕様(R-A1〜R-A14、F-17)を追加。関連箇所(機能一覧、ディレクトリ構成、フェーズ計画、リスク表)を整合化
- v0.1: 初版