初期検討段階のドキュメントと運用ルールを取り込む

ルール策定前に書かれた設計ドキュメント7点を、以降の基準点として
そのままの状態で記録する。CLAUDE.md のルール1〜4は未適用。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MdLT5x5ka5y6CaWLeE3EKb
This commit is contained in:
2026-09-18 08:43:01 +09:00
co-authored by Claude Opus 5
commit 55243f626d
12 changed files with 2395 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
---
name: decide
description: 設計上の決定を記録する。メインドキュメントの更新・検討済み案の記録・journal作成・コミットを一度に行う。
disable-model-invocation: true
argument-hint: [決定の要旨]
---
# 決定を記録する
CLAUDE.md のルール1〜4に従い、決定を4箇所に分散して記録する。
どれか1つでも欠けると後から追えなくなるので、全部やる。
## 手順
### 0. 決定内容を確認する
以下が揃っているか確認し、足りなければユーザーに聞く。推測で埋めない。
- 採用した案は何か
- それによって**今後効く制約**は何か(却下理由ではなく、現在の状態として書ける文)
- 落とした案があるか
- この決定が未確認の推測に依存していないか
### 1. メインドキュメントを更新する
該当する直下の `*.md` を更新する。
- 採用した案・現在の設定を書く。「なぜAなのか」は1〜3行まで。
- 「なぜBではないのか」は書かない。代わりに**制約文**として書く。
- ✗ `B案はKLEパーサーが壊れるので却下した`
- ○ `クラスター情報はKLE JSONに出力しない(KLE互換性の制約)`
- 比較表・検討過程・ベンチマーク結果をここに書かない。
### 2. 落とした案を1行残す
メインドキュメント末尾の `## 検討済み・非採用` に追記する。
```
- labels[9] へのID埋め込み (journal/2026-09-16-cluster-metadata.md)
```
- 理由は書かない。journalファイル名だけ。
- セクションが5行を超えたら、古いものから落とす。長くなるのは書きすぎのサイン。
### 3. journal を書く
`journal/YYYY-MM-DD-<topic>.md` を作る。トピックを詰め込まない(1決定1ファイル)。
構成は上から順に:
1. **結論** — 何を採用したか
2. **案の比較** — 簡潔な表。メリット/デメリットを1行ずつ
3. 検証ログ・根拠・捨てた案の詳細
冒頭20行で結論と選択肢が分かること。整形は最低限でよい。
他ファイルへのリンクも、他ファイルからのリンクも張らない。
### 4. 推測を扱う
この決定が未確認の推測に依存する場合:
- メインドキュメント本文に `[推測:P-n]` を付ける
- 末尾の `## 未確認事項` に項目を足し、**何を確認できれば事実に格上げできるか**を書く
逆に、この決定で検証が済んだ推測があれば、タグを外して `## 未確認事項` から削除する。
本文タグと末尾項目は必ず対で操作する(片方だけ残さない)。
### 5. コミットする
メインドキュメントと journal を**同じコミット**に入れる。1コミット1決定。
メッセージ:
```
<何を変えたか(1行)>
<なぜそうしたか 数行>
Journal: journal/2026-09-16-cluster-metadata.md
```
「なぜ」の詳細はコミットメッセージに書ききらない。journal 側にある。
## 注意
- 決定が固まっていない段階では使わない。調査中は journal だけ書けばよい。
- 複数の決定をまとめてコミットしない。分ける。
+64
View File
@@ -0,0 +1,64 @@
---
name: doc-lint
description: ドキュメントがCLAUDE.mdの運用ルール1〜4に沿っているか検査する。推測タグと未確認事項の対応漏れ、journalへのリンク、比較表の混入などを検出する。
argument-hint: [ファイル...]
---
# ドキュメントのルール違反を検査する
機械的に判定できる項目はスクリプトで検出し、判断が要る項目だけ目視で見る。
**検出して報告するだけで、勝手に直さない。** 修正は内容を確認してもらってから。
## 1. スクリプトを走らせる
```
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
```
引数なしで直下の `*.md` と `journal/*.md` を全部見る。ERROR があれば終了コード 1。
検出する項目:
| 種別 | 内容 |
|---|---|
| ERROR | 本文の `[推測:P-n]` に対応する `## 未確認事項` の項目がない |
| ERROR | `## 未確認事項` の項目に `- 格上げ条件:` の行がない |
| ERROR | `## 未確認事項` のIDが重複している |
| ERROR | journal へのリンク / journal から他ファイルへのリンク |
| ERROR | `## 検討済み・非採用` の項目が journal ファイル名を参照していない |
| ERROR | journal のファイル名が `YYYY-MM-DD-<topic>.md` 形式でない |
| WARN | `## 検討済み・非採用` が5件を超えた |
| WARN | journal の冒頭20行に結論が見当たらない |
| WARN | メインドキュメントに比較表・優先度評価・却下案の記述らしきもの |
**本文タグのない `## 未確認事項` の項目はエラーにしない。** 「まだ本文に書けない未解決の問い」として正当。
WARN の散文検出は当たりが粗い。取りこぼしも誤検出もあるので、出た/出ないを結論にしない。
## 2. スクリプトで見られない項目を確認する
こちらが本体。ファイルを読んで判断する。
- **「なぜBではないのか」が理由として残っていないか。**
却下理由ではなく、現在効いている制約として書かれているか。
- ✗ `階層構造はKLEが非対応なので諦めた`
- ○ `階層クラスターはKLE JSONに出力しない(KLE互換性の制約)`
- **裏取りしていない記述にタグが付いていないか。** 検査項目の中で最も重要で、
機械では判定できない。「〜できるはず」「〜と考えられる」「実現可能」で実際に試していない
記述を探し、`[推測:P-n]` が付いているか確認する。
- **同じ事実が複数ドキュメントに複製されていないか。** 片方だけ古くなる。
- **journal の冒頭20行で、結論と選択肢が分かるか。** 見出しの有無ではなく中身を読む。
- **コミット規約**(必要なら `git log --oneline` を見る)
- 1コミット1決定になっているか
- journal を伴う決定のメッセージに `Journal:` 行があるか
## 3. 報告する
ERROR / WARN / 目視で見つけたもの を分けて出す。
修正案は出してよいが、適用は確認を取ってから。事実関係の誤りを見つけた場合も、直さず指摘する。
## 注意
- 運用ルール策定前に書かれた既存ドキュメントは、まだルール1〜4が適用されていない。
WARN が大量に出るのは想定内で、再編(`/restructure-doc`)のタイミングで解消する。
再編前のファイルの WARN を個別に直そうとしない。
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""CLAUDE.md の運用ルール1〜4のうち、機械的に判定できる項目を検査する。
使い方:
python .claude/skills/doc-lint/doc-lint.py [ファイル...]
引数なしなら リポジトリ直下の *.md と journal/*.md を対象にする。
ERROR が1件でもあれば終了コード 1。
"""
import sys, re, os, io, glob
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
TAG = re.compile(r"\[推測:(P-\d+)\]")
ENTRY = re.compile(r"^###\s+(P-\d+)\b")
H2 = re.compile(r"^##\s+(.*?)\s*$")
MDLINK = re.compile(r"\]\(([^)]+\.md[^)]*)\)")
JOURNALFN= re.compile(r"^\d{4}-\d{2}-\d{2}-.+\.md$")
PROMOTE = re.compile(r"^\s*[-*]\s*格上げ条件\s*[::]\s*\S")
# ルールブック自身は、アンチパターンを例として引用するため散文ヒューリスティックの対象外
PROSE_EXEMPT = {"CLAUDE.md"}
issues = []
def add(sev, path, line, msg):
issues.append((sev, path, line, msg))
def load(path):
"""コードフェンス内を空行に潰した (行番号, 本文) の列を返す。"""
raw = io.open(path, encoding="utf-8").read().splitlines()
out, fence = [], False
for i, l in enumerate(raw, 1):
if l.lstrip().startswith("```"):
fence = not fence
out.append((i, ""))
continue
out.append((i, "" if fence else l))
return out
def section(lines, title):
"""## <title> 配下の (行番号, 本文) を返す。無ければ None。"""
start = None
for idx, (n, l) in enumerate(lines):
m = H2.match(l)
if m and m.group(1) == title:
start = idx + 1
break
if start is None:
return None
body = []
for n, l in lines[start:]:
if H2.match(l):
break
body.append((n, l))
return body
def check(path):
rel = path.replace(os.sep, "/")
base = os.path.basename(rel)
is_journal = rel.startswith("journal/")
lines = load(path)
unconf = section(lines, "未確認事項")
rejected = section(lines, "検討済み・非採用")
unconf_span = set(n for n, _ in unconf) if unconf else set()
# --- ルール4: 推測タグと未確認事項の対応 ---
entries = set()
if unconf is not None:
blocks = [] # [(pid, 行番号, [本文行])] 重複IDも個別に検査する
for n, l in unconf:
m = ENTRY.match(l)
if m:
pid = m.group(1)
if pid in entries:
add("ERROR", rel, n, "未確認事項のID %s が重複している" % pid)
entries.add(pid)
blocks.append((pid, n, []))
elif blocks:
blocks[-1][2].append(l)
for pid, ln, body in blocks:
if not any(PROMOTE.match(x) for x in body):
add("ERROR", rel, ln,
"%s に「- 格上げ条件:」の行がない(何を確認すれば事実に格上げできるか)" % pid)
for n, l in lines:
if n in unconf_span:
continue
for pid in TAG.findall(l):
if unconf is None or pid not in entries:
add("ERROR", rel, n,
"本文の [推測:%s] に対応する項目が ## 未確認事項 にない" % pid)
# 逆向き(本文タグのない未確認事項)は「未解決の問い」として正当なのでエラーにしない
# --- ルール3: journal への/からのリンク禁止 ---
for n, l in lines:
for target in MDLINK.findall(l):
if target.lstrip("./").startswith("journal/"):
add("ERROR", rel, n, "journal へのリンクが張られている: %s" % target)
elif is_journal:
add("ERROR", rel, n, "journal から他ファイルへのリンクが張られている: %s" % target)
# --- ルール1: 検討済み・非採用 ---
if rejected is not None:
items = [(n, l) for n, l in rejected if l.strip().startswith(("-", "*"))]
for n, l in items:
if not re.search(r"\(journal/[^)]+\.md\)", l):
add("ERROR", rel, n,
"検討済み・非採用の項目が journal ファイル名を参照していない")
if len(items) > 5:
add("WARN", rel, items[5][0],
"検討済み・非採用が %d 件(目安5件)。古いものから落とす" % len(items))
# --- journal 固有 ---
if is_journal:
if not JOURNALFN.match(base):
add("ERROR", rel, 1, "journal のファイル名が YYYY-MM-DD-<topic>.md 形式でない")
head = " ".join(l for n, l in lines[:20])
if "結論" not in head:
add("WARN", rel, 1, "冒頭20行に結論が見当たらない(結論を先頭に置く)")
# --- ルール1: 調査・比較の混入(要目視確認) ---
if not is_journal and base not in PROSE_EXEMPT:
# 検討済み・非採用 は却下案を列挙する場所なので対象外
skip = set(n for n, _ in rejected) if rejected else set()
for n, l in lines:
if n in skip:
continue
if "メリット" in l and "デメリット" in l:
add("WARN", rel, n, "比較表らしき記述(メリット/デメリット)。journal 行きでは?")
elif re.search(r"却下|不採用|見送っ|も検討したが|案[A-Z]|[A-Z]案", l):
add("WARN", rel, n, "却下案の検討過程らしき記述。制約文に書き換えるか journal へ")
elif re.search(r"優先度", l) and l.lstrip().startswith("#"):
add("WARN", rel, n, "優先度評価は journal 行きでは?")
def main(argv):
targets = argv[1:]
if not targets:
targets = sorted(glob.glob("*.md")) + sorted(glob.glob("journal/*.md"))
if not targets:
print("対象ファイルなし"); return 0
for t in targets:
if os.path.isfile(t):
check(t)
else:
print("見つからない: %s" % t)
errs = [i for i in issues if i[0] == "ERROR"]
warns = [i for i in issues if i[0] == "WARN"]
for sev, path, line, msg in errs + warns:
print("%-5s %s:%d %s" % (sev, path, line, msg))
print("\n%d files / ERROR %d / WARN %d" % (len(targets), len(errs), len(warns)))
return 1 if errs else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+96
View File
@@ -0,0 +1,96 @@
---
name: restructure-doc
description: 既存ドキュメント1ファイルをCLAUDE.mdのルール1〜4に沿って仕分ける。事実・推測・採用案を残し、調査過程をjournalへ移す。
disable-model-invocation: true
argument-hint: <ファイル名>
---
# ドキュメントを再編する
運用ルール策定前に書かれたドキュメントを、CLAUDE.md のルール1〜4に沿った形に作り直す。
**1回の実行で1ファイルだけ**扱う。
## 最重要の制約
**情報を失わせない。** この作業は「移動と再配置」であって、要約でも削除でもない。
- 内容を短くしない。判断に迷った記述は、消さずに残してユーザーに聞く。
- メインドキュメントから外す記述は、必ず journal に移す。捨てない。
- 元の主張を勝手に言い換えない。特に断定度を上げ下げしない。
## 手順
### 1. 全体を読む
対象ファイルを最初から最後まで読む。部分的に読んで判断しない。
### 2. 4分類する
各ブロック(見出し単位、または段落単位)を次のどれかに分ける。
| 分類 | 行き先 |
|---|---|
| a. 採用した案 / 現在の設定 / 設計に影響する制約 | メインドキュメントに残す |
| b. 裏取りの済んだ事実 | メインドキュメントに残す(文中のどこでもよい) |
| c. 未確認の推測・未解決の問い | メインドキュメント末尾の `## 未確認事項` |
| d. 調査過程 / 案の比較 / 却下案 / 優先度評価 | `journal/` へ移す |
判定のヒント:
- メリット/デメリット表、優先度リスト、「〜も検討したが」 → d
- 「〜できるはず」「〜と考えられる」「実現可能」で、実際に試していない → c
- 外部仕様の記述(KLEのプロパティ定義など)は、出典を確認できていれば b、していなければ c
### 3. 分類結果を提示して承認を得る
**ここで必ず止まる。** ファイルを書き換える前に、分類の一覧をユーザーに見せて確認する。
特に b と c の境目(裏取りできているか)は自分で判断せず聞く。
### 4. メインドキュメントを書き直す
- a を中心に再構成する。b は文中に自然に置く。
- 「なぜBではないのか」の記述が残っていたら、**制約文**に書き換える。
- ✗ `階層構造はKLEが非対応なので諦めた`
- ○ `階層クラスターはKLE JSONに出力しない(KLE互換性の制約)`
- 落とした案があれば末尾 `## 検討済み・非採用` に1案1行(理由なし、journalファイル名を参照)。
### 5. 未確認事項を立てる
c に分類したものを末尾の `## 未確認事項` にまとめる。
- 本文が前提にしている推測 → 本文に `[推測:P-n]` を付け、対応する項目を置く
- まだ本文に書けない未解決の問い → 本文タグなしで項目だけ置く
- 各項目に**格上げ条件**を必ず書く。思いつかないものはユーザーに聞く(空欄で残さない)
```markdown
### P-1 KLEはlabels[9]の内容を保存時に保持する
- 位置づけ: 本文「クラスター管理」が前提にしている
- 格上げ条件: KLE本家でインポート→エクスポートし、labels[9]が残ることを確認する
```
### 6. journal へ移す
d を `journal/YYYY-MM-DD-<元ファイル名>.md` に移す。
- 冒頭に**結論**(このファイルの再編で何が確定し、何を落としたか)
- 次に案の比較(元ファイルの表をそのまま持ってきてよい)
- 以下に元の調査記述
- 整形は最低限でよい
### 7. コミットする
1ファイル1コミット。メインドキュメントと journal を同じコミットに入れる。
```
<ファイル名> を運用ルールに沿って再編
<何を残し、何をjournalへ移したか 数行>
Journal: journal/2026-09-16-<元ファイル名>.md
```
## 注意
- 複数ファイルを一度に触らない。ファイル間で内容が重複していると気づいた場合は、
勝手に統合せず、指摘だけしてそのファイルの再編を終える。
- 再編の途中で事実関係の誤りを見つけても、勝手に直さない。指摘する。
+91
View File
@@ -0,0 +1,91 @@
# Beyond KLE
キーボード設計のデータモデルとワークフローを再設計するプロジェクト。
PHY(物理配置) / ELC(電気配線) / IDX(キー識別) / MAP(キーマップ) / IME(入力変換) の5層に分離する。
- 応答・ドキュメントは日本語。
- 現状はコード未着手、設計ドキュメントのみ。
## ドキュメント地図
| ファイル | 内容 |
|---|---|
| `keyboard-design-layers.md` | レイヤー構造とツール分割方針(最上位) |
| `PHYレイヤー.md` | PHY層の仕様 |
| `phase1-json-schema.md` | 各層のJSON Schema |
| `kle-compatibility.md` | KLEとの双方向変換仕様 |
| `kle-compatibility-analysis.md` | KLE互換性の調査・案比較 |
| `keyboard-making-workflow.md` | 既存の自作キーボード制作フローとの対応 |
| `typescript-implementation-plan.md` | 実装計画・技術スタック |
**既存ドキュメントは下記ルールの策定前に書かれたもので、ルール1〜4は未適用。**
全体の再編は別タスクとして行う。それまで各ファイルをメイン/journalのどちらに分類するかは未確定なので、
指示なく移動・分割・書き直しをしない。
初期コミット以前の経緯は追跡できない。現在のドキュメント群を「初期検討段階」のベースラインとして扱い、
それ以前の決定理由を掘り起こそうとしない。
## ドキュメント運用ルール
### 1. メインドキュメント(リポジトリ直下の `*.md`)
書くもの: **採用した案 / 現在の設定 / 設計に影響する制約**。
- 複数案から選んだ場合、「なぜAなのか」を1〜3行で書く。「なぜBではないのか」は書かない。
- 却下案の比較表・検討過程・ベンチマーク結果はここに置かない(→ `journal/`)。
- 却下案の再提案を防ぐ情報は、比較ではなく**制約文**として書く。
- ✗ `B案はKLEパーサーが壊れるので却下した`
- ○ `クラスター情報はKLE JSONに出力しない(KLE互換性の制約)`
- 末尾に `## 検討済み・非採用` を置き、落とした案を**1案1行**で残す。理由は書かず、journalファイル名を参照する。
- 例: `- labels[9] へのID埋め込み (journal/2026-09-16-cluster-metadata.md)`
- コミットは `git log --oneline -- journal/2026-09-16-cluster-metadata.md` で引ける(journal削除後も追跡可)
- 目安5行まで。増えたら古いものから落とす。ここが長くなるのは書きすぎのサイン。
### 2. 履歴と根拠
- **何を変えたか** + 理由数行 → コミットメッセージ。`git log --oneline` で意思決定の流れが追えること。1コミット1決定。
- **なぜそう決めたか / 調査 / 案の比較** → `journal/` に置き、その決定と**同じコミット**に含める。
- コミットメッセージ末尾に参照を1行入れる: `Journal: journal/2026-09-16-cluster-metadata.md`
- journal は作業ツリーから削除する前提(人間にとってのノイズになるため)。履歴には残るので、
削除後は `git show <sha>:journal/...` で読む。
### 3. journal の書き方
- 1ファイル1トピック。`journal/YYYY-MM-DD-<topic>.md`。トピックを詰め込まない。
- 構成は上から順に: **結論** → **案の比較(簡潔な表)** → 検証ログ・根拠・捨てた案。
冒頭20行で結論と選択肢が分かること。
- 他のファイルから journal へリンクを張らない。journal 同士も張らない。参照経路はコミットメッセージのみ。
- 結論がメインドキュメントに反映されたら削除してよい。
- 整形は最低限でよい。人間向けの読みやすさは要求しない。
### 4. 事実と推測
- **裏取りの済んだ事実**は、ドキュメント内のどこに書いてもよい。文中に自然に混ぜてよい。
ただし同じ事実を複数ドキュメントに複製しない。必要ならリンクする。
- **未確認の推測**だけを末尾に隔離する。2種類ある:
- 本文が前提にしている推測 → 本文に `[推測:P-n]` を付け、末尾の `## 未確認事項` に対応項目を置く
- まだ本文に書けない未解決の問い → 本文タグなしで `## 未確認事項` に直接置く
- `## 未確認事項` の各項目には、**何を確認できれば事実に格上げできるか**を必ず書く。
- 検証できたらタグを外し、該当項目を削除する。
- 「今回検討して捨てた仮説」は `## 未確認事項` に書かない。journal に残して消す。
- 推測に基づいて実装しない。先に検証するか、ユーザーに確認する。
書式:
```markdown
## 未確認事項
### P-1 KLEはlabels[9]の内容を保存時に保持する
- 位置づけ: 本文「クラスター管理」が前提にしている
- 格上げ条件: KLE本家でインポート→エクスポートし、labels[9]が残ることを確認する
```
## Claude への作業指示
- ドキュメントを更新するときは、上のルール1〜4のどこに属する情報かを判断してから書く。分類に迷うものは書く前に確認する。
- 調査タスクの結果は、結論だけをメインドキュメントに書き、調査過程は `journal/` に落とす。
- 既存ドキュメントの記述と食い違う事実を見つけたら、勝手に上書きせず指摘する。
## 未確認事項
(なし)
+188
View File
@@ -0,0 +1,188 @@
# PHYレイヤー
## 概要
PHYレイヤーは、キースイッチやキーキャップ、その他周辺部品の物理的な配置に関する情報を提供する。
付加的な情報として、matrix情報(ELCレイヤー)や入力キー情報(MAP/IMEレイヤー)を含ませることができるが、必須ではない。
## 構造
### キースイッチ/キーキャップの配置
各キーはスイッチ・キャップの情報を統合された状態で持ち、キャップの位置・サイズからスイッチ座標を特定する。
以下、単に「キー」と呼ぶ場合、キーキャップの位置・サイズを表すオブジェクトをさすこととする。
### プロファイル設定
1ファイル内に複数のプロファイルを持たせることができる。
ユニットサイズ(1Uが何mmか、x軸・y軸それぞれ)を規定。
ビジュアル表現として、内側へのオフセット位置及びオフセット距離(mm or U単位)・R(U単位)を規定。
### キーの座標
キーキャップの左上角をローカル原点とする。
座標単位はU。1Uのサイズは別途指定する。
回転中心はローカル原点からの相対座標で表す。グローバル
座標・回転角度は0.25U単位・15度単位でスナップするが正規化しない。
### キーのサイズ
0.25U単位でスナップするが正規化しない。
### グルーピング
複数のキーを「クラスター」としてまとめて運用することができる。
クラスターのみにフォーカスして編集が可能。
クラスターごとの回転中心を設定できる。
**内部処理での利用**:
- クラスターごとの一括編集・移動・回転
- 階層構造による管理(クラスターの入れ子構造)
- 編集時のフォーカス制御
**外部連携での扱い**:
- KLE JSON出力時は、個別のキー配置のみを出力(クラスター情報は含めない)
- 別途クラスター情報JSONとして構造を保存
- キーIDによる紐づけで関連性を管理
### 何かある?
## 保存形式
キーごとに絶対座標を持つ。
全てのキー、またはキー及びクラスターをまとめたクラスターを配列として持つ
```ts
type Position={
x:number,
y:number,
w:number,
h:number,
rx:number,
ry:number,
ra:number
}
type Key={
position:Position,
profile:Profile
}
type Cluster={
position:Position,
clusters:(Key[]|Cluster[])
}
const keys:(Key|Cluster)[]=[]
```
## 連携用出力形式
### 設計方針
KLE向けの出力は既存のKLE JSON形式を維持し、クラスター情報は別のJSONファイルとして出力する。
両者の紐づけは、KLEのlabel部分に埋め込むユニークIDを使用する。
### KLE JSON出力
標準的なKLE JSON形式で出力。各キーのlabelにはユニークIDを含める。
```ts
type ConfigObject = {
x?: number,
y?: number,
w?: number,
h?: number,
x2?: number,
y2?: number,
w2?: number,
h2?: number,
rx?: number,
ry?: number,
ra?: number,
l?: boolean,
n?: boolean,
d?: boolean,
c?: string,
t?: string,
g?: boolean,
a?: number,
f?: number,
f2?: number,
p?: string,
s?: string
}
type KeyLabel = string // ユニークID + 表示用ラベルを含む
const kleData = (ConfigObject | KeyLabel)[][]
```
### クラスター情報JSON
キーのクラスター構造を管理する別ファイル。
```ts
interface ClusterInfo {
id: string, // クラスターのユニークID
name?: string, // 表示用クラスター名
position?: Position, // クラスター自体の位置・回転情報
keyIds: string[], // 所属するキーのIDリスト
subClusters?: string[] // 子クラスターのIDリスト(ネスト構造用)
}
interface ClusterData {
version: string,
profiles: Profile[], // プロファイル定義
keys: { // キーID -> 追加情報のマップ
[keyId: string]: {
originalLabel?: string, // KLEのlabelから抽出した表示用文字列
matrix?: MatrixPosition, // matrix情報(オプション)
mapping?: KeyMapping // キーマッピング情報(オプション)
}
},
clusters: ClusterInfo[] // クラスター構造定義
}
```
### 出力例
#### keyboard.json (KLE形式)
```json
[
["key-001\nQ", "key-002\nW", "key-003\nE"],
["key-004\nA", "key-005\nS", "key-006\nD"]
]
```
#### keyboard-clusters.json
```json
{
"version": "1.0.0",
"keys": {
"key-001": { "originalLabel": "Q", "matrix": [0, 0] },
"key-002": { "originalLabel": "W", "matrix": [0, 1] },
"key-003": { "originalLabel": "E", "matrix": [0, 2] },
"key-004": { "originalLabel": "A", "matrix": [1, 0] },
"key-005": { "originalLabel": "S", "matrix": [1, 1] },
"key-006": { "originalLabel": "D", "matrix": [1, 2] }
},
"clusters": [
{
"id": "cluster-alpha",
"name": "英字キー",
"keyIds": ["key-001", "key-002", "key-003", "key-004", "key-005", "key-006"]
}
]
}
```
### 分離設計のメリット
この設計により以下の利点が得られる:
1. **KLE互換性の維持**:既存のKLE関連ツールとの完全な互換性を保持
2. **柔軟な拡張性**:クラスター情報を独立して管理・拡張可能
3. **段階的移行**:既存のKLEユーザーが段階的に拡張機能を利用可能
4. **ツール間連携**:KLE標準形式での出力により、他のキーボード設計ツールとの相互運用性を確保
5. **データの軽量化**:KLE出力時に不要な情報を含めず、ファイルサイズを最小化
+167
View File
@@ -0,0 +1,167 @@
# Beyond KLE: キーボード設計レイヤー構造とツール設計方針
## 背景
### KLEの問題点
- **物理配置と論理構造の混同**: y座標や回転角度で行を判定するため、カラムスタッガードや曲線配列で大量の行が生成される
- **データモデルの限界**: 物理配置(PHY)のみを扱い、マトリックス座標やキーマップ情報を保持できない
- **VIAのhackyな対応**: ラベルフィールドに"1,5"と入れて`matrix:[1,5]`を表現
### 設計思想
- 「Better KLE」ではなく「Beyond KLE」: UIではなくデータモデルとワークフロー全体の再設計
- UNIX哲想の適用: 小さな役割のツールがそれぞれしっかり機能する
- 統合環境(IDE)の提供: 独立したツール群をオーケストレート
## レイヤー構造
### 1. PHYレイヤー (物理配置)
- **役割**: キーの物理的な位置、サイズ、回転
- **対応ツール**: PCBエディタ、KLE
- **出力**: 座標とキーサイズの配列
### 2. ELCレイヤー (電気配線)
- **役割**: マイコンのpinと各キーの対応
- **対応ツール**: 回路図エディタ、ファームウェア
- **出力**: ピン配列定義、マトリックス配線情報
- **備考**: charlieplexingなど非標準的な実装にも対応
### 3. IDXレイヤー (キー識別)
- **役割**: 物理位置と"キー番号"の対応付け
- **重要性**: 物理世界と論理世界を繋ぐ最も重要な抽象化層
- **出力**: `{x, y, matrix:[row,col], label}[]`
- **備考**: 現状のエコシステムで最も欠けている層
### 4. MAPレイヤー (キーマップ)
- **役割**: キー番号とキーコードの対応
- **対応ツール**: ファームウェア、VIA、Remap
- **出力**: レイヤー別キーコード配列
### 5. IMEレイヤー (入力変換)
- **役割**: キーコードと実際の入力文字の対応
- **対応ツール**: OS
- **備考**: MAPレイヤー設計時に考慮が必要
## ツール設計方針
### 個別ツール (独立動作可能)
#### PHY Editor
- 物理配置の視覚的編集
- KLE互換フォーマット対応
- 出力: PHY JSON
#### Matrix Mapper
- PHY Editorの出力を入力
- キーにrow/colを割り当て
- 視覚的にマトリックス配線を確認
- 出力: IDX JSON
#### Pin Configurator
- マトリックス定義を入力
- マイコンのピン配置を指定
- 配線図を出力
- 出力: ELC JSON
#### Keymap Designer
- キー識別子リストを入力
- レイヤー別キーコード割り当て
- VIA/Remapとの互換性
- 出力: MAP JSON
#### Firmware Generator
- 全レイヤーの情報を統合
- QMK/VIA/KMK等へ変換
- 出力: ターゲット固有の設定ファイル
### 統合環境 (IDE)
- **プロジェクトファイル**: 全レイヤーの状態を一元管理
- **リアルタイムプレビュー**: レイヤー間の依存関係を可視化
- **バージョン管理統合**: テキストベース定義でgit管理
- **ワークフローサポート**: ガイド付きウィザード、テンプレート
## プロトコル設計
### レイヤー間インターフェース
```txt
PHY → IDX
Input: KLE JSON (or compatible)
Output: Augmented layout (coordinates + key IDs)
IDX → ELC
Input: Key ID list
Output: Matrix definition + Pin mapping
IDX → MAP
Input: Key ID list
Output: Keymap arrays
All → Firmware
Output: QMK keyboard.json / VIA JSON / KMK .py
```
### データフォーマット
- JSON Schema で各レイヤーの入出力を定義
- 後方互換性の維持 (KLE JSONからのインポート)
- 拡張可能な設計 (3D配置、非標準マトリックス対応)
## 実装戦略
### Phase 1: プロトコル策定
- JSON Schemaでフォーマット定義
- TypeScriptでリファレンス実装
- CLIツールとして動作確認
### Phase 2: 個別ツール開発
- 各レイヤー用エディタを独立開発
- 既存ツールからのインポート機能
- 単独利用可能な設計
### Phase 3: IDE統合
- Electron/Tauri or Webアプリ
- プロジェクト管理機能
- ワークフロー支援
## 既存ツールとの関係
- **KLE**: PHY Editorの一実装、インポート対応
- **VIA/Remap**: MAP Layerクライアントとして連携
- **QMK Configurator**: Firmware Generatorのターゲット
## 配慮事項
### 学習コスト
- テンプレートプロジェクト提供 (60%, Split等)
- 簡易モード/詳細モードの切り替え
- 段階的な機能開示
### 互換性
- 既存KLE JSONの完全サポート
- 段階的移行パスの提供
- レガシーツールとの共存
## 次のステップ
1. プロトコル仕様ドラフト作成
2. GitHubでRFCリポジトリ立ち上げ
3. 小規模コミュニティでフィードバック収集
4. リファレンス実装の開発
+157
View File
@@ -0,0 +1,157 @@
# 自作キーボード制作ワークフロー分析
## 現在の一般的な制作手順
### 1. デザイン・設計段階
#### a) レイアウト設計
- **ツール**: KLE (Keyboard Layout Editor)
- **作業内容**:
- キーの物理配置を決定
- キーサイズ・間隔・角度を調整
- 視覚的な配置確認
- **出力**: KLE JSON ファイル
- **Beyond KLE対応**: **PHYレイヤー**
#### b) 回路設計・マトリックス設計
- **ツール**: 手動計算、KiCad等の回路図エディタ
- **作業内容**:
- キーマトリックスの行列構成を決定
- マイコンピンとの対応付け
- ダイオード配置の検討
- **出力**: 回路図、ピン配置表
- **現在の課題**: KLEからマトリックス情報への変換が手動
- **Beyond KLE対応**: **ELCレイヤー** + **IDXレイヤー**
### 2. PCB設計段階
#### a) フットプリント配置
- **ツール**: KiCad、Eagle、Fusion360等
- **作業内容**:
- KLE JSONから座標を抽出(手動またはスクリプト)
- スイッチフットプリントの配置
- マイコン・コネクタ等の部品配置
- **現在の課題**: KLEとPCBツール間の座標変換
- **Beyond KLE対応**: **PHYレイヤー** → PCBツール連携
#### b) 配線・ルーティング
- **ツール**: PCB CADソフト
- **作業内容**:
- マトリックス配線の実装
- 電源・信号線のルーティング
- **Beyond KLE対応**: **ELCレイヤー**情報の活用
### 3. ファームウェア設定段階
#### a) ファームウェア基本設定
- **ツール**: QMK、KMK、VIA等
- **作業内容**:
- マトリックス定義(ROW/COL)
- ピン配置設定
- 物理レイアウト定義
- **現在の課題**:
- KLEとファームウェア設定の手動変換
- VIAではlabelフィールドに"1,5"等を記入してmatrix座標を表現
- **Beyond KLE対応**: **IDXレイヤー** + **ELCレイヤー** → ファームウェア自動生成
#### b) キーマップ設定
- **ツール**: QMK Configurator、VIA、Remap等
- **作業内容**:
- 各キーへのキーコード割り当て
- レイヤー設計
- マクロ・特殊機能の設定
- **Beyond KLE対応**: **MAPレイヤー**
### 4. 製造・組み立て段階
#### a) PCB製造
- **出力**: Gerberファイル、BOM、Pick&Place
- **Beyond KLE対応**: PHY/ELCレイヤーからの自動生成
#### b) 部品調達・組み立て
- **作業内容**: はんだ付け、組み立て、動作確認
## 現在のワークフローの問題点
### 1. データの分断
- **問題**: KLE → PCB → ファームウェアで毎回手動変換
- **影響**: 一箇所変更時の全体への反映が困難
### 2. マトリックス情報の欠落
- **問題**: KLEは物理配置のみ、論理構造(マトリックス)は別管理
- **影響**: VIAでのhackyな対応(labelフィールド悪用)
### 3. 設計変更時のトレーサビリティ不足
- **問題**: どのレイヤーでの変更が他に影響するかが不明確
- **影響**: デバッグ・修正作業の複雑化
### 4. ツール間連携の脆弱性
- **問題**: 手動変換によるヒューマンエラー
- **影響**: 設計ミス、製造後の修正コスト
## Beyond KLEによる解決策
### 1. 統一データモデル
```
物理設計(PHY) → キー識別(IDX) → 電気設計(ELC) → キーマップ(MAP)
↓ ↓ ↓ ↓
PCBツール ファームウェア 回路図 VIA/Remap
```
### 2. レイヤー間の明確な責任分離
- **PHY**: 物理配置のみに集中
- **IDX**: 物理と論理の橋渡し(最重要レイヤー)
- **ELC**: 電気的接続の明確化
- **MAP**: キーマップロジックの分離
### 3. 双方向の依存関係管理
- 上流レイヤーの変更が下流に自動反映
- 下流での制約が上流設計にフィードバック
### 4. 既存ツールとの互換性維持
- KLE JSONの完全サポート
- VIA/QMK等への出力対応
- 段階的移行の実現
## Phase 1で策定すべきプロトコル
### 1. レイヤー間インターフェース仕様
- 各レイヤーの入出力JSON Schema
- 依存関係の明確化
- エラーハンドリング規則
### 2. 既存フォーマットとの変換仕様
- KLE JSON → PHY変換
- QMK設定 ← IDX+ELC+MAP変換
- VIA JSON ← 全レイヤー統合
### 3. 拡張性設計
- 非標準マトリックス(charlieplexing等)対応
- 3D配置への拡張可能性
- カスタムキーコードサポート
## 次のアクション
1. **JSON Schema策定**: 各レイヤーのデータ構造定義
2. **変換ロジック設計**: レイヤー間の変換アルゴリズム
3. **リファレンス実装**: TypeScript/Rust等でのプロトタイプ
4. **テストケース作成**: 代表的なキーボード設計での検証
+453
View File
@@ -0,0 +1,453 @@
# PHYレイヤーとKLE(Keyboard Layout Editor)の互換性分析
## 概要
検討中のPHYレイヤー構造とKeyboard Layout Editor(KLE)の互換性について詳細に分析した結果をまとめます。KLEをベースとしたフロントエンドの実現可能性を評価します。
## KLEデータモデルの特徴
### KLEで対応可能な主要プロパティ
KLEの`Key`オブジェクトは以下の構造を持ちます:
```typescript
export class Key {
// 位置・サイズ関連
x: number; // 絶対位置 (U単位)
y: number; // 絶対位置 (U単位)
width: number; // サイズ (U単位)
height: number; // サイズ (U単位)
// 回転関連
rotation_x: number; // 回転中心X座標
rotation_y: number; // 回転中心Y座標
rotation_angle: number; // 回転角度
// 複雑な形状対応
x2: number; // 第2矩形の相対位置
y2: number; // 第2矩形の相対位置
width2: number; // 第2矩形のサイズ
height2: number; // 第2矩形のサイズ
// 表示・装飾関連
color: string; // キーキャップ色
labels: string[]; // 最大12個のラベル
textColor: Array<string | undefined>;
textSize: Array<number | undefined>;
profile: string; // キープロファイル
// フラグ類
decal: boolean; // デカールモード
ghost: boolean; // ゴーストモード
stepped: boolean; // ステップキー
nub: boolean; // ホーミング突起
// スイッチ情報
sm: string; // スイッチマウント
sb: string; // スイッチブランド
st: string; // スイッチタイプ
}
```
## 互換性マトリックス
### ✅ 完全対応可能
| PHYレイヤー機能 | KLEプロパティ | 備考 |
|---|---|---|
| キー座標(x,y) | `x`, `y` | 1:1対応、U単位 |
| キーサイズ(w,h) | `width`, `height` | 1:1対応、U単位 |
| 回転角度 | `rotation_angle` | 1:1対応 |
| 回転中心 | `rotation_x`, `rotation_y` | 1:1対応 |
| 0.25U単位スナップ | - | KLEも小数点座標対応 |
| 複雑形状キー | `x2`, `y2`, `width2`, `height2` | ISO Enter等に対応 |
| プロファイル設定 | `profile` | SA、DCS、OEM等対応 |
### ⚠️ 部分対応・要改造
| PHYレイヤー機能 | KLE対応状況 | 改造内容 |
|---|---|---|
| クラスター管理 | 非対応 | カスタムメタデータとしてlabelsに格納 |
| 複数プロファイル | 一部対応 | メタデータで管理、UI拡張必要 |
| ユニットサイズ設定 | 非対応 | グローバル設定として追加実装 |
| ビジュアルオフセット | 非対応 | レンダリング層で独自実装 |
### ❌ 対応困難・KLE制約
| PHYレイヤー機能 | KLE制約 | 回避策 |
|---|---|---|
| 15度以外の回転角度 | UI制約(内部は対応可能) | UI改造またはAPI直接操作 |
| 階層的クラスター | データモデル非対応 | ゴーストキーによるメタデータ保存 |
| クラスター別回転中心 | 直接非対応 | 各キーの回転中心で代替 |
## 具体的な実装戦略
### 1. KLEベースデータ変換
PHYレイヤーからKLE形式への変換:
```typescript
// PHYレイヤー → KLE変換例
function convertToKLE(phyData: PHYLayer): KLEData {
return phyData.elements.map(element => {
if (element.type === 'key') {
return {
x: element.position.x,
y: element.position.y,
width: element.size.width,
height: element.size.height,
rotation_x: element.position.rotationOrigin?.x || 0,
rotation_y: element.position.rotationOrigin?.y || 0,
rotation_angle: element.position.rotation || 0,
// クラスター情報をlabelsの特定位置に格納
labels: [element.clusterId || '', '', '', '', '', '', '', '', '', '', '', ''],
// プロファイル情報
profile: element.profile || 'OEM'
};
}
// クラスターの場合は展開処理
});
}
```
### 2. KLE UI改造点
#### 必要な改造項目
1. **クラスター管理UI**
- クラスター作成・編集パネル
- クラスター単位の選択・移動
- クラスター階層表示
2. **プロファイル管理**
- 複数プロファイル切り替えUI
- プロファイル別ユニットサイズ設定
3. **メタデータ拡張**
- PHYレイヤー固有情報の保存
- 互換性情報の管理
#### UI改造の難易度
- **低難易度**: 既存プロパティパネルの拡張
- **中難易度**: クラスター管理機能の追加
- **高難易度**: レンダリングエンジンの改造
### 3. 制限事項と回避策
#### 現在のKLE制約
1. **データモデル制約**
- フラットな配列構造のみ対応
- 階層的クラスター管理は困難
2. **レンダリング制約**
- HTMLベースのレンダリング
- 複雑な視覚効果には限界
#### 回避策
1. **段階的対応**
- Phase 1: 基本的な位置・サイズ・回転のみ対応
- Phase 2: クラスター管理機能追加
- Phase 3: 高度なビジュアル機能
2. **ハイブリッドアプローチ**
- KLEをベースエディタとして使用
- 独自機能は別パネルで管理
- 最終出力時にPHYレイヤー形式で統合
## 推奨実装アプローチ
### フェーズ1: KLE最小改造アプローチ(推奨)
1. **KLEの既存機能をそのまま活用**
- 基本的な配置・サイズ・回転
- プロファイル設定
- 色・ラベル設定
2. **最小限の拡張**
- カスタムメタデータ保存機能
- PHYレイヤー形式エクスポート
3. **段階的機能追加**
- クラスター管理(Phase 2)
- 高度なビジュアル機能(Phase 3)
### 実装優先順位
1. **高優先度** - 即座に対応可能
- ✅ 基本位置・サイズ・回転
- ✅ プロファイル設定
- ✅ KLE→PHYレイヤー変換
2. **中優先度** - 改造が必要だが実現可能
- ⚠️ 簡易クラスター管理
- ⚠️ 複数プロファイル対応
- ⚠️ カスタムユニットサイズ
3. **低優先度** - 大規模改造が必要
- ❌ 階層的クラスター
- ❌ 高度なビジュアルオフセット
- ❌ リアルタイム3Dプレビュー
## 結論
**KLEをベースとしたPHYレイヤーエディタの実現は十分可能**です。
### 対応可能な機能(約80%)
- 基本的なキー配置・サイズ・回転
- 複雑形状キー(ISO Enter等)
- プロファイル管理
- 基本的なクラスター管理
### 改造が必要な機能(約15%)
- 高度なクラスター管理
- 複数プロファイル切り替え
- カスタムユニットサイズ
### 対応困難な機能(約5%)
- 完全階層的クラスター構造
- 高度なビジュアルエフェクト
PHYレイヤーの主要機能はKLEで十分実現可能であり、段階的な改造により完全対応も期待できます。
## 階層型クラスターの実装戦略
### 問題提起
ユーザーからの要求:
- **階層型クラスター管理は絶対に実装したい**
- 出力時はフラット化してもOK(階層情報を何らかの形で記録できれば)
- ラベルに突っ込むと見づらくなる懸念
- **ゴーストキーによる情報保存**のアイデア
### ゴーストキーアプローチの詳細設計
#### 基本概念
クラスター情報を`ghost: true`のキーとして保存し、実際のキーレンダリングには影響を与えずにメタデータとして扱う。
```typescript
// クラスター情報保存用ゴーストキー
type ClusterGhostKey = {
ghost: true,
decal: false,
x: number, // クラスター範囲の左上角
y: number, // クラスター範囲の左上角
width: number, // クラスター範囲の幅
height: number, // クラスター範囲の高さ
rotation_x: number, // クラスター回転中心
rotation_y: number, // クラスター回転中心
rotation_angle: number, // クラスター回転角度
color: "#transparent", // 透明
labels: [
"CLUSTER_META", // [0] 識別子
clusterId, // [1] クラスターID
parentClusterId, // [2] 親クラスターID(階層用)
clusterName, // [3] クラスター名
JSON.stringify(metadata), // [4] その他メタデータ
"", "", "", "", "", "", "" // [5-11] 予約
]
}
```
#### 階層構造の表現
```typescript
// 例:分割キーボードの左右ブロック + 各ブロック内のキークラスター
const clusterHierarchy = {
"root": {
children: ["left_block", "right_block"]
},
"left_block": {
parent: "root",
children: ["left_alpha", "left_modifiers"]
},
"right_block": {
parent: "root",
children: ["right_alpha", "right_modifiers"]
},
"left_alpha": {
parent: "left_block",
keyIds: ["Q", "W", "E", "R", "T", "A", "S", "D", "F", "G", ...]
}
// ...
}
```
#### KLE UI拡張による階層管理
1. **クラスター管理パネル**
```html
<!-- 新しいサイドパネル -->
<div id="cluster-manager">
<tree-view>
<cluster-node id="root">
<cluster-node id="left_block">
<cluster-node id="left_alpha" />
<cluster-node id="left_modifiers" />
</cluster-node>
<cluster-node id="right_block">
<cluster-node id="right_alpha" />
<cluster-node id="right_modifiers" />
</cluster-node>
</cluster-node>
</tree-view>
</div>
```
2. **クラスター操作機能**
- ドラッグ&ドロップによるキーのクラスター移動
- 階層ツリーでのクラスター選択
- クラスター単位での一括変換(移動・回転・リサイズ)
- クラスター単位でのコピー&ペースト
### データフロー設計
#### 1. 編集時(UI → 内部データ)
```javascript
// クラスター作成時
function createCluster(name, parentId, selectedKeys) {
const clusterId = generateUniqueId();
// ゴーストキーを作成
const ghostKey = createClusterGhostKey({
id: clusterId,
name: name,
parentId: parentId,
boundingBox: calculateBoundingBox(selectedKeys)
});
// 選択されたキーにクラスターIDを付与
selectedKeys.forEach(key => {
key.clusterId = clusterId; // カスタムプロパティとして追加
});
// KLEのキー配列に追加
scope.keys().push(ghostKey);
}
```
#### 2. 保存時(内部データ → KLE JSON)
```javascript
function serializeWithClusters(keyboard) {
const kleData = serial.serialize(keyboard);
// 通常のキーとゴーストキーが混在した状態で保存
// ゴーストキーがクラスター情報を保持
return kleData;
}
```
#### 3. 読み込み時(KLE JSON → 内部データ)
```javascript
function deserializeWithClusters(kleData) {
const keyboard = serial.deserialize(kleData);
// ゴーストキーからクラスター情報を復元
const clusterMap = new Map();
const normalKeys = [];
keyboard.keys.forEach(key => {
if (key.ghost && key.labels[0] === 'CLUSTER_META') {
const clusterId = key.labels[1];
const parentId = key.labels[2];
const metadata = JSON.parse(key.labels[4]);
clusterMap.set(clusterId, {
ghostKey: key,
parentId: parentId,
metadata: metadata,
children: new Set()
});
} else {
normalKeys.push(key);
}
});
// 階層構造を再構築
buildClusterHierarchy(clusterMap);
return { keyboard: { ...keyboard, keys: normalKeys }, clusters: clusterMap };
}
```
### 15度回転制約について
#### 現在の制約の詳細
KLEのソースコード分析結果:
```javascript
$scope.rotateStep = 15; // UI上のデフォルト回転ステップ
// 実際の制約チェック(kb.js内)
rotation_angle: function() {
return Math.max(-180, Math.min(180, value)); // ±180度の範囲制約のみ
}
```
**つまり:**
- **UIでは15度ステップで回転操作**(ユーザビリティのため)
- **内部データには制約なし**(任意の角度を保存可能)
- **JSON出力も制約なし**(小数点を含む任意の角度値)
#### PHYレイヤーでの対応
```javascript
// PHY → KLE変換時に任意角度をそのまま渡す
function convertRotation(phyRotation) {
return phyRotation; // 制約なし、そのまま変換
}
// KLE UI拡張で任意角度入力を許可
function enableCustomRotation() {
// 既存の15度ステップに加えて、直接入力機能を追加
scope.customRotateStep = 1; // 1度ステップ
scope.allowCustomAngle = true; // 任意角度入力許可
}
```
## 実装優先度の再評価
### 修正版優先度
1. **最高優先度** - 即座に対応可能
- ✅ 基本位置・サイズ・回転(任意角度対応)
- ✅ プロファイル設定
- ✅ KLE→PHYレイヤー変換
2. **高優先度** - ゴーストキーアプローチで対応
- 🔶 階層型クラスター管理(ゴーストキー方式)
- 🔶 クラスターメタデータ保存
- 🔶 クラスター単位操作UI
3. **中優先度** - UI改造が必要
- ⚠️ 複数プロファイル対応
- ⚠️ カスタムユニットサイズ
- ⚠️ 15度以外の回転角度UI対応
4. **低優先度** - 大規模改造が必要
- ❌ 高度なビジュアルオフセット
- ❌ リアルタイム3Dプレビュー
### 実装結論
**ゴーストキーアプローチにより、階層型クラスター管理は実現可能**です。この方法なら:
- KLEの既存データモデルを活用
- 見た目に影響しない形でメタデータ保存
- 階層構造の完全な再現が可能
- 段階的な実装が可能
階層型クラスターが「絶対実装したい」要求に対して、技術的に実現可能な解決策を提供できます。
+249
View File
@@ -0,0 +1,249 @@
# KLE JSON 互換性設計
## 双方向変換戦略
### Beyond KLE → KLE JSON
#### 1. フラット化プロセス
```typescript
interface ConversionOptions {
preserveIds: boolean; // IDを保持するか
preserveClusterInfo: boolean; // クラスター情報を保持するか
idEmbedMethod: 'metadata' | 'label' | 'comment';
}
function convertToKLE(beyondKle: PHYLayer, options: ConversionOptions): KLELayout {
const flatKeys = flattenElements(beyondKle.elements);
return {
name: beyondKle.metadata.name,
keys: flatKeys.map(key => convertKey(key, options))
};
}
function flattenElements(elements: Element[], parentTransform?: Transform): FlatKey[] {
const result: FlatKey[] = [];
for (const element of elements) {
if (element.type === 'key') {
const absolutePos = applyTransform(element.position, parentTransform);
result.push({
...element,
absolutePosition: absolutePos,
clusterPath: getClusterPath(element)
});
} else if (element.type === 'cluster') {
const clusterTransform = combineTransforms(parentTransform, element.position);
result.push(...flattenElements(element.elements, clusterTransform));
}
}
return result;
}
```
#### 2. ID保持方法の比較
| 方法 | メリット | デメリット | KLE互換性 |
|------|----------|------------|-----------|
| `_beyondkle` メタデータ | 構造化された情報保持 | KLEパーサーが無視する可能性 | △ |
| ラベル9番目位置 | 確実に保存される | 表示されてしまう | ○ |
| コメントフィールド | 非表示 | 標準的でない | △ |
#### 3. 推奨アプローチ: ハイブリッド方式
```json
{
"name": "Split Layout (Generated by Beyond KLE)",
"_beyondkle": {
"version": "1.0.0",
"sourceFile": "layout.beyond.json",
"conversionSettings": {
"flattenClusters": true,
"preserveRotations": true
}
},
"keys": [
{
"x": 0, "y": 0, "w": 1, "h": 1,
"t": "#000000\n\n\n\n\n\n\n\n\nk00",
"_beyondkle": {
"id": "k00",
"clusterPath": ["left_half", "left_main"],
"relativePosition": { "x": 0, "y": 0 },
"absolutePosition": { "x": 0, "y": 0 }
}
}
]
}
```
### KLE JSON → Beyond KLE
#### 1. ID復元戦略
```typescript
function convertFromKLE(kleLayout: KLELayout): PHYLayer {
// 1. メタデータからBeyond KLE情報を取得
const beyondKleInfo = kleLayout._beyondkle;
if (beyondKleInfo?.sourceFile) {
// 元のBeyond KLEファイルが指定されている場合
return reconstructFromSource(kleLayout, beyondKleInfo);
}
// 2. キーからIDとクラスター情報を抽出
const keys = kleLayout.keys.map(kleKey => {
const id = extractId(kleKey);
const clusterInfo = extractClusterInfo(kleKey);
return {
type: 'key' as const,
id: id || generateId(kleKey),
profile: 'default',
position: { x: kleKey.x, y: kleKey.y },
size: { width: kleKey.w || 1, height: kleKey.h || 1 },
metadata: extractMetadata(kleKey)
};
});
// 3. クラスター構造の再構築
return reconstructClusters(keys);
}
function extractId(kleKey: any): string | null {
// 1. _beyondkle メタデータから
if (kleKey._beyondkle?.id) return kleKey._beyondkle.id;
// 2. ラベル9番目から
if (kleKey.labels?.[8]) return kleKey.labels[8];
// 3. t文字列の解析
const tMatch = kleKey.t?.match(/\n([a-zA-Z0-9_]+)$/);
if (tMatch) return tMatch[1];
return null;
}
```
#### 2. クラスター再構築アルゴリズム
```typescript
function reconstructClusters(flatKeys: Key[]): Element[] {
// クラスター情報が保持されている場合
const clusteredKeys = groupByCluster(flatKeys);
if (Object.keys(clusteredKeys).length > 1) {
return Object.entries(clusteredKeys).map(([clusterName, keys]) => ({
type: 'cluster' as const,
id: clusterName,
name: clusterName,
position: calculateClusterOrigin(keys),
elements: keys.map(key => ({
...key,
position: makeRelativeToCluster(key.position, clusterOrigin)
}))
}));
}
// クラスター情報がない場合は自動推定
return autoDetectClusters(flatKeys);
}
function autoDetectClusters(keys: Key[]): Element[] {
// 座標の近さとレイアウトパターンからクラスターを推定
// 1. 連続する矩形領域の検出
// 2. 回転角度の類似性
// 3. 一定の間隔での分離
const clusters = detectSpatialClusters(keys);
if (clusters.length === 1) {
// 単一クラスターの場合はフラットに
return keys;
}
return clusters;
}
```
## 実装における考慮事項
### 1. バージョン管理
```json
{
"_beyondkle": {
"version": "1.0.0",
"generator": "beyond-kle-converter",
"timestamp": "2025-11-09T12:00:00Z",
"preservedData": {
"clusters": true,
"rotations": true,
"profiles": false
}
}
}
```
### 2. 設定可能な変換オプション
```typescript
interface ConversionSettings {
// ID保持方法
idPreservation: 'metadata' | 'label' | 'both' | 'none';
// クラスター処理
clusterHandling: 'preserve' | 'flatten' | 'auto';
// 回転の処理
rotationHandling: 'preserve' | 'flatten';
// プロファイル情報
includeProfiles: boolean;
// 互換性レベル
compatibilityLevel: 'strict' | 'extended' | 'beyondkle';
}
```
### 3. 往復変換の整合性保証
```typescript
// テストケース
const originalBeyondKLE = loadBeyondKLE('test.beyond.json');
const kleJson = convertToKLE(originalBeyondKLE, settings);
const reconstructed = convertFromKLE(kleJson);
// 構造的等価性の検証
assert(structurallyEquivalent(originalBeyondKLE, reconstructed));
```
## ワークフロー例
### 1. Beyond KLE → 外部ツール → Beyond KLE
```bash
# Beyond KLE → KLE JSON (ID付き)
beyond-kle export --format=kle --preserve-ids layout.beyond.json
# 外部ツール(KLE等)で編集
# ...
# KLE JSON → Beyond KLE (構造復元)
beyond-kle import --format=kle --auto-cluster edited.kle.json
```
### 2. 段階的移行
```bash
# 既存のKLE JSON → Beyond KLE
beyond-kle import legacy.kle.json
# Beyond KLE で編集・管理
# 必要に応じてKLE JSONで共有
beyond-kle export --format=kle-compatible project.beyond.json
```
この設計により、Beyond KLEの高度な機能を使いながらも、既存のKLEエコシステムとの互換性を保てると思いますが、いかがでしょうか?
+483
View File
@@ -0,0 +1,483 @@
# Phase 1: JSON Schema仕様策定
## レイヤー間データフロー
```
PHY (物理配置) → IDX (キー識別) → ELC (電気配線) + MAP (キーマップ)
↓ ↓
ファームウェア生成 VIA/Remap出力
```
## 1. PHYレイヤー JSON Schema
### 基本構造
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://beyond-kle.org/schemas/phy-layer.json",
"title": "PHY Layer - Physical Key Layout",
"description": "Physical positioning and sizing of keys",
"type": "object",
"required": ["version", "metadata", "profiles", "elements"],
"properties": {
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+$",
"description": "Schema version (semver)"
},
"metadata": {
"type": "object",
"properties": {
"name": { "type": "string" },
"author": { "type": "string" },
"description": { "type": "string" },
"created": { "type": "string", "format": "date-time" },
"modified": { "type": "string", "format": "date-time" }
}
},
"compatibility": {
"type": "object",
"description": "Information for external tool compatibility",
"properties": {
"kleJson": {
"type": "object",
"properties": {
"source": { "type": "string" },
"checksum": { "type": "string" },
"preservedFields": { "type": "array", "items": { "type": "string" } }
}
},
"originalFormat": {
"type": "string",
"enum": ["kle", "via", "qmk", "native"]
}
}
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profile"
}
},
"elements": {
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/$defs/key" },
{ "$ref": "#/$defs/cluster" }
]
}
}
},
"$defs": {
"profile": {
"type": "object",
"required": ["id", "name", "unitSize"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"unitSize": {
"type": "object",
"required": ["x", "y"],
"properties": {
"x": { "type": "number", "minimum": 0 },
"y": { "type": "number", "minimum": 0 }
}
},
"visualOffset": {
"type": "object",
"properties": {
"x": { "type": "number" },
"y": { "type": "number" },
"radius": { "type": "number", "minimum": 0 }
}
}
}
},
"key": {
"type": "object",
"required": ["type", "id", "profile", "position", "size"],
"properties": {
"type": {
"type": "string",
"const": "key"
},
"id": {
"type": "string",
"description": "Unique key identifier"
},
"profile": {
"type": "string",
"description": "Reference to profile ID"
},
"position": {
"type": "object",
"required": ["x", "y"],
"properties": {
"x": { "type": "number" },
"y": { "type": "number" },
"rotation": { "type": "number" },
"rotationOrigin": {
"type": "object",
"properties": {
"x": { "type": "number" },
"y": { "type": "number" }
}
}
}
},
"size": {
"type": "object",
"required": ["width", "height"],
"properties": {
"width": { "type": "number", "minimum": 0 },
"height": { "type": "number", "minimum": 0 }
}
},
"metadata": {
"type": "object",
"properties": {
"label": { "type": "string" },
"color": { "type": "string" },
"textColor": { "type": "string" },
"kleCompatibility": {
"type": "object",
"description": "Original KLE fields for round-trip compatibility",
"properties": {
"originalIndex": { "type": "integer" },
"labels": { "type": "array", "items": { "type": "string" } },
"textSizes": { "type": "array", "items": { "type": "number" } },
"decalData": { "type": "object" }
}
}
}
}
}
},
"cluster": {
"type": "object",
"required": ["type", "id", "name", "elements"],
"properties": {
"type": {
"type": "string",
"const": "cluster"
},
"id": { "type": "string" },
"name": { "type": "string" },
"position": {
"type": "object",
"properties": {
"x": { "type": "number" },
"y": { "type": "number" },
"rotation": { "type": "number" },
"rotationOrigin": {
"type": "object",
"properties": {
"x": { "type": "number" },
"y": { "type": "number" }
}
}
}
},
"elements": {
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/$defs/key" },
{ "$ref": "#/$defs/cluster" }
]
}
}
}
}
}
}
```
### 使用例
```json
{
"version": "1.0.0",
"metadata": {
"name": "Split Keyboard Layout",
"author": "John Doe",
"description": "Ergonomic split keyboard with rotated thumb clusters",
"created": "2025-01-01T00:00:00Z"
},
"profiles": [
{
"id": "standard",
"name": "Standard Cherry MX",
"unitSize": { "x": 19.05, "y": 19.05 },
"visualOffset": { "x": 1, "y": 1, "radius": 1 }
}
],
"elements": [
{
"type": "cluster",
"id": "left_main",
"name": "Left Main Block",
"position": {
"x": 0, "y": 0,
"rotation": 0,
"rotationOrigin": { "x": 3, "y": 2 }
},
"elements": [
{
"type": "key",
"id": "k00",
"profile": "standard",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"metadata": { "label": "Q" }
},
{
"type": "key",
"id": "k01",
"profile": "standard",
"position": { "x": 1, "y": 0 },
"size": { "width": 1, "height": 1 },
"metadata": { "label": "W" }
}
]
},
{
"type": "cluster",
"id": "left_thumb",
"name": "Left Thumb Cluster",
"position": {
"x": 2, "y": 3,
"rotation": 15,
"rotationOrigin": { "x": 1, "y": 0.5 }
},
"elements": [
{
"type": "key",
"id": "k30",
"profile": "standard",
"position": { "x": 0, "y": 0 },
"size": { "width": 1.25, "height": 1 },
"metadata": { "label": "Space" }
},
{
"type": "key",
"id": "k31",
"profile": "standard",
"position": { "x": 1.25, "y": 0 },
"size": { "width": 1, "height": 1 },
"metadata": { "label": "Enter" }
}
]
}
]
}
```
## 2. IDXレイヤー JSON Schema
### 基本構造
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://beyond-kle.org/schemas/idx-layer.json",
"title": "IDX Layer - Key Identification",
"description": "Mapping between physical keys and logical identifiers",
"type": "object",
"required": ["version", "phyReference", "keyMap"],
"properties": {
"version": { "type": "string" },
"phyReference": {
"type": "object",
"required": ["source", "checksum"],
"properties": {
"source": { "type": "string" },
"checksum": { "type": "string" }
}
},
"keyMap": {
"type": "array",
"items": {
"$ref": "#/$defs/keyMapping"
}
}
},
"$defs": {
"keyMapping": {
"type": "object",
"required": ["physicalId", "logicalId"],
"properties": {
"physicalId": {
"type": "string",
"description": "Reference to PHY layer key ID"
},
"logicalId": {
"type": "object",
"required": ["index"],
"properties": {
"index": { "type": "integer", "minimum": 0 },
"matrix": {
"type": "object",
"properties": {
"row": { "type": "integer", "minimum": 0 },
"col": { "type": "integer", "minimum": 0 }
}
},
"label": { "type": "string" }
}
}
}
}
}
}
```
## 3. ELCレイヤー JSON Schema
### 基本構造
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://beyond-kle.org/schemas/elc-layer.json",
"title": "ELC Layer - Electrical Configuration",
"description": "Electrical wiring and pin mapping",
"type": "object",
"required": ["version", "idxReference", "microcontroller", "matrix"],
"properties": {
"version": { "type": "string" },
"idxReference": {
"type": "object",
"required": ["source", "checksum"],
"properties": {
"source": { "type": "string" },
"checksum": { "type": "string" }
}
},
"microcontroller": {
"type": "object",
"required": ["type", "pinMapping"],
"properties": {
"type": { "type": "string" },
"pinMapping": {
"type": "object",
"properties": {
"rows": {
"type": "array",
"items": { "type": "string" }
},
"cols": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
},
"matrix": {
"type": "object",
"required": ["type", "dimensions"],
"properties": {
"type": {
"type": "string",
"enum": ["standard", "charlieplexing", "direct"]
},
"dimensions": {
"type": "object",
"required": ["rows", "cols"],
"properties": {
"rows": { "type": "integer", "minimum": 1 },
"cols": { "type": "integer", "minimum": 1 }
}
}
}
}
}
}
```
## 4. MAPレイヤー JSON Schema
### 基本構造
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://beyond-kle.org/schemas/map-layer.json",
"title": "MAP Layer - Keymap Configuration",
"description": "Key code assignments and layers",
"type": "object",
"required": ["version", "idxReference", "layers"],
"properties": {
"version": { "type": "string" },
"idxReference": {
"type": "object",
"required": ["source", "checksum"],
"properties": {
"source": { "type": "string" },
"checksum": { "type": "string" }
}
},
"layers": {
"type": "array",
"items": {
"$ref": "#/$defs/layer"
}
}
},
"$defs": {
"layer": {
"type": "object",
"required": ["id", "name", "keymap"],
"properties": {
"id": { "type": "integer", "minimum": 0 },
"name": { "type": "string" },
"keymap": {
"type": "array",
"items": {
"$ref": "#/$defs/keyAssignment"
}
}
}
},
"keyAssignment": {
"type": "object",
"required": ["keyIndex", "keycode"],
"properties": {
"keyIndex": { "type": "integer" },
"keycode": { "type": "string" }
}
}
}
}
```
## 変換フロー設計
### PHY → IDX変換
1. PHYレイヤーから物理キーリストを取得
2. 各キーに論理IDを割り当て
3. マトリックス座標の自動生成(行列最適化)
### IDX → ELC変換
1. マトリックス次元を算出
2. ピン配置の最適化提案
3. 配線効率の検証
### IDX + ELC → MAP変換
1. キーインデックス配列の生成
2. デフォルトキーマップの提案
3. VIA/QMK形式への出力
## 次のステップ
1. **TypeScriptでの型定義作成**
2. **変換ロジックのプロトタイプ実装**
3. **テストケース(60%、Split等)での検証**
4. **既存KLE JSONからの変換機能**
+203
View File
@@ -0,0 +1,203 @@
# Beyond KLE: TypeScript実装計画
## プロジェクト構造
```
beyond-kle/
├── packages/
│ ├── core/ # 共通型定義とユーティリティ
│ │ ├── src/
│ │ │ ├── types/ # 各レイヤーの型定義
│ │ │ ├── validators/ # JSON Schema バリデーター
│ │ │ └── utils/ # 共通ユーティリティ
│ │ └── package.json
│ ├── phy-editor/ # PHYレイヤーエディタ
│ ├── matrix-mapper/ # IDX/ELCレイヤーマッパー
│ ├── keymap-designer/ # MAPレイヤーデザイナー
│ ├── converter/ # 既存フォーマット変換
│ └── firmware-gen/ # ファームウェア生成
├── tools/
│ ├── cli/ # CLI ツール
│ └── web-editor/ # Web統合エディタ
├── schemas/ # JSON Schema定義
├── examples/ # サンプルプロジェクト
└── docs/ # ドキュメント
```
## 開発フェーズ
### Phase 1a: 基盤実装
1. **型定義とSchema**
- TypeScript型定義
- JSON Schema validation
- テスト用サンプルデータ
2. **変換エンジン**
- KLE JSON → PHY変換
- PHY → IDX自動生成
- QMK/VIA出力
3. **CLIツール**
- 変換機能のコマンドライン提供
- バリデーション機能
### Phase 1b: エディタプロトタイプ
1. **PHY Editor (Web)**
- React + Canvas での編集機能
- KLE互換インポート
2. **Matrix Mapper**
- 視覚的マトリックス割り当て
- 最適化アルゴリズム
## 技術スタック
### コア
- **言語**: TypeScript
- **ランタイム**: Node.js / Bun
- **パッケージ管理**: pnpm + workspace
- **バリデーション**: Ajv (JSON Schema)
### CLI
- **フレームワーク**: Commander.js
- **出力**: chalk (色付き), table
### Web Editor
- **フロントエンド**: React + Vite
- **描画**: Konva.js / Fabric.js
- **状態管理**: Zustand
- **UI**: Mantine / shadcn/ui
### テスト
- **テストランナー**: Vitest
- **E2E**: Playwright
## 実装優先度
### 最優先 (2週間)
1. 型定義とJSON Schema
2. KLE → PHY変換
3. PHY → IDX基本変換
4. CLIでの変換確認
### 高優先 (1ヶ月)
1. QMK/VIA出力
2. 基本的なWebエディタ
3. マトリックス最適化
### 中優先 (2ヶ月)
1. 高度なエディタ機能
2. プロジェクト管理
3. 既存ツール連携
## 開発環境セットアップ
### 1. プロジェクト初期化
```bash
# プロジェクト作成
mkdir beyond-kle-proto
cd beyond-kle-proto
# pnpm workspace設定
echo 'packages:\n - "packages/*"\n - "tools/*"' > pnpm-workspace.yaml
# TypeScript設定
pnpm add -D typescript @types/node
npx tsc --init
# 共通依存関係
pnpm add -D vitest ajv
```
### 2. パッケージ構成
```bash
# Core package
mkdir -p packages/core/src/{types,validators,utils}
cd packages/core && pnpm init
# CLI tool
mkdir -p tools/cli/src
cd tools/cli && pnpm init
# 各エディタパッケージ
mkdir -p packages/{phy-editor,matrix-mapper,keymap-designer}
```
### 3. 開発ツール設定
```bash
# Linting & Formatting
pnpm add -D eslint prettier @typescript-eslint/eslint-plugin
# Testing
pnpm add -D @vitest/ui
# Build tools
pnpm add -D tsup
```
## サンプルプロジェクト設計
### テストケース
1. **60% ANSI** - 標準的なレイアウト
2. **Split keyboard** - 左右分離型
3. **Ergonomic layout** - カラムスタッガード + 回転
4. **Macro pad** - シンプルな4x4
### データ例
#### 60% ANSI PHY Layer
```json
{
"version": "1.0.0",
"metadata": {
"name": "60% ANSI",
"description": "Standard 60% layout"
},
"profiles": [
{
"id": "standard",
"name": "Cherry MX",
"unitSize": { "x": 19.05, "y": 19.05 }
}
],
"keys": [
// Escape key
{
"id": "k00", "profile": "standard",
"position": { "x": 0, "y": 0 },
"size": { "width": 1, "height": 1 },
"metadata": { "label": "Esc" }
},
// Space bar
{
"id": "k60", "profile": "standard",
"position": { "x": 5, "y": 4 },
"size": { "width": 6.25, "height": 1 },
"metadata": { "label": "Space" }
}
]
}
```
## 次のアクション
1. **GitHub リポジトリ作成**
2. **開発環境セットアップ**
3. **型定義の実装開始**
4. **KLE変換機能のプロトタイプ**
実装を開始しますか?最初にどの部分から取り組みたいでしょうか?