UNPKG

ecljs

Version:

electric circuits library

347 lines (346 loc) 12.1 kB
import { isArr } from 'dabbjs/dist/lib/dab'; import { attr, tag } from 'dabbjs/dist/lib/dom'; import { extend } from 'dabbjs/dist/lib/misc'; import { Point } from 'dabbjs/dist/lib/point'; import { Rect } from 'dabbjs/dist/lib/rect'; import { Size } from 'dabbjs/dist/lib/size'; import { Type } from './interfaces'; import { ItemBoard } from './itemsBoard'; export class Wire extends ItemBoard { get type() { return Type.WIRE; } get count() { return this.$.points.length; } get last() { return this.$.points.length - 1; } get lastLine() { return this.edit ? this.$.lines.length : 0; } get isOpen() { return !this.container.nodeBonds(this, 0) || !this.container.nodeBonds(this, this.last); } rect() { return Rect.create(this.box); } get points() { return Array.from(this.$.points); } get edit() { return this.$.edit; } /** * @description get/set wire edit mode */ set edit(value) { if (this.edit == value) return; this.g.innerHTML = ""; this.$.dir && (this.$.arrow = poly(this.g, "arrow", -1)); if (this.edit) { // will change to false // .destroy lines this.$.lines = []; // .recreate polyline this.$.poly = poly(this.g); } else { // will change to true // .destroy polyline this.$.poly = void 0; // .recreate lines setlines(this, this.$); } //has to be at the end, because logic this.$.edit = value; //refresh only with polyline !value && this.refresh(); } get head() { return this.$.headLength; } get swipe() { return this.$.headAngle; } /** * @description returns wire size, it's computed every time, so save locally if called multiple times */ get size() { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; this.$.points.forEach(p => { minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x); minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y); }); return new Size(maxX - minX + 1, maxY - minY + 1); } /** * @description customize arrow for directional wires only * @param length arrow line length * @param angle arrow lines swipe angle */ arrow(length, angle) { if (!this.$.dir) return; this.$.headLength = length; this.$.headAngle = angle; arrow(this.$); } constructor(container, options) { super(container, options); this.$.dir = container.dir; this.setPoints(options.points); this.onProp && this.onProp({ id: `#${this.id}`, code: 1 // "create" code = 1 }); } refresh() { if (this.edit) { for (let i = 0, a = this.$.points[0], last = this.last; i < last; i++) { let b = this.$.points[i + 1], ln = this.$.lines[i]; attr(ln, { line: i + 1, x1: a.x, y1: a.y, x2: b.x, y2: b.y }); a = b; } } else attr(this.$.poly, { points: this.$.points.map(p => `${p.x}, ${p.y}`).join(' ') }); arrow(this.$); // full-refresh return this; } nodeRefresh(node) { if (this.edit) { let ln, p = this.$.points[node]; (ln = this.$.lines[node - 1]) && attr(ln, { x2: p.x, y2: p.y }); (ln = this.$.lines[node]) && attr(ln, { x1: p.x, y1: p.y }); arrow(this.$, node); // partial-refresh } else { this.refresh(); } if (!(node == 0 || node == this.last)) { let bond = this.container.nodeBonds(this, node), p = this.$.points[node]; bond && bond.to.forEach(b => { var _a; (_a = this.container.get(b.id)) === null || _a === void 0 ? void 0 : _a.setNode(b.ndx, p); }); } return this; } setNode(node, p) { this.$.points[node].x = p.x | 0; this.$.points[node].y = p.y | 0; (node == 0) && moveToStart(this.$); this.refreshHighlight(node); return this.nodeRefresh(node); } translate(dx, dy) { super.translate(dx, dy); //don't translate bonded end points because it should have been|will be moved by bonded EC or Wire let savededit = this.edit; this.edit = false; for (let i = 0, p = this.$.points[i], end = this.last; i <= end; p = this.$.points[++i]) { //avoid circular reference, bonded start/end nodes are refresed by EC's nodes if ((i > 0 && i < end) || ((i == 0 || i == end) && !this.container.nodeBonds(this, i))) { this.setNode(i, Point.translateBy(p, dx, dy)); } } this.edit = savededit; arrow(this.$); // full-refresh return this; } /** * @description returns true if a point is valid * @comment later see how to change this to validNode, conflict in !ic.valid(node) * because we don't know if it's a IC or a wire * @param {number} node 0-based point index it can be -1 * @returns {boolean} true if point is valid */ valid(node) { //(i) => ((i = i | 0) >= 0 && i < points.length); //return (node = <any>node | 0) >= -1 && node < this.points.length; // NOW ACCEPTS -1 // -1 0 ... last -> true // "-1" "0" ... "last" -> true // "" " " "1." "1a" -> false return node >= 0 //-1 //String(Number(node)) == node && node <= this.last; // NOW ACCEPTS -1 } /** * @description appends a new node at the end, only works in edit mode, creating a wire * @param p new point */ append(p) { //only works in edit mode = false, so far return !this.edit && (this.$.points.push(p), this.refresh(), true); } highlightable(node) { //any Wire node and that it is not a start|end bonded node return !((node == 0 || node == this.last) && this.container.nodeBonds(this, node)); } setPoints(points) { if (!isArr(points) || points.length < 2) throw new Error('wire min 2 points'); //cleanup this.g.innerHTML = ""; this.$.points = points.map(p => new Point(p.x | 0, p.y | 0)); this.$.dir && (this.$.arrow = poly(this.g, "arrow", -1)); moveToStart(this.$); if (this.edit) { this.$.poly = void 0; setlines(this, this.$); } else { this.$.lines = []; this.$.poly = poly(this.g); this.refresh(); } return this; } /** * @description returns the node information * @param node 0-based pin/node number * @param onlyPoint it's discarded * * this returns absolute (x, y) position */ node(node) { let p = this.$.points[node]; return p && { x: p.x, y: p.y, label: `node::${node}` }; } /** * @description detects a point over a node * @param p point to check for component node * @param ln 1-based line number, ln undefined or 0, checks the whole wire, otherwise just check this line */ over(p, ln) { let inside = (np) => (Math.pow(p.x - np.x, 2) + Math.pow(p.y - np.y, 2)) <= Wire.nodeArea; if (ln) { //the fast way //lines are 1-based let p0 = this.$.points[ln - 1], p1 = this.$.points[ln]; return (!p0 || !p1) ? -1 : inside(p0) ? ln - 1 : inside(p1) ? ln : -1; } else { //the long way for (let i = 0, np = this.$.points[i], len = this.$.points.length; i < len; np = this.$.points[++i]) { //radius 5 => 5^2 = 25 if (inside(np)) return i; } return -1; } } deleteLine(line) { //cannot delete first or last line if (line <= 1 || line >= this.last) return false; deleteWireNode(this, this.$, line); deleteWireNode(this, this.$, line - 1); this.refresh(); this.highlight(false); return true; } deleteNode(node) { let p = deleteWireNode(this, this.$, node); this.refresh(); this.highlight(false); return p; } insertNode(node, p) { //cannot insert node in first or after last position if (node <= 0 || node > this.last || isNaN(node)) return false; //fix all bonds link indexes from last to this node for (let n = this.last; n >= node; n--) { this.container.moveBond(this.id, n, n + 1); } this.$.points.splice(node, 0, p); if (this.edit) { let newline = line(0, Point.origin, Point.origin); this.g.insertBefore(newline, this.$.lines[0]); //this's for ARROW next to last this.$.lines.unshift(newline); } this.refresh(); this.highlight(false); return true; } /** * @description standarizes a wire node number to 0..points.length * @param {number} node 0-based can be -1:last 0..points.length-1 * @returns {number} -1 for wrong node or standarized node number, where -1 == last, otherwise node */ standarizeNode(node) { if (this.valid(node)) return node == -1 ? this.last : node; return -1; } defaults() { return extend(super.defaults(), { name: "wire", class: "wire", edit: false, headLength: 14, headAngle: 0.78 }); } } Wire.nodeArea = 25; function moveToStart($) { $.x = $.points[0].x; $.y = $.points[0].y; } function deleteWireNode(wire, $, node) { let last = wire.last; //first or last node cannot be deleted, only middle nodes if (node <= 0 || node >= last || isNaN(node)) return; wire.container.unbondNode(wire, node); wire.container.moveBond(wire.id, last, last - 1); let p = $.points.splice(node, 1)[0]; if (wire.edit) { wire.g.removeChild($.lines[0]); //use shift for ARROW next to last line $.lines.shift(); } return p; } function line(ln, a, b, arrow) { let options = { line: ln, x1: a.x, y1: a.y, x2: b.x, y2: b.y }; return !arrow && (options["svg-type"] = "line"), tag("line", "", options); } function setlines(w, $) { $.lines = []; for (let i = 0, a = $.points[0], last = w.last; i < last; i++) { let b = $.points[i + 1], ln = line(i + 1, a, b); $.lines.push(ln); w.g.append(ln); a = b; } arrow($); } function poly(g, type, line) { let polyline = tag("polyline", "", { "svg-type": type || "line", line: line || "0", points: "", }); return g.append(polyline), polyline; } /** * * @param $ wire internal data * @param node 0-based node to be refreshed. * * node == undefined, then draw arrow if wire is directional * node != undefined, only draw arrow if node is prev|last node for a directional wire */ function arrow($, node) { if (!$.dir) return; let c = $.points.length - 1, last = $.points[c], prev = $.points[c - 1], r = $.headLength, angle = Math.atan2(last.y - prev.y, last.x - prev.x), swipe = $.headAngle, p = (ang) => new Point((last.x - r * Math.cos(ang)) | 0, (last.y - r * Math.sin(ang)) | 0); //if node is defined, only redraw arrow when node is prev|last node of wire if (node != undefined && !(node == c || node == c - 1)) return; attr($.arrow, { points: [p(angle - swipe), last, p(angle + swipe)].map(p => `${p.x}, ${p.y}`).join(' ') }); }