Compare commits

...
10 Commits
Author SHA1 Message Date
ewdernaandClaude Opus 5 c31360a772 phy: 行の先頭以外の r, rx, ry を取り込みで受け入れる(P-16解消)
KLE本家は拒否するが、VIAとRemapは読む。Remapで扱える基板を取り込めない
状態にしないため、位置を問わず受け入れる。本家のコードから行頭チェックを
外した挙動をそのまま採る。出力は従来どおり行頭にのみ置く。

Journal: journal/2026-09-21-kle-lenient-row.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSKcJLPAaPLchG4LXxk6cY
2026-09-21 21:57:45 +09:00
ewdernaandClaude Opus 5 6d0eba9346 phy: 複数のバリアントで成立するキーはVariantごとに複製して置く
隣り合うキャップが同じフットプリントを食い合う基板(MACRO1)があり、選択を独立な
VariantGroupに必ず分解できるわけではない。分解を強いると、キャップが重なる組み合わせを
作れてしまう。複製してもELCは接続先を、IDXは番号を共有できるので他レイヤーは壊れない。
どのキーノードが同じ物理キーかはBeyond KLEが判定せず、設計者の情報として扱う。

前のコミットで「独立したVariantGroupに分けて表す」と書いたが、MACRO1は下段を含めると
分解できない。あわせて訂正する。

Journal: journal/2026-09-21-variant-shared-key.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01246a5KFLmwcRXriZvX8fdJ
2026-09-21 21:34:10 +09:00
ewdernaandClaude Opus 5 8e7d21534d phy: 未確認事項のP-16を番号順の位置へ移す
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01246a5KFLmwcRXriZvX8fdJ
2026-09-21 19:01:27 +09:00
ewdernaandClaude Opus 5 e1aee6c92c elc: LEDチェーンをバリアントごとに分けないことを確定する
QMKの rgb_matrix はボードに1つで、レイアウトごとに分かれない。
バリアント固有の位置にLEDが載る基板も載らない基板も実在するが、どちらもチェーンは1本で、
選択によって並びが変わらない。載らないキーはPHYのper-key LEDの有無でそのまま表せる。

Journal: journal/2026-09-21-led-chain-variant.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01246a5KFLmwcRXriZvX8fdJ
2026-09-21 19:01:00 +09:00
ewdernaandClaude Opus 5 d7a0f99cb4 idx: コミュニティレイアウトの並びが取捨だけでは得られない場合を記録する
QMKの実データで、コミュニティレイアウトとの173組のうち30組(すべてISO系)が
`LAYOUT_all` の並びの部分列にならない。同じマトリクス位置がレイアウトによって
別の物理キーになり、残ったキーの読み順が変わるため。
埋め方は未定として未確認事項に残す。

Journal: journal/2026-09-21-community-layout-order.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01246a5KFLmwcRXriZvX8fdJ
2026-09-21 19:01:00 +09:00
ewdernaandClaude Opus 5 ea7bf01e72 phy: 選択肢をまたぐ共通キーは独立したVariantGroupに分けて表す
VIAの定義824件を調べ、複数の選択肢で成立するキーを持つ基板を2件見つけた(LW67、MACRO1)。
どちらも独立した2つの選択を1つの選択肢リストに畳んだもので、VariantGroupを2つ置けば
キーを複製せずに表せる。残るのは、積のうち一部しか提供しない基板の扱いだけになった。

Journal: journal/2026-09-21-variant-shared-key.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01246a5KFLmwcRXriZvX8fdJ
2026-09-21 19:00:46 +09:00
ewdernaandClaude Opus 5 12a47fe822 phy: 公開KLE JSON 1505件で変換可否を確かめ、書き戻しの制約を加える
VIAの定義とKLE本家のプリセット・サンプルを集め、本家の serial.js を通して往復させた。
幾何は全件無損失で、PHYで表せないKLEの幾何情報は無かった。
KLEは p, sm, sb, st を空文字に戻す指定を無視するため、混在する配置は書き戻せない。
本家が読めないJSONも実在するので、取り込みの寛容さを問いとして残す。

Journal: journal/2026-09-21-kle-corpus.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01246a5KFLmwcRXriZvX8fdJ
2026-09-21 19:00:32 +09:00
ewdernaandClaude Opus 5 ebe1434860 phy: KLE出力の rx, ry をクラスター原点で共有し、座標を丸める
KLEが1行にまとめるのは r, rx, ry が一致し y が厳密に等しいキーだけなので、
同じ絶対角度のキーには共通の軸を与え、絶対座標への変換で入った誤差は丸めて落とす。
ErgoDox・Kinesis Advantage で元のKLE JSONとバイト単位で一致する出力になった。

Journal: journal/2026-09-21-kle-output-rowgroup.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01246a5KFLmwcRXriZvX8fdJ
2026-09-21 19:00:17 +09:00
ewdernaandClaude Opus 5 69d9c73fff phy: プロファイルをファイル単位の器に置き、キーはIDで参照する
保存形式に PhyFile を書き、profiles を木の外に出した。同じ定義を複数のキーに
複製しないため、Key.profile は実体ではなくIDの参照にする。
ビジュアル表現は上面矩形の4辺インセット + R とし、非対称な上面を表せる形にした。
プロファイル内の距離はすべてmmに統一。

Journal: journal/2026-09-21-profile-container.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01246a5KFLmwcRXriZvX8fdJ
2026-09-21 18:58:38 +09:00
ewdernaandClaude Opus 5 a175ee3c53 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
2026-09-21 09:34:51 +09:00
13 changed files with 1124 additions and 38 deletions
+13
View File
@@ -61,6 +61,19 @@ per-key LEDを載せたキーか、LEDの部品ノードを指す。並びの位
現時点で定めるのはチェーン方式(WS2812系)のみとする。ドライバICでアドレスを与える方式は未定。
### レイアウトバリアント
PHYがレイアウトバリアントを持つ場合も、ELCは選択に関係なく全バリアントのキーを配線する。
基板上のフットプリントは選ばれなかった側も実在するため。
互いに排他なキーは、同じ接続先の組を共有してよい。排他でないキー同士が同じ接続先の組を持つのは
設計エラーとする。接点マトリクスなら、どちらが押されたかを区別できないため。
排他かどうかはPHYの木から判る。
LEDチェーンもバリアントごとに分けない。排他なキー位置の両方にLEDが載る基板も、
バリアント固有のキーにLEDが載らない基板もあるが、どちらもチェーンは1本で、選択によって並びが変わらない。
載らないキーは、PHYがキーごとに持つper-key LEDの有無でそのまま表せる。
### 他レイヤーへの参照
キーとLEDの参照には、PHYのノードIDをそのまま使う。ELCはIDXを参照しない。
+30 -6
View File
@@ -49,6 +49,27 @@ row / col 自体がELCの正本ではなく出力時の派生情報である。
| ZMK | matrix transform の `map` の並び順 |
| KMK | `coord_mapping` の並び順 |
## レイアウトバリアント
PHYがレイアウトバリアントを持つ場合も、キー番号の空間は1つとする。バリアントごとにIDXを分けない。
キー番号は公開インターフェースであり、どの空間のものかを断らずに解釈できなければならない。
特定のレイアウト向けの並びは、選んだバリアントに属さないキーを落として得る。
ただしこれだけでは足りない場合がある。QMKのコミュニティレイアウトでは、同じマトリクス位置が
レイアウトによって別の物理キーになり(ANSIのバックスラッシュとISOの `#~`)、残ったキーの読み順が変わる。
ISO系で引数順が一致しない。
排他なキー同士は、同じキー番号を共有できる。`order` の要素にキーIDの配列を書くと、
その位置の番号を配列内のキーで共有する。
番号を共有できるのは、ELCで同じ接続先の組に配線されたキーに限る。
3つの処理系が要求するのはいずれも「番号の位置 → マトリクス位置」の1対1の対応表であり、
1つの番号に対応するマトリクス位置は1つしかないため。
逆に、同じ接続先の組に配線された排他キーは番号を共有しなければならない。
別々の番号を与えると、その対応表に同じマトリクス位置が2回現れ、1回のキー押下で両方の番号が反応する。
選択によって番号が詰まることはない。あるバリアントでしか実装されないキーの番号は、
別のバリアントを選んだときには使われない番号として残る。
## 他レイヤーとの関係
IDXはPHYのキーIDだけを参照する。ELCを参照しない。
@@ -71,7 +92,8 @@ IDXの `order` と突き合わせて検出できる。
```ts
type IDX = {
order: string[] // PHYのキーID。並びの位置がキー番号
order: (string | string[])[] // PHYのキーID。並びの位置がキー番号。
// 配列は、その位置の番号を共有する排他バリアントのキー
}
```
@@ -86,12 +108,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の代替キーの表現とあわせて、マルチレイアウトの扱いをユーザーと決める
- 位置づけ: 本文「レイアウトバリアント」の残余。一致しない場合があることは確認済み
- 現状の認識: QMKの実データで、コミュニティレイアウトとの173組のうち30組(すべてISO系)が
`order` からの取捨では得られない。並べ替えの規則を与えるか、レイアウトごとの並びを持つかが未定。
後者は「レイアウトごとに別のIDXを持つ」として一度落としている
- 格上げ条件: 埋め方をユーザーが決める
+106 -17
View File
@@ -16,9 +16,16 @@ Stepped Caps Lockのようにスイッチがキャップ中心からずれるキ
### プロファイル設定
1ファイル内に複数のプロファイルを持たせることができる。
ユニットサイズ(1Uが何mmか、x軸・y軸それぞれ)を規定。
ビジュアル表現として、内側へのオフセット位置及びオフセット距離(mm or U単位)・R(U単位)を規定。
1ファイル内に複数のプロファイルを持たせることができる。プロファイルはノードではなくファイル単位に置き、
キーはIDで参照する。同じ定義を複数のキーに複製しないため、実体ではなく参照とする。
プロファイルが規定するのは次の2つ。
- ユニットサイズ: 1Uが何mmか。x軸・y軸それぞれ
- ビジュアル表現: キー上面の矩形を、外形の各辺(上下左右)から内側への距離と、角のRで表す
上面の距離を4辺それぞれ持つのは、上面が外形の中心からずれるプロファイルを表せるようにするため。
プロファイル内の距離はすべてmmで持つ。実寸を規定する定義なので、U単位の値を混ぜない。
### 座標と回転
@@ -124,6 +131,47 @@ 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を分けて選択肢の数を減らしてよい。
ANSI/ISOとBackspaceの分割のように、独立な2つの選択が1つの選択肢リストに畳まれている基板がある
VariantGroupはクラスターと同じ変換を持つ。Variantは変換を持たない。
バリアントは同じ場所を占める代替物であり、選択によって配下の絶対配置が動く構造を作らないため。
1つのバリアントの中で回転中心が要る場合は、その中にクラスターを置く。
サイズや形が変わるキーは、同じキーを書き換えるのではなく、別のキーノードとして各Variantに置く。
ANSI EnterとISO Enterは別のIDを持つ2つのキーであり、同じ場所に重なる。
同じ位置・同じサイズのキーが複数のバリアントで成立する場合も同じで、Variantごとに別のキーノードを置く。
隣り合うキャップが同じフットプリントを食い合う基板があり、選択を分解できるとは限らないため。
どのキーノードが同じ物理キーを指すかはBeyond KLEが判定しない。設計者が知っている情報として扱う。
VariantGroupは既定のバリアントを1つ持つ。KLE JSONのように単一の配置を要求する出力は、
VariantGroupごとにバリアントを1つ選んでから階層を畳んで生成する。既定値はそのときの初期選択になる。
編集中にどのバリアントを表示しているかはエディタの状態であり、PHYには保存しない。
Variantをまたぐノードの移動は、クラスター間の移動と同じreparentとして扱う。絶対配置は変わらない。
組み替えの対象を同一の親に限る規則がそのまま効くため、複数のVariantにまたがる選択は組み替えできない。
選択はPHYを畳むときにだけ使う。他レイヤーはPHYのノードIDを参照するだけで、選択を見ない。
### クラスターの組み替え
クラスター構造は、配置を作ったあとから組み替える。KLE JSONから取り込んだ平坦なキー配置に
@@ -151,10 +199,23 @@ Transform は回転と平行移動だけを持つ剛体変換であり、合成
## 保存形式
各ノードは親から見た変換を持つ。キー・クラスター・部品ノードを混在させた木構造として保持する。
各ノードは親から見た変換を持つ。キー・クラスター・部品ノード・バリアントノードを混在させた木構造として保持する。
各ノードはIDを持つ。木の中のノードを一意に指すために持ち、キーのIDは他レイヤーからの参照にも使う。
プロファイルは木の外に置き、ファイル単位の配列として持つ。
```ts
type Profile = {
id: string,
name: string,
unitSize: { x: number, y: number }, // 1Uのmm数
visualOffset?: { // キー上面。単位はmm
top: number, // 外形の各辺から内側への距離
right: number,
bottom: number,
left: number,
radius: number
}
}
type Transform = {
x: number, // 親座標系での位置
y: number,
@@ -172,7 +233,7 @@ type Key = {
id: string,
transform: Transform,
size: Size,
profile: Profile,
profile: string, // Profile.id を参照
decal?: boolean,
ghost?: boolean,
labels?: string[], // 参考情報
@@ -186,12 +247,36 @@ type Component = {
transform: Transform,
partGroup?: string // `@` 始まりは描画用の予約名
}
type Node = Key | Cluster | Component | VariantGroup
type Cluster = {
id: string,
transform: Transform,
children: (Key | Cluster | Component)[]
children: Node[]
}
type VariantGroup = {
id: string,
name: string, // UI上の選択肢の見出し
transform: Transform,
default: string, // 既定のVariantのid
children: Variant[] // 2つ以上。並びが表示順
}
type Variant = {
id: string,
name: string,
children: Node[] // 空でもよい
}
type PhyFile = {
version: string,
metadata: {
name?: string,
author?: string,
description?: string,
created?: string,
modified?: string
},
profiles: Profile[], // 1つ以上
root: Node[]
}
const root: (Key | Cluster | Component)[] = []
```
## 連携用出力形式
@@ -243,9 +328,15 @@ const kleData = (ConfigObject | KeyLabel)[][]
```
- 回転角度は正規化せずそのまま出力する。KLEのデータは任意の角度を受け付け、15度刻みはKLEのUI上の操作単位にすぎない
- rx, ry はPHYに持たないので、出力時に選ぶ
- rx, ry はPHYに持たないので、出力時に選ぶ。同じ絶対角度を持つキーには共通の値を与える
(クラスターの原点が使える)。KLEが1行にまとめるのは r, rx, ry が一致し y が等しいキーだけという制約による
- 書き出す x, y は丸める。行のまとまりは y の厳密一致で決まるので、絶対座標への変換で入った誤差が残ると行が割れる
- ユニットサイズ(mm)、ビジュアルオフセット、クラスター構造、部品ノード、per-key LEDはKLE JSONに出力されない
- バリアント構造はKLE JSONに出力しない。VariantGroupごとにバリアントを1つ選び、その配下だけを畳んで出力する
- 部品グループ・スイッチの向きはKLE JSONに出力しない。sm, sb, st には取り込み時に保持した値をそのまま書き戻す
- r, rx, ry は行の先頭にのみ置く。KLE本家はそれ以外の位置に置かれたものを読まない
- p, sm, sb, st は空文字に戻す指定がKLE側で無視される。値を持つキーと持たないキーが混在する配置は、
KLEで開いた時点で後続のキーに前の値が残る
- KLEの `p` はキーキャップのプロファイル名(SA、DCSなど)であり、PHYのプロファイルとは対応させない
### KLE JSONの取り込み
@@ -253,10 +344,15 @@ const kleData = (ConfigObject | KeyLabel)[][]
既存のPHYデータを畳んだ配置に対して、KLE JSONの各キーを形状(位置・サイズ・回転)とラベルで照合する。
- 照合できたキーは元のノードとして扱う。KLE側で配置が変わっていれば、元の親から見た相対座標に戻して反映する
- 照合できないキーは root 直下に新規キーとして置く。クラスターへの割り当てはユーザーが行う
- 照合できないキーは root 直下に新規キーとして置く。クラスターとバリアントへの割り当てはユーザーが行う
- 照合の対象は、出力時に選んだバリアントの配下にあるキーに限る。選ばれなかったバリアントのキーは照合にも削除にも関与しない
- 構造の復元は保証しない。KLE側での編集が大きいほど、クラスター構造は失われる
- 配列の順番は照合に使わない。KLEは保存時にキーを「角度 → rx → ry → y → x」の順に並べ替えるため、順番は形状から決まる以上の情報を持たない
r, rx, ry は行の先頭以外に置かれていても受け入れ、そのキー以降に適用する。
rx, ry を持つキーでは、座標を (rx, ry) に戻してから x, y を加える。
KLE本家はこの位置の r, rx, ry をエラーにするが、VIAとRemapは読む。Remapで扱える基板を取り込めない状態にしない。
KLEの rx, ry, r はキーの絶対配置 (x, y, rotation) に変換して取り込む。元の rx, ry は保持しない。
見た目の属性(c, t, p, a, f, f2, fa, l, n)は、KLE JSONへ書き戻せるように保持する。どの層で持つかは決まっていない。
@@ -266,21 +362,14 @@ 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)
## 未確認事項
### P-1 KLE JSONへ書き出した配置が行のまとまりを保てるか
- 位置づけ: 本文が前提にしているものではない。連携用出力形式の設計に関わる未解決の問い
- 現状の認識: 階層を畳んでKLEに読み込ませると行のまとまりが崩れる。他レイヤーの情報を使ってでも行ごとにまとめたいが、方法は未検討
- 手がかり: KLEは r, rx, ry が同じで y が等しいキーを1行にまとめる。同じ絶対角度を持つキーに共通の rx, ry(例えばクラスターの原点)を選べば、行にまとめられる見込み
- 格上げ条件: 回転を含むPHYデータをKLE JSONに変換してKLE本家に読み込ませ、rotation groupと行配列がどう再構成されるかを確認する
### P-2 KLE JSON取り込み時の照合基準
- 位置づけ: 本文「KLE JSONの取り込み」の具体化に必要な未解決の問い
@@ -0,0 +1,111 @@
# コミュニティレイアウトの引数順がキーの取捨だけで得られるか
IDX P-1 の検証。
## 結論
得られない場合がある。ISO系のコミュニティレイアウトで、`LAYOUT_all` の並びからキーを落としただけでは
引数順が合わない。QMKの実データで 173組中30組。全部ISO系だった。
原因は、同じマトリクス位置が、レイアウトによって別の物理キーになるため。
| レイアウト | matrix (2,12) の位置とサイズ |
| --- | --- |
| `LAYOUT_all` | (13.5, 1) 1.5U — ANSIのバックスラッシュ |
| `LAYOUT_60_iso` | (12.75, 2) 1U — ISOの `#~` |
キーが物理的に別の場所へ移るので、読み順での位置も変わる。`LAYOUT_all` では2行目に、
ISOでは3行目に現れる。取捨では並べ替えられない。
PHYの用語では、この2つは別のキーノードである(サイズや形が変わるキーは別のIDを持つ)。
「選んだバリアントに属さないキーを落とす」だけでは、残ったキーの並びが変わらないので、
コミュニティレイアウトが要求する順にならない。
## 調べ方
QMKのキーボード一覧3759件から400件を無作為抽出し、`keyboards.qmk.fm` の info.json を取得した。
`LAYOUT_all`(無ければ `LAYOUT`)を基準に、同じボードの他のレイアウトと比較した。
| 対象 | 件数 |
| --- | --- |
| 取得したボード | 400 |
| 複数のレイアウトを持つボード | 146 |
| community_layouts に載っているレイアウトとの組 | 173 |
| うち基準の並びの部分列になっている | 143 |
| うち部分列にならない | 30 (すべてISO系) |
| うち同じマトリクス位置で形状が違うキーを含む | 172 |
部分列にならない例: alf/dc60, argyle, artifact/stratum, cannonkeys/brutalv2_65, era/linx3/n87,
kapcave/gskt00, karlb/kbic65, kbdfans/kbd67/rev2, lucid/phantom_solder, phdesign/ph60/multi,
rmi_kb/tkl_ff/v2 の各ISOレイアウト。
era/linx3/n87 は `LAYOUT_tkl_ansi` に `LAYOUT_all` へ無いマトリクス位置が1つある。
QMKの `LAYOUT_all` は全バリアントの重ね合わせではなく、それ自体が1つの選択でしかない
(1つのマトリクス位置を2回書けないため)。PHYが全バリアントを同時に持つのとは別物である。
## 172/173 が「形状違い」を含むことについて
ANSI系のレイアウトでも、Backspaceが2Uか1U×2か、右Shiftの幅、Tsanganの下段など、
同じマトリクス位置のキーの形が変わる組がほとんどである。これらは並び順は保たれるが、
PHY上は別のキーノードになる。IDXが「キーIDの並び」で番号を持つ以上、
落とすだけでなく差し替えが要ることは変わらない。
## 検証スクリプト
```py
# QMKのinfo.jsonを無作為抽出で取得する。qmk_list.json は
# curl -o qmk_list.json https://keyboards.qmk.fm/v1/keyboard_list.json
import json, random, urllib.request, concurrent.futures, os
kbs=json.load(open('qmk_list.json'))['keyboards']
random.seed(42)
os.makedirs('qmk', exist_ok=True)
def fetch(kb):
p='qmk/'+kb.replace('/','__')+'.json'
if os.path.exists(p): return True
try:
with urllib.request.urlopen(f'https://keyboards.qmk.fm/v1/keyboards/{kb}/info.json', timeout=30) as r:
open(p,'wb').write(r.read())
return True
except Exception as e:
return str(e)[:60]
with concurrent.futures.ThreadPoolExecutor(8) as ex:
res=list(ex.map(fetch, random.sample(kbs, 400)))
print('fetched', sum(1 for r in res if r is True), 'failed', sum(1 for r in res if r is not True))
```
```py
# LAYOUT_all の並びから、他のレイアウトがキーの取捨だけで得られるか
import json, glob, collections
st=collections.Counter(); rows=[]
for f in glob.glob('qmk/*.json'):
d=json.load(open(f,encoding='utf-8'))
for kb,v in (d.get('keyboards') or {}).items():
lays=v.get('layouts') or {}
if not lays: continue
st['boards']+=1
if len(lays)>1: st['multi-layout boards']+=1
base=next((n for n in ('LAYOUT_all','LAYOUT') if n in lays), None)
if base is None or len(lays)<2: continue
shape=lambda k:(k.get('x'),k.get('y'),k.get('w',1),k.get('h',1),k.get('r',0))
shA={}
for k in lays[base]['layout']: shA.setdefault(tuple(k['matrix']),[]).append(shape(k))
A=[tuple(k['matrix']) for k in lays[base]['layout']]
for name,l in lays.items():
if name==base: continue
B=[tuple(k['matrix']) for k in l['layout']]
it=iter(A); subseq=all(any(a==b for a in it) for b in B) # 部分列か
rows.append(dict(kb=kb, name=name, subseq=subseq,
missing=sum(1 for m in B if m not in set(A)),
shapediff=sum(1 for k in l['layout']
if tuple(k['matrix']) in shA and shape(k) not in shA[tuple(k['matrix'])]),
community=name.replace('LAYOUT_','') in (v.get('community_layouts') or [])))
print(st)
comm=[r for r in rows if r['community']]
print('community layout pairs:', len(comm))
print(' 部分列になる:', sum(1 for r in comm if r['subseq']))
print(' 部分列にならない:', sum(1 for r in comm if not r['subseq']))
print(' baseに無いキーを含む:', sum(1 for r in comm if r['missing']))
print(' 同じmatrixで形状が違うキーを含む:', sum(1 for r in comm if r['shapediff']))
for r in comm:
if not r['subseq']: print(' NG', r['kb'], r['name'])
```
+215
View File
@@ -0,0 +1,215 @@
# 公開KLE JSONの収集とPHYへの変換可否
## 結論
公開されているKLE JSON 1505件(キー111,619個)を集め、PHYへ変換して書き戻す往復を通した。
- 幾何(位置・サイズ・回転・2つ目の矩形・decal・ghost)は全件で無損失。PHYのデータモデルで表せないKLEの
幾何情報は無かった
- 置き場がないのは見た目の属性だけ(c, t, p, a, f, f2, fa, n, l と sm, sb, st、ファイル単位の
backcolor, radii, background, css)。検証では退避用のフィールドに逃がした。PHY P-6 の対象そのもの
- KLE本家のデシリアライザが読めないJSONが実在する(1505件中1件)
- `l`(stepped)が付いたキーが16個あった。KLEはstepped Caps Lockに「段差がある」ことしか持たず、
スイッチ位置のオフセットは持たない。PHYの `switchOffset` は取り込みでは決まらない
## コーパス
| 出所 | 件数 | 備考 |
| --- | --- | --- |
| KLE本家のプリセット (`layouts.json`) | 12 | ANSI 104, ISO 105, ErgoDox, Kinesis Advantage, Atreus など |
| KLE本家のサンプル (`samples/`) | 9 | Symbolics Space Cadet, Apple Wireless, Commodore VIC-20 など |
| VIA キーボード定義 (the-via/keyboards の `layouts.keymap`) | 1484 | 市販・自作基板の実データ |
Geekhackやgistは当たっていない。VIAの定義はKLE JSONそのもの(`layouts.keymap`)で、
実際に流通している基板をほぼ網羅しているため、これで足りると判断した。
## 検証方法
KLE本家のリポジトリ (ijprest/keyboard-layout-editor) から `serial.js` と `extensions.js` を
そのままNodeで読み込み、本家の `deserialize` / `serialize` を使った。自前の再実装ではない。
1. 元データ → `deserialize` → キー配列A
2. A → PHY(root直下に平坦なKey木、絶対座標を `Translate·Rotate` に分解)
3. PHY → KLEのキー配列 → 本家 `serialize` → `deserialize` → キー配列B
4. AとBを、絶対位置・角度・サイズ・ラベル・見た目属性のフィンガープリントの多重集合で比較
比較対象は「Aをそのまま本家で往復させた結果」とした(本家自身の往復の癖を分離するため)。
## 結果
- 1504/1504 で配置が一致(1件は手順1で本家が例外を投げるため対象外)
- 直列化結果そのものが本家の出力と完全一致: 1408件。小数4桁に丸めると一致(誤差のみ): 20件。
残り76件は行の切り方が違うだけで、読み直した配置は一致する
出現頻度(キー単位、111,619個中):
| 属性 | 件数 |
| --- | --- |
| c (色) | 35,888 |
| 0.25U刻みでない座標・サイズ | 6,824 |
| 回転あり | 4,583 (うち15度の倍数でない 4,155) |
| rx/ry あり | 2,567 |
| f/f2/fa | 893 |
| d (decal) | 830 |
| 2つ目の矩形 (x2,y2,w2,h2) | 570 |
| sm/sb/st | 142 |
| t (文字色) | 61 |
| n (ホームポジションの突起) | 52 |
| l (stepped) | 16 |
| g (ghost) | 8 |
ファイル単位のメタデータは name 21件、backcolor 7件、author 2件、radii 2件、
background/switchMount/switchBrand/switchType 各1件。
座標・サイズ・回転は正規化されていない値が普通に出てくる。PHYが「スナップするが正規化しない」と
決めているのと整合する。
## 本家が読めないJSON
`owlab/spring/spring.json` の `layouts.keymap` は `{"r":12,"y":-0.8,"x":0.1}` を行の先頭以外に置いており、
本家の `deserialize` は `'r' can only be used on the first key in a row` で例外を投げる。
VIA側のパーサーは通してしまうため、このまま公開されている。
取り込みでこれをどう扱うか(本家と同じく拒否するか、行の途中の r を受け入れるか)は未定。
PHY の未確認事項に置いた。
## 見た目属性の扱い
退避して書き戻す方針は、次の制約とセットでないと成立しない。
本家の `deserialize` は `p` / `sm` / `sb` / `st` を `if(key.p)` の形で読む。空文字への書き戻しが無視されるため、
一度値を持ったキーの後ろに値を持たないキーが来る並びでは、前の値が後続に残る。
```txt
入力: [[{p:'SA',sm:'cherry'},'A',{p:'',sm:''},'B']]
結果: A → p=SA sm=cherry / B → p=SA sm=cherry (Bの空文字指定は無視される)
```
実コーパスでは、クラスターの原点を rx,ry に選ぶ出力では1件も発生しなかった。
キーごとに rx,ry を選ぶ出力では1件発生した(floookay/adelheid、`p:"SPACE"` が11キーに漏れる)。
並び順に依存する事故なので、出力側で回避しきれない。
## 検証スクリプト
コーパスと本家のコードを用意する。
```sh
git clone --depth 1 https://github.com/ijprest/keyboard-layout-editor.git kle
git clone --depth 1 https://github.com/the-via/keyboards.git via
# 本家の serial.js を Node から使えるようにする(angular は toJson しか使われない)
{ echo 'global.angular = { toJson: (o) => JSON.stringify(o) };'; cat kle/extensions.js; cat kle/serial.js; } > kleio.js
```
`convert.js` — KLEキーとPHYノードの相互変換と比較用フィンガープリント。
```js
// KLE JSON <-> Beyond KLE PHY の変換(検証用)
const $serial = require('./kleio.js');
const R = (d) => d * Math.PI / 180;
// KLE key -> PHY Key (root直下の平坦な木)
function kleKeyToPhy(k, i) {
const th = R(k.rotation_angle), c = Math.cos(th), s = Math.sin(th);
const dx = k.x - k.rotation_x, dy = k.y - k.rotation_y;
const node = {
id: 'k' + i,
transform: {
x: k.rotation_x + dx * c - dy * s,
y: k.rotation_y + dx * s + dy * c,
rotation: k.rotation_angle,
},
size: { w: k.width, h: k.height },
profile: 'default',
};
if (k.width2 !== k.width || k.height2 !== k.height || k.x2 !== 0 || k.y2 !== 0) {
Object.assign(node.size, { x2: k.x2, y2: k.y2, w2: k.width2, h2: k.height2 });
}
if (k.decal) node.decal = true;
if (k.ghost) node.ghost = true;
const labels = k.labels.map(x => x === undefined ? '' : x);
if (labels.some(x => x !== '')) node.labels = labels;
// PHYに置き場のない属性(P-6)。検証用に退避
node._kle = { c: k.color, t: k.textColor, p: k.profile, n: k.nub, l: k.stepped,
sm: k.sm, sb: k.sb, st: k.st, ts: k.textSize, dflt: k.default };
return node;
}
// PHY Key -> KLE key (rx,ry の選び方は出力側の裁量)
function phyToKleKey(n, opts = {}) {
const k = $serial.defaultKeyProps();
const rot = n.transform.rotation;
const [rx, ry] = opts.pivot ? opts.pivot(n) : (rot === 0 ? [0, 0] : [n.transform.x, n.transform.y]);
const th = R(rot), c = Math.cos(th), s = Math.sin(th);
// 絶対座標 -> KLE の (x,y): 逆回転
const dx = n.transform.x - rx, dy = n.transform.y - ry;
k.x = rx + dx * c + dy * s;
k.y = ry + -dx * s + dy * c;
k.rotation_angle = rot; k.rotation_x = rx; k.rotation_y = ry;
k.width = n.size.w; k.height = n.size.h;
k.width2 = n.size.w2 !== undefined ? n.size.w2 : n.size.w;
k.height2 = n.size.h2 !== undefined ? n.size.h2 : n.size.h;
k.x2 = n.size.x2 || 0; k.y2 = n.size.y2 || 0;
k.decal = !!n.decal; k.ghost = !!n.ghost;
k.labels = (n.labels || []).slice();
const e = n._kle || {};
k.color = e.c !== undefined ? e.c : k.color;
k.textColor = e.t || []; k.profile = e.p || ''; k.nub = !!e.n; k.stepped = !!e.l;
k.sm = e.sm || ''; k.sb = e.sb || ''; k.st = e.st || '';
k.textSize = e.ts || []; if (e.dflt) k.default = e.dflt;
return k;
}
// 比較用: キーの見た目を決める絶対量だけを取り出す
function fingerprint(k) {
const th = R(k.rotation_angle), c = Math.cos(th), s = Math.sin(th);
const dx = k.x - k.rotation_x, dy = k.y - k.rotation_y;
const ax = k.rotation_x + dx * c - dy * s, ay = k.rotation_y + dx * s + dy * c;
const r6 = (v) => Math.round(v * 1e6) / 1e6;
const ang = ((k.rotation_angle % 360) + 360) % 360;
return JSON.stringify([r6(ax), r6(ay), r6(ang), k.width, k.height, k.width2, k.height2, k.x2, k.y2,
k.decal, k.ghost, k.stepped, k.nub, (k.labels || []).map(x => x || '').join('\u0001'),
k.color, k.profile, k.sm, k.sb, k.st,
(k.textColor || []).map(x => x || '').join('\u0001'),
(k.textSize || []).map(x => x || 0).join('\u0001'), k.default && k.default.textSize]);
}
module.exports = { $serial, kleKeyToPhy, phyToKleKey, fingerprint };
```
`run2.js` — コーパス全件の往復。
```js
const fs=require('fs'), path=require('path');
const { $serial, kleKeyToPhy, phyToKleKey, fingerprint } = require('./convert.js');
const rnd=v=>Math.round(v*1e6)/1e6;
function walk(d,out=[]){for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name); e.isDirectory()?walk(p,out):e.name.endsWith('.json')&&out.push(p);}return out;}
const corpus=[];
for(const p of JSON.parse(fs.readFileSync('kle/layouts.json','utf8')).presets) if(p.data.length) corpus.push({name:'preset:'+p.name,data:p.data});
for(const f of fs.readdirSync('kle/samples')) { try{ corpus.push({name:'sample:'+f,data:JSON.parse(fs.readFileSync('kle/samples/'+f,'utf8'))});}catch(e){} }
for(const p of walk('via/src')){ try{ const d=JSON.parse(fs.readFileSync(p,'utf8')); if(d.layouts&&Array.isArray(d.layouts.keymap)&&d.layouts.keymap.length) corpus.push({name:'via:'+path.relative('via/src',p),data:d.layouts.keymap}); }catch(e){} }
let ok=0, sameSer=0, n=0, parseFail=0;
let stickyA=0, stickyB=0; const stickyBoards=[];
const RESET=/"(p|sm|sb|st|t)":""/;
const rowsOf=s=>s.filter(Array.isArray).length;
let rowsOrig=0,rowsA=0,rowsB=0, rotBoards=0;
for(const b of corpus){
let A; try{ A=$serial.deserialize(b.data).keys; }catch(e){ parseFail++; continue; }
n++;
const orig=$serial.serialize({meta:{},keys:JSON.parse(JSON.stringify(A))});
const phy=A.map((k,i)=>{const nd=kleKeyToPhy(k,i); nd._pivot=[k.rotation_x,k.rotation_y]; return nd;});
const mk=(opts)=>$serial.serialize({meta:{},keys:phy.map(nd=>{const k=phyToKleKey(nd,opts); k.x=rnd(k.x); k.y=rnd(k.y); return k;})});
const sA=mk(), sB=mk({pivot:nd=>nd._pivot});
const g=s=>$serial.deserialize(s).keys.map(fingerprint).sort().join('|');
if(g(sB)===g(orig)) ok++;
if(JSON.stringify(sB)===JSON.stringify(orig)) sameSer++;
if(RESET.test(JSON.stringify(sA))) stickyA++;
if(RESET.test(JSON.stringify(sB))) { stickyB++; stickyBoards.push(b.name); }
if(A.some(k=>k.rotation_angle)){ rotBoards++; rowsOrig+=rowsOf(orig); rowsA+=rowsOf(sA); rowsB+=rowsOf(sB); }
}
console.log(`boards=${n} parseFail=${parseFail}`);
console.log(`PHY経由(クラスター原点を軸+丸め)で元と同じ配置: ${ok}/${n}`);
console.log(`直列化結果が元のKLE出力と完全一致: ${sameSer}/${n}`);
console.log(`回転を含む板: ${rotBoards} 行数合計 orig=${rowsOrig} A(キーごと軸)=${rowsA} B(クラスター軸)=${rowsB}`);
console.log(`sticky属性の""リセットを含む出力: A=${stickyA} B=${stickyB}`, JSON.stringify(stickyBoards.slice(0,6)));
```
+66
View File
@@ -0,0 +1,66 @@
# 行の先頭以外の r, rx, ry を受け入れるか
## 結論
受け入れる。位置を問わず、そのキー以降に適用する。rx, ry は座標を (rx, ry) に戻してから x, y を加える
(KLE本家のコードから行頭チェックだけを外したときの挙動と同じ)。
決め手は Remap が読めること。ユーザーの判断: 「Remapで扱えるものを拒否したくない」。
| 案 | 取り込める基板 | 既存の解釈への影響 | 出力への影響 |
| --- | --- | --- | --- |
| 本家に合わせて拒否 | 1505件中1504件 | なし | なし |
| **r だけ受け入れ** | 1505件 | なし | なし |
| **r, rx, ry とも受け入れ(採用)** | 1505件 | なし | なし |
3案とも出力は変わらない。本家の serialize はクラスターが変わると必ず行を割るので、
r, rx, ry は常に行頭にしか出ない。寛容化しても非互換なJSONを生む側には回らない。
## 実データ
コーパス1505件(KLE本家プリセット12 + samples 9 + the-via/keyboards 1484)で、
行の先頭以外の r, rx, ry を持つのは `owlab/spring/spring.json` の1件だけ。内訳は r が15回、rx/ry は0回。
spring は Alice系。物理行ごとに配列を1つ書き、行の途中で r を 0 → 12 → -12 → 0 と切り替えている。
行頭チェックを外して読むと素直に開く(67キー、左右±12度のスプレイ + 回転なしの外側2列)。
```
0,3 x=3.25 r=0 0,4 x=4.44 r=12 0,8 x=8.72 r=-12 0,12 x=12.85 r=0
1,2 x=2.50 r=0 1,3 x=3.70 r=12 1,7 x=8.48 r=-12 1,11 x=12.60 r=0
```
## 寛容化が既存の解釈を変えないこと
本家 serial.js の該当箇所は throw だけで、意味づけは残りの行が決めている。
```js
if(key.r != null) { if(k!=0) {deserializeError(...)} current.rotation_angle = key.r; }
if(key.rx != null) { if(k!=0) {deserializeError(...)} current.rotation_x = cluster.x = key.rx; extend(current, cluster); }
```
throw を3つ消したパーサーと本家で全1505件を読み比べ、本家が読めた1504件は**キー配列が完全一致**。
変わるのは今エラーになる1件だけ。
## 他のツールの受け入れ
| ツール | 行の途中の r/rx/ry | 根拠 |
| --- | --- | --- |
| KLE本家 `serial.js` | 拒否 | `'r' can only be used on the first key in a row` |
| kle-serial (ijprest, TS移植) | 拒否 | 同じチェックを移植 |
| Vial `kle_serial.py` | 拒否 | `k != 0 and ("r" in item or ...)` |
| kicad-kbplacer | 拒否 | 同じチェックあり |
| VIA `@the-via/reader` | 許容 | reduceで位置チェックなし |
| Remap `KeyboardModel.ts` | 許容 | `Current.setOp()` に位置チェックなし |
| QMK `kle2xy.py` | 無関係 | r/rx/ry を読まない(回転を捨てる) |
Remap の `Current` を位置計算だけ移植して、throw を外した本家と突き合わせた。行の途中の r、
行の途中の rx/ry、途中で r=0 に戻す、spring 全体、いずれも一致する。VIA reader はコードを読んだだけで、
`cursor.y = ry` としか書いていないため rx/ry の扱いは本家と違う可能性がある(spring は rx=ry=0 なので影響なし)。
同じ突き合わせをコーパス全件でやると19件が不一致になるが、いずれも行頭側の ry の解釈やラベルなしキーの扱いで、
今回の話とは別件。簡易移植での比較なので Remap 本体で再現したわけでもない。追っていない。
## 取り込んで書き戻す
spring を寛容パーサーで読み、PHY経由で本家の serialize に通すと、5行 → 26行のJSONになる。
本家の deserialize が読め、配置も元と一致した。寛容化は本家で開けないファイルの修復経路になる。
+100
View File
@@ -0,0 +1,100 @@
# KLE JSON出力での rx, ry の選び方と行のまとまり
PHY P-1「KLE JSONへ書き出した配置が行のまとまりを保てるか」の検証。
## 結論
保てる。条件は2つ。
1. 同じ絶対角度を持つキーには共通の rx, ry を与える(クラスターの原点が使える)
2. 書き出す x, y を丸める
この2つを満たすと、ErgoDox・Kinesis Advantage では元のKLE JSONとバイト単位で一致する出力になった。
KLEが行を切る条件は本家 `serial.js` の `serialize` にそのまま書かれている。
`(r, rx, ry)` のいずれかが変わるか、`y` が前のキーと**厳密に**異なると新しい行になる。
キーは書き出し前に `角度 → rx → ry → y → x` で並べ替えられる。
## 案の比較
回転を含む実データをPHY(絶対座標)に変換し、rx, ry の選び方だけを変えて書き出した。
行数が少ないほど、元の行のまとまりに近い。
| 軸の選び方 | 回転を含む153枚の行数合計 | 備考 |
| --- | --- | --- |
| 元のKLE JSON | 3465 | 比較の基準 |
| キーごとに自分の原点を軸にする | 6369 | 回転キーが1つずつ別の回転グループになり、1キー1行に分解される |
| 同じ角度のキーで軸を共有する(クラスター原点) | 3526 | ほぼ基準どおり |
丸めなしだと、軸を共有しても行が余分に割れる。ErgoDox で 26行 → 28行。
`serialize` の行判定が `key.y !== current.y` の厳密比較で、絶対座標への変換で入った
1e-16 程度の誤差が残るため。丸めれば 1e-4 でも 1e-9 でも結果は同じだった。
| ボード | 元の行数 | 軸共有+丸め | 一致 |
| --- | --- | --- | --- |
| ErgoDox (KLEプリセット) | 26 | 26 | 完全一致 |
| ErgoDox EZ (VIA) | 26 | 26 | 完全一致 |
| Kinesis Advantage | 17 | 17 | 完全一致 |
| Atreus | 34 | 34 | 数値の誤差のみ(本家は保存時に小数4桁へ丸めるので、その時点で一致) |
## 検証の範囲
本家のWebアプリのUIに読み込ませたわけではなく、本家リポジトリの `serial.js` をそのままNodeで実行した。
行の再構成と回転グループの判定はこのファイルに閉じているので、UIを経由しても同じ結果になるはずだが、
描画そのものは確認していない。
## 副作用
キーごとに軸を選ぶ出力は、行が割れるだけでなく、見た目属性の書き戻しも壊しやすい。
KLEは `p` / `sm` / `sb` / `st` の空文字への書き戻しを無視するため、並びが変わると前のキーの値が漏れる。
軸を共有した出力では1504件中0件、キーごとに軸を選んだ出力では1件発生した。
## 検証スクリプト
```sh
git clone --depth 1 https://github.com/ijprest/keyboard-layout-editor.git kle
git clone --depth 1 https://github.com/the-via/keyboards.git via
{ echo 'global.angular = { toJson: (o) => JSON.stringify(o) };'; cat kle/extensions.js; cat kle/serial.js; } > kleio.js
```
```js
// 使い方: 下の setup の手順でコーパスを用意してから node rowgroup.js
const fs=require('fs');
const $serial=require('./kleio.js'); // 本家 extensions.js + serial.js を連結したもの
const R=d=>d*Math.PI/180, rnd=v=>Math.round(v*1e6)/1e6;
const rowsOf=s=>s.filter(Array.isArray).length;
// KLEキー -> 絶対配置(PHYのTransformと同じ意味)。rx,ry は元の値をクラスター原点として覚えておく
function toPhy(k){
const th=R(k.rotation_angle), c=Math.cos(th), s=Math.sin(th);
const dx=k.x-k.rotation_x, dy=k.y-k.rotation_y;
return {x:k.rotation_x+dx*c-dy*s, y:k.rotation_y+dx*s+dy*c, rot:k.rotation_angle,
pivot:[k.rotation_x,k.rotation_y], src:k};
}
// 絶対配置 -> KLEキー。pivot の選び方だけを差し替えて比べる
function toKle(n, pivot){
const k=JSON.parse(JSON.stringify(n.src));
const [rx,ry]=pivot, th=R(n.rot), c=Math.cos(th), s=Math.sin(th);
const dx=n.x-rx, dy=n.y-ry;
k.x=rnd(rx+dx*c+dy*s); k.y=rnd(ry-dx*s+dy*c);
k.rotation_angle=n.rot; k.rotation_x=rx; k.rotation_y=ry;
return k;
}
const load=p=>{const d=JSON.parse(fs.readFileSync(p,'utf8')); return d.layouts?d.layouts.keymap:d;};
const targets=[];
for(const p of JSON.parse(fs.readFileSync('kle/layouts.json','utf8')).presets)
if(['ErgoDox','Kinesis Advantage','Atreus'].includes(p.name)) targets.push({name:p.name,data:p.data});
targets.push({name:'ergodox_ez(via)',data:load('via/src/ergodox_ez/ergodox_ez.json')});
for(const t of targets){
const A=$serial.deserialize(t.data).keys;
const orig=$serial.serialize({meta:{},keys:JSON.parse(JSON.stringify(A))});
const phy=A.map(toPhy);
const ser=(pick)=>$serial.serialize({meta:{},keys:phy.map(n=>toKle(n,pick(n)))});
const perKey=ser(n=>n.rot?[n.x,n.y]:[0,0]); // 回転キーはキーごとに自分の原点を軸にする
const cluster=ser(n=>n.pivot); // 同じ角度のキーで軸を共有する
console.log(t.name.padEnd(20),'元',rowsOf(orig),
'| キーごと',rowsOf(perKey),
'| 軸共有',rowsOf(cluster), JSON.stringify(cluster)===JSON.stringify(orig)?'(元と完全一致)':'');
}
```
+81
View File
@@ -0,0 +1,81 @@
# LEDチェーンとレイアウトバリアント
ELC P-4 の検証。
## 結論
チェーンはレイアウトの選択と無関係に固定されている。バリアントごとにチェーンの並びを分ける必要はない。
理由は、per-key LEDがスイッチとは独立に基板へ実装されるため。排他な2つのキー位置の両方にLEDが載る。
QMKの `rgb_matrix.layout` はボードに1つしかなく、レイアウトごとに分かれていない。
| ボード | レイアウト数 | LED数 | バリアント固有のマトリクス位置 | そこへのLED割り当て |
| --- | --- | --- | --- | --- |
| dp60 | 7 | 72 | 8 | 8個すべてにあり |
| cherrybstudio/cb87rgb | 9 | 92 | 6 | 6個すべてにあり |
| crkbd/r2g | 2 | 54 | 6 (外側の列) | 6個すべてにあり |
| annepro2/c18 | 2 | 61 | 1 (ISOの追加キー) | なし |
| boardsource/sessanta | 2 | 72 | 4 (分割スペース) | なし |
annepro2/c18 と boardsource/sessanta は、バリアント固有のキーにLEDが無い。
チェーンが切れるのではなく、そのキーにper-key LEDが載っていないだけである。
PHYがper-key LEDの有無をキーごとに持つ形でそのまま表せる。
どちらの型でも、LEDの番号はレイアウトの選択で動かない。
## 調べ方
QMKのinfo.json 400件から、レイアウトを2つ以上持ち `rgb_matrix.layout` を持つ19件を抽出。
全レイアウトのマトリクス位置の和集合から共通部分を引いた「バリアント固有の位置」に、
LEDが割り当てられているかを見た。11件は割り当てあり、12件は一部に無し(両方に該当する板がある)。
## 限界
回路図そのものではなく、ファームウェアが持つLED配列を見ている。
LEDが基板に実装されているかどうかまでは確認できない。ただしQMKの `rgb_matrix.layout` は
レイアウトごとに分けられない構造なので、「選択によってチェーンの並びが変わる基板」は
少なくともQMKでは成立しない。
## 検証スクリプト
```py
# QMKのinfo.jsonを無作為抽出で取得する。qmk_list.json は
# curl -o qmk_list.json https://keyboards.qmk.fm/v1/keyboard_list.json
import json, random, urllib.request, concurrent.futures, os
kbs=json.load(open('qmk_list.json'))['keyboards']
random.seed(42)
os.makedirs('qmk', exist_ok=True)
def fetch(kb):
p='qmk/'+kb.replace('/','__')+'.json'
if os.path.exists(p): return True
try:
with urllib.request.urlopen(f'https://keyboards.qmk.fm/v1/keyboards/{kb}/info.json', timeout=30) as r:
open(p,'wb').write(r.read())
return True
except Exception as e:
return str(e)[:60]
with concurrent.futures.ThreadPoolExecutor(8) as ex:
res=list(ex.map(fetch, random.sample(kbs, 400)))
print('fetched', sum(1 for r in res if r is True), 'failed', sum(1 for r in res if r is not True))
```
```py
# レイアウトバリアントとper-key LEDの関係。rgb_matrix.layout はボードに1つしかない
import json, glob, collections
st=collections.Counter()
for f in glob.glob('qmk/*.json'):
d=json.load(open(f,encoding='utf-8'))
for kb,v in (d.get('keyboards') or {}).items():
lays=v.get('layouts') or {}; rm=(v.get('rgb_matrix') or {}).get('layout')
if not rm or len(lays)<2: continue
st['対象ボード']+=1
used=[set(tuple(k['matrix']) for k in l['layout']) for l in lays.values()]
variant_only=set().union(*used)-set.intersection(*used) # バリアント固有の位置
led=set(tuple(l['matrix']) for l in rm if 'matrix' in l)
if not variant_only: continue
if variant_only & led: st['固有位置にLEDあり']+=1
if variant_only - led: st['固有位置にLEDなし']+=1
print(kb, 'レイアウト', len(lays), 'LED', len(rm),
'固有位置', len(variant_only), 'うちLEDあり', len(variant_only & led))
print(st)
```
+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に持たせれば足りる。編集中の表示切り替えはエディタの状態
+56
View File
@@ -0,0 +1,56 @@
# プロファイル定義の置き場
## 結論
- ファイル単位の器 `PhyFile` を保存形式に書き、`profiles: Profile[]` をそこに置く
- `Key.profile` は Profile 実体ではなく ID 参照にする
- ビジュアル表現は上面矩形の4辺インセット + R
- プロファイル内の距離はすべて mm
phase1-json-schema.md には最初からファイル単位の器(version/metadata/profiles/root)と profile 定義があり、
PHYレイヤー.md の保存形式だけが `const root: Node[] = []` のまま取り残されていた。今回は実質その追認。
ただし visualOffset の形だけはスキーマ側も書き換えた。
## 案の比較
### ビジュアル表現の形
| 案 | 内容 | 採否 |
| --- | --- | --- |
| 4辺インセット + R | top/right/bottom/left/radius | 採用。非対称な上面を表せる |
| 位置 + サイズ差 | x, y, dw, dh, radius | 不採用。表現力は同じだが、非対称のとき値の読み取りが直感的でない |
| スキーマ現状のまま | x, y, radius | 不採用。本文が言う「オフセット距離」に当たる項目がなく、上面のサイズが決まらない |
kbd-editor の stores/displayConfig.ts が top{l,r,t,b} + cornerRadius で、4辺独立と同じ形だった
(過去の kbd-editor 調査による。概念の参考は可としている)。
### 単位
| 案 | 採否 |
| --- | --- |
| すべて mm | 採用 |
| unitSize は mm、オフセットと R は U | 不採用 |
| 値ごとに mm/U を指定 | 不採用 |
元の本文は「オフセット距離(mm or U単位)・R(U単位)」と単位が混在していた。
プロファイルは実寸を規定する定義なので、中で U 換算を持つと unitSize への依存が入る。
### 参照方法
| 案 | 採否 |
| --- | --- |
| ID 参照 | 採用。スキーマが既にこの形。定義の複製が起きない |
| Key が Profile 実体を持つ | 不採用 |
座標モデルを決めたときに「今回の決定の範囲外」として実体のまま残されていた項目。
今回で決着。
## 今回触っていないもの
ファイル単位のデータとして他に候補が出ていたが、置き場の決定は保留:
- KLE の switchMount / switchBrand / switchType、backcolor / radii / background / css → P-6 待ち
- KLE 出力時の rx, ry の選び方、行のまとまり → P-1。ファイルではなく出力設定の単位
implementation-plan.md の 60% ANSI サンプルは keys/position/size.width という古い平坦な形のままで、
今回の保存形式と合っていない。プロファイル部分だけは unitSize のみなので矛盾しない。別タスク。
+148
View File
@@ -0,0 +1,148 @@
# 複数のバリアントで共通のキーを持つ基板があるか
## 結論
実在する。そのうえで、共通キーはVariantごとに複製して置く。同じ物理キーかどうかはBeyond KLEが判定しない。
分解して複製を避けられる基板もあるが、避けられない基板もある。
| ボード | 選択肢 | 分解 |
| --- | --- | --- |
| LW67 | ANSI / ANSI Split Backspace / ISO / ISO Split Backspace | 独立した2つの選択の積。VariantGroupを2つに分ければ複製は要らない |
| MACRO1 | Numpad / Southpaw / Macropad | 分解できない。独立な選択に割ると、キャップが重なる組み合わせが作れてしまう |
複製を許す判断の根拠は、複製しても他レイヤーが壊れないこと。
ELCは排他キーで接続先の組を共有でき、IDXは `order` の配列形式で番号を共有できる。
MAPは同じキーコードを2回書くことになるが、どれが同じ物理キーかは設計者が知っている。
位置で自動判定しようとしても、排他キーは位置もサイズも違ってよい(2UのBackspaceと分割後の1U)ので、
判定の基準にならない。
## 調べ方
VIAのキーボード定義1484件のうち、レイアウトオプションを持つ824件を対象にした。
VIAは1つのキーを1つの選択肢にしか置けないので、複数の選択肢で成立するキーは定義側で複製されている。
生ラベルの4番目(`"行,列"` の形)がオプション番号と選択肢番号で、これで選択肢ごとのキー集合を作り、
マトリクス位置と形状(x, y, w, h, r)が一致するキーが複数の選択肢に現れるものを探した。
3択以上のオプション415件のうち、該当するのは2件だけだった。
## LW67
ANSI/ISO と Backspace分割の2択の積で、4つの選択肢はその全組み合わせ。
ISO Enter は ISO 側の2つ、分割Backspaceのキーは分割側の2つに現れる。
QMKも `LAYOUT_ansi` / `LAYOUT_ansi_splitbs` / `LAYOUT_iso` / `LAYOUT_iso_splitbs` の4つを持つ。
VariantGroupを「ANSI/ISO」と「Backspace分割の有無」に分ければ、キーの複製は要らない。
## MACRO1
6行4列。VIAは3択、QMK (`laneware/macro1`) も `LAYOUT_numpad` / `LAYOUT_southpaw` / `LAYOUT_macropad` の
3つしか持たない。`LAYOUT_macropad` は全キー1Uで23キーあり、他の2つが使うフットプリントを全部含む。
配線はすべての位置に来ていて、組み合わせを3つに絞っているのはファームウェア側である。
| | 左列 (2,0 / 3,0 / 4,0) | 下段左 (5,0 / 5,1) | 右列 (2,3 / 3,3 / 4,3) | 下段右 (5,2 / 5,3) |
| --- | --- | --- | --- | --- |
| Numpad | 1U×3 | `5,0` が2U幅。`5,1` は潰れる | 2U縦×2 (`3,3` なし) | `5,2` 1U。`5,3` は `4,3` が潰す |
| Southpaw | 2U縦×2 (`3,0` なし) | `5,0` は `4,0` が潰す。`5,1` 1U | 1U×3 | `5,2` が2U幅。`5,3` は潰れる |
| Macropad | 1U×3 | 両方1U | 1U×3 | 両方1U |
共通キーは8個。`3,0` などの左列1Uが {Numpad, Macropad}、`5,1` と右列1Uが {Southpaw, Macropad} に現れる。
出現集合が交差するので入れ子では表せない。
左右の列だけを見ると独立な2択に見えるが、下段が絡むと分解できない。
「左列と下段左」を1つの選択、「右列と下段右」をもう1つの選択として2つのVariantGroupに割ると、
`4,0` が2U縦のまま `5,0` に1Uキーを置く組み合わせが作れる。これは提供されないどころか、
キャップが重なって物理的に成立しない。
## 捨てた見方
- 入れ子のVariantGroupで表す: 出現集合が交差するので不可能
- 独立したVariantGroupに分けて、成立しない組み合わせを禁止する仕組みを足す:
複製を許せば要らない。禁止の表現を足すほうが重い
- 同じ位置のキーに違う番号が振られたら警告する: 排他キーは位置もサイズも違ってよいので基準にならない
## この調査の限界
VIAの表現力に引きずられている。VIAは選択肢ごとのキー集合しか書けないので、
設計者が最初から「2つの独立した選択」として設計した基板は、VIAの定義でも2つのオプションに分かれ、
この検索には引っかからない。引っかかった2件は「1つのオプションに畳んでしまった」定義だけである。
## 検証スクリプト
`rawkle.js` — VIAのオプションタグを読むため、並べ替え前の生ラベルを残すデシリアライザ。
```js
// 生ラベル(並べ替え前)を保持する最小デシリアライザ。幾何情報は serial.js と同じ規則。
function deserializeRaw(rows){
const cur={x:0,y:0,w:1,h:1,x2:0,y2:0,w2:0,h2:0,r:0,rx:0,ry:0,d:false};
const keys=[];
for(const row of rows){
if(!Array.isArray(row)){ continue; }
for(const item of row){
if(typeof item==='string'){
keys.push({x:cur.x,y:cur.y,w:cur.w,h:cur.h,
w2:cur.w2===0?cur.w:cur.w2,h2:cur.h2===0?cur.h:cur.h2,x2:cur.x2,y2:cur.y2,
r:cur.r,rx:cur.rx,ry:cur.ry,d:cur.d,raw:item.split('\n')});
cur.x+=cur.w; cur.w=cur.h=1; cur.x2=cur.y2=cur.w2=cur.h2=0; cur.d=false;
} else {
if(item.r!=null) cur.r=item.r;
if(item.rx!=null){ cur.rx=item.rx; cur.x=cur.rx; cur.y=cur.ry; }
if(item.ry!=null){ cur.ry=item.ry; cur.x=cur.rx; cur.y=cur.ry; }
if(item.x) cur.x+=item.x;
if(item.y) cur.y+=item.y;
if(item.w){ cur.w=cur.w2=item.w; }
if(item.h){ cur.h=cur.h2=item.h; }
if(item.x2) cur.x2=item.x2;
if(item.y2) cur.y2=item.y2;
if(item.w2) cur.w2=item.w2;
if(item.h2) cur.h2=item.h2;
if(item.d) cur.d=item.d;
}
}
cur.y++; cur.x=cur.rx;
}
return keys;
}
module.exports={deserializeRaw};
```
```js
const fs=require('fs'), path=require('path');
const {deserializeRaw}=require('./rawkle.js');
function walk(d,out=[]){for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name); e.isDirectory()?walk(p,out):e.name.endsWith('.json')&&out.push(p);}return out;}
const OPT=/^(\d+),(\d+)$/;
const rows=[];
for(const f of walk('via/src')){
let d; try{ d=JSON.parse(fs.readFileSync(f,'utf8')); }catch(e){ continue; }
const lay=d.layouts; if(!lay||!Array.isArray(lay.keymap)||!lay.keymap.length) continue;
let keys; try{ keys=deserializeRaw(lay.keymap); }catch(e){ continue; }
const byOpt={};
for(const k of keys){
const tag=k.raw[3]; if(!tag||!OPT.test(tag)) continue;
const [o,c]=tag.split(',').map(Number);
const id=[k.raw[0],k.x,k.y,k.w,k.h,k.r].join('|'); // マトリクス位置+形状で「同じキー」とみなす
((byOpt[o]=byOpt[o]||{})[c]=byOpt[o][c]||new Set()).add(id);
}
for(const [o,ch] of Object.entries(byOpt)){
const cs=Object.keys(ch).map(Number).sort((a,b)=>a-b);
if(cs.length<3) continue;
// 各キーが現れる選択肢集合
const occ={};
for(const c of cs) for(const id of ch[c]) (occ[id]=occ[id]||[]).push(c);
const multi=Object.entries(occ).filter(([,v])=>v.length>1 && v.length<cs.length);
if(!multi.length) continue;
// 出現集合の族が laminar(入れ子で表せる)か
const sets=[...new Set(multi.map(([,v])=>v.join(',')))].map(s=>new Set(s.split(',').map(Number)));
let laminar=true;
for(let i=0;i<sets.length;i++) for(let j=i+1;j<sets.length;j++){
const inter=[...sets[i]].filter(x=>sets[j].has(x)).length;
if(inter>0 && inter!==Math.min(sets[i].size,sets[j].size)) laminar=false;
}
rows.push({f:path.relative('via/src',f),name:d.name,opt:+o,
label: Array.isArray(lay.labels&&lay.labels[o])? lay.labels[o] : (lay.labels?lay.labels[o]:null),
choices:cs.length, sharedKeys:multi.length,
patterns:[...new Set(multi.map(([,v])=>'{'+v.join(',')+'}'))].join(' '), laminar});
}
}
console.log('3択以上で「一部の選択肢だけに現れる共通キー」を持つオプション:',rows.length);
rows.forEach(r=>console.log(JSON.stringify(r)));
```
+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レイヤー (キーマップ)
+131 -14
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" }
]
}
}
@@ -85,6 +86,7 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
"name": { "type": "string" },
"unitSize": {
"type": "object",
"description": "Millimeters per 1U",
"required": ["x", "y"],
"properties": {
"x": { "type": "number", "minimum": 0 },
@@ -93,9 +95,13 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
},
"visualOffset": {
"type": "object",
"description": "Key top rectangle, in millimeters: inward distance from each edge of the outline, plus corner radius",
"required": ["top", "right", "bottom", "left", "radius"],
"properties": {
"x": { "type": "number" },
"y": { "type": "number" },
"top": { "type": "number" },
"right": { "type": "number" },
"bottom": { "type": "number" },
"left": { "type": "number" },
"radius": { "type": "number", "minimum": 0 }
}
}
@@ -186,6 +192,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 +261,8 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
"oneOf": [
{ "$ref": "#/$defs/key" },
{ "$ref": "#/$defs/cluster" },
{ "$ref": "#/$defs/component" }
{ "$ref": "#/$defs/component" },
{ "$ref": "#/$defs/variantGroup" }
]
}
}
@@ -229,7 +288,7 @@ ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAP
"id": "standard",
"name": "Standard Cherry MX",
"unitSize": { "x": 19.05, "y": 19.05 },
"visualOffset": { "x": 1, "y": 1, "radius": 1 }
"visualOffset": { "top": 3, "right": 3, "bottom": 3, "left": 3, "radius": 1 }
}
],
"root": [
@@ -296,13 +355,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 +451,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"
}
]
}
}
}