Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion js/arena-configs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Arena Configurations
* Auto-generated from maDisplayTools/configs/arenas/
* Last updated: 2026-01-30T15:23:37.594Z
* Last updated: 2026-02-05T04:51:33.254Z
*
* DO NOT EDIT MANUALLY - regenerate with: node scripts/generate-arena-configs.js
*/
Expand Down Expand Up @@ -68,6 +68,19 @@ const STANDARD_CONFIGS = {
"angle_offset_deg": -60
}
},
"G6_3x16_full": {
"label": "G6 (3×16) - 360°",
"description": "G6 arena, 3 rows × 16 columns",
"arena": {
"generation": "G6",
"num_rows": 3,
"num_cols": 16,
"columns_installed": null,
"orientation": "normal",
"column_order": "cw",
"angle_offset_deg": 0
}
},
"G41_2x12_ccw": {
"label": "G4.1 CCW (2×12) - 360°",
"description": "Standard G4.1 arena, 2 rows x 12 columns, 360 degree coverage, CCW column order",
Expand Down
267 changes: 207 additions & 60 deletions scripts/generate-arena-configs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@
* Usage:
* node scripts/generate-arena-configs.js [config-dir]
*
* If config-dir is not specified, looks for:
* 1. temp_configs/ (CI/CD fetched configs)
* 2. ../maDisplayTools/configs/arenas/ (local development)
* Config sources (in order of priority):
* 1. Command-line argument path
* 2. temp_configs/ (CI/CD fetched configs)
* 3. ../maDisplayTools/configs/arenas/ (local development)
* 4. GitHub: reiserlab/maDisplayTools feature/g6-tools branch (fallback download)
*/

const fs = require('fs');
const path = require('path');
const https = require('https');

// GitHub configuration for fallback download
const GITHUB_REPO = 'reiserlab/maDisplayTools';
const GITHUB_BRANCH = 'feature/g6-tools';
const GITHUB_CONFIG_PATH = 'configs/arenas';

// Simple YAML parser for our arena config format
function parseYAML(yamlText) {
Expand Down Expand Up @@ -48,7 +56,7 @@ function parseYAML(yamlText) {
if (arrayContent.trim() === '') {
currentSection[key] = [];
} else {
currentSection[key] = arrayContent.split(',').map(v => {
currentSection[key] = arrayContent.split(',').map((v) => {
v = v.trim();
const num = parseFloat(v);
return isNaN(num) ? v.replace(/^"|"$/g, '') : num;
Expand Down Expand Up @@ -101,7 +109,7 @@ function generateLabel(parsed) {
if (columnsInstalled && Array.isArray(columnsInstalled)) {
// columns_installed is always column indices (0-indexed)
const installedCols = columnsInstalled.length;
const coverageDeg = Math.round(360 * installedCols / cols);
const coverageDeg = Math.round((360 * installedCols) / cols);
coverage = `${coverageDeg}°`;
}

Expand All @@ -111,78 +119,151 @@ function generateLabel(parsed) {
return `${gen}${orderSuffix} (${rows}×${cols}) - ${coverage}`;
}

// Find config directory
function findConfigDir() {
// Check command line argument
if (process.argv[2]) {
return process.argv[2];
}
/**
* Make an HTTPS GET request and return the response body
* @param {string} url - URL to fetch
* @returns {Promise<string>} Response body
*/
function httpsGet(url) {
return new Promise((resolve, reject) => {
const options = {
headers: {
'User-Agent': 'webDisplayTools-config-generator'
}
};

// Check for CI/CD fetched configs
const ciDir = path.join(process.cwd(), 'temp_configs');
if (fs.existsSync(ciDir)) {
return ciDir;
}
https
.get(url, options, (res) => {
// Handle redirects
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
httpsGet(res.headers.location).then(resolve).catch(reject);
return;
}

// Check for local maDisplayTools
const localDir = path.join(process.cwd(), '..', 'maDisplayTools', 'configs', 'arenas');
if (fs.existsSync(localDir)) {
return localDir;
}
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
return;
}

console.error('Error: Could not find config directory.');
console.error('Provide path as argument or ensure temp_configs/ or ../maDisplayTools/configs/arenas/ exists.');
process.exit(1);
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(data));
res.on('error', reject);
})
.on('error', reject);
});
}

// Main
function main() {
const configDir = findConfigDir();
const outputFile = path.join(process.cwd(), 'js', 'arena-configs.js');
/**
* Fetch the list of YAML files from GitHub API
* @returns {Promise<Array<{name: string, download_url: string}>>}
*/
async function fetchGitHubFileList() {
const apiUrl = `https://api.github.com/repos/${GITHUB_REPO}/contents/${GITHUB_CONFIG_PATH}?ref=${GITHUB_BRANCH}`;
console.log(` Fetching file list from GitHub API...`);

const response = await httpsGet(apiUrl);
const files = JSON.parse(response);

// Filter for YAML files only
return files
.filter((f) => f.type === 'file' && (f.name.endsWith('.yaml') || f.name.endsWith('.yml')))
.map((f) => ({
name: f.name,
download_url: f.download_url
}));
}

console.log(`Reading configs from: ${configDir}`);
/**
* Download YAML files from GitHub to a temporary directory
* @returns {Promise<string>} Path to temp directory with downloaded files
*/
async function downloadFromGitHub() {
console.log(`Downloading configs from GitHub: ${GITHUB_REPO}@${GITHUB_BRANCH}`);

const configs = {};
const files = fs.readdirSync(configDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
// Create temp directory
const tempDir = path.join(process.cwd(), 'temp_configs');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}

// Get list of YAML files
const files = await fetchGitHubFileList();

if (files.length === 0) {
console.error('Error: No YAML files found in config directory.');
process.exit(1);
throw new Error('No YAML files found in GitHub repository');
}

console.log(` Found ${files.length} YAML files`);

// Download each file
for (const file of files) {
const content = fs.readFileSync(path.join(configDir, file), 'utf8');
const parsed = parseYAML(content);
const name = file.replace(/\.ya?ml$/, '');
console.log(` Downloading: ${file.name}`);
const content = await httpsGet(file.download_url);
fs.writeFileSync(path.join(tempDir, file.name), content);
}

configs[name] = {
label: generateLabel(parsed),
description: parsed.description || '',
arena: parsed.arena
};
console.log(` Downloaded to: ${tempDir}`);
return tempDir;
}

console.log(` Parsed: ${name} -> ${configs[name].label}`);
/**
* Find config directory, downloading from GitHub if necessary
* @returns {Promise<string>} Path to config directory
*/
async function findConfigDir() {
// Check command line argument
if (process.argv[2]) {
const argDir = process.argv[2];
if (fs.existsSync(argDir)) {
return argDir;
}
console.error(`Warning: Specified directory not found: ${argDir}`);
}

// Sort configs by generation then by name
const sortedConfigs = {};
const sortOrder = ['G6', 'G4.1', 'G4', 'G3'];
// Check for CI/CD fetched configs
const ciDir = path.join(process.cwd(), 'temp_configs');
if (fs.existsSync(ciDir)) {
const yamlFiles = fs
.readdirSync(ciDir)
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
if (yamlFiles.length > 0) {
console.log('Using existing temp_configs/ directory');
return ciDir;
}
}

Object.keys(configs)
.sort((a, b) => {
const genA = configs[a].arena?.generation || '';
const genB = configs[b].arena?.generation || '';
const orderA = sortOrder.indexOf(genA);
const orderB = sortOrder.indexOf(genB);
if (orderA !== orderB) return orderA - orderB;
return a.localeCompare(b);
})
.forEach(key => {
sortedConfigs[key] = configs[key];
});
// Check for local maDisplayTools
const localDir = path.join(process.cwd(), '..', 'maDisplayTools', 'configs', 'arenas');
if (fs.existsSync(localDir)) {
const yamlFiles = fs
.readdirSync(localDir)
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
if (yamlFiles.length > 0) {
console.log('Using local maDisplayTools configs');
return localDir;
}
}

// Generate output
const output = `/**
// Fallback: download from GitHub
console.log('Local configs not found, downloading from GitHub...');
try {
return await downloadFromGitHub();
} catch (err) {
console.error(`Error downloading from GitHub: ${err.message}`);
console.error('\nCould not find config directory. Options:');
console.error(
' 1. Provide path as argument: node scripts/generate-arena-configs.js <path>'
);
console.error(' 2. Ensure ../maDisplayTools/configs/arenas/ exists locally');
console.error(' 3. Check network connection for GitHub download');
process.exit(1);
}
}

// Generate the output JavaScript file
function generateOutput(sortedConfigs) {
return `/**
* Arena Configurations
* Auto-generated from maDisplayTools/configs/arenas/
* Last updated: ${new Date().toISOString()}
Expand Down Expand Up @@ -253,7 +334,70 @@ function getConfigsByGeneration() {
if (typeof module !== 'undefined' && module.exports) {
module.exports = { STANDARD_CONFIGS, PANEL_SPECS, getConfig, getConfigsByGeneration };
}

// Browser global export (for non-module scripts)
if (typeof window !== 'undefined') {
window.STANDARD_CONFIGS = STANDARD_CONFIGS;
window.PANEL_SPECS = PANEL_SPECS;
window.getConfig = getConfig;
window.getConfigsByGeneration = getConfigsByGeneration;
}

// ES6 module export
export { STANDARD_CONFIGS, PANEL_SPECS, getConfig, getConfigsByGeneration };
`;
}

// Main
async function main() {
const configDir = await findConfigDir();
const outputFile = path.join(process.cwd(), 'js', 'arena-configs.js');

console.log(`Reading configs from: ${configDir}`);

const configs = {};
const files = fs
.readdirSync(configDir)
.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));

if (files.length === 0) {
console.error('Error: No YAML files found in config directory.');
process.exit(1);
}

for (const file of files) {
const content = fs.readFileSync(path.join(configDir, file), 'utf8');
const parsed = parseYAML(content);
const name = file.replace(/\.ya?ml$/, '');

configs[name] = {
label: generateLabel(parsed),
description: parsed.description || '',
arena: parsed.arena
};

console.log(` Parsed: ${name} -> ${configs[name].label}`);
}

// Sort configs by generation then by name
const sortedConfigs = {};
const sortOrder = ['G6', 'G4.1', 'G4', 'G3'];

Object.keys(configs)
.sort((a, b) => {
const genA = configs[a].arena?.generation || '';
const genB = configs[b].arena?.generation || '';
const orderA = sortOrder.indexOf(genA);
const orderB = sortOrder.indexOf(genB);
if (orderA !== orderB) return orderA - orderB;
return a.localeCompare(b);
})
.forEach((key) => {
sortedConfigs[key] = configs[key];
});

// Generate output
const output = generateOutput(sortedConfigs);

// Ensure js/ directory exists
const jsDir = path.dirname(outputFile);
Expand All @@ -265,4 +409,7 @@ if (typeof module !== 'undefined' && module.exports) {
console.log(`\nGenerated ${outputFile} with ${Object.keys(sortedConfigs).length} configs`);
}

main();
main().catch((err) => {
console.error('Error:', err.message);
process.exit(1);
});