@willwade/aac-processors 0.0.21 → 0.0.22
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/dist/core/treeStructure.d.ts +2 -2
- package/dist/core/treeStructure.js +4 -1
- package/dist/processors/gridsetProcessor.js +2 -0
- package/dist/processors/obfProcessor.js +11 -4
- package/dist/processors/snapProcessor.js +19 -9
- package/dist/processors/touchchatProcessor.js +72 -21
- package/dist/utilities/analytics/metrics/core.d.ts +1 -1
- package/dist/utilities/analytics/metrics/core.js +191 -212
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AACButton as IAACButton, AACPage as IAACPage, AACTree as IAACTree, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, AACStyle } from '../types/aac';
|
|
2
|
-
export { AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata };
|
|
1
|
+
import { AACButton as IAACButton, AACPage as IAACPage, AACTree as IAACTree, AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, AACStyle, CellScanningOrder, ScanningSelectionMethod } from '../types/aac';
|
|
2
|
+
export { AACTreeMetadata, SnapMetadata, GridSetMetadata, AstericsGridMetadata, TouchChatMetadata, CellScanningOrder, ScanningSelectionMethod, };
|
|
3
3
|
export declare enum AACSemanticCategory {
|
|
4
4
|
COMMUNICATION = "communication",// Speech, text output
|
|
5
5
|
NAVIGATION = "navigation",// Page/grid navigation
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AACTree = exports.AACPage = exports.AACButton = exports.AACScanType = exports.AACSemanticIntent = exports.AACSemanticCategory = void 0;
|
|
3
|
+
exports.AACTree = exports.AACPage = exports.AACButton = exports.AACScanType = exports.AACSemanticIntent = exports.AACSemanticCategory = exports.ScanningSelectionMethod = exports.CellScanningOrder = void 0;
|
|
4
|
+
const aac_1 = require("../types/aac");
|
|
5
|
+
Object.defineProperty(exports, "CellScanningOrder", { enumerable: true, get: function () { return aac_1.CellScanningOrder; } });
|
|
6
|
+
Object.defineProperty(exports, "ScanningSelectionMethod", { enumerable: true, get: function () { return aac_1.ScanningSelectionMethod; } });
|
|
4
7
|
// Semantic action categories for cross-platform compatibility
|
|
5
8
|
var AACSemanticCategory;
|
|
6
9
|
(function (AACSemanticCategory) {
|
|
@@ -1400,6 +1400,8 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
1400
1400
|
const homeGridId = gridNameToIdMap.get(startGridName);
|
|
1401
1401
|
if (homeGridId) {
|
|
1402
1402
|
metadata.defaultHomePageId = homeGridId;
|
|
1403
|
+
// Also set tree.rootId so BoardViewer knows which page to show first
|
|
1404
|
+
tree.rootId = homeGridId;
|
|
1403
1405
|
}
|
|
1404
1406
|
}
|
|
1405
1407
|
const keyboardGridName = settingsData?.GridSetSettings?.KeyboardGrid ||
|
|
@@ -29,6 +29,12 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
29
29
|
}
|
|
30
30
|
processBoard(boardData, _boardPath) {
|
|
31
31
|
const sourceButtons = boardData.buttons || [];
|
|
32
|
+
// Calculate page ID first (used to make button IDs unique)
|
|
33
|
+
const pageId = _boardPath && _boardPath.endsWith('.obf') && !_boardPath.includes('/')
|
|
34
|
+
? _boardPath // Zip entry - use filename to match navigation paths
|
|
35
|
+
: boardData?.id
|
|
36
|
+
? String(boardData.id)
|
|
37
|
+
: _boardPath?.split('/').pop() || '';
|
|
32
38
|
const buttons = sourceButtons.map((btn) => {
|
|
33
39
|
const semanticAction = btn.load_board
|
|
34
40
|
? {
|
|
@@ -50,7 +56,8 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
50
56
|
},
|
|
51
57
|
};
|
|
52
58
|
return new treeStructure_1.AACButton({
|
|
53
|
-
|
|
59
|
+
// Make button ID unique by combining page ID and button ID
|
|
60
|
+
id: `${pageId}::${btn?.id || ''}`,
|
|
54
61
|
label: String(btn?.label || ''),
|
|
55
62
|
message: String(btn?.vocalization || btn?.label || ''),
|
|
56
63
|
visibility: mapObfVisibility(btn.hidden),
|
|
@@ -65,7 +72,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
65
72
|
});
|
|
66
73
|
const buttonMap = new Map(buttons.map((btn) => [btn.id, btn]));
|
|
67
74
|
const page = new treeStructure_1.AACPage({
|
|
68
|
-
id:
|
|
75
|
+
id: pageId, // Use the page ID we calculated earlier
|
|
69
76
|
name: String(boardData?.name || ''),
|
|
70
77
|
grid: [],
|
|
71
78
|
buttons,
|
|
@@ -96,7 +103,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
96
103
|
return;
|
|
97
104
|
if (rowIndex >= rows || colIndex >= cols)
|
|
98
105
|
return;
|
|
99
|
-
const aacBtn = buttonMap.get(
|
|
106
|
+
const aacBtn = buttonMap.get(`${pageId}::${cellId}`);
|
|
100
107
|
if (aacBtn) {
|
|
101
108
|
grid[rowIndex][colIndex] = aacBtn;
|
|
102
109
|
}
|
|
@@ -109,7 +116,7 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
109
116
|
const row = Math.floor(btn.box_id / cols);
|
|
110
117
|
const col = btn.box_id % cols;
|
|
111
118
|
if (row < rows && col < cols) {
|
|
112
|
-
const aacBtn = buttonMap.get(
|
|
119
|
+
const aacBtn = buttonMap.get(`${pageId}::${btn.id}`);
|
|
113
120
|
if (aacBtn) {
|
|
114
121
|
grid[row][col] = aacBtn;
|
|
115
122
|
}
|
|
@@ -104,7 +104,8 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
104
104
|
// Set toolbarId if there's a global toolbar
|
|
105
105
|
if (hasGlobalToolbar) {
|
|
106
106
|
tree.toolbarId = toolbarId || null;
|
|
107
|
-
|
|
107
|
+
// Use defaultHomePageId as root (the content pageset), not the toolbar
|
|
108
|
+
tree.rootId = defaultHomePageId || null;
|
|
108
109
|
}
|
|
109
110
|
else if (defaultHomePageId) {
|
|
110
111
|
tree.rootId = defaultHomePageId;
|
|
@@ -114,14 +115,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
114
115
|
catch (e) {
|
|
115
116
|
console.warn('[SnapProcessor] Failed to load PageSetProperties:', e);
|
|
116
117
|
}
|
|
117
|
-
// If still no root,
|
|
118
|
-
if (!tree.rootId || tree.rootId === defaultHomePageId) {
|
|
119
|
-
const toolbarPage = pages.find((p) => p.Title === 'Tool Bar' || p.Name === 'Tool Bar');
|
|
120
|
-
if (toolbarPage) {
|
|
121
|
-
tree.rootId = String(toolbarPage.UniqueId || toolbarPage.Id);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// If still no root, fallback to first page
|
|
118
|
+
// If still no root, fallback to first page (but don't override a valid defaultHomePageId)
|
|
125
119
|
if (!tree.rootId && pages.length > 0) {
|
|
126
120
|
tree.rootId = String(pages[0].UniqueId || pages[0].Id);
|
|
127
121
|
}
|
|
@@ -144,6 +138,22 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
144
138
|
});
|
|
145
139
|
tree.addPage(page);
|
|
146
140
|
});
|
|
141
|
+
// Try to find toolbar page even if not set in PageSetProperties
|
|
142
|
+
// Some SNAP files have a toolbar page but don't set ToolBarUniqueId
|
|
143
|
+
// This must be done AFTER pages are added to the tree
|
|
144
|
+
if (!tree.toolbarId || tree.toolbarId === '00000000-0000-0000-0000-000000000000') {
|
|
145
|
+
const toolbarPage = Object.values(tree.pages).find((p) => {
|
|
146
|
+
const name = (p.name || '').toLowerCase();
|
|
147
|
+
return name === 'tool bar' || name === 'toolbar';
|
|
148
|
+
});
|
|
149
|
+
if (toolbarPage) {
|
|
150
|
+
tree.toolbarId = toolbarPage.id;
|
|
151
|
+
// Update metadata to reflect toolbar detection
|
|
152
|
+
if (tree.metadata) {
|
|
153
|
+
tree.metadata.hasGlobalToolbar = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
147
157
|
const scanGroupsByPageLayout = new Map();
|
|
148
158
|
try {
|
|
149
159
|
const scanGroupRows = db
|
|
@@ -159,7 +159,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
159
159
|
});
|
|
160
160
|
// Load button boxes and their cells
|
|
161
161
|
const buttonBoxQuery = `
|
|
162
|
-
SELECT bbc.*, b.*, bb.id as box_id
|
|
162
|
+
SELECT bbc.*, b.*, bb.id as box_id, bb.layout_x, bb.layout_y
|
|
163
163
|
FROM button_box_cells bbc
|
|
164
164
|
JOIN buttons b ON b.resource_id = bbc.resource_id
|
|
165
165
|
JOIN button_boxes bb ON bb.id = bbc.button_box_id
|
|
@@ -168,8 +168,14 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
168
168
|
const buttonBoxCells = db.prepare(buttonBoxQuery).all();
|
|
169
169
|
const buttonBoxes = new Map();
|
|
170
170
|
buttonBoxCells.forEach((cell) => {
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
let boxData = buttonBoxes.get(cell.box_id);
|
|
172
|
+
if (!boxData) {
|
|
173
|
+
boxData = {
|
|
174
|
+
layoutX: cell.layout_x || 10,
|
|
175
|
+
layoutY: cell.layout_y || 6,
|
|
176
|
+
buttons: [],
|
|
177
|
+
};
|
|
178
|
+
buttonBoxes.set(cell.box_id, boxData);
|
|
173
179
|
}
|
|
174
180
|
const style = buttonStyles.get(cell.button_style_id);
|
|
175
181
|
// Create semantic action for TouchChat button
|
|
@@ -213,7 +219,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
213
219
|
labelOnTop: toBooleanOrUndefined(style?.label_on_top),
|
|
214
220
|
},
|
|
215
221
|
});
|
|
216
|
-
|
|
222
|
+
boxData.buttons.push({
|
|
217
223
|
button,
|
|
218
224
|
location: cell.location || 0,
|
|
219
225
|
spanX: cell.span_x || 1,
|
|
@@ -228,8 +234,8 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
228
234
|
// Use mapped string ID if available, otherwise use numeric ID as string
|
|
229
235
|
const pageId = numericToRid.get(instance.page_id) || String(instance.page_id);
|
|
230
236
|
const page = tree.getPage(pageId);
|
|
231
|
-
const
|
|
232
|
-
if (page &&
|
|
237
|
+
const boxData = buttonBoxes.get(instance.button_box_id);
|
|
238
|
+
if (page && boxData) {
|
|
233
239
|
// Initialize page grid if not exists (assume max 10x10 grid)
|
|
234
240
|
if (!pageGrids.has(pageId)) {
|
|
235
241
|
const grid = [];
|
|
@@ -243,15 +249,13 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
243
249
|
return;
|
|
244
250
|
const boxX = Number(instance.position_x) || 0;
|
|
245
251
|
const boxY = Number(instance.position_y) || 0;
|
|
246
|
-
const boxWidth =
|
|
252
|
+
const boxWidth = boxData.layoutX; // Use layout_x from button_boxes, not size_x from instance
|
|
247
253
|
// boxHeight not currently used but kept for future span calculations
|
|
248
|
-
// const boxHeight =
|
|
249
|
-
buttons.forEach(({ button, location, spanX, spanY }) => {
|
|
254
|
+
// const boxHeight = boxData.layoutY;
|
|
255
|
+
boxData.buttons.forEach(({ button, location, spanX, spanY }) => {
|
|
250
256
|
const safeLocation = Number(location) || 0;
|
|
251
257
|
const safeSpanX = Number(spanX) || 1;
|
|
252
258
|
const safeSpanY = Number(spanY) || 1;
|
|
253
|
-
// Add button to page
|
|
254
|
-
page.addButton(button);
|
|
255
259
|
// Calculate button position within the button box
|
|
256
260
|
// location is a linear index, convert to grid coordinates
|
|
257
261
|
const buttonX = safeLocation % boxWidth;
|
|
@@ -259,6 +263,11 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
259
263
|
// Calculate absolute position on page
|
|
260
264
|
const absoluteX = boxX + buttonX;
|
|
261
265
|
const absoluteY = boxY + buttonY;
|
|
266
|
+
// Set button's x and y coordinates
|
|
267
|
+
button.x = absoluteX;
|
|
268
|
+
button.y = absoluteY;
|
|
269
|
+
// Add button to page
|
|
270
|
+
page.addButton(button);
|
|
262
271
|
// Place button in grid (handle span)
|
|
263
272
|
for (let r = absoluteY; r < absoluteY + safeSpanY && r < 10; r++) {
|
|
264
273
|
for (let c = absoluteX; c < absoluteX + safeSpanX && c < 10; c++) {
|
|
@@ -412,21 +421,49 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
412
421
|
catch (e) {
|
|
413
422
|
// console.log('No navigation actions found:', e);
|
|
414
423
|
}
|
|
415
|
-
// Try to load root ID from
|
|
424
|
+
// Try to load root ID from multiple sources in order of priority
|
|
416
425
|
try {
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
426
|
+
// First, try to get HOME page from special_pages table (TouchChat specific)
|
|
427
|
+
const specialPagesQuery = "SELECT page_id FROM special_pages WHERE name = 'HOME'";
|
|
428
|
+
const homePageRow = db.prepare(specialPagesQuery).get();
|
|
429
|
+
if (homePageRow) {
|
|
430
|
+
// The page_id is the page's id (not resource_id), need to get the RID
|
|
431
|
+
const homePageIdQuery = `
|
|
432
|
+
SELECT p.id, r.rid
|
|
433
|
+
FROM pages p
|
|
434
|
+
JOIN resources r ON r.id = p.resource_id
|
|
435
|
+
WHERE p.id = ?
|
|
436
|
+
LIMIT 1
|
|
437
|
+
`;
|
|
438
|
+
const homePage = db.prepare(homePageIdQuery).get(homePageRow.page_id);
|
|
439
|
+
if (homePage) {
|
|
440
|
+
const homePageUUID = homePage.rid || String(homePage.id);
|
|
441
|
+
if (tree.getPage(homePageUUID)) {
|
|
442
|
+
tree.rootId = homePageUUID;
|
|
443
|
+
tree.metadata.defaultHomePageId = homePageUUID;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// If no HOME page found, try tree_metadata table (general fallback)
|
|
448
|
+
if (!tree.rootId) {
|
|
449
|
+
const metadataQuery = "SELECT value FROM tree_metadata WHERE key = 'rootId'";
|
|
450
|
+
const rootIdRow = db.prepare(metadataQuery).get();
|
|
451
|
+
if (rootIdRow && tree.getPage(rootIdRow.value)) {
|
|
452
|
+
tree.rootId = rootIdRow.value;
|
|
453
|
+
tree.metadata.defaultHomePageId = rootIdRow.value;
|
|
454
|
+
}
|
|
421
455
|
}
|
|
422
|
-
|
|
456
|
+
// Final fallback: first page
|
|
457
|
+
if (!tree.rootId && rootPageId) {
|
|
423
458
|
tree.rootId = rootPageId;
|
|
459
|
+
tree.metadata.defaultHomePageId = rootPageId;
|
|
424
460
|
}
|
|
425
461
|
}
|
|
426
462
|
catch (e) {
|
|
427
|
-
// No metadata table, use first page as root
|
|
463
|
+
// No metadata table or other error, use first page as root
|
|
428
464
|
if (rootPageId) {
|
|
429
465
|
tree.rootId = rootPageId;
|
|
466
|
+
tree.metadata.defaultHomePageId = rootPageId;
|
|
430
467
|
}
|
|
431
468
|
}
|
|
432
469
|
// Set metadata for TouchChat files
|
|
@@ -519,7 +556,14 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
519
556
|
);
|
|
520
557
|
|
|
521
558
|
CREATE TABLE IF NOT EXISTS button_boxes (
|
|
522
|
-
id INTEGER PRIMARY KEY
|
|
559
|
+
id INTEGER PRIMARY KEY,
|
|
560
|
+
resource_id INTEGER,
|
|
561
|
+
layout_x INTEGER DEFAULT 10,
|
|
562
|
+
layout_y INTEGER DEFAULT 6,
|
|
563
|
+
init_size_x INTEGER DEFAULT 10000,
|
|
564
|
+
init_size_y INTEGER DEFAULT 10000,
|
|
565
|
+
scan_pattern_id INTEGER DEFAULT 0,
|
|
566
|
+
FOREIGN KEY (resource_id) REFERENCES resources (id)
|
|
523
567
|
);
|
|
524
568
|
|
|
525
569
|
CREATE TABLE IF NOT EXISTS button_box_cells (
|
|
@@ -677,8 +721,15 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
677
721
|
}
|
|
678
722
|
// Create a button box for this page's buttons
|
|
679
723
|
const buttonBoxId = buttonBoxIdCounter++;
|
|
680
|
-
|
|
681
|
-
|
|
724
|
+
// Create a resource for the button box
|
|
725
|
+
const buttonBoxResourceId = resourceIdCounter++;
|
|
726
|
+
const insertButtonBoxResource = db.prepare('INSERT INTO resources (id, name, type) VALUES (?, ?, ?)');
|
|
727
|
+
insertButtonBoxResource.run(buttonBoxResourceId, page.name || 'ButtonBox', 0);
|
|
728
|
+
// Insert button box with layout dimensions
|
|
729
|
+
const insertButtonBox = db.prepare('INSERT INTO button_boxes (id, resource_id, layout_x, layout_y, init_size_x, init_size_y) VALUES (?, ?, ?, ?, ?, ?)');
|
|
730
|
+
insertButtonBox.run(buttonBoxId, buttonBoxResourceId, gridWidth, gridHeight, 10000, // init_size_x in internal units
|
|
731
|
+
10000 // init_size_y in internal units
|
|
732
|
+
);
|
|
682
733
|
// Create button box instance with calculated dimensions
|
|
683
734
|
const insertButtonBoxInstance = db.prepare('INSERT INTO button_box_instances (id, page_id, button_box_id, position_x, position_y, size_x, size_y) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
684
735
|
insertButtonBoxInstance.run(buttonBoxInstanceIdCounter++, numericPageId, buttonBoxId, 0, // Box starts at origin
|
|
@@ -30,7 +30,7 @@ export declare class MetricsCalculator {
|
|
|
30
30
|
* Count scan items for visual scanning effort
|
|
31
31
|
* When block scanning is enabled, count unique scan blocks instead of individual buttons
|
|
32
32
|
*/
|
|
33
|
-
private
|
|
33
|
+
private countScanBlocks;
|
|
34
34
|
/**
|
|
35
35
|
* Analyze starting from a specific board
|
|
36
36
|
*/
|
|
@@ -221,44 +221,26 @@ class MetricsCalculator {
|
|
|
221
221
|
* Count scan items for visual scanning effort
|
|
222
222
|
* When block scanning is enabled, count unique scan blocks instead of individual buttons
|
|
223
223
|
*/
|
|
224
|
-
|
|
225
|
-
if (!blockScanEnabled) {
|
|
226
|
-
// Linear scanning: count all buttons before current position
|
|
227
|
-
let count = 0;
|
|
228
|
-
for (let r = 0; r <= currentRowIndex; r++) {
|
|
229
|
-
const row = board.grid[r];
|
|
230
|
-
if (!row)
|
|
231
|
-
continue;
|
|
232
|
-
for (let c = 0; c < row.length; c++) {
|
|
233
|
-
if (r === currentRowIndex && c === currentColIndex)
|
|
234
|
-
return count;
|
|
235
|
-
const btn = row[c];
|
|
236
|
-
if (btn && (btn.label || btn.id).length > 0) {
|
|
237
|
-
count++;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return count;
|
|
242
|
-
}
|
|
224
|
+
countScanBlocks(board, currentRowIndex, currentColIndex, priorScanBlocks) {
|
|
243
225
|
// Block scanning: count unique scan blocks before current position
|
|
244
|
-
|
|
226
|
+
// Reuse the priorScanBlocks set from the parent scope
|
|
245
227
|
for (let r = 0; r <= currentRowIndex; r++) {
|
|
246
228
|
const row = board.grid[r];
|
|
247
229
|
if (!row)
|
|
248
230
|
continue;
|
|
249
231
|
for (let c = 0; c < row.length; c++) {
|
|
250
232
|
if (r === currentRowIndex && c === currentColIndex)
|
|
251
|
-
return
|
|
233
|
+
return priorScanBlocks.size;
|
|
252
234
|
const btn = row[c];
|
|
253
235
|
if (btn && (btn.label || btn.id).length > 0) {
|
|
254
236
|
const block = btn.scanBlock ||
|
|
255
237
|
(btn.scanBlocks && btn.scanBlocks.length > 0 ? btn.scanBlocks[0] : null);
|
|
256
238
|
if (block !== null)
|
|
257
|
-
|
|
239
|
+
priorScanBlocks.add(block);
|
|
258
240
|
}
|
|
259
241
|
}
|
|
260
242
|
}
|
|
261
|
-
return
|
|
243
|
+
return priorScanBlocks.size;
|
|
262
244
|
}
|
|
263
245
|
/**
|
|
264
246
|
* Analyze starting from a specific board
|
|
@@ -317,216 +299,213 @@ class MetricsCalculator {
|
|
|
317
299
|
const scanningConfig = options.scanningConfig || board.scanningConfig;
|
|
318
300
|
const blockScanEnabled = scanningConfig?.blockScanEnabled || false;
|
|
319
301
|
// Process each button
|
|
320
|
-
const priorScanBlocks = new Set();
|
|
321
302
|
const btnHeight = 1.0 / rows;
|
|
322
303
|
const btnWidth = 1.0 / cols;
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (btn.semantic_id && boardPcts[btn.semantic_id]) {
|
|
352
|
-
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.semantic_id];
|
|
353
|
-
const old = buttonEffort;
|
|
354
|
-
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
355
|
-
if (debugSpecificButton)
|
|
356
|
-
console.log(` Semantic board discount: ${old.toFixed(6)} -> ${buttonEffort.toFixed(6)} (pct=${boardPcts[btn.semantic_id].toFixed(4)})`);
|
|
357
|
-
}
|
|
358
|
-
else if (btn.semantic_id && boardPcts[`upstream-${btn.semantic_id}`]) {
|
|
359
|
-
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT /
|
|
360
|
-
boardPcts[`upstream-${btn.semantic_id}`];
|
|
361
|
-
const old = buttonEffort;
|
|
362
|
-
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
363
|
-
if (debugSpecificButton)
|
|
364
|
-
console.log(` Semantic upstream discount: ${old.toFixed(6)} -> ${buttonEffort.toFixed(6)} (pct=${boardPcts[`upstream-${btn.semantic_id}`].toFixed(4)})`);
|
|
365
|
-
}
|
|
366
|
-
// Apply clone_id discounts
|
|
367
|
-
if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
368
|
-
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.clone_id];
|
|
369
|
-
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
370
|
-
}
|
|
371
|
-
else if (btn.clone_id && boardPcts[`upstream-${btn.clone_id}`]) {
|
|
372
|
-
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT /
|
|
373
|
-
boardPcts[`upstream-${btn.clone_id}`];
|
|
374
|
-
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
375
|
-
}
|
|
376
|
-
// Calculate button effort based on access method (Touch vs Scanning)
|
|
377
|
-
const isScanning = !!scanningConfig || !!board.scanType;
|
|
378
|
-
if (isScanning) {
|
|
379
|
-
const { steps, selections, loopSteps } = this.calculateScanSteps(board, btn, rowIndex, colIndex, scanningConfig);
|
|
380
|
-
// Determine effective costs based on selection method
|
|
381
|
-
let currentStepCost = options.scanStepCost ?? effort_1.EFFORT_CONSTANTS.SCAN_STEP_COST;
|
|
382
|
-
const currentSelectionCost = options.scanSelectionCost ?? effort_1.EFFORT_CONSTANTS.SCAN_SELECTION_COST;
|
|
383
|
-
// Step Scan 2 Switch: Every step is a physical selection with Switch 1
|
|
384
|
-
if (scanningConfig?.selectionMethod === aac_1.ScanningSelectionMethod.StepScan2Switch) {
|
|
385
|
-
// The cost of moving is now a selection cost
|
|
386
|
-
currentStepCost = currentSelectionCost;
|
|
387
|
-
}
|
|
388
|
-
else if (scanningConfig?.selectionMethod === aac_1.ScanningSelectionMethod.StepScan1Switch) {
|
|
389
|
-
// Single switch step scan: every step is a physical selection
|
|
390
|
-
currentStepCost = currentSelectionCost;
|
|
391
|
-
}
|
|
392
|
-
let sEffort = (0, effort_1.scanningEffort)(steps, selections, currentStepCost, currentSelectionCost);
|
|
393
|
-
// Factor in error correction if enabled
|
|
394
|
-
if (scanningConfig?.errorCorrectionEnabled) {
|
|
395
|
-
const errorRate = scanningConfig.errorRate ?? effort_1.EFFORT_CONSTANTS.DEFAULT_SCAN_ERROR_RATE;
|
|
396
|
-
// A "miss" results in needing to wait for a loop (or part of one)
|
|
397
|
-
// We model this as errorRate * (loopSteps * stepCost)
|
|
398
|
-
const retryPenalty = loopSteps * currentStepCost;
|
|
399
|
-
sEffort += errorRate * retryPenalty;
|
|
304
|
+
// Track scan blocks for block scanning
|
|
305
|
+
const priorScanBlocks = new Set();
|
|
306
|
+
// Iterate over grid positions directly (not just buttons)
|
|
307
|
+
// This matches Ruby's nested loop: rows.times do |row_idx|; columns.times do |col_idx|
|
|
308
|
+
for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
|
|
309
|
+
for (let colIndex = 0; colIndex < cols; colIndex++) {
|
|
310
|
+
const btn = board.grid[rowIndex]?.[colIndex];
|
|
311
|
+
if (!btn)
|
|
312
|
+
continue; // Skip empty cells
|
|
313
|
+
const x = btnWidth / 2 + btnWidth * colIndex;
|
|
314
|
+
const y = btnHeight / 2 + btnHeight * rowIndex;
|
|
315
|
+
// Calculate prior grid positions (not just buttons)
|
|
316
|
+
// This matches Ruby's prior_buttons which increments for each grid position
|
|
317
|
+
const priorGridPositions = rowIndex * cols + colIndex;
|
|
318
|
+
// For block scanning, count unique scan blocks instead
|
|
319
|
+
const priorItems = blockScanEnabled
|
|
320
|
+
? this.countScanBlocks(board, rowIndex, colIndex, priorScanBlocks)
|
|
321
|
+
: priorGridPositions;
|
|
322
|
+
// Calculate button-level effort
|
|
323
|
+
let buttonEffort = boardEffort;
|
|
324
|
+
// Debug for specific button (disabled for production)
|
|
325
|
+
const debugSpecificButton = btn.label === '$938c2cc0dc';
|
|
326
|
+
if (debugSpecificButton) {
|
|
327
|
+
console.log(`\n🔍 DEBUG Button ${btn.label} at [${rowIndex},${colIndex}] on ${board.id}:`);
|
|
328
|
+
console.log(` Entry point: (${entryX.toFixed(4)}, ${entryY.toFixed(4)})`);
|
|
329
|
+
console.log(` Current level: ${level}`);
|
|
330
|
+
console.log(` Prior positions: ${priorItems}`);
|
|
331
|
+
console.log(` Starting effort: ${buttonEffort.toFixed(6)}`);
|
|
400
332
|
}
|
|
401
|
-
// Apply discounts
|
|
333
|
+
// Apply semantic_id discounts
|
|
402
334
|
if (btn.semantic_id && boardPcts[btn.semantic_id]) {
|
|
403
335
|
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.semantic_id];
|
|
404
|
-
|
|
336
|
+
const old = buttonEffort;
|
|
337
|
+
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
338
|
+
if (debugSpecificButton)
|
|
339
|
+
console.log(` Semantic board discount: ${old.toFixed(6)} -> ${buttonEffort.toFixed(6)} (pct=${boardPcts[btn.semantic_id].toFixed(4)})`);
|
|
405
340
|
}
|
|
406
|
-
else if (btn.
|
|
341
|
+
else if (btn.semantic_id && boardPcts[`upstream-${btn.semantic_id}`]) {
|
|
342
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT /
|
|
343
|
+
boardPcts[`upstream-${btn.semantic_id}`];
|
|
344
|
+
const old = buttonEffort;
|
|
345
|
+
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
346
|
+
if (debugSpecificButton)
|
|
347
|
+
console.log(` Semantic upstream discount: ${old.toFixed(6)} -> ${buttonEffort.toFixed(6)} (pct=${boardPcts[`upstream-${btn.semantic_id}`].toFixed(4)})`);
|
|
348
|
+
}
|
|
349
|
+
// Apply clone_id discounts
|
|
350
|
+
if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
407
351
|
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.clone_id];
|
|
408
|
-
|
|
352
|
+
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
409
353
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
354
|
+
else if (btn.clone_id && boardPcts[`upstream-${btn.clone_id}`]) {
|
|
355
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT /
|
|
356
|
+
boardPcts[`upstream-${btn.clone_id}`];
|
|
357
|
+
buttonEffort = Math.min(buttonEffort, buttonEffort * discount);
|
|
358
|
+
}
|
|
359
|
+
// Calculate button effort based on access method (Touch vs Scanning)
|
|
360
|
+
const isScanning = !!scanningConfig || !!board.scanType;
|
|
361
|
+
if (isScanning) {
|
|
362
|
+
const { steps, selections, loopSteps } = this.calculateScanSteps(board, btn, rowIndex, colIndex, scanningConfig);
|
|
363
|
+
// Determine effective costs based on selection method
|
|
364
|
+
let currentStepCost = options.scanStepCost ?? effort_1.EFFORT_CONSTANTS.SCAN_STEP_COST;
|
|
365
|
+
const currentSelectionCost = options.scanSelectionCost ?? effort_1.EFFORT_CONSTANTS.SCAN_SELECTION_COST;
|
|
366
|
+
// Step Scan 2 Switch: Every step is a physical selection with Switch 1
|
|
367
|
+
if (scanningConfig?.selectionMethod === aac_1.ScanningSelectionMethod.StepScan2Switch) {
|
|
368
|
+
// The cost of moving is now a selection cost
|
|
369
|
+
currentStepCost = currentSelectionCost;
|
|
425
370
|
}
|
|
426
|
-
else if (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
distance = Math.min(distance, distance * discount);
|
|
371
|
+
else if (scanningConfig?.selectionMethod === aac_1.ScanningSelectionMethod.StepScan1Switch) {
|
|
372
|
+
// Single switch step scan: every step is a physical selection
|
|
373
|
+
currentStepCost = currentSelectionCost;
|
|
430
374
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (
|
|
434
|
-
const
|
|
435
|
-
|
|
375
|
+
let sEffort = (0, effort_1.scanningEffort)(steps, selections, currentStepCost, currentSelectionCost);
|
|
376
|
+
// Factor in error correction if enabled
|
|
377
|
+
if (scanningConfig?.errorCorrectionEnabled) {
|
|
378
|
+
const errorRate = scanningConfig.errorRate ?? effort_1.EFFORT_CONSTANTS.DEFAULT_SCAN_ERROR_RATE;
|
|
379
|
+
// A "miss" results in needing to wait for a loop (or part of one)
|
|
380
|
+
// We model this as errorRate * (loopSteps * stepCost)
|
|
381
|
+
const retryPenalty = loopSteps * currentStepCost;
|
|
382
|
+
sEffort += errorRate * retryPenalty;
|
|
436
383
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
384
|
+
// Apply discounts to scanning effort (similar to touch)
|
|
385
|
+
if (btn.semantic_id && boardPcts[btn.semantic_id]) {
|
|
386
|
+
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.semantic_id];
|
|
387
|
+
sEffort = Math.min(sEffort, sEffort * discount);
|
|
441
388
|
}
|
|
442
|
-
else if (
|
|
443
|
-
const discount = effort_1.EFFORT_CONSTANTS.
|
|
444
|
-
|
|
389
|
+
else if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
390
|
+
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.clone_id];
|
|
391
|
+
sEffort = Math.min(sEffort, sEffort * discount);
|
|
445
392
|
}
|
|
446
|
-
|
|
447
|
-
buttonEffort += distance;
|
|
448
|
-
// Add visual scan or local scan effort
|
|
449
|
-
if (distance > effort_1.EFFORT_CONSTANTS.DISTANCE_THRESHOLD_TO_SKIP_VISUAL_SCAN ||
|
|
450
|
-
(entryX === 1.0 && entryY === 1.0)) {
|
|
451
|
-
buttonEffort += (0, effort_1.visualScanEffort)(priorItems);
|
|
393
|
+
buttonEffort += sEffort;
|
|
452
394
|
}
|
|
453
395
|
else {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
396
|
+
// Add distance effort (Touch only)
|
|
397
|
+
let distance = (0, effort_1.distanceEffort)(x, y, entryX, entryY);
|
|
398
|
+
// Apply distance discounts
|
|
399
|
+
if (btn.semantic_id) {
|
|
400
|
+
if (boardPcts[btn.semantic_id]) {
|
|
401
|
+
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.semantic_id];
|
|
402
|
+
distance = Math.min(distance, distance * discount);
|
|
403
|
+
}
|
|
404
|
+
else if (boardPcts[`upstream-${btn.semantic_id}`]) {
|
|
405
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_SEMANTIC_FROM_PRIOR_DISCOUNT /
|
|
406
|
+
boardPcts[`upstream-${btn.semantic_id}`];
|
|
407
|
+
distance = Math.min(distance, distance * discount);
|
|
408
|
+
}
|
|
409
|
+
else if (level > 0 && setPcts[btn.semantic_id]) {
|
|
410
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_SEMANTIC_FROM_OTHER_DISCOUNT /
|
|
411
|
+
setPcts[btn.semantic_id];
|
|
412
|
+
distance = Math.min(distance, distance * discount);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (btn.clone_id) {
|
|
416
|
+
if (boardPcts[btn.clone_id]) {
|
|
417
|
+
const discount = effort_1.EFFORT_CONSTANTS.SAME_LOCATION_AS_PRIOR_DISCOUNT / boardPcts[btn.clone_id];
|
|
418
|
+
distance = Math.min(distance, distance * discount);
|
|
419
|
+
}
|
|
420
|
+
else if (boardPcts[`upstream-${btn.clone_id}`]) {
|
|
421
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_CLONE_FROM_PRIOR_DISCOUNT /
|
|
422
|
+
boardPcts[`upstream-${btn.clone_id}`];
|
|
423
|
+
distance = Math.min(distance, distance * discount);
|
|
424
|
+
}
|
|
425
|
+
else if (level > 0 && setPcts[btn.clone_id]) {
|
|
426
|
+
const discount = effort_1.EFFORT_CONSTANTS.RECOGNIZABLE_CLONE_FROM_OTHER_DISCOUNT / setPcts[btn.clone_id];
|
|
427
|
+
distance = Math.min(distance, distance * discount);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
buttonEffort += distance;
|
|
431
|
+
// Add visual scan or local scan effort
|
|
432
|
+
if (distance > effort_1.EFFORT_CONSTANTS.DISTANCE_THRESHOLD_TO_SKIP_VISUAL_SCAN ||
|
|
433
|
+
(entryX === 1.0 && entryY === 1.0)) {
|
|
434
|
+
buttonEffort += (0, effort_1.visualScanEffort)(priorItems);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
buttonEffort += (0, effort_1.localScanEffort)(distance);
|
|
490
438
|
}
|
|
491
439
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const addToSentence = btn.semanticAction?.platformData?.grid3?.parameters?.add_to_sentence ||
|
|
501
|
-
btn.semanticAction?.fallback?.add_to_sentence;
|
|
502
|
-
if (isSpeak || addToSentence) {
|
|
503
|
-
let finalEffort = buttonEffort;
|
|
504
|
-
// Apply Board Change Processing Effort Discount (matching Ruby lines 347-350)
|
|
505
|
-
const changeEffort = effort_1.EFFORT_CONSTANTS.BOARD_CHANGE_PROCESSING_EFFORT;
|
|
506
|
-
if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
507
|
-
const discount = Math.min(changeEffort, (changeEffort * 0.3) / boardPcts[btn.clone_id]);
|
|
508
|
-
finalEffort -= discount;
|
|
440
|
+
// Add cumulative prior effort
|
|
441
|
+
buttonEffort += priorEffort;
|
|
442
|
+
// Track scan blocks for block scanning, otherwise track individual buttons
|
|
443
|
+
if (blockScanEnabled) {
|
|
444
|
+
const scanBlockId = btn.scanBlock ?? btn.scanBlocks?.[0];
|
|
445
|
+
if (scanBlockId !== undefined && scanBlockId !== null) {
|
|
446
|
+
priorScanBlocks.add(scanBlockId);
|
|
447
|
+
}
|
|
509
448
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
449
|
+
// Handle navigation buttons
|
|
450
|
+
if (btn.targetPageId) {
|
|
451
|
+
const nextBoard = tree.getPage(btn.targetPageId);
|
|
452
|
+
if (nextBoard) {
|
|
453
|
+
// Only add to toVisit if this board hasn't been visited yet at any level
|
|
454
|
+
// The visitedBoardIds map stores the *lowest* level a board was visited.
|
|
455
|
+
// If it's already in the map, it means we've processed it or scheduled it at a lower level.
|
|
456
|
+
if (visitedBoardIds.get(nextBoard.id) === undefined) {
|
|
457
|
+
const changeEffort = effort_1.EFFORT_CONSTANTS.BOARD_CHANGE_PROCESSING_EFFORT;
|
|
458
|
+
const tempHomeId = btn.semanticAction?.platformData?.grid3?.parameters?.temporary_home === 'prior'
|
|
459
|
+
? board.id
|
|
460
|
+
: btn.semanticAction?.platformData?.grid3?.parameters?.temporary_home === true
|
|
461
|
+
? btn.targetPageId
|
|
462
|
+
: temporaryHomeId;
|
|
463
|
+
toVisit.push({
|
|
464
|
+
board: nextBoard,
|
|
465
|
+
level: level + 1,
|
|
466
|
+
priorEffort: buttonEffort + changeEffort,
|
|
467
|
+
temporaryHomeId: tempHomeId,
|
|
468
|
+
entryX: x,
|
|
469
|
+
entryY: y,
|
|
470
|
+
entryCloneId: btn.clone_id,
|
|
471
|
+
entrySemanticId: btn.semantic_id,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
513
475
|
}
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
clone_id
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
476
|
+
// Track word if it speaks or adds to sentence
|
|
477
|
+
const isSpeak = btn.semanticAction?.category === treeStructure_1.AACSemanticCategory.COMMUNICATION && !btn.targetPageId; // Must not be a navigation button
|
|
478
|
+
const addToSentence = btn.semanticAction?.platformData?.grid3?.parameters?.add_to_sentence ||
|
|
479
|
+
btn.semanticAction?.fallback?.add_to_sentence;
|
|
480
|
+
if (isSpeak || addToSentence) {
|
|
481
|
+
let finalEffort = buttonEffort;
|
|
482
|
+
// Apply Board Change Processing Effort Discount (matching Ruby lines 347-350)
|
|
483
|
+
const changeEffort = effort_1.EFFORT_CONSTANTS.BOARD_CHANGE_PROCESSING_EFFORT;
|
|
484
|
+
if (btn.clone_id && boardPcts[btn.clone_id]) {
|
|
485
|
+
const discount = Math.min(changeEffort, (changeEffort * 0.3) / boardPcts[btn.clone_id]);
|
|
486
|
+
finalEffort -= discount;
|
|
487
|
+
}
|
|
488
|
+
else if (btn.semantic_id && boardPcts[btn.semantic_id]) {
|
|
489
|
+
const discount = Math.min(changeEffort, (changeEffort * 0.5) / boardPcts[btn.semantic_id]);
|
|
490
|
+
finalEffort -= discount;
|
|
491
|
+
}
|
|
492
|
+
const existing = knownButtons.get(btn.label);
|
|
493
|
+
const knownBtn = {
|
|
494
|
+
id: btn.id,
|
|
495
|
+
label: btn.label,
|
|
496
|
+
level,
|
|
497
|
+
effort: finalEffort,
|
|
498
|
+
count: (existing?.count || 0) + 1,
|
|
499
|
+
semantic_id: btn.semantic_id,
|
|
500
|
+
clone_id: btn.clone_id,
|
|
501
|
+
temporary_home_id: temporaryHomeId || undefined,
|
|
502
|
+
};
|
|
503
|
+
if (!existing || finalEffort < existing.effort) {
|
|
504
|
+
knownButtons.set(btn.label, knownBtn);
|
|
505
|
+
}
|
|
527
506
|
}
|
|
528
507
|
}
|
|
529
|
-
}
|
|
508
|
+
}
|
|
530
509
|
}
|
|
531
510
|
// Convert to array and group by level
|
|
532
511
|
const buttons = Array.from(knownButtons.values());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@willwade/aac-processors",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
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",
|