logicsimulator/ts/simulator.ts

636 lines
20 KiB
TypeScript

// Simulator main class of the LogicSimulator
// Copyright (C) 2022 Sascha Nitsch
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/* eslint-disable @typescript-eslint/no-explicit-any */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Window {
[propName: string]: any;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class Simulator {
private svg: SVGElement;
private parent: HTMLElement;
private root: SVGGElement;
private componentsToLoad: string[];
private onComponentsLoadedCallback: null;
private components: Map<string, BaseComponent>;
private wires: Wire[];
private mode: string;
private type: string;
private basepath: string;
private runInit: Array<Array<String>>;
private cors: boolean;
private ajax: boolean;
private zoom: number;
private x: number;
private y: number;
private zoomlevel: number[] = [0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5];
private json: Object;
private boundkeypress: any;
private loading: Map<string, Array<[any, any, any]>> = new Map<string, Array<[any, any, any]>>();
private isFullScreen = false;
private moveStart: Array<number> = [0, 0];
private moving: any = undefined;
constructor(parent: string, width: string, height: string, className: string, zoom: number) {
this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
if (width.indexOf === undefined || width.indexOf('%') === -1) width += 'px';
this.svg.setAttribute('width', width);
this.svg.setAttribute('height', height + 'px');
if (!className) {
className = 'logic';
}
this.svg.setAttribute('class', className);
const p = document.getElementById(parent);
if (p === null) {
throw new TypeError('parent element not found');
}
this.parent = p;
this.parent.appendChild(this.svg);
this.root = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.svg.appendChild(this.root);
this.componentsToLoad = [];
this.onComponentsLoadedCallback = null;
this.components = new Map<string, BaseComponent>();
this.wires = [];
this.mode = 'manual';
this.type = className;
this.basepath = '';
this.runInit = [];
const xhrSupported = this.xhr();
this.cors = xhrSupported !== null && 'withCredentials' in xhrSupported;
this.ajax = xhrSupported !== null;
if (zoom) {
this.zoom = 0;
while (this.zoom < this.zoomlevel.length + 1 && this.zoomlevel[this.zoom + 1] <= zoom) {
++this.zoom;
}
} else {
this.zoom = 5;
}
this.x = this.y = 0;
this.json = {};
}
close(): void {
for (const i in this.components) {
const c = this.components.get(i);
if (c !== undefined) c.close();
}
if (this.svg.parentNode !== null) {
this.svg.parentNode.removeChild(this.svg);
}
window.removeEventListener('keydown', this.boundkeypress);
}
setBasePath(path: string): void {
this.basepath = path;
}
callback(scope: any, funct: any, parameter: any) {
if (!scope) {
funct.call(this, parameter);
} else {
funct.call(scope, parameter);
}
}
loadJS(file: string, scope: any, callback: any, parameter: any) {
const f = this.loading.get(file);
if (f !== undefined) {
f.push([scope, callback, parameter]);
return;
}
const script = document.createElement('script');
script.setAttribute('src', this.basepath + 'components/' + file + '.min.js');
this.loading.set(file, [[scope, callback, parameter]]);
if (callback) {
script.onload = () => {
this.callbackList(file);
};
}
document.getElementsByTagName('head')[0].appendChild(script);
}
callbackList(file: string) {
const f = this.loading.get(file);
if (f === undefined) return;
for (let i = 0; i < f.length; ++i) {
const c = f[i];
if (c[0]) {
this.callback(c[0], c[1], c[2]);
}
}
}
loadSVG(file: string, scope: any, callback: any, parameter: any) {
const obj = document.createElement('object');
obj.setAttribute('data', this.basepath + 'components/' + file + '.svg');
obj.setAttribute('id', file);
obj.setAttribute('type', 'image/svg+xml');
obj.setAttribute('class', 'invisible');
if (callback) {
obj.onload = (e: any) => {
if (!e.target.loaded) {
e.target.loaded = true;
this.callback(scope, callback, parameter);
}
};
}
document.getElementsByTagName('body')[0].appendChild(obj);
}
loadCSS(file: string): void {
const link = document.createElement('link');
link.setAttribute('href', this.basepath + 'components/' + file + '.css');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
document.getElementsByTagName('head')[0].appendChild(link);
}
loadComponent(classname: string, loadsvg: boolean): void {
if (!(classname in window) && this.componentsToLoad.indexOf(classname + 'js') === -1) {
this.componentsToLoad.push(classname + 'js');
if (loadsvg) {
this.componentsToLoad.push(classname + 'svg');
}
this.loadJS(classname.toLowerCase(), this, this.componentLoaded, classname + 'js');
if (loadsvg) {
this.loadSVG(classname.toLowerCase(), this, this.componentLoaded, classname + 'svg');
}
}
}
componentLoaded(classname: string): void {
for (let i = 0; i < this.componentsToLoad.length; ++i) {
if (this.componentsToLoad[i] === classname) {
this.componentsToLoad.splice(i, 1);
break;
}
}
const cn = classname.substr(0, classname.length - 2);
for (let j = 0; j < this.runInit.length; ++j) {
const init = this.runInit[j][1] + 'Init';
if (this.runInit[j][0] === cn && typeof window[init] === 'function') {
window[init]();
this.runInit.splice(j, 1);
--j;
}
}
if (typeof window[cn + 'Depends'] === 'function') {
const depends = window[cn + 'Depends']();
if (!window[depends]) {
if (this.componentsToLoad.indexOf(depends + 'js') === -1) {
this.componentsToLoad.push(depends + 'js');
this.loadJS(depends.toLowerCase(), this, this.componentLoaded, depends + 'js');
}
this.runInit.push([depends, cn]);
} else {
if (window[classname + 'Init']) {
window[classname + 'Init']();
}
}
}
if (this.componentsToLoad.length === 0) {
this.callback(this, this.onComponentsLoadedCallback, null);
}
}
onComponentsLoaded(callback: any) {
if (this.componentsToLoad.length === 0) {
this.onComponentsLoadedCallback = null;
this.callback(this, callback, null);
} else {
this.onComponentsLoadedCallback = callback;
}
}
add(id: string | null, name: string, param: any): BaseComponent | null {
if (!(name in window)) {
console.log('no such component:', name);
return null;
}
if (id === null) {
do {
id = makeId(10);
} while (id in this.components);
}
const component = new (<any>window[name])(this, id, param);
this.components.set(id, component);
return component;
}
wire(id: string | null, points: any, param?: any) {
const wire = new Wire(id, param, this.type === 'physical');
wire.connect(points);
this.wires.push(wire);
}
svgButton(parent: SVGElement, x: string, y: string, label: string, onclick: Function) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('transform', 'translate(' + x + ',' + y + ')');
g.setAttribute('data-y', y);
g.setAttribute('class', 'zoombutton');
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', '0');
rect.setAttribute('y', '0');
rect.setAttribute('width', '20');
rect.setAttribute('height', '20');
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('y', '16');
const txt = document.createTextNode(label);
text.appendChild(txt);
g.appendChild(rect);
g.appendChild(text);
parent.appendChild(g);
if (onclick) {
g.addEventListener('click', onclick.bind(this));
}
const bbox = text.getBBox();
text.setAttribute('x', ((20 - bbox.width) / 2).toString());
}
svgZoomIn() {
if (this.zoom < this.zoomlevel.length - 1) {
++this.zoom;
this.svgTransform();
}
}
svgZoomOut() {
if (this.zoom > 0) {
--this.zoom;
this.svgTransform();
}
}
svgFullScreen(): void {
this.isFullScreen = !this.isFullScreen;
let oldWidth;
let width;
if (this.isFullScreen) {
this.svg.setAttribute('style', 'position:fixed; left:0; top:0; width:100%; height:100%;z-index:1000');
oldWidth = this.svg.getBoundingClientRect().width;
width = document.body.clientWidth;
} else {
this.svg.removeAttribute('style');
width = this.svg.getBoundingClientRect().width;
oldWidth = document.body.clientWidth;
}
const buttons = this.svg.querySelectorAll('.zoombutton');
for (let i = 0; i < buttons.length; ++i) {
buttons[i].setAttribute('transform', 'translate(' + (width - 21) + ',' + buttons[i].getAttribute('data-y') + ')');
}
// calc new zoom
const zoom = (this.zoomlevel[this.zoom] * width) / oldWidth;
this.zoom = 0;
while (this.zoom < this.zoomlevel.length + 1 && this.zoomlevel[this.zoom + 1] <= zoom) {
++this.zoom;
}
this.svgTransform();
}
svgTransform() {
this.root.setAttribute(
'transform',
'translate(' + this.x + ',' + this.y + '), scale(' + this.zoomlevel[this.zoom] + ')'
);
}
startMove(event: MouseEvent) {
if (event.button !== 0) {
return;
}
this.moveStart = [event.clientX, event.clientY];
if (this.moving) {
this.svg.removeEventListener('mousemove', this.moving);
}
this.moving = this.doMove.bind(this);
this.svg.addEventListener('mousemove', this.moving);
}
endMove() {
this.svg.removeEventListener('mousemove', this.moving);
delete this.moving;
}
doMove(event: MouseEvent) {
this.x += event.clientX - this.moveStart[0];
this.y += event.clientY - this.moveStart[1];
this.moveStart = [event.clientX, event.clientY];
this.svgTransform();
}
mouseScroll(event: any) {
if (!event.ctrlKey) {
return;
}
event.preventDefault();
if (event.deltaY > 0) {
this.svgZoomOut();
} else {
this.svgZoomIn();
}
}
keypress(event: KeyboardEvent) {
if (event.altKey) {
if (event.key === '-') {
event.preventDefault();
this.svgZoomOut();
} else if (event.key === '+') {
event.preventDefault();
this.svgZoomIn();
}
}
}
run() {
//create zoom and fullscreeen buttons
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('class', 'labelbg');
this.svg.appendChild(g);
const width = this.svg.getBoundingClientRect().width;
this.svgButton(g, (width - 21).toString(), '1', '+', this.svgZoomIn);
this.svgButton(g, (width - 21).toString(), '23', '-', this.svgZoomOut);
this.svgButton(g, (width - 21).toString(), '46', '⇔', this.svgFullScreen);
this.svg.addEventListener('mousedown', this.startMove.bind(this));
this.svg.addEventListener('mouseup', this.endMove.bind(this));
this.svg.addEventListener('wheel', this.mouseScroll.bind(this));
this.boundkeypress = this.keypress.bind(this);
window.addEventListener('keydown', this.boundkeypress);
this.svgTransform();
for (let i = 0; i < this.wires.length; ++i) {
this.wires[i].update();
}
this.tick();
}
manualtick(): void {
if (this.mode === 'manual') {
this.tick();
}
}
tick(): void {
let i;
let instantUpdate = false;
let reruns = 10;
do {
instantUpdate = false;
this.components.forEach((c: BaseComponent) => {
c.io();
});
this.components.forEach((c: BaseComponent) => {
instantUpdate = c.update() || instantUpdate;
});
for (i = 0; i < this.wires.length; ++i) {
instantUpdate = this.wires[i].update() || instantUpdate;
}
} while (instantUpdate && --reruns > 0);
}
fromJson(json: Object) {
this.json = json;
// round 1, extract components
let id: keyof typeof json;
for (id in json) {
const j = <any>json[id];
const component = j.component;
if (component !== 'Wire') {
if ('svg' in j) {
this.componentsToLoad.push(component + 'svg');
this.loadSVG(j.svg.toLowerCase(), this, this.componentLoaded, component + 'svg');
this.loadComponent(component, false);
} else {
this.loadComponent(component, j.nosvg !== true);
}
}
}
this.onComponentsLoaded(this.jsonSetup);
}
xhr(): XMLHttpRequest | null {
try {
return new window.XMLHttpRequest();
} catch (e) {
return null;
}
}
fromJsonFile(url: string) {
// load json
let xhr = null;
if (this.ajax && this.cors) {
xhr = this.xhr();
if (xhr !== null) {
xhr.open('get', url);
}
}
if (xhr === null) {
return;
}
xhr.onreadystatechange = function (this: Simulator, a: Event) {
if (a === null || a.target === null) return;
const target = <XMLHttpRequest>a.target;
if (target.readyState === 4 && target.status === 200) {
const json = JSON.parse(target.responseText);
this.fromJson(json);
}
}.bind(this);
// Send the request
xhr.send(null);
}
jsonSetup() {
const components = new Map<string, BaseComponent>();
//round 2, create and place components
let json: any;
let id: keyof Object;
for (id in this.json) {
json = this.json[id];
if (json.component !== 'Wire') {
const component = this.add(id, json.component, json.param);
if (component === null) {
continue;
}
if (json.position && !component.setPosition) {
console.log('error on component', component, json);
}
if (json.position && component.setPosition) {
if (typeof json.position === 'string') {
const pos = json.position.split('.');
const c = components.get(pos[0]);
if (c !== undefined) {
component.setPosition(c, pos[1]);
}
} else {
component.setPosition(json.position[0], json.position[1]);
}
}
if (json.label) {
component.showLabel(json.label[0], json.label[1], json.label.length === 3 ? json.label[2] : undefined);
}
if (json.ref) {
component.showRef(json.ref[0], json.ref[1], json.ref[2]);
}
if (json.startdelay) {
component.setStartDelay(json.startdelay);
}
components.set(id, component);
}
}
this.components.forEach(function (this: Simulator, c: BaseComponent) {
if (c.zIndex === 0) {
c.setup(this.root);
}
}, this);
// round 3, add wiring
let i;
let tokens;
let obj;
for (id in this.json) {
json = this.json[id];
if (json.component === 'Wire') {
if (json.path) {
json.paths = [json.path];
}
const base = json.base;
if (json.paths) {
for (let p = 0; p < json.paths.length; ++p) {
const path = json.paths[p];
for (i = 0; i < path.length; ++i) {
if (typeof path[i] === 'string') {
tokens = path[i].split('.');
if (tokens[0] === '') {
tokens[0] = base;
}
obj = components.get(tokens[0]);
if (obj !== undefined) {
path[i] = obj.getPin(tokens[1]);
} else {
console.log('could not find ', tokens[0], '.');
}
} else if (typeof path[i] === 'number') {
if (path[path[i]] instanceof TriState) {
const t = path[path[i]];
path[i] = [t.x(), t.y()];
}
}
}
let rerun = true;
let reruns = 0;
while (rerun && reruns < 100) {
let other;
++reruns;
rerun = false;
for (i = 0; i < path.length; ++i) {
if (path[i] && typeof path[i] === 'object') {
let pa;
if (typeof path[i][0] === 'string') {
// relative
const offsetX = path[i][0].length > 1 ? parseInt(path[i][0].substr(1)) : 0;
pa = path[i][0][0];
if (pa === 'b') {
const c = components.get(base);
if (c !== undefined) {
path[i][0] = c.getX() + offsetX;
}
} else {
other = pa === 'p' ? i - 1 : i + 1;
if (path[other] instanceof TriState) {
path[i][0] = path[other].x() + offsetX;
} else {
if (typeof path[other] === 'number' || typeof path[other][0] === 'string') {
rerun = true;
} else {
path[i][0] = path[other][0] + offsetX;
}
}
}
}
if (typeof path[i][1] === 'string') {
// relative
const offsetY = path[i][1].length > 1 ? parseInt(path[i][1].substr(1)) : 0;
pa = path[i][1][0];
if (pa === 'b') {
const c = components.get(base);
if (c !== undefined) {
path[i][1] = c.getY() + offsetY;
}
} else {
other = pa === 'p' ? i - 1 : i + 1;
if (path[other] instanceof TriState) {
path[i][1] = path[other].y() + offsetY;
} else {
if (typeof path[other] === 'number' || typeof path[other][1] === 'string') {
rerun = true;
} else {
path[i][1] = path[other][1] + offsetY;
}
}
}
}
} else if (path[i] && typeof path[i] === 'number') {
other = path[i];
if (path[other] instanceof TriState) {
path[i] = path[other];
} else {
if (typeof path[other][0] === 'string' || typeof path[other][1] === 'string') {
rerun = true;
} else {
path[i] = path[other];
}
}
}
}
}
this.wire(p ? null : id, path, json.param); // first wire gets id
}
}
}
}
this.components.forEach(function (this: Simulator, c: BaseComponent) {
if (c.zIndex > 0) {
c.setup(this.root);
}
}, this);
//finalize component setup
this.components.forEach((c: BaseComponent) => {
c.finalize();
});
for (i = 0; i < this.wires.length; ++i) {
const s = this.wires[i].setup(this.root);
if (!s) {
this.wires.splice(i, 1);
--i;
}
}
this.run();
}
getParent(): HTMLElement {
return this.parent;
}
getType() {
return this.type;
}
}