Files
Beyond-KLE/journal/2026-09-21-community-layout-order.md
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

5.5 KiB
Raw Permalink Blame History

コミュニティレイアウトの引数順がキーの取捨だけで得られるか

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'])