隣り合うキャップが同じフットプリントを食い合う基板(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
149 lines
8.0 KiB
Markdown
149 lines
8.0 KiB
Markdown
# 複数のバリアントで共通のキーを持つ基板があるか
|
||
|
||
## 結論
|
||
|
||
実在する。そのうえで、共通キーは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)));
|
||
```
|