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
5.5 KiB
コミュニティレイアウトの引数順がキーの取捨だけで得られるか
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の並び」で番号を持つ以上、 落とすだけでなく差し替えが要ることは変わらない。
検証スクリプト
# 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))
# 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'])