@willwade/aac-processors 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,7 +94,7 @@ This step is only required for Electron apps; regular Node.js consumers do not n
94
94
 
95
95
  ## 🔧 Quick Start
96
96
 
97
- ### Basic Usage (TypeScript/ES6)
97
+ ### Basic Usage (TypeScript)
98
98
 
99
99
  ```typescript
100
100
  import {
@@ -118,15 +118,20 @@ const aacTree = dotProcessor.loadIntoTree("examples/example.dot");
118
118
  console.log("Pages:", Object.keys(aacTree.pages).length);
119
119
  ```
120
120
 
121
- ### Basic Usage (CommonJS)
121
+ ### Platform Support
122
122
 
123
- ```javascript
124
- const { getProcessor, DotProcessor } = require("aac-processors");
123
+ **AACProcessors is designed for Node.js environments only.** It requires Node.js v20+ and cannot run in browsers due to:
125
124
 
126
- const processor = getProcessor("board.dot");
127
- const tree = processor.loadIntoTree("board.dot");
128
- console.log(tree);
129
- ```
125
+ - **File system access** - Required for reading/writing AAC files
126
+ - **Native SQLite** - Used by Snap, TouchChat, and Analytics features
127
+ - **Binary format processing** - ZIP, encrypted formats, etc.
128
+
129
+ **For browser-based AAC display**, consider these alternatives:
130
+ - **obf-renderer** - Display OBF/OBZ files in web apps
131
+ - **Arc Core** - Browser-based AAC communication
132
+ - **Cboard** - Web-based AAC display system
133
+
134
+ This library focuses on **server-side file processing**, not client-side rendering.
130
135
 
131
136
  ### Button Filtering System
132
137
 
@@ -893,3 +898,7 @@ Inspired by the Python AACProcessors project
893
898
  ### Contributing
894
899
 
895
900
  Want to help with any of these items? See our [Contributing Guidelines](#-contributing) and pick an issue that interests you!
901
+
902
+ ### Credits
903
+
904
+ Some of the OBF work is directly from https://github.com/open-aac/obf and https://github.com/open-aac/aac-metrics - OBLA too https://www.openboardformat.org/logs
@@ -86,6 +86,7 @@ export interface AACSemanticAction {
86
86
  touchChat?: {
87
87
  actionCode: number;
88
88
  actionData: string;
89
+ resourceId?: number;
89
90
  };
90
91
  snap?: {
91
92
  navigatePageId?: number;
@@ -699,6 +699,21 @@ exports.GRID3_COMMANDS = {
699
699
  description: 'Clear word prediction buffer',
700
700
  platforms: ['desktop', 'ios', 'medicare', 'medicareBionics'],
701
701
  },
702
+ 'Prediction.PredictThis': {
703
+ id: 'Prediction.PredictThis',
704
+ category: Grid3CommandCategory.AUTO_CONTENT,
705
+ pluginId: 'prediction',
706
+ displayName: 'Predict This',
707
+ description: 'Provide suggestions based on word list',
708
+ parameters: [
709
+ {
710
+ key: 'wordlist',
711
+ type: 'string', // Actually highly structured, but string type is a placeholder
712
+ required: true,
713
+ description: 'Word list for prediction',
714
+ },
715
+ ],
716
+ },
702
717
  'Grammar.Change': {
703
718
  id: 'Grammar.Change',
704
719
  category: Grid3CommandCategory.AUTO_CONTENT,
@@ -128,12 +128,12 @@ function detectPluginCellType(content) {
128
128
  }
129
129
  // AutoContent detection - dynamic word/content suggestions
130
130
  if (contentType === 'AutoContent' || content.Style?.BasedOnStyle === 'AutoContent') {
131
- const autoContentType = extractAutoContentType(content);
131
+ const autoContentType = extractAutoContentType(content) || contentSubType;
132
132
  return {
133
133
  cellType: Grid3CellType.AutoContent,
134
- autoContentType: autoContentType || undefined,
135
- pluginId: inferAutoContentPlugin(autoContentType),
136
- displayName: autoContentType || 'Auto Content',
134
+ autoContentType: autoContentType ? String(autoContentType) : undefined,
135
+ pluginId: inferAutoContentPlugin(autoContentType ? String(autoContentType) : undefined),
136
+ displayName: autoContentType ? String(autoContentType) : 'Auto Content',
137
137
  };
138
138
  }
139
139
  // Regular cell
@@ -12,6 +12,10 @@ declare class GridsetProcessor extends BaseProcessor {
12
12
  private decryptGridsetEntry;
13
13
  private getGridsetPassword;
14
14
  private ensureAlphaChannel;
15
+ /**
16
+ * Extract words from Grid3 WordList structure
17
+ */
18
+ private _extractWordsFromWordList;
15
19
  private generateCommandsFromSemanticAction;
16
20
  private convertGrid3StyleToAACStyle;
17
21
  private getStyleById;
@@ -74,6 +74,35 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
74
74
  // Invalid or unknown format, return white
75
75
  return '#FFFFFFFF';
76
76
  }
77
+ /**
78
+ * Extract words from Grid3 WordList structure
79
+ */
80
+ _extractWordsFromWordList(param) {
81
+ if (!param)
82
+ return [];
83
+ // Sometimes the param itself is the WordList, sometimes it has a WordList property
84
+ const wordList = param.WordList || param.wordlist || (param.Items || param.items ? param : undefined);
85
+ if (!wordList || !(wordList.Items || wordList.items))
86
+ return [];
87
+ const items = wordList.Items?.WordListItem || wordList.items?.wordlistitem || [];
88
+ const itemArr = Array.isArray(items) ? items : [items];
89
+ const words = [];
90
+ for (const item of itemArr) {
91
+ const text = item.Text || item.text;
92
+ if (text) {
93
+ const val = this.textOf(text);
94
+ if (val)
95
+ words.push(val);
96
+ }
97
+ else if (item['#text'] !== undefined) {
98
+ words.push(String(item['#text']));
99
+ }
100
+ else if (typeof item === 'string') {
101
+ words.push(item);
102
+ }
103
+ }
104
+ return words;
105
+ }
77
106
  // Helper function to generate Grid3 commands from semantic actions
78
107
  generateCommandsFromSemanticAction(button, tree) {
79
108
  const semanticAction = button.semanticAction;
@@ -280,8 +309,45 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
280
309
  return undefined;
281
310
  if (typeof val === 'string')
282
311
  return val;
283
- if (typeof val === 'object' && '#text' in val)
284
- return String(val['#text']);
312
+ if (typeof val === 'number')
313
+ return String(val);
314
+ if (typeof val === 'object') {
315
+ if ('#text' in val)
316
+ return String(val['#text']);
317
+ // Handle Grid3 structured format <p><s><r>text</r></s></p>
318
+ // Can start at p, s, or r level
319
+ const parts = [];
320
+ const processS = (s) => {
321
+ if (!s)
322
+ return;
323
+ if (s.r !== undefined) {
324
+ const rElements = Array.isArray(s.r) ? s.r : [s.r];
325
+ for (const r of rElements) {
326
+ if (typeof r === 'object' && r !== null && '#text' in r) {
327
+ parts.push(String(r['#text']));
328
+ }
329
+ else {
330
+ parts.push(String(r));
331
+ }
332
+ }
333
+ }
334
+ };
335
+ if (val.p) {
336
+ const p = val.p;
337
+ const sElements = Array.isArray(p.s) ? p.s : p.s ? [p.s] : [];
338
+ sElements.forEach(processS);
339
+ }
340
+ else if (val.s) {
341
+ const sElements = Array.isArray(val.s) ? val.s : [val.s];
342
+ sElements.forEach(processS);
343
+ }
344
+ else if (val.r !== undefined) {
345
+ processS(val);
346
+ }
347
+ if (parts.length > 0) {
348
+ return parts.join('').trim();
349
+ }
350
+ }
285
351
  return undefined;
286
352
  }
287
353
  extractTexts(filePathOrBuffer) {
@@ -404,15 +470,21 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
404
470
  if (!grid)
405
471
  return;
406
472
  const gridId = this.textOf(grid.GridGuid || grid.gridGuid || grid.id);
407
- let gridName = this.textOf(grid.Name) || this.textOf(grid.name) || this.textOf(grid['@_Name']);
408
- if (!gridName) {
409
- const match = entry.entryName.match(/^Grids\/([^/]+)\//);
410
- if (match)
411
- gridName = match[1];
412
- }
413
- if (gridId && gridName) {
414
- gridNameToIdMap.set(gridName, gridId);
415
- gridIdToNameMap.set(gridId, gridName);
473
+ const gridName = this.textOf(grid.Name) || this.textOf(grid.name) || this.textOf(grid['@_Name']);
474
+ const folderMatch = entry.entryName.match(/^Grids\/([^/]+)\//);
475
+ const folderName = folderMatch ? folderMatch[1] : undefined;
476
+ if (gridId) {
477
+ if (gridName) {
478
+ gridNameToIdMap.set(gridName, gridId);
479
+ gridIdToNameMap.set(gridId, gridName);
480
+ }
481
+ if (folderName) {
482
+ // Folder name is often used as the grid name in Jump.To commands
483
+ gridNameToIdMap.set(folderName, gridId);
484
+ if (!gridName) {
485
+ gridIdToNameMap.set(gridId, folderName);
486
+ }
487
+ }
416
488
  }
417
489
  }
418
490
  catch (e) {
@@ -483,6 +555,32 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
483
555
  for (let r = 0; r < maxRows; r++) {
484
556
  gridLayout[r] = new Array(maxCols).fill(null);
485
557
  }
558
+ const pagePredictedWords = new Set();
559
+ // Extract words from grid-level AutoContentCommands (e.g., Prediction Bar)
560
+ if (grid.AutoContentCommands) {
561
+ const collections = grid.AutoContentCommands.AutoContentCommandCollection;
562
+ const collectionArr = Array.isArray(collections)
563
+ ? collections
564
+ : collections
565
+ ? [collections]
566
+ : [];
567
+ collectionArr.forEach((collection) => {
568
+ const commands = collection.Commands?.Command;
569
+ const commandArr = Array.isArray(commands) ? commands : commands ? [commands] : [];
570
+ commandArr.forEach((command) => {
571
+ const commandId = command['@_ID'] || command.ID || command.id;
572
+ if (commandId === 'Prediction.PredictThis') {
573
+ const params = command.Parameter;
574
+ const paramArr = Array.isArray(params) ? params : params ? [params] : [];
575
+ const wordListParam = paramArr.find((p) => (p['@_Key'] || p.Key || p.key) === 'wordlist');
576
+ if (wordListParam) {
577
+ const words = this._extractWordsFromWordList(wordListParam);
578
+ words.forEach((w) => pagePredictedWords.add(w));
579
+ }
580
+ }
581
+ });
582
+ });
583
+ }
486
584
  cellArr.forEach((cell, idx) => {
487
585
  if (!cell || !cell.Content)
488
586
  return;
@@ -526,7 +624,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
526
624
  // Extract label from CaptionAndImage/Caption
527
625
  const content = cell.Content;
528
626
  const captionAndImage = content.CaptionAndImage || content.captionAndImage;
529
- let label = captionAndImage?.Caption || captionAndImage?.caption || '';
627
+ let label = this.textOf(captionAndImage?.Caption || captionAndImage?.caption) || '';
530
628
  // Check if cell has an image/symbol (needed to decide if we should keep it)
531
629
  const hasImageCandidate = !!(captionAndImage?.Image ||
532
630
  captionAndImage?.image ||
@@ -586,6 +684,25 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
586
684
  if (commands) {
587
685
  const commandArr = Array.isArray(commands) ? commands : [commands];
588
686
  detectedCommands = commandArr.map((cmd) => (0, commands_1.detectCommand)(cmd));
687
+ // Scan all commands for vocabulary (predictions) before identifying primary action
688
+ commandArr.forEach((cmd) => {
689
+ const id = cmd['@_ID'] || cmd.ID || cmd.id;
690
+ if (id === 'Prediction.PredictThis') {
691
+ const params = cmd.Parameter || cmd.parameter;
692
+ const pArr = params ? (Array.isArray(params) ? params : [params]) : [];
693
+ let wlP;
694
+ for (const p of pArr) {
695
+ if (p['@_Key'] === 'wordlist' || p.Key === 'wordlist' || p.key === 'wordlist') {
696
+ wlP = p;
697
+ break;
698
+ }
699
+ }
700
+ if (wlP) {
701
+ const words = this._extractWordsFromWordList(wlP);
702
+ words.forEach((w) => pagePredictedWords.add(w));
703
+ }
704
+ }
705
+ });
589
706
  for (const command of commandArr) {
590
707
  const commandId = command['@_ID'] || command.ID || command.id;
591
708
  const parameters = command.Parameter || command.parameter;
@@ -594,50 +711,52 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
594
711
  ? parameters
595
712
  : [parameters]
596
713
  : [];
597
- // Helper to extract text from Grid3's structured format <p><s><r>text</r></s></p>
598
- const extractStructuredText = (param) => {
599
- // Try to extract from nested p.s structure
600
- if (param.p) {
601
- const p = param.p;
602
- // Handle p.s array or single s element
603
- const sElements = Array.isArray(p.s) ? p.s : p.s ? [p.s] : [];
604
- // Extract all r values and concatenate
605
- const parts = [];
606
- for (const s of sElements) {
607
- if (s && s.r !== undefined) {
608
- parts.push(String(s.r));
609
- }
610
- }
611
- if (parts.length > 0) {
612
- return parts.join('');
714
+ // Helper to get raw parameter object
715
+ const getRawParam = (key) => {
716
+ for (const param of paramArr) {
717
+ if (param['@_Key'] === key || param.Key === key || param.key === key) {
718
+ return param;
613
719
  }
614
720
  }
615
721
  return undefined;
616
722
  };
617
723
  // Helper to get parameter value
618
724
  const getParam = (key) => {
619
- if (!parameters)
725
+ const param = getRawParam(key);
726
+ if (param === undefined)
620
727
  return undefined;
621
- for (const param of paramArr) {
622
- if (param['@_Key'] === key || param.Key === key || param.key === key) {
623
- // First try simple #text value
624
- const simpleValue = param['#text'] ?? param.text ?? param.value;
625
- if (typeof simpleValue === 'string') {
626
- return simpleValue;
627
- }
628
- // Try to extract from structured format (Grid3's <p><s><r> format)
629
- const structuredValue = extractStructuredText(param);
630
- if (structuredValue !== undefined) {
631
- return structuredValue;
632
- }
633
- // Fallback to string conversion
634
- if (typeof param === 'string') {
635
- return param;
636
- }
637
- }
638
- }
728
+ const simpleValue = param['#text'] ?? param.text ?? param.value;
729
+ if (typeof simpleValue === 'string')
730
+ return simpleValue;
731
+ if (typeof simpleValue === 'number')
732
+ return String(simpleValue);
733
+ const structuredValue = this.textOf(param);
734
+ if (structuredValue !== undefined)
735
+ return structuredValue;
736
+ if (typeof param === 'string')
737
+ return param;
639
738
  return undefined;
640
739
  };
740
+ // Skip PredictThis in primary action loop as it was handled in pre-pass
741
+ // unless we need a primary action and nothing else exists
742
+ if (commandId === 'Prediction.PredictThis') {
743
+ if (!semanticAction) {
744
+ const wlParam = getRawParam('wordlist');
745
+ if (wlParam) {
746
+ const words = this._extractWordsFromWordList(wlParam);
747
+ semanticAction = {
748
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
749
+ intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
750
+ text: words.slice(0, 3).join(', '),
751
+ platformData: {
752
+ grid3: { commandId, parameters: { wordlist: words } },
753
+ },
754
+ fallback: { type: 'ACTION', message: 'Predict words' },
755
+ };
756
+ }
757
+ }
758
+ continue;
759
+ }
641
760
  switch (commandId) {
642
761
  case 'Jump.To': {
643
762
  const gridTarget = getParam('grid');
@@ -689,10 +808,13 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
689
808
  };
690
809
  break;
691
810
  case 'Jump.Home':
811
+ case 'Jump.SetHome':
692
812
  // action
813
+ navigationTarget = tree.rootId || undefined;
693
814
  semanticAction = {
694
815
  category: treeStructure_1.AACSemanticCategory.NAVIGATION,
695
816
  intent: treeStructure_1.AACSemanticIntent.GO_HOME,
817
+ targetId: tree.rootId || undefined,
696
818
  platformData: {
697
819
  grid3: {
698
820
  commandId,
@@ -708,6 +830,79 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
708
830
  type: 'GO_HOME',
709
831
  };
710
832
  break;
833
+ case 'Jump.ToKeyboard': {
834
+ // Navigate to the set keyboard if we found one in settings
835
+ const keyboardGridName = tree.keyboardGridName;
836
+ const keyboardPageId = gridNameToIdMap.get(keyboardGridName);
837
+ if (keyboardPageId) {
838
+ navigationTarget = keyboardPageId;
839
+ }
840
+ semanticAction = {
841
+ category: treeStructure_1.AACSemanticCategory.NAVIGATION,
842
+ intent: treeStructure_1.AACSemanticIntent.GO_HOME, // Close enough to 'navigation to keyboard'
843
+ targetId: keyboardPageId,
844
+ platformData: {
845
+ grid3: {
846
+ commandId,
847
+ parameters: {},
848
+ },
849
+ },
850
+ fallback: {
851
+ type: 'NAVIGATE',
852
+ targetPageId: keyboardPageId,
853
+ },
854
+ };
855
+ break;
856
+ }
857
+ case 'Action.InsertTextAndSpeak': {
858
+ const insertText = getParam('text');
859
+ semanticAction = {
860
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
861
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_IMMEDIATE,
862
+ text: insertText,
863
+ platformData: {
864
+ grid3: {
865
+ commandId,
866
+ parameters: { text: insertText },
867
+ },
868
+ },
869
+ fallback: {
870
+ type: 'SPEAK',
871
+ message: insertText,
872
+ },
873
+ };
874
+ break;
875
+ }
876
+ case 'Prediction.PredictThis': {
877
+ const wlParam = getRawParam('wordlist');
878
+ if (wlParam) {
879
+ const words = this._extractWordsFromWordList(wlParam);
880
+ // Add to page-wide set of predicted words
881
+ words.forEach((w) => pagePredictedWords.add(w));
882
+ // Store words in a way that analyzer can find them
883
+ // For now, we'll attach to semanticAction so it can be used later
884
+ // We only set this as the primary action if we don't have one yet
885
+ if (!semanticAction) {
886
+ semanticAction = {
887
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
888
+ intent: treeStructure_1.AACSemanticIntent.PLATFORM_SPECIFIC,
889
+ text: words.slice(0, 3).join(', '), // Provide first few as preview
890
+ platformData: {
891
+ grid3: {
892
+ commandId,
893
+ parameters: { wordlist: words },
894
+ },
895
+ },
896
+ fallback: {
897
+ type: 'ACTION',
898
+ message: 'Predict words',
899
+ },
900
+ };
901
+ }
902
+ }
903
+ // Continue to check other commands (e.g. Action.InsertText)
904
+ continue;
905
+ }
711
906
  case 'Action.Speak': {
712
907
  // speak
713
908
  const speakUnit = getParam('unit');
@@ -963,6 +1158,19 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
963
1158
  if (content.Style.FontSize)
964
1159
  inlineStyle.fontSize = parseInt(String(content.Style.FontSize));
965
1160
  }
1161
+ // Extract grammar tags from commands (Smart Grammar)
1162
+ const grammar = {};
1163
+ detectedCommands.forEach((cmd) => {
1164
+ if (cmd.parameters.pos)
1165
+ grammar.pos = cmd.parameters.pos;
1166
+ if (cmd.parameters.person)
1167
+ grammar.person = cmd.parameters.person;
1168
+ if (cmd.parameters.number)
1169
+ grammar.number = cmd.parameters.number;
1170
+ if (cmd.parameters.feature)
1171
+ grammar.feature = cmd.parameters.feature;
1172
+ });
1173
+ const isSmartGrammarCell = Object.keys(grammar).length > 0;
966
1174
  const button = new treeStructure_1.AACButton({
967
1175
  id: `${gridId}_btn_${idx}`,
968
1176
  label: String(label),
@@ -998,6 +1206,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
998
1206
  pluginMetadata: pluginMetadata, // Store full plugin metadata for future use
999
1207
  grid3Commands: detectedCommands, // Store detected command metadata
1000
1208
  symbolLibraryRef: symbolLibraryRef, // Store full symbol reference
1209
+ grammar: isSmartGrammarCell ? grammar : undefined,
1210
+ isSmartGrammarCell: isSmartGrammarCell,
1001
1211
  },
1002
1212
  });
1003
1213
  // Add button to page
@@ -1011,6 +1221,59 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1011
1221
  }
1012
1222
  }
1013
1223
  });
1224
+ // Process predicted words: Populate AutoContent slots first, then add virtual buttons at bottom
1225
+ if (pagePredictedWords.size > 0) {
1226
+ const extraWords = Array.from(pagePredictedWords).filter((w) => w.trim().length > 0);
1227
+ if (extraWords.length > 0) {
1228
+ let wordIdx = 0;
1229
+ // Step 1: Fill dedicated AutoContent prediction slots (e.g. at the top)
1230
+ page.buttons.forEach((btn) => {
1231
+ if (btn.contentType === 'AutoContent' &&
1232
+ btn.contentSubType === 'Prediction' &&
1233
+ wordIdx < extraWords.length) {
1234
+ const word = extraWords[wordIdx++];
1235
+ btn.label = word;
1236
+ btn.message = word;
1237
+ btn.semanticAction = {
1238
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
1239
+ intent: treeStructure_1.AACSemanticIntent.INSERT_TEXT,
1240
+ text: word,
1241
+ fallback: { type: 'SPEAK', message: word },
1242
+ };
1243
+ }
1244
+ });
1245
+ // Step 2: Add remaining words as virtual buttons at the bottom
1246
+ if (wordIdx < extraWords.length) {
1247
+ const remainingWords = extraWords.slice(wordIdx);
1248
+ const extraRowsCount = Math.ceil(remainingWords.length / maxCols);
1249
+ for (let r = 0; r < extraRowsCount; r++) {
1250
+ const row = new Array(maxCols).fill(null);
1251
+ for (let c = 0; c < maxCols; c++) {
1252
+ const idx = r * maxCols + c;
1253
+ if (idx < remainingWords.length) {
1254
+ const word = remainingWords[idx];
1255
+ const vBtn = new treeStructure_1.AACButton({
1256
+ id: `${gridId}_vpredict_${wordIdx + idx}`,
1257
+ label: word,
1258
+ message: word,
1259
+ x: c,
1260
+ y: maxRows + r,
1261
+ semanticAction: {
1262
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
1263
+ intent: treeStructure_1.AACSemanticIntent.INSERT_TEXT,
1264
+ text: word,
1265
+ fallback: { type: 'SPEAK', message: word },
1266
+ },
1267
+ });
1268
+ row[c] = vBtn;
1269
+ page.addButton(vBtn);
1270
+ }
1271
+ }
1272
+ gridLayout.push(row);
1273
+ }
1274
+ }
1275
+ }
1276
+ }
1014
1277
  // Set the page's grid layout
1015
1278
  page.grid = gridLayout;
1016
1279
  // Generate clone_id for each button in the grid
@@ -1068,6 +1331,11 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1068
1331
  tree.rootId = homeGridId;
1069
1332
  }
1070
1333
  }
1334
+ const keyboardGridName = settingsData?.GridSetSettings?.KeyboardGrid ||
1335
+ settingsData?.gridSetSettings?.keyboardGrid;
1336
+ if (keyboardGridName && typeof keyboardGridName === 'string') {
1337
+ tree.keyboardGridName = keyboardGridName;
1338
+ }
1071
1339
  }
1072
1340
  }
1073
1341
  catch (e) {