Files
Beyond-KLE/journal/2026-09-21-variant-shared-key.md
T
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

6.6 KiB

複数のバリアントで共通のキーを持つ基板があるか

PHY P-15 の検証。

結論

実在する。ただし稀で、どれも「独立した2つの選択」を1つの選択肢リストに畳んだ結果だった。 VariantGroupを2つ並べれば、キーを複製せずに表せる。

残る問題は別のところにある。MACRO1 は2つの選択の積4通りのうち3通りしか提供していない。 VariantGroupを2つ置くと、基板が提供しない4通り目も選べてしまう。

調べ方

VIAのキーボード定義1484件のうち、レイアウトオプションを持つ824件を対象にした。 VIAは1つのキーを1つの選択肢にしか置けないので、複数の選択肢で成立するキーは定義側で複製されている。 生ラベルの4番目("行,列" の形)がオプション番号と選択肢番号で、これで選択肢ごとのキー集合を作り、 マトリクス位置と形状(x, y, w, h, r)が一致するキーが複数の選択肢に現れるものを探した。

3択以上のオプション415件のうち、「一部の選択肢だけに現れる共通キー」を持つのは2件。

ボード 選択肢 共通キーの現れ方
LW67 ANSI / ANSI Split Backspace / ISO / ISO Split Backspace {ANSI,ANSI-split} {ISO,ISO-split} {ANSI,ISO} {ANSI-split,ISO-split}
MACRO1 Numpad / Southpaw / Macropad {Numpad,Macropad} {Southpaw,Macropad}

中身

LW67 は ANSI/ISO と Backspace分割の2択の積(2×2)で、4つの選択肢はその全組み合わせ。 ISO Enter は ISO 側の2つ、分割Backspaceのキーは分割側の2つに現れる。 VariantGroupを「ANSI/ISO」と「Backspace分割の有無」の2つに分ければ、キーの複製は要らない。

MACRO1 は左列と右列がそれぞれ「1U×2 か 2U縦 か」の2択で、

選択肢 左列 右列
Numpad 1U 2U縦
Southpaw 2U縦 1U
Macropad 1U 1U

「左右とも2U縦」は提供されていない。左列と右列を別のVariantGroupにすれば、 3,0@(0,3.5)1U(Numpad と Macropad で共通)のようなキーを複製せずに置けるが、 提供されない4通り目も表現できてしまう。

捨てた見方

入れ子のVariantGroupで表す案。MACRO1 は {Numpad,Macropad} と {Southpaw,Macropad} が交差するので、 入れ子(片方がもう片方を含む形)では表せない。積として2つ並べる形でしか表せない。

この調査の限界

VIAの表現力に引きずられている。VIAは選択肢ごとのキー集合しか書けないので、 設計者が最初から「2つの独立した選択」として設計した基板は、VIAの定義でも2つのオプションに分かれ、 この検索には引っかからない。実際、2択のオプションを複数持つ定義が多数ある。 引っかかった2件は「1つのオプションに畳んでしまった」定義だけである。

検証スクリプト

rawkle.js — VIAのオプションタグを読むため、並べ替え前の生ラベルを残すデシリアライザ。

// 生ラベル(並べ替え前)を保持する最小デシリアライザ。幾何情報は 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};
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)));