@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.
- package/README.md +44 -41
- package/dist/core/treeStructure.d.ts +35 -2
- package/dist/core/treeStructure.js +18 -3
- package/dist/optional/analytics/history.d.ts +12 -1
- package/dist/optional/analytics/index.d.ts +2 -0
- package/dist/optional/analytics/index.js +6 -1
- package/dist/optional/analytics/metrics/comparison.js +8 -4
- package/dist/optional/analytics/metrics/core.d.ts +9 -0
- package/dist/optional/analytics/metrics/core.js +190 -37
- package/dist/optional/analytics/metrics/effort.d.ts +10 -0
- package/dist/optional/analytics/metrics/effort.js +13 -0
- package/dist/optional/analytics/metrics/obl-types.d.ts +93 -0
- package/dist/optional/analytics/metrics/obl-types.js +7 -0
- package/dist/optional/analytics/metrics/obl.d.ts +40 -0
- package/dist/optional/analytics/metrics/obl.js +287 -0
- package/dist/optional/analytics/metrics/vocabulary.js +6 -4
- package/dist/optional/symbolTools.js +13 -16
- package/dist/processors/astericsGridProcessor.d.ts +15 -0
- package/dist/processors/astericsGridProcessor.js +17 -0
- package/dist/processors/gridset/helpers.d.ts +4 -1
- package/dist/processors/gridset/helpers.js +4 -0
- package/dist/processors/gridset/pluginTypes.js +51 -50
- package/dist/processors/gridset/symbolExtractor.js +3 -2
- package/dist/processors/gridset/symbolSearch.js +9 -7
- package/dist/processors/gridsetProcessor.js +57 -20
- package/dist/processors/obfProcessor.js +12 -0
- package/dist/processors/snap/helpers.d.ts +5 -1
- package/dist/processors/snap/helpers.js +5 -0
- package/dist/processors/snapProcessor.d.ts +2 -0
- package/dist/processors/snapProcessor.js +155 -4
- package/dist/processors/touchchatProcessor.js +24 -5
- package/dist/types/aac.d.ts +63 -0
- package/dist/types/aac.js +33 -0
- package/dist/validation/gridsetValidator.js +10 -0
- 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
|
|
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
|
-
//
|
|
1194
|
-
const yOffset =
|
|
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
|
-
|
|
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
|
-
|
|
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),
|
package/dist/types/aac.d.ts
CHANGED
|
@@ -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.
|
|
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",
|