# Phase 1: JSON Schema仕様策定 ## レイヤー間データフロー ```txt PHY (物理配置) ├─→ ELC (電気配線) ├─→ IDX (キー識別) └─→ MAP (キーマップ) ``` ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAPもIDXを参照しない。 ファームウェアやVIA/Remap向けの出力は、各レイヤーの情報を統合して生成する。 他レイヤーからキーや部品を指す参照には、PHYのノードIDを使う。 ## 1. PHYレイヤー JSON Schema 各フィールドの意味は `PHYレイヤー.md` を正とする。 ### 基本構造 ```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", "root"], "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" } } }, "kleCompatibility": { "type": "object", "description": "File-level opaque KLE fields kept for round-trip output (switchMount, switchBrand, switchType, backcolor, radii, background, css). The owning layer is undecided; see P-6 in PHYレイヤー.md", "additionalProperties": true }, "profiles": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/profile" } }, "root": { "type": "array", "items": { "oneOf": [ { "$ref": "#/$defs/key" }, { "$ref": "#/$defs/cluster" }, { "$ref": "#/$defs/component" }, { "$ref": "#/$defs/variantGroup" } ] } } }, "$defs": { "transform": { "type": "object", "description": "Placement relative to the parent: rotate by rotation around the local origin, then translate by (x, y)", "required": ["x", "y", "rotation"], "properties": { "x": { "type": "number" }, "y": { "type": "number" }, "rotation": { "type": "number", "description": "Degrees" } } }, "profile": { "type": "object", "required": ["id", "name", "unitSize"], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "unitSize": { "type": "object", "description": "Millimeters per 1U", "required": ["x", "y"], "properties": { "x": { "type": "number", "minimum": 0 }, "y": { "type": "number", "minimum": 0 } } }, "visualOffset": { "type": "object", "description": "Key top rectangle, in millimeters: inward distance from each edge of the outline, plus corner radius", "required": ["top", "right", "bottom", "left", "radius"], "properties": { "top": { "type": "number" }, "right": { "type": "number" }, "bottom": { "type": "number" }, "left": { "type": "number" }, "radius": { "type": "number", "minimum": 0 } } } } }, "key": { "type": "object", "required": ["type", "id", "profile", "transform", "size"], "properties": { "type": { "type": "string", "const": "key" }, "id": { "type": "string", "description": "Unique key identifier" }, "profile": { "type": "string", "description": "Reference to profile ID" }, "transform": { "$ref": "#/$defs/transform" }, "size": { "type": "object", "description": "Local size. Not affected by the parent transform", "required": ["w", "h"], "properties": { "w": { "type": "number", "minimum": 0 }, "h": { "type": "number", "minimum": 0 }, "x2": { "type": "number", "description": "Second rectangle for non-rectangular keys, relative to the local origin" }, "y2": { "type": "number" }, "w2": { "type": "number", "minimum": 0 }, "h2": { "type": "number", "minimum": 0 } } }, "switchOffset": { "type": "object", "description": "Switch position, offset from the center of the (w, h) rectangle. Key local coordinates, unit U. Defaults to 0", "required": ["x", "y"], "properties": { "x": { "type": "number" }, "y": { "type": "number" } } }, "partGroup": { "type": "string", "description": "Label grouping keys that use the same part. Not interpreted by Beyond KLE" }, "orientation": { "type": "integer", "enum": [0, 90, 180, 270], "default": 0, "description": "Switch orientation in the key local coordinates" }, "perKeyLED": { "type": "boolean", "default": false, "description": "Whether a per-key LED is mounted" }, "decal": { "type": "boolean", "default": false }, "ghost": { "type": "boolean", "default": false }, "labels": { "type": "array", "items": { "type": "string" }, "description": "Reference information. Used for matching on KLE JSON import" }, "kleCompatibility": { "type": "object", "description": "Opaque KLE fields kept for round-trip output (c, t, p, a, f, f2, fa, l, n, sm, sb, st). The owning layer is undecided; see P-6 in PHYレイヤー.md", "additionalProperties": true } } }, "component": { "type": "object", "description": "Non-key part placed by the designer (LED, OLED, encoder, screw hole, ...)", "required": ["type", "id", "transform"], "properties": { "type": { "type": "string", "const": "component" }, "id": { "type": "string" }, "transform": { "$ref": "#/$defs/transform" }, "partGroup": { "type": "string", "description": "Names starting with @ are reserved for editor rendering" } } }, "variantGroup": { "type": "object", "description": "Mutually exclusive layout alternatives. Children are variants only", "required": ["type", "id", "name", "transform", "default", "children"], "properties": { "type": { "type": "string", "const": "variantGroup" }, "id": { "type": "string" }, "name": { "type": "string", "description": "Heading shown for the choice in the editor" }, "transform": { "$ref": "#/$defs/transform" }, "default": { "type": "string", "description": "id of the variant used when a single layout must be chosen" }, "children": { "type": "array", "minItems": 2, "description": "Display order", "items": { "$ref": "#/$defs/variant" } } } }, "variant": { "type": "object", "description": "One alternative. Appears only directly under a variantGroup. Carries no transform", "required": ["type", "id", "name", "children"], "properties": { "type": { "type": "string", "const": "variant" }, "id": { "type": "string" }, "name": { "type": "string" }, "children": { "type": "array", "description": "May be empty", "items": { "oneOf": [ { "$ref": "#/$defs/key" }, { "$ref": "#/$defs/cluster" }, { "$ref": "#/$defs/component" }, { "$ref": "#/$defs/variantGroup" } ] } } } }, "cluster": { "type": "object", "required": ["type", "id", "transform", "children"], "properties": { "type": { "type": "string", "const": "cluster" }, "id": { "type": "string" }, "name": { "type": "string" }, "transform": { "$ref": "#/$defs/transform" }, "children": { "type": "array", "items": { "oneOf": [ { "$ref": "#/$defs/key" }, { "$ref": "#/$defs/cluster" }, { "$ref": "#/$defs/component" }, { "$ref": "#/$defs/variantGroup" } ] } } } } } } ``` ### 使用例 ```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": { "top": 3, "right": 3, "bottom": 3, "left": 3, "radius": 1 } } ], "root": [ { "type": "cluster", "id": "left_main", "name": "Left Main Block", "transform": { "x": 0, "y": 0, "rotation": 0 }, "children": [ { "type": "key", "id": "k00", "profile": "standard", "transform": { "x": 0, "y": 0, "rotation": 0 }, "size": { "w": 1, "h": 1 }, "partGroup": "main", "perKeyLED": true, "labels": ["Q"] }, { "type": "key", "id": "k01", "profile": "standard", "transform": { "x": 1, "y": 0, "rotation": 0 }, "size": { "w": 1, "h": 1 }, "partGroup": "main", "perKeyLED": true, "labels": ["W"] } ] }, { "type": "cluster", "id": "left_thumb", "name": "Left Thumb Cluster", "transform": { "x": 2, "y": 3, "rotation": 15 }, "children": [ { "type": "key", "id": "k30", "profile": "standard", "transform": { "x": 0, "y": 0, "rotation": 0 }, "size": { "w": 1.25, "h": 1 }, "partGroup": "thumb", "orientation": 180, "labels": ["Space"] }, { "type": "key", "id": "k31", "profile": "standard", "transform": { "x": 1.25, "y": 0, "rotation": 0 }, "size": { "w": 1, "h": 1 }, "partGroup": "thumb", "orientation": 180, "labels": ["Enter"] }, { "type": "component", "id": "enc0", "transform": { "x": 2.25, "y": 0.5, "rotation": 0 }, "partGroup": "encoder" } ] }, { "type": "variantGroup", "id": "vg_enter", "name": "Enter", "transform": { "x": 6, "y": 0, "rotation": 0 }, "default": "v_ansi", "children": [ { "type": "variant", "id": "v_ansi", "name": "ANSI", "children": [ { "type": "key", "id": "k_ansi_bslash", "profile": "standard", "transform": { "x": 1, "y": 0, "rotation": 0 }, "size": { "w": 1.5, "h": 1 }, "labels": ["|", "\\"] }, { "type": "key", "id": "k_ansi_enter", "profile": "standard", "transform": { "x": 0.25, "y": 1, "rotation": 0 }, "size": { "w": 2.25, "h": 1 }, "labels": ["Enter"] } ] }, { "type": "variant", "id": "v_iso", "name": "ISO", "children": [ { "type": "key", "id": "k_iso_enter", "profile": "standard", "transform": { "x": 1, "y": 0, "rotation": 0 }, "size": { "w": 1.5, "h": 1, "x2": 0.25, "y2": 1, "w2": 1.25, "h2": 1 }, "switchOffset": { "x": 0, "y": 0.5 }, "labels": ["Enter"] }, { "type": "key", "id": "k_iso_hash", "profile": "standard", "transform": { "x": 0.25, "y": 1, "rotation": 0 }, "size": { "w": 1, "h": 1 }, "labels": ["~", "#"] } ] } ] }, { "type": "component", "id": "screw0", "transform": { "x": 0, "y": 4.5, "rotation": 0 }, "partGroup": "screw-m2" } ] } ``` ## 2. IDXレイヤー JSON Schema 各フィールドの意味は `IDXレイヤー.md` を正とする。 全バリアントの対象キーIDを、重複なく1つの並びに持つ。同じ配線を使う排他キーも別々の要素とする。 出力時は選択したバリアントで使わないキーを除き、残った並びの位置を0から連続するキー番号とする。 保存した `order` は変更しない。 ### 基本構造 ```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": "Ordered PHY key IDs across all variants. Output key numbers are contiguous positions after filtering by variant selection", "type": "object", "required": ["version", "phyReference", "order"], "properties": { "version": { "type": "string" }, "phyReference": { "type": "object", "required": ["source", "checksum"], "properties": { "source": { "type": "string" }, "checksum": { "type": "string" } } }, "order": { "type": "array", "uniqueItems": true, "description": "Each PHY key has its own entry, including mutually exclusive keys wired to the same terminals. Variant selection does not modify this list", "items": { "type": "string", "description": "Reference to PHY layer key ID" } } } } ``` ## 3. ELCレイヤー JSON Schema 各フィールドの意味は `ELCレイヤー.md` を正とする。 ### 基本構造 ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://beyond-kle.org/schemas/elc-layer.json", "title": "ELC Layer - Electrical Wiring", "description": "Wiring between provider pins and key terminals", "type": "object", "required": ["version", "phyReference", "providers", "keys"], "properties": { "version": { "type": "string" }, "phyReference": { "type": "object", "required": ["source", "checksum"], "properties": { "source": { "type": "string" }, "checksum": { "type": "string" } } }, "providers": { "type": "array", "items": { "$ref": "#/$defs/provider" } }, "keys": { "type": "array", "items": { "$ref": "#/$defs/keyWiring" } }, "ledChains": { "type": "array", "items": { "$ref": "#/$defs/ledChain" } } }, "$defs": { "pinRef": { "type": "string", "pattern": "^(VCC|GND|[^.]+\\.[^.]+)$", "description": "\".\", or the reserved power rails VCC / GND" }, "provider": { "oneOf": [ { "$ref": "#/$defs/mcuProvider" }, { "$ref": "#/$defs/analogMuxProvider" } ] }, "mcuProvider": { "type": "object", "required": ["id", "kind", "part"], "properties": { "id": { "type": "string" }, "kind": { "const": "mcu" }, "part": { "type": "string", "description": "Part number. Used to interpret pin names" } } }, "analogMuxProvider": { "type": "object", "required": ["id", "kind", "channels", "select", "common"], "properties": { "id": { "type": "string" }, "kind": { "const": "analog-mux" }, "channels": { "type": "integer", "minimum": 1, "description": "Number of logical pins provided. Pin names are \"0\"..\"channels-1\"" }, "select": { "type": "array", "items": { "$ref": "#/$defs/pinRef" }, "description": "Select pins, least significant bit first" }, "common": { "$ref": "#/$defs/pinRef" }, "enable": { "$ref": "#/$defs/pinRef" } } }, "keyWiring": { "oneOf": [ { "$ref": "#/$defs/contactKey" }, { "$ref": "#/$defs/analogKey" } ] }, "contactKey": { "type": "object", "required": ["key", "detect", "terminals"], "properties": { "key": { "type": "string", "description": "Reference to PHY layer key ID" }, "detect": { "const": "contact" }, "terminals": { "type": "object", "description": "Non-polar. a and b carry no ordering meaning", "required": ["a", "b"], "properties": { "a": { "$ref": "#/$defs/pinRef" }, "b": { "$ref": "#/$defs/pinRef" } } }, "diode": { "type": "string", "enum": ["a-to-b", "b-to-a"], "description": "Anode side comes first. Omitted means no diode" } } }, "analogKey": { "type": "object", "required": ["key", "detect", "terminals"], "properties": { "key": { "type": "string", "description": "Reference to PHY layer key ID" }, "detect": { "const": "analog" }, "terminals": { "type": "object", "required": ["out", "vcc", "gnd"], "properties": { "out": { "$ref": "#/$defs/pinRef" }, "vcc": { "$ref": "#/$defs/pinRef" }, "gnd": { "$ref": "#/$defs/pinRef" } } } } }, "ledChain": { "type": "object", "required": ["id", "data", "order"], "properties": { "id": { "type": "string" }, "data": { "$ref": "#/$defs/pinRef" }, "order": { "type": "array", "items": { "type": "string" }, "description": "PHY node IDs. The position in the array is the LED number" } } } } } ``` ## 4. MAPレイヤー JSON Schema キーの参照にはPHYのキーIDを使う。キー番号は参照しない。 配列としての並べ替えは、ファームウェア出力時にバリアント選択後のIDXの並び順で行う。 キー番号と接続先の対応表も同じ並びで生成し、MAPの保存データは変更しない。 ### 基本構造 ```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", "phyReference", "layers"], "properties": { "version": { "type": "string" }, "phyReference": { "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": ["keyId", "keycode"], "properties": { "keyId": { "type": "string", "description": "Reference to PHY layer key ID" }, "keycode": { "type": "string" } } } } } ``` ## 変換フロー設計 ### PHY → IDX変換 1. PHYレイヤーから全バリアントのキーのリストを取得 2. 番号を振る対象キーを1つの `order` に重複なく並べる。同じ配線の排他キーも別々の要素にする ### PHY → ELC変換 1. ピン提供者を置き、各提供者が消費する上位ピンを指定 2. 各キーの端子の接続先を指定 3. LEDチェーンの並びを指定 ### IDX + ELC + MAP → ファームウェア出力 1. PHYのVariantGroupごとにバリアントを選び、共通キーと、祖先を含めて選択されたバリアントのキーを特定 2. IDXの `order` から対象外のキーを除き、残った順序のまま出力用のキー番号を0から連続して割り当て 3. ELCの配線から、ターゲットが要求する row / col を割り当て、手順2の並びでキー番号との対応表を生成 4. 同じ並びでMAPのキーコードを配列へ並べ替え、VIA/QMK形式へ出力 保存したPHY・IDX・ELC・MAPは書き換えない。ELCのLEDチェーンの並び・番号は、このキーの取捨では変わらない。 ## 次のステップ 1. **型定義の作成**(言語は `implementation-plan.md` で選定) 2. **変換ロジックのプロトタイプ実装** 3. **テストケース(60%、Split等)での検証** 4. **既存KLE JSONからの変換機能** ## 未確認事項 (なし)