@willwade/aac-processors 0.0.11 → 0.0.13

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.
Files changed (56) hide show
  1. package/README.md +44 -41
  2. package/dist/cli/index.js +7 -0
  3. package/dist/core/analyze.js +1 -0
  4. package/dist/core/treeStructure.d.ts +45 -2
  5. package/dist/core/treeStructure.js +22 -3
  6. package/dist/index.d.ts +2 -1
  7. package/dist/index.js +20 -3
  8. package/dist/{analytics → optional/analytics}/history.d.ts +15 -4
  9. package/dist/{analytics → optional/analytics}/history.js +3 -3
  10. package/dist/optional/analytics/index.d.ts +30 -0
  11. package/dist/optional/analytics/index.js +78 -0
  12. package/dist/optional/analytics/metrics/comparison.d.ts +36 -0
  13. package/dist/optional/analytics/metrics/comparison.js +334 -0
  14. package/dist/optional/analytics/metrics/core.d.ts +45 -0
  15. package/dist/optional/analytics/metrics/core.js +575 -0
  16. package/dist/optional/analytics/metrics/effort.d.ts +147 -0
  17. package/dist/optional/analytics/metrics/effort.js +211 -0
  18. package/dist/optional/analytics/metrics/index.d.ts +15 -0
  19. package/dist/optional/analytics/metrics/index.js +36 -0
  20. package/dist/optional/analytics/metrics/obl-types.d.ts +93 -0
  21. package/dist/optional/analytics/metrics/obl-types.js +7 -0
  22. package/dist/optional/analytics/metrics/obl.d.ts +40 -0
  23. package/dist/optional/analytics/metrics/obl.js +287 -0
  24. package/dist/optional/analytics/metrics/sentence.d.ts +49 -0
  25. package/dist/optional/analytics/metrics/sentence.js +112 -0
  26. package/dist/optional/analytics/metrics/types.d.ts +157 -0
  27. package/dist/optional/analytics/metrics/types.js +7 -0
  28. package/dist/optional/analytics/metrics/vocabulary.d.ts +65 -0
  29. package/dist/optional/analytics/metrics/vocabulary.js +142 -0
  30. package/dist/optional/analytics/reference/index.d.ts +51 -0
  31. package/dist/optional/analytics/reference/index.js +102 -0
  32. package/dist/optional/analytics/utils/idGenerator.d.ts +59 -0
  33. package/dist/optional/analytics/utils/idGenerator.js +96 -0
  34. package/dist/optional/symbolTools.js +13 -16
  35. package/dist/processors/astericsGridProcessor.d.ts +15 -0
  36. package/dist/processors/astericsGridProcessor.js +17 -0
  37. package/dist/processors/gridset/helpers.d.ts +4 -1
  38. package/dist/processors/gridset/helpers.js +4 -0
  39. package/dist/processors/gridset/pluginTypes.js +51 -50
  40. package/dist/processors/gridset/symbolExtractor.js +3 -2
  41. package/dist/processors/gridset/symbolSearch.js +9 -7
  42. package/dist/processors/gridsetProcessor.js +82 -20
  43. package/dist/processors/index.d.ts +1 -0
  44. package/dist/processors/index.js +5 -3
  45. package/dist/processors/obfProcessor.js +37 -2
  46. package/dist/processors/obfsetProcessor.d.ts +26 -0
  47. package/dist/processors/obfsetProcessor.js +179 -0
  48. package/dist/processors/snap/helpers.d.ts +5 -1
  49. package/dist/processors/snap/helpers.js +5 -0
  50. package/dist/processors/snapProcessor.d.ts +2 -0
  51. package/dist/processors/snapProcessor.js +184 -5
  52. package/dist/processors/touchchatProcessor.js +50 -4
  53. package/dist/types/aac.d.ts +67 -0
  54. package/dist/types/aac.js +33 -0
  55. package/dist/validation/gridsetValidator.js +10 -0
  56. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * OBF Set Processor - Handles JSON-formatted .obfset files
4
+ * These are pre-extracted board sets in JSON array format
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ObfsetProcessor = void 0;
11
+ const treeStructure_1 = require("../core/treeStructure");
12
+ const treeStructure_2 = require("../core/treeStructure");
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const baseProcessor_1 = require("../core/baseProcessor");
15
+ class ObfsetProcessor extends baseProcessor_1.BaseProcessor {
16
+ constructor(options = {}) {
17
+ super(options);
18
+ }
19
+ /**
20
+ * Extract all text content
21
+ */
22
+ extractTexts(filePathOrBuffer) {
23
+ const tree = this.loadIntoTree(filePathOrBuffer);
24
+ const texts = new Set();
25
+ Object.values(tree.pages).forEach((page) => {
26
+ if (page.name)
27
+ texts.add(page.name);
28
+ page.buttons.forEach((button) => {
29
+ if (button.label)
30
+ texts.add(button.label);
31
+ });
32
+ });
33
+ return Array.from(texts);
34
+ }
35
+ /**
36
+ * Load an .obfset file (JSON array of boards)
37
+ */
38
+ loadIntoTree(filePathOrBuffer) {
39
+ const tree = new treeStructure_1.AACTree();
40
+ let content;
41
+ if (Buffer.isBuffer(filePathOrBuffer)) {
42
+ content = filePathOrBuffer.toString('utf-8');
43
+ }
44
+ else {
45
+ content = fs_1.default.readFileSync(filePathOrBuffer, 'utf-8');
46
+ }
47
+ const boards = JSON.parse(content);
48
+ // Track board ID mappings
49
+ const boardMap = new Map();
50
+ // First pass: create all boards
51
+ boards.forEach((boardData) => {
52
+ const rows = boardData.grid?.rows || 4;
53
+ const cols = boardData.grid?.columns || 6;
54
+ const name = boardData.name || boardData.id || `Board ${boardData.id}`;
55
+ const page = new treeStructure_2.AACPage({
56
+ id: boardData.id,
57
+ name,
58
+ grid: { columns: cols, rows: rows },
59
+ buttons: [],
60
+ });
61
+ tree.addPage(page);
62
+ boardMap.set(boardData.id, page);
63
+ });
64
+ // Second pass: process buttons and establish parent relationships
65
+ boards.forEach((boardData) => {
66
+ const page = boardMap.get(boardData.id);
67
+ if (!page)
68
+ return;
69
+ const rows = boardData.grid?.rows || 4;
70
+ const cols = boardData.grid?.columns || 6;
71
+ // Initialize grid with nulls
72
+ page.grid = Array.from({ length: rows }, () => Array.from({ length: cols }, () => null));
73
+ // Create button map by ID
74
+ const buttonMap = new Map();
75
+ const buttons = boardData.buttons || [];
76
+ buttons.forEach((btnData) => {
77
+ buttonMap.set(btnData.id, btnData);
78
+ });
79
+ // Process grid order to place buttons in correct positions
80
+ const gridOrder = boardData.grid?.order || [];
81
+ const semanticIds = [];
82
+ const cloneIds = [];
83
+ gridOrder.forEach((row, rowIndex) => {
84
+ row.forEach((buttonId, colIndex) => {
85
+ const btnData = buttonMap.get(buttonId);
86
+ if (btnData) {
87
+ // Create semantic action
88
+ let semanticAction;
89
+ if (btnData.load_board?.id) {
90
+ // Navigation button
91
+ semanticAction = {
92
+ category: treeStructure_2.AACSemanticCategory.NAVIGATION,
93
+ intent: treeStructure_2.AACSemanticIntent.NAVIGATE_TO,
94
+ targetId: btnData.load_board.id,
95
+ fallback: {
96
+ type: 'NAVIGATE',
97
+ targetPageId: btnData.load_board.id,
98
+ add_to_sentence: btnData.load_board.add_to_sentence,
99
+ temporary_home: btnData.load_board.temporary_home,
100
+ },
101
+ platformData: {
102
+ grid3: {
103
+ commandId: 'GO_TO_BOARD',
104
+ parameters: {
105
+ add_to_sentence: btnData.load_board.add_to_sentence,
106
+ temporary_home: btnData.load_board.temporary_home,
107
+ },
108
+ },
109
+ },
110
+ };
111
+ }
112
+ else {
113
+ // Speaking button
114
+ semanticAction = {
115
+ category: treeStructure_2.AACSemanticCategory.COMMUNICATION,
116
+ intent: treeStructure_2.AACSemanticIntent.SPEAK_TEXT,
117
+ text: btnData.label || '',
118
+ fallback: { type: 'SPEAK', message: btnData.label || '' },
119
+ };
120
+ }
121
+ const button = new treeStructure_2.AACButton({
122
+ id: btnData.id,
123
+ label: btnData.label || '',
124
+ message: btnData.label || '',
125
+ targetPageId: btnData.load_board?.id,
126
+ semanticAction,
127
+ semantic_id: btnData.semantic_id,
128
+ clone_id: btnData.clone_id,
129
+ });
130
+ // Add to grid at the correct position
131
+ if (rowIndex < rows && colIndex < cols) {
132
+ page.grid[rowIndex][colIndex] = button;
133
+ }
134
+ page.buttons.push(button);
135
+ // Track IDs
136
+ if (btnData.semantic_id) {
137
+ semanticIds.push(String(btnData.semantic_id));
138
+ }
139
+ if (btnData.clone_id) {
140
+ cloneIds.push(String(btnData.clone_id));
141
+ }
142
+ // Establish parent relationship if this button links to another board
143
+ if (btnData.load_board?.id) {
144
+ const targetPage = boardMap.get(String(btnData.load_board.id));
145
+ if (targetPage) {
146
+ targetPage.parentId = page.id;
147
+ }
148
+ }
149
+ }
150
+ });
151
+ });
152
+ // Store IDs on page
153
+ page.semantic_ids = semanticIds;
154
+ page.clone_ids = cloneIds;
155
+ });
156
+ // Set root board (first board or one with no parent)
157
+ const rootBoard = Array.from(boardMap.values()).find((p) => !p.parentId);
158
+ if (rootBoard) {
159
+ tree.rootId = rootBoard.id;
160
+ }
161
+ return tree;
162
+ }
163
+ /**
164
+ * Process texts (not supported for .obfset currently)
165
+ */
166
+ processTexts(_filePathOrBuffer, _translations, _outputPath) {
167
+ throw new Error('processTexts is not supported for .obfset currently');
168
+ }
169
+ /**
170
+ * Save tree structure back to file
171
+ */
172
+ saveFromTree(_tree, _outputPath) {
173
+ throw new Error('saveFromTree is not supported for .obfset currently');
174
+ }
175
+ supportsExtension(extension) {
176
+ return extension === '.obfset';
177
+ }
178
+ }
179
+ exports.ObfsetProcessor = ObfsetProcessor;
@@ -1,4 +1,4 @@
1
- import { AACTree } from '../../core/treeStructure';
1
+ import { AACTree, AACSemanticCategory, AACSemanticIntent } from '../../core/treeStructure';
2
2
  /**
3
3
  * Build a map of button IDs to resolved image entries for a specific page.
4
4
  * Mirrors the Grid helper for consumers that expect image reference data.
@@ -33,6 +33,10 @@ export interface SnapUsageEntry {
33
33
  timestamp: Date;
34
34
  modeling?: boolean;
35
35
  accessMethod?: number | null;
36
+ type?: 'button' | 'action' | 'utterance' | 'note' | 'other';
37
+ buttonId?: string | null;
38
+ intent?: AACSemanticIntent | string;
39
+ category?: AACSemanticCategory;
36
40
  }>;
37
41
  platform?: {
38
42
  label?: string;
@@ -37,6 +37,7 @@ exports.findSnapUserHistory = findSnapUserHistory;
37
37
  exports.isSnapInstalled = isSnapInstalled;
38
38
  exports.readSnapUsage = readSnapUsage;
39
39
  exports.readSnapUsageForUser = readSnapUsageForUser;
40
+ const treeStructure_1 = require("../../core/treeStructure");
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
@@ -271,6 +272,10 @@ function readSnapUsage(pagesetPath) {
271
272
  timestamp: (0, dotnetTicks_1.dotNetTicksToDate)(BigInt(row.TickValue ?? 0)),
272
273
  modeling: row.Modeling === 1,
273
274
  accessMethod: row.AccessMethod ?? null,
275
+ type: 'button',
276
+ buttonId: row.ButtonId,
277
+ intent: treeStructure_1.AACSemanticIntent.SPEAK_TEXT,
278
+ category: treeStructure_1.AACSemanticCategory.COMMUNICATION,
274
279
  });
275
280
  events.set(buttonId, entry);
276
281
  }
@@ -4,8 +4,10 @@ import { ValidationResult } from '../validation/validationTypes';
4
4
  declare class SnapProcessor extends BaseProcessor {
5
5
  private symbolResolver;
6
6
  private loadAudio;
7
+ private pageLayoutPreference;
7
8
  constructor(symbolResolver?: unknown | null, options?: ProcessorOptions & {
8
9
  loadAudio?: boolean;
10
+ pageLayoutPreference?: 'largest' | 'smallest' | 'scanning' | number;
9
11
  });
10
12
  extractTexts(filePathOrBuffer: string | Buffer): string[];
11
13
  loadIntoTree(filePathOrBuffer: string | Buffer): AACTree;
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.SnapProcessor = void 0;
7
7
  const baseProcessor_1 = require("../core/baseProcessor");
8
8
  const treeStructure_1 = require("../core/treeStructure");
9
- // Removed unused import: FileProcessor
9
+ const idGenerator_1 = require("../optional/analytics/utils/idGenerator");
10
10
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const fs_1 = __importDefault(require("fs"));
@@ -17,8 +17,11 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
17
17
  super(options);
18
18
  this.symbolResolver = null;
19
19
  this.loadAudio = false;
20
+ this.pageLayoutPreference = 'scanning'; // Default to scanning for metrics
20
21
  this.symbolResolver = symbolResolver;
21
22
  this.loadAudio = options.loadAudio !== undefined ? options.loadAudio : true;
23
+ this.pageLayoutPreference =
24
+ options.pageLayoutPreference !== undefined ? options.pageLayoutPreference : 'scanning'; // Default to scanning
22
25
  }
23
26
  extractTexts(filePathOrBuffer) {
24
27
  const tree = this.loadIntoTree(filePathOrBuffer);
@@ -79,11 +82,127 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
79
82
  });
80
83
  tree.addPage(page);
81
84
  });
85
+ const scanGroupsByPageLayout = new Map();
86
+ try {
87
+ const scanGroupRows = db
88
+ .prepare('SELECT Id, SerializedGridPositions, PageLayoutId FROM ScanGroup ORDER BY Id')
89
+ .all();
90
+ if (scanGroupRows && scanGroupRows.length > 0) {
91
+ // Group by PageLayoutId first
92
+ const groupsByLayout = new Map();
93
+ scanGroupRows.forEach((sg) => {
94
+ if (!groupsByLayout.has(sg.PageLayoutId)) {
95
+ groupsByLayout.set(sg.PageLayoutId, []);
96
+ }
97
+ const layoutGroups = groupsByLayout.get(sg.PageLayoutId);
98
+ if (layoutGroups) {
99
+ layoutGroups.push(sg);
100
+ }
101
+ });
102
+ // For each PageLayout, assign scan block numbers based on order (1-based index)
103
+ groupsByLayout.forEach((groups, layoutId) => {
104
+ groups.forEach((sg, index) => {
105
+ // Parse SerializedGridPositions JSON
106
+ let positions = [];
107
+ try {
108
+ positions = JSON.parse(sg.SerializedGridPositions);
109
+ }
110
+ catch (e) {
111
+ // Invalid JSON, skip this group
112
+ return;
113
+ }
114
+ const scanGroup = {
115
+ id: sg.Id,
116
+ scanBlock: index + 1, // Scan block is 1-based index
117
+ positions: positions,
118
+ };
119
+ if (!scanGroupsByPageLayout.has(layoutId)) {
120
+ scanGroupsByPageLayout.set(layoutId, []);
121
+ }
122
+ const layoutGroups = scanGroupsByPageLayout.get(layoutId);
123
+ if (layoutGroups) {
124
+ layoutGroups.push(scanGroup);
125
+ }
126
+ });
127
+ });
128
+ }
129
+ }
130
+ catch (e) {
131
+ // No ScanGroups table or error loading, continue without scan blocks
132
+ console.warn('[SnapProcessor] Failed to load ScanGroups:', e);
133
+ }
82
134
  // Load buttons per page, using UniqueId for page id
83
135
  for (const pageRow of pages) {
84
- let buttons = [];
85
136
  // Create a map to track page grid layouts
86
137
  const pageGrids = new Map();
138
+ // Select PageLayout for this page based on preference
139
+ let selectedPageLayoutId = null;
140
+ try {
141
+ const pageLayouts = db
142
+ .prepare('SELECT Id, PageLayoutSetting FROM PageLayout WHERE PageId = ?')
143
+ .all(pageRow.Id);
144
+ if (pageLayouts && pageLayouts.length > 0) {
145
+ // Parse PageLayoutSetting: "columns,rows,hasScanGroups,?"
146
+ const layoutsWithInfo = pageLayouts.map((pl) => {
147
+ const parts = pl.PageLayoutSetting.split(',');
148
+ const cols = parseInt(parts[0], 10) || 0;
149
+ const rows = parseInt(parts[1], 10) || 0;
150
+ const hasScanning = parts[2] === 'True';
151
+ const size = cols * rows;
152
+ return { id: pl.Id, cols, rows, size, hasScanning };
153
+ });
154
+ // Select based on preference
155
+ if (typeof this.pageLayoutPreference === 'number') {
156
+ // Specific PageLayoutId
157
+ selectedPageLayoutId = this.pageLayoutPreference;
158
+ }
159
+ else if (this.pageLayoutPreference === 'largest') {
160
+ // Select layout with largest grid size, prefer layouts with ScanGroups
161
+ layoutsWithInfo.sort((a, b) => {
162
+ const sizeDiff = b.size - a.size;
163
+ if (sizeDiff !== 0)
164
+ return sizeDiff;
165
+ // Same size, prefer one with ScanGroups
166
+ const aHasScanning = scanGroupsByPageLayout.has(a.id);
167
+ const bHasScanning = scanGroupsByPageLayout.has(b.id);
168
+ return (bHasScanning ? 1 : 0) - (aHasScanning ? 1 : 0);
169
+ });
170
+ selectedPageLayoutId = layoutsWithInfo[0].id;
171
+ }
172
+ else if (this.pageLayoutPreference === 'smallest') {
173
+ // Select layout with smallest grid size, prefer layouts with ScanGroups
174
+ layoutsWithInfo.sort((a, b) => {
175
+ const sizeDiff = a.size - b.size;
176
+ if (sizeDiff !== 0)
177
+ return sizeDiff;
178
+ // Same size, prefer one with ScanGroups
179
+ const aHasScanning = scanGroupsByPageLayout.has(a.id);
180
+ const bHasScanning = scanGroupsByPageLayout.has(b.id);
181
+ return (bHasScanning ? 1 : 0) - (aHasScanning ? 1 : 0);
182
+ });
183
+ selectedPageLayoutId = layoutsWithInfo[0].id;
184
+ }
185
+ else if (this.pageLayoutPreference === 'scanning') {
186
+ // Select layout with scanning enabled (check against actual ScanGroups)
187
+ const scanningLayouts = layoutsWithInfo.filter((l) => scanGroupsByPageLayout.has(l.id));
188
+ if (scanningLayouts.length > 0) {
189
+ scanningLayouts.sort((a, b) => b.size - a.size);
190
+ selectedPageLayoutId = scanningLayouts[0].id;
191
+ }
192
+ else {
193
+ // Fallback to largest
194
+ layoutsWithInfo.sort((a, b) => b.size - a.size);
195
+ selectedPageLayoutId = layoutsWithInfo[0].id;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ catch (e) {
201
+ // Error selecting PageLayout, will load all buttons
202
+ console.warn(`[SnapProcessor] Failed to select PageLayout for page ${pageRow.Id}:`, e);
203
+ }
204
+ // Load buttons
205
+ let buttons = [];
87
206
  try {
88
207
  const buttonColumns = getTableColumns('Button');
89
208
  const selectFields = [
@@ -112,15 +231,19 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
112
231
  ? 'b.SerializedMessageSoundMetadata'
113
232
  : 'NULL AS SerializedMessageSoundMetadata');
114
233
  }
115
- selectFields.push('ep.GridPosition', 'er.PageId as ButtonPageId');
234
+ const placementColumns = getTableColumns('ElementPlacement');
235
+ selectFields.push(placementColumns.has('GridPosition') ? 'ep.GridPosition' : 'NULL AS GridPosition', placementColumns.has('PageLayoutId') ? 'ep.PageLayoutId' : 'NULL AS PageLayoutId', 'er.PageId as ButtonPageId');
116
236
  const buttonQuery = `
117
237
  SELECT ${selectFields.join(', ')}
118
238
  FROM Button b
119
239
  INNER JOIN ElementReference er ON b.ElementReferenceId = er.Id
120
240
  LEFT JOIN ElementPlacement ep ON ep.ElementReferenceId = er.Id
121
- WHERE er.PageId = ?
241
+ WHERE er.PageId = ? ${selectedPageLayoutId ? 'AND ep.PageLayoutId = ?' : ''}
122
242
  `;
123
- buttons = db.prepare(buttonQuery).all(pageRow.Id);
243
+ const queryParams = selectedPageLayoutId
244
+ ? [pageRow.Id, selectedPageLayoutId]
245
+ : [pageRow.Id];
246
+ buttons = db.prepare(buttonQuery).all(...queryParams);
124
247
  }
125
248
  catch (err) {
126
249
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -226,6 +349,9 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
226
349
  targetPageId: targetPageUniqueId,
227
350
  semanticAction: semanticAction,
228
351
  audioRecording: audioRecording,
352
+ semantic_id: btnRow.LibrarySymbolId
353
+ ? `snap_symbol_${btnRow.LibrarySymbolId}`
354
+ : undefined, // Extract semantic_id from LibrarySymbolId
229
355
  style: {
230
356
  backgroundColor: btnRow.BackgroundColor
231
357
  ? `#${btnRow.BackgroundColor.toString(16)}`
@@ -249,6 +375,34 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
249
375
  const [xStr, yStr] = gridPositionStr.split(',');
250
376
  const gridX = parseInt(xStr, 10);
251
377
  const gridY = parseInt(yStr, 10);
378
+ // Set button x,y properties (critical for metrics!)
379
+ if (!isNaN(gridX) && !isNaN(gridY)) {
380
+ button.x = gridX;
381
+ button.y = gridY;
382
+ // Determine scan block from ScanGroups (TD Snap "Group Scan")
383
+ // IMPORTANT: Only match against ScanGroups from the SAME PageLayout
384
+ // A button can exist in multiple layouts with different positions
385
+ const buttonPageLayoutId = btnRow.PageLayoutId;
386
+ if (buttonPageLayoutId && scanGroupsByPageLayout.has(buttonPageLayoutId)) {
387
+ const scanGroups = scanGroupsByPageLayout.get(buttonPageLayoutId);
388
+ if (scanGroups && scanGroups.length > 0) {
389
+ // Find which ScanGroup contains this button's position
390
+ for (const scanGroup of scanGroups) {
391
+ // Skip if positions array is null or undefined
392
+ if (!scanGroup.positions || !Array.isArray(scanGroup.positions)) {
393
+ continue;
394
+ }
395
+ const foundInGroup = scanGroup.positions.some((pos) => pos.Column === gridX && pos.Row === gridY);
396
+ if (foundInGroup) {
397
+ // Use the scan block number from the ScanGroup
398
+ // ScanGroup scanBlock is already 1-based (index + 1)
399
+ button.scanBlock = scanGroup.scanBlock;
400
+ break; // Found the scan block, stop looking
401
+ }
402
+ }
403
+ }
404
+ }
405
+ }
252
406
  // Place button in grid if within bounds and coordinates are valid
253
407
  if (!isNaN(gridX) &&
254
408
  !isNaN(gridY) &&
@@ -258,6 +412,10 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
258
412
  gridX < 10 &&
259
413
  pageGrid[gridY] &&
260
414
  pageGrid[gridY][gridX] === null) {
415
+ // Generate clone_id for button at this position
416
+ const rows = pageGrid.length;
417
+ const cols = pageGrid[0] ? pageGrid[0].length : 10;
418
+ button.clone_id = (0, idGenerator_1.generateCloneId)(rows, cols, gridY, gridX, button.label);
261
419
  pageGrid[gridY][gridX] = button;
262
420
  }
263
421
  }
@@ -274,6 +432,27 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
274
432
  const currentPage = tree.getPage(uniqueId);
275
433
  if (currentPage && pageGrid) {
276
434
  currentPage.grid = pageGrid;
435
+ // Track semantic_ids and clone_ids on the page
436
+ const semanticIds = [];
437
+ const cloneIds = [];
438
+ pageGrid.forEach((row) => {
439
+ row.forEach((btn) => {
440
+ if (btn) {
441
+ if (btn.semantic_id) {
442
+ semanticIds.push(btn.semantic_id);
443
+ }
444
+ if (btn.clone_id) {
445
+ cloneIds.push(btn.clone_id);
446
+ }
447
+ }
448
+ });
449
+ });
450
+ if (semanticIds.length > 0) {
451
+ currentPage.semantic_ids = semanticIds;
452
+ }
453
+ if (cloneIds.length > 0) {
454
+ currentPage.clone_ids = cloneIds;
455
+ }
277
456
  }
278
457
  }
279
458
  return tree;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TouchChatProcessor = void 0;
7
7
  const baseProcessor_1 = require("../core/baseProcessor");
8
8
  const treeStructure_1 = require("../core/treeStructure");
9
+ const idGenerator_1 = require("../optional/analytics/utils/idGenerator");
9
10
  const stringCasing_1 = require("../core/stringCasing");
10
11
  const adm_zip_1 = __importDefault(require("adm-zip"));
11
12
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
@@ -23,6 +24,17 @@ function intToHex(colorInt) {
23
24
  // Assuming the color is in ARGB format, we mask out the alpha channel
24
25
  return `#${(colorInt & 0x00ffffff).toString(16).padStart(6, '0')}`;
25
26
  }
27
+ /**
28
+ * Map TouchChat visible value to AAC standard visibility
29
+ * TouchChat: 0 = Hidden, 1 = Visible
30
+ * Maps to: 'Hidden' | 'Visible' | undefined
31
+ */
32
+ function mapTouchChatVisibility(visible) {
33
+ if (visible === null || visible === undefined) {
34
+ return undefined; // Default to visible
35
+ }
36
+ return visible === 0 ? 'Hidden' : 'Visible';
37
+ }
26
38
  class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
27
39
  constructor(options) {
28
40
  super(options);
@@ -131,7 +143,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
131
143
  });
132
144
  // Load button boxes and their cells
133
145
  const buttonBoxQuery = `
134
- SELECT bbc.*, b.*, bb.id as box_id
146
+ SELECT bbc.*, b.*, bb.id as box_id
135
147
  FROM button_box_cells bbc
136
148
  JOIN buttons b ON b.resource_id = bbc.resource_id
137
149
  JOIN button_boxes bb ON bb.id = bbc.button_box_id
@@ -165,6 +177,11 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
165
177
  label: cell.label || '',
166
178
  message: cell.message || '',
167
179
  semanticAction: semanticAction,
180
+ semantic_id: (cell.symbol_link_id || cell.symbolLinkId) || undefined, // Extract semantic_id from symbol_link_id
181
+ visibility: mapTouchChatVisibility(cell.visible || undefined),
182
+ // Note: TouchChat does not use scan blocks in the file
183
+ // Scanning is a runtime feature (linear/row-column patterns)
184
+ // scanBlock defaults to 1 (no grouping)
168
185
  style: {
169
186
  backgroundColor: intToHex(style?.body_color),
170
187
  borderColor: intToHex(style?.border_color),
@@ -181,9 +198,9 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
181
198
  });
182
199
  buttonBoxes.get(cell.box_id)?.push({
183
200
  button,
184
- location: cell.location,
185
- spanX: cell.span_x,
186
- spanY: cell.span_y,
201
+ location: cell.location || 0,
202
+ spanX: cell.span_x || 1,
203
+ spanY: cell.span_y || 1,
187
204
  });
188
205
  });
189
206
  // Map button boxes to pages
@@ -241,6 +258,31 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
241
258
  const page = tree.getPage(pageId);
242
259
  if (page) {
243
260
  page.grid = grid;
261
+ // Generate clone_id for each button in the grid
262
+ const semanticIds = [];
263
+ const cloneIds = [];
264
+ grid.forEach((row, rowIndex) => {
265
+ row.forEach((btn, colIndex) => {
266
+ if (btn) {
267
+ // Generate clone_id based on position and label
268
+ const rows = grid.length;
269
+ const cols = grid[0] ? grid[0].length : 10;
270
+ btn.clone_id = (0, idGenerator_1.generateCloneId)(rows, cols, rowIndex, colIndex, btn.label);
271
+ cloneIds.push(btn.clone_id);
272
+ // Track semantic_id if present
273
+ if (btn.semantic_id) {
274
+ semanticIds.push(btn.semantic_id);
275
+ }
276
+ }
277
+ });
278
+ });
279
+ // Track IDs on the page
280
+ if (semanticIds.length > 0) {
281
+ page.semantic_ids = semanticIds;
282
+ }
283
+ if (cloneIds.length > 0) {
284
+ page.clone_ids = cloneIds;
285
+ }
244
286
  }
245
287
  });
246
288
  }
@@ -279,6 +321,10 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
279
321
  label: btnRow.label || '',
280
322
  message: btnRow.message || '',
281
323
  semanticAction: semanticAction,
324
+ visibility: mapTouchChatVisibility(btnRow.visible),
325
+ // Note: TouchChat does not use scan blocks in the file
326
+ // Scanning is a runtime feature (linear/row-column patterns)
327
+ // scanBlock defaults to 1 (no grouping)
282
328
  style: {
283
329
  backgroundColor: intToHex(style?.body_color),
284
330
  borderColor: intToHex(style?.border_color),