@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.
- package/README.md +44 -41
- package/dist/cli/index.js +7 -0
- package/dist/core/analyze.js +1 -0
- package/dist/core/treeStructure.d.ts +45 -2
- package/dist/core/treeStructure.js +22 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +20 -3
- package/dist/{analytics → optional/analytics}/history.d.ts +15 -4
- package/dist/{analytics → optional/analytics}/history.js +3 -3
- package/dist/optional/analytics/index.d.ts +30 -0
- package/dist/optional/analytics/index.js +78 -0
- package/dist/optional/analytics/metrics/comparison.d.ts +36 -0
- package/dist/optional/analytics/metrics/comparison.js +334 -0
- package/dist/optional/analytics/metrics/core.d.ts +45 -0
- package/dist/optional/analytics/metrics/core.js +575 -0
- package/dist/optional/analytics/metrics/effort.d.ts +147 -0
- package/dist/optional/analytics/metrics/effort.js +211 -0
- package/dist/optional/analytics/metrics/index.d.ts +15 -0
- package/dist/optional/analytics/metrics/index.js +36 -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/sentence.d.ts +49 -0
- package/dist/optional/analytics/metrics/sentence.js +112 -0
- package/dist/optional/analytics/metrics/types.d.ts +157 -0
- package/dist/optional/analytics/metrics/types.js +7 -0
- package/dist/optional/analytics/metrics/vocabulary.d.ts +65 -0
- package/dist/optional/analytics/metrics/vocabulary.js +142 -0
- package/dist/optional/analytics/reference/index.d.ts +51 -0
- package/dist/optional/analytics/reference/index.js +102 -0
- package/dist/optional/analytics/utils/idGenerator.d.ts +59 -0
- package/dist/optional/analytics/utils/idGenerator.js +96 -0
- 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 +82 -20
- package/dist/processors/index.d.ts +1 -0
- package/dist/processors/index.js +5 -3
- package/dist/processors/obfProcessor.js +37 -2
- package/dist/processors/obfsetProcessor.d.ts +26 -0
- package/dist/processors/obfsetProcessor.js +179 -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 +184 -5
- package/dist/processors/touchchatProcessor.js +50 -4
- package/dist/types/aac.d.ts +67 -0
- package/dist/types/aac.js +33 -0
- package/dist/validation/gridsetValidator.js +10 -0
- 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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -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),
|