@willwade/aac-processors 0.0.12 → 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 (35) hide show
  1. package/README.md +44 -41
  2. package/dist/core/treeStructure.d.ts +35 -2
  3. package/dist/core/treeStructure.js +18 -3
  4. package/dist/optional/analytics/history.d.ts +12 -1
  5. package/dist/optional/analytics/index.d.ts +2 -0
  6. package/dist/optional/analytics/index.js +6 -1
  7. package/dist/optional/analytics/metrics/comparison.js +8 -4
  8. package/dist/optional/analytics/metrics/core.d.ts +9 -0
  9. package/dist/optional/analytics/metrics/core.js +190 -37
  10. package/dist/optional/analytics/metrics/effort.d.ts +10 -0
  11. package/dist/optional/analytics/metrics/effort.js +13 -0
  12. package/dist/optional/analytics/metrics/obl-types.d.ts +93 -0
  13. package/dist/optional/analytics/metrics/obl-types.js +7 -0
  14. package/dist/optional/analytics/metrics/obl.d.ts +40 -0
  15. package/dist/optional/analytics/metrics/obl.js +287 -0
  16. package/dist/optional/analytics/metrics/vocabulary.js +6 -4
  17. package/dist/optional/symbolTools.js +13 -16
  18. package/dist/processors/astericsGridProcessor.d.ts +15 -0
  19. package/dist/processors/astericsGridProcessor.js +17 -0
  20. package/dist/processors/gridset/helpers.d.ts +4 -1
  21. package/dist/processors/gridset/helpers.js +4 -0
  22. package/dist/processors/gridset/pluginTypes.js +51 -50
  23. package/dist/processors/gridset/symbolExtractor.js +3 -2
  24. package/dist/processors/gridset/symbolSearch.js +9 -7
  25. package/dist/processors/gridsetProcessor.js +57 -20
  26. package/dist/processors/obfProcessor.js +12 -0
  27. package/dist/processors/snap/helpers.d.ts +5 -1
  28. package/dist/processors/snap/helpers.js +5 -0
  29. package/dist/processors/snapProcessor.d.ts +2 -0
  30. package/dist/processors/snapProcessor.js +155 -4
  31. package/dist/processors/touchchatProcessor.js +24 -5
  32. package/dist/types/aac.d.ts +63 -0
  33. package/dist/types/aac.js +33 -0
  34. package/dist/validation/gridsetValidator.js +10 -0
  35. package/package.json +1 -1
@@ -490,18 +490,62 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
490
490
  const cellY = Math.max(0, parseInt(String(cell['@_Y'] || '1'), 10) - 1);
491
491
  const colSpan = parseInt(String(cell['@_ColumnSpan'] || '1'), 10);
492
492
  const rowSpan = parseInt(String(cell['@_RowSpan'] || '1'), 10);
493
+ // Extract scan block number (1-8) for block scanning support
494
+ const scanBlock = parseInt(String(cell['@_ScanBlock'] || '1'), 10);
495
+ // Extract visibility from Grid 3's <Visibility> child element
496
+ // Grid 3 stores visibility as a child element, not an attribute
497
+ // Valid values: Visible, Hidden, Disabled, PointerAndTouchOnly, TouchOnly, PointerOnly
498
+ const grid3Visibility = cell.Visibility || cell.visibility;
499
+ // Map Grid 3 visibility values to AAC standard values
500
+ // Grid 3 can have additional values like TouchOnly, PointerOnly that map to PointerAndTouchOnly
501
+ let cellVisibility;
502
+ if (grid3Visibility) {
503
+ const vis = String(grid3Visibility);
504
+ // Direct mapping for standard values
505
+ if (vis === 'Visible' ||
506
+ vis === 'Hidden' ||
507
+ vis === 'Disabled' ||
508
+ vis === 'PointerAndTouchOnly') {
509
+ cellVisibility = vis;
510
+ }
511
+ // Map Grid 3 specific values to AAC standard
512
+ else if (vis === 'TouchOnly' || vis === 'PointerOnly') {
513
+ cellVisibility = 'PointerAndTouchOnly';
514
+ }
515
+ // Grid 3 may use 'Empty' for cells that exist but have no content
516
+ else if (vis === 'Empty') {
517
+ cellVisibility = 'Empty';
518
+ }
519
+ // Unknown visibility - default to Visible
520
+ else {
521
+ cellVisibility = undefined; // Let it default
522
+ }
523
+ }
493
524
  // Extract label from CaptionAndImage/Caption
494
525
  const content = cell.Content;
495
526
  const captionAndImage = content.CaptionAndImage || content.captionAndImage;
496
527
  let label = captionAndImage?.Caption || captionAndImage?.caption || '';
528
+ // Check if cell has an image/symbol (needed to decide if we should keep it)
529
+ const hasImageCandidate = !!(captionAndImage?.Image ||
530
+ captionAndImage?.image ||
531
+ captionAndImage?.ImageName ||
532
+ captionAndImage?.imageName ||
533
+ captionAndImage?.Symbol ||
534
+ captionAndImage?.symbol);
497
535
  // If no caption, try other sources or create a placeholder
498
536
  if (!label) {
499
- // For cells without captions (like AutoContent cells), create a meaningful label
537
+ // For cells without captions, check if they have images/symbols before skipping
500
538
  if (content.ContentType === 'AutoContent') {
501
539
  label = `AutoContent_${idx}`;
502
540
  }
541
+ else if (hasImageCandidate ||
542
+ content.ContentType === 'Workspace' ||
543
+ content.ContentType === 'LiveCell') {
544
+ // Keep cells with images/symbols even if no caption
545
+ label = `Cell_${idx}`;
546
+ }
503
547
  else {
504
- return; // Skip cells without labels
548
+ return; // Skip cells without labels AND without images/symbols
505
549
  }
506
550
  }
507
551
  const message = label; // Use caption as message
@@ -930,6 +974,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
930
974
  y: cellY,
931
975
  columnSpan: colSpan,
932
976
  rowSpan: rowSpan,
977
+ scanBlock: scanBlock, // Add scan block number for block scanning metrics
933
978
  contentType: pluginMetadata.cellType === pluginTypes_1.Grid3CellType.Regular
934
979
  ? 'Normal'
935
980
  : pluginMetadata.cellType === pluginTypes_1.Grid3CellType.Workspace
@@ -942,6 +987,7 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
942
987
  pluginMetadata.autoContentType,
943
988
  symbolLibrary: symbolLibraryRef?.library || undefined,
944
989
  symbolPath: symbolLibraryRef?.path || undefined,
990
+ visibility: cellVisibility,
945
991
  style: {
946
992
  ...cellStyle,
947
993
  ...inlineStyle, // Inline styles override referenced styles
@@ -1168,30 +1214,18 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1168
1214
  GridGuid: page.id,
1169
1215
  // Calculate grid dimensions based on actual layout
1170
1216
  ColumnDefinitions: this.calculateColumnDefinitions(page),
1171
- RowDefinitions: this.calculateRowDefinitions(page),
1217
+ RowDefinitions: this.calculateRowDefinitions(page, false), // No automatic workspace row injection
1172
1218
  AutoContentCommands: '',
1173
1219
  Cells: page.buttons.length > 0
1174
1220
  ? {
1175
1221
  Cell: [
1176
- // Add workspace/message bar cell at the top of ALL pages
1177
- // Grid3 uses 0-based coordinates; omit X and Y to use defaults (0, 0)
1178
- {
1179
- '@_ColumnSpan': 4,
1180
- Content: {
1181
- ContentType: 'Workspace',
1182
- ContentSubType: 'Chat',
1183
- Style: {
1184
- BasedOnStyle: 'Workspace',
1185
- },
1186
- },
1187
- },
1188
1222
  // Regular button cells
1189
1223
  ...this.filterPageButtons(page.buttons).map((button, btnIndex) => {
1190
1224
  const buttonStyleId = button.style ? addStyle(button.style) : '';
1191
1225
  // Find button position in grid layout
1192
1226
  const position = this.findButtonPosition(page, button, btnIndex);
1193
- // Shift all buttons down by 1 row to make room for workspace
1194
- const yOffset = 1;
1227
+ // Use position directly from tree
1228
+ const yOffset = 0;
1195
1229
  // Build CaptionAndImage object
1196
1230
  const captionAndImage = {
1197
1231
  Caption: button.label || '',
@@ -1232,6 +1266,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1232
1266
  '@_ColumnSpan': position.columnSpan,
1233
1267
  '@_RowSpan': position.rowSpan,
1234
1268
  Content: {
1269
+ ContentType: button.contentType === 'Normal' ? undefined : button.contentType,
1270
+ ContentSubType: button.contentSubType,
1235
1271
  Commands: this.generateCommandsFromSemanticAction(button, tree),
1236
1272
  CaptionAndImage: captionAndImage,
1237
1273
  },
@@ -1347,15 +1383,16 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
1347
1383
  };
1348
1384
  }
1349
1385
  // Helper method to calculate row definitions based on page layout
1350
- calculateRowDefinitions(page) {
1386
+ calculateRowDefinitions(page, addWorkspaceOffset = false) {
1351
1387
  let maxRows = 4; // Default minimum
1388
+ const offset = addWorkspaceOffset ? 1 : 0;
1352
1389
  if (page.grid && page.grid.length > 0) {
1353
- maxRows = Math.max(maxRows, page.grid.length);
1390
+ maxRows = Math.max(maxRows, page.grid.length + offset);
1354
1391
  }
1355
1392
  else {
1356
1393
  // Fallback: estimate from button count
1357
1394
  const estimatedCols = Math.ceil(Math.sqrt(page.buttons.length));
1358
- maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols));
1395
+ maxRows = Math.max(4, Math.ceil(page.buttons.length / estimatedCols)) + offset;
1359
1396
  }
1360
1397
  return {
1361
1398
  RowDefinition: Array(maxRows).fill({}),
@@ -11,6 +11,17 @@ const adm_zip_1 = __importDefault(require("adm-zip"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const obfValidator_1 = require("../validation/obfValidator");
13
13
  const OBF_FORMAT_VERSION = 'open-board-0.1';
14
+ /**
15
+ * Map OBF hidden value to AAC standard visibility
16
+ * OBF: true = hidden, false/undefined = visible
17
+ * Maps to: 'Hidden' | 'Visible' | undefined
18
+ */
19
+ function mapObfVisibility(hidden) {
20
+ if (hidden === undefined) {
21
+ return undefined; // Default to visible
22
+ }
23
+ return hidden ? 'Hidden' : 'Visible';
24
+ }
14
25
  class ObfProcessor extends baseProcessor_1.BaseProcessor {
15
26
  constructor(options) {
16
27
  super(options);
@@ -41,6 +52,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
41
52
  id: String(btn?.id || ''),
42
53
  label: String(btn?.label || ''),
43
54
  message: String(btn?.vocalization || btn?.label || ''),
55
+ visibility: mapObfVisibility(btn.hidden),
44
56
  style: {
45
57
  backgroundColor: btn.background_color,
46
58
  borderColor: btn.border_color,
@@ -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;
@@ -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);
@@ -252,6 +375,34 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
252
375
  const [xStr, yStr] = gridPositionStr.split(',');
253
376
  const gridX = parseInt(xStr, 10);
254
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
+ }
255
406
  // Place button in grid if within bounds and coordinates are valid
256
407
  if (!isNaN(gridX) &&
257
408
  !isNaN(gridY) &&
@@ -24,6 +24,17 @@ function intToHex(colorInt) {
24
24
  // Assuming the color is in ARGB format, we mask out the alpha channel
25
25
  return `#${(colorInt & 0x00ffffff).toString(16).padStart(6, '0')}`;
26
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
+ }
27
38
  class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
28
39
  constructor(options) {
29
40
  super(options);
@@ -132,7 +143,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
132
143
  });
133
144
  // Load button boxes and their cells
134
145
  const buttonBoxQuery = `
135
- SELECT bbc.*, b.*, bb.id as box_id
146
+ SELECT bbc.*, b.*, bb.id as box_id
136
147
  FROM button_box_cells bbc
137
148
  JOIN buttons b ON b.resource_id = bbc.resource_id
138
149
  JOIN button_boxes bb ON bb.id = bbc.button_box_id
@@ -166,7 +177,11 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
166
177
  label: cell.label || '',
167
178
  message: cell.message || '',
168
179
  semanticAction: semanticAction,
169
- semantic_id: cell.symbol_link_id || cell.symbolLinkId || undefined, // Extract semantic_id from symbol_link_id
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)
170
185
  style: {
171
186
  backgroundColor: intToHex(style?.body_color),
172
187
  borderColor: intToHex(style?.border_color),
@@ -183,9 +198,9 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
183
198
  });
184
199
  buttonBoxes.get(cell.box_id)?.push({
185
200
  button,
186
- location: cell.location,
187
- spanX: cell.span_x,
188
- spanY: cell.span_y,
201
+ location: cell.location || 0,
202
+ spanX: cell.span_x || 1,
203
+ spanY: cell.span_y || 1,
189
204
  });
190
205
  });
191
206
  // Map button boxes to pages
@@ -306,6 +321,10 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
306
321
  label: btnRow.label || '',
307
322
  message: btnRow.message || '',
308
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)
309
328
  style: {
310
329
  backgroundColor: intToHex(style?.body_color),
311
330
  borderColor: intToHex(style?.border_color),
@@ -1,4 +1,54 @@
1
1
  import { AACSemanticAction } from '../core/treeStructure';
2
+ /**
3
+ * Scanning selection methods for switch access
4
+ * Determines how the scanning advances through items
5
+ */
6
+ export declare enum ScanningSelectionMethod {
7
+ /** Automatically advance through items at timed intervals */
8
+ AutoScan = "AutoScan",
9
+ /** Automatic scanning with overscan (two-stage scanning) */
10
+ AutoScanWithOverscan = "AutoScanWithOverscan",
11
+ /** Hold switch to advance, release to select */
12
+ HoldToAdvance = "HoldToAdvance",
13
+ /** Hold to advance with overscan */
14
+ HoldToAdvanceWithOverscan = "HoldToAdvanceWithOverscan",
15
+ /** Tap switch to advance, tap again to select */
16
+ TapToAdvance = "TapToAdvance"
17
+ }
18
+ /**
19
+ * Cell scanning order patterns
20
+ * Determines the sequence in which cells are highlighted
21
+ */
22
+ export declare enum CellScanningOrder {
23
+ /** Simple linear scan across rows (left-to-right, top-to-bottom) */
24
+ SimpleScan = "SimpleScan",
25
+ /** Simple linear scan down columns (top-to-bottom, left-to-right) */
26
+ SimpleScanColumnsFirst = "SimpleScanColumnsFirst",
27
+ /** Row-group scanning: highlight rows first, then cells within selected row */
28
+ RowColumnScan = "RowColumnScan",
29
+ /** Column-group scanning: highlight columns first, then cells within selected column */
30
+ ColumnRowScan = "ColumnRowScan"
31
+ }
32
+ /**
33
+ * Scanning configuration for a page or pageset
34
+ * Controls how switch scanning operates
35
+ */
36
+ export interface ScanningConfig {
37
+ /** Method for advancing through items */
38
+ selectionMethod?: ScanningSelectionMethod;
39
+ /** Order in which cells are scanned */
40
+ cellScanningOrder?: CellScanningOrder;
41
+ /** Whether block scanning is enabled (group cells by scanBlock number) */
42
+ blockScanEnabled?: boolean;
43
+ /** Whether to include the workspace/message bar in scanning */
44
+ scanWorkspace?: boolean;
45
+ /** Time in milliseconds to highlight each item */
46
+ forwardScanSpeed?: number;
47
+ /** Time in milliseconds to wait before auto-accepting selection */
48
+ dwellTime?: number;
49
+ /** How the selection is accepted */
50
+ acceptScanMethod?: 'Switch' | 'Timeout' | 'Hold';
51
+ }
2
52
  export interface AACStyle {
3
53
  backgroundColor?: string;
4
54
  fontColor?: string;
@@ -35,7 +85,18 @@ export interface AACButton {
35
85
  y?: number;
36
86
  columnSpan?: number;
37
87
  rowSpan?: number;
88
+ /**
89
+ * Scan block number (1-8) for block scanning
90
+ * Buttons with the same scanBlock number are highlighted together
91
+ * @deprecated Use scanBlock instead (singular, not array)
92
+ */
38
93
  scanBlocks?: number[];
94
+ /**
95
+ * Scan block number (1-8) for block scanning
96
+ * Buttons with the same scanBlock number are highlighted together
97
+ * Reduces scanning effort by grouping buttons
98
+ */
99
+ scanBlock?: number;
39
100
  visibility?: 'Visible' | 'Hidden' | 'Disabled' | 'PointerAndTouchOnly' | 'Empty';
40
101
  directActivate?: boolean;
41
102
  audioDescription?: string;
@@ -58,6 +119,8 @@ export interface AACPage {
58
119
  sounds?: any[];
59
120
  semantic_ids?: string[];
60
121
  clone_ids?: string[];
122
+ scanningConfig?: ScanningConfig;
123
+ scanBlocksConfig?: any[];
61
124
  }
62
125
  export interface AACTree {
63
126
  pages: {
package/dist/types/aac.js CHANGED
@@ -1,2 +1,35 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CellScanningOrder = exports.ScanningSelectionMethod = void 0;
4
+ /**
5
+ * Scanning selection methods for switch access
6
+ * Determines how the scanning advances through items
7
+ */
8
+ var ScanningSelectionMethod;
9
+ (function (ScanningSelectionMethod) {
10
+ /** Automatically advance through items at timed intervals */
11
+ ScanningSelectionMethod["AutoScan"] = "AutoScan";
12
+ /** Automatic scanning with overscan (two-stage scanning) */
13
+ ScanningSelectionMethod["AutoScanWithOverscan"] = "AutoScanWithOverscan";
14
+ /** Hold switch to advance, release to select */
15
+ ScanningSelectionMethod["HoldToAdvance"] = "HoldToAdvance";
16
+ /** Hold to advance with overscan */
17
+ ScanningSelectionMethod["HoldToAdvanceWithOverscan"] = "HoldToAdvanceWithOverscan";
18
+ /** Tap switch to advance, tap again to select */
19
+ ScanningSelectionMethod["TapToAdvance"] = "TapToAdvance";
20
+ })(ScanningSelectionMethod || (exports.ScanningSelectionMethod = ScanningSelectionMethod = {}));
21
+ /**
22
+ * Cell scanning order patterns
23
+ * Determines the sequence in which cells are highlighted
24
+ */
25
+ var CellScanningOrder;
26
+ (function (CellScanningOrder) {
27
+ /** Simple linear scan across rows (left-to-right, top-to-bottom) */
28
+ CellScanningOrder["SimpleScan"] = "SimpleScan";
29
+ /** Simple linear scan down columns (top-to-bottom, left-to-right) */
30
+ CellScanningOrder["SimpleScanColumnsFirst"] = "SimpleScanColumnsFirst";
31
+ /** Row-group scanning: highlight rows first, then cells within selected row */
32
+ CellScanningOrder["RowColumnScan"] = "RowColumnScan";
33
+ /** Column-group scanning: highlight columns first, then cells within selected column */
34
+ CellScanningOrder["ColumnRowScan"] = "ColumnRowScan";
35
+ })(CellScanningOrder || (exports.CellScanningOrder = CellScanningOrder = {}));
@@ -243,6 +243,16 @@ class GridsetValidator extends baseValidator_1.BaseValidator {
243
243
  this.warn(`cell ${cellIdx} on page ${pageIdx} should have a label or image`);
244
244
  }
245
245
  });
246
+ // Validate scan block number (Grid 3 attribute)
247
+ await this.add_check(`page[${pageIdx}]_cell[${cellIdx}]_scanblock`, `cell scan block`, async () => {
248
+ const scanBlock = cell.$.scanBlock || cell.$.ScanBlock;
249
+ if (scanBlock !== undefined) {
250
+ const blockNum = parseInt(scanBlock, 10);
251
+ if (isNaN(blockNum) || blockNum < 1 || blockNum > 8) {
252
+ this.err(`cell ${cellIdx} on page ${pageIdx} has invalid scanBlock value: ${scanBlock} (must be 1-8)`, false);
253
+ }
254
+ }
255
+ });
246
256
  // Check for color attributes
247
257
  const backgroundColor = cell.$.backgroundColor || cell.$.BackgroundColor;
248
258
  const _color = cell.$.color || cell.$.Color;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",