# コミュニティレイアウトの引数順がキーの取捨だけで得られるか 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']) ```