├── watch.bat ├── .gitignore ├── assets ├── icon_grab.png ├── icon_wire.png ├── icon_battery.png ├── icon_ground.png ├── icon_inductor.png ├── icon_resistor.png ├── icon_capacitor.png ├── icon_currentsource.png └── icon_voltagesource.png ├── run_local_server.bat ├── webpack.config.js ├── package.json ├── src ├── componentWire.js ├── math.js ├── componentGround.js ├── main.js ├── componentBattery.js ├── componentCurrentSource.js ├── uiToolbar.js ├── componentResistor.js ├── uiEditBox.js ├── componentVoltageSource.js ├── component.js ├── componentCapacitor.js ├── componentInductor.js ├── circuitSolver.js ├── matrix.js ├── componentSingleEnded.js ├── componentDoubleEnded.js └── circuitEditor.js └── index.html /watch.bat: -------------------------------------------------------------------------------- 1 | npm run watch -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /webpack/ 3 | package-lock.json -------------------------------------------------------------------------------- /assets/icon_grab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_grab.png -------------------------------------------------------------------------------- /assets/icon_wire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_wire.png -------------------------------------------------------------------------------- /assets/icon_battery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_battery.png -------------------------------------------------------------------------------- /assets/icon_ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_ground.png -------------------------------------------------------------------------------- /assets/icon_inductor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_inductor.png -------------------------------------------------------------------------------- /assets/icon_resistor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_resistor.png -------------------------------------------------------------------------------- /assets/icon_capacitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_capacitor.png -------------------------------------------------------------------------------- /assets/icon_currentsource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_currentsource.png -------------------------------------------------------------------------------- /assets/icon_voltagesource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlorenzi/circuitsim/HEAD/assets/icon_voltagesource.png -------------------------------------------------------------------------------- /run_local_server.bat: -------------------------------------------------------------------------------- 1 | where /q http-server 2 | 3 | IF ERRORLEVEL 1 (npm i -g http-server) 4 | 5 | http-server -p 80 -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | module.exports = 4 | { 5 | mode: "production", 6 | entry: 7 | { 8 | main: path.resolve(__dirname, "src/main.js"), 9 | }, 10 | 11 | output: 12 | { 13 | filename: "[name].js", 14 | path: path.resolve(__dirname, "webpack") 15 | }, 16 | 17 | module: 18 | { 19 | rules: 20 | [ 21 | { 22 | test: /\.(js|jsx)$/, 23 | exclude: /node_modules/, 24 | use: 25 | { 26 | loader: "babel-loader", 27 | options: { 28 | presets: ["@babel/preset-env", "@babel/preset-react"] 29 | } 30 | } 31 | } 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "circuitsim", 3 | "version": "0.1.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack", 8 | "watch": "webpack --watch --mode development", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hlorenzi/circuitsim.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/hlorenzi/circuitsim/issues" 17 | }, 18 | "homepage": "https://github.com/hlorenzi/circuitsim#readme", 19 | "browserslist": [ 20 | "> 0.2%", 21 | "not dead", 22 | "not ie 11" 23 | ], 24 | "dependencies": { 25 | "@babel/cli": "^7.5.0", 26 | "@babel/core": "^7.5.4", 27 | "@babel/preset-env": "^7.5.5", 28 | "@babel/preset-react": "^7.0.0", 29 | "babel-loader": "^8.0.6", 30 | "core-js": "^3.1.4", 31 | "react": "^16.8.6", 32 | "react-dom": "^16.8.6", 33 | "regenerator-runtime": "^0.13.3", 34 | "webpack": "^4.35.3", 35 | "webpack-cli": "^3.3.5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/componentWire.js: -------------------------------------------------------------------------------- 1 | import { ComponentDoubleEnded } from "./componentDoubleEnded.js" 2 | 3 | 4 | export class ComponentWire extends ComponentDoubleEnded 5 | { 6 | constructor(p) 7 | { 8 | super(p) 9 | 10 | this.isVoltageSource = true 11 | } 12 | 13 | 14 | static getSaveId() 15 | { 16 | return "w" 17 | } 18 | 19 | 20 | static getName() 21 | { 22 | return "Wire" 23 | } 24 | 25 | 26 | saveToString(manager) 27 | { 28 | return this.joints[0] + "," + this.joints[1] + "," 29 | } 30 | 31 | 32 | solverBegin(manager, solver) 33 | { 34 | solver.stampVoltage(this.voltageSourceIndex, this.nodes[0], this.nodes[1], 0) 35 | } 36 | 37 | 38 | solverIterationEnd(manager, solver) 39 | { 40 | this.current = -manager.getVoltageSourceCurrent(this.voltageSourceIndex) 41 | } 42 | 43 | 44 | render(manager, ctx) 45 | { 46 | ctx.save() 47 | 48 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])) 49 | 50 | ctx.beginPath() 51 | ctx.arc(this.points[0].x, this.points[0].y, 2, 0, Math.PI * 2) 52 | ctx.moveTo(this.points[0].x, this.points[0].y) 53 | ctx.lineTo(this.points[1].x, this.points[1].y) 54 | ctx.arc(this.points[1].x, this.points[1].y, 2, 0, Math.PI * 2) 55 | ctx.stroke() 56 | 57 | ctx.restore() 58 | } 59 | } -------------------------------------------------------------------------------- /src/math.js: -------------------------------------------------------------------------------- 1 | const multiplierPrefixes = ["k", "M", "G", "T", "P", "E", "Z", "Y"] 2 | const dividerPrefixes = ["m", "u", "n", "p", "f", "a", "z", "y"] 3 | 4 | 5 | export function valueToStringWithUnitPrefix(x, separator = "") 6 | { 7 | if (x == 0) 8 | return x.toString() 9 | 10 | const sign = (x > 0 ? 1 : -1) 11 | let xAbs = Math.abs(x) 12 | 13 | if (xAbs >= 1000) 14 | { 15 | let prefixIndex = -1 16 | while (xAbs >= 1000 && prefixIndex + 1 < multiplierPrefixes.length) 17 | { 18 | xAbs /= 1000 19 | prefixIndex += 1 20 | } 21 | 22 | xAbs = Math.round(xAbs * 1000) / 1000 23 | 24 | return (xAbs * sign).toString() + separator + multiplierPrefixes[prefixIndex] 25 | } 26 | 27 | else if (xAbs < 1) 28 | { 29 | let prefixIndex = -1 30 | while (xAbs < 1 && prefixIndex + 1 < dividerPrefixes.length) 31 | { 32 | xAbs *= 1000 33 | prefixIndex += 1 34 | } 35 | 36 | xAbs = Math.round(xAbs * 1000) / 1000 37 | 38 | return (xAbs * sign).toString() + separator + dividerPrefixes[prefixIndex] 39 | } 40 | 41 | else 42 | return x.toString() + separator 43 | } 44 | 45 | 46 | export function stringWithUnitPrefixToValue(str) 47 | { 48 | str = str.trim() 49 | 50 | for (let i = 0; i < multiplierPrefixes.length; i++) 51 | { 52 | if (str.endsWith(multiplierPrefixes[i])) 53 | return parseFloat(str.substr(0, str.length - 1)) * Math.pow(1000, i + 1) 54 | } 55 | 56 | for (let i = 0; i < dividerPrefixes.length; i++) 57 | { 58 | if (str.endsWith(dividerPrefixes[i])) 59 | return parseFloat(str.substr(0, str.length - 1)) * Math.pow(1000, -i - 1) 60 | } 61 | 62 | return parseFloat(str) 63 | } -------------------------------------------------------------------------------- /src/componentGround.js: -------------------------------------------------------------------------------- 1 | import { ComponentSingleEnded } from "./componentSingleEnded.js" 2 | 3 | 4 | export class ComponentGround extends ComponentSingleEnded 5 | { 6 | constructor(pos) 7 | { 8 | super(pos) 9 | 10 | this.isVoltageSource = true 11 | } 12 | 13 | 14 | static getSaveId() 15 | { 16 | return "g" 17 | } 18 | 19 | 20 | static getName() 21 | { 22 | return "Ground" 23 | } 24 | 25 | 26 | saveToString(manager) 27 | { 28 | return this.joints[0] + "," + this.joints[1] + "," 29 | } 30 | 31 | 32 | solverBegin(manager, solver) 33 | { 34 | solver.stampVoltage(this.voltageSourceIndex, this.nodes[0], 0, 0) 35 | } 36 | 37 | 38 | solverIterationEnd(manager, solver) 39 | { 40 | this.current = -manager.getVoltageSourceCurrent(this.voltageSourceIndex) 41 | } 42 | 43 | 44 | render(manager, ctx) 45 | { 46 | const symbolSize = Math.min(20, this.getLength()) 47 | const smallStrokeSize = 2 48 | const mediumStrokeSize = 10 49 | const bigStrokeSize = 20 50 | 51 | this.drawSymbolBegin(manager, ctx, symbolSize) 52 | 53 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])) 54 | 55 | ctx.beginPath() 56 | ctx.moveTo(-symbolSize / 2, -smallStrokeSize) 57 | ctx.lineTo(-symbolSize / 2, smallStrokeSize) 58 | ctx.stroke() 59 | 60 | ctx.beginPath() 61 | ctx.moveTo(0, -mediumStrokeSize) 62 | ctx.lineTo(0, mediumStrokeSize) 63 | ctx.stroke() 64 | 65 | ctx.beginPath() 66 | ctx.moveTo(symbolSize / 2, -bigStrokeSize) 67 | ctx.lineTo(symbolSize / 2, bigStrokeSize) 68 | ctx.stroke() 69 | 70 | this.drawSymbolEnd(manager, ctx) 71 | } 72 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import "core-js" 2 | import "regenerator-runtime/runtime" 3 | 4 | 5 | import React from "react" 6 | import ReactDOM from "react-dom" 7 | import { CircuitEditor } from "./circuitEditor.js" 8 | import { UIToolbar } from "./uiToolbar.js" 9 | import { UIEditBox } from "./uiEditBox.js" 10 | 11 | 12 | let gEditor = null 13 | 14 | 15 | document.body.onload = function() 16 | { 17 | gEditor = new CircuitEditor(document.getElementById("canvasMain")) 18 | gEditor.run() 19 | 20 | ReactDOM.render(, document.getElementById("divToolbox")) 21 | 22 | gEditor.refreshUI = refreshUI 23 | 24 | onResize() 25 | document.body.onresize = (ev) => onResize() 26 | 27 | const urlData = getURLQueryParameter("circuit") 28 | if (urlData != null) 29 | gEditor.loadFromString(urlData) 30 | } 31 | 32 | 33 | function refreshUI() 34 | { 35 | ReactDOM.render( 36 | { gEditor.refreshSolver(); refreshUI() } } 39 | onDelete={ () => { gEditor.removeComponentsForEditing(); refreshUI() } } 40 | />, 41 | document.getElementById("divFloatingEditBox")) 42 | } 43 | 44 | 45 | function onResize() 46 | { 47 | const divEditor = document.getElementById("divEditor") 48 | const rect = divEditor.getBoundingClientRect() 49 | gEditor.resize(Math.floor(rect.width), Math.floor(rect.height)) 50 | } 51 | 52 | 53 | function saveToURL() 54 | { 55 | const url = [location.protocol, "//", location.host, location.pathname].join("") 56 | window.location = url + "?circuit=" + gEditor.saveToString() 57 | } 58 | 59 | 60 | function getURLQueryParameter(name) 61 | { 62 | const url = window.location.search 63 | 64 | name = name.replace(/[\[\]]/g, "\\$&") 65 | 66 | const regex = new RegExp("[?&]" + name + "(=([^]*)|&|#|$)") 67 | const results = regex.exec(url) 68 | 69 | if (!results) 70 | return null 71 | 72 | if (!results[2]) 73 | return "" 74 | 75 | return decodeURIComponent(results[2].replace(/\+/g, " ")) 76 | } -------------------------------------------------------------------------------- /src/componentBattery.js: -------------------------------------------------------------------------------- 1 | import { ComponentDoubleEnded } from "./componentDoubleEnded.js" 2 | import * as MathUtils from "./math.js" 3 | 4 | 5 | export class ComponentBattery extends ComponentDoubleEnded 6 | { 7 | constructor(pos) 8 | { 9 | super(pos) 10 | 11 | this.voltage = 5 12 | this.isVoltageSource = true 13 | } 14 | 15 | 16 | static getSaveId() 17 | { 18 | return "v" 19 | } 20 | 21 | 22 | static getName() 23 | { 24 | return "Battery" 25 | } 26 | 27 | 28 | saveToString(manager) 29 | { 30 | return this.joints[0] + "," + this.joints[1] + "," + MathUtils.valueToStringWithUnitPrefix(this.voltage) + "," 31 | } 32 | 33 | 34 | loadFromString(manager, loadData, reader) 35 | { 36 | super.loadFromString(manager, loadData, reader) 37 | this.voltage = reader.readNumber() 38 | } 39 | 40 | 41 | solverBegin(manager, solver) 42 | { 43 | solver.stampVoltage(this.voltageSourceIndex, this.nodes[0], this.nodes[1], this.voltage) 44 | } 45 | 46 | 47 | solverIterationEnd(manager, solver) 48 | { 49 | this.current = -manager.getVoltageSourceCurrent(this.voltageSourceIndex) 50 | } 51 | 52 | 53 | getEditBox(editBoxDef) 54 | { 55 | editBoxDef.addNumberInput("Voltage", "V", this.voltage, (x) => { this.voltage = x }) 56 | } 57 | 58 | 59 | render(manager, ctx) 60 | { 61 | const symbolSize = Math.min(15, this.getLength()) 62 | const smallPlateSize = 12.5 63 | const bigPlateSize = 25 64 | 65 | this.drawSymbolBegin(manager, ctx, symbolSize) 66 | 67 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])) 68 | ctx.beginPath() 69 | ctx.moveTo(-symbolSize / 2, -smallPlateSize) 70 | ctx.lineTo(-symbolSize / 2, smallPlateSize) 71 | ctx.stroke() 72 | 73 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[1])) 74 | ctx.beginPath() 75 | ctx.moveTo( symbolSize / 2, -bigPlateSize) 76 | ctx.lineTo( symbolSize / 2, bigPlateSize) 77 | ctx.stroke() 78 | 79 | this.drawSymbolEnd(manager, ctx) 80 | this.drawRatingText(manager, ctx, this.voltage, "V") 81 | } 82 | } -------------------------------------------------------------------------------- /src/componentCurrentSource.js: -------------------------------------------------------------------------------- 1 | import { ComponentDoubleEnded } from "./componentDoubleEnded.js" 2 | import * as MathUtils from "./math.js" 3 | 4 | 5 | export class ComponentCurrentSource extends ComponentDoubleEnded 6 | { 7 | constructor(pos) 8 | { 9 | super(pos) 10 | 11 | this.currentSetting = 0.01 12 | } 13 | 14 | 15 | static getSaveId() 16 | { 17 | return "i" 18 | } 19 | 20 | 21 | static getName() 22 | { 23 | return "Current Source" 24 | } 25 | 26 | 27 | saveToString(manager) 28 | { 29 | return this.joints[0] + "," + this.joints[1] + "," + MathUtils.valueToStringWithUnitPrefix(this.currentSetting) + "," 30 | } 31 | 32 | 33 | loadFromString(manager, loadData, reader) 34 | { 35 | super.loadFromString(manager, loadData, reader) 36 | this.currentSetting = reader.readNumber() 37 | } 38 | 39 | 40 | solverBegin(manager, solver) 41 | { 42 | solver.stampCurrentSource(this.nodes[0], this.nodes[1], this.currentSetting) 43 | } 44 | 45 | 46 | solverIterationEnd(manager, solver) 47 | { 48 | this.current = this.currentSetting 49 | } 50 | 51 | 52 | getEditBox(editBoxDef) 53 | { 54 | editBoxDef.addNumberInput("Current", "A", this.currentSetting, (x) => { this.currentSetting = x }) 55 | } 56 | 57 | 58 | render(manager, ctx) 59 | { 60 | const symbolSize = Math.min(50, this.getLength()) 61 | 62 | this.drawSymbolBegin(manager, ctx, symbolSize) 63 | this.drawSymbolSetGradient(manager, ctx, symbolSize, 64 | manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])), 65 | manager.getVoltageColor(manager.getNodeVoltage(this.nodes[1]))) 66 | 67 | const centerX = (this.nodes[0].x + this.nodes[1].x) / 2 68 | const centerY = (this.nodes[0].y + this.nodes[1].y) / 2 69 | 70 | ctx.beginPath() 71 | ctx.arc(0, 0, symbolSize / 2, 0, Math.PI * 2) 72 | ctx.stroke() 73 | 74 | ctx.beginPath() 75 | ctx.moveTo(-symbolSize * 0.3, 0) 76 | ctx.lineTo( symbolSize * 0.2, 0) 77 | ctx.stroke() 78 | 79 | ctx.beginPath() 80 | ctx.moveTo(symbolSize * 0.4, 0) 81 | ctx.lineTo(symbolSize * 0.2, -symbolSize * 0.2) 82 | ctx.lineTo(symbolSize * 0.2, symbolSize * 0.2) 83 | ctx.lineTo(symbolSize * 0.4, 0) 84 | ctx.fill() 85 | 86 | this.drawSymbolEnd(manager, ctx) 87 | this.drawRatingText(manager, ctx, this.current, "A") 88 | } 89 | } -------------------------------------------------------------------------------- /src/uiToolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { ComponentWire } from "./componentWire.js" 3 | import { ComponentBattery } from "./componentBattery.js" 4 | import { ComponentResistor } from "./componentResistor.js" 5 | import { ComponentCurrentSource } from "./componentCurrentSource.js" 6 | import { ComponentCapacitor } from "./componentCapacitor.js" 7 | import { ComponentInductor } from "./componentInductor.js" 8 | import { ComponentVoltageSource } from "./componentVoltageSource.js" 9 | import { ComponentGround } from "./componentGround.js" 10 | 11 | 12 | export class UIToolbar extends React.Component 13 | { 14 | constructor(props) 15 | { 16 | super(props) 17 | this.state = { currentComponent: null } 18 | } 19 | 20 | 21 | render() 22 | { 23 | return 24 | { this.makeMenuItem("Save to URL", this.props.saveToURL) } 25 | 26 | { this.makeToolButton("assets/icon_grab.png", null) } 27 | { this.makeToolButton("assets/icon_wire.png", ComponentWire) } 28 | { this.makeToolButton("assets/icon_battery.png", ComponentBattery) } 29 | { this.makeToolButton("assets/icon_resistor.png", ComponentResistor) } 30 | { this.makeToolButton("assets/icon_currentsource.png", ComponentCurrentSource) } 31 | { this.makeToolButton("assets/icon_capacitor.png", ComponentCapacitor) } 32 | { this.makeToolButton("assets/icon_inductor.png", ComponentInductor) } 33 | { this.makeToolButton("assets/icon_voltagesource.png", ComponentVoltageSource) } 34 | { this.makeToolButton("assets/icon_ground.png", ComponentGround) } 35 | 36 | } 37 | 38 | 39 | makeMenuItem(text, onClick) 40 | { 41 | return { text } 42 | } 43 | 44 | 45 | makeToolButton(iconSrc, componentClass) 46 | { 47 | const onClick = () => 48 | { 49 | this.props.editor.mouseAddComponentClass = componentClass 50 | this.setState({ currentComponent: componentClass }) 51 | } 52 | 53 | const isCurrent = (this.state.currentComponent == componentClass) 54 | 55 | return 60 | 61 | 62 | } 63 | } -------------------------------------------------------------------------------- /src/componentResistor.js: -------------------------------------------------------------------------------- 1 | import { ComponentDoubleEnded } from "./componentDoubleEnded.js" 2 | import * as MathUtils from "./math.js" 3 | 4 | 5 | export class ComponentResistor extends ComponentDoubleEnded 6 | { 7 | constructor(pos) 8 | { 9 | super(pos) 10 | 11 | this.resistance = 1000 12 | } 13 | 14 | 15 | static getSaveId() 16 | { 17 | return "r" 18 | } 19 | 20 | 21 | static getName() 22 | { 23 | return "Resistor" 24 | } 25 | 26 | 27 | saveToString(manager) 28 | { 29 | return this.joints[0] + "," + this.joints[1] + "," + MathUtils.valueToStringWithUnitPrefix(this.resistance) + "," 30 | } 31 | 32 | 33 | loadFromString(manager, loadData, reader) 34 | { 35 | super.loadFromString(manager, loadData, reader) 36 | this.resistance = reader.readNumber() 37 | } 38 | 39 | 40 | solverBegin(manager, solver) 41 | { 42 | solver.stampResistance(this.nodes[0], this.nodes[1], this.resistance) 43 | } 44 | 45 | 46 | solverIterationEnd(manager, solver) 47 | { 48 | const v0 = manager.getNodeVoltage(this.nodes[0]) 49 | const v1 = manager.getNodeVoltage(this.nodes[1]) 50 | this.current = (v0 - v1) / this.resistance 51 | } 52 | 53 | 54 | getEditBox(editBoxDef) 55 | { 56 | editBoxDef.addNumberInput("Resistance", "Ω", this.resistance, (x) => { this.resistance = x }) 57 | } 58 | 59 | 60 | render(manager, ctx) 61 | { 62 | const symbolSize = Math.min(75, this.getLength()) 63 | const symbolAmplitude = 12.5 64 | const symbolSegments = 9 65 | const symbolSegmentSize = symbolSize / symbolSegments 66 | 67 | this.drawSymbolBegin(manager, ctx, symbolSize) 68 | this.drawSymbolSetGradient(manager, ctx, symbolSize, 69 | manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])), 70 | manager.getVoltageColor(manager.getNodeVoltage(this.nodes[1]))) 71 | 72 | ctx.beginPath() 73 | ctx.moveTo(-symbolSize / 2, 0) 74 | ctx.lineTo(-symbolSize / 2 + symbolSegmentSize / 2, 0) 75 | 76 | let segmentX = -symbolSize / 2 77 | let segmentSide = 1 78 | for (let i = 0; i < symbolSegments - 1; i++) 79 | { 80 | segmentX += symbolSegmentSize 81 | segmentSide *= -1 82 | ctx.lineTo(segmentX, symbolAmplitude * segmentSide) 83 | } 84 | 85 | ctx.lineTo(symbolSize / 2 - symbolSegmentSize / 2, 0) 86 | ctx.lineTo(symbolSize / 2, 0) 87 | ctx.stroke() 88 | 89 | this.drawSymbolEnd(manager, ctx) 90 | this.drawRatingText(manager, ctx, this.resistance, "Ω", 25) 91 | } 92 | } -------------------------------------------------------------------------------- /src/uiEditBox.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { ComponentWire } from "./componentWire.js" 3 | import * as MathUtils from "./math.js" 4 | 5 | 6 | function EditBoxContents(props) 7 | { 8 | let elems = [] 9 | 10 | const [state, setState] = React.useState({}) 11 | 12 | React.useEffect(() => setState({}), [props.componentToEdit]) 13 | 14 | const editBoxDef = 15 | { 16 | addNumberInput: (valueLabel, unitLabel, value, setValue) => 17 | { 18 | const key = elems.length.toString() 19 | const str = state[key] === undefined ? MathUtils.valueToStringWithUnitPrefix(value) : state[key] 20 | const setStr = (x) => setState({ ...state, [key]: x }) 21 | 22 | const onChange = (ev) => 23 | { 24 | setStr(ev.target.value) 25 | 26 | const val = MathUtils.stringWithUnitPrefixToValue(ev.target.value) 27 | if (isNaN(val) || !isFinite(val)) 28 | return 29 | 30 | setValue(val) 31 | props.onChange() 32 | } 33 | 34 | elems.push( 35 | 36 | 37 | 44 | { valueLabel } 45 | 46 | 47 | ev.stopPropagation() } 52 | onFocus={ (ev) => ev.target.setSelectionRange(0, ev.target.value.length) } 53 | /> 54 | 55 | { unitLabel } 56 | 57 | 58 | ) 59 | } 60 | } 61 | 62 | props.componentToEdit.getEditBox(editBoxDef) 63 | 64 | return 65 | { elems.length == 0 ? null : } 66 | { elems } 67 | 68 | } 69 | 70 | 71 | export function UIEditBox(props) 72 | { 73 | if (props.editor.componentsForEditing.length != 1) 74 | return null 75 | 76 | const componentToEdit = props.editor.componentsForEditing[0] 77 | const componentBBox = componentToEdit.getBBox() 78 | 79 | const absolutePos = props.editor.getAbsolutePosition({ x: componentBBox.xMax + 10, y: (componentBBox.yMin + componentBBox.yMax) / 2 }) 80 | 81 | return ( 82 | 83 | 89 | ev.preventDefault() } style={{ 90 | position: "relative", 91 | top: "50%", 92 | transform: "translate(0, -50%)", 93 | }}> 94 | 95 | 96 | { componentToEdit.constructor.getName() } 97 | 98 | 🗑️ 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | ) 107 | } -------------------------------------------------------------------------------- /src/componentVoltageSource.js: -------------------------------------------------------------------------------- 1 | import { ComponentDoubleEnded } from "./componentDoubleEnded.js" 2 | import * as MathUtils from "./math.js" 3 | 4 | 5 | export class ComponentVoltageSource extends ComponentDoubleEnded 6 | { 7 | constructor(pos) 8 | { 9 | super(pos) 10 | 11 | this.voltage = 5 12 | this.isVoltageSource = true 13 | 14 | this.dcBias = 0 15 | this.frequency = 60 16 | this.amplitude = 5 17 | this.phaseOffset = 0 18 | } 19 | 20 | 21 | static getSaveId() 22 | { 23 | return "vs" 24 | } 25 | 26 | 27 | static getName() 28 | { 29 | return "Voltage Source" 30 | } 31 | 32 | 33 | saveToString(manager) 34 | { 35 | return this.joints[0] + "," + this.joints[1] + ",0," + 36 | MathUtils.valueToStringWithUnitPrefix(this.dcBias) + "," + 37 | MathUtils.valueToStringWithUnitPrefix(this.frequency) + "," + 38 | MathUtils.valueToStringWithUnitPrefix(this.amplitude) + "," + 39 | MathUtils.valueToStringWithUnitPrefix(this.phaseOffset) + "," 40 | } 41 | 42 | 43 | loadFromString(manager, loadData, reader) 44 | { 45 | super.loadFromString(manager, loadData, reader) 46 | const version = parseInt(reader.read()) 47 | this.dcBias = reader.readNumber() 48 | this.frequency = reader.readNumber() 49 | this.amplitude = reader.readNumber() 50 | this.phaseOffset = reader.readNumber() 51 | } 52 | 53 | 54 | calculateVoltage(manager) 55 | { 56 | return this.dcBias + Math.sin((this.phaseOffset / 180 * Math.PI) + (manager.time * Math.PI * 2 * this.frequency)) * this.amplitude 57 | } 58 | 59 | 60 | solverBegin(manager, solver) 61 | { 62 | solver.stampVoltage(this.voltageSourceIndex, this.nodes[0], this.nodes[1], this.calculateVoltage(manager)) 63 | } 64 | 65 | 66 | solverIterationBegin(manager, solver) 67 | { 68 | solver.stampVoltage(this.voltageSourceIndex, this.nodes[0], this.nodes[1], this.calculateVoltage(manager)) 69 | } 70 | 71 | 72 | solverIterationEnd(manager, solver) 73 | { 74 | this.current = -manager.getVoltageSourceCurrent(this.voltageSourceIndex) 75 | } 76 | 77 | 78 | getEditBox(editBoxDef) 79 | { 80 | editBoxDef.addNumberInput("Amplitude", "V", this.amplitude, (x) => { this.amplitude = x }) 81 | editBoxDef.addNumberInput("DC Bias", "V", this.dcBias, (x) => { this.dcBias = x }) 82 | editBoxDef.addNumberInput("Frequency", "Hz", this.frequency, (x) => { this.frequency = x }) 83 | editBoxDef.addNumberInput("Phase Offset", "deg", this.phaseOffset, (x) => { this.phaseOffset = x }) 84 | } 85 | 86 | 87 | render(manager, ctx) 88 | { 89 | const symbolSize = Math.min(50, this.getLength()) 90 | 91 | const centerX = (this.points[0].x + this.points[1].x) / 2 92 | const centerY = (this.points[0].y + this.points[1].y) / 2 93 | 94 | this.drawSymbolBegin(manager, ctx, symbolSize) 95 | this.drawSymbolEnd(manager, ctx) 96 | 97 | ctx.save() 98 | ctx.translate(centerX, centerY) 99 | 100 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[1])) 101 | 102 | ctx.beginPath() 103 | ctx.arc(0, 0, symbolSize / 2, 0, Math.PI * 2) 104 | ctx.stroke() 105 | 106 | ctx.beginPath() 107 | ctx.moveTo (-symbolSize * 0.3, 0) 108 | ctx.quadraticCurveTo(-symbolSize * 0.15, -symbolSize * 0.3, 0, 0) 109 | ctx.quadraticCurveTo( symbolSize * 0.15, symbolSize * 0.3, symbolSize * 0.3, 0) 110 | ctx.stroke() 111 | 112 | ctx.restore() 113 | } 114 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Circuit Simulator 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /src/component.js: -------------------------------------------------------------------------------- 1 | export class Component 2 | { 3 | constructor(p) 4 | { 5 | this.points = [] 6 | this.nodes = [] 7 | this.selected = [] 8 | this.dragOrigin = [] 9 | 10 | this.isVoltageSource = false 11 | this.voltageSourceIndex = -1 12 | } 13 | 14 | 15 | static getSaveId() 16 | { 17 | return "-" 18 | } 19 | 20 | 21 | static getName() 22 | { 23 | return "" 24 | } 25 | 26 | 27 | saveToString(manager) 28 | { 29 | return "" 30 | } 31 | 32 | 33 | loadFromString(manager, loadData, reader) 34 | { 35 | 36 | } 37 | 38 | 39 | reset(manager) 40 | { 41 | 42 | } 43 | 44 | 45 | updateCurrentAnim(manager, mult) 46 | { 47 | 48 | } 49 | 50 | 51 | solverBegin(manager, solver) 52 | { 53 | 54 | } 55 | 56 | 57 | solverFrameBegin(manager, solver) 58 | { 59 | 60 | } 61 | 62 | 63 | solverIterationBegin(manager, solver) 64 | { 65 | 66 | } 67 | 68 | 69 | solverIterationEnd(manager, solver) 70 | { 71 | 72 | } 73 | 74 | 75 | solverFrameEnd(manager, solver) 76 | { 77 | 78 | } 79 | 80 | 81 | isDegenerate() 82 | { 83 | return true 84 | } 85 | 86 | 87 | isFullySelected() 88 | { 89 | for (let i = 0; i < this.selected.length; i++) 90 | if (!this.selected[i]) 91 | return false 92 | 93 | return true 94 | } 95 | 96 | 97 | isAnySelected() 98 | { 99 | for (let i = 0; i < this.selected.length; i++) 100 | if (this.selected[i]) 101 | return true 102 | 103 | return false 104 | } 105 | 106 | 107 | getOutgoingDirectionFromNode(index) 108 | { 109 | return 0 110 | } 111 | 112 | 113 | getHover(pos) 114 | { 115 | return null 116 | } 117 | 118 | 119 | dragStart() 120 | { 121 | for (let i = 0; i < this.points.length; i++) 122 | this.dragOrigin[i] = this.points[i] 123 | } 124 | 125 | 126 | dragMove(manager, deltaPos) 127 | { 128 | for (let i = 0; i < this.selected.length; i++) 129 | { 130 | if (this.selected[i]) 131 | { 132 | this.points[i] = manager.snapPos({ 133 | x: this.dragOrigin[i].x + deltaPos.x, 134 | y: this.dragOrigin[i].y + deltaPos.y 135 | }) 136 | } 137 | } 138 | } 139 | 140 | 141 | getBBox() 142 | { 143 | let xMin = this.points[0].x 144 | let xMax = this.points[0].x 145 | let yMin = this.points[0].y 146 | let yMax = this.points[0].y 147 | 148 | for (let i = 1; i < this.points.length; i++) 149 | { 150 | xMin = Math.min(xMin, this.points[i].x) 151 | xMax = Math.max(xMax, this.points[i].x) 152 | yMin = Math.min(yMin, this.points[i].y) 153 | yMax = Math.max(yMax, this.points[i].y) 154 | } 155 | 156 | return { xMin, xMax, yMin, yMax } 157 | } 158 | 159 | 160 | getEditBox(editBoxDef) 161 | { 162 | 163 | } 164 | 165 | 166 | render(manager, ctx) 167 | { 168 | 169 | } 170 | 171 | 172 | renderCurrent(manager, ctx) 173 | { 174 | 175 | } 176 | 177 | 178 | renderHover(manager, ctx, hoverData) 179 | { 180 | 181 | } 182 | 183 | 184 | renderSelection(manager, ctx) 185 | { 186 | 187 | } 188 | 189 | 190 | renderEditing(manager, ctx) 191 | { 192 | 193 | } 194 | 195 | 196 | drawCurrent(manager, ctx, anim, p1, p2) 197 | { 198 | if (manager.debugDrawClean) 199 | return 200 | 201 | ctx.save() 202 | 203 | ctx.lineWidth = 8 204 | ctx.lineCap = "round" 205 | ctx.strokeStyle = "#ff0" 206 | 207 | ctx.lineDashOffset = -47.5 * anim 208 | ctx.setLineDash([2.5, 45]) 209 | 210 | ctx.beginPath() 211 | ctx.moveTo(p1.x, p1.y) 212 | ctx.lineTo(p2.x, p2.y) 213 | ctx.stroke() 214 | 215 | ctx.restore() 216 | } 217 | } -------------------------------------------------------------------------------- /src/componentCapacitor.js: -------------------------------------------------------------------------------- 1 | import { ComponentDoubleEnded } from "./componentDoubleEnded.js" 2 | import * as MathUtils from "./math.js" 3 | 4 | 5 | export class ComponentCapacitor extends ComponentDoubleEnded 6 | { 7 | constructor(pos) 8 | { 9 | super(pos) 10 | 11 | this.capacitance = 1e-6 12 | 13 | this.useTrapezoidalIntegration = true 14 | this.companionModelResistance = 0 15 | this.companionModelCurrent = 0 16 | } 17 | 18 | 19 | static getSaveId() 20 | { 21 | return "c" 22 | } 23 | 24 | 25 | static getName() 26 | { 27 | return "Capacitor" 28 | } 29 | 30 | 31 | saveToString(manager) 32 | { 33 | return this.joints[0] + "," + this.joints[1] + "," + MathUtils.valueToStringWithUnitPrefix(this.capacitance) + "," 34 | } 35 | 36 | 37 | loadFromString(manager, loadData, reader) 38 | { 39 | super.loadFromString(manager, loadData, reader) 40 | this.capacitance = reader.readNumber() 41 | } 42 | 43 | 44 | solverBegin(manager, solver) 45 | { 46 | this.current = 0 47 | this.currentAnim = 0 48 | this.companionModelCurrent = 0 49 | 50 | if (this.useTrapezoidalIntegration) 51 | { 52 | this.companionModelResistance = manager.timePerIteration / (2 * this.capacitance) 53 | solver.stampResistance(this.nodes[0], this.nodes[1], this.companionModelResistance) 54 | //console.log("comp resistance (trapezoidal)", manager.timePerIteration, this.capacitance) 55 | //console.log("comp resistance (trapezoidal)", this.companionModelResistance) 56 | } 57 | else 58 | { 59 | this.companionModelResistance = manager.timePerIteration / this.capacitance 60 | solver.stampResistance(this.nodes[0], this.nodes[1], this.companionModelResistance) 61 | //console.log("comp resistance (back euler)", this.companionModelResistance) 62 | } 63 | } 64 | 65 | 66 | solverIterationBegin(manager, solver) 67 | { 68 | const voltage = manager.getNodeVoltage(this.nodes[0]) - manager.getNodeVoltage(this.nodes[1]) 69 | 70 | if (this.useTrapezoidalIntegration) 71 | { 72 | this.companionModelCurrent = -voltage / this.companionModelResistance - this.current 73 | solver.stampCurrentSource(this.nodes[0], this.nodes[1], this.companionModelCurrent) 74 | //console.log("voltage, comp current (trapezoidal)", voltage, this.companionModelCurrent) 75 | } 76 | else 77 | { 78 | this.companionModelCurrent = -voltage / this.companionModelResistance 79 | solver.stampCurrentSource(this.nodes[0], this.nodes[1], this.companionModelCurrent) 80 | //console.log("voltage, comp current (back euler)", voltage, this.companionModelCurrent) 81 | } 82 | } 83 | 84 | 85 | solverIterationEnd(manager, solver) 86 | { 87 | const voltage = manager.getNodeVoltage(this.nodes[0]) - manager.getNodeVoltage(this.nodes[1]) 88 | 89 | this.current = voltage / this.companionModelResistance + this.companionModelCurrent 90 | } 91 | 92 | 93 | getEditBox(editBoxDef) 94 | { 95 | editBoxDef.addNumberInput("Capacitance", "F", this.capacitance, (x) => { this.capacitance = x }) 96 | } 97 | 98 | 99 | render(manager, ctx) 100 | { 101 | const symbolSize = Math.min(15, this.getLength()) 102 | const plateSize = 25 103 | 104 | this.drawSymbolBegin(manager, ctx, symbolSize) 105 | 106 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])) 107 | ctx.beginPath() 108 | ctx.moveTo(-symbolSize / 2, -plateSize) 109 | ctx.lineTo(-symbolSize / 2, plateSize) 110 | ctx.stroke() 111 | 112 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[1])) 113 | ctx.beginPath() 114 | ctx.moveTo( symbolSize / 2, -plateSize) 115 | ctx.lineTo( symbolSize / 2, plateSize) 116 | ctx.stroke() 117 | 118 | this.drawSymbolEnd(manager, ctx) 119 | this.drawRatingText(manager, ctx, this.capacitance, "F") 120 | } 121 | } -------------------------------------------------------------------------------- /src/componentInductor.js: -------------------------------------------------------------------------------- 1 | import { ComponentDoubleEnded } from "./componentDoubleEnded.js" 2 | import * as MathUtils from "./math.js" 3 | 4 | 5 | export class ComponentInductor extends ComponentDoubleEnded 6 | { 7 | constructor(pos) 8 | { 9 | super(pos) 10 | 11 | this.inductance = 1 12 | 13 | this.useTrapezoidalIntegration = true 14 | this.companionModelResistance = 0 15 | this.companionModelCurrent = 0 16 | } 17 | 18 | 19 | static getSaveId() 20 | { 21 | return "l" 22 | } 23 | 24 | 25 | static getName() 26 | { 27 | return "Inductor" 28 | } 29 | 30 | 31 | saveToString(manager) 32 | { 33 | return this.joints[0] + "," + this.joints[1] + "," + MathUtils.valueToStringWithUnitPrefix(this.inductance) + "," 34 | } 35 | 36 | 37 | loadFromString(manager, loadData, reader) 38 | { 39 | super.loadFromString(manager, loadData, reader) 40 | this.inductance = reader.readNumber() 41 | } 42 | 43 | 44 | solverBegin(manager, solver) 45 | { 46 | this.current = 0 47 | this.currentAnim = 0 48 | this.companionModelCurrent = 0 49 | 50 | if (this.useTrapezoidalIntegration) 51 | { 52 | this.companionModelResistance = (2 * this.inductance) / manager.timePerIteration 53 | solver.stampResistance(this.nodes[0], this.nodes[1], this.companionModelResistance) 54 | //console.log("comp resistance (trapezoidal)", this.companionModelResistance) 55 | } 56 | else 57 | { 58 | this.companionModelResistance = this.inductance / manager.timePerIteration 59 | solver.stampResistance(this.nodes[0], this.nodes[1], this.companionModelResistance) 60 | //console.log("comp resistance (back euler)", this.companionModelResistance) 61 | } 62 | } 63 | 64 | 65 | solverIterationBegin(manager, solver) 66 | { 67 | const voltage = manager.getNodeVoltage(this.nodes[0]) - manager.getNodeVoltage(this.nodes[1]) 68 | 69 | if (this.useTrapezoidalIntegration) 70 | { 71 | this.companionModelCurrent = voltage / this.companionModelResistance + this.current 72 | solver.stampCurrentSource(this.nodes[0], this.nodes[1], this.companionModelCurrent) 73 | //console.log("voltage, comp current (trapezoidal)", voltage, this.companionModelCurrent) 74 | } 75 | else 76 | { 77 | this.companionModelCurrent = this.current 78 | solver.stampCurrentSource(this.nodes[0], this.nodes[1], this.companionModelCurrent) 79 | //console.log("voltage, comp current (back euler)", voltage, this.companionModelCurrent) 80 | } 81 | } 82 | 83 | 84 | solverIterationEnd(manager, solver) 85 | { 86 | const voltage = manager.getNodeVoltage(this.nodes[0]) - manager.getNodeVoltage(this.nodes[1]) 87 | 88 | this.current = voltage / this.companionModelResistance + this.companionModelCurrent 89 | } 90 | 91 | 92 | getEditBox(editBoxDef) 93 | { 94 | editBoxDef.addNumberInput("Inductance", "H", this.inductance, (x) => { this.inductance = x }) 95 | } 96 | 97 | 98 | render(manager, ctx) 99 | { 100 | const symbolSize = Math.min(75, this.getLength()) 101 | const arcNum = 3 102 | const arcAmplitude = 25 103 | 104 | this.drawSymbolBegin(manager, ctx, symbolSize) 105 | this.drawSymbolSetGradient(manager, ctx, symbolSize, 106 | manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])), 107 | manager.getVoltageColor(manager.getNodeVoltage(this.nodes[1]))) 108 | 109 | ctx.lineJoin = "round" 110 | 111 | ctx.beginPath() 112 | ctx.moveTo(-symbolSize / 2, 0) 113 | 114 | for (let arc = 0; arc < arcNum; arc++) 115 | { 116 | const arcX1 = -symbolSize / 2 + symbolSize / arcNum * (arc + 0) 117 | const arcX2 = -symbolSize / 2 + symbolSize / arcNum * (arc + 1) 118 | 119 | ctx.bezierCurveTo( 120 | arcX1, arcAmplitude, 121 | arcX2, arcAmplitude, 122 | arcX2, 0) 123 | } 124 | 125 | ctx.stroke() 126 | 127 | this.drawSymbolEnd(manager, ctx) 128 | this.drawRatingText(manager, ctx, this.inductance, "H", 25) 129 | } 130 | } -------------------------------------------------------------------------------- /src/circuitSolver.js: -------------------------------------------------------------------------------- 1 | import { Matrix } from "./matrix.js" 2 | 3 | 4 | // From http://www.swarthmore.edu/NatSci/echeeve1/Ref/mna/MNA3.html 5 | 6 | 7 | export class CircuitSolver 8 | { 9 | constructor() 10 | { 11 | this.readyToStamp = false 12 | this.readyToRun = false 13 | 14 | this.matrixG = null 15 | this.matrixB = null 16 | this.matrixC = null 17 | this.matrixI = null 18 | this.matrixE = null 19 | this.matrixIOriginal = null 20 | this.matrixEOriginal = null 21 | this.matrixA = null 22 | this.matrixZ = null 23 | this.matrixAPivots = null 24 | 25 | this.solution = null 26 | } 27 | 28 | 29 | stampBegin(nodeNum, voltNum, groundNodeIndex) 30 | { 31 | this.readyToStamp = true 32 | this.readyToRun = false 33 | 34 | this.nodeNum = nodeNum 35 | this.voltNum = voltNum 36 | this.groundNodeIndex = groundNodeIndex 37 | 38 | this.matrixG = new Matrix(nodeNum, nodeNum) 39 | this.matrixB = new Matrix(voltNum, nodeNum) 40 | this.matrixC = new Matrix(nodeNum, voltNum) 41 | 42 | this.matrixI = new Matrix(1, nodeNum) 43 | this.matrixE = new Matrix(1, voltNum) 44 | 45 | this.matrixIOriginal = null 46 | this.matrixEOriginal = null 47 | 48 | this.matrixA = null 49 | this.matrixZ = null 50 | this.matrixAPivots = null 51 | 52 | this.solution = null 53 | } 54 | 55 | 56 | stampResistance(node1, node2, resistance) 57 | { 58 | this.matrixG.add(node1, node1, 1 / resistance) 59 | this.matrixG.add(node2, node2, 1 / resistance) 60 | this.matrixG.add(node1, node2, -1 / resistance) 61 | this.matrixG.add(node2, node1, -1 / resistance) 62 | } 63 | 64 | 65 | stampVoltage(voltageSourceIndex, negNode, posNode, voltage) 66 | { 67 | this.matrixB.set(voltageSourceIndex, posNode, 1) 68 | this.matrixB.set(voltageSourceIndex, negNode, -1) 69 | 70 | this.matrixC.set(posNode, voltageSourceIndex, 1) 71 | this.matrixC.set(negNode, voltageSourceIndex, -1) 72 | 73 | this.matrixE.set(0, voltageSourceIndex, voltage) 74 | } 75 | 76 | 77 | stampCurrentSource(negNode, posNode, current) 78 | { 79 | this.matrixI.add(0, posNode, current) 80 | this.matrixI.add(0, negNode, -current) 81 | } 82 | 83 | 84 | stampEnd() 85 | { 86 | // Consolidate into bigger matrices. 87 | this.matrixA = new Matrix(this.nodeNum + this.voltNum, this.nodeNum + this.voltNum) 88 | this.matrixG.copyTo(this.matrixA, 0, 0) 89 | this.matrixB.copyTo(this.matrixA, this.nodeNum, 0) 90 | this.matrixC.copyTo(this.matrixA, 0, this.nodeNum) 91 | 92 | //console.log("---") 93 | //console.log(this.matrixA.toString()) 94 | //console.log(this.matrixZ.toString()) 95 | 96 | if (this.nodeNum + this.voltNum <= 1) 97 | return 98 | 99 | // Remove ground node rows and columns. 100 | this.matrixA = this.matrixA.removeRow(this.groundNodeIndex) 101 | this.matrixA = this.matrixA.removeColumn(this.groundNodeIndex) 102 | 103 | //console.log(this.matrixA.toString()) 104 | //console.log(this.matrixZ.toString()) 105 | 106 | //console.log("matrixA:") 107 | //console.log(this.matrixA.toString()) 108 | 109 | this.matrixAPivots = this.matrixA.luDecompose() 110 | if (this.matrixAPivots == null) 111 | { 112 | console.log("singular matrix") 113 | return 114 | } 115 | 116 | //console.log("matrixA decomposed:") 117 | //console.log(this.matrixA.toString()) 118 | //console.log("matrixA pivots:") 119 | //console.log(this.matrixAPivots.toString()) 120 | 121 | 122 | this.matrixIOriginal = this.matrixI.clone() 123 | this.matrixEOriginal = this.matrixE.clone() 124 | 125 | this.readyToRun = true 126 | } 127 | 128 | 129 | beginIteration() 130 | { 131 | this.matrixI = this.matrixIOriginal.clone() 132 | this.matrixE = this.matrixEOriginal.clone() 133 | } 134 | 135 | 136 | solve() 137 | { 138 | if (this.matrixAPivots == null) 139 | return 140 | 141 | this.matrixZ = new Matrix(1, this.nodeNum + this.voltNum) 142 | this.matrixI.copyTo(this.matrixZ, 0, 0) 143 | this.matrixE.copyTo(this.matrixZ, 0, this.nodeNum) 144 | this.matrixZ = this.matrixZ.removeRow(this.groundNodeIndex) 145 | 146 | this.solution = this.matrixA.luSolve(this.matrixAPivots, this.matrixZ).insertRow(this.groundNodeIndex) 147 | 148 | //console.log("solution:") 149 | //console.log(this.solution.toString()) 150 | } 151 | 152 | 153 | getNodeVoltage(index) 154 | { 155 | if (this.solution == null) 156 | return 0 157 | 158 | const v = this.solution.get(0, index) 159 | if (isNaN(v)) 160 | return 0 161 | 162 | return v 163 | } 164 | 165 | 166 | getVoltageSourceCurrent(voltageSourceIndex) 167 | { 168 | if (this.solution == null) 169 | return 0 170 | 171 | const a = this.solution.get(0, this.nodeNum + voltageSourceIndex) 172 | if (isNaN(a)) 173 | return 0 174 | 175 | return a 176 | } 177 | } -------------------------------------------------------------------------------- /src/matrix.js: -------------------------------------------------------------------------------- 1 | export class Matrix 2 | { 3 | constructor(n, m) 4 | { 5 | this.n = n 6 | this.m = m 7 | this.cells = new Float64Array(n * m) 8 | } 9 | 10 | 11 | get(i, j) 12 | { 13 | return this.cells[j * this.n + i] 14 | } 15 | 16 | 17 | set(i, j, value) 18 | { 19 | this.cells[j * this.n + i] = value 20 | } 21 | 22 | 23 | add(i, j, value) 24 | { 25 | this.cells[j * this.n + i] += value 26 | } 27 | 28 | 29 | mult(i, j, value) 30 | { 31 | this.cells[j * this.n + i] *= value 32 | } 33 | 34 | 35 | clone() 36 | { 37 | let newMatrix = new Matrix(this.n, this.m) 38 | 39 | for (let y = 0; y < this.m; y++) 40 | for (let x = 0; x < this.n; x++) 41 | newMatrix.set(x, y, this.get(x, y)) 42 | 43 | return newMatrix 44 | } 45 | 46 | 47 | copyTo(other, i, j) 48 | { 49 | for (let y = 0; y < this.m; y++) 50 | for (let x = 0; x < this.n; x++) 51 | other.set(i + x, j + y, this.get(x, y)) 52 | } 53 | 54 | 55 | removeRow(j) 56 | { 57 | let newMatrix = new Matrix(this.n, this.m - 1) 58 | 59 | for (let x = 0; x < this.n; x++) 60 | { 61 | for (let y = 0; y < j; y++) 62 | newMatrix.set(x, y, this.get(x, y)) 63 | 64 | for (let y = j + 1; y < this.m; y++) 65 | newMatrix.set(x, y - 1, this.get(x, y)) 66 | } 67 | 68 | return newMatrix 69 | } 70 | 71 | 72 | removeColumn(i) 73 | { 74 | let newMatrix = new Matrix(this.n - 1, this.m) 75 | 76 | for (let y = 0; y < this.m; y++) 77 | { 78 | for (let x = 0; x < i; x++) 79 | newMatrix.set(x, y, this.get(x, y)) 80 | 81 | for (let x = i + 1; x < this.n; x++) 82 | newMatrix.set(x - 1, y, this.get(x, y)) 83 | } 84 | 85 | return newMatrix 86 | } 87 | 88 | 89 | insertRow(beforeJ) 90 | { 91 | let newMatrix = new Matrix(this.n, this.m + 1) 92 | 93 | for (let x = 0; x < this.n; x++) 94 | { 95 | for (let y = 0; y < beforeJ; y++) 96 | newMatrix.set(x, y, this.get(x, y)) 97 | 98 | newMatrix.set(x, beforeJ, 0) 99 | 100 | for (let y = beforeJ; y < this.m; y++) 101 | newMatrix.set(x, y + 1, this.get(x, y)) 102 | } 103 | 104 | return newMatrix 105 | } 106 | 107 | 108 | luDecompose() 109 | { 110 | // From https://github.com/pfalstad/circuitjs1 111 | 112 | let pivots = new Float64Array(this.n) 113 | 114 | // check for a possible singular matrix by scanning for rows that 115 | // are all zeroes 116 | for (let i = 0; i < this.n; i++) 117 | { 118 | let rowAllZeroes = true 119 | for (let j = 0; j < this.n; j++) 120 | { 121 | if (this.get(j, i) != 0) 122 | rowAllZeroes = false 123 | } 124 | 125 | if (rowAllZeroes) 126 | return null 127 | } 128 | 129 | // use Crout's method; loop through the columns 130 | for (let j = 0; j < this.n; j++) 131 | { 132 | // calculate upper triangular elements for this column 133 | for (let i = 0; i < j; i++) 134 | { 135 | let q = this.get(j, i) 136 | for (let k = 0; k < i; k++) 137 | q -= this.get(k, i) * this.get(j, k) 138 | 139 | this.set(j, i, q) 140 | } 141 | 142 | // calculate lower triangular elements for this column 143 | let largest = 0 144 | let largestRow = -1 145 | for (let i = j; i < this.n; i++) 146 | { 147 | let q = this.get(j, i) 148 | for (let k = 0; k < j; k++) 149 | q -= this.get(k, i) * this.get(j, k) 150 | 151 | this.set(j, i, q) 152 | 153 | const x = Math.abs(q) 154 | if (x >= largest) 155 | { 156 | largest = x 157 | largestRow = i 158 | } 159 | } 160 | 161 | // pivoting 162 | if (j != largestRow) 163 | { 164 | for (let k = 0; k < this.n; k++) 165 | { 166 | const x = this.get(k, largestRow) 167 | this.set(k, largestRow, this.get(k, j)) 168 | this.set(k, j, x) 169 | } 170 | } 171 | 172 | // keep track of row interchanges 173 | pivots[j] = largestRow 174 | 175 | // avoid zeros 176 | if (this.get(j, j) == 0) 177 | { 178 | console.log("avoided zero") 179 | this.set(j, j, 1e-18) 180 | } 181 | 182 | if (j != this.n - 1) 183 | { 184 | const mult = 1 / this.get(j, j) 185 | for (let i = j + 1; i < this.n; i++) 186 | this.mult(j, i, mult) 187 | } 188 | } 189 | 190 | return pivots 191 | } 192 | 193 | 194 | luSolve(pivots, rightHandSide) 195 | { 196 | // From https://github.com/pfalstad/circuitjs1 197 | 198 | let b = rightHandSide.clone() 199 | 200 | // find first nonzero b element 201 | let i = 0 202 | for (; i != this.n; i++) 203 | { 204 | const row = pivots[i] 205 | 206 | const swap = b.get(0, row) 207 | b.set(0, row, b.get(0, i)) 208 | b.set(0, i, swap) 209 | 210 | if (swap != 0) 211 | break 212 | } 213 | 214 | let bi = i 215 | i += 1 216 | 217 | for (; i < this.n; i++) 218 | { 219 | let row = pivots[i] 220 | let tot = b.get(0, row) 221 | 222 | b.set(0, row, b.get(0, i)) 223 | // forward substitution using the lower triangular matrix 224 | for (let j = bi; j < i; j++) 225 | tot -= this.get(j, i) * b.get(0, j) 226 | 227 | b.set(0, i, tot) 228 | } 229 | 230 | for (i = this.n - 1; i >= 0; i--) 231 | { 232 | let tot = b.get(0, i) 233 | 234 | // back-substitution using the upper triangular matrix 235 | for (let j = i + 1; j != this.n; j++) 236 | tot -= this.get(j, i) * b.get(0, j); 237 | 238 | b.set(0, i, tot / this.get(i, i)) 239 | } 240 | 241 | return b 242 | } 243 | 244 | 245 | luDecompose2() 246 | { 247 | // From https://en.wikipedia.org/wiki/LU_decomposition#C_code_examples 248 | 249 | const tolerance = 1e-18 250 | 251 | let pivots = new Float32Array(this.n) 252 | for (let i = 0; i < this.n; i++) 253 | pivots[i] = i 254 | 255 | for (let i = 0; i < this.n; i++) 256 | { 257 | let maxA = 0 258 | let iMax = i 259 | 260 | for (let k = i; k < this.n; k++) 261 | { 262 | const abs = Math.abs(this.get(i, k)) 263 | if (abs > maxA) 264 | { 265 | maxA = abs 266 | iMax = k 267 | } 268 | } 269 | 270 | if (maxA < tolerance) 271 | return null 272 | 273 | if (iMax != i) 274 | { 275 | // permute pivot 276 | const pivotTemp = pivots[i] 277 | pivots[i] = pivots[iMax] 278 | pivots[iMax] = pivots[i] 279 | 280 | // permute row 281 | for (let k = 0; k < this.n; k++) 282 | { 283 | const rowTemp = this.get(k, i) 284 | this.set(k, i, this.get(k, iMax)) 285 | this.set(k, iMax, rowTemp) 286 | } 287 | } 288 | 289 | for (let j = i + 1; j < this.n; j++) 290 | { 291 | this.mult(i, j, 1 / this.get(i, i)) 292 | 293 | for (let k = i + 1; k < this.n; k++) 294 | this.add(k, j, this.get(i, j) * this.get(k, i)) 295 | } 296 | } 297 | 298 | return pivots 299 | } 300 | 301 | 302 | luSolve2(pivots, rightHandSide) 303 | { 304 | // From https://en.wikipedia.org/wiki/LU_decomposition#C_code_examples 305 | 306 | let result = new Matrix(1, this.n) 307 | 308 | for (let i = 0; i < this.n; i++) 309 | { 310 | result.set(0, i, rightHandSide.get(0, pivots[i])) 311 | 312 | for (let k = 0; k < i; k++) 313 | result.add(0, i, -this.get(k, i) * result[k]) 314 | } 315 | 316 | for (let i = this.n - 1; i >= 0; i--) 317 | { 318 | for (let k = i + 1; k < this.n; k++) 319 | result.add(0, i, -this.get(k, i) * result[k]) 320 | 321 | result.mult(0, i, 1 / this.get(i, i)) 322 | } 323 | 324 | return result 325 | } 326 | 327 | 328 | toString() 329 | { 330 | let str = "" 331 | 332 | for (let y = 0; y < this.m; y++) 333 | { 334 | str += "[ " 335 | for (let x = 0; x < this.n; x++) 336 | str += this.get(x, y).toFixed(5).padStart(8) + " " 337 | 338 | str += "]\n" 339 | } 340 | 341 | return str 342 | } 343 | } -------------------------------------------------------------------------------- /src/componentSingleEnded.js: -------------------------------------------------------------------------------- 1 | import { Component } from "./component.js" 2 | import * as MathUtils from "./math.js" 3 | 4 | 5 | export class ComponentSingleEnded extends Component 6 | { 7 | constructor(p) 8 | { 9 | super(p) 10 | 11 | this.points = [p, p] 12 | this.joints = [-1, -1] 13 | this.nodes = [-1, -1] 14 | this.selected = [false, false] 15 | this.dragOrigin = [p, p] 16 | 17 | this.isVoltageSource = false 18 | this.voltageSourceIndex = -1 19 | 20 | this.current = 0 21 | this.currentAnim = 0 22 | } 23 | 24 | 25 | loadFromString(manager, loadData, reader) 26 | { 27 | const joint1 = parseInt(reader.read()) 28 | const joint2 = parseInt(reader.read()) 29 | 30 | this.points[0] = { x: loadData.joints[joint1].x, y: loadData.joints[joint1].y } 31 | this.points[1] = { x: loadData.joints[joint2].x, y: loadData.joints[joint2].y } 32 | } 33 | 34 | 35 | reset(manager) 36 | { 37 | this.current = 0 38 | this.currentAnim = 0 39 | } 40 | 41 | 42 | updateCurrentAnim(manager, mult) 43 | { 44 | const delta = Math.max(-0.25, Math.min(0.25, mult * (this.current * 4.5))) 45 | 46 | this.currentAnim = (1 + this.currentAnim + delta) % 1 47 | } 48 | 49 | 50 | isDegenerate() 51 | { 52 | return this.points[0].x == this.points[1].x && this.points[0].y == this.points[1].y 53 | } 54 | 55 | 56 | getOutgoingDirectionFromNode(index) 57 | { 58 | const p1 = this.points[index] 59 | const p2 = this.points[index == 0 ? 1 : 0] 60 | 61 | return Math.atan2(p1.y - p2.y, p2.x - p1.x) 62 | } 63 | 64 | 65 | getHover(pos) 66 | { 67 | const pax = pos.x - this.points[0].x 68 | const pay = pos.y - this.points[0].y 69 | const bax = this.points[1].x - this.points[0].x 70 | const bay = this.points[1].y - this.points[0].y 71 | const dotPaBa = pax * bax + pay * bay 72 | const dotBaBa = bax * bax + bay * bay 73 | const t = Math.max(0, Math.min(1, dotPaBa / dotBaBa)) 74 | 75 | const fx = pax - bax * t 76 | const fy = pay - bay * t 77 | const distSqr = (fx * fx + fy * fy) 78 | 79 | if (distSqr > 25 * 25) 80 | return null 81 | 82 | if (t < 0.1) 83 | return { kind: "junction", index: 0, distSqr } 84 | 85 | if (t < 0.2) 86 | return { kind: "vertex", index: 0, distSqr } 87 | 88 | if (t > 0.8) 89 | return { kind: "vertex", index: 1, distSqr } 90 | 91 | return { kind: "full", distSqr } 92 | } 93 | 94 | 95 | getLength() 96 | { 97 | const vector = { 98 | x: this.points[1].x - this.points[0].x, 99 | y: this.points[1].y - this.points[0].y 100 | } 101 | 102 | return Math.sqrt(vector.x * vector.x + vector.y * vector.y) 103 | } 104 | 105 | 106 | draw(manager, ctx) 107 | { 108 | ctx.save() 109 | 110 | ctx.strokeStyle = "#eeeeee" 111 | 112 | ctx.beginPath() 113 | ctx.arc(this.points[0].x, this.points[0].y, 2, 0, Math.PI * 2) 114 | ctx.moveTo(this.points[0].x, this.points[0].y) 115 | ctx.lineTo(this.points[1].x, this.points[1].y) 116 | ctx.arc(this.points[1].x, this.points[1].y, 2, 0, Math.PI * 2) 117 | ctx.stroke() 118 | 119 | ctx.restore() 120 | } 121 | 122 | 123 | drawSymbolBegin(manager, ctx, symbolSize) 124 | { 125 | const vector = { 126 | x: this.points[0].x - this.points[1].x, 127 | y: this.points[0].y - this.points[1].y 128 | } 129 | 130 | const vectorLen = Math.sqrt(vector.x * vector.x + vector.y * vector.y) 131 | 132 | const vectorUnit = { 133 | x: vector.x / vectorLen, 134 | y: vector.y / vectorLen 135 | } 136 | 137 | const break1 = Math.min(vectorLen, symbolSize) 138 | 139 | ctx.save() 140 | 141 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])) 142 | ctx.beginPath() 143 | ctx.arc(this.points[0].x, this.points[0].y, 2, 0, Math.PI * 2) 144 | ctx.moveTo(this.points[0].x, this.points[0].y) 145 | ctx.lineTo(this.points[1].x + vectorUnit.x * break1, this.points[1].y + vectorUnit.y * break1) 146 | ctx.stroke() 147 | 148 | ctx.translate(this.points[1].x + vectorUnit.x * symbolSize / 2, this.points[1].y + vectorUnit.y * symbolSize / 2) 149 | ctx.transform(vectorUnit.x, vectorUnit.y, -vectorUnit.y, vectorUnit.x, 0, 0) 150 | } 151 | 152 | 153 | drawSymbolSetGradient(manager, ctx, symbolSize, color1, color2) 154 | { 155 | let grad = ctx.createLinearGradient(-symbolSize / 2, 0, symbolSize / 2, 0) 156 | grad.addColorStop(0, color1) 157 | grad.addColorStop(1, color2) 158 | ctx.strokeStyle = grad 159 | ctx.fillStyle = grad 160 | } 161 | 162 | 163 | drawSymbolEnd(manager, ctx) 164 | { 165 | ctx.restore() 166 | } 167 | 168 | 169 | renderCurrent(manager, ctx) 170 | { 171 | this.drawCurrent(manager, ctx, this.currentAnim, this.points[0], this.points[1]) 172 | } 173 | 174 | 175 | renderHover(manager, ctx, hover) 176 | { 177 | if (manager.debugDrawClean) 178 | return 179 | 180 | ctx.save() 181 | 182 | const highlightSize = 20 183 | 184 | ctx.lineWidth = highlightSize 185 | 186 | if (hover.kind == "full") 187 | { 188 | ctx.beginPath() 189 | ctx.moveTo(this.points[0].x, this.points[0].y) 190 | ctx.lineTo(this.points[1].x, this.points[1].y) 191 | ctx.stroke() 192 | } 193 | 194 | if (hover.kind == "junction") 195 | { 196 | ctx.beginPath() 197 | ctx.arc(this.points[hover.index].x, this.points[hover.index].y, highlightSize / 2, 0, Math.PI * 2) 198 | ctx.fill() 199 | } 200 | 201 | if (hover.kind == "vertex") 202 | { 203 | let centerX = (this.points[0].x + this.points[1].x) / 2 204 | let centerY = (this.points[0].y + this.points[1].y) / 2 205 | 206 | for (let i = 0; i < 3; i++) 207 | { 208 | centerX = (centerX + this.points[hover.index].x) / 2 209 | centerY = (centerY + this.points[hover.index].y) / 2 210 | } 211 | 212 | ctx.beginPath() 213 | ctx.moveTo(this.points[hover.index].x, this.points[hover.index].y) 214 | ctx.lineTo(centerX, centerY) 215 | ctx.stroke() 216 | } 217 | 218 | ctx.restore() 219 | } 220 | 221 | 222 | renderSelection(manager, ctx) 223 | { 224 | if (manager.debugDrawClean) 225 | return 226 | 227 | ctx.save() 228 | 229 | const highlightSize = 18 230 | 231 | ctx.lineWidth = highlightSize 232 | 233 | if (this.selected[0] && this.selected[1]) 234 | { 235 | ctx.beginPath() 236 | ctx.moveTo(this.points[0].x, this.points[0].y) 237 | ctx.lineTo(this.points[1].x, this.points[1].y) 238 | ctx.stroke() 239 | } 240 | 241 | else if (this.selected[0]) 242 | { 243 | ctx.beginPath() 244 | ctx.arc(this.points[0].x, this.points[0].y, highlightSize / 2, 0, Math.PI * 2) 245 | ctx.fill() 246 | } 247 | 248 | else if (this.selected[1]) 249 | { 250 | ctx.beginPath() 251 | ctx.arc(this.points[1].x, this.points[1].y, highlightSize / 2, 0, Math.PI * 2) 252 | ctx.fill() 253 | } 254 | 255 | ctx.restore() 256 | } 257 | 258 | 259 | renderEditing(manager, ctx) 260 | { 261 | if (manager.debugDrawClean) 262 | return 263 | 264 | ctx.save() 265 | 266 | const highlightSize = 20 267 | 268 | ctx.lineWidth = highlightSize 269 | 270 | ctx.beginPath() 271 | ctx.moveTo(this.points[0].x, this.points[0].y) 272 | ctx.lineTo(this.points[1].x, this.points[1].y) 273 | ctx.stroke() 274 | 275 | ctx.restore() 276 | } 277 | 278 | 279 | drawRatingText(manager, ctx, value, unit, xDistance = 35, yDistance = 35) 280 | { 281 | ctx.font = "15px Verdana" 282 | ctx.textBaseline = "middle" 283 | 284 | const labelDirection = this.getOutgoingDirectionFromNode(1) + Math.PI / 2 285 | 286 | const xCenter = (this.points[0].x + this.points[1].x) / 2 287 | const yCenter = (this.points[0].y + this.points[1].y) / 2 288 | const xOffset = xDistance * Math.cos(labelDirection) 289 | const yOffset = yDistance * -Math.sin(labelDirection) 290 | 291 | if (Math.abs(xOffset) < Math.abs(yOffset) * 0.1) 292 | ctx.textAlign = "center" 293 | else if (xOffset > 0) 294 | ctx.textAlign = "left" 295 | else 296 | ctx.textAlign = "right" 297 | 298 | const str = MathUtils.valueToStringWithUnitPrefix(value, " ") + unit 299 | 300 | ctx.fillStyle = "#fff" 301 | ctx.fillText(str, xCenter + xOffset, yCenter + yOffset) 302 | } 303 | } -------------------------------------------------------------------------------- /src/componentDoubleEnded.js: -------------------------------------------------------------------------------- 1 | import { Component } from "./component.js" 2 | import * as MathUtils from "./math.js" 3 | 4 | 5 | export class ComponentDoubleEnded extends Component 6 | { 7 | constructor(p) 8 | { 9 | super(p) 10 | 11 | this.points = [p, p] 12 | this.joints = [-1, -1] 13 | this.nodes = [-1, -1] 14 | this.selected = [false, false] 15 | this.dragOrigin = [p, p] 16 | 17 | this.isVoltageSource = false 18 | this.voltageSourceIndex = -1 19 | 20 | this.current = 0 21 | this.currentAnim = 0 22 | } 23 | 24 | 25 | loadFromString(manager, loadData, reader) 26 | { 27 | const joint1 = parseInt(reader.read()) 28 | const joint2 = parseInt(reader.read()) 29 | 30 | this.points[0] = { x: loadData.joints[joint1].x, y: loadData.joints[joint1].y } 31 | this.points[1] = { x: loadData.joints[joint2].x, y: loadData.joints[joint2].y } 32 | } 33 | 34 | 35 | reset(manager) 36 | { 37 | this.current = 0 38 | this.currentAnim = 0 39 | } 40 | 41 | 42 | updateCurrentAnim(manager, mult) 43 | { 44 | const delta = Math.max(-0.25, Math.min(0.25, mult * (this.current * 4.5))) 45 | 46 | this.currentAnim = (1 + this.currentAnim + delta) % 1 47 | } 48 | 49 | 50 | isDegenerate() 51 | { 52 | return this.points[0].x == this.points[1].x && this.points[0].y == this.points[1].y 53 | } 54 | 55 | 56 | getOutgoingDirectionFromNode(index) 57 | { 58 | const p1 = this.points[index] 59 | const p2 = this.points[index == 0 ? 1 : 0] 60 | 61 | return Math.atan2(p1.y - p2.y, p2.x - p1.x) 62 | } 63 | 64 | 65 | getHover(pos) 66 | { 67 | const pax = pos.x - this.points[0].x 68 | const pay = pos.y - this.points[0].y 69 | const bax = this.points[1].x - this.points[0].x 70 | const bay = this.points[1].y - this.points[0].y 71 | const dotPaBa = pax * bax + pay * bay 72 | const dotBaBa = bax * bax + bay * bay 73 | const t = Math.max(0, Math.min(1, dotPaBa / dotBaBa)) 74 | 75 | const fx = pax - bax * t 76 | const fy = pay - bay * t 77 | const distSqr = (fx * fx + fy * fy) 78 | 79 | if (distSqr > 25 * 25) 80 | return null 81 | 82 | if (t < 0.1) 83 | return { kind: "junction", index: 0, distSqr } 84 | 85 | if (t > 0.9) 86 | return { kind: "junction", index: 1, distSqr } 87 | 88 | if (t < 0.2) 89 | return { kind: "vertex", index: 0, distSqr } 90 | 91 | if (t > 0.8) 92 | return { kind: "vertex", index: 1, distSqr } 93 | 94 | return { kind: "full", distSqr } 95 | } 96 | 97 | 98 | getLength() 99 | { 100 | const vector = { 101 | x: this.points[1].x - this.points[0].x, 102 | y: this.points[1].y - this.points[0].y 103 | } 104 | 105 | return Math.sqrt(vector.x * vector.x + vector.y * vector.y) 106 | } 107 | 108 | 109 | draw(manager, ctx) 110 | { 111 | ctx.save() 112 | 113 | ctx.strokeStyle = "#eeeeee" 114 | 115 | ctx.beginPath() 116 | ctx.arc(this.points[0].x, this.points[0].y, 2, 0, Math.PI * 2) 117 | ctx.moveTo(this.points[0].x, this.points[0].y) 118 | ctx.lineTo(this.points[1].x, this.points[1].y) 119 | ctx.arc(this.points[1].x, this.points[1].y, 2, 0, Math.PI * 2) 120 | ctx.stroke() 121 | 122 | ctx.restore() 123 | } 124 | 125 | 126 | drawSymbolBegin(manager, ctx, symbolSize) 127 | { 128 | const vector = { 129 | x: this.points[1].x - this.points[0].x, 130 | y: this.points[1].y - this.points[0].y 131 | } 132 | 133 | const vectorLen = Math.sqrt(vector.x * vector.x + vector.y * vector.y) 134 | 135 | const vectorUnit = { 136 | x: vector.x / vectorLen, 137 | y: vector.y / vectorLen 138 | } 139 | 140 | const break1 = Math.max( 0, vectorLen / 2 - symbolSize / 2) 141 | const break2 = Math.min(vectorLen, vectorLen / 2 + symbolSize / 2) 142 | 143 | ctx.save() 144 | 145 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[0])) 146 | ctx.beginPath() 147 | ctx.arc(this.points[0].x, this.points[0].y, 2, 0, Math.PI * 2) 148 | ctx.moveTo(this.points[0].x, this.points[0].y) 149 | ctx.lineTo(this.points[0].x + vectorUnit.x * break1, this.points[0].y + vectorUnit.y * break1) 150 | ctx.stroke() 151 | 152 | ctx.strokeStyle = manager.getVoltageColor(manager.getNodeVoltage(this.nodes[1])) 153 | ctx.beginPath() 154 | ctx.arc(this.points[1].x, this.points[1].y, 2, 0, Math.PI * 2) 155 | ctx.moveTo(this.points[0].x + vectorUnit.x * break2, this.points[0].y + vectorUnit.y * break2) 156 | ctx.lineTo(this.points[1].x, this.points[1].y) 157 | ctx.stroke() 158 | 159 | ctx.translate(this.points[0].x + vector.x / 2, this.points[0].y + vector.y / 2) 160 | ctx.transform(vectorUnit.x, vectorUnit.y, -vectorUnit.y, vectorUnit.x, 0, 0) 161 | } 162 | 163 | 164 | drawSymbolSetGradient(manager, ctx, symbolSize, color1, color2) 165 | { 166 | let grad = ctx.createLinearGradient(-symbolSize / 2, 0, symbolSize / 2, 0) 167 | grad.addColorStop(0, color1) 168 | grad.addColorStop(1, color2) 169 | ctx.strokeStyle = grad 170 | ctx.fillStyle = grad 171 | } 172 | 173 | 174 | drawSymbolEnd(manager, ctx) 175 | { 176 | ctx.restore() 177 | } 178 | 179 | 180 | renderCurrent(manager, ctx) 181 | { 182 | this.drawCurrent(manager, ctx, this.currentAnim, this.points[0], this.points[1]) 183 | } 184 | 185 | 186 | renderHover(manager, ctx, hover) 187 | { 188 | if (manager.debugDrawClean) 189 | return 190 | 191 | ctx.save() 192 | 193 | const highlightSize = 20 194 | 195 | ctx.lineWidth = highlightSize 196 | 197 | if (hover.kind == "full") 198 | { 199 | ctx.beginPath() 200 | ctx.moveTo(this.points[0].x, this.points[0].y) 201 | ctx.lineTo(this.points[1].x, this.points[1].y) 202 | ctx.stroke() 203 | } 204 | 205 | if (hover.kind == "junction") 206 | { 207 | ctx.beginPath() 208 | ctx.arc(this.points[hover.index].x, this.points[hover.index].y, highlightSize / 2, 0, Math.PI * 2) 209 | ctx.fill() 210 | } 211 | 212 | if (hover.kind == "vertex") 213 | { 214 | let centerX = (this.points[0].x + this.points[1].x) / 2 215 | let centerY = (this.points[0].y + this.points[1].y) / 2 216 | 217 | for (let i = 0; i < 3; i++) 218 | { 219 | centerX = (centerX + this.points[hover.index].x) / 2 220 | centerY = (centerY + this.points[hover.index].y) / 2 221 | } 222 | 223 | ctx.beginPath() 224 | ctx.moveTo(this.points[hover.index].x, this.points[hover.index].y) 225 | ctx.lineTo(centerX, centerY) 226 | ctx.stroke() 227 | } 228 | 229 | ctx.restore() 230 | } 231 | 232 | 233 | renderSelection(manager, ctx) 234 | { 235 | if (manager.debugDrawClean) 236 | return 237 | 238 | ctx.save() 239 | 240 | const highlightSize = 18 241 | 242 | ctx.lineWidth = highlightSize 243 | 244 | if (this.selected[0] && this.selected[1]) 245 | { 246 | ctx.beginPath() 247 | ctx.moveTo(this.points[0].x, this.points[0].y) 248 | ctx.lineTo(this.points[1].x, this.points[1].y) 249 | ctx.stroke() 250 | } 251 | 252 | else if (this.selected[0]) 253 | { 254 | ctx.beginPath() 255 | ctx.arc(this.points[0].x, this.points[0].y, highlightSize / 2, 0, Math.PI * 2) 256 | ctx.fill() 257 | } 258 | 259 | else if (this.selected[1]) 260 | { 261 | ctx.beginPath() 262 | ctx.arc(this.points[1].x, this.points[1].y, highlightSize / 2, 0, Math.PI * 2) 263 | ctx.fill() 264 | } 265 | 266 | ctx.restore() 267 | } 268 | 269 | 270 | renderEditing(manager, ctx) 271 | { 272 | if (manager.debugDrawClean) 273 | return 274 | 275 | ctx.save() 276 | 277 | const highlightSize = 20 278 | 279 | ctx.lineWidth = highlightSize 280 | 281 | ctx.beginPath() 282 | ctx.moveTo(this.points[0].x, this.points[0].y) 283 | ctx.lineTo(this.points[1].x, this.points[1].y) 284 | ctx.stroke() 285 | 286 | ctx.restore() 287 | } 288 | 289 | 290 | drawRatingText(manager, ctx, value, unit, xDistance = 35, yDistance = 35) 291 | { 292 | ctx.font = "15px Verdana" 293 | ctx.textBaseline = "middle" 294 | 295 | const labelDirection = this.getOutgoingDirectionFromNode(1) + Math.PI / 2 296 | 297 | const xCenter = (this.points[0].x + this.points[1].x) / 2 298 | const yCenter = (this.points[0].y + this.points[1].y) / 2 299 | const xOffset = xDistance * Math.cos(labelDirection) 300 | const yOffset = yDistance * -Math.sin(labelDirection) 301 | 302 | if (Math.abs(xOffset) < Math.abs(yOffset) * 0.1) 303 | ctx.textAlign = "center" 304 | else if (xOffset > 0) 305 | ctx.textAlign = "left" 306 | else 307 | ctx.textAlign = "right" 308 | 309 | const str = MathUtils.valueToStringWithUnitPrefix(value, " ") + unit 310 | 311 | ctx.fillStyle = "#fff" 312 | ctx.fillText(str, xCenter + xOffset, yCenter + yOffset) 313 | } 314 | } -------------------------------------------------------------------------------- /src/circuitEditor.js: -------------------------------------------------------------------------------- 1 | import { CircuitSolver } from "./circuitSolver.js" 2 | import { ComponentSingleEnded } from "./componentSingleEnded.js" 3 | import { ComponentDoubleEnded } from "./componentDoubleEnded.js" 4 | import { ComponentWire } from "./componentWire.js" 5 | import { ComponentBattery } from "./componentBattery.js" 6 | import { ComponentResistor } from "./componentResistor.js" 7 | import { ComponentCurrentSource } from "./componentCurrentSource.js" 8 | import { ComponentCapacitor } from "./componentCapacitor.js" 9 | import { ComponentInductor } from "./componentInductor.js" 10 | import { ComponentVoltageSource } from "./componentVoltageSource.js" 11 | import { ComponentGround } from "./componentGround.js" 12 | import * as MathUtils from "./math.js" 13 | 14 | 15 | export class CircuitEditor 16 | { 17 | constructor(canvas) 18 | { 19 | this.canvas = canvas 20 | this.ctx = canvas.getContext("2d") 21 | this.width = parseInt(canvas.width) 22 | this.height = parseInt(canvas.height) 23 | 24 | this.tileSize = 25 25 | 26 | this.time = 0 27 | this.timePerIteration = 1e-6 28 | 29 | this.components = [] 30 | this.componentsForEditing = [] 31 | 32 | this.solver = new CircuitSolver() 33 | this.joints = new Map() 34 | this.nodes = [] 35 | this.groundNodeIndex = -1 36 | this.voltageSources = 0 37 | 38 | this.cameraPos = { x: 0, y: 0 } 39 | this.cameraZoomLevel = 0 40 | 41 | this.mouseDown = false 42 | this.mousePos = null 43 | this.mousePosRaw = null 44 | this.mousePosSnapped = null 45 | this.mouseDragOrigin = null 46 | this.mouseDragOriginRaw = null 47 | this.mouseDragOriginSnapped = null 48 | this.mouseAddComponentClass = null 49 | this.mouseCurrentAction = null 50 | this.mouseCurrentHoverData = null 51 | 52 | this.canvas.onmousedown = (ev) => this.onMouseDown (ev) 53 | this.canvas.onmousemove = (ev) => this.onMouseMove (ev) 54 | this.canvas.onmouseup = (ev) => this.onMouseUp (ev) 55 | this.canvas.onmouseleave = (ev) => this.onMouseUp (ev) 56 | this.canvas.onwheel = (ev) => this.onMouseWheel(ev) 57 | 58 | this.canvas.oncontextmenu = (ev) => ev.preventDefault() 59 | 60 | window.onkeydown = (ev) => this.onKeyDown(ev) 61 | 62 | this.refreshUI = () => { } 63 | 64 | this.debugDrawClean = false 65 | 66 | this.debugSkipIterationFrames = 0 67 | this.debugSkipIterationFramesCur = 0 68 | } 69 | 70 | 71 | run() 72 | { 73 | this.debugSkipIterationFramesCur++ 74 | if (this.debugSkipIterationFramesCur >= this.debugSkipIterationFrames) 75 | { 76 | this.debugSkipIterationFramesCur = 0 77 | 78 | if (this.solver != null && this.solver.readyToRun && this.components.length > 0) 79 | { 80 | for (const component of this.components) 81 | component.solverFrameBegin(this, this.solver) 82 | 83 | const iters = 50 84 | const initialTime = this.time 85 | 86 | for (let iter = 0; iter < iters; iter++) 87 | { 88 | this.time = initialTime + iter * this.timePerIteration 89 | 90 | this.solver.beginIteration() 91 | 92 | for (const component of this.components) 93 | component.solverIterationBegin(this, this.solver) 94 | 95 | this.solver.solve() 96 | 97 | for (const component of this.components) 98 | component.solverIterationEnd(this, this.solver) 99 | } 100 | 101 | for (const component of this.components) 102 | component.solverFrameEnd(this, this.solver) 103 | 104 | this.time = initialTime + iters * this.timePerIteration 105 | } 106 | 107 | for (const component of this.components) 108 | component.updateCurrentAnim(this, 1) 109 | } 110 | 111 | this.render() 112 | window.requestAnimationFrame(() => this.run()) 113 | } 114 | 115 | 116 | resize(width, height) 117 | { 118 | this.width = width 119 | this.height = height 120 | 121 | this.canvas.width = width 122 | this.canvas.height = height 123 | 124 | this.render() 125 | } 126 | 127 | 128 | getAbsolutePosition(pos) 129 | { 130 | const zoom = this.getZoomFactor(this.cameraZoomLevel) 131 | const rect = this.canvas.getBoundingClientRect() 132 | 133 | return { 134 | x: (pos.x - this.cameraPos.x) * zoom + this.width / 2 + rect.left, 135 | y: (pos.y - this.cameraPos.y) * zoom + this.height / 2 + rect.top 136 | } 137 | } 138 | 139 | 140 | getRawMousePos(ev) 141 | { 142 | const rect = this.canvas.getBoundingClientRect() 143 | return { 144 | x: ev.clientX - rect.left, 145 | y: ev.clientY - rect.top 146 | } 147 | } 148 | 149 | 150 | transformRawPos(pos) 151 | { 152 | const zoom = this.getZoomFactor(this.cameraZoomLevel) 153 | 154 | return { 155 | x: (pos.x - this.width / 2) / zoom + this.cameraPos.x, 156 | y: (pos.y - this.height / 2) / zoom + this.cameraPos.y 157 | } 158 | } 159 | 160 | 161 | snapPos(pos) 162 | { 163 | return { 164 | x: Math.round(pos.x / this.tileSize) * this.tileSize, 165 | y: Math.round(pos.y / this.tileSize) * this.tileSize 166 | } 167 | } 168 | 169 | 170 | getHoverData(pos) 171 | { 172 | let data = null 173 | 174 | for (let component of this.components) 175 | { 176 | const hover = component.getHover(pos) 177 | if (hover == null) 178 | continue 179 | 180 | if (data == null || 181 | hover.distSqr < data.distSqr) 182 | { 183 | data = { ...hover, component } 184 | } 185 | } 186 | 187 | return data 188 | } 189 | 190 | 191 | getZoomFactor(level) 192 | { 193 | if (level >= 0) 194 | return 1 + level 195 | else 196 | return 1 / (-level + 1) 197 | } 198 | 199 | 200 | onMouseDown(ev) 201 | { 202 | ev.preventDefault() 203 | 204 | if (this.mouseDown) 205 | return 206 | 207 | this.mousePosRaw = this.getRawMousePos(ev) 208 | this.mousePos = this.transformRawPos(this.mousePosRaw) 209 | this.mousePosSnapped = this.snapPos(this.mousePos) 210 | 211 | this.mouseDragOriginRaw = this.mousePosRaw 212 | this.mouseDragOrigin = this.mousePos 213 | this.mouseDragOriginSnapped = this.mousePosSnapped 214 | 215 | this.mouseDown = true 216 | this.mouseCurrentAction = null 217 | this.componentsForEditing = [] 218 | 219 | if (!ev.ctrlKey)// && (this.mouseCurrentHoverData.component == null || !this.mouseCurrentHoverData.component.isAnySelected())) 220 | this.unselectAll() 221 | 222 | if (ev.button != 0) 223 | { 224 | this.mouseCurrentAction = "pan" 225 | } 226 | 227 | else if (this.mouseAddComponentClass != null) 228 | { 229 | this.mouseCurrentAction = "drag" 230 | 231 | let component = new (this.mouseAddComponentClass)(this.mousePosSnapped) 232 | component.selected[1] = true 233 | component.dragStart() 234 | this.components.push(component) 235 | } 236 | 237 | else if (this.mouseCurrentHoverData != null) 238 | { 239 | this.mouseCurrentAction = "drag" 240 | 241 | for (let component of this.components) 242 | component.dragStart() 243 | 244 | if (this.mouseCurrentHoverData.kind == "full") 245 | { 246 | for (let i = 0; i < this.mouseCurrentHoverData.component.selected.length; i++) 247 | this.mouseCurrentHoverData.component.selected[i] = true 248 | 249 | for (let component of this.components) 250 | for (let i = 0; i < component.points.length; i++) 251 | for (let j = 0; j < this.mouseCurrentHoverData.component.points.length; j++) 252 | { 253 | if (component.points[i].x == this.mouseCurrentHoverData.component.points[j].x && 254 | component.points[i].y == this.mouseCurrentHoverData.component.points[j].y) 255 | component.selected[i] = true 256 | } 257 | } 258 | else if (this.mouseCurrentHoverData.kind == "vertex") 259 | { 260 | this.mouseCurrentHoverData.component.selected[this.mouseCurrentHoverData.index] = true 261 | } 262 | else if (this.mouseCurrentHoverData.kind == "junction") 263 | { 264 | const x = this.mouseCurrentHoverData.component.points[this.mouseCurrentHoverData.index].x 265 | const y = this.mouseCurrentHoverData.component.points[this.mouseCurrentHoverData.index].y 266 | 267 | for (let component of this.components) 268 | for (let i = 0; i < component.points.length; i++) 269 | { 270 | if (component.points[i].x == x && component.points[i].y == y) 271 | component.selected[i] = true 272 | } 273 | } 274 | } 275 | 276 | this.refreshUI() 277 | this.render() 278 | } 279 | 280 | 281 | onMouseMove(ev) 282 | { 283 | ev.preventDefault() 284 | 285 | const mousePosRawLast = this.mousePosRaw 286 | this.mousePosRaw = this.getRawMousePos(ev) 287 | this.mousePos = this.transformRawPos(this.mousePosRaw) 288 | this.mousePosSnapped = this.snapPos(this.mousePos) 289 | 290 | this.mouseCurrentHoverData = this.getHoverData(this.mousePos) 291 | 292 | if (this.mouseDown) 293 | { 294 | if (this.mouseCurrentAction == "pan") 295 | { 296 | const deltaPosRaw = { 297 | x: this.mousePosRaw.x - mousePosRawLast.x, 298 | y: this.mousePosRaw.y - mousePosRawLast.y 299 | } 300 | 301 | const zoom = this.getZoomFactor(this.cameraZoomLevel) 302 | this.cameraPos.x -= deltaPosRaw.x / zoom 303 | this.cameraPos.y -= deltaPosRaw.y / zoom 304 | } 305 | 306 | else if (this.mouseCurrentAction == "drag") 307 | { 308 | const dragPosSnapped = { 309 | x: this.mousePosSnapped.x - this.mouseDragOriginSnapped.x, 310 | y: this.mousePosSnapped.y - this.mouseDragOriginSnapped.y 311 | } 312 | 313 | for (let component of this.components) 314 | component.dragMove(this, dragPosSnapped) 315 | } 316 | 317 | this.refreshNodes() 318 | this.render() 319 | } 320 | } 321 | 322 | 323 | onMouseUp(ev) 324 | { 325 | ev.preventDefault() 326 | 327 | if (!this.mouseDown) 328 | return 329 | 330 | this.componentsForEditing = [] 331 | 332 | if (this.mouseCurrentAction == "pan" && this.mouseCurrentHoverData != null) 333 | this.componentsForEditing = [this.mouseCurrentHoverData.component] 334 | 335 | this.mouseDown = false 336 | this.removeDegenerateComponents() 337 | this.refreshNodes() 338 | this.render() 339 | this.refreshUI() 340 | } 341 | 342 | 343 | onMouseWheel(ev) 344 | { 345 | ev.preventDefault() 346 | 347 | if (this.mousePosRaw == null) 348 | return 349 | 350 | const prevMousePos = this.transformRawPos(this.mousePosRaw) 351 | 352 | this.cameraZoomLevel += (ev.deltaY > 0 ? -1 : ev.deltaY < 0 ? 1 : 0) 353 | 354 | const newMousePos = this.transformRawPos(this.mousePosRaw) 355 | 356 | this.cameraPos.x -= newMousePos.x - prevMousePos.x 357 | this.cameraPos.y -= newMousePos.y - prevMousePos.y 358 | 359 | this.render() 360 | } 361 | 362 | 363 | onKeyDown(ev) 364 | { 365 | if (ev.key == "Delete" || ev.key == "Backspace") 366 | { 367 | ev.preventDefault() 368 | 369 | const hasOneFullySelected = this.components.reduce((acc, c) => acc || c.isFullySelected(), false) 370 | 371 | for (let i = this.components.length - 1; i >= 0; i--) 372 | { 373 | if ((hasOneFullySelected && this.components[i].isFullySelected()) || 374 | (!hasOneFullySelected && this.components[i].isAnySelected())) 375 | { 376 | this.components.splice(i, 1) 377 | } 378 | } 379 | 380 | this.refreshNodes() 381 | } 382 | } 383 | 384 | 385 | unselectAll() 386 | { 387 | for (let component of this.components) 388 | for (let i = 0; i < component.selected.length; i++) 389 | component.selected[i] = false 390 | } 391 | 392 | 393 | removeComponentsForEditing() 394 | { 395 | for (const componentForEditing of this.componentsForEditing) 396 | this.components = this.components.filter(c => c !== componentForEditing) 397 | 398 | this.componentsForEditing = [] 399 | this.refreshNodes() 400 | this.render() 401 | } 402 | 403 | 404 | removeDegenerateComponents() 405 | { 406 | for (let i = this.components.length - 1; i >= 0; i--) 407 | { 408 | if (this.components[i].isDegenerate()) 409 | this.components.splice(i, 1) 410 | } 411 | } 412 | 413 | 414 | refreshNodes() 415 | { 416 | this.joints = new Map() 417 | this.nodes = [{}] 418 | this.groundNodeIndex = 0 419 | this.voltageSources = 0 420 | 421 | const jointKey = (p) => Math.floor(p.x / this.tileSize).toString() + "," + Math.floor(p.y / this.tileSize).toString() 422 | 423 | // Assign ground nodes. 424 | let hasGround = false 425 | for (let component of this.components) 426 | { 427 | if (component instanceof ComponentGround) 428 | { 429 | hasGround = true 430 | //const joint = { jointIndex: this.joints.size, nodeIndex: 0, pos: component.points[0], outgoingDirections: [], labelDirection: 0, visible: true } 431 | //this.joints.set(jointKey(component.points[0]), joint) 432 | } 433 | } 434 | 435 | // Assign ground node to a voltage source if no ground components. 436 | if (!hasGround) 437 | { 438 | for (let component of this.components) 439 | { 440 | if (component instanceof ComponentBattery || component instanceof ComponentVoltageSource) 441 | { 442 | const key = jointKey(component.points[0]) 443 | 444 | let joint = this.joints.get(key) 445 | if (!joint) 446 | { 447 | joint = { jointIndex: this.joints.size, nodeIndex: 0, pos: component.points[0], outgoingDirections: [], labelDirection: 0, visible: true } 448 | this.joints.set(key, joint) 449 | } 450 | 451 | break 452 | } 453 | } 454 | } 455 | 456 | // Assign joints. 457 | for (let component of this.components) 458 | { 459 | if (component.isVoltageSource) 460 | component.voltageSourceIndex = (this.voltageSources++) 461 | 462 | for (let i = 0; i < component.points.length; i++) 463 | { 464 | const key = jointKey(component.points[i]) 465 | 466 | let joint = this.joints.get(key) 467 | if (!joint) 468 | { 469 | joint = { jointIndex: this.joints.size, nodeIndex: -1, pos: component.points[i], outgoingDirections: [], labelDirection: 0, visible: true } 470 | this.joints.set(key, joint) 471 | } 472 | 473 | joint.outgoingDirections.push(component.getOutgoingDirectionFromNode(i)) 474 | } 475 | } 476 | 477 | // Assign nodes to joints. 478 | for (let component of this.components) 479 | { 480 | for (let i = 0; i < component.points.length; i++) 481 | { 482 | const isNode = !(i == 1 && component instanceof ComponentSingleEnded) 483 | 484 | const key = jointKey(component.points[i]) 485 | 486 | let joint = this.joints.get(key) 487 | if (isNode && joint.nodeIndex < 0) 488 | { 489 | joint.nodeIndex = this.nodes.length 490 | this.nodes.push({}) 491 | } 492 | 493 | component.nodes[i] = joint.nodeIndex 494 | component.joints[i] = joint.jointIndex 495 | } 496 | } 497 | 498 | // Find voltage label position for joints. 499 | for (let [key, joint] of this.joints) 500 | { 501 | if (joint.outgoingDirections.length == 1) 502 | joint.labelDirection = joint.outgoingDirections[0] + Math.PI 503 | 504 | else 505 | { 506 | joint.outgoingDirections.sort((a, b) => a - b) 507 | 508 | let biggestGapSize = 0 509 | for (let i = 0; i < joint.outgoingDirections.length; i++) 510 | { 511 | const iNext = (i + 1) % joint.outgoingDirections.length 512 | 513 | const curDir = joint.outgoingDirections[i] 514 | const nextDir = joint.outgoingDirections[iNext] 515 | 516 | const wrapAround = (nextDir < curDir ? Math.PI * 2 : 0) 517 | const gapSize = (wrapAround * 2 + nextDir) - (wrapAround + curDir) 518 | 519 | if (gapSize > biggestGapSize) 520 | { 521 | biggestGapSize = gapSize 522 | joint.labelDirection = curDir + gapSize / 2 523 | } 524 | } 525 | } 526 | } 527 | 528 | this.refreshSolver() 529 | } 530 | 531 | 532 | refreshSolver() 533 | { 534 | this.time = 0 535 | 536 | for (const component of this.components) 537 | component.reset(this) 538 | 539 | this.solver.stampBegin(this.nodes.length, this.voltageSources, this.groundNodeIndex) 540 | 541 | for (const component of this.components) 542 | component.solverBegin(this, this.solver) 543 | 544 | this.solver.stampEnd() 545 | } 546 | 547 | 548 | getVoltageSourceCurrent(voltageSourceIndex) 549 | { 550 | return this.solver.getVoltageSourceCurrent(voltageSourceIndex) 551 | } 552 | 553 | 554 | getNodeVoltage(nodeIndex) 555 | { 556 | return this.solver.getNodeVoltage(nodeIndex) 557 | } 558 | 559 | 560 | getVoltageColor(voltage) 561 | { 562 | const gray = 160 563 | 564 | if (!voltage) 565 | return "rgb(" + gray + "," + gray + "," + gray + ")" 566 | 567 | if (voltage > 0) 568 | { 569 | const factor = Math.min(1, voltage / 10) 570 | const cGreen = Math.floor(gray + factor * (255 - gray)) 571 | const cOther = Math.floor(gray - factor * gray) 572 | return "rgb(" + cOther + "," + cGreen + "," + cOther + ")" 573 | } 574 | else 575 | { 576 | const factor = Math.min(1, -voltage / 10) 577 | const cRed = Math.floor(gray + factor * (255 - gray)) 578 | const cOther = Math.floor(gray - factor * gray) 579 | return "rgb(" + cRed + "," + cOther + "," + cOther + ")" 580 | } 581 | } 582 | 583 | 584 | render() 585 | { 586 | this.ctx.save() 587 | 588 | if (this.debugDrawClean) 589 | this.ctx.clearRect(0, 0, this.width, this.height) 590 | else 591 | { 592 | this.ctx.fillStyle = "#000022" 593 | this.ctx.fillRect(0, 0, this.width, this.height) 594 | } 595 | 596 | if (this.components.length == 0) 597 | { 598 | this.ctx.font = "15px Verdana" 599 | this.ctx.textAlign = "center" 600 | this.ctx.textBaseline = "middle" 601 | this.ctx.fillStyle = "#aac" 602 | this.ctx.fillText("Select a tool and draw here!", this.width / 2, this.height / 2) 603 | } 604 | 605 | this.ctx.fillStyle = "#aac" 606 | this.ctx.font = "15px Verdana" 607 | this.ctx.textAlign = "left" 608 | this.ctx.textBaseline = "top" 609 | this.ctx.fillText("t = " + this.time.toFixed(3) + " s", 10, 10) 610 | 611 | const zoom = this.getZoomFactor(this.cameraZoomLevel) 612 | this.ctx.translate(this.width / 2, this.height / 2) 613 | this.ctx.scale(zoom, zoom) 614 | this.ctx.translate(-this.cameraPos.x, -this.cameraPos.y) 615 | 616 | this.ctx.lineWidth = 4 617 | this.ctx.lineCap = "round" 618 | 619 | this.ctx.strokeStyle = "#26a" 620 | this.ctx.fillStyle = "#26a" 621 | for (const component of this.components) 622 | component.renderSelection(this, this.ctx) 623 | 624 | this.ctx.strokeStyle = "#4af" 625 | this.ctx.fillStyle = "#4af" 626 | if (this.mouseCurrentHoverData != null && this.mouseAddComponentClass == null && !this.mouseDown) 627 | this.mouseCurrentHoverData.component.renderHover(this, this.ctx, this.mouseCurrentHoverData) 628 | 629 | this.ctx.strokeStyle = "#f80" 630 | this.ctx.fillStyle = "#f80" 631 | for (const component of this.componentsForEditing) 632 | component.renderEditing(this, this.ctx) 633 | 634 | for (const component of this.components) 635 | component.render(this, this.ctx) 636 | 637 | if (!this.mouseDown) 638 | for (const component of this.components) 639 | component.renderCurrent(this, this.ctx) 640 | 641 | this.drawNodeVoltages() 642 | //this.drawDebugNodes() 643 | 644 | if (!this.mouseDown && this.mousePosSnapped != null && this.mouseAddComponentClass != null) 645 | { 646 | this.ctx.fillStyle = "#eeeeee" 647 | this.ctx.beginPath() 648 | this.ctx.arc(this.mousePosSnapped.x, this.mousePosSnapped.y, 6, 0, Math.PI * 2) 649 | this.ctx.fill() 650 | } 651 | 652 | this.ctx.restore() 653 | } 654 | 655 | 656 | drawDebugNodes() 657 | { 658 | this.ctx.font = "15px Verdana" 659 | for (const component of this.components) 660 | for (let i = 0; i < component.points.length; i++) 661 | { 662 | this.ctx.fillStyle = (component.nodes[i] == this.groundNodeIndex ? "#888888" : "#ffffff") 663 | this.ctx.fillText(component.nodes[i].toString(), component.points[i].x - 15, component.points[i].y - 15) 664 | } 665 | 666 | this.ctx.fillStyle = "#00ff00" 667 | for (const component of this.components) 668 | if (component.isVoltageSource) 669 | this.ctx.fillText(component.voltageSourceIndex.toString(), component.points[1].x + 15, component.points[1].y - 15) 670 | } 671 | 672 | 673 | drawNodeVoltages() 674 | { 675 | this.ctx.font = "15px Verdana" 676 | this.ctx.textBaseline = "middle" 677 | 678 | for (const [key, joint] of this.joints) 679 | { 680 | if (!joint.visible) 681 | continue 682 | 683 | const xOffset = 15 * Math.cos(joint.labelDirection) 684 | const yOffset = 15 * -Math.sin(joint.labelDirection) 685 | 686 | if (Math.abs(xOffset) < Math.abs(yOffset) * 0.1) 687 | this.ctx.textAlign = "center" 688 | else if (xOffset > 0) 689 | this.ctx.textAlign = "left" 690 | else 691 | this.ctx.textAlign = "right" 692 | 693 | const v = this.getNodeVoltage(joint.nodeIndex) 694 | const str = v.toFixed(3) + " V" 695 | 696 | this.ctx.fillStyle = this.getVoltageColor(v) 697 | this.ctx.fillText(str, joint.pos.x + xOffset, joint.pos.y + yOffset) 698 | } 699 | } 700 | 701 | 702 | fitCircuitToCamera() 703 | { 704 | this.cameraPos = { x: 0, y: 0 } 705 | this.cameraZoomLevel = 0 706 | 707 | let totalBBox = null 708 | for (const c of this.components) 709 | { 710 | const bbox = c.getBBox() 711 | if (totalBBox == null) 712 | totalBBox = bbox 713 | else 714 | { 715 | totalBBox.xMin = Math.min(totalBBox.xMin, bbox.xMin) 716 | totalBBox.yMin = Math.min(totalBBox.yMin, bbox.yMin) 717 | totalBBox.xMax = Math.max(totalBBox.xMax, bbox.xMax) 718 | totalBBox.yMax = Math.max(totalBBox.yMax, bbox.yMax) 719 | } 720 | } 721 | 722 | if (totalBBox != null) 723 | { 724 | this.cameraPos.x = (totalBBox.xMin + totalBBox.xMax) / 2 725 | this.cameraPos.y = (totalBBox.yMin + totalBBox.yMax) / 2 726 | 727 | const circuitW = totalBBox.xMax - totalBBox.xMin + this.tileSize * 3 728 | const circuitH = totalBBox.yMax - totalBBox.yMin + this.tileSize * 3 729 | 730 | while (this.cameraZoomLevel > -50) 731 | { 732 | const screenMin = this.transformRawPos({ x: 0, y: 0 }) 733 | const screenMax = this.transformRawPos({ x: this.width, y: this.height }) 734 | 735 | const screenW = screenMax.x - screenMin.x 736 | const screenH = screenMax.y - screenMin.y 737 | 738 | if (screenW >= circuitW && screenH >= circuitH) 739 | break 740 | 741 | this.cameraZoomLevel -= 1 742 | } 743 | } 744 | } 745 | 746 | 747 | saveToString() 748 | { 749 | let str = "0," 750 | str += this.joints.size + "," 751 | 752 | for (const [key, joint] of this.joints) 753 | { 754 | str += (joint.pos.x / this.tileSize).toString() + "," 755 | str += (joint.pos.y / this.tileSize).toString() + "," 756 | } 757 | 758 | for (const component of this.components) 759 | { 760 | str += component.constructor.getSaveId() + "," 761 | str += component.saveToString(this) 762 | } 763 | 764 | return str 765 | } 766 | 767 | 768 | loadFromString(str) 769 | { 770 | let strParts = str.split(",") 771 | 772 | let reader = 773 | { 774 | index: 0, 775 | isOver() { return this.index >= strParts.length }, 776 | read() { return strParts[this.index++] }, 777 | readNumber() { return MathUtils.stringWithUnitPrefixToValue(strParts[this.index++]) } 778 | } 779 | 780 | let loadData = 781 | { 782 | joints: [] 783 | } 784 | 785 | const version = parseInt(reader.read()) 786 | const jointNum = parseInt(reader.read()) 787 | 788 | for (let i = 0; i < jointNum; i++) 789 | { 790 | const x = parseInt(reader.read()) * this.tileSize 791 | const y = parseInt(reader.read()) * this.tileSize 792 | loadData.joints.push({ x, y }) 793 | } 794 | 795 | const componentClasses = 796 | [ 797 | ComponentWire, 798 | ComponentBattery, 799 | ComponentResistor, 800 | ComponentCurrentSource, 801 | ComponentCapacitor, 802 | ComponentInductor, 803 | ComponentVoltageSource, 804 | ComponentGround, 805 | ] 806 | 807 | let componentIds = new Map() 808 | for (const c of componentClasses) 809 | componentIds.set(c.getSaveId(), c) 810 | 811 | while (!reader.isOver()) 812 | { 813 | const id = reader.read() 814 | if (id == null || id == "") 815 | break 816 | 817 | const componentClass = componentIds.get(id) 818 | const component = new componentClass({ x: 0, y: 0 }) 819 | component.loadFromString(this, loadData, reader) 820 | 821 | this.components.push(component) 822 | } 823 | 824 | this.removeDegenerateComponents() 825 | this.fitCircuitToCamera() 826 | this.refreshNodes() 827 | this.render() 828 | } 829 | } --------------------------------------------------------------------------------