PHY: レイアウトバリアントを木のノードとして導入し、ELC/IDXの扱いを決める

交換可能なキークラスタ(ISO Enter、スペース周りのキー数など)を、
VariantGroup / Variant の2ノードでPHYの木に表す。全バリアントのキーを
同時に持ち、単一の配置を要求する出力のときだけ1つ選んで畳む。

ELCは選択に関係なくsupersetを配線する。排他キー同士は同じ接続先を共有してよい。
IDXはキー番号の空間を1つに保ち、同じ接続先に配線された排他キーは
`order` の配列要素として同じ番号を共有する。

Journal: journal/2026-09-21-multi-layout.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPCpuPv6hDN76RgNe2PsRR
This commit is contained in:
2026-09-21 09:34:51 +09:00
co-authored by Claude Opus 5
parent 2eb2867c13
commit a175ee3c53
6 changed files with 296 additions and 23 deletions
+17
View File
@@ -61,6 +61,15 @@ per-key LEDを載せたキーか、LEDの部品ノードを指す。並びの位
現時点で定めるのはチェーン方式(WS2812系)のみとする。ドライバICでアドレスを与える方式は未定。
### レイアウトバリアント
PHYがレイアウトバリアントを持つ場合も、ELCは選択に関係なく全バリアントのキーを配線する。
基板上のフットプリントは選ばれなかった側も実在するため。
互いに排他なキーは、同じ接続先の組を共有してよい。排他でないキー同士が同じ接続先の組を持つのは
設計エラーとする。接点マトリクスなら、どちらが押されたかを区別できないため。
排他かどうかはPHYの木から判る。
### 他レイヤーへの参照
キーとLEDの参照には、PHYのノードIDをそのまま使う。ELCはIDXを参照しない。
@@ -144,3 +153,11 @@ type ELC = {
- 現状の認識: 接点マトリクスではダイオードの向きから、directでは電源レールの参照から、
どちらを駆動しどちらを検出するかが決まりそうに見えるが、全方式で導出できるか確かめていない
- 格上げ条件: 各方式について、端子の接続先とダイオードの向きだけから走査手順が一意に決まるかを確認する
### P-4 LEDチェーンがレイアウトバリアントをまたぐ場合
- 位置づけ: 本文「LEDの識別番号」がレイアウトバリアントを考慮していない
- 現状の認識: 選ばれなかったバリアントのキーにper-key LEDが載らないと、チェーンがそこで切れる。
基板側でバイパスする話であればBeyond KLEの範囲外だが、チェーンの並びをバリアントごとに分ける必要が
あるかもしれない
- 格上げ条件: レイアウトバリアントとper-key LEDを両方持つ実在のPCBで、チェーンがどう配線されているかを確認する
+27 -6
View File
@@ -49,6 +49,24 @@ row / col 自体がELCの正本ではなく出力時の派生情報である。
| ZMK | matrix transform の `map` の並び順 |
| KMK | `coord_mapping` の並び順 |
## レイアウトバリアント
PHYがレイアウトバリアントを持つ場合も、キー番号の空間は1つとする。バリアントごとにIDXを分けない。
キー番号は公開インターフェースであり、どの空間のものかを断らずに解釈できなければならない。
特定のレイアウト向けの並びは、選んだバリアントに属さないキーを落として得られる派生情報として扱う [推測:P-1]。
排他なキー同士は、同じキー番号を共有できる。`order` の要素にキーIDの配列を書くと、
その位置の番号を配列内のキーで共有する。
番号を共有できるのは、ELCで同じ接続先の組に配線されたキーに限る。
3つの処理系が要求するのはいずれも「番号の位置 → マトリクス位置」の1対1の対応表であり、
1つの番号に対応するマトリクス位置は1つしかないため。
逆に、同じ接続先の組に配線された排他キーは番号を共有しなければならない。
別々の番号を与えると、その対応表に同じマトリクス位置が2回現れ、1回のキー押下で両方の番号が反応する。
選択によって番号が詰まることはない。あるバリアントでしか実装されないキーの番号は、
別のバリアントを選んだときには使われない番号として残る。
## 他レイヤーとの関係
IDXはPHYのキーIDだけを参照する。ELCを参照しない。
@@ -71,7 +89,8 @@ IDXの `order` と突き合わせて検出できる。
```ts
type IDX = {
order: string[] // PHYのキーID。並びの位置がキー番号
order: (string | string[])[] // PHYのキーID。並びの位置がキー番号。
// 配列は、その位置の番号を共有する排他バリアントのキー
}
```
@@ -86,12 +105,14 @@ type IDX = {
- ELCのマトリクス順からキー番号を導出する (journal/2026-09-21-idx-necessity.md)
- `keyId` と `index` の対を並べる形 (journal/2026-09-21-idx-necessity.md)
- MAPがキー番号を参照する (journal/2026-09-21-idx-necessity.md)
- レイアウトごとに別のIDXを持つ (journal/2026-09-21-multi-layout.md)
## 未確認事項
### P-1 レイアウトバリアントの扱い
### P-1 特定レイアウト向けの並びが既存の慣習と一致するか
- 位置づけ: 未解決の問い。本文は単一レイアウトだけを前提にしている
- 現状の認識: QMKの `LAYOUT_60_ansi` / `LAYOUT_60_iso` のように、1つの基板が複数のキー番号空間を持つ場合がある。
1つのPHY+ELCに複数のIDXがぶら下がる形になりそうだが、PHYが代替キーをどう持つかが今のPHY仕様にない
- 格上げ条件: PHYの代替キーの表現とあわせて、マルチレイアウトの扱いをユーザーと決める
- 位置づけ: 本文「レイアウトバリアント」が前提にしている
- 現状の認識: `order` からキーを落として得た並びが、QMKのコミュニティレイアウト(`LAYOUT_60_ansi` など)が
要求する引数順と一致するかを確認していない。一致しないなら、レイアウトごとの並びを別に持つ必要が出る
- 格上げ条件: 実在のキーボードの `LAYOUT_all` とコミュニティレイアウトのマクロを読み、
前者から後者がキーの取捨だけで得られるかを確認する
+62 -5
View File
@@ -124,6 +124,41 @@ IDXはLEDの番号を持たない。RGB効果が必要とする位置と区分
- クラスター構造はBeyond KLEのデータにのみ存在する。外部形式へは階層を畳んだキー配置だけを出力する
### レイアウトバリアント
一部のキークラスタを別の構成と交換できるようにした基板がある。
`\ |` と `Enter` を ISO Enter と1Uキーに変える、スペースキー周りのキー数を変える、といったもの。
交換にはスイッチの差し替えが伴い、同時に成立するのは1つだけである。
PHYは全バリアントのキーを同時に持つ。基板上のフットプリントは選ばれなかった側も実在するため、
どれか1つに畳んだ状態を正本にできない。
排他関係は木構造で表す。バリアントグループ(VariantGroup)と、その子であるバリアント(Variant)の
2種類のノードを置く。Variantの配下にあるノードが、そのバリアントを選んだときに実装されるものになる。
- VariantGroupの子はVariantだけ。VariantはVariantGroupの直下にしか現れない
- 選択肢が3つ以上ある場合はVariantを3つ以上置く
- 配下にノードを持たないVariantを置いてよい(「エンコーダーを載せない」など)
- 部品ノードもVariantの配下に置ける。1Uキーとロータリーエンコーダーの選択がこれにあたる
- Variantの中に別のVariantGroupを置ける。あるバリアントを選んだときだけ現れる選択肢を表す
- 選択箇所が複数ある場合はVariantGroupを複数置く。同じ親の下に並べてよい
VariantGroupはクラスターと同じ変換を持つ。Variantは変換を持たない。
バリアントは同じ場所を占める代替物であり、選択によって配下の絶対配置が動く構造を作らないため。
1つのバリアントの中で回転中心が要る場合は、その中にクラスターを置く。
サイズや形が変わるキーは、同じキーを書き換えるのではなく、別のキーノードとして各Variantに置く。
ANSI EnterとISO Enterは別のIDを持つ2つのキーであり、同じ場所に重なる。
VariantGroupは既定のバリアントを1つ持つ。KLE JSONのように単一の配置を要求する出力は、
VariantGroupごとにバリアントを1つ選んでから階層を畳んで生成する。既定値はそのときの初期選択になる。
編集中にどのバリアントを表示しているかはエディタの状態であり、PHYには保存しない。
Variantをまたぐノードの移動は、クラスター間の移動と同じreparentとして扱う。絶対配置は変わらない。
組み替えの対象を同一の親に限る規則がそのまま効くため、複数のVariantにまたがる選択は組み替えできない。
選択はPHYを畳むときにだけ使う。他レイヤーはPHYのノードIDを参照するだけで、選択を見ない。
### クラスターの組み替え
クラスター構造は、配置を作ったあとから組み替える。KLE JSONから取り込んだ平坦なキー配置に
@@ -151,7 +186,7 @@ Transform は回転と平行移動だけを持つ剛体変換であり、合成
## 保存形式
各ノードは親から見た変換を持つ。キー・クラスター・部品ノードを混在させた木構造として保持する。
各ノードは親から見た変換を持つ。キー・クラスター・部品ノード・バリアントノードを混在させた木構造として保持する。
各ノードはIDを持つ。木の中のノードを一意に指すために持ち、キーのIDは他レイヤーからの参照にも使う。
```ts
@@ -186,12 +221,25 @@ type Component = {
transform: Transform,
partGroup?: string // `@` 始まりは描画用の予約名
}
type Node = Key | Cluster | Component | VariantGroup
type Cluster = {
id: string,
transform: Transform,
children: (Key | Cluster | Component)[]
children: Node[]
}
const root: (Key | Cluster | Component)[] = []
type VariantGroup = {
id: string,
name: string, // UI上の選択肢の見出し
transform: Transform,
default: string, // 既定のVariantのid
children: Variant[] // 2つ以上。並びが表示順
}
type Variant = {
id: string,
name: string,
children: Node[] // 空でもよい
}
const root: Node[] = []
```
## 連携用出力形式
@@ -245,6 +293,7 @@ const kleData = (ConfigObject | KeyLabel)[][]
- 回転角度は正規化せずそのまま出力する。KLEのデータは任意の角度を受け付け、15度刻みはKLEのUI上の操作単位にすぎない
- rx, ry はPHYに持たないので、出力時に選ぶ
- ユニットサイズ(mm)、ビジュアルオフセット、クラスター構造、部品ノード、per-key LEDはKLE JSONに出力されない
- バリアント構造はKLE JSONに出力しない。VariantGroupごとにバリアントを1つ選び、その配下だけを畳んで出力する
- 部品グループ・スイッチの向きはKLE JSONに出力しない。sm, sb, st には取り込み時に保持した値をそのまま書き戻す
- KLEの `p` はキーキャップのプロファイル名(SA、DCSなど)であり、PHYのプロファイルとは対応させない
@@ -253,7 +302,8 @@ const kleData = (ConfigObject | KeyLabel)[][]
既存のPHYデータを畳んだ配置に対して、KLE JSONの各キーを形状(位置・サイズ・回転)とラベルで照合する。
- 照合できたキーは元のノードとして扱う。KLE側で配置が変わっていれば、元の親から見た相対座標に戻して反映する
- 照合できないキーは root 直下に新規キーとして置く。クラスターへの割り当てはユーザーが行う
- 照合できないキーは root 直下に新規キーとして置く。クラスターとバリアントへの割り当てはユーザーが行う
- 照合の対象は、出力時に選んだバリアントの配下にあるキーに限る。選ばれなかったバリアントのキーは照合にも削除にも関与しない
- 構造の復元は保証しない。KLE側での編集が大きいほど、クラスター構造は失われる
- 配列の順番は照合に使わない。KLEは保存時にキーを「角度 → rx → ry → y → x」の順に並べ替えるため、順番は形状から決まる以上の情報を持たない
@@ -266,11 +316,11 @@ Beyond KLEはスイッチの品種を持たないため、対応づける先が
## 検討済み・非採用
- スイッチのブランドをPHYで持つ (journal/2026-09-20-switch-terminal.md)
- スイッチの品種・ソケット方式をPHYで持つ (journal/2026-09-20-switch-terminal-reversal.md)
- LED専用ノード (journal/2026-09-20-led-node.md)
- per-key LEDに座標を持たせる (journal/2026-09-20-led-node.md)
- KLE取り込み時のクラスター自動推定 (journal/2026-09-20-cluster-manual-grouping.md)
- レイアウトバリアントをキーごとのタグとファイル単位の宣言で持つ (journal/2026-09-21-multi-layout.md)
## 未確認事項
@@ -317,3 +367,10 @@ Beyond KLEはスイッチの品種を持たないため、対応づける先が
- 位置づけ: 未解決の問い。数値入力以外に、原点をどこへ吸着させるか
- 現状の認識: キーのローカル原点(左上角)・キー中心・境界矩形の角と中心が候補。相対グリッドとは別の機構になる
- 格上げ条件: 原点移動を実装し、吸着なしで足りるか、足りない場合どの候補が要るかを確認する
### P-15 複数のバリアントで共通のキーをどう置くか
- 位置づけ: 未解決の問い。本文「レイアウトバリアント」は1つのキーが1つのVariantにしか属さない形になっている
- 現状の認識: 3択のうち2つで存在し1つでは存在しないキーは、今の木では表せない。同じキーを2つのVariantに
複製すると別IDになり、ELC・IDX・MAPの記述が二重になる。そもそもそういう構成が実在するかを確認していない
- 格上げ条件: 選択肢をまたいで共通のキーが要る実在の基板を探す。あれば表現方法を決める
+64
View File
@@ -0,0 +1,64 @@
# マルチレイアウト(交換可能なキークラスタ)の扱い
## 結論
- PHYは全バリアントのキーを同時に持つ(superset)。どれか1つに畳んだ状態を正本にしない
- 排他関係は木で表す。VariantGroup / Variant の2ノードを追加する
- ELCは変更なし。supersetをそのまま配線する。排他キー同士は同じ接続先を共有してよい
- IDXの番号空間は1つ。排他キー同士は同じ番号を共有できるので `order` の要素にキーIDの配列を許す
- MAPは変更なし
## PHYでの表現
| 案 | 形 | 判断 |
| --- | --- | --- |
| 木にバリアントノード | VariantGroup > Variant > ノード | 採用 |
| キーにタグ + ファイル単位で選択肢を宣言 | `variant?: {option, choice}` + `layoutOptions[]` | 不採用 |
| 別レイヤーに切り出す | PHYはsupersetだけ持ち、選択箇所は新レイヤー | 不採用 |
タグ案の利点は、reparentでキーを出し入れしても所属が変わらないこと。
木案はそこが弱いが、次の点で勝る。
- 条件付きの選択肢(あるバリアントを選んだときだけ現れる選択肢)が入れ子で自然に出る。
タグ案では選択肢の依存関係を別に持つ必要がある
- 「どのキーがどの選択肢に属するか」を木の上で直接見られる。タグ案は全ノードを走査しないと分からない
- 空のVariant(「エンコーダーを載せない」)が、子のいないノードとしてそのまま置ける。
タグ案ではファイル単位の宣言が必須になる(キーのタグから選択肢を復元できないため)
別レイヤー案は、選択箇所が「どのスイッチを実装するか」という物理の話であり、
PHY以外の3レイヤーがすべてPHYのノードIDを参照する構造からも外れるので落とした。
## VariantがTransformを持たない理由
バリアントは同じ場所を占める代替物なので、選択によって配下の絶対配置が動く構造にしたくない。
1つのバリアントの中で回転中心が要る場合は、その中にクラスターを置けば足りる。
VariantGroup側はクラスターと同じくTransformを持つ。
## IDXの番号共有
ELCで同じマトリクス交点に配線された排他キーは、同じキー番号を共有しなければならない。
出力先3つ(QMK `LAYOUT` マクロ / ZMK matrix transform の `map` / KMK `coord_mapping`)は、
どれも「番号の位置 → マトリクス位置」の1対1の対応表である。
同じ交点に別々の番号を与えると、その表に同じ交点が2回現れることになる。
逆に、別々の交点のキーに同じ番号を与えることもできない。
よって `order` の配列要素は「同じ交点に配線された排他キーの集合」と一致する。
### レイアウトごとに別のIDXを持つ案
不採用。キー番号が公開インターフェースであることと相性が悪い。
番号空間が複数あると、キーマップがどの空間のものかを指定しないと解釈できない。
また選択箇所が3つあれば組み合わせの数だけ空間が要る。
QMKの `LAYOUT_60_ansi` のような特定レイアウト向けの並びは、
supersetの並びから選択外のキーを落とした派生情報として出す。
ただし既存のコミュニティレイアウトの引数順と一致するかは未確認(IDXレイヤー.md P-1)。
## 捨てた検討
- キーごとに `variants: string[]` を持たせて複数の選択肢にまたがらせる形。
そもそも複数選択肢で共通のキーが実在するかを確認できていないので、必要になってから決める
(PHYレイヤー.md P-15)
- 選択状態(どのバリアントを選んでいるか)をPHYとは別ファイルに持つ案。
既定のバリアントをVariantGroupに持たせれば足りる。編集中の表示切り替えはエディタの状態
+3 -1
View File
@@ -24,7 +24,8 @@ ELCはIDXを参照せず、IDXもELCを参照しない。MAPもIDXを参照し
- **役割**: キーと部品の物理的な位置、サイズ、回転
- **対応ツール**: PCBエディタ、KLE
- **出力**: 相対座標の木(キー・クラスター・部品ノード)。各ノードはIDを持つ
- **出力**: 相対座標の木(キー・クラスター・部品ノード・バリアントノード)。各ノードはIDを持つ
- **備考**: 交換可能なレイアウト(ISO Enter等)は全パターンを同時に持ち、外部出力時に1つ選んで畳む
### 2. ELCレイヤー (電気配線)
@@ -44,6 +45,7 @@ ELCはIDXを参照せず、IDXもELCを参照しない。MAPもIDXを参照し
- **備考**: キー番号はPHYの幾何からもELCの配線からも導出しない(導出すると、幾何の編集や再配線でキーマップがずれる)
- **備考**: キー番号は公開インターフェースであり、出力のたびに生成し直さず固定する
- **備考**: マトリックス座標とLEDの識別番号はIDXが持たない(どちらも配線で決まるためELCが持つ)
- **備考**: レイアウトバリアントがあってもキー番号の空間は1つ。同じマトリクス位置に配線された排他キーは同じ番号を共有する
- **備考**: 既存のエコシステムでは、この並びが `LAYOUT` マクロの引数順などとして手書きされている。欠けている層ではなく、形式化されていない層
### 4. MAPレイヤー (キーマップ)
+123 -11
View File
@@ -61,7 +61,8 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
"oneOf": [
{ "$ref": "#/$defs/key" },
{ "$ref": "#/$defs/cluster" },
{ "$ref": "#/$defs/component" }
{ "$ref": "#/$defs/component" },
{ "$ref": "#/$defs/variantGroup" }
]
}
}
@@ -186,6 +187,58 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
}
}
},
"variantGroup": {
"type": "object",
"description": "Mutually exclusive layout alternatives. Children are variants only",
"required": ["type", "id", "name", "transform", "default", "children"],
"properties": {
"type": {
"type": "string",
"const": "variantGroup"
},
"id": { "type": "string" },
"name": {
"type": "string",
"description": "Heading shown for the choice in the editor"
},
"transform": { "$ref": "#/$defs/transform" },
"default": {
"type": "string",
"description": "id of the variant used when a single layout must be chosen"
},
"children": {
"type": "array",
"minItems": 2,
"description": "Display order",
"items": { "$ref": "#/$defs/variant" }
}
}
},
"variant": {
"type": "object",
"description": "One alternative. Appears only directly under a variantGroup. Carries no transform",
"required": ["type", "id", "name", "children"],
"properties": {
"type": {
"type": "string",
"const": "variant"
},
"id": { "type": "string" },
"name": { "type": "string" },
"children": {
"type": "array",
"description": "May be empty",
"items": {
"oneOf": [
{ "$ref": "#/$defs/key" },
{ "$ref": "#/$defs/cluster" },
{ "$ref": "#/$defs/component" },
{ "$ref": "#/$defs/variantGroup" }
]
}
}
}
},
"cluster": {
"type": "object",
"required": ["type", "id", "transform", "children"],
@@ -203,7 +256,8 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
"oneOf": [
{ "$ref": "#/$defs/key" },
{ "$ref": "#/$defs/cluster" },
{ "$ref": "#/$defs/component" }
{ "$ref": "#/$defs/component" },
{ "$ref": "#/$defs/variantGroup" }
]
}
}
@@ -296,13 +350,60 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
]
},
{
"type": "key",
"id": "k_iso_enter",
"profile": "standard",
"transform": { "x": 6, "y": 1, "rotation": 0 },
"size": { "w": 1.5, "h": 1, "x2": 0.25, "y2": 1, "w2": 1.25, "h2": 1 },
"switchOffset": { "x": 0, "y": 0.5 },
"labels": ["Enter"]
"type": "variantGroup",
"id": "vg_enter",
"name": "Enter",
"transform": { "x": 6, "y": 0, "rotation": 0 },
"default": "v_ansi",
"children": [
{
"type": "variant",
"id": "v_ansi",
"name": "ANSI",
"children": [
{
"type": "key",
"id": "k_ansi_bslash",
"profile": "standard",
"transform": { "x": 1, "y": 0, "rotation": 0 },
"size": { "w": 1.5, "h": 1 },
"labels": ["|", "\\"]
},
{
"type": "key",
"id": "k_ansi_enter",
"profile": "standard",
"transform": { "x": 0.25, "y": 1, "rotation": 0 },
"size": { "w": 2.25, "h": 1 },
"labels": ["Enter"]
}
]
},
{
"type": "variant",
"id": "v_iso",
"name": "ISO",
"children": [
{
"type": "key",
"id": "k_iso_enter",
"profile": "standard",
"transform": { "x": 1, "y": 0, "rotation": 0 },
"size": { "w": 1.5, "h": 1, "x2": 0.25, "y2": 1, "w2": 1.25, "h2": 1 },
"switchOffset": { "x": 0, "y": 0.5 },
"labels": ["Enter"]
},
{
"type": "key",
"id": "k_iso_hash",
"profile": "standard",
"transform": { "x": 0.25, "y": 1, "rotation": 0 },
"size": { "w": 1, "h": 1 },
"labels": ["~", "#"]
}
]
}
]
},
{
"type": "component",
@@ -345,8 +446,19 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
"type": "array",
"uniqueItems": true,
"items": {
"type": "string",
"description": "Reference to PHY layer key ID"
"oneOf": [
{
"type": "string",
"description": "Reference to PHY layer key ID"
},
{
"type": "array",
"minItems": 2,
"uniqueItems": true,
"items": { "type": "string" },
"description": "Mutually exclusive variant keys sharing this key number. They must be wired to the same terminals in ELC"
}
]
}
}
}