Files
TRIMSP/TrimSPelec.js
T
salman a6f0b28cc5 Fix Electron plotting: expose Plotly as a global
Electron wraps every renderer <script> in its own CommonJS module scope,
so const Plotly = require('plotly.js-dist') stayed module-local and the
bare Plotly references in myplots.js (Plot_xy) and TrimSPlib.js
(Plotly.purge) threw ReferenceError, breaking both the Plot menu popups
and the in-app Plots tab. Export window.Plotly = Plotly, matching the
window.openElectronAuxiliaryWindow pattern already used in the file.
2026-10-08 16:18:11 +02:00

404 lines
12 KiB
JavaScript

// This file contains function that are Electron/Node.JS specific
// Load required packages
const Plotly = require('plotly.js-dist');
// Electron wraps every renderer <script> in its own CommonJS module scope, so
// the bundled Plotly object must be exported to the global scope explicitly.
// myplots.js and TrimSPlib.js call Plotly.plot/relayout/purge as a global on
// the main window and inside the Plot-menu popup windows.
window.Plotly = Plotly;
const ipcRenderer = require('electron').ipcRenderer;
const fs = require('fs');
const { execSync, execFile } = require('child_process');
const os = require('os');
const path = require('path');
// Renderer requests for native auxiliary windows stay in the main process.
function openElectronAuxiliaryWindow(kind, options = {}) {
if (kind !== 'quickStart') { return false; }
ipcRenderer.send('openQuickStartWindow', options || {});
return true;
}
window.openElectronAuxiliaryWindow = openElectronAuxiliaryWindow;
// Electron links always point to the public web application rather than a
// machine-local file:// URL, so they can be sent directly to another user.
var TRIMSP_CANONICAL_SHARE_URL = 'http://musruser.psi.ch/TRIMSP-NL/';
function encodeElectronSharedConfiguration(config) {
let json = JSON.stringify(config);
try {
let zipped = require('zlib').gzipSync(require('buffer').Buffer.from(json, 'utf8'));
return {
prefix: '#c=',
payload: zipped.toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
};
} catch (error) {
return { prefix: '#cfg=', payload: encodeURIComponent(json) };
}
}
async function copyWebConfigurationLink() {
let encoded = encodeElectronSharedConfiguration(buildSharedConfiguration());
let url = TRIMSP_CANONICAL_SHARE_URL + encoded.prefix + encoded.payload;
try {
await ipcRenderer.invoke('copyConfigurationLink', url);
showTransientWarning('Configuration link copied.', document.getElementById('shareConfigButton'), 3500);
return true;
} catch (error) {
if (typeof window.prompt === 'function') {
window.prompt('Copy configuration link:', url);
return true;
}
showTransientWarning('Could not copy configuration link.', document.getElementById('shareConfigButton'), 3500);
return false;
}
}
function getFiles(dir, filelist){
fileList = [];
var files = fs.readdirSync(dir);
for(var i of files){
if (!files.hasOwnProperty(i)) continue;
var name = dir+'/'+files[i];
if (!fs.statSync(name).isDirectory()){
fileList.push(name);
}
}
return fileList;
}
// Run one TRIM.SP-NL process asynchronously in a given working directory so the
// scan loop can keep several local jobs in flight at once.
function executeFileAsync(binary, args, workdir) {
return new Promise((resolve, reject) => {
execFile(binary, args, {cwd: workdir}, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
// Use the available CPU parallelism as the default worker-pool size for local
// scan jobs. The caller still caps this against the number of scan points.
function getParallelJobCount() {
if (typeof os.availableParallelism === 'function') {
return Math.max(1, os.availableParallelism());
}
return Math.max(1, os.cpus().length);
}
function checkDir(directory) {
// Check whether directory exists, if not create it
if (typeof directory !== 'string' || directory.length === 0) {
console.log('Invalid directory passed to checkDir:', directory);
return(0);
}
try {
fs.mkdirSync(directory, { recursive: true });
} catch (err) {
alert('Cannot create data folder '+directory);
return(0);
}
return(1);
}
function writeAsciiFile(filename,content) {
// Write string content into ascii file filename
try { fs.writeFileSync(filename, content, 'utf-8'); }
catch(e) {
alert('Failed to save '+filename);
return(0);
}
return(1);
}
function readAsciiFile(filename) {
// Read ascii file filename and return content in a string
var content;
// If an array of filenames was sent take the first one only
if (Array.isArray(filename)) {filename=filename[0];}
try {
content = fs.readFileSync(filename, 'utf8');
} catch(e) {
alert('Failed to read '+filename);
return(0);
}
return(content);
}
// Async counterparts used by the shared scan pipeline. Local disk IO is fast,
// but returning promises keeps the call sites identical across web and Electron.
async function writeAsciiFileAsync(filename,content) {
try {
await fs.promises.writeFile(filename, content, 'utf-8');
} catch(e) {
alert('Failed to save '+filename);
return(0);
}
return(1);
}
async function readAsciiFileAsync(filename) {
if (Array.isArray(filename)) {filename=filename[0];}
try {
return await fs.promises.readFile(filename, 'utf8');
} catch(e) {
alert('Failed to read '+filename);
// Match the web backend contract: the scan pipeline awaits this and
// expects a rejected promise, not a falsy value that would later fail
// with a confusing "0.trim is not a function" error.
throw new Error('Failed to read '+filename);
}
}
function fileExists(filename) {
if (typeof filename !== 'string' || filename.length === 0) {
return 0;
}
try {
if (fs.existsSync(filename)) {
return 1;
}
}catch(err) {
console.log(err);
}
return 0;
}
function getTrimBinaryPath(configuredPath) {
const candidates = [];
if (configuredPath) {
candidates.push(path.join(configuredPath, "trimspNL"));
}
if (process.resourcesPath) {
candidates.push(path.join(process.resourcesPath, "bin", "trimspNL"));
candidates.push(path.join(process.resourcesPath, "trimspNL"));
}
candidates.push(path.join(process.cwd(), "trimspNL"));
for (const candidate of candidates) {
if (fileExists(candidate)) {
return candidate;
}
}
return "trimspNL";
}
function setWorkFolder(foldername) {
const folder = Array.isArray(foldername) ? foldername[0] : foldername;
if (folder) {
document.getElementById("workPath").value = folder;
if (typeof updateStatusBar === 'function') {
updateStatusBar({ workPath: folder });
}
ipcRenderer.send('updateWorkPath', folder);
try {
// Change process directory
process.chdir(folder);
} catch (err) {
console.log('Failed to change directory:', err);
}
}
}
function browseForFolder() {
ipcRenderer.send('browseFolder');
}
function fallbackBrowseForFolder() {
const picker = document.getElementById("folderPicker");
if (!picker) { return; }
picker.value = '';
picker.click();
}
function getConfigValue(content, key) {
const lines = content.toString().split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('[')) {
continue;
}
const separator = trimmed.indexOf('=');
if (separator === -1) {
continue;
}
const param = trimmed.slice(0, separator).trim();
if (param === key) {
return trimmed.slice(separator + 1).trim();
}
}
return null;
}
function resolveConfigWorkPath(filename, content) {
const configuredWorkPath = getConfigValue(content, 'workPath');
if (!configuredWorkPath) {
return null;
}
if (configuredWorkPath.startsWith('./')) {
return path.resolve(configuredWorkPath);
}
if (path.isAbsolute(configuredWorkPath)) {
return configuredWorkPath;
}
return null;
}
function loadConfigFile(filename, autoRun = false) {
const configPath = Array.isArray(filename) ? filename[0] : filename.toString();
console.log('Open file ' + configPath);
return new Promise((resolve, reject) => {
fs.readFile(configPath, function read(err, data) {
if (err) {
reject(err);
return;
}
setValues(data);
const resolvedWorkPath = resolveConfigWorkPath(configPath, data);
if (resolvedWorkPath) {
setWorkFolder([resolvedWorkPath]);
}
if (autoRun) {
setTimeout(() => {
startSim().then(resolve).catch(reject);
}, 0);
} else {
resolve();
}
});
});
}
async function runStartupQueue(configPaths, autoRun) {
if (!configPaths || configPaths.length === 0) {
return;
}
if (!autoRun) {
await loadConfigFile(configPaths[0], false);
return;
}
for (let i = 0; i < configPaths.length; i++) {
const configPath = configPaths[i];
const queueLabel = `Queue ${i + 1}/${configPaths.length}`;
if (typeof updateStatusBar === 'function') {
updateStatusBar({ run: `${queueLabel}: loading ${path.basename(configPath)}` });
}
await loadConfigFile(configPath, false);
if (typeof updateStatusBar === 'function') {
updateStatusBar({ run: `${queueLabel}: running ${path.basename(configPath)}` });
}
await startSim();
}
if (typeof updateStatusBar === 'function') {
updateStatusBar({ run: `Completed queue of ${configPaths.length} config file(s)` });
}
}
function firstLoad() {
document.getElementById("trimPath").value = process.cwd();
if (typeof syncStatusBar === 'function') {
syncStatusBar();
}
const folderPicker = document.getElementById("folderPicker");
if (folderPicker) {
folderPicker.addEventListener('change', function(event) {
const files = event.target.files;
if (!files || files.length === 0) {
return;
}
const folder = path.dirname(files[0].path);
setWorkFolder([folder]);
});
}
// Catch calls for open file
ipcRenderer.on('openFile', function(event, filename) {
loadConfigFile(filename, false);
});
ipcRenderer.on('openFileAndMaybeRun', function(event, payload) {
if (!payload || !payload.filename) { return; }
loadConfigFile(payload.filename, !!payload.autoRun);
});
// Catch calls for selectfolder
ipcRenderer.on('selectFolder', function(event, foldername) {
setWorkFolder(foldername);
console.log("currentdir",process.cwd());
});
// Catch calls for save as
ipcRenderer.on('saveFile', function(event, filename) {
const workPath = document.getElementById("workPath").value || process.cwd();
if (filename === undefined || filename === null) { return; }
// If filename is empty use default value in the selected work folder
if (filename == '') {filename = path.join(workPath, 'TrimSP.cfg');}
if (path.extname(filename) === '') {
filename += '.cfg';
}
// Get values from all fields and prepare config file
let trimSPcfg=prep_cfg(0);
// Save file to filename
console.log('Save file to '+filename);
try {
fs.writeFileSync(filename, trimSPcfg, 'utf-8');
if (typeof updateStatusBar === 'function') {
updateStatusBar({ lastSave: filename });
}
if (typeof showTransientWarning === 'function') {
showTransientWarning('Saved configuration to ' + filename, document.getElementById('workPath'), 4000);
}
}
catch(e) { alert('Failed to save the file !'); }
});
// Catch calls for plotProf
ipcRenderer.on('plotProf', function(event, filename) {
console.log("filename="+filename);
plotProfiles(filename);
});
// Catch calls for plotFrac
ipcRenderer.on('plotFrac', function(event, filename) {
console.log("filename="+filename);
plotFractions(filename);
});
// Catch calls for plotMean
ipcRenderer.on('plotMean', function(event, filename) {
console.log("filename="+filename);
plotMean(filename);
});
// Catch clicks for Browse button
ipcRenderer.on('browseFolder', function(event, foldername) {
console.log('browseFolder reply', foldername);
setWorkFolder(foldername);
});
ipcRenderer.on('browseFolderFallback', function() {
fallbackBrowseForFolder();
});
}
window.addEventListener('load', async function() {
try {
const startupOptions = await ipcRenderer.invoke('getStartupOptions');
if (startupOptions && startupOptions.configPaths && startupOptions.configPaths.length > 0) {
setTimeout(() => {
runStartupQueue(startupOptions.configPaths, !!startupOptions.autoRun).catch((err) => {
console.log('Failed to run startup queue:', err);
});
}, 0);
}
} catch (err) {
console.log('Failed to read startup options:', err);
}
});