diff --git a/PHYレイヤー.md b/PHYレイヤー.md index 4140154..a2b63ee 100644 --- a/PHYレイヤー.md +++ b/PHYレイヤー.md @@ -322,7 +322,9 @@ const kleData = (ConfigObject | KeyLabel)[][] ``` - 回転角度は正規化せずそのまま出力する。KLEのデータは任意の角度を受け付け、15度刻みはKLEのUI上の操作単位にすぎない -- rx, ry はPHYに持たないので、出力時に選ぶ +- rx, ry はPHYに持たないので、出力時に選ぶ。同じ絶対角度を持つキーには共通の値を与える + (クラスターの原点が使える)。KLEが1行にまとめるのは r, rx, ry が一致し y が等しいキーだけという制約による +- 書き出す x, y は丸める。行のまとまりは y の厳密一致で決まるので、絶対座標への変換で入った誤差が残ると行が割れる - ユニットサイズ(mm)、ビジュアルオフセット、クラスター構造、部品ノード、per-key LEDはKLE JSONに出力されない - バリアント構造はKLE JSONに出力しない。VariantGroupごとにバリアントを1つ選び、その配下だけを畳んで出力する - 部品グループ・スイッチの向きはKLE JSONに出力しない。sm, sb, st には取り込み時に保持した値をそのまま書き戻す @@ -355,13 +357,6 @@ Beyond KLEはスイッチの品種を持たないため、対応づける先が ## 未確認事項 -### P-1 KLE JSONへ書き出した配置が行のまとまりを保てるか - -- 位置づけ: 本文が前提にしているものではない。連携用出力形式の設計に関わる未解決の問い -- 現状の認識: 階層を畳んでKLEに読み込ませると行のまとまりが崩れる。他レイヤーの情報を使ってでも行ごとにまとめたいが、方法は未検討 -- 手がかり: KLEは r, rx, ry が同じで y が等しいキーを1行にまとめる。同じ絶対角度を持つキーに共通の rx, ry(例えばクラスターの原点)を選べば、行にまとめられる見込み -- 格上げ条件: 回転を含むPHYデータをKLE JSONに変換してKLE本家に読み込ませ、rotation groupと行配列がどう再構成されるかを確認する - ### P-2 KLE JSON取り込み時の照合基準 - 位置づけ: 本文「KLE JSONの取り込み」の具体化に必要な未解決の問い diff --git a/journal/2026-09-21-kle-output-rowgroup.md b/journal/2026-09-21-kle-output-rowgroup.md new file mode 100644 index 0000000..b20385d --- /dev/null +++ b/journal/2026-09-21-kle-output-rowgroup.md @@ -0,0 +1,100 @@ +# KLE JSON出力での rx, ry の選び方と行のまとまり + +PHY P-1「KLE JSONへ書き出した配置が行のまとまりを保てるか」の検証。 + +## 結論 + +保てる。条件は2つ。 + +1. 同じ絶対角度を持つキーには共通の rx, ry を与える(クラスターの原点が使える) +2. 書き出す x, y を丸める + +この2つを満たすと、ErgoDox・Kinesis Advantage では元のKLE JSONとバイト単位で一致する出力になった。 + +KLEが行を切る条件は本家 `serial.js` の `serialize` にそのまま書かれている。 +`(r, rx, ry)` のいずれかが変わるか、`y` が前のキーと**厳密に**異なると新しい行になる。 +キーは書き出し前に `角度 → rx → ry → y → x` で並べ替えられる。 + +## 案の比較 + +回転を含む実データをPHY(絶対座標)に変換し、rx, ry の選び方だけを変えて書き出した。 +行数が少ないほど、元の行のまとまりに近い。 + +| 軸の選び方 | 回転を含む153枚の行数合計 | 備考 | +| --- | --- | --- | +| 元のKLE JSON | 3465 | 比較の基準 | +| キーごとに自分の原点を軸にする | 6369 | 回転キーが1つずつ別の回転グループになり、1キー1行に分解される | +| 同じ角度のキーで軸を共有する(クラスター原点) | 3526 | ほぼ基準どおり | + +丸めなしだと、軸を共有しても行が余分に割れる。ErgoDox で 26行 → 28行。 +`serialize` の行判定が `key.y !== current.y` の厳密比較で、絶対座標への変換で入った +1e-16 程度の誤差が残るため。丸めれば 1e-4 でも 1e-9 でも結果は同じだった。 + +| ボード | 元の行数 | 軸共有+丸め | 一致 | +| --- | --- | --- | --- | +| ErgoDox (KLEプリセット) | 26 | 26 | 完全一致 | +| ErgoDox EZ (VIA) | 26 | 26 | 完全一致 | +| Kinesis Advantage | 17 | 17 | 完全一致 | +| Atreus | 34 | 34 | 数値の誤差のみ(本家は保存時に小数4桁へ丸めるので、その時点で一致) | + +## 検証の範囲 + +本家のWebアプリのUIに読み込ませたわけではなく、本家リポジトリの `serial.js` をそのままNodeで実行した。 +行の再構成と回転グループの判定はこのファイルに閉じているので、UIを経由しても同じ結果になるはずだが、 +描画そのものは確認していない。 + +## 副作用 + +キーごとに軸を選ぶ出力は、行が割れるだけでなく、見た目属性の書き戻しも壊しやすい。 +KLEは `p` / `sm` / `sb` / `st` の空文字への書き戻しを無視するため、並びが変わると前のキーの値が漏れる。 +軸を共有した出力では1504件中0件、キーごとに軸を選んだ出力では1件発生した。 + +## 検証スクリプト + +```sh +git clone --depth 1 https://github.com/ijprest/keyboard-layout-editor.git kle +git clone --depth 1 https://github.com/the-via/keyboards.git via +{ echo 'global.angular = { toJson: (o) => JSON.stringify(o) };'; cat kle/extensions.js; cat kle/serial.js; } > kleio.js +``` + +```js +// 使い方: 下の setup の手順でコーパスを用意してから node rowgroup.js +const fs=require('fs'); +const $serial=require('./kleio.js'); // 本家 extensions.js + serial.js を連結したもの +const R=d=>d*Math.PI/180, rnd=v=>Math.round(v*1e6)/1e6; +const rowsOf=s=>s.filter(Array.isArray).length; + +// KLEキー -> 絶対配置(PHYのTransformと同じ意味)。rx,ry は元の値をクラスター原点として覚えておく +function toPhy(k){ + const th=R(k.rotation_angle), c=Math.cos(th), s=Math.sin(th); + const dx=k.x-k.rotation_x, dy=k.y-k.rotation_y; + return {x:k.rotation_x+dx*c-dy*s, y:k.rotation_y+dx*s+dy*c, rot:k.rotation_angle, + pivot:[k.rotation_x,k.rotation_y], src:k}; +} +// 絶対配置 -> KLEキー。pivot の選び方だけを差し替えて比べる +function toKle(n, pivot){ + const k=JSON.parse(JSON.stringify(n.src)); + const [rx,ry]=pivot, th=R(n.rot), c=Math.cos(th), s=Math.sin(th); + const dx=n.x-rx, dy=n.y-ry; + k.x=rnd(rx+dx*c+dy*s); k.y=rnd(ry-dx*s+dy*c); + k.rotation_angle=n.rot; k.rotation_x=rx; k.rotation_y=ry; + return k; +} +const load=p=>{const d=JSON.parse(fs.readFileSync(p,'utf8')); return d.layouts?d.layouts.keymap:d;}; +const targets=[]; +for(const p of JSON.parse(fs.readFileSync('kle/layouts.json','utf8')).presets) + if(['ErgoDox','Kinesis Advantage','Atreus'].includes(p.name)) targets.push({name:p.name,data:p.data}); +targets.push({name:'ergodox_ez(via)',data:load('via/src/ergodox_ez/ergodox_ez.json')}); + +for(const t of targets){ + const A=$serial.deserialize(t.data).keys; + const orig=$serial.serialize({meta:{},keys:JSON.parse(JSON.stringify(A))}); + const phy=A.map(toPhy); + const ser=(pick)=>$serial.serialize({meta:{},keys:phy.map(n=>toKle(n,pick(n)))}); + const perKey=ser(n=>n.rot?[n.x,n.y]:[0,0]); // 回転キーはキーごとに自分の原点を軸にする + const cluster=ser(n=>n.pivot); // 同じ角度のキーで軸を共有する + console.log(t.name.padEnd(20),'元',rowsOf(orig), + '| キーごと',rowsOf(perKey), + '| 軸共有',rowsOf(cluster), JSON.stringify(cluster)===JSON.stringify(orig)?'(元と完全一致)':''); +} +```