diff --git a/Sources/WLKit/OAIProtocol.swift b/Sources/WLKit/OAIProtocol.swift index a6329a1..8f4072a 100644 --- a/Sources/WLKit/OAIProtocol.swift +++ b/Sources/WLKit/OAIProtocol.swift @@ -152,17 +152,20 @@ public enum Pad { /// when turning everything off, not just the visible keys. public static let maxThreadID = 19 /// Agent slot N lights and answers to `agentKeyIDs[N]`, in READING order: - /// left to right, top to bottom as you look at the pad. The firmware's top - /// row is wired right to left — key 0 is the top-RIGHT key — so reading - /// order starts 1, 0. The other rows run left to right in index order. - public static let agentKeyIDs = [1, 0, 2, 3, 4, 5] - /// The rows as you look at the pad, for on-screen mirrors. `rows` stays in - /// firmware order because the keymap file is addressed that way. - public static let displayRows: [[Int]] = [ - [1, 0], + /// left to right, top to bottom as you look at the pad. Every row runs left + /// to right in index order, so this is plain index order: key 0 is the + /// top-LEFT key (checked on hardware — the top row is not reversed). + public static let agentKeyIDs = [0, 1, 2, 3, 4, 5] + /// The rows as you look at the pad, for on-screen mirrors, on the pad's + /// four-column grid: nil is a column with no key in it. The top row sits + /// in the middle two columns and the bottom row starts one column in. + /// `rows` stays in firmware order because the keymap file is addressed + /// that way. + public static let displayRows: [[Int?]] = [ + [nil, 0, 1, nil], [2, 3, 4, 5], [6, 7, 8, 9], - [10, 11, 12], + [nil, 10, 11, 12], ] /// The agent slot a key answers for, or nil for non-agent keys. diff --git a/Sources/WLMicroManager/EmulatorPanel.swift b/Sources/WLMicroManager/EmulatorPanel.swift index 4969d36..51b11d7 100644 --- a/Sources/WLMicroManager/EmulatorPanel.swift +++ b/Sources/WLMicroManager/EmulatorPanel.swift @@ -77,7 +77,13 @@ struct EmulatorView: View { // Rows 0–2 are plain grids of single keys. ForEach(Array(Pad.displayRows.prefix(3).enumerated()), id: \.offset) { _, row in HStack(spacing: Self.gap) { - ForEach(row, id: \.self) { key in keyButton(key) } + ForEach(Array(row.enumerated()), id: \.offset) { _, key in + if let key { + keyButton(key) + } else { + Color.clear.frame(width: Self.keyW, height: Self.keyH) + } + } } } // The bottom row starts one column in, and its wide keycap diff --git a/Sources/WLMicroManager/MenuPanelView.swift b/Sources/WLMicroManager/MenuPanelView.swift index dd172d1..94d8461 100644 --- a/Sources/WLMicroManager/MenuPanelView.swift +++ b/Sources/WLMicroManager/MenuPanelView.swift @@ -229,7 +229,13 @@ struct MenuPanelView: View { VStack(alignment: .leading, spacing: 6) { ForEach(Array(Pad.displayRows.enumerated()), id: \.offset) { _, row in HStack(spacing: 6) { - ForEach(row, id: \.self) { key in keyView(key) } + ForEach(Array(row.enumerated()), id: \.offset) { _, key in + if let key { + keyView(key) + } else { + Color.clear.frame(width: Self.keyW, height: Self.keyH) + } + } Spacer(minLength: 0) } } @@ -237,6 +243,9 @@ struct MenuPanelView: View { .padding(.horizontal, 14).padding(.vertical, 12) } + private static let keyW: CGFloat = 34 + private static let keyH: CGFloat = 26 + private func keyView(_ index: Int) -> some View { let color = bridge.keyColors[index] let isBound = Pad.boundKeyIDs.contains(index) @@ -247,8 +256,8 @@ struct MenuPanelView: View { let isUnavailable = bridge.isRemote && (isStackKey || isLandKey) let macroText = bridge.keyBindings.text(for: index) let isVoiceKey = macroText == nil && Pad.voiceKeyIDs.contains(index) - // Key index and agent slot are different orderings — the top row is - // wired right to left — so the slot lookup goes through the pad map. + // Only some keys are agent keys, so the slot lookup goes through the + // pad map. let slot = Pad.agentSlot(for: index) let agent = slot.flatMap { $0 < bridge.agents.count ? bridge.agents[$0] : nil } @@ -269,7 +278,7 @@ struct MenuPanelView: View { } label: { RoundedRectangle(cornerRadius: 5) .fill(color ?? Color.secondary.opacity(isBound ? 0.16 : 0.07)) - .frame(width: 34, height: 26) + .frame(width: Self.keyW, height: Self.keyH) .overlay( RoundedRectangle(cornerRadius: 5) .strokeBorder(Color.secondary.opacity(0.25), lineWidth: 0.5) diff --git a/Tests/WLKitTests/PlaceholderTests.swift b/Tests/WLKitTests/PlaceholderTests.swift index 80ec7ee..e6ab9fc 100644 --- a/Tests/WLKitTests/PlaceholderTests.swift +++ b/Tests/WLKitTests/PlaceholderTests.swift @@ -4,8 +4,13 @@ import XCTest final class PlaceholderTests: XCTestCase { func testPadGeometry() { XCTAssertEqual(Pad.rows.flatMap { $0 }.count, Pad.keyCount) - // Reading order: the top row is wired right to left. - XCTAssertEqual(Pad.agentKeyIDs, [1, 0, 2, 3, 4, 5]) - XCTAssertEqual(Pad.displayRows.flatMap { $0 }.sorted(), Pad.rows.flatMap { $0 }.sorted()) + // Reading order is index order: key 0 is the top-left key. + XCTAssertEqual(Pad.agentKeyIDs, [0, 1, 2, 3, 4, 5]) + XCTAssertEqual(Pad.displayRows.flatMap { $0 }.compactMap { $0 }.sorted(), + Pad.rows.flatMap { $0 }.sorted()) + // Every display row spans the pad's four columns, so gaps line up. + XCTAssertEqual(Pad.displayRows.map(\.count), [4, 4, 4, 4]) + XCTAssertEqual(Pad.displayRows.first, [nil, 0, 1, nil]) + XCTAssertEqual(Pad.displayRows.last, [nil, 10, 11, 12]) } } diff --git a/Tests/WLKitTests/StatusMapperTests.swift b/Tests/WLKitTests/StatusMapperTests.swift index 0df7c86..9db3b68 100644 --- a/Tests/WLKitTests/StatusMapperTests.swift +++ b/Tests/WLKitTests/StatusMapperTests.swift @@ -47,9 +47,9 @@ final class StatusMapperTests: XCTestCase { agent("working"), agent("blocked"), agent("idle"), ]) XCTAssertEqual(threads.count, 6, "one entry per agent key") - // The pad's top row is wired right to left, so reading order starts - // at key 1: the first agent lights the top-LEFT key. - XCTAssertEqual(threads.map(\.id), [1, 0, 2, 3, 4, 5], "keys in reading order") + // Every row runs left to right, so reading order is index order: the + // first agent lights key 0, the top-LEFT key. + XCTAssertEqual(threads.map(\.id), [0, 1, 2, 3, 4, 5], "keys in reading order") XCTAssertEqual(threads[0].color, 0xFFA000) XCTAssertEqual(threads[1].color, 0xFF2D2D) XCTAssertEqual(threads[2].color, 0x00C853) diff --git a/docs/hacking.md b/docs/hacking.md index e22b985..e87573c 100644 --- a/docs/hacking.md +++ b/docs/hacking.md @@ -375,23 +375,25 @@ to take a whole row's keycodes with it. ### Geometry -The matrix is `[2, 4, 4, 3]` and the key index runs row-major from 0. One -surprise: **the top row is wired right to left**, so index 0 is the top-**right** -key. +The matrix is `[2, 4, 4, 3]` and the key index runs row-major from 0, every +row left to right, on a four-column grid. The top row's two keys sit in the +middle columns, between the dial and the joystick; the bottom row starts one +column in. ``` - ┌─────────────┬─────────────┐ - │ 1 │ 0 │ row 0 <- reversed: 0 is on the RIGHT - ├──────┬──────┼──────┬──────┤ + ┌──────┬──────┐ + (dial) │ 0 │ 1 │ (joy) row 0 + ┌──────┼──────┼──────┼──────┐ │ 2 │ 3 │ 4 │ 5 │ row 1 ├──────┼──────┼──────┼──────┤ │ 6 │ 7 │ 8 │ 9 │ row 2 - ├──────┴──────┼──────┴──────┤ - │ 10 + 11 │ 12 │ row 3 - └─────────────┴─────────────┘ + └──────┼──────┴──────┼──────┤ + │ 10 + 11 │ 12 │ row 3 + └─────────────┴──────┘ ``` -Rows 1–3 run left to right in index order. Matrix positions 10 and 11 sit under +An earlier version of this guide had the top row wired right to left; on the +hardware, pressing the top-left key reports `AG00`. Matrix positions 10 and 11 sit under one wide keycap — the stock map leaves 11 as `KC_NONE`; bind and light both if you want the whole cap to glow evenly.