You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CircuitSim.svelte 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. <main>
  2. <canvas bind:this={canvas}></canvas>
  3. <div class="controls">
  4. {#each components as comp}
  5. <button on:click={sim.addNodeAtCursor(new comp.ctor())}>{comp.name}</button>
  6. {/each}
  7. </div>
  8. </main>
  9. <style>
  10. main, canvas {
  11. width: 100vw;
  12. height: 100vh;
  13. position: absolute;
  14. top: 0px;
  15. left: 0px;
  16. }
  17. .controls {
  18. position: absolute;
  19. bottom: 0px;
  20. left: 0px;
  21. width: 100%;
  22. height: 40px;
  23. }
  24. .controls > * {
  25. box-sizing: border-box;
  26. height: 100%;
  27. }
  28. </style>
  29. <script>
  30. import {onMount, onDestroy} from 'svelte';
  31. export let components;
  32. export let nodes = [];
  33. class LogicSim {
  34. constructor(can) {
  35. this.scale = 40;
  36. this.can = can;
  37. this.ctx = can.getContext("2d");
  38. this.nodes = [];
  39. this.frameRequested = false;
  40. this.clickCancelled = false;
  41. this.currentTouch = null;
  42. this.x = 0;
  43. this.y = 0;
  44. this.cursorX = 0;
  45. this.cursorY = 0;
  46. this.tooltip = null;
  47. this.selectedNodes = [];
  48. this.mouseMoveStart = null;
  49. this.currentLink = null;
  50. this.selection = null;
  51. this.cursorAttachedNode;
  52. this.requestFrame();
  53. window.addEventListener("resize", () => {
  54. this.requestFrame();
  55. });
  56. window.addEventListener("keydown", evt => {
  57. this.onKeyDown(evt.key);
  58. });
  59. can.addEventListener("wheel", evt => {
  60. this.onScroll(evt.deltaY / 10);
  61. });
  62. can.addEventListener("mousedown", evt => {
  63. this.onMouseDown(evt.offsetX, evt.offsetY, evt.buttons, evt);
  64. });
  65. can.addEventListener("mouseup", evt => {
  66. this.onMouseUp();
  67. });
  68. can.addEventListener("mousemove", evt => {
  69. this.onMouseMove(evt.offsetX, evt.offsetY, evt.movementX, evt.movementY, evt.buttons);
  70. });
  71. can.addEventListener("click", evt => {
  72. this.onClick(evt.offsetX, evt.offsetY);
  73. });
  74. can.addEventListener("touchstart", evt => {
  75. if (this.currentTouch != null || evt.changedTouches.length == 0) {
  76. return;
  77. }
  78. let relevantTouch = evt.changedTouches[0];
  79. this.currentTouch = relevantTouch;
  80. this.onMouseDown(relevantTouch.clientX, relevantTouch.clientY, 1, evt);
  81. });
  82. can.addEventListener("touchend", evt => {
  83. if (this.currentTouch == null) {
  84. return;
  85. }
  86. let relevantTouch = null;
  87. for (let touch of evt.changedTouches) {
  88. if (touch.identifier == this.currentTouch.identifier) {
  89. relevantTouch = touch;
  90. break;
  91. }
  92. }
  93. if (relevantTouch == null) {
  94. return;
  95. }
  96. this.currentTouch = null;
  97. tis.onMouseUp();
  98. });
  99. can.addEventListener("touchmove", evt => {
  100. if (this.currentTouch == null) {
  101. return;
  102. }
  103. let relevantTouch = null;
  104. for (let touch of evt.changedTouches) {
  105. if (touch.identifier == this.currentTouch.identifier) {
  106. relevantTouch = touch;
  107. break;
  108. }
  109. }
  110. if (relevantTouch == null) {
  111. return;
  112. }
  113. let deltaX = this.currentTouch.clientX - relevantTouch.clientX;
  114. let deltaY = this.currentTouch.clientY - relevantTouch.clientY;
  115. this.currentTouch = relevantTouch;
  116. this.onMouseMove(relevantTouch.clientX, relevantTouch.clientY, -deltaX, -deltaY, 1);
  117. });
  118. }
  119. onKeyDown(key) {
  120. if (key == "Escape" || key == "q") {
  121. this.tooltip = null;
  122. this.selectedNodes = [];
  123. this.mouseMoveStart = null;
  124. this.currentLink = null;
  125. this.selection = null;
  126. if (this.cursorAttachedNode != null) {
  127. this.deleteNode(this.cursorAttachedNode);
  128. this.cursorAttachedNode = null;
  129. }
  130. this.requestFrame();
  131. } else if (this.currentLink != null && key == "Backspace") {
  132. if (this.currentLink.path.length > 0) {
  133. this.currentLink.path.pop();
  134. } else {
  135. this.currentLink = null;
  136. }
  137. this.requestFrame();
  138. } else if (key == "Delete" || key == "Backspace") {
  139. let newSelectedNodes = [];
  140. for (let node of this.selectedNodes) {
  141. if (node.protected) {
  142. newSelectedNodes.push(node);
  143. } else {
  144. this.deleteNode(node);
  145. }
  146. }
  147. this.selectedNodes = newSelectedNodes;
  148. } else if (key == "Enter") {
  149. for (let node of this.selectedNodes) {
  150. if (node.activate) {
  151. node.activate();
  152. this.requestFrame();
  153. }
  154. }
  155. }
  156. }
  157. onScroll(delta) {
  158. this.scale -= delta * (this.scale / 40);
  159. if (this.scale > 200) {
  160. this.scale = 200;
  161. } else if (this.scale < 10) {
  162. this.scale = 10;
  163. }
  164. this.requestFrame();
  165. }
  166. onMouseDown(offsetX, offsetY, buttons, keys) {
  167. this.clickCancelled = false;
  168. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  169. let node = this.getNodeAt(x, y);
  170. this.mouseMoveStart = {x, y, node};
  171. if (keys.shiftKey && buttons == 1) {
  172. this.selectedNodes = [];
  173. this.selection = {fromX: x, fromY: y, toX: x, toY: y};
  174. this.tooltip = null;
  175. this.requestFrame();
  176. } else if (node != null && buttons == 1) {
  177. if (this.selectedNodes.indexOf(node) == -1) {
  178. this.selectedNodes = [node];
  179. this.requestFrame();
  180. }
  181. } else if (buttons == 1) {
  182. this.selectedNodes = [];
  183. this.requestFrame();
  184. }
  185. }
  186. onMouseUp() {
  187. this.mouseMoveStart = null;
  188. if (this.selection) {
  189. this.selectedNodes = [];
  190. let [x1, y1] = [this.selection.fromX, this.selection.fromY];
  191. let [x2, y2] = [this.selection.toX, this.selection.toY];
  192. // Make sure we actually have a rect from top-left to bottom-right
  193. let tmp;
  194. if (x1 > x2) {
  195. tmp = x1;
  196. x1 = x2;
  197. x2 = tmp;
  198. }
  199. if (y1 > y2) {
  200. tmp = y1;
  201. y1 = y2;
  202. y2 = tmp;
  203. }
  204. for (let node of this.nodes) {
  205. let nodeX1 = node.x;
  206. let nodeY1 = node.y - 0.5;
  207. let nodeX2 = nodeX1 + node.width;
  208. let nodeY2 = nodeY1 + node.height;
  209. if (x1 < nodeX2 && x2 > nodeX1 && y1 < nodeY2 && y2 > nodeY1) {
  210. this.selectedNodes.push(node);
  211. }
  212. }
  213. this.selection = null;
  214. this.requestFrame();
  215. }
  216. }
  217. onMouseMove(offsetX, offsetY, movementX, movementY, buttons) {
  218. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  219. this.cursorX = x;
  220. this.cursorY = y;
  221. let dx = 0, dy = 0;
  222. if (this.mouseMoveStart) {
  223. dx = x - this.mouseMoveStart.x;
  224. dy = y - this.mouseMoveStart.y;
  225. }
  226. if (
  227. buttons == 1 && this.selection == null &&
  228. this.mouseMoveStart != null && this.currentLink == null) {
  229. if (this.mouseMoveStart.node == null) {
  230. this.x -= movementX / this.scale;
  231. this.y -= movementY / this.scale;
  232. this.requestFrame();
  233. } else {
  234. let nodeDX = Math.round(dx);
  235. let nodeDY = Math.round(dy);
  236. if (nodeDX != 0 || nodeDY != 0) {
  237. this.clickCancelled = true;
  238. this.mouseMoveStart.x += nodeDX;
  239. this.mouseMoveStart.y += nodeDY;
  240. for (let node of this.selectedNodes) {
  241. node.x += nodeDX;
  242. node.y += nodeDY;
  243. }
  244. this.requestFrame();
  245. }
  246. }
  247. }
  248. let node = this.getNodeAt(x, y);
  249. let io = null;
  250. if (node != null) {
  251. io = this.getNodeIOAt(node, x, y);
  252. }
  253. if (io == null && this.tooltip == null) {
  254. // Do nothing
  255. } else if (io == null && this.tooltip != null) {
  256. this.tooltip = null;
  257. this.requestFrame();
  258. } else {
  259. this.tooltip = {
  260. text: io.io.name,
  261. x, y,
  262. };
  263. this.requestFrame();
  264. }
  265. if (this.selection != null) {
  266. this.selection.toX = x;
  267. this.selection.toY = y;
  268. this.requestFrame();
  269. }
  270. if (this.currentLink != null) {
  271. this.requestFrame();
  272. }
  273. if (this.cursorAttachedNode != null) {
  274. let node = this.cursorAttachedNode;
  275. let newX = Math.round(x - node.width / 2);
  276. let newY = Math.round(y - node.height / 2 + 0.5);
  277. if (newX != node.x || newY != node.y) {
  278. node.x = newX;
  279. node.y = newY;
  280. this.requestFrame();
  281. }
  282. }
  283. }
  284. onClick(offsetX, offsetY) {
  285. if (this.clickCancelled) {
  286. return;
  287. }
  288. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  289. if (this.cursorAttachedNode != null) {
  290. let node = this.cursorAttachedNode;
  291. node.x = Math.round(x - node.width / 2);
  292. node.y = Math.round(y - node.height / 2 + 0.5);
  293. this.cursorAttachedNode = null;
  294. return;
  295. }
  296. let node = this.getNodeAt(x, y);
  297. let io = null;
  298. if (node != null) {
  299. io = this.getNodeIOAt(node, x, y);
  300. }
  301. if (this.currentLink != null) {
  302. if (io) {
  303. let fromNode = this.currentLink.from.node;
  304. let fromIO = this.currentLink.from.io;
  305. if (fromIO.type == "input" && io.type == "output") {
  306. this.currentLink.path.reverse();
  307. io.io.link.connect(fromNode, fromIO.index, this.currentLink.path);
  308. } else if (fromIO.type == "output" && io.type == "input") {
  309. fromIO.io.link.connect(node, io.index, this.currentLink.path);
  310. }
  311. if (fromIO.type != io.type) {
  312. this.currentLink = null;
  313. this.requestFrame();
  314. }
  315. } else {
  316. let path = this.currentLink.path;
  317. path.push({x: Math.round(x), y: Math.round(y)});
  318. this.requestFrame();
  319. }
  320. } else if (io != null) {
  321. this.currentLink = {from: {node, io}, path: []};
  322. } else {
  323. if (node && node.activate) {
  324. node.activate();
  325. this.requestFrame();
  326. }
  327. }
  328. }
  329. deleteNode(node) {
  330. for (let i = 0; i < node.inputs.length; ++i) {
  331. let input = node.inputs[i];
  332. for (let link of input.links) {
  333. link.disconnect(node, i);
  334. }
  335. }
  336. for (let out of node.outputs) {
  337. out.link.destroy();
  338. }
  339. this.nodes.splice(this.nodes.indexOf(node), 1);
  340. }
  341. coordsFromScreenPos(screenX, screenY) {
  342. return [
  343. (screenX - this.can.offsetWidth / 2) / this.scale + this.x,
  344. (screenY - this.can.offsetHeight / 2) / this.scale + this.y,
  345. ];
  346. }
  347. getNodeAt(x, y) {
  348. for (let i = this.nodes.length - 1; i >= 0; --i) {
  349. let node = this.nodes[i];
  350. let nodeX1 = node.x - 0.5;
  351. let nodeY1 = node.y - 0.5;
  352. let nodeX2 = node.x + node.width + 0.5;
  353. let nodeY2 = node.y - 0.5 + node.height;
  354. if (
  355. x >= nodeX1 && x <= nodeX2 &&
  356. y >= nodeY1 && y <= nodeY2) {
  357. return node;
  358. }
  359. }
  360. return null;
  361. }
  362. getNodeIOAt(node, x, y) {
  363. for (let n = 0; n < node.inputs.length; ++n) {
  364. let dx = node.x - x;
  365. let dy = node.y + n - y;
  366. let dist = Math.abs(Math.sqrt(dx * dx + dy * dy));
  367. if (dist < 0.5) {
  368. return {type: "input", io: node.inputs[n], index: n};
  369. }
  370. }
  371. for (let n = 0; n < node.outputs.length; ++n) {
  372. let dx = node.x + node.width - x;
  373. let dy = node.y + n - y;
  374. let dist = Math.abs(Math.sqrt(dx * dx + dy * dy));
  375. if (dist < 0.5) {
  376. return {type: "output", io: node.outputs[n], index: n};
  377. }
  378. }
  379. return null;
  380. }
  381. requestFrame() {
  382. if (this.frameRequested) {
  383. return;
  384. }
  385. this.frameRequested = true;
  386. window.requestAnimationFrame(() => {
  387. this.frameRequested = false;
  388. this.draw();
  389. });
  390. }
  391. addNodeAtCursor(node) {
  392. node.x = Math.round(this.cursorX - node.width / 2);
  393. node.y = Math.round(this.cursorY - node.height / 2 + 0.5);
  394. this.nodes.push(node);
  395. this.cursorAttachedNode = node;
  396. this.requestFrame();
  397. }
  398. draw() {
  399. let pixelRatio = 1;
  400. if (window.devicePixelRatio != null) {
  401. pixelRatio = window.devicePixelRatio;
  402. }
  403. this.can.width = this.can.offsetWidth * pixelRatio;
  404. this.can.height = this.can.offsetHeight * pixelRatio;
  405. let canWidth = this.can.width / this.scale;
  406. let canHeight = this.can.height / this.scale;
  407. this.ctx.save();
  408. this.ctx.translate(this.can.width / 2, this.can.height / 2);
  409. this.ctx.scale(this.scale * pixelRatio, this.scale * pixelRatio);
  410. this.ctx.translate(-this.x, -this.y);
  411. this.ctx.strokeStyle = "rgba(255, 255, 255, 0.2)";
  412. this.ctx.lineWidth = 1 / this.scale;
  413. let gridStartX = Math.floor(this.x - canWidth / 2);
  414. let gridStartY = Math.floor(this.y - canHeight / 2);
  415. for (let x = gridStartX; x < canWidth + gridStartX + 1; x += 1) {
  416. this.ctx.beginPath();
  417. this.ctx.moveTo(x, gridStartY - canHeight - 1);
  418. this.ctx.lineTo(x, gridStartY + canHeight + 1);
  419. this.ctx.stroke();
  420. }
  421. for (let y = gridStartY; y < canHeight + gridStartY + 1; y += 1) {
  422. this.ctx.beginPath();
  423. this.ctx.moveTo(gridStartX - canWidth - 1, y);
  424. this.ctx.lineTo(gridStartX + canWidth + 1, y);
  425. this.ctx.stroke();
  426. }
  427. this.ctx.font = "0.8px Arial";
  428. this.ctx.lineWidth = 0.1;
  429. for (let node of this.nodes) {
  430. for (let output of node.outputs) {
  431. let link = output.link;
  432. if (link.current) {
  433. this.ctx.strokeStyle = "red";
  434. } else {
  435. this.ctx.strokeStyle = "brown";
  436. }
  437. for (let conn of link.connections) {
  438. this.ctx.beginPath();
  439. this.ctx.moveTo(link.from.x + link.from.width, link.from.y + link.index);
  440. for (let point of conn.path) {
  441. this.ctx.lineTo(point.x, point.y);
  442. }
  443. this.ctx.lineTo(conn.node.x, conn.node.y + conn.index);
  444. this.ctx.stroke();
  445. }
  446. }
  447. }
  448. this.ctx.strokeStyle = "rgba(255, 255, 255, 1)";
  449. this.ctx.lineWidth = 0.05;
  450. this.ctx.setLineDash([0.2, 0.1]);
  451. for (let node of this.selectedNodes) {
  452. this.ctx.beginPath();
  453. this.ctx.strokeRect(node.x - 0.1, node.y - 0.5 - 0.1, node.width + 0.2, node.height + 0.2);
  454. }
  455. this.ctx.lineWidth = 0.1;
  456. this.ctx.setLineDash([]);
  457. for (let node of this.nodes) {
  458. if (node.lit) {
  459. this.ctx.fillStyle = "red";
  460. } else {
  461. this.ctx.fillStyle = "brown";
  462. }
  463. this.ctx.strokeStyle = "black";
  464. let metrics = this.ctx.measureText(node.name);
  465. let textWidth = metrics.width;
  466. this.ctx.fillRect(node.x, node.y - 0.5, node.width, node.height);
  467. this.ctx.fillStyle = "white";
  468. this.ctx.fillText(
  469. node.name,
  470. node.x + node.width / 2 - textWidth / 2,
  471. node.y - 0.5 + 0.8);
  472. this.ctx.fillStyle = "black";
  473. this.ctx.strokeStyle = "white";
  474. for (let y = 0; y < node.inputs.length; ++y) {
  475. this.ctx.beginPath();
  476. this.ctx.arc(node.x, node.y + y, 0.45, 0, 2 * Math.PI);
  477. this.ctx.fill();
  478. this.ctx.stroke();
  479. }
  480. for (let y = 0; y < node.outputs.length; ++y) {
  481. this.ctx.beginPath();
  482. this.ctx.arc(node.x + node.width, node.y + y, 0.45, 0, 2 * Math.PI);
  483. this.ctx.fill();
  484. this.ctx.stroke();
  485. }
  486. }
  487. if (this.currentLink != null) {
  488. this.ctx.strokeStyle = "brown";
  489. let from = this.currentLink.from;
  490. let fromX = from.node.x;
  491. let fromY = from.node.y + from.io.index;
  492. if (from.io.type == "output") {
  493. fromX += from.node.width;
  494. }
  495. this.ctx.beginPath();
  496. this.ctx.moveTo(fromX, fromY);
  497. for (let el of this.currentLink.path) {
  498. this.ctx.lineTo(el.x, el.y);
  499. }
  500. this.ctx.lineTo(this.cursorX, this.cursorY);
  501. this.ctx.stroke();
  502. }
  503. if (this.tooltip != null) {
  504. this.ctx.fillStyle = "black";
  505. this.ctx.strokeStyle = "grey";
  506. let metrics = this.ctx.measureText(this.tooltip.text);
  507. let textWidth = metrics.width;
  508. let textAscent = metrics.actualBoundingBoxAscent;
  509. let x = this.tooltip.x - (textWidth / 4);
  510. let y = this.tooltip.y - 1.2;
  511. this.ctx.beginPath();
  512. this.ctx.rect(
  513. x - 0.17, y - 0.2, textWidth + 0.4, 1.1);
  514. this.ctx.fill();
  515. this.ctx.stroke();
  516. this.ctx.fillStyle = "white";
  517. this.ctx.fillText(this.tooltip.text, x, y + 0.6);
  518. }
  519. if (this.selection != null) {
  520. this.ctx.fillStyle = "rgba(100, 100, 250, 0.2)";
  521. this.ctx.strokeStyle = "black";
  522. this.ctx.beginPath();
  523. this.ctx.rect(
  524. this.selection.fromX, this.selection.fromY,
  525. this.selection.toX - this.selection.fromX,
  526. this.selection.toY - this.selection.fromY);
  527. this.ctx.fill();
  528. this.ctx.stroke();
  529. }
  530. this.ctx.restore();
  531. }
  532. update() {
  533. for (let node of this.nodes) {
  534. node.tick();
  535. }
  536. for (let node of this.nodes) {
  537. node.commit();
  538. }
  539. this.requestFrame();
  540. }
  541. }
  542. let canvas;
  543. let sim;
  544. let interval = null;
  545. onMount(() => {
  546. sim = new LogicSim(canvas);
  547. for (let node of nodes) {
  548. sim.nodes.push(node);
  549. }
  550. interval = setInterval(sim.update.bind(sim), 100);
  551. });
  552. onDestroy(() => {
  553. if (interval != null) {
  554. clearInterval(interval);
  555. interval = null;
  556. }
  557. });
  558. </script>