services/project.js
angular
.module('icestudio')
.service(
'project',
function (
$rootScope,
alerts,
common,
compiler,
gettextCatalog,
graph,
gui,
nodeFs,
nodePath,
utils
) {
'use strict';
function _tcStr(str, args) {
return gettextCatalog.getString(str, args);
}
this.name = ''; // Used in File dialogs
this.path = ''; // Used in Save / Save as
this.filepath = ''; // Used to find external resources (.v, .vh, .list)
this.changed = false;
this.backup = false;
var project = _default();
function _default() {
return {
version: common.VERSION,
package: {
name: '',
version: '',
description: '',
author: '',
image: '',
},
design: {
board: '',
graph: {blocks: [], wires: []},
},
dependencies: {},
};
}
this.get = function (key) {
return key in project ? project[key] : project;
};
this.set = function (key, obj) {
if (key in project) {
project[key] = obj;
}
};
this.open = function (filepath, emptyPath) {
console.debug('[srv.project.open] filepath:', filepath);
console.debug('[srv.project.open] emptyPath:', emptyPath);
var self = this;
this.path = emptyPath ? '' : filepath;
this.filepath = filepath;
common
.readFile(filepath)
.then(function (data) {
var name = common.basename(filepath);
self.filename = name;
self.dirname = nodePath.dirname(filepath);
self.load(name, data);
})
.catch((error) => {
console.debug('[srv.project.open] catch:', error);
alertify.error(_tcStr('Invalid project format'), 30);
});
};
this.load = function (name, data) {
var self = this;
if (!checkVersion(data.version)) {
return;
}
project = _safeLoad(data, name);
console.debug(
'[srv.project.load] common.selectedBoard',
common.selectedBoard
);
// FIXME: when opening an example in a new window, sometimes (frequently) common.selectedBoard is 'null' here
if (
common.selectedBoard &&
project.design.board !== common.selectedBoard.name
) {
var projectBoard = common.boardLabel(project.design.board);
alerts.confirm({
icon: 'microchip',
title: _tcStr('This project is designed for <{{name}}>', {
name: projectBoard,
}),
body: _tcStr(
'You can convert it for the <{{name}}> board or change the selected board.',
{name: common.selectedBoard.info.label}
),
onok: () => {
project.design.board = common.selectedBoard.name;
_load(
true,
boardMigration(projectBoard, common.selectedBoard.name)
);
},
oncancel: _load,
labels: {
ok: _tcStr('Convert'),
cancel: _tcStr('Change board'),
},
});
} else {
_load();
}
function _filepath2buildpath(filepath) {
let b = nodePath.basename(filepath);
let localdir = filepath.substr(0, filepath.lastIndexOf(b));
let dname = b.substr(0, b.lastIndexOf('.'));
let path = nodePath.join(localdir, 'ice-build');
//If we want to remove spaces return nodePath.join(path,dname).replace(/ /g, '_');
return nodePath.join(path, dname);
}
function _load(reset, originalBoard) {
common.allDependencies = project.dependencies;
var opt = {reset: reset || false, disabled: false};
if (originalBoard !== undefined && originalBoard !== false) {
for (var i = 0; i < common.boards.length; i++) {
if (String(common.boards[i].name) === String(originalBoard)) {
opt.originalPinout = common.boards[i].info['pinout'];
}
if (
String(common.boards[i].name) === String(project.design.board)
) {
opt.designPinout = common.boards[i].info['pinout'];
}
}
}
var ret = graph.loadDesign(project.design, opt, function () {
graph.resetCommandStack();
graph.fitContent();
alertify.success(
_tcStr('Project {{name}} loaded', {
name: `<b>${name}</b>`,
})
);
common.hasChangesSinceBuild = true;
});
if (ret) {
utils.selectBoard(project.design.board);
common.set('board', common.selectedBoard.name);
self.updateTitle(name);
let bdir = _filepath2buildpath(self.filepath);
common.setBuildDir(bdir);
} else {
alertify.error(
_tcStr('Wrong project format: {{name}}', {
name: `<b>${name}</b>`,
}),
30
);
}
}
};
function boardMigration(oldBoard, newBoard) {
var pboard = false;
switch (oldBoard.toLowerCase()) {
case 'icezum alhambra':
case 'icezum':
switch (newBoard.toLowerCase()) {
case 'alhambra-ii':
pboard = 'icezum';
break;
default:
pboard = 'icezum';
}
break;
}
return pboard;
}
function checkVersion(version) {
if (version > common.VERSION) {
var errorAlert = alertify.error(
_tcStr('Unsupported project format {{version}}', {
version: version,
}),
30
);
alertify.message(
_tcStr(
'Click here to <b>download a newer version</b> of Icestudio'
),
30
).callback = function (isClicked) {
if (isClicked) {
errorAlert.dismiss(false);
gui.Shell.openExternal(
'https://github.com/FPGAwars/icestudio/releases'
);
}
};
return false;
}
return true;
}
function _safeLoad(data, name) {
// Backwards compatibility
var project = {};
switch (data.version) {
case common.VERSION:
case '1.1':
project = data;
break;
case '1.0':
project = convert10To12(data);
break;
default:
project = convertTo10(data, name);
project = convert10To12(project);
break;
}
project.version = common.VERSION;
return project;
}
function convert10To12(data) {
var project = _default();
project.package = data.package;
project.design.board = data.design.board;
project.design.graph = data.design.graph;
var depsInfo = findSubDependencies10(data.design.deps);
replaceType10(project, depsInfo);
for (var d in depsInfo) {
var dep = depsInfo[d];
replaceType10(dep.content, depsInfo);
project.dependencies[dep.id] = dep.content;
}
return project;
}
function findSubDependencies10(deps) {
var depsInfo = {};
for (var key in deps) {
var block = utils.clone(deps[key]);
// Go recursive
var subDepsInfo = findSubDependencies10(block.design.deps);
for (var name in subDepsInfo) {
if (!(name in depsInfo)) {
depsInfo[name] = subDepsInfo[name];
}
}
// Add current dependency
block = pruneBlock(block);
delete block.design.deps;
block.package.name = block.package.name || key;
block.package.description = block.package.description || key;
if (!(key in depsInfo)) {
depsInfo[key] = {
id: utils.dependencyID(block),
content: block,
};
}
}
return depsInfo;
}
function replaceType10(project, depsInfo) {
for (var i in project.design.graph.blocks) {
var type = project.design.graph.blocks[i].type;
if (type.indexOf('basic.') === -1) {
project.design.graph.blocks[i].type = depsInfo[type].id;
}
}
}
function convertTo10(data, name) {
var project = {
version: '1.0',
package: {
name: name || '',
version: '',
description: name || '',
author: '',
image: '',
},
design: {
board: '',
graph: {},
deps: {},
},
};
for (var b in data.graph.blocks) {
var block = data.graph.blocks[b];
switch (block.type) {
case 'basic.input':
case 'basic.output':
case 'basic.outputLabel':
case 'basic.inputLabel':
block.data = {
name: block.data.label,
pins: [
{
index: '0',
name: block.data.pin ? block.data.pin.name : '',
value: block.data.pin ? block.data.pin.value : '0',
},
],
virtual: false,
};
break;
case 'basic.constant':
block.data = {
name: block.data.label,
value: block.data.value,
local: false,
};
break;
case 'basic.code':
var params = [];
for (var p in block.data.params) {
params.push({
name: block.data.params[p],
});
}
var inPorts = [];
for (var i in block.data.ports.in) {
inPorts.push({
name: block.data.ports.in[i],
});
}
var outPorts = [];
for (var o in block.data.ports.out) {
outPorts.push({
name: block.data.ports.out[o],
});
}
block.data = {
code: block.data.code,
params: params,
ports: {
in: inPorts,
out: outPorts,
},
};
break;
}
}
project.design.board = data.board;
project.design.graph = data.graph;
// Safe load all dependencies recursively
for (var key in data.deps) {
project.design.deps[key] = convertTo10(data.deps[key], key);
}
return project;
}
this.save = function (filepath, callback) {
var backupProject = false;
var name = common.basename(filepath);
if (common.isEditingSubmodule) {
backupProject = utils.clone(project);
} else {
this.updateTitle(name);
}
sortGraph();
this.update();
// Copy included files if the previous filepath
// is different from the new filepath
if (this.filepath !== filepath) {
var origPath = nodePath.dirname(this.filepath);
var destPath = nodePath.dirname(filepath);
// 1. Parse and find included files
var code = compiler.generate('verilog', project)[0].content;
var listFiles = compiler.generate('list', project);
var internalFiles = listFiles.map(function (res) {
return res.name;
});
var files = utils.findIncludedFiles(code);
files = _.difference(files, internalFiles);
// Are there included files?
if (files.length > 0) {
// 2. Check project's directory
if (filepath) {
// 3. Copy the included files
copyIncludedFiles(files, origPath, destPath, function (success) {
if (success) {
// 4. Success: save project
doSaveProject();
}
});
}
} else {
// No included files to copy
// 4. Save project
doSaveProject();
}
} else {
// Same filepath
// 4. Save project
doSaveProject();
}
if (common.isEditingSubmodule) {
project = utils.clone(backupProject);
// sortGraph();
// this.update();
} else {
this.path = filepath;
this.filepath = filepath;
}
let self = this;
function doSaveProject() {
common
.saveFile(filepath, pruneProject(project))
.then(function () {
if (callback) {
callback();
}
let bdir = _filepath2buildpath(self.filepath);
common.setBuildDir(bdir);
alertify.success(
_tcStr('Project {{name}} saved', {
name: `<b>${name}</b>`,
})
);
})
.catch(function (error) {
alertify.error(error, 30);
});
}
};
function sortGraph() {
var cells = graph.getCells();
// Sort Constant/Memory cells by x-coordinate
cells = _.sortBy(cells, function (cell) {
if (
cell.get('type') === 'ice.Constant' ||
cell.get('type') === 'ice.Memory'
) {
return cell.get('position').x;
}
});
// Sort I/O cells by y-coordinate
cells = _.sortBy(cells, function (cell) {
if (
cell.get('type') === 'ice.Input' ||
cell.get('type') === 'ice.Output'
) {
return cell.get('position').y;
}
});
graph.setCells(cells);
}
function _addBlockFile(self, orig, name, data, notify) {
function _importBlock() {
self.addBlock(block);
if (notify) {
alertify.success(
_tcStr('Block {{name}} imported', {
name: `<b>${block.package.name}</b>`,
})
);
}
}
var block = _safeLoad(data, name);
if (!block) {
return;
} // FIXME: should produce a meaningful error
// 1. Parse and find included files
var code = compiler.generate('verilog', block)[0].content;
var internalFiles = compiler
.generate('list', block)
.map(function (res) {
return res.name;
});
var files = _.difference(utils.findIncludedFiles(code), internalFiles);
function _importBlockWithFiles() {
copyIncludedFiles(
files,
orig,
nodePath.dirname(self.path),
function (success) {
if (success) {
_importBlock();
} // FIXME: should notify if something went wrong, instead of failing silently...
}
);
}
if (!files.length) {
_importBlock();
return;
}
if (self.path) {
_importBlockWithFiles();
return;
}
alerts.confirm({
title: _tcStr('This import operation requires a project path'),
body: _tcStr(
'You need to save the current project. Do you want to continue?'
),
onok: () => {
$rootScope.saveProjectAs();
_importBlockWithFiles();
},
labels: {
ok: _tcStr('Save'),
cancel: _tcStr('Cancel'),
},
});
}
this.addBlockFile = function (filepath, notify) {
var self = this;
common
.readFile(filepath)
.then(function (data) {
if (!checkVersion(data.version)) {
return;
} // FIXME: should produce a meaningful error
_addBlockFile(
self,
nodePath.dirname(filepath),
common.basename(filepath),
data,
notify
);
})
.catch(function (e) {
console.log(e);
alertify.error(_tcStr('Invalid project format'), 30); // FIXME: other reasons might produce an error
});
};
function copyIncludedFiles(files, origPath, destPath, callback) {
var success = true;
async.eachSeries(
files,
function (filename, next) {
if (origPath === destPath) {
return next();
}
function _ok() {
if (!doCopySync(origPath, destPath, filename)) {
success = false;
return next();
}
next();
}
if (!nodeFs.existsSync(nodePath.join(destPath, filename))) {
_ok();
}
alerts.confirm({
icon: 'question-circle',
title: _tcStr(
'File {{file}} already exists in the project path',
{
file: `<b>${filename}</b>`,
}
),
body: _tcStr('Do you want to replace it?'),
onok: _ok,
oncancel: next,
});
},
function () {
return callback(success);
}
);
}
function doCopySync(origPath, destPath, filename) {
if (
utils.copySync(
nodePath.join(origPath, filename),
nodePath.join(destPath, filename)
)
) {
alertify.message(
_tcStr('File {{file}} imported', {
file: `<b>${filename}</b>`,
}),
5
);
return true;
}
alertify.error(
_tcStr('Original file {{file}} does not exist', {
file: `<b>${filename}</b>`,
}),
30
);
return false;
}
function pruneProject(project) {
var _project = utils.clone(project);
_prune(_project);
for (var d in _project.dependencies) {
_prune(_project.dependencies[d]);
}
function _prune(_project) {
delete _project.design.state;
for (var i in _project.design.graph.blocks) {
var block = _project.design.graph.blocks[i];
switch (block.type) {
case 'basic.input':
case 'basic.output':
case 'basic.outputLabel':
case 'basic.inputLabel':
case 'basic.constant':
case 'basic.memory':
break;
case 'basic.code':
for (var j in block.data.ports.in) {
delete block.data.ports.in[j].default;
}
break;
case 'basic.info':
delete block.data.text;
break;
default:
// Generic block
delete block.data;
break;
}
}
}
return _project;
}
this.snapshot = function () {
this.backup = utils.clone(project);
};
this.restoreSnapshot = function () {
project = utils.clone(this.backup);
};
this.update = function (opt, callback) {
var graphData = graph.toJSON();
var p = utils.cellsToProject(graphData.cells, opt);
project.design.board = p.design.board;
project.design.graph = p.design.graph;
project.dependencies = p.dependencies;
if (
common.isEditingSubmodule &&
common.submoduleId &&
common.allDependencies[common.submoduleId]
) {
project.package = common.allDependencies[common.submoduleId].package;
}
var state = graph.getState();
project.design.state = {
pan: {
x: parseFloat(state.pan.x.toFixed(4)),
y: parseFloat(state.pan.y.toFixed(4)),
},
zoom: parseFloat(state.zoom.toFixed(4)),
};
if (callback) {
callback();
}
};
this.updateTitle = function (name) {
if (name) {
this.name = name;
graph.resetTitle(name);
}
var title = (this.changed ? '*' : '') + this.name + ' ─ Icestudio';
utils.updateWindowTitle(title);
};
this.compile = function (target) {
this.update();
var opt = {boardRules: common.get('boardRules')};
return compiler.generate(target, project, opt);
};
this.addBasicBlock = function (type) {
graph.createBasicBlock(type);
};
this.addBlock = function (block) {
if (block) {
block = _safeLoad(block);
block = pruneBlock(block);
if (block.package.name.toLowerCase().indexOf('generic-') === 0) {
var dat = new Date();
var seq = dat.getTime();
block.package.otid = seq;
}
var type = utils.dependencyID(block);
utils.mergeDependencies(type, block);
graph.createBlock(type, block);
}
};
function pruneBlock(block) {
// Remove all unnecessary information for a dependency:
// - version, board, FPGA I/O pins (->size if >1), virtual flag
delete block.version;
delete block.design.board;
var i, pins;
for (i in block.design.graph.blocks) {
if (
block.design.graph.blocks[i].type === 'basic.input' ||
block.design.graph.blocks[i].type === 'basic.output' ||
block.design.graph.blocks[i].type === 'basic.outputLabel' ||
block.design.graph.blocks[i].type === 'inputLabel'
) {
if (block.design.graph.blocks[i].data.size === undefined) {
pins = block.design.graph.blocks[i].data.pins;
block.design.graph.blocks[i].data.size =
pins && pins.length > 1 ? pins.length : undefined;
}
delete block.design.graph.blocks[i].data.pins;
delete block.design.graph.blocks[i].data.virtual;
}
}
return block;
}
this.clear = function () {
project = _default();
graph.clearAll();
graph.resetTitle();
graph.resetCommandStack();
};
}
);