Files
Beyond-KLE/phase1-json-schema.md
T
ewdernaandClaude Opus 5 a175ee3c53 PHY: レイアウトバリアントを木のノードとして導入し、ELC/IDXの扱いを決める
交換可能なキークラスタ(ISO Enter、スペース周りのキー数など)を、
VariantGroup / Variant の2ノードでPHYの木に表す。全バリアントのキーを
同時に持ち、単一の配置を要求する出力のときだけ1つ選んで畳む。

ELCは選択に関係なくsupersetを配線する。排他キー同士は同じ接続先を共有してよい。
IDXはキー番号の空間を1つに保ち、同じ接続先に配線された排他キーは
`order` の配列要素として同じ番号を共有する。

Journal: journal/2026-09-21-multi-layout.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPCpuPv6hDN76RgNe2PsRR
2026-09-21 09:34:51 +09:00

20 KiB

Phase 1: JSON Schema仕様策定

レイヤー間データフロー

PHY (物理配置)
 ├─→ ELC (電気配線)
 ├─→ IDX (キー識別)
 └─→ MAP (キーマップ)

ELC・IDX・MAPはどれもPHYを参照する。ELCはIDXを参照せず、MAPもIDXを参照しない。 ファームウェアやVIA/Remap向けの出力は、各レイヤーの情報を統合して生成する。 他レイヤーからキーや部品を指す参照には、PHYのノードIDを使う。

1. PHYレイヤー JSON Schema

各フィールドの意味は PHYレイヤー.md を正とする。

基本構造

{
  "$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",
          "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", "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" }
            ]
          }
        }
      }
    }
  }
}

使用例

{
  "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 }
    }
  ],
  "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 を正とする。

番号を振るキーの並びだけを持つ。並びの位置がキー番号になるため、欠番・重複・番号の飛びは 構造として起こらない。

基本構造

{
  "$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 list of PHY keys; the position in the list is the key number",
  "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,
      "items": {
        "oneOf": [
          {
            "type": "string",
            "description": "Reference to PHY layer key ID"
          },
          {
            "type": "array",
            "minItems": 2,
            "uniqueItems": true,
            "items": { "type": "string" },
            "description": "Mutually exclusive variant keys sharing this key number. They must be wired to the same terminals in ELC"
          }
        ]
      }
    }
  }
}

3. ELCレイヤー JSON Schema

各フィールドの意味は ELCレイヤー.md を正とする。

基本構造

{
  "$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": "\"<provider id>.<pin name>\", 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の並び順で行う。

基本構造

{
  "$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. 番号を振るキーを選び、並び順を決める

PHY → ELC変換

  1. ピン提供者を置き、各提供者が消費する上位ピンを指定
  2. 各キーの端子の接続先を指定
  3. LEDチェーンの並びを指定

IDX + ELC + MAP → ファームウェア出力

  1. ELCの配線から、ターゲットが要求する row / col を割り当て
  2. IDXの並び順に従って、MAPのキーコードを配列へ並べ替え
  3. VIA/QMK形式への出力

次のステップ

  1. 型定義の作成(言語は implementation-plan.md で選定)
  2. 変換ロジックのプロトタイプ実装
  3. テストケース(60%、Split等)での検証
  4. 既存KLE JSONからの変換機能

未確認事項

(なし)