// 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 . /* 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; private wires: Wire[]; private mode: string; private type: string; private basepath: string; private runInit: Array>; 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> = new Map>(); private isFullScreen = false; private moveStart: Array = [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(); 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 (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 = 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 = 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(); //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; } }