commit aaff1cb8980cb2f7c208634b7ac96d684d8b5d19 Author: Jake Date: Thu Jul 2 18:19:24 2026 +0800 first diff --git a/144key copy.json b/144key copy.json new file mode 100644 index 0000000..ff70618 --- /dev/null +++ b/144key copy.json @@ -0,0 +1,524 @@ +[ + { + "backcolor": "#000000", + "name": "Elite v1", + "author": "TrackGear Ltd", + "switchMount": "cherry", + "css": "@font-face {\r\n font-family: 'Roboto';\r\n font-style: normal;\r\n font-weight: 400;\r\n font-stretch: 75%;\r\n src: url(https://fonts.gstatic.com/s/roboto/v51/KFOMCnqEu92Fr1ME7kSn66aGLdTylR4MQXC89YmC2DPNWubEbVmUiAo.woff2) format('woff2');\r\n}\r\n@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wdth@0,75;1,75&display=swap');\r\n* { font-family: 'Roboto'; }" + }, + [ + { + "c": "#8ed7b0", + "t": "#1a1a1a", + "a": 7, + "f": 2 + }, + "SLECT MEETING", + { + "c": "#f8d615", + "t": "#000000", + "a": 4, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "Q\n\n\n7", + "W\n\n\n8", + "E\n\n\n9", + "R\n\n\n10", + "T\n\n\n11", + "Y\n\n\n12", + "U\n\n\n14", + "I\n\n\n16", + "O\n\n\n20", + "P\n\n\n33", + { + "c": "#fbbbc9", + "a": 7, + "f": 2 + }, + "SCREEN MENU", + { + "f": 2 + }, + "SCREEN SORT", + { + "f": 2 + }, + "PRIVACY", + { + "c": "#d0ccc0", + "f": 2 + }, + "BOARD MENU", + { + "f": 2 + }, + "BOARD LAYOUT", + { + "f": 2 + }, + "DISPLAY ON/OFF", + { + "f": 2 + }, + "BLANK BOARD" + ], + [ + { + "c": "#8ed7b0", + "t": "#1a1a1a", + "f": 2 + }, + "DOWN LOAD", + { + "c": "#f8d615", + "t": "#000000", + "a": 4, + "f": 2 + }, + "A\n\n\n11/4", + { + "f": 2 + }, + "S\n\n\n3", + { + "f": 2 + }, + "D\n\n\n10/3", + { + "f": 2 + }, + "F\n\n\n7/2", + { + "f": 2 + }, + "G\n\n\n4", + { + "f": 2 + }, + "H\n\n\n9/2", + { + "f": 2 + }, + "J\n\n\n5", + { + "f": 2 + }, + "K\n\n\n11/2", + { + "f": 2 + }, + "L\n\n\n6", + { + "a": 7, + "f": 3 + }, + "ODDS ON", + { + "c": "#f67f00", + "f": 2 + }, + "CASH IN/OUT", + "REPORT MENU", + { + "c": "#5eb1e7" + }, + "AUTO ODDS", + { + "c": "#8ed7b0", + "f": 1 + }, + "NETWORK MENU", + { + "c": "#d0ccc0", + "f": 2 + }, + "HIDE", + "FLASH", + "BOARD SORT" + ], + [ + { + "c": "#8ed7b0", + "t": "#1a1a1a", + "f": 7, + "w": 2 + }, + "TAB", + { + "c": "#f8d615", + "t": "#000000", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "Z\n\n\n1", + "X\n\n\n11/10", + "C\n\n\n5/4", + "V\n\n\n6/4", + "B\n\n\n7/4", + "N\n\n\n2", + "M\n\n\n9/4", + { + "fa": [ + 4, + 0, + 0, + 3 + ] + }, + "SPACE\n\n\n5/2", + { + "c": "#5eb1e7", + "a": 7, + "f": 7, + "w": 2 + }, + "SET ALL", + { + "c": "#f67f00", + "f": 2 + }, + "PROFIT LOSS", + { + "c": "#8ed7b0", + "f": 1 + }, + "JOIN NETWORK", + "NETWORK PRICE CONTROL", + { + "c": "#c13828", + "t": "#ffffff" + }, + "ACCOUNT MENU", + { + "c": "#f67f00", + "t": "#1a1a1a", + "f": 3 + }, + "ROW UP", + { + "f": 2 + }, + "TOP" + ], + [ + { + "c": "#5eb1e7", + "f": 3 + }, + "EXCH MENU", + "LEVEL PLACE", + "LEVEL WIN", + { + "c": "#fbbbc9", + "t": "#000000", + "f": 4, + "w": 2 + }, + "CYCLE RACE", + { + "c": "#f8d615" + }, + "B.O.G", + { + "c": "#c13828", + "t": "#ffffff", + "f": 7 + }, + "€", + { + "c": "#fbbbc9", + "t": "#000000", + "f": 1 + }, + "FRACTION", + { + "c": "#ffffff", + "t": "#1a1a1a", + "f": 7 + }, + "5 EW", + "10 EW", + "20 EW", + "25 EW", + { + "c": "#d0ccc0", + "t": "#000000", + "f": 2 + }, + "NON RUNNER", + { + "f": 3 + }, + "BET W/O", + { + "t": "#1a1a1a", + "f": 7, + "w": 2 + }, + "RESULT", + { + "c": "#f67f00", + "f": 3 + }, + "ROW DOWN", + { + "f": 2 + }, + "BOTTOM" + ], + [ + { + "c": "#5eb1e7", + "f": 3 + }, + "EXCH BACK", + { + "c": "#fbbbc9", + "t": "#000000" + }, + "EXCH LAY", + { + "c": "#f67f00" + }, + "SP", + { + "c": "#d0ccc0", + "f": 7 + }, + "7", + "8", + "9", + "←", + { + "c": "#f67f00", + "t": "#1a1a1a", + "f": 4, + "h": 2 + }, + "ODDS UP", + { + "c": "#ffffff", + "f": 7 + }, + "1 EW", + "1.50 EW", + "2 EW", + "2.50 EW", + { + "c": "#1db63a", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "16\n\n\n36", + "17\n\n\n37", + "18\n\n\n38", + "19\n\n\n39", + "20\n\n\n40", + { + "c": "#8ed7b0", + "t": "#000000", + "a": 7, + "f": 2 + }, + "RELATED RACE" + ], + [ + { + "c": "#5eb1e7", + "t": "#1a1a1a", + "f": 5, + "w": 2 + }, + "CANCEL ALL", + { + "c": "#f8d615", + "t": "#000000", + "f": 3 + }, + "PLACE", + { + "c": "#d0ccc0", + "f": 7 + }, + "4", + "5", + "6", + "-", + { + "x": 1, + "c": "#c13828", + "t": "#ffffff" + }, + "10", + "20", + "40", + "50", + { + "c": "#1db63a", + "t": "#1a1a1a", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "11\n\n\n31", + "12\n\n\n32", + "13\n\n\n33", + "14\n\n\n34", + "15\n\n\n35", + { + "c": "#8ed7b0", + "t": "#000000", + "a": 7, + "f": 2 + }, + "MANAGE RACE MENU" + ], + [ + { + "c": "#d02f1c", + "t": "#ffffff", + "f": 7, + "w": 2 + }, + "VOID BET", + { + "c": "#5eb1e7", + "t": "#000000", + "f": 3 + }, + "HEDGE", + { + "c": "#d0ccc0", + "f": 7 + }, + "1", + "2", + "3", + { + "f": 4, + "h": 2 + }, + "ENTER", + { + "c": "#f67f00", + "t": "#1a1a1a", + "h": 2 + }, + "ODDS DOWN", + { + "c": "#c13828", + "t": "#ffffff", + "f": 7 + }, + "1", + "2", + "2.5", + "5", + { + "c": "#1db63a", + "t": "#1a1a1a", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "6\n\n\n26", + "7\n\n\n27", + "8\n\n\n28", + "9\n\n\n29", + "10\n\n\n30", + { + "c": "#8ed7b0", + "t": "#000000", + "a": 7, + "f": 2 + }, + "OPEN RACE" + ], + [ + { + "c": "#1a1a1a", + "t": "#ffffff", + "f": 7, + "w": 2 + }, + "RESET", + { + "c": "#fbbbc9", + "t": "#000000", + "f": 3 + }, + "OWN PRICE", + { + "c": "#d0ccc0", + "f": 7 + }, + "/", + "0", + ".", + { + "x": 2, + "c": "#cccccc", + "w": 2 + }, + "EACH WAY", + { + "f": 3 + }, + "LIST BETS", + { + "c": "#1db63a", + "f": 7 + }, + "20+", + { + "t": "#1a1a1a", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "1\n\n\n21", + "2\n\n\n22", + "3\n\n\n23", + "4\n\n\n24", + "5\n\n\n25", + { + "c": "#8ed7b0", + "t": "#000000", + "a": 7, + "f": 2 + }, + "CLOSE RACE" + ] +] \ No newline at end of file diff --git a/144key.json b/144key.json new file mode 100644 index 0000000..9bbdece --- /dev/null +++ b/144key.json @@ -0,0 +1,578 @@ +[ + { + "backcolor": "#000000", + "name": "Elite v1", + "author": "TrackGear Ltd", + "switchMount": "cherry", + "css": "@font-face {\r\n font-family: 'Roboto';\r\n font-style: normal;\r\n font-weight: 400;\r\n font-stretch: 75%;\r\n src: url(https://fonts.gstatic.com/s/roboto/v51/KFOMCnqEu92Fr1ME7kSn66aGLdTylR4MQXC89YmC2DPNWubEbVmUiAo.woff2) format('woff2');\r\n}\r\n@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wdth@0,75;1,75&display=swap');\r\n* { font-family: 'Roboto'; }" + }, + [ + { + "c": "#8ed7b0", + "t": "#1a1a1a", + "a": 7, + "f": 2 + }, + "SLECT MEETING", + { + "c": "#f8d615", + "t": "#000000", + "a": 4, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "Q\n\n\n7", + "W\n\n\n8", + "E\n\n\n9", + "R\n\n\n10", + "T\n\n\n11", + "Y\n\n\n12", + "U\n\n\n14", + "I\n\n\n16", + "O\n\n\n20", + "P\n\n\n33", + { + "c": "#fbbbc9", + "a": 7, + "f": 2 + }, + "SCREEN MENU", + { + "f": 2 + }, + "SCREEN SORT", + { + "f": 2 + }, + "PRIVACY", + { + "c": "#d0ccc0", + "f": 2 + }, + "BOARD MENU", + { + "f": 2 + }, + "BOARD LAYOUT", + { + "f": 2 + }, + "DISPLAY ON/OFF", + { + "f": 2 + }, + "BLANK BOARD" + ], + [ + { + "c": "#8ed7b0", + "t": "#1a1a1a", + "f": 2 + }, + "DOWN LOAD", + { + "c": "#f8d615", + "t": "#000000", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "A\n\n\n11/4", + { + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "S\n\n\n3", + { + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "D\n\n\n10/3", + { + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "F\n\n\n7/2", + { + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "G\n\n\n4", + { + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "H\n\n\n9/2", + { + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "J\n\n\n5", + { + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "K\n\n\n11/2", + { + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "L\n\n\n6", + { + "a": 7, + "f": 3 + }, + "ODDS ON", + { + "c": "#f67f00", + "f": 2 + }, + "CASH IN/OUT", + "REPORT MENU", + { + "c": "#5eb1e7" + }, + "AUTO ODDS", + { + "c": "#8ed7b0", + "f": 1 + }, + "NETWORK MENU", + { + "c": "#d0ccc0", + "f": 2 + }, + "HIDE", + "FLASH", + "BOARD SORT" + ], + [ + { + "c": "#8ed7b0", + "t": "#1a1a1a", + "f": 7, + "w": 2 + }, + "TAB", + { + "c": "#f8d615", + "t": "#000000", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "Z\n\n\n1", + "X\n\n\n11/10", + "C\n\n\n5/4", + "V\n\n\n6/4", + "B\n\n\n7/4", + "N\n\n\n2", + "M\n\n\n9/4", + { + "fa": [ + 4, + 0, + 0, + 3 + ] + }, + "SPACE\n\n\n5/2", + { + "c": "#5eb1e7", + "a": 7, + "f": 7, + "w": 2 + }, + "SET ALL", + { + "c": "#f67f00", + "f": 2 + }, + "PROFIT LOSS", + { + "c": "#8ed7b0", + "f": 1 + }, + "JOIN NETWORK", + "NETWORK PRICE CONTROL", + { + "c": "#c13828", + "t": "#ffffff" + }, + "ACCOUNT MENU", + { + "c": "#f67f00", + "t": "#1a1a1a", + "f": 3 + }, + "ROW UP", + { + "f": 2 + }, + "TOP" + ], + [ + { + "c": "#5eb1e7", + "f": 3 + }, + "EXCH MENU", + "LEVEL PLACE", + "LEVEL WIN", + { + "c": "#fbbbc9", + "t": "#000000", + "f": 4, + "w": 2 + }, + "CYCLE RACE", + { + "c": "#f8d615" + }, + "B.O.G", + { + "c": "#c13828", + "t": "#ffffff", + "f": 7 + }, + "€", + { + "c": "#fbbbc9", + "t": "#000000", + "f": 1 + }, + "FRACTION", + { + "c": "#ffffff", + "t": "#1a1a1a", + "f": 7 + }, + "5 EW", + "10 EW", + "20 EW", + "25 EW", + { + "c": "#d0ccc0", + "t": "#000000", + "f": 2 + }, + "NON RUNNER", + { + "f": 3 + }, + "BET W/O", + { + "t": "#1a1a1a", + "f": 7, + "w": 2 + }, + "RESULT", + { + "c": "#f67f00", + "f": 3 + }, + "ROW DOWN", + { + "f": 2 + }, + "BOTTOM" + ], + [ + { + "c": "#5eb1e7", + "f": 3 + }, + "EXCH BACK", + { + "c": "#fbbbc9", + "t": "#000000" + }, + "EXCH LAY", + { + "c": "#f67f00" + }, + "SP", + { + "c": "#d0ccc0", + "f": 7 + }, + "7", + "8", + "9", + "←", + { + "c": "#f67f00", + "t": "#1a1a1a", + "f": 4, + "h": 2 + }, + "ODDS UP", + { + "c": "#ffffff", + "f": 7 + }, + "1 EW", + "1.50 EW", + "2 EW", + "2.50 EW", + { + "c": "#1db63a", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "16\n\n\n36", + "17\n\n\n37", + "18\n\n\n38", + "19\n\n\n39", + "20\n\n\n40", + { + "c": "#8ed7b0", + "t": "#000000", + "a": 7, + "f": 2 + }, + "RELATED RACE" + ], + [ + { + "c": "#5eb1e7", + "t": "#1a1a1a", + "f": 5, + "w": 2 + }, + "CANCEL ALL", + { + "c": "#f8d615", + "t": "#000000", + "f": 3 + }, + "PLACE", + { + "c": "#d0ccc0", + "f": 7 + }, + "4", + "5", + "6", + "-", + { + "x": 1, + "c": "#c13828", + "t": "#ffffff" + }, + "10", + "20", + "40", + "50", + { + "c": "#1db63a", + "t": "#1a1a1a", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "11\n\n\n31", + "12\n\n\n32", + "13\n\n\n33", + "14\n\n\n34", + "15\n\n\n35", + { + "c": "#8ed7b0", + "t": "#000000", + "a": 7, + "f": 2 + }, + "MANAGE RACE MENU" + ], + [ + { + "c": "#d02f1c", + "t": "#ffffff", + "f": 7, + "w": 2 + }, + "VOID BET", + { + "c": "#5eb1e7", + "t": "#000000", + "f": 3 + }, + "HEDGE", + { + "c": "#d0ccc0", + "f": 7 + }, + "1", + "2", + "3", + { + "f": 4, + "h": 2 + }, + "ENTER", + { + "c": "#f67f00", + "t": "#1a1a1a", + "h": 2 + }, + "ODDS DOWN", + { + "c": "#c13828", + "t": "#ffffff", + "f": 7 + }, + "1", + "2", + "2.5", + "5", + { + "c": "#1db63a", + "t": "#1a1a1a", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "6\n\n\n26", + "7\n\n\n27", + "8\n\n\n28", + "9\n\n\n29", + "10\n\n\n30", + { + "c": "#8ed7b0", + "t": "#000000", + "a": 7, + "f": 2 + }, + "OPEN RACE" + ], + [ + { + "c": "#1a1a1a", + "t": "#ffffff", + "f": 7, + "w": 2 + }, + "RESET", + { + "c": "#fbbbc9", + "t": "#000000", + "f": 3 + }, + "OWN PRICE", + { + "c": "#d0ccc0", + "f": 7 + }, + "/", + "0", + ".", + { + "x": 2, + "c": "#cccccc", + "w": 2 + }, + "EACH WAY", + { + "f": 3 + }, + "LIST BETS", + { + "c": "#1db63a", + "f": 7 + }, + "20+", + { + "t": "#1a1a1a", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "1\n\n\n21", + "2\n\n\n22", + "3\n\n\n23", + "4\n\n\n24", + "5\n\n\n25", + { + "c": "#8ed7b0", + "t": "#000000", + "a": 7, + "f": 2 + }, + "CLOSE RACE" + ] +] \ No newline at end of file diff --git a/144key.svg b/144key.svg new file mode 100644 index 0000000..cc00ef0 --- /dev/null +++ b/144key.svg @@ -0,0 +1,386 @@ + + + + + + + + SLECT + MEETING + + Q + 7 + + W + 8 + + E + 9 + + R + 10 + + T + 11 + + Y + 12 + + U + 14 + + I + 16 + + O + 20 + + P + 33 + + SCREEN + MENU + + SCREEN + SORT + + PRIVACY + + BOARD + MENU + + BOARD + LAYOUT + + DISPLAY + ON/OFF + + BLANK + BOARD + + DOWN + LOAD + + A + 11/4 + + S + 3 + + D + 10/3 + + F + 7/2 + + G + 4 + + H + 9/2 + + J + 5 + + K + 11/2 + + L + 6 + + ODDS + ON + + CASH + IN/OUT + + REPORT + MENU + + AUTO + ODDS + + NETWORK + MENU + + HIDE + + FLASH + + BOARD + SORT + + TAB + + Z + 1 + + X + 11/10 + + C + 5/4 + + V + 6/4 + + B + 7/4 + + N + 2 + + M + 9/4 + + SPACE + 5/2 + + SET ALL + + PROFIT + LOSS + + JOIN + NETWORK + + NETWORK + PRICE + CONTROL + + ACCOUNT + MENU + + ROW + UP + + TOP + + EXCH + MENU + + LEVEL + PLACE + + LEVEL + WIN + + CYCLE RACE + + B.O.G + + + + FRACTION + + 5 + EW + + 10 + EW + + 20 + EW + + 25 + EW + + NON + RUNNER + + BET + W/O + + RESULT + + ROW + DOWN + + BOTTOM + + EXCH + BACK + + EXCH + LAY + + SP + + 7 + + 8 + + 9 + + + + ODDS + UP + + 1 + EW + + 1.50 + EW + + 2 + EW + + 2.50 + EW + + 16 + 36 + + 17 + 37 + + 18 + 38 + + 19 + 39 + + 20 + 40 + + RELATED + RACE + + CANCEL + ALL + + PLACE + + 4 + + 5 + + 6 + + - + + 10 + + 20 + + 40 + + 50 + + 11 + 31 + + 12 + 32 + + 13 + 33 + + 14 + 34 + + 15 + 35 + + MANAGE + RACE + MENU + + VOID + BET + + HEDGE + + 1 + + 2 + + 3 + + ENTER + + ODDS + DOWN + + 1 + + 2 + + 2.5 + + 5 + + 6 + 26 + + 7 + 27 + + 8 + 28 + + 9 + 29 + + 10 + 30 + + OPEN + RACE + + RESET + + OWN + PRICE + + / + + 0 + + . + + EACH + WAY + + LIST + BETS + + 20+ + + 1 + 21 + + 2 + 22 + + 3 + 23 + + 4 + 24 + + 5 + 25 + + CLOSE + RACE + + \ No newline at end of file diff --git a/160key copy.json b/160key copy.json new file mode 100644 index 0000000..219e4db --- /dev/null +++ b/160key copy.json @@ -0,0 +1,580 @@ +[ + { + "backcolor": "#000000", + "name": "RDT v1", + "author": "TrackGear Ltd", + "switchMount": "cherry", + "css": "@font-face {\r\n font-family: 'Roboto';\r\n font-style: normal;\r\n font-weight: 400;\r\n font-stretch: 75%;\r\n src: url(https://fonts.gstatic.com/s/roboto/v51/KFOMCnqEu92Fr1ME7kSn66aGLdTylR4MQXC89YmC2DPNWubEbVmUiAo.woff2) format('woff2');\r\n}\r\n@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wdth@0,75;1,75&display=swap');\r\n* { font-family: 'Roboto'; }" + }, + [ + { + "c": "#f8d615", + "t": "#1a1a1a", + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "Q\n\n\n9", + { + "t": "#000000" + }, + "W\n\n\n10", + "E\n\n\n11", + "R\n\n\n12", + "T\n\n\n14", + "Y\n\n\n16", + "U\n\n\n20", + "I\n\n\n25", + "O\n\n\n33", + "P\n\n\n50", + "\n\n\n66", + { + "fa": [ + 9, + 0, + 0, + 3, + 0, + 0, + 0, + 3 + ] + }, + "\n\n\n100\n\n\n\n80", + { + "c": "#f67f00", + "a": 7, + "fa": [ + 7 + ], + "w": 2 + }, + "VENUE", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "←", + { + "f": 2 + }, + "SCREEN", + { + "c": "#5eb1e7", + "fa": [ + 3 + ] + }, + "HIDE", + "FLASH", + { + "f": 3, + "fa": [ + 2 + ] + }, + "DISPLAY ON/OFF", + "DISPLAY RACE" + ], + [ + { + "c": "#f8d615", + "a": 4, + "fa": [ + 9 + ] + }, + "A\n\n\n11/4", + "S\n\n\n3", + "D\n\n\n10/3", + "F\n\n\n7/2", + "G\n\n\n4", + "H\n\n\n9/2", + "J\n\n\n5", + "K\n\n\n11/2", + "L\n\n\n6", + "\n\n\n13/2", + "\n\n\n7\n\n\n\n15/2", + "\n\n\n8\n\n\n\n17/2", + { + "c": "#5eb1e7", + "a": 7, + "fa": [ + 7 + ], + "w": 2 + }, + "RACE", + { + "f": 3 + }, + "RESULT", + { + "fa": [ + 4 + ] + }, + "WITH DRAW", + { + "fa": [ + 2 + ] + }, + "SORT ORDER", + "HIDE ALL", + { + "fa": [ + 1 + ] + }, + "ADVERT TOGGLE", + "MESSAGE TOGGLE" + ], + [ + { + "c": "#f8d615", + "a": 4, + "fa": [ + 9 + ] + }, + "Z\n\n\n1", + "X\n\n\n11/10", + "C\n\n\n6/5", + "V\n\n\n5/4", + "B\n\n\n11/8", + "N\n\n\n6/4", + "M\n\n\n13/8", + { + "fa": [ + 2 + ] + }, + "SPACE\n\n\n7/4", + { + "fa": [ + 9 + ] + }, + ".\n\n\n18/8", + "\n\n\n2", + "\n\n\n9/4\n\n\n\n11/5", + "\n\n\n5/2", + { + "c": "#1db63a", + "a": 7, + "f": 1 + }, + "NETWORK", + { + "c": "#d0ccc0", + "f": 3 + }, + "PLACE ONLY", + { + "c": "#5eb1e7" + }, + "DED", + { + "c": "#0046ad", + "t": "#ffffff", + "fa": [ + 6 + ], + "h": 2 + }, + "SET ALL", + { + "c": "#f67f00", + "t": "#000000", + "fa": [ + 1 + ] + }, + "ACCOUNTS", + { + "c": "#0046ad", + "t": "#ffffff", + "fa": [ + 9 + ] + }, + "↑", + { + "c": "#f67f00", + "t": "#000000", + "fa": [ + 4 + ] + }, + "TALLY", + { + "c": "#c13828", + "t": "#ffffff", + "fa": [ + 2 + ] + }, + "COL CHANGE" + ], + [ + { + "c": "#ac97d8", + "t": "#000000" + }, + "BACK", + "LAY ALL", + "CANCEL ALL", + { + "c": "#f67f00" + }, + "TOGGLE VIEW", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "-", + { + "f": 3 + }, + "OWN PRICE", + { + "c": "#c13828", + "t": "#ffffff", + "f": 3 + }, + "ODDS ON", + { + "c": "#f67f00", + "t": "#000000", + "fa": [ + 5 + ] + }, + "£100", + "£200", + "£500", + { + "fa": [ + 4 + ] + }, + "£1000", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "€", + { + "c": "#fbbbc9", + "fa": [ + 4 + ] + }, + "FILL ACC", + "SELF", + "CASH AFTER", + { + "x": 1, + "c": "#0046ad", + "t": "#ffffff" + }, + "BACK TAB", + { + "fa": [ + 7 + ], + "w": 2 + }, + "TAB", + { + "c": "#c13828", + "fa": [ + 2 + ] + }, + "SCREEN SORT" + ], + [ + { + "c": "#ac97d8", + "t": "#000000" + }, + "LAY", + "FILL & KILL", + { + "c": "#5eb1e7", + "f": 3 + }, + "FAIR PRICE", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "7", + "8", + "9", + "/", + { + "c": "#f67f00", + "fa": [ + 5 + ] + }, + "£30", + "£40", + "£50", + "£80", + { + "c": "#8ed7b0", + "a": 4, + "fa": [ + 0, + 0, + 0, + 9 + ] + }, + "36\n\n\n16", + "37\n\n\n17", + "38\n\n\n18", + "39\n\n\n19", + "40\n\n\n20", + { + "c": "#f67f00", + "a": 7, + "fa": [ + 2 + ] + }, + "CASH IN/OUT", + { + "c": "#0046ad", + "t": "#ffffff", + "fa": [ + 9 + ] + }, + "↓", + { + "c": "#f67f00", + "t": "#000000", + "fa": [ + 2 + ] + }, + "PRINT OUTS", + "PRINT PAYOUT" + ], + [ + { + "c": "#5eb1e7", + "fa": [ + 7 + ], + "w": 2 + }, + "HEDGE", + { + "fa": [ + 1 + ] + }, + "FRACTION", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "4", + "5", + "6", + "↑", + { + "c": "#f67f00", + "fa": [ + 5 + ] + }, + "£2.50 EW", + "£5 EW", + "£10 EW", + "£20 EW", + { + "c": "#8ed7b0", + "a": 4, + "fa": [ + 0, + 0, + 0, + 9 + ] + }, + "31\n\n\n11", + "32\n\n\n12", + "33\n\n\n13", + "34\n\n\n14", + "35\n\n\n15", + { + "c": "#d0ccc0", + "a": 7 + }, + "ACC 9", + "ACC 10", + "ACC 11", + "ACC 12" + ], + [ + { + "c": "#8ed7b0", + "fa": [ + 7 + ], + "w": 2 + }, + "VOID BET", + { + "c": "#5eb1e7", + "fa": [ + 5 + ] + }, + "SP", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "1", + "2", + "3", + "↓", + { + "c": "#f67f00", + "fa": [ + 5 + ] + }, + "£2.50", + "£5", + "£10", + "£20", + { + "c": "#8ed7b0", + "a": 4, + "fa": [ + 0, + 0, + 0, + 9 + ] + }, + "26\n\n\n6", + "27\n\n\n7", + "28\n\n\n8", + "29\n\n\n9", + "30\n\n\n10", + { + "c": "#d0ccc0", + "a": 7 + }, + "ACC5", + "ACC6", + "ACC7", + "ACC8" + ], + [ + { + "c": "#c13828", + "t": "#ffffff", + "fa": [ + 7 + ], + "w": 2 + }, + "ABORT", + { + "c": "#1db63a", + "t": "#000000", + "fa": [ + 4 + ] + }, + "MORE", + { + "c": "#d0ccc0", + "fa": [ + 9 + ], + "w": 2 + }, + "0", + { + "a": 5, + "fa": [ + 9, + 9 + ] + }, + "\n.", + { + "c": "#0046ad", + "t": "#ffffff", + "a": 7, + "fa": [ + 7 + ], + "w": 2 + }, + "ENTER", + { + "c": "#f67f00", + "t": "#000000", + "w": 2 + }, + "EACH WAY", + { + "c": "#8ed7b0", + "fa": [ + 9 + ] + }, + "20+", + { + "a": 4, + "fa": [ + 0, + 0, + 0, + 9 + ] + }, + "21\n\n\n1", + "22\n\n\n2", + "23\n\n\n3", + "24\n\n\n4", + "\n\n\n5", + { + "c": "#d0ccc0", + "a": 7 + }, + "ACC 1", + "ACC2", + "ACC3", + "ACC4" + ] +] \ No newline at end of file diff --git a/160key.json b/160key.json new file mode 100644 index 0000000..1c9f52c --- /dev/null +++ b/160key.json @@ -0,0 +1,584 @@ +[ + { + "backcolor": "#000000", + "name": "RDT v1", + "author": "TrackGear Ltd", + "switchMount": "cherry", + "css": "@font-face {\r\n font-family: 'Roboto';\r\n font-style: normal;\r\n font-weight: 400;\r\n font-stretch: 75%;\r\n src: url(https://fonts.gstatic.com/s/roboto/v51/KFOMCnqEu92Fr1ME7kSn66aGLdTylR4MQXC89YmC2DPNWubEbVmUiAo.woff2) format('woff2');\r\n}\r\n@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wdth@0,75;1,75&display=swap');\r\n* { font-family: 'Roboto'; }" + }, + [ + { + "c": "#f8d615", + "t": "#1a1a1a", + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "Q\n\n\n9", + { + "t": "#000000" + }, + "W\n\n\n10", + "E\n\n\n11", + "R\n\n\n12", + "T\n\n\n14", + "Y\n\n\n16", + "U\n\n\n20", + "I\n\n\n25", + "O\n\n\n33", + "P\n\n\n50", + "\n\n\n66", + { + "fa": [ + 9, + 0, + 0, + 3, + 0, + 0, + 0, + 3 + ] + }, + "\n\n\n100\n\n\n\n80", + { + "c": "#f67f00", + "a": 7, + "fa": [ + 7 + ], + "w": 2 + }, + "VENUE", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "←", + { + "f": 2 + }, + "SCREEN", + { + "c": "#5eb1e7", + "fa": [ + 3 + ] + }, + "HIDE", + "FLASH", + { + "f": 3, + "fa": [ + 2 + ] + }, + "DISPLAY ON/OFF", + "DISPLAY RACE" + ], + [ + { + "c": "#f8d615", + "a": 4, + "f": 2, + "fa": [ + 9, + 0, + 0, + 3 + ] + }, + "A\n\n\n11/4", + "S\n\n\n3", + "D\n\n\n10/3", + "F\n\n\n7/2", + "G\n\n\n4", + "H\n\n\n9/2", + "J\n\n\n5", + "K\n\n\n11/2", + "L\n\n\n6", + "\n\n\n13/2", + "\n\n\n7\n\n\n\n15/2", + "\n\n\n8\n\n\n\n17/2", + { + "c": "#5eb1e7", + "a": 7, + "fa": [ + 7 + ], + "w": 2 + }, + "RACE", + { + "f": 3 + }, + "RESULT", + { + "fa": [ + 4 + ] + }, + "WITH DRAW", + { + "fa": [ + 2 + ] + }, + "SORT ORDER", + "HIDE ALL", + { + "fa": [ + 1 + ] + }, + "ADVERT TOGGLE", + "MESSAGE TOGGLE" + ], + [ + { + "c": "#f8d615", + "a": 4, + "fa": [ + 9 + ] + }, + "Z\n\n\n1", + "X\n\n\n11/10", + "C\n\n\n6/5", + "V\n\n\n5/4", + "B\n\n\n11/8", + "N\n\n\n6/4", + "M\n\n\n13/8", + { + "fa": [ + 2 + ] + }, + "SPACE\n\n\n7/4", + { + "fa": [ + 9 + ] + }, + ".\n\n\n15/8", + "\n\n\n2", + "\n\n\n9/4\n\n\n\n11/5", + "\n\n\n5/2", + { + "c": "#1db63a", + "a": 7, + "f": 1 + }, + "NETWORK", + { + "c": "#d0ccc0", + "f": 3 + }, + "PLACE ONLY", + { + "c": "#5eb1e7" + }, + "DED", + { + "c": "#0046ad", + "t": "#ffffff", + "fa": [ + 6 + ], + "h": 2 + }, + "SET ALL", + { + "c": "#f67f00", + "t": "#000000", + "fa": [ + 1 + ] + }, + "ACCOUNTS", + { + "c": "#0046ad", + "t": "#ffffff", + "fa": [ + 9 + ] + }, + "↑", + { + "c": "#f67f00", + "t": "#000000", + "fa": [ + 4 + ] + }, + "TALLY", + { + "c": "#c13828", + "t": "#ffffff", + "fa": [ + 2 + ] + }, + "COL CHANGE" + ], + [ + { + "c": "#ac97d8", + "t": "#000000" + }, + "BACK", + "LAY ALL", + "CANCEL ALL", + { + "c": "#f67f00" + }, + "TOGGLE VIEW", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "-", + { + "f": 3 + }, + "OWN PRICE", + { + "c": "#c13828", + "t": "#ffffff", + "f": 3 + }, + "ODDS ON", + { + "c": "#f67f00", + "t": "#000000", + "fa": [ + 5 + ] + }, + "£100", + "£200", + "£500", + { + "fa": [ + 4 + ] + }, + "£1000", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "€", + { + "c": "#fbbbc9", + "fa": [ + 4 + ] + }, + "FILL ACC", + "SELF", + "CASH AFTER", + { + "x": 1, + "c": "#0046ad", + "t": "#ffffff" + }, + "BACK TAB", + { + "fa": [ + 7 + ], + "w": 2 + }, + "TAB", + { + "c": "#c13828", + "fa": [ + 2 + ] + }, + "SCREEN SORT" + ], + [ + { + "c": "#ac97d8", + "t": "#000000" + }, + "LAY", + "FILL & KILL", + { + "c": "#5eb1e7", + "f": 3 + }, + "FAIR PRICE", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "7", + "8", + "9", + "/", + { + "c": "#f67f00", + "fa": [ + 5 + ] + }, + "£30", + "£40", + "£50", + "£80", + { + "c": "#8ed7b0", + "a": 4, + "fa": [ + 0, + 0, + 0, + 9 + ] + }, + "36\n\n\n16", + "37\n\n\n17", + "38\n\n\n18", + "39\n\n\n19", + "40\n\n\n20", + { + "c": "#f67f00", + "a": 7, + "fa": [ + 2 + ] + }, + "CASH IN/OUT", + { + "c": "#0046ad", + "t": "#ffffff", + "fa": [ + 9 + ] + }, + "↓", + { + "c": "#f67f00", + "t": "#000000", + "fa": [ + 2 + ] + }, + "PRINT OUTS", + "PRINT PAYOUT" + ], + [ + { + "c": "#5eb1e7", + "fa": [ + 7 + ], + "w": 2 + }, + "HEDGE", + { + "fa": [ + 1 + ] + }, + "FRACTION", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "4", + "5", + "6", + "↑", + { + "c": "#f67f00", + "fa": [ + 5 + ] + }, + "£2.50 EW", + "£5 EW", + "£10 EW", + "£20 EW", + { + "c": "#8ed7b0", + "a": 4, + "fa": [ + 0, + 0, + 0, + 9 + ] + }, + "31\n\n\n11", + "32\n\n\n12", + "33\n\n\n13", + "34\n\n\n14", + "35\n\n\n15", + { + "c": "#d0ccc0", + "a": 7 + }, + "ACC 9", + "ACC 10", + "ACC 11", + "ACC 12" + ], + [ + { + "c": "#8ed7b0", + "fa": [ + 7 + ], + "w": 2 + }, + "VOID BET", + { + "c": "#5eb1e7", + "fa": [ + 5 + ] + }, + "SP", + { + "c": "#d0ccc0", + "fa": [ + 9 + ] + }, + "1", + "2", + "3", + "↓", + { + "c": "#f67f00", + "fa": [ + 5 + ] + }, + "£2.50", + "£5", + "£10", + "£20", + { + "c": "#8ed7b0", + "a": 4, + "fa": [ + 0, + 0, + 0, + 9 + ] + }, + "26\n\n\n6", + "27\n\n\n7", + "28\n\n\n8", + "29\n\n\n9", + "30\n\n\n10", + { + "c": "#d0ccc0", + "a": 7 + }, + "ACC5", + "ACC6", + "ACC7", + "ACC8" + ], + [ + { + "c": "#c13828", + "t": "#ffffff", + "fa": [ + 7 + ], + "w": 2 + }, + "ABORT", + { + "c": "#1db63a", + "t": "#000000", + "fa": [ + 4 + ] + }, + "MORE", + { + "c": "#d0ccc0", + "fa": [ + 9 + ], + "w": 2 + }, + "0", + { + "a": 5, + "fa": [ + 9, + 9 + ] + }, + "\n.", + { + "c": "#0046ad", + "t": "#ffffff", + "a": 7, + "fa": [ + 7 + ], + "w": 2 + }, + "ENTER", + { + "c": "#f67f00", + "t": "#000000", + "w": 2 + }, + "EACH WAY", + { + "c": "#8ed7b0", + "fa": [ + 9 + ] + }, + "20+", + { + "a": 4, + "fa": [ + 0, + 0, + 0, + 9 + ] + }, + "21\n\n\n1", + "22\n\n\n2", + "23\n\n\n3", + "24\n\n\n4", + "\n\n\n5", + { + "c": "#d0ccc0", + "a": 7 + }, + "ACC 1", + "ACC2", + "ACC3", + "ACC4" + ] +] \ No newline at end of file diff --git a/160key.svg b/160key.svg new file mode 100644 index 0000000..f325bd6 --- /dev/null +++ b/160key.svg @@ -0,0 +1,409 @@ + + + + + + + + Q + 9 + + W + 10 + + E + 11 + + R + 12 + + T + 14 + + Y + 16 + + U + 20 + + I + 25 + + O + 33 + + P + 50 + + 66 + + 100 + 80 + + VENUE + + + + SCREEN + + HIDE + + FLASH + + DISPLAY + ON/OFF + + DISPLAY + RACE + + A + 11/4 + + S + 3 + + D + 10/3 + + F + 7/2 + + G + 4 + + H + 9/2 + + J + 5 + + K + 11/2 + + L + 6 + + 13/2 + + 7 + 15/2 + + 8 + 17/2 + + RACE + + RESULT + + WITH + DRAW + + SORT + ORDER + + HIDE + ALL + + ADVERT + TOGGLE + + MESSAGE + TOGGLE + + Z + 1 + + X + 11/10 + + C + 6/5 + + V + 5/4 + + B + 11/8 + + N + 6/4 + + M + 13/8 + + SPACE + 7/4 + + . + 15/8 + + 2 + + 9/4 + 11/5 + + 5/2 + + NETWORK + + PLACE + ONLY + + DED + + SET + ALL + + ACCOUNTS + + + + TALLY + + COL + CHANGE + + BACK + + LAY + ALL + + CANCEL + ALL + + TOGGLE + VIEW + + - + + OWN + PRICE + + ODDS + ON + + £100 + + £200 + + £500 + + £1000 + + + + FILL + ACC + + SELF + + CASH + AFTER + + BACK + TAB + + TAB + + SCREEN + SORT + + LAY + + FILL & + KILL + + FAIR + PRICE + + 7 + + 8 + + 9 + + / + + £30 + + £40 + + £50 + + £80 + + 36 + 16 + + 37 + 17 + + 38 + 18 + + 39 + 19 + + 40 + 20 + + CASH + IN/OUT + + + + PRINT + OUTS + + PRINT + PAYOUT + + HEDGE + + FRACTION + + 4 + + 5 + + 6 + + + + £2.50 + EW + + £5 + EW + + £10 + EW + + £20 + EW + + 31 + 11 + + 32 + 12 + + 33 + 13 + + 34 + 14 + + 35 + 15 + + ACC 9 + + ACC + 10 + + ACC + 11 + + ACC + 12 + + VOID + BET + + SP + + 1 + + 2 + + 3 + + + + £2.50 + + £5 + + £10 + + £20 + + 26 + 6 + + 27 + 7 + + 28 + 8 + + 29 + 9 + + 30 + 10 + + ACC5 + + ACC6 + + ACC7 + + ACC8 + + ABORT + + MORE + + 0 + + . + + ENTER + + EACH + WAY + + 20+ + + 21 + 1 + + 22 + 2 + + 23 + 3 + + 24 + 4 + + 5 + + ACC 1 + + ACC2 + + ACC3 + + ACC4 + + \ No newline at end of file diff --git a/1st keycap order missing/PXL_20260509_110231980.MP.jpg b/1st keycap order missing/PXL_20260509_110231980.MP.jpg new file mode 100644 index 0000000..a61adc4 Binary files /dev/null and b/1st keycap order missing/PXL_20260509_110231980.MP.jpg differ diff --git a/1st keycap order missing/PXL_20260509_110238685.LS.mp4 b/1st keycap order missing/PXL_20260509_110238685.LS.mp4 new file mode 100644 index 0000000..c0a51bd Binary files /dev/null and b/1st keycap order missing/PXL_20260509_110238685.LS.mp4 differ diff --git a/1st keycap order missing/PXL_20260510_085106086.MP.jpg b/1st keycap order missing/PXL_20260510_085106086.MP.jpg new file mode 100644 index 0000000..34b64eb Binary files /dev/null and b/1st keycap order missing/PXL_20260510_085106086.MP.jpg differ diff --git a/1st keycap order missing/PXL_20260510_104859939.MP.jpg b/1st keycap order missing/PXL_20260510_104859939.MP.jpg new file mode 100644 index 0000000..c034f32 Binary files /dev/null and b/1st keycap order missing/PXL_20260510_104859939.MP.jpg differ diff --git a/1st keycap order missing/PXL_20260511_072227523.MP.jpg b/1st keycap order missing/PXL_20260511_072227523.MP.jpg new file mode 100644 index 0000000..4a7929f Binary files /dev/null and b/1st keycap order missing/PXL_20260511_072227523.MP.jpg differ diff --git a/1st keycap order missing/PXL_20260511_072232384.MP.jpg b/1st keycap order missing/PXL_20260511_072232384.MP.jpg new file mode 100644 index 0000000..2664e19 Binary files /dev/null and b/1st keycap order missing/PXL_20260511_072232384.MP.jpg differ diff --git a/1st keycap order missing/PXL_20260511_072306847.MP.jpg b/1st keycap order missing/PXL_20260511_072306847.MP.jpg new file mode 100644 index 0000000..248247f Binary files /dev/null and b/1st keycap order missing/PXL_20260511_072306847.MP.jpg differ diff --git a/1st keycap order missing/missing_1_30pound.png b/1st keycap order missing/missing_1_30pound.png new file mode 100644 index 0000000..6392d0e Binary files /dev/null and b/1st keycap order missing/missing_1_30pound.png differ diff --git a/Screenshot 2026-06-07 182154.png b/Screenshot 2026-06-07 182154.png new file mode 100644 index 0000000..ae2841a Binary files /dev/null and b/Screenshot 2026-06-07 182154.png differ diff --git a/app.js b/app.js new file mode 100644 index 0000000..2195f4f --- /dev/null +++ b/app.js @@ -0,0 +1,600 @@ +const state = { + rows: 6, + cols: 16, + pitch: 19.05, + inset: 0.48, + radius: 1, + margin: 3, + bg: "#000000", + fontFamily: "'Roboto Condensed', 'Roboto', Arial Narrow, Arial, Helvetica, sans-serif", + fontSizeMm: 3.8, + keys: [], + selected: null +}; + +const ns = "http://www.w3.org/2000/svg"; +const FONT_STRETCH = "70%"; +const KLE_FS_MM = { 0: 4.5, 1: 3.0, 2: 3.8, 3: 4.5, 4: 5.3, 5: 6.0, 6: 6.8, 7: 7.5, 8: 8.3, 9: 9.0 }; +const LABEL_POS = { + 0: [0, 0], 1: [0, 2], 2: [2, 0], 3: [2, 2], + 4: [0, 3], 5: [2, 3], 6: [0, 1], 7: [2, 1], + 8: [1, 0], 9: [1, 1], 10: [1, 2], 11: [1, 3] +}; +const VISUAL_SLOT_TO_KLE = [0, 8, 2, 6, 9, 7, 1, 10, 3]; + +const el = { + rows: document.getElementById("rows"), + cols: document.getElementById("cols"), + pitch: document.getElementById("pitch"), + inset: document.getElementById("inset"), + radius: document.getElementById("radius"), + margin: document.getElementById("margin"), + bg: document.getElementById("bg"), + font: document.getElementById("font"), + fontSize: document.getElementById("fontSize"), + makeGridBtn: document.getElementById("makeGridBtn"), + downloadSvgBtn: document.getElementById("downloadSvgBtn"), + downloadJsonBtn: document.getElementById("downloadJsonBtn"), + loadJsonInput: document.getElementById("loadJsonInput"), + keyLabel: document.getElementById("keyLabel"), + keyColor: document.getElementById("keyColor"), + keyTextColor: document.getElementById("keyTextColor"), + keyW: document.getElementById("keyW"), + keyH: document.getElementById("keyH"), + keyFontScale: document.getElementById("keyFontScale"), + keyAlign: document.getElementById("keyAlign"), + applyKeyBtn: document.getElementById("applyKeyBtn"), + clearKeyBtn: document.getElementById("clearKeyBtn"), + selectedHint: document.getElementById("selectedHint"), + svgWrap: document.getElementById("svgWrap") +}; +el.legendTextInputs = Array.from({ length: 9 }, (_, i) => document.getElementById(`legendText${i}`)); +el.legendSizeInputs = Array.from({ length: 9 }, (_, i) => document.getElementById(`legendSize${i}`)); + +function wrapText(text, availMm, fmm) { + const charW = fmm * 0.60; + if (text.length * charW <= availMm) return [text]; + const maxChars = Math.max(Math.floor(availMm / charW), 1); + const words = text.split(" "); + const lines = []; + let cur = ""; + for (const w of words) { + const trial = cur ? `${cur} ${w}` : w; + if (trial.length <= maxChars || !cur) { + cur = trial; + } else { + lines.push(cur); + cur = w; + } + } + if (cur) lines.push(cur); + return lines; +} + +function decodeEntities(text) { + const ta = document.createElement("textarea"); + ta.innerHTML = text; + return ta.value; +} + +function makeDefaultGrid() { + state.keys = []; + for (let y = 0; y < state.rows; y++) { + for (let x = 0; x < state.cols; x++) { + state.keys.push({ + id: `${x},${y}`, + x, + y, + w: 1, + h: 1, + label: "", + fill: "#d0ccc0", + text: "#000000", + fontScale: 1 + }); + } + } + state.selected = null; +} + +function getBoardSizeMm() { + if (!Array.isArray(state.keys)) { + state.keys = []; + } + let maxX = 0; + let maxY = 0; + for (const k of state.keys) { + maxX = Math.max(maxX, k.x + k.w); + maxY = Math.max(maxY, k.y + k.h); + } + return { + w: maxX * state.pitch + state.margin * 2, + h: maxY * state.pitch + state.margin * 2 + }; +} + +function buildSvg() { + const board = getBoardSizeMm(); + const svg = document.createElementNS(ns, "svg"); + svg.setAttribute("xmlns", ns); + svg.setAttribute("viewBox", `0 0 ${board.w.toFixed(2)} ${board.h.toFixed(2)}`); + svg.setAttribute("width", `${board.w.toFixed(2)}mm`); + svg.setAttribute("height", `${board.h.toFixed(2)}mm`); + + const defs = document.createElementNS(ns, "defs"); + const style = document.createElementNS(ns, "style"); + style.textContent = "@font-face {font-family:'Roboto Condensed';font-style:normal;font-weight:400;font-stretch:75%;src:url('https://fonts.gstatic.com/s/roboto/v51/KFOMCnqEu92Fr1ME7kSn66aGLdTylR4MQXC89YmC2DPNWubEbVmUiAo.woff2') format('woff2');}"; + defs.appendChild(style); + svg.appendChild(defs); + + const bgRect = document.createElementNS(ns, "rect"); + bgRect.setAttribute("x", "0"); + bgRect.setAttribute("y", "0"); + bgRect.setAttribute("width", board.w.toFixed(2)); + bgRect.setAttribute("height", board.h.toFixed(2)); + bgRect.setAttribute("fill", state.bg); + svg.appendChild(bgRect); + + const root = document.createElementNS(ns, "g"); + root.setAttribute("transform", `translate(${state.margin},${state.margin})`); + root.setAttribute("font-family", state.fontFamily); + root.setAttribute("font-stretch", FONT_STRETCH); + svg.appendChild(root); + + for (const key of state.keys) { + const x = key.x * state.pitch + state.inset; + const y = key.y * state.pitch + state.inset; + const w = key.w * state.pitch - state.inset * 2; + const h = key.h * state.pitch - state.inset * 2; + + const r = document.createElementNS(ns, "rect"); + r.setAttribute("x", x.toFixed(2)); + r.setAttribute("y", y.toFixed(2)); + r.setAttribute("width", w.toFixed(2)); + r.setAttribute("height", h.toFixed(2)); + r.setAttribute("rx", state.radius.toString()); + r.setAttribute("ry", state.radius.toString()); + r.setAttribute("fill", key.fill); + r.setAttribute("stroke", "#2f2f2f"); + r.setAttribute("stroke-width", "0.2"); + r.classList.add("key-hit"); + if (state.selected && state.selected.id === key.id) { + r.classList.add("selected"); + } + r.addEventListener("click", () => { + state.selected = key; + syncSelectedForm(); + render(); + }); + root.appendChild(r); + + if (Array.isArray(key.legends) && key.legends.length) { + renderKleLegends(root, key, x, y, w, h); + } else if (key.label.trim()) { + const text = document.createElementNS(ns, "text"); + const lines = key.label.split("\n"); + const f = (state.fontSizeMm * key.fontScale); + const lineHeight = f * 1.2; + const totalH = lines.length * lineHeight; + const tx = x + w / 2; + const startY = y + (h - totalH) / 2 + f * 0.85; + text.setAttribute("x", tx.toFixed(2)); + text.setAttribute("y", startY.toFixed(2)); + text.setAttribute("fill", key.text); + text.setAttribute("font-size", f.toFixed(2)); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("font-weight", "700"); + + lines.forEach((line, i) => { + const tspan = document.createElementNS(ns, "tspan"); + tspan.setAttribute("x", tx.toFixed(2)); + tspan.setAttribute("dy", i === 0 ? "0" : lineHeight.toFixed(2)); + tspan.textContent = line; + text.appendChild(tspan); + }); + root.appendChild(text); + } + } + return svg; +} + +function renderKleLegends(root, key, x, y, w, h) { + const pad = 1.5; + const centered = key.align === 7; + const availW = w - 2 * pad; + const nonEmpty = []; + for (let i = 0; i < Math.min(12, key.legends.length); i++) { + if ((key.legends[i] || "").trim()) nonEmpty.push(i); + } + + if (centered && nonEmpty.length === 1) { + const idx = nonEmpty[0]; + const raw = (key.legends[idx] || "").trim(); + const fmm = Array.isArray(key.legendFontMm) && Number.isFinite(key.legendFontMm[idx]) + ? key.legendFontMm[idx] + : state.fontSizeMm; + const lines = wrapText(raw, availW, fmm); + const totalH = lines.length * fmm * 1.25; + const startY = y + (h - totalH) / 2 + fmm * 0.9; + const cx = x + w / 2; + lines.forEach((ln, li) => { + const text = document.createElementNS(ns, "text"); + text.setAttribute("x", cx.toFixed(2)); + text.setAttribute("y", (startY + li * fmm * 1.25).toFixed(2)); + text.setAttribute("fill", key.text); + text.setAttribute("font-size", fmm.toFixed(2)); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("font-weight", "700"); + text.textContent = ln; + root.appendChild(text); + }); + return; + } + + for (let i = 0; i < Math.min(12, key.legends.length); i++) { + const legend = (key.legends[i] || "").trim(); + if (!legend) continue; + + const pos = LABEL_POS[i]; + if (!pos) continue; + const [col, row] = pos; + if (row === 3) continue; + + const fmm = Array.isArray(key.legendFontMm) && Number.isFinite(key.legendFontMm[i]) + ? key.legendFontMm[i] + : state.fontSizeMm; + + let tx; + let anchor; + if (centered) { + tx = x + w / 2; + anchor = "middle"; + } else if (col === 0) { + tx = x + pad; + anchor = "start"; + } else if (col === 1) { + tx = x + w / 2; + anchor = "middle"; + } else { + tx = x + w - pad; + anchor = "end"; + } + + let ty = y + pad + fmm * 0.85; + if (row === 1) { + ty = y + h / 2 + fmm * 0.35; + } else if (row === 2) { + ty = y + h - pad; + } + + const lines = wrapText(legend, availW, fmm); + if (lines.length === 1) { + const text = document.createElementNS(ns, "text"); + text.setAttribute("x", tx.toFixed(2)); + text.setAttribute("y", ty.toFixed(2)); + text.setAttribute("fill", key.text); + text.setAttribute("font-size", fmm.toFixed(2)); + text.setAttribute("text-anchor", anchor); + text.setAttribute("font-weight", "700"); + text.textContent = lines[0]; + root.appendChild(text); + } else { + const text = document.createElementNS(ns, "text"); + text.setAttribute("x", tx.toFixed(2)); + text.setAttribute("y", ty.toFixed(2)); + text.setAttribute("fill", key.text); + text.setAttribute("font-size", fmm.toFixed(2)); + text.setAttribute("text-anchor", anchor); + text.setAttribute("font-weight", "700"); + lines.forEach((ln, li) => { + const tspan = document.createElementNS(ns, "tspan"); + tspan.setAttribute("x", tx.toFixed(2)); + tspan.setAttribute("dy", li === 0 ? "0" : (fmm * 1.25).toFixed(2)); + tspan.textContent = ln; + text.appendChild(tspan); + }); + root.appendChild(text); + } + } +} + +function render() { + el.svgWrap.innerHTML = ""; + const svg = buildSvg(); + el.svgWrap.appendChild(svg); +} + +function syncStateFromControls() { + state.rows = parseInt(el.rows.value, 10) || 1; + state.cols = parseInt(el.cols.value, 10) || 1; + state.pitch = parseFloat(el.pitch.value) || 19.05; + state.inset = parseFloat(el.inset.value) || 0; + state.radius = parseFloat(el.radius.value) || 0; + state.margin = parseFloat(el.margin.value) || 0; + state.bg = el.bg.value; + state.fontFamily = el.font.value.trim() || "'Roboto Condensed', 'Roboto', Arial Narrow, Arial, Helvetica, sans-serif"; + state.fontSizeMm = parseFloat(el.fontSize.value) || 3.8; +} + +function syncSelectedForm() { + if (!state.selected) { + el.selectedHint.textContent = "Click a key in the preview."; + return; + } + el.selectedHint.textContent = `Selected key at (${state.selected.x}, ${state.selected.y})`; + el.keyLabel.value = state.selected.label; + el.keyColor.value = state.selected.fill; + el.keyTextColor.value = state.selected.text; + el.keyW.value = state.selected.w; + el.keyH.value = state.selected.h; + el.keyFontScale.value = state.selected.fontScale; + const legends = Array.isArray(state.selected.legends) ? state.selected.legends : []; + const sizeIdx = Array.isArray(state.selected.legendSizeIdx) ? state.selected.legendSizeIdx : []; + for (let vi = 0; vi < 9; vi++) { + const kleSlot = VISUAL_SLOT_TO_KLE[vi]; + el.legendTextInputs[vi].value = legends[kleSlot] || ""; + el.legendSizeInputs[vi].value = Number.isFinite(sizeIdx[kleSlot]) ? sizeIdx[kleSlot] : 3; + } + el.keyAlign.value = Number.isFinite(state.selected.align) ? state.selected.align : 4; +} + +function updateSelectedKeyFromInputs() { + if (!state.selected) return; + state.selected.label = decodeEntities(el.keyLabel.value); + state.selected.fill = el.keyColor.value; + state.selected.text = el.keyTextColor.value; + state.selected.w = Math.max(0.25, parseFloat(el.keyW.value) || 1); + state.selected.h = Math.max(0.25, parseFloat(el.keyH.value) || 1); + state.selected.fontScale = Math.max(0.5, parseFloat(el.keyFontScale.value) || 1); + + const legends = new Array(12).fill(""); + const sizeIdx = new Array(12).fill(3); + for (let vi = 0; vi < 9; vi++) { + const kleSlot = VISUAL_SLOT_TO_KLE[vi]; + legends[kleSlot] = decodeEntities(el.legendTextInputs[vi].value || ""); + const s = parseInt(el.legendSizeInputs[vi].value, 10); + sizeIdx[kleSlot] = Number.isFinite(s) ? Math.max(0, Math.min(9, s)) : 3; + } + state.selected.legends = legends; + state.selected.legendSizeIdx = sizeIdx; + state.selected.legendFontMm = legends.map((_, i) => { + const idx = Number.isFinite(sizeIdx[i]) && sizeIdx[i] > 0 ? sizeIdx[i] : 3; + return KLE_FS_MM[idx] || KLE_FS_MM[3]; + }); + state.selected.align = Math.max(0, Math.min(7, parseInt(el.keyAlign.value, 10) || 4)); + state.selected.label = legends.filter(Boolean).join(" "); +} + +function downloadTextFile(name, content, type) { + const blob = new Blob([content], { type }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = name; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +function wireEvents() { + [ + el.rows, el.cols, el.pitch, el.inset, el.radius, el.margin, + el.bg, el.font, el.fontSize + ].forEach(node => node.addEventListener("input", () => { + syncStateFromControls(); + render(); + })); + + el.makeGridBtn.addEventListener("click", () => { + syncStateFromControls(); + makeDefaultGrid(); + syncSelectedForm(); + render(); + }); + + el.applyKeyBtn.addEventListener("click", () => { + if (!state.selected) return; + updateSelectedKeyFromInputs(); + render(); + }); + + [ + el.keyLabel, el.keyColor, el.keyTextColor, el.keyW, el.keyH, + el.keyFontScale, el.keyAlign + ].forEach(node => node.addEventListener("input", () => { + if (!state.selected) return; + updateSelectedKeyFromInputs(); + render(); + })); + el.legendTextInputs.forEach(node => node.addEventListener("input", () => { + if (!state.selected) return; + updateSelectedKeyFromInputs(); + render(); + })); + el.legendSizeInputs.forEach(node => node.addEventListener("input", () => { + if (!state.selected) return; + updateSelectedKeyFromInputs(); + render(); + })); + + el.clearKeyBtn.addEventListener("click", () => { + if (!state.selected) return; + state.selected.label = ""; + state.selected.legends = new Array(12).fill(""); + state.selected.legendSizeIdx = new Array(12).fill(3); + state.selected.legendFontMm = new Array(12).fill(KLE_FS_MM[3]); + el.keyLabel.value = ""; + for (let vi = 0; vi < 9; vi++) { + el.legendTextInputs[vi].value = ""; + el.legendSizeInputs[vi].value = 3; + } + render(); + }); + + el.downloadSvgBtn.addEventListener("click", () => { + const svg = buildSvg(); + const content = `\n${svg.outerHTML}\n`; + downloadTextFile("keyboard-layout.svg", content, "image/svg+xml"); + }); + + el.downloadJsonBtn.addEventListener("click", () => { + const payload = { + rows: state.rows, + cols: state.cols, + pitch: state.pitch, + inset: state.inset, + radius: state.radius, + margin: state.margin, + bg: state.bg, + fontFamily: state.fontFamily, + fontSizeMm: state.fontSizeMm, + keys: state.keys + }; + downloadTextFile("keyboard-layout.json", JSON.stringify(payload, null, 2), "application/json"); + }); + + el.loadJsonInput.addEventListener("change", async (ev) => { + const file = ev.target.files && ev.target.files[0]; + if (!file) return; + try { + const text = await file.text(); + const loaded = JSON.parse(text); + loadAnyJson(loaded); + syncSelectedForm(); + render(); + } catch (err) { + console.error(err); + alert("Could not load JSON. Expected editor JSON or KLE JSON."); + } finally { + el.loadJsonInput.value = ""; + } + }); +} + +function loadAnyJson(loaded) { + if (Array.isArray(loaded)) { + loadKleJson(loaded); + return; + } + loadEditorJson(loaded); +} + +function loadEditorJson(loaded) { + state.rows = Math.max(1, parseInt(loaded.rows, 10) || 6); + state.cols = Math.max(1, parseInt(loaded.cols, 10) || 16); + state.pitch = Number.isFinite(loaded.pitch) ? loaded.pitch : 19.05; + state.inset = Number.isFinite(loaded.inset) ? loaded.inset : 0.48; + state.radius = Number.isFinite(loaded.radius) ? loaded.radius : 1; + state.margin = Number.isFinite(loaded.margin) ? loaded.margin : 3; + state.bg = loaded.bg || "#000000"; + state.fontFamily = loaded.fontFamily || "'Roboto Condensed', 'Roboto', Arial Narrow, Arial, Helvetica, sans-serif"; + state.fontSizeMm = Number.isFinite(loaded.fontSizeMm) ? loaded.fontSizeMm : 3.8; + state.keys = Array.isArray(loaded.keys) ? loaded.keys : []; + state.selected = null; + syncControlsFromState(); +} + +function loadKleJson(data) { + const meta = (data[0] && typeof data[0] === "object" && !Array.isArray(data[0])) ? data[0] : {}; + const rows = data.filter(Array.isArray); + + const keys = []; + let cy = 0; + let maxCol = 0; + // KLE properties are sticky across rows. + let c = "#d0ccc0"; + let t = "#000000"; + let f = 1; + let fa = []; + let a = 4; + for (const row of rows) { + let cx = 0; + let w = 1; + let h = 1; + let x = 0; + let y = 0; + + for (const item of row) { + if (typeof item === "string") { + cx += x; + cy += y; + const legends = item.split("\n").map(decodeEntities); + const legendFontMm = []; + const legendSizeIdx = []; + for (let li = 0; li < legends.length; li++) { + const sizeIdx = Array.isArray(fa) && Number.isFinite(fa[li]) && fa[li] > 0 ? fa[li] : f; + legendSizeIdx[li] = sizeIdx; + legendFontMm[li] = KLE_FS_MM[sizeIdx] || KLE_FS_MM[3]; + } + keys.push({ + id: `${cx},${cy},${keys.length}`, + x: cx, + y: cy, + w, + h, + label: legends.filter(Boolean).join(" "), + legends, + legendSizeIdx, + legendFontMm, + align: a, + fill: c, + text: t, + fontScale: 1 + }); + cx += w; + maxCol = Math.max(maxCol, cx); + w = 1; + h = 1; + x = 0; + y = 0; + } else if (item && typeof item === "object" && !Array.isArray(item)) { + c = item.c || c; + t = item.t || t; + if (Number.isFinite(item.a)) a = item.a; + if (Number.isFinite(item.f)) { + f = item.f; + fa = []; + } + if (Array.isArray(item.fa)) { + fa = item.fa; + } + if (Number.isFinite(item.w)) w = item.w; + if (Number.isFinite(item.h)) h = item.h; + if (Number.isFinite(item.x)) x = item.x; + if (Number.isFinite(item.y)) y = item.y; + } + } + cy += 1; + } + + state.pitch = 19.05; + state.inset = 0.48; + state.radius = 1; + state.margin = 3; + state.bg = meta.backcolor || "#000000"; + state.fontFamily = "'Roboto Condensed', 'Roboto', Arial Narrow, Arial, Helvetica, sans-serif"; + state.fontSizeMm = 3.8; + state.rows = Math.max(1, Math.ceil(Math.max(...keys.map(k => k.y + k.h), 1))); + state.cols = Math.max(1, Math.ceil(maxCol || 1)); + state.keys = keys; + state.selected = null; + syncControlsFromState(); +} + +function syncControlsFromState() { + el.rows.value = state.rows; + el.cols.value = state.cols; + el.pitch.value = state.pitch; + el.inset.value = state.inset; + el.radius.value = state.radius; + el.margin.value = state.margin; + el.bg.value = state.bg; + el.font.value = state.fontFamily; + el.fontSize.value = state.fontSizeMm; +} + +syncStateFromControls(); +makeDefaultGrid(); +wireEvents(); +render(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..889db38 --- /dev/null +++ b/index.html @@ -0,0 +1,122 @@ + + + + + + Keyboard SVG Editor + + + +
+

Keyboard SVG Editor

+

WYSIWYG grid editor for manufacturer-ready SVG output.

+
+ +
+
+

Layout

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ +

Selected Key

+
Click a key in the preview.
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
Top Left
+
Top Center
+
Top Right
+
Mid Left
+
Mid Center
+
Mid Right
+
Bottom Left
+
Bottom Center
+
Bottom Right
+
+
+
+ + +
+
+ + +
+
+ +
+

Preview (this is your exported SVG)

+
+
+
+ + + + diff --git a/kle_to_svg.py b/kle_to_svg.py new file mode 100644 index 0000000..f4a09fa --- /dev/null +++ b/kle_to_svg.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +"""Convert a KLE (keyboard-layout-editor.com) JSON file to a flat SVG +suitable for keycap manufacturing — single-colour rectangles, no 3-D shading.""" + +import json +import html +import sys +import math + +# ── layout constants (mm) ────────────────────────────────────────────── +KEY_PITCH = 19.05 # centre-to-centre spacing +CAP_INSET = 0.475 # gap between adjacent caps (each side) +CORNER_R = 1.0 # rounded-corner radius on each cap +PAD = 1.5 # text padding inside cap edge +MARGIN = 3.0 # margin around the whole board +STROKE_W = 0.25 # thin border on each cap for definition + +FONT = "'Roboto Condensed', 'Roboto', Arial Narrow, Arial, Helvetica, sans-serif" +FONT_STRETCH = "condensed" # 75% width axis + +# KLE font-size index (1-9) → mm height. 0 means "use default". +# KLE uses fontSize_px = 6 + 2*size on a ~48 px key; scale to 18.1 mm cap +FS_MM = {0: 4.5, 1: 3.0, 2: 3.8, 3: 4.5, 4: 5.3, + 5: 6.0, 6: 6.8, 7: 7.5, 8: 8.3, 9: 9.0} + +# Label-index → (column 0-2, row 0-3) in the 3×4 keycap label grid. +# col: 0=left 1=centre 2=right +# row: 0=top 1=middle 2=bottom 3=front-face (skipped in top-view) +LABEL_POS = { + 0: (0, 0), 1: (0, 2), 2: (2, 0), 3: (2, 2), + 4: (0, 3), 5: (2, 3), 6: (0, 1), 7: (2, 1), + 8: (1, 0), 9: (1, 1), 10: (1, 2), 11: (1, 3), +} + + +# ── KLE JSON parser ──────────────────────────────────────────────────── +def parse_kle(path): + with open(path, encoding="utf-8") as fh: + data = json.load(fh) + + meta, keys = {}, [] + + # sticky state + c, t, a, f = "#cccccc", "#000000", 0, 3 + fa = [] + + cy = -1 + for item in data: + if isinstance(item, dict): + meta = item + continue + if not isinstance(item, list): + continue + + cy += 1 + cx = 0.0 + nw, nh, nx, ny = 1, 1, 0, 0 + + for el in item: + if isinstance(el, dict): + c = el.get("c", c) + t = el.get("t", t) + a = el.get("a", a) + if "f" in el: + f = el["f"]; fa = [] + if "fa" in el: + fa = list(el["fa"]) + nw = el.get("w", nw) + nh = el.get("h", nh) + nx = el.get("x", nx) + ny = el.get("y", ny) + elif isinstance(el, str): + cx += nx + cy += ny + labels = [html.unescape(s) for s in el.split("\n")] + keys.append(dict( + x=cx, y=cy, w=nw, h=nh, + c=c, t=t, a=a, f=f, fa=list(fa), + labels=labels, + )) + cx += nw + nw, nh, nx, ny = 1, 1, 0, 0 + + return meta, keys + + +# ── helpers ───────────────────────────────────────────────────────────── +def _xml(text): + return (text.replace("&", "&").replace("<", "<") + .replace(">", ">").replace('"', """)) + + +def _font_mm(key, idx): + v = key["fa"][idx] if idx < len(key["fa"]) else 0 + return FS_MM.get(v if v > 0 else key["f"], FS_MM[0]) + + +def _wrap(text, avail_mm, fmm): + """Break *text* into lines that roughly fit *avail_mm* at *fmm* size.""" + char_w = fmm * 0.60 + if len(text) * char_w <= avail_mm: + return [text] + max_chars = max(int(avail_mm / char_w), 1) + words, lines, cur = text.split(" "), [], "" + for w in words: + trial = f"{cur} {w}" if cur else w + if len(trial) <= max_chars or not cur: + cur = trial + else: + lines.append(cur) + cur = w + if cur: + lines.append(cur) + return lines + + +def _darken(hex_color, factor=0.15): + """Return a slightly darker version of *hex_color* for the key border.""" + h = hex_color.lstrip("#") + if len(h) != 6: + return "#333333" + r, g, b = (int(h[i:i+2], 16) for i in (0, 2, 4)) + r = int(r * (1 - factor)) + g = int(g * (1 - factor)) + b = int(b * (1 - factor)) + return f"#{r:02x}{g:02x}{b:02x}" + + +# ── SVG builder ───────────────────────────────────────────────────────── +def build_svg(meta, keys): + max_x = max((k["x"] + k["w"]) * KEY_PITCH for k in keys) + max_y = max((k["y"] + k["h"]) * KEY_PITCH for k in keys) + W = max_x + 2 * MARGIN + H = max_y + 2 * MARGIN + bg = meta.get("backcolor", "#333333") + name = meta.get("name", "keyboard") + + out = [ + '', + f'', + f'', + ' ', + f' ', + f' ', + ] + + for k in keys: + kx = k["x"] * KEY_PITCH + CAP_INSET + ky = k["y"] * KEY_PITCH + CAP_INSET + kw = k["w"] * KEY_PITCH - 2 * CAP_INSET + kh = k["h"] * KEY_PITCH - 2 * CAP_INSET + border = _darken(k["c"], 0.20) + + out.append( + f' ' + ) + + non_empty = [(i, l) for i, l in enumerate(k["labels"]) + if l.strip() and i < 12] + if not non_empty: + continue + + centred = (k["a"] == 7) + avail_w = kw - 2 * PAD + + # single centred label — most common for function keys + if centred and len(non_empty) == 1: + idx, lab = non_empty[0] + fmm = _font_mm(k, idx) + lines = _wrap(lab, avail_w, fmm) + total_h = len(lines) * fmm * 1.25 + start_y = ky + (kh - total_h) / 2 + fmm * 0.9 + cx = kx + kw / 2 + for li, ln in enumerate(lines): + ty = start_y + li * fmm * 1.25 + out.append( + f' {_xml(ln)}' + ) + continue + + # multiple labels at indexed positions + for idx, lab in non_empty: + col, row = LABEL_POS[idx] + if row == 3: + continue # skip front-face labels + fmm = _font_mm(k, idx) + + if centred: + tx, anc = kx + kw / 2, "middle" + elif col == 0: + tx, anc = kx + PAD, "start" + elif col == 1: + tx, anc = kx + kw / 2, "middle" + else: + tx, anc = kx + kw - PAD, "end" + + if row == 0: + ty = ky + PAD + fmm * 0.85 + elif row == 1: + ty = ky + kh / 2 + fmm * 0.35 + else: + ty = ky + kh - PAD + + lines = _wrap(lab, avail_w, fmm) + if len(lines) == 1: + out.append( + f' {_xml(lines[0])}' + ) + else: + out.append( + f' ' + ) + for li, ln in enumerate(lines): + dy = 0 if li == 0 else fmm * 1.25 + out.append( + f' ' + f'{_xml(ln)}' + ) + out.append(' ') + + out.append(' ') + out.append('') + return "\n".join(out) + + +# ── main ──────────────────────────────────────────────────────────────── +def main(): + src = sys.argv[1] if len(sys.argv) > 1 else "rdt-v1.json" + dst = sys.argv[2] if len(sys.argv) > 2 else src.replace(".json", ".svg") + meta, keys = parse_kle(src) + svg = build_svg(meta, keys) + with open(dst, "w", encoding="utf-8") as fh: + fh.write(svg) + print(f"Done: {dst}") + print(f" {len(keys)} keys, " + f"{max((k['x']+k['w'])*KEY_PITCH for k in keys):.1f} × " + f"{max((k['y']+k['h'])*KEY_PITCH for k in keys):.1f} mm layout") + + +if __name__ == "__main__": + main() diff --git a/prod1/144key.svg b/prod1/144key.svg new file mode 100644 index 0000000..8e27cc5 --- /dev/null +++ b/prod1/144key.svg @@ -0,0 +1,386 @@ + + + + + + + + SLECT + MEETING + + Q + 7 + + W + 8 + + E + 9 + + R + 10 + + T + 11 + + Y + 12 + + U + 14 + + I + 16 + + O + 20 + + P + 33 + + SCREEN + MENU + + SCREEN + SORT + + PRIVACY + + BOARD + MENU + + BOARD + LAYOUT + + DISPLAY + ON/OFF + + BLANK + BOARD + + DOWN + LOAD + + A + 11/4 + + S + 3 + + D + 10/3 + + F + 7/2 + + G + 4 + + H + 9/2 + + J + 5 + + K + 11/2 + + L + 6 + + ODDS + ON + + CASH + IN/OUT + + REPORT + MENU + + AUTO + ODDS + + NETWORK + MENU + + HIDE + + FLASH + + BOARD + SORT + + TAB + + Z + 1 + + X + 11/10 + + C + 5/4 + + V + 6/4 + + B + 7/4 + + N + 2 + + M + 9/4 + + SPACE + 5/2 + + SET ALL + + PROFIT + LOSS + + JOIN + NETWORK + + NETWORK + PRICE + CONTROL + + ACCOUNT + MENU + + ROW + UP + + TOP + + EXCH + MENU + + LEVEL + PLACE + + LEVEL + WIN + + CYCLE RACE + + B.O.G + + + + FRACTION + + 5 + EW + + 10 + EW + + 20 + EW + + 25 + EW + + NON + RUNNER + + BET + W/O + + RESULT + + ROW + DOWN + + BOTTOM + + EXCH + BACK + + EXCH + LAY + + SP + + 7 + + 8 + + 9 + + + + ODDS + UP + + 1 + EW + + 1.50 + EW + + 2 + EW + + 2.50 + EW + + 16 + 36 + + 17 + 37 + + 18 + 38 + + 19 + 39 + + 20 + 40 + + RELATED + RACE + + CANCEL + ALL + + PLACE + + 4 + + 5 + + 6 + + - + + 10 + + 20 + + 40 + + 50 + + 11 + 31 + + 12 + 32 + + 13 + 33 + + 14 + 34 + + 15 + 35 + + MANAGE + RACE + MENU + + VOID + BET + + HEDGE + + 1 + + 2 + + 3 + + ENTER + + ODDS + DOWN + + 1 + + 2 + + 2.5 + + 5 + + 6 + 26 + + 7 + 27 + + 8 + 28 + + 9 + 29 + + 10 + 30 + + OPEN + RACE + + RESET + + OWN + PRICE + + / + + 0 + + . + + EACH + WAY + + LIST + BETS + + 20+ + + 1 + 21 + + 2 + 22 + + 3 + 23 + + 4 + 24 + + 5 + 25 + + CLOSE + RACE + + \ No newline at end of file diff --git a/prod1/160key.svg b/prod1/160key.svg new file mode 100644 index 0000000..9237ae1 --- /dev/null +++ b/prod1/160key.svg @@ -0,0 +1,409 @@ + + + + + + + + Q + 9 + + W + 10 + + E + 11 + + R + 12 + + T + 14 + + Y + 16 + + U + 20 + + I + 25 + + O + 33 + + P + 50 + + 66 + + 100 + 80 + + VENUE + + + + SCREEN + + HIDE + + FLASH + + DISPLAY + ON/OFF + + DISPLAY + RACE + + A + 11/4 + + S + 3 + + D + 10/3 + + F + 7/2 + + G + 4 + + H + 9/2 + + J + 5 + + K + 11/2 + + L + 6 + + 13/2 + + 7 + 15/2 + + 8 + 17/2 + + RACE + + RESULT + + WITH + DRAW + + SORT + ORDER + + HIDE + ALL + + ADVERT + TOGGLE + + MESSAGE + TOGGLE + + Z + 1 + + X + 11/10 + + C + 6/5 + + V + 5/4 + + B + 11/8 + + N + 6/4 + + M + 13/8 + + SPACE + 7/4 + + . + 18/8 + + 2 + + 9/4 + 11/5 + + 5/2 + + NETWORK + + PLACE + ONLY + + DED + + SET + ALL + + ACCOUNTS + + + + TALLY + + COL + CHANGE + + BACK + + LAY + ALL + + CANCEL + ALL + + TOGGLE + VIEW + + - + + OWN + PRICE + + ODDS + ON + + £100 + + £200 + + £500 + + £1000 + + + + FILL + ACC + + SELF + + CASH + AFTER + + BACK + TAB + + TAB + + SCREEN + SORT + + LAY + + FILL & + KILL + + FAIR + PRICE + + 7 + + 8 + + 9 + + / + + £30 + + £40 + + £50 + + £80 + + 36 + 16 + + 37 + 17 + + 38 + 18 + + 39 + 19 + + 40 + 20 + + CASH + IN/OUT + + + + PRINT + OUTS + + PRINT + PAYOUT + + HEDGE + + FRACTION + + 4 + + 5 + + 6 + + + + £2.50 + EW + + £5 + EW + + £10 + EW + + £20 + EW + + 31 + 11 + + 32 + 12 + + 33 + 13 + + 34 + 14 + + 35 + 15 + + ACC 9 + + ACC + 10 + + ACC + 11 + + ACC + 12 + + VOID + BET + + SP + + 1 + + 2 + + 3 + + + + £2.50 + + £5 + + £10 + + £20 + + 26 + 6 + + 27 + 7 + + 28 + 8 + + 29 + 9 + + 30 + 10 + + ACC5 + + ACC6 + + ACC7 + + ACC8 + + ABORT + + MORE + + 0 + + . + + ENTER + + EACH + WAY + + 20+ + + 21 + 1 + + 22 + 2 + + 23 + 3 + + 24 + 4 + + 5 + + ACC 1 + + ACC2 + + ACC3 + + ACC4 + + \ No newline at end of file diff --git a/prod2/144key.svg b/prod2/144key.svg new file mode 100644 index 0000000..cc00ef0 --- /dev/null +++ b/prod2/144key.svg @@ -0,0 +1,386 @@ + + + + + + + + SLECT + MEETING + + Q + 7 + + W + 8 + + E + 9 + + R + 10 + + T + 11 + + Y + 12 + + U + 14 + + I + 16 + + O + 20 + + P + 33 + + SCREEN + MENU + + SCREEN + SORT + + PRIVACY + + BOARD + MENU + + BOARD + LAYOUT + + DISPLAY + ON/OFF + + BLANK + BOARD + + DOWN + LOAD + + A + 11/4 + + S + 3 + + D + 10/3 + + F + 7/2 + + G + 4 + + H + 9/2 + + J + 5 + + K + 11/2 + + L + 6 + + ODDS + ON + + CASH + IN/OUT + + REPORT + MENU + + AUTO + ODDS + + NETWORK + MENU + + HIDE + + FLASH + + BOARD + SORT + + TAB + + Z + 1 + + X + 11/10 + + C + 5/4 + + V + 6/4 + + B + 7/4 + + N + 2 + + M + 9/4 + + SPACE + 5/2 + + SET ALL + + PROFIT + LOSS + + JOIN + NETWORK + + NETWORK + PRICE + CONTROL + + ACCOUNT + MENU + + ROW + UP + + TOP + + EXCH + MENU + + LEVEL + PLACE + + LEVEL + WIN + + CYCLE RACE + + B.O.G + + + + FRACTION + + 5 + EW + + 10 + EW + + 20 + EW + + 25 + EW + + NON + RUNNER + + BET + W/O + + RESULT + + ROW + DOWN + + BOTTOM + + EXCH + BACK + + EXCH + LAY + + SP + + 7 + + 8 + + 9 + + + + ODDS + UP + + 1 + EW + + 1.50 + EW + + 2 + EW + + 2.50 + EW + + 16 + 36 + + 17 + 37 + + 18 + 38 + + 19 + 39 + + 20 + 40 + + RELATED + RACE + + CANCEL + ALL + + PLACE + + 4 + + 5 + + 6 + + - + + 10 + + 20 + + 40 + + 50 + + 11 + 31 + + 12 + 32 + + 13 + 33 + + 14 + 34 + + 15 + 35 + + MANAGE + RACE + MENU + + VOID + BET + + HEDGE + + 1 + + 2 + + 3 + + ENTER + + ODDS + DOWN + + 1 + + 2 + + 2.5 + + 5 + + 6 + 26 + + 7 + 27 + + 8 + 28 + + 9 + 29 + + 10 + 30 + + OPEN + RACE + + RESET + + OWN + PRICE + + / + + 0 + + . + + EACH + WAY + + LIST + BETS + + 20+ + + 1 + 21 + + 2 + 22 + + 3 + 23 + + 4 + 24 + + 5 + 25 + + CLOSE + RACE + + \ No newline at end of file diff --git a/prod2/160key.svg b/prod2/160key.svg new file mode 100644 index 0000000..f325bd6 --- /dev/null +++ b/prod2/160key.svg @@ -0,0 +1,409 @@ + + + + + + + + Q + 9 + + W + 10 + + E + 11 + + R + 12 + + T + 14 + + Y + 16 + + U + 20 + + I + 25 + + O + 33 + + P + 50 + + 66 + + 100 + 80 + + VENUE + + + + SCREEN + + HIDE + + FLASH + + DISPLAY + ON/OFF + + DISPLAY + RACE + + A + 11/4 + + S + 3 + + D + 10/3 + + F + 7/2 + + G + 4 + + H + 9/2 + + J + 5 + + K + 11/2 + + L + 6 + + 13/2 + + 7 + 15/2 + + 8 + 17/2 + + RACE + + RESULT + + WITH + DRAW + + SORT + ORDER + + HIDE + ALL + + ADVERT + TOGGLE + + MESSAGE + TOGGLE + + Z + 1 + + X + 11/10 + + C + 6/5 + + V + 5/4 + + B + 11/8 + + N + 6/4 + + M + 13/8 + + SPACE + 7/4 + + . + 15/8 + + 2 + + 9/4 + 11/5 + + 5/2 + + NETWORK + + PLACE + ONLY + + DED + + SET + ALL + + ACCOUNTS + + + + TALLY + + COL + CHANGE + + BACK + + LAY + ALL + + CANCEL + ALL + + TOGGLE + VIEW + + - + + OWN + PRICE + + ODDS + ON + + £100 + + £200 + + £500 + + £1000 + + + + FILL + ACC + + SELF + + CASH + AFTER + + BACK + TAB + + TAB + + SCREEN + SORT + + LAY + + FILL & + KILL + + FAIR + PRICE + + 7 + + 8 + + 9 + + / + + £30 + + £40 + + £50 + + £80 + + 36 + 16 + + 37 + 17 + + 38 + 18 + + 39 + 19 + + 40 + 20 + + CASH + IN/OUT + + + + PRINT + OUTS + + PRINT + PAYOUT + + HEDGE + + FRACTION + + 4 + + 5 + + 6 + + + + £2.50 + EW + + £5 + EW + + £10 + EW + + £20 + EW + + 31 + 11 + + 32 + 12 + + 33 + 13 + + 34 + 14 + + 35 + 15 + + ACC 9 + + ACC + 10 + + ACC + 11 + + ACC + 12 + + VOID + BET + + SP + + 1 + + 2 + + 3 + + + + £2.50 + + £5 + + £10 + + £20 + + 26 + 6 + + 27 + 7 + + 28 + 8 + + 29 + 9 + + 30 + 10 + + ACC5 + + ACC6 + + ACC7 + + ACC8 + + ABORT + + MORE + + 0 + + . + + ENTER + + EACH + WAY + + 20+ + + 21 + 1 + + 22 + 2 + + 23 + 3 + + 24 + 4 + + 5 + + ACC 1 + + ACC2 + + ACC3 + + ACC4 + + \ No newline at end of file diff --git a/request 15-8.png b/request 15-8.png new file mode 100644 index 0000000..5ee7b1e Binary files /dev/null and b/request 15-8.png differ diff --git a/request ASDFGHJKL.png b/request ASDFGHJKL.png new file mode 100644 index 0000000..e07d9e8 Binary files /dev/null and b/request ASDFGHJKL.png differ diff --git a/style.css b/style.css new file mode 100644 index 0000000..e1d78c5 --- /dev/null +++ b/style.css @@ -0,0 +1,116 @@ +* { box-sizing: border-box; } +body { + margin: 0; + font-family: Inter, Segoe UI, Arial, sans-serif; + background: #111318; + color: #e8edf2; +} +header { + padding: 14px 18px; + border-bottom: 1px solid #2d333b; +} +h1, h2, p { margin: 0; } +header p { margin-top: 4px; color: #b5c0cb; } +.layout { + display: grid; + grid-template-columns: 360px 1fr; + min-height: calc(100vh - 74px); +} +.panel { padding: 14px; } +.controls { + border-right: 1px solid #2d333b; + overflow: auto; +} +.controls h2 { + margin: 12px 0 10px; + font-size: 15px; + color: #c8d3de; +} +.row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + align-items: center; + margin-bottom: 8px; +} +.row-full { + grid-template-columns: 1fr; +} +.row small { grid-column: 1 / -1; color: #9ea8b2; } +label { font-size: 13px; color: #c0cad4; } +input, button, .file-label { + border: 1px solid #39424d; + background: #1b2129; + color: #e8edf2; + border-radius: 6px; + padding: 7px 9px; + font-size: 13px; +} +textarea { + border: 1px solid #39424d; + background: #1b2129; + color: #e8edf2; + border-radius: 6px; + padding: 7px 9px; + font-size: 13px; + resize: vertical; +} +.legend-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + width: 100%; + min-width: 0; +} +.legend-cell { + border: 1px solid #39424d; + border-radius: 6px; + padding: 6px; + background: #171d24; + display: grid; + grid-template-columns: 1fr minmax(2.25rem, 2.75rem); + gap: 4px; + align-items: center; + min-width: 0; + overflow: hidden; +} +.legend-cell small { + grid-column: 1 / -1; + color: #9ea8b2; + font-size: 11px; + line-height: 1.2; +} +.legend-cell input[type="text"] { + min-width: 0; + width: 100%; + padding: 4px 6px; + font-size: 12px; +} +.legend-cell input[type="number"] { + width: 100%; + min-width: 0; + padding: 4px 2px; + font-size: 12px; + text-align: center; +} +button, .file-label { + cursor: pointer; + text-align: center; +} +button:hover, .file-label:hover { background: #242b35; } +input[type="file"] { display: none; } +.preview { overflow: auto; } +#svgWrap { + margin-top: 10px; + padding: 12px; + background: #0e1217; + border: 1px solid #2d333b; + border-radius: 8px; +} +.key-hit { + cursor: pointer; +} +.key-hit.selected { + stroke: #4da3ff !important; + stroke-width: 0.7 !important; +} diff --git a/vendor/keyboard-layout-editor b/vendor/keyboard-layout-editor new file mode 160000 index 0000000..580b916 --- /dev/null +++ b/vendor/keyboard-layout-editor @@ -0,0 +1 @@ +Subproject commit 580b916084e69e600b2144b0217c8b1d9710daa0