Skip to content

Commit d3463d7

Browse files
committed
better grid calcs
1 parent 3715b66 commit d3463d7

4 files changed

Lines changed: 317 additions & 6 deletions

File tree

src/processors/gridset/resolver.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ export function resolveGrid3CellImage(
5757
const entries = new Set(listZipEntries(zip, zipEntries));
5858
const has = (p: string): boolean => entries.has(normalizeZipPathLocal(p));
5959

60+
// Debug logging for cells that fail to resolve
61+
const shouldDebug = imageName?.startsWith('-') && x !== undefined && y !== undefined;
62+
const debugLog = (msg: string) => {
63+
if (shouldDebug) {
64+
console.log(`[Resolver] ${baseDir} (${x},${y}) "${imageName}": ${msg}`);
65+
}
66+
};
67+
6068
// Built-in resource like [grid3x]... (old format, not symbol library)
6169
// Check this BEFORE general symbol references to avoid misclassification
6270
if (imageName && imageName.startsWith('[')) {
@@ -87,6 +95,7 @@ export function resolveGrid3CellImage(
8795
// to be prefixed with the cell coordinates
8896
if (imageName.startsWith('-') && x != null && y != null) {
8997
const coordPrefixed = joinBaseDir(baseDir, `${x}-${y}${imageName}`);
98+
debugLog(`trying coord-prefixed: ${coordPrefixed}, found: ${has(coordPrefixed)}`);
9099
if (has(coordPrefixed)) return coordPrefixed;
91100
}
92101

@@ -125,12 +134,14 @@ export function resolveGrid3CellImage(
125134
`${x}-${y}.jpg`,
126135
`${x}-${y}.png`,
127136
].map((n) => joinBaseDir(baseDir, n));
137+
debugLog(`trying candidates: ${candidates.filter(has).join(', ') || 'none found'}`);
128138
for (const c of candidates) {
129139
if (has(c)) return c;
130140
}
131141
}
132142
}
133143

144+
debugLog(`NOT FOUND - returning null`);
134145
return null;
135146
}
136147

src/processors/gridsetProcessor.ts

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -741,16 +741,145 @@ class GridsetProcessor extends BaseProcessor {
741741
}> = [];
742742
let wordListCellIndex = 0;
743743

744+
// Helper function to find next available position in grid (auto-flow)
745+
// Returns {x, y} for next available slot that can accommodate the given span
746+
const findNextAvailablePosition = (
747+
width: number,
748+
height: number,
749+
gridLayout: (AACButton | null)[][]
750+
): { x: number; y: number } => {
751+
for (let y = 0; y < maxRows; y++) {
752+
for (let x = 0; x <= maxCols - width; x++) {
753+
// Check if this position and the required span area are all free
754+
let fits = true;
755+
for (let dy = 0; dy < height && y + dy < maxRows; dy++) {
756+
for (let dx = 0; dx < width && x + dx < maxCols; dx++) {
757+
if (gridLayout[y + dy][x + dx] !== null) {
758+
fits = false;
759+
break;
760+
}
761+
}
762+
if (!fits) break;
763+
}
764+
if (fits) {
765+
return { x, y };
766+
}
767+
}
768+
}
769+
// If no position found, return 0,0 (will be placed at first available)
770+
return { x: 0, y: 0 };
771+
};
772+
773+
// Helper function to find next available X position in a specific row
774+
const findNextAvailableXInRow = (
775+
rowY: number,
776+
width: number,
777+
gridLayout: (AACButton | null)[][]
778+
): number => {
779+
for (let x = 0; x <= maxCols - width; x++) {
780+
let fits = true;
781+
for (let dx = 0; dx < width; dx++) {
782+
if (gridLayout[rowY][x + dx] !== null) {
783+
fits = false;
784+
break;
785+
}
786+
}
787+
if (fits) return x;
788+
}
789+
return 0;
790+
};
791+
792+
// First pass: categorize cells by their positioning
793+
interface CellWithIndex {
794+
cell: any;
795+
idx: number;
796+
}
797+
const cellsWithExplicitPosition: CellWithIndex[] = [];
798+
const cellsWithYOnly: CellWithIndex[] = [];
799+
const cellsWithXOnly: CellWithIndex[] = [];
800+
const cellsWithAutoFlow: CellWithIndex[] = [];
801+
744802
cellArr.forEach((cell: any, idx: number) => {
745803
if (!cell || !cell.Content) return;
746804

747-
// Extract position information from cell attributes
748-
// Grid3 uses 1-based coordinates, convert to 0-based for internal use
749-
const cellX = Math.max(0, parseInt(String(cell['@_X'] || '1'), 10) - 1);
750-
const cellY = Math.max(0, parseInt(String(cell['@_Y'] || '1'), 10) - 1);
805+
const hasX = cell['@_X'] !== undefined;
806+
const hasY = cell['@_Y'] !== undefined;
807+
808+
if (hasX && hasY) {
809+
cellsWithExplicitPosition.push({ cell, idx });
810+
} else if (hasY && !hasX) {
811+
cellsWithYOnly.push({ cell, idx });
812+
} else if (!hasY && hasX) {
813+
cellsWithXOnly.push({ cell, idx });
814+
} else {
815+
cellsWithAutoFlow.push({ cell, idx });
816+
}
817+
});
818+
819+
// Process cells in order: explicit -> Y-only -> X-only -> auto-flow
820+
const allCellsToProcess = [
821+
...cellsWithExplicitPosition,
822+
...cellsWithYOnly,
823+
...cellsWithXOnly,
824+
...cellsWithAutoFlow,
825+
];
826+
827+
allCellsToProcess.forEach(({ cell, idx }) => {
828+
// Extract span information first
751829
const colSpan = parseInt(String(cell['@_ColumnSpan'] || '1'), 10);
752830
const rowSpan = parseInt(String(cell['@_RowSpan'] || '1'), 10);
753831

832+
// Determine position based on what attributes are present
833+
const hasX = cell['@_X'] !== undefined;
834+
const hasY = cell['@_Y'] !== undefined;
835+
836+
let cellX: number;
837+
let cellY: number;
838+
839+
if (hasX && hasY) {
840+
// Explicit position: both X and Y provided
841+
// Grid 3 XML coordinates are already 0-based, use them directly
842+
cellX = Math.max(0, parseInt(String(cell['@_X']), 10));
843+
cellY = Math.max(0, parseInt(String(cell['@_Y']), 10));
844+
} else if (hasY && !hasX) {
845+
// Y-only: auto-flow X in the specified row
846+
// Grid 3 XML coordinates are already 0-based, use them directly
847+
cellY = Math.max(0, parseInt(String(cell['@_Y']), 10));
848+
cellX = findNextAvailableXInRow(cellY, colSpan, gridLayout);
849+
} else if (!hasY && hasX) {
850+
// X-only: place at specified X in next available row
851+
// Grid 3 XML coordinates are already 0-based, use them directly
852+
cellX = Math.max(0, parseInt(String(cell['@_X']), 10));
853+
// Find first row where this X position is available
854+
cellY = 0;
855+
let found = false;
856+
for (let y = 0; y < maxRows; y++) {
857+
let fits = true;
858+
for (let dx = 0; dx < colSpan && cellX + dx < maxCols; dx++) {
859+
if (gridLayout[y][cellX + dx] !== null) {
860+
fits = false;
861+
break;
862+
}
863+
}
864+
if (fits) {
865+
cellY = y;
866+
found = true;
867+
break;
868+
}
869+
}
870+
if (!found) {
871+
// No available row found, use auto-flow
872+
const pos = findNextAvailablePosition(colSpan, rowSpan, gridLayout);
873+
cellX = pos.x;
874+
cellY = pos.y;
875+
}
876+
} else {
877+
// No position: auto-flow both X and Y
878+
const pos = findNextAvailablePosition(colSpan, rowSpan, gridLayout);
879+
cellX = pos.x;
880+
cellY = pos.y;
881+
}
882+
754883
// Extract scan block number (1-8) for block scanning support
755884
const scanBlock = parseInt(String(cell['@_ScanBlock'] || '1'), 10);
756885

@@ -927,6 +1056,13 @@ class GridsetProcessor extends BaseProcessor {
9271056
entries
9281057
) || undefined;
9291058

1059+
// Debug: log resolution for cells with images
1060+
if (declaredImageName && resolvedImageEntry) {
1061+
console.log(`[GridsetProcessor] Cell (${cellX + 1},${cellY + 1}) [XML coords]: ${declaredImageName} -> ${resolvedImageEntry}`);
1062+
} else if (declaredImageName && !resolvedImageEntry) {
1063+
console.log(`[GridsetProcessor] Cell (${cellX + 1},${cellY + 1}) [XML coords]: ${declaredImageName} -> NOT FOUND`);
1064+
}
1065+
9301066
// Check if image is a symbol library reference
9311067
let symbolLibraryRef: SymbolReference | null = null;
9321068
if (declaredImageName && isSymbolLibraryReference(declaredImageName)) {

test/audit-images.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Audit image resolution in gridset files
3+
*
4+
* This test audits a gridset file to ensure all images in the ZIP
5+
* are being resolved correctly by the processor.
6+
*/
7+
8+
import { GridsetProcessor } from '../src/processors/gridsetProcessor';
9+
import { resolveGrid3CellImage } from '../src/processors/gridset/resolver';
10+
import path from 'node:path';
11+
12+
interface AuditResult {
13+
totalCells: number;
14+
cellsWithDeclaredImages: number;
15+
cellsWithResolvedImages: number;
16+
cellsWithoutResolvedImages: number;
17+
actualImageFilesInZip: number;
18+
resolvedImagePaths: string[];
19+
unresolvedCells: Array<{
20+
label: string;
21+
x: number;
22+
y: number;
23+
imageName: string;
24+
page: string;
25+
}>;
26+
imageFilesInZip: string[];
27+
resolvedImagesNotInZip: string[];
28+
}
29+
30+
function countImageFilesInZip(entries: string[]): string[] {
31+
// Count all image files in the ZIP (excluding XML files)
32+
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.webp'];
33+
return entries.filter((entry) => {
34+
const ext = entry.toLowerCase().split('.').pop();
35+
return ext && imageExtensions.includes(`.${ext}`);
36+
});
37+
}
38+
39+
async function auditGridsetImages(gridsetPath: string): Promise<AuditResult> {
40+
const processor = new GridsetProcessor();
41+
42+
// Load the gridset
43+
const tree = await processor.loadIntoTree(gridsetPath);
44+
45+
// Get all entries from the ZIP for manual inspection
46+
// We need to access the internal ZIP entries
47+
const fs = await import('node:fs');
48+
const AdmZip = (await import('adm-zip')).default;
49+
const zip = new AdmZip(gridsetPath);
50+
const allEntries = zip.getEntries().map((e: any) => e.entryName);
51+
52+
const imageFilesInZip = countImageFilesInZip(allEntries);
53+
54+
const resolvedImagePaths = new Set<string>();
55+
const unresolvedCells: AuditResult['unresolvedCells'] = [];
56+
57+
let totalCells = 0;
58+
let cellsWithDeclaredImages = 0;
59+
let cellsWithResolvedImages = 0;
60+
61+
// Audit each page
62+
for (const [pageId, page] of Object.entries(tree.pages)) {
63+
for (const button of page.buttons) {
64+
totalCells++;
65+
66+
if (button.image || button.resolvedImageEntry) {
67+
cellsWithDeclaredImages++;
68+
}
69+
70+
if (button.resolvedImageEntry) {
71+
cellsWithResolvedImages++;
72+
resolvedImagePaths.add(button.resolvedImageEntry);
73+
} else if (button.image) {
74+
// Has image name but couldn't resolve
75+
const cellX = (button.parameters as any)?.cellX;
76+
const cellY = (button.parameters as any)?.cellY;
77+
unresolvedCells.push({
78+
label: button.label,
79+
x: cellX,
80+
y: cellY,
81+
imageName: button.image,
82+
page: pageId,
83+
});
84+
}
85+
}
86+
}
87+
88+
// Check for resolved images that aren't actually in the ZIP
89+
const resolvedImagesNotInZip = Array.from(resolvedImagePaths).filter(
90+
(img) => !allEntries.includes(img) && !allEntries.includes(img.replace(/^Grids\//, ''))
91+
);
92+
93+
return {
94+
totalCells,
95+
cellsWithDeclaredImages,
96+
cellsWithResolvedImages,
97+
cellsWithoutResolvedImages: cellsWithDeclaredImages - cellsWithResolvedImages,
98+
actualImageFilesInZip: imageFilesInZip.length,
99+
resolvedImagePaths: Array.from(resolvedImagePaths),
100+
unresolvedCells,
101+
imageFilesInZip,
102+
resolvedImagesNotInZip,
103+
};
104+
}
105+
106+
describe('Gridset Image Audit', () => {
107+
const exampleGridset = path.join(process.cwd(), 'examples/example-images.gridset');
108+
109+
test('should resolve all images that exist in the ZIP', async () => {
110+
const audit = await auditGridsetImages(exampleGridset);
111+
112+
console.log('\n=== Gridset Image Audit ===');
113+
console.log(`Total cells: ${audit.totalCells}`);
114+
console.log(`Cells with declared images: ${audit.cellsWithDeclaredImages}`);
115+
console.log(`Cells with resolved images: ${audit.cellsWithResolvedImages}`);
116+
console.log(`Cells without resolved images: ${audit.cellsWithoutResolvedImages}`);
117+
console.log(`Actual image files in ZIP: ${audit.actualImageFilesInZip}`);
118+
console.log(`Unique resolved image paths: ${audit.resolvedImagePaths.length}`);
119+
console.log(`Resolved images not found in ZIP: ${audit.resolvedImagesNotInZip.length}`);
120+
121+
if (audit.unresolvedCells.length > 0) {
122+
console.log(`\nUnresolved cells (${audit.unresolvedCells.length}):`);
123+
audit.unresolvedCells.forEach((cell) => {
124+
console.log(` - "${cell.label}" at (${cell.x}, ${cell.y}): ${cell.imageName}`);
125+
});
126+
}
127+
128+
if (audit.resolvedImagesNotInZip.length > 0) {
129+
console.log(`\nResolved images not in ZIP (${audit.resolvedImagesNotInZip.length}):`);
130+
audit.resolvedImagesNotInZip.forEach((img) => {
131+
console.log(` - ${img}`);
132+
});
133+
}
134+
135+
console.log('\n=== Sample resolved images ===');
136+
audit.resolvedImagePaths.slice(0, 10).forEach((img) => {
137+
console.log(` - ${img}`);
138+
});
139+
if (audit.resolvedImagePaths.length > 10) {
140+
console.log(` ... and ${audit.resolvedImagePaths.length - 10} more`);
141+
}
142+
143+
console.log('\n=== Sample image files in ZIP ===');
144+
audit.imageFilesInZip.slice(0, 10).forEach((img) => {
145+
console.log(` - ${img}`);
146+
});
147+
if (audit.imageFilesInZip.length > 10) {
148+
console.log(` ... and ${audit.imageFilesInZip.length - 10} more`);
149+
}
150+
151+
// The resolved images should be a subset of images in the ZIP
152+
expect(audit.resolvedImagesNotInZip.length).toBe(0);
153+
154+
// Log the summary
155+
console.log('\n=== Summary ===');
156+
console.log(`✓ All ${audit.cellsWithResolvedImages} resolved images exist in the ZIP`);
157+
console.log(
158+
`✓ ${audit.cellsWithDeclaredImages - audit.cellsWithResolvedImages} cells could not be resolved`
159+
);
160+
console.log(
161+
`✓ ${audit.actualImageFilesInZip - audit.resolvedImagePaths.length} images in ZIP are not referenced by cells`
162+
);
163+
}, 30000);
164+
});

test/gridsetProcessor.coverage.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ describe('GridsetProcessor Coverage Tests', () => {
212212
const button = page.buttons[0];
213213
expect(button).toBeDefined();
214214
expect(button.label).toBe('Test Button');
215-
expect(button.x).toBe(0); // 1-based to 0-based
216-
expect(button.y).toBe(0);
215+
expect(button.x).toBe(1); // Grid 3 XML coordinates are already 0-based
216+
expect(button.y).toBe(1);
217217
expect(button.columnSpan).toBe(2);
218218
expect(button.rowSpan).toBe(2);
219219
expect(button.scanBlock).toBe(3);

0 commit comments

Comments
 (0)