Skip to content

Commit da74cf5

Browse files
committed
Add standardized visibility mapping for grid processors
Introduces consistent mapping of visibility/hidden fields to the AAC standard ('Visible', 'Hidden', etc.) across Asterics, OBF, Grid 3, and TouchChat processors. Updates symbol extraction in Grid3SymbolExtractor to use the shared extractSymbolReferences utility. Ensures that cell/button visibility is correctly parsed and exported for all supported grid formats.
1 parent 35996aa commit da74cf5

5 files changed

Lines changed: 95 additions & 17 deletions

File tree

src/optional/symbolTools.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getZipEntriesWithPassword,
55
resolveGridsetPasswordFromEnv,
66
} from '../processors/gridset/password';
7+
import { extractSymbolReferences } from '../processors/gridset/symbols';
78

89
// Dynamic imports for optional dependencies
910
type Database = typeof import('better-sqlite3');
@@ -79,21 +80,14 @@ try {
7980
export class Grid3SymbolExtractor extends SymbolExtractor {
8081
getSymbolReferences(filePath: string): string[] {
8182
if (!AdmZip || !XMLParser) throw new Error('adm-zip or fast-xml-parser not installed');
82-
const zip = new AdmZip(filePath);
83-
const parser = new XMLParser();
84-
const refs = new Set<string>();
85-
86-
const entries = getZipEntriesWithPassword(zip, resolveGridsetPasswordFromEnv());
87-
entries.forEach((entry) => {
88-
if (entry.entryName.endsWith('.gridset') || entry.entryName.endsWith('.gridsetx')) {
89-
const xmlBuffer = entry.getData();
90-
// Parse to validate XML structure (future: extract refs)
91-
parser.parse(xmlBuffer.toString('utf8'));
92-
// TODO: Extract symbol references from Grid 3 XML structure when needed
93-
}
94-
});
95-
96-
return Array.from(refs);
83+
84+
// Import GridsetProcessor dynamically to avoid circular dependencies
85+
const { GridsetProcessor } = require('../processors/gridsetProcessor');
86+
const proc = new GridsetProcessor();
87+
const tree = proc.loadIntoTree(filePath);
88+
89+
// Use the existing extractSymbolReferences function from gridset/symbols.ts
90+
return extractSymbolReferences(tree);
9791
}
9892
}
9993

src/processors/astericsGridProcessor.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ interface GridElement {
4949
dontCollect?: boolean;
5050
type?: string;
5151
additionalProps?: any;
52+
hidden?: boolean; // Asterics Grid uses boolean hidden field
5253
}
5354

5455
interface GridImage {
@@ -701,6 +702,18 @@ function getContrastingTextColor(backgroundColor: string): string {
701702
return luminance < 0.5 ? '#FFFFFF' : '#000000';
702703
}
703704

705+
/**
706+
* Map Asterics Grid hidden value to AAC standard visibility
707+
* Asterics Grid: true = hidden, false = visible
708+
* Maps to: 'Hidden' | 'Visible' | undefined
709+
*/
710+
function mapAstericsVisibility(hidden: boolean | undefined): 'Hidden' | 'Visible' | undefined {
711+
if (hidden === undefined) {
712+
return undefined; // Default to visible
713+
}
714+
return hidden ? 'Hidden' : 'Visible';
715+
}
716+
704717
class AstericsGridProcessor extends BaseProcessor {
705718
private loadAudio: boolean = false;
706719

@@ -1212,6 +1225,7 @@ class AstericsGridProcessor extends BaseProcessor {
12121225

12131226
semanticAction: semanticAction,
12141227
audioRecording: audioRecording,
1228+
visibility: mapAstericsVisibility(element.hidden),
12151229
image: imageName, // Store image filename/reference
12161230
parameters: imageData
12171231
? {

src/processors/gridsetProcessor.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,18 +525,59 @@ class GridsetProcessor extends BaseProcessor {
525525
// Extract scan block number (1-8) for block scanning support
526526
const scanBlock = parseInt(String(cell['@_ScanBlock'] || '1'), 10);
527527

528+
// Extract visibility from Grid 3's <Visibility> child element
529+
// Grid 3 stores visibility as a child element, not an attribute
530+
// Valid values: Visible, Hidden, Disabled, PointerAndTouchOnly, TouchOnly, PointerOnly
531+
const grid3Visibility = cell.Visibility || cell.visibility;
532+
533+
// Map Grid 3 visibility values to AAC standard values
534+
// Grid 3 can have additional values like TouchOnly, PointerOnly that map to PointerAndTouchOnly
535+
let cellVisibility: 'Visible' | 'Hidden' | 'Disabled' | 'PointerAndTouchOnly' | 'Empty' | undefined;
536+
if (grid3Visibility) {
537+
const vis = String(grid3Visibility);
538+
// Direct mapping for standard values
539+
if (vis === 'Visible' || vis === 'Hidden' || vis === 'Disabled' || vis === 'PointerAndTouchOnly') {
540+
cellVisibility = vis;
541+
}
542+
// Map Grid 3 specific values to AAC standard
543+
else if (vis === 'TouchOnly' || vis === 'PointerOnly') {
544+
cellVisibility = 'PointerAndTouchOnly';
545+
}
546+
// Grid 3 may use 'Empty' for cells that exist but have no content
547+
else if (vis === 'Empty') {
548+
cellVisibility = 'Empty';
549+
}
550+
// Unknown visibility - default to Visible
551+
else {
552+
cellVisibility = undefined; // Let it default
553+
}
554+
}
555+
528556
// Extract label from CaptionAndImage/Caption
529557
const content = cell.Content;
530558
const captionAndImage = content.CaptionAndImage || content.captionAndImage;
531559
let label = captionAndImage?.Caption || captionAndImage?.caption || '';
532560

561+
// Check if cell has an image/symbol (needed to decide if we should keep it)
562+
const hasImageCandidate = !!(
563+
captionAndImage?.Image ||
564+
captionAndImage?.image ||
565+
captionAndImage?.ImageName ||
566+
captionAndImage?.imageName ||
567+
captionAndImage?.Symbol ||
568+
captionAndImage?.symbol
569+
);
570+
533571
// If no caption, try other sources or create a placeholder
534572
if (!label) {
535-
// For cells without captions (like AutoContent cells), create a meaningful label
573+
// For cells without captions, check if they have images/symbols before skipping
536574
if (content.ContentType === 'AutoContent') {
537575
label = `AutoContent_${idx}`;
576+
} else if (hasImageCandidate || content.ContentType === 'Workspace' || content.ContentType === 'LiveCell') {
577+
// Keep cells with images/symbols even if no caption
578+
label = `Cell_${idx}`;
538579
} else {
539-
return; // Skip cells without labels
580+
return; // Skip cells without labels AND without images/symbols
540581
}
541582
}
542583

@@ -1013,6 +1054,7 @@ class GridsetProcessor extends BaseProcessor {
10131054
pluginMetadata.autoContentType,
10141055
symbolLibrary: symbolLibraryRef?.library || undefined,
10151056
symbolPath: symbolLibraryRef?.path || undefined,
1057+
visibility: cellVisibility,
10161058
style: {
10171059
...cellStyle,
10181060
...inlineStyle, // Inline styles override referenced styles

src/processors/obfProcessor.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ interface ObfButton {
3232
background_color?: string;
3333
border_color?: string;
3434
semantic_id?: string; // Optional semantic identifier for motor planning
35+
hidden?: boolean; // OBF uses boolean hidden field
36+
}
37+
38+
/**
39+
* Map OBF hidden value to AAC standard visibility
40+
* OBF: true = hidden, false/undefined = visible
41+
* Maps to: 'Hidden' | 'Visible' | undefined
42+
*/
43+
function mapObfVisibility(hidden: boolean | undefined): 'Hidden' | 'Visible' | undefined {
44+
if (hidden === undefined) {
45+
return undefined; // Default to visible
46+
}
47+
return hidden ? 'Hidden' : 'Visible';
3548
}
3649

3750
interface ObfGrid {
@@ -83,6 +96,7 @@ class ObfProcessor extends BaseProcessor {
8396
id: String(btn?.id || ''),
8497
label: String(btn?.label || ''),
8598
message: String(btn?.vocalization || btn?.label || ''),
99+
visibility: mapObfVisibility(btn.hidden),
86100
style: {
87101
backgroundColor: btn.background_color,
88102
borderColor: btn.border_color,

src/processors/touchchatProcessor.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ function intToHex(colorInt: number | null | undefined): string | undefined {
8484
return `#${(colorInt & 0x00ffffff).toString(16).padStart(6, '0')}`;
8585
}
8686

87+
/**
88+
* Map TouchChat visible value to AAC standard visibility
89+
* TouchChat: 0 = Hidden, 1 = Visible
90+
* Maps to: 'Hidden' | 'Visible' | undefined
91+
*/
92+
function mapTouchChatVisibility(visible: number | null | undefined): 'Visible' | 'Hidden' | undefined {
93+
if (visible === null || visible === undefined) {
94+
return undefined; // Default to visible
95+
}
96+
return visible === 0 ? 'Hidden' : 'Visible';
97+
}
98+
8799
class TouchChatProcessor extends BaseProcessor {
88100
private tree: AACTree | null = null;
89101
private sourceFile: string | Buffer | null = null;
@@ -257,6 +269,7 @@ class TouchChatProcessor extends BaseProcessor {
257269
message: cell.message || '',
258270
semanticAction: semanticAction,
259271
semantic_id: (cell as any).symbol_link_id || (cell as any).symbolLinkId || undefined, // Extract semantic_id from symbol_link_id
272+
visibility: mapTouchChatVisibility((cell as any).visible),
260273
// Note: TouchChat does not use scan blocks in the file
261274
// Scanning is a runtime feature (linear/row-column patterns)
262275
// scanBlock defaults to 1 (no grouping)
@@ -422,6 +435,7 @@ class TouchChatProcessor extends BaseProcessor {
422435
label: btnRow.label || '',
423436
message: btnRow.message || '',
424437
semanticAction: semanticAction,
438+
visibility: mapTouchChatVisibility(btnRow.visible),
425439
// Note: TouchChat does not use scan blocks in the file
426440
// Scanning is a runtime feature (linear/row-column patterns)
427441
// scanBlock defaults to 1 (no grouping)

0 commit comments

Comments
 (0)