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 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  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 (key == "Delete") {
  132. for (let node of this.selectedNodes) {
  133. this.deleteNode(node);
  134. }
  135. this.selectedNodes = [];
  136. } else if (key == "Enter") {
  137. for (let node of this.selectedNodes) {
  138. if (node.activate) {
  139. node.activate();
  140. this.requestFrame();
  141. }
  142. }
  143. }
  144. }
  145. onScroll(delta) {
  146. this.scale -= delta * (this.scale / 40);
  147. if (this.scale > 200) {
  148. this.scale = 200;
  149. } else if (this.scale < 10) {
  150. this.scale = 10;
  151. }
  152. this.requestFrame();
  153. }
  154. onMouseDown(offsetX, offsetY, buttons, keys) {
  155. this.clickCancelled = false;
  156. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  157. let node = this.getNodeAt(x, y);
  158. this.mouseMoveStart = {x, y, node};
  159. if (keys.shiftKey && buttons == 1) {
  160. this.selectedNodes = [];
  161. this.selection = {fromX: x, fromY: y, toX: x, toY: y};
  162. this.tooltip = null;
  163. this.requestFrame();
  164. } else if (node != null && buttons == 1) {
  165. if (this.selectedNodes.indexOf(node) == -1) {
  166. this.selectedNodes = [node];
  167. this.requestFrame();
  168. }
  169. } else if (buttons == 1) {
  170. this.selectedNodes = [];
  171. this.requestFrame();
  172. }
  173. }
  174. onMouseUp() {
  175. this.mouseMoveStart = null;
  176. if (this.selection) {
  177. this.selectedNodes = [];
  178. let [x1, y1] = [this.selection.fromX, this.selection.fromY];
  179. let [x2, y2] = [this.selection.toX, this.selection.toY];
  180. // Make sure we actually have a rect from top-left to bottom-right
  181. let tmp;
  182. if (x1 > x2) {
  183. tmp = x1;
  184. x1 = x2;
  185. x2 = tmp;
  186. }
  187. if (y1 > y2) {
  188. tmp = y1;
  189. y1 = y2;
  190. y2 = tmp;
  191. }
  192. for (let node of this.nodes) {
  193. let nodeX1 = node.x;
  194. let nodeY1 = node.y - 0.5;
  195. let nodeX2 = nodeX1 + node.width;
  196. let nodeY2 = nodeY1 + node.height;
  197. if (x1 < nodeX2 && x2 > nodeX1 && y1 < nodeY2 && y2 > nodeY1) {
  198. this.selectedNodes.push(node);
  199. }
  200. }
  201. this.selection = null;
  202. this.requestFrame();
  203. }
  204. }
  205. onMouseMove(offsetX, offsetY, movementX, movementY, buttons) {
  206. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  207. this.cursorX = x;
  208. this.cursorY = y;
  209. let dx = 0, dy = 0;
  210. if (this.mouseMoveStart) {
  211. dx = x - this.mouseMoveStart.x;
  212. dy = y - this.mouseMoveStart.y;
  213. }
  214. if (
  215. buttons == 1 && this.selection == null &&
  216. this.mouseMoveStart != null && this.currentLink == null) {
  217. if (this.mouseMoveStart.node == null) {
  218. this.x -= movementX / this.scale;
  219. this.y -= movementY / this.scale;
  220. this.requestFrame();
  221. } else {
  222. let nodeDX = Math.round(dx);
  223. let nodeDY = Math.round(dy);
  224. if (nodeDX != 0 || nodeDY != 0) {
  225. this.clickCancelled = true;
  226. this.mouseMoveStart.x += nodeDX;
  227. this.mouseMoveStart.y += nodeDY;
  228. for (let node of this.selectedNodes) {
  229. node.x += nodeDX;
  230. node.y += nodeDY;
  231. }
  232. this.requestFrame();
  233. }
  234. }
  235. }
  236. let node = this.getNodeAt(x, y);
  237. let io = null;
  238. if (node != null) {
  239. io = this.getNodeIOAt(node, x, y);
  240. }
  241. if (io == null && this.tooltip == null) {
  242. // Do nothing
  243. } else if (io == null && this.tooltip != null) {
  244. this.tooltip = null;
  245. this.requestFrame();
  246. } else {
  247. this.tooltip = {
  248. text: io.io.name,
  249. x, y,
  250. };
  251. this.requestFrame();
  252. }
  253. if (this.selection != null) {
  254. this.selection.toX = x;
  255. this.selection.toY = y;
  256. this.requestFrame();
  257. }
  258. if (this.currentLink != null) {
  259. let p = this.currentLink.path[this.currentLink.path.length - 1];
  260. p.x = x;
  261. p.y = y;
  262. this.requestFrame();
  263. }
  264. if (this.cursorAttachedNode != null) {
  265. let node = this.cursorAttachedNode;
  266. let newX = Math.round(x - node.width / 2);
  267. let newY = Math.round(y - node.height / 2 + 0.5);
  268. if (newX != node.x || newY != node.y) {
  269. node.x = newX;
  270. node.y = newY;
  271. this.requestFrame();
  272. }
  273. }
  274. }
  275. onClick(offsetX, offsetY) {
  276. if (this.clickCancelled) {
  277. return;
  278. }
  279. let [x, y] = this.coordsFromScreenPos(offsetX, offsetY);
  280. if (this.cursorAttachedNode != null) {
  281. let node = this.cursorAttachedNode;
  282. node.x = Math.round(x - node.width / 2);
  283. node.y = Math.round(y - node.height / 2 + 0.5);
  284. this.cursorAttachedNode = null;
  285. return;
  286. }
  287. let node = this.getNodeAt(x, y);
  288. let io = null;
  289. if (node != null) {
  290. io = this.getNodeIOAt(node, x, y);
  291. }
  292. if (this.currentLink != null) {
  293. if (io) {
  294. let fromNode = this.currentLink.from.node;
  295. let fromIO = this.currentLink.from.io;
  296. if (fromIO.type == "input" && io.type == "output") {
  297. io.io.link.connect(fromNode, fromIO.index);
  298. } else if (fromIO.type == "output" && io.type == "input") {
  299. fromIO.io.link.connect(node, io.index);
  300. }
  301. if (fromIO.type != io.type) {
  302. this.currentLink = null;
  303. this.requestFrame();
  304. }
  305. } else {
  306. let path = this.currentLink.path;
  307. path[path.length - 1].x = Math.round(path[path.length - 1].x);
  308. path[path.length - 1].y = Math.round(path[path.length - 1].y);
  309. path.push({x, y});
  310. this.requestFrame();
  311. }
  312. } else if (io != null) {
  313. this.currentLink = {from: {node, io}, path: [{x, y}]};
  314. } else {
  315. if (node && node.activate) {
  316. node.activate();
  317. this.requestFrame();
  318. }
  319. }
  320. }
  321. deleteNode(node) {
  322. for (let i = 0; i < node.inputs.length; ++i) {
  323. let input = node.inputs[i];
  324. for (let link of input.links) {
  325. link.disconnect(node, i);
  326. }
  327. }
  328. for (let out of node.outputs) {
  329. out.link.destroy();
  330. }
  331. this.nodes.splice(this.nodes.indexOf(node), 1);
  332. }
  333. coordsFromScreenPos(screenX, screenY) {
  334. return [
  335. (screenX - this.can.offsetWidth / 2) / this.scale + this.x,
  336. (screenY - this.can.offsetHeight / 2) / this.scale + this.y,
  337. ];
  338. }
  339. getNodeAt(x, y) {
  340. for (let i = this.nodes.length - 1; i >= 0; --i) {
  341. let node = this.nodes[i];
  342. let nodeX1 = node.x - 0.5;
  343. let nodeY1 = node.y - 0.5;
  344. let nodeX2 = node.x + node.width + 0.5;
  345. let nodeY2 = node.y - 0.5 + node.height;
  346. if (
  347. x >= nodeX1 && x <= nodeX2 &&
  348. y >= nodeY1 && y <= nodeY2) {
  349. return node;
  350. }
  351. }
  352. return null;
  353. }
  354. getNodeIOAt(node, x, y) {
  355. for (let n = 0; n < node.inputs.length; ++n) {
  356. let dx = node.x - x;
  357. let dy = node.y + n - y;
  358. let dist = Math.abs(Math.sqrt(dx * dx + dy * dy));
  359. if (dist < 0.5) {
  360. return {type: "input", io: node.inputs[n], index: n};
  361. }
  362. }
  363. for (let n = 0; n < node.outputs.length; ++n) {
  364. let dx = node.x + node.width - 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: "output", io: node.outputs[n], index: n};
  369. }
  370. }
  371. return null;
  372. }
  373. requestFrame() {
  374. if (this.frameRequested) {
  375. return;
  376. }
  377. this.frameRequested = true;
  378. window.requestAnimationFrame(() => {
  379. this.frameRequested = false;
  380. this.draw();
  381. });
  382. }
  383. addNodeAtCursor(node) {
  384. node.x = Math.round(this.cursorX - node.width / 2);
  385. node.y = Math.round(this.cursorY - node.height / 2 + 0.5);
  386. this.nodes.push(node);
  387. this.cursorAttachedNode = node;
  388. this.requestFrame();
  389. }
  390. draw() {
  391. let pixelRatio = 1;
  392. if (window.devicePixelRatio != null) {
  393. pixelRatio = window.devicePixelRatio;
  394. }
  395. this.can.width = this.can.offsetWidth * pixelRatio;
  396. this.can.height = this.can.offsetHeight * pixelRatio;
  397. let canWidth = this.can.width / this.scale;
  398. let canHeight = this.can.height / this.scale;
  399. this.ctx.save();
  400. this.ctx.translate(this.can.width / 2, this.can.height / 2);
  401. this.ctx.scale(this.scale * pixelRatio, this.scale * pixelRatio);
  402. this.ctx.translate(-this.x, -this.y);
  403. this.ctx.strokeStyle = "rgba(255, 255, 255, 0.2)";
  404. this.ctx.lineWidth = 1 / this.scale;
  405. let gridStartX = Math.floor(this.x - canWidth / 2);
  406. let gridStartY = Math.floor(this.y - canHeight / 2);
  407. for (let x = gridStartX; x < canWidth + gridStartX + 1; x += 1) {
  408. this.ctx.beginPath();
  409. this.ctx.moveTo(x, gridStartY - canHeight - 1);
  410. this.ctx.lineTo(x, gridStartY + canHeight + 1);
  411. this.ctx.stroke();
  412. }
  413. for (let y = gridStartY; y < canHeight + gridStartY + 1; y += 1) {
  414. this.ctx.beginPath();
  415. this.ctx.moveTo(gridStartX - canWidth - 1, y);
  416. this.ctx.lineTo(gridStartX + canWidth + 1, y);
  417. this.ctx.stroke();
  418. }
  419. this.ctx.font = "0.8px Arial";
  420. this.ctx.lineWidth = 0.1;
  421. for (let node of this.nodes) {
  422. for (let output of node.outputs) {
  423. let link = output.link;
  424. if (link.current) {
  425. this.ctx.strokeStyle = "red";
  426. } else {
  427. this.ctx.strokeStyle = "brown";
  428. }
  429. for (let conn of link.connections) {
  430. this.ctx.beginPath();
  431. this.ctx.moveTo(link.from.x + link.from.width, link.from.y + link.index);
  432. this.ctx.lineTo(conn.node.x, conn.node.y + conn.index);
  433. this.ctx.stroke();
  434. }
  435. }
  436. }
  437. this.ctx.strokeStyle = "rgba(255, 255, 255, 1)";
  438. this.ctx.lineWidth = 0.05;
  439. this.ctx.setLineDash([0.2, 0.1]);
  440. for (let node of this.selectedNodes) {
  441. this.ctx.beginPath();
  442. this.ctx.strokeRect(node.x - 0.1, node.y - 0.5 - 0.1, node.width + 0.2, node.height + 0.2);
  443. }
  444. this.ctx.lineWidth = 0.1;
  445. this.ctx.setLineDash([]);
  446. for (let node of this.nodes) {
  447. if (node.lit) {
  448. this.ctx.fillStyle = "red";
  449. } else {
  450. this.ctx.fillStyle = "brown";
  451. }
  452. this.ctx.strokeStyle = "black";
  453. let metrics = this.ctx.measureText(node.name);
  454. let textWidth = metrics.width;
  455. this.ctx.fillRect(node.x, node.y - 0.5, node.width, node.height);
  456. this.ctx.fillStyle = "white";
  457. this.ctx.fillText(
  458. node.name,
  459. node.x + node.width / 2 - textWidth / 2,
  460. node.y - 0.5 + 0.8);
  461. this.ctx.fillStyle = "black";
  462. this.ctx.strokeStyle = "white";
  463. for (let y = 0; y < node.inputs.length; ++y) {
  464. this.ctx.beginPath();
  465. this.ctx.arc(node.x, node.y + y, 0.45, 0, 2 * Math.PI);
  466. this.ctx.fill();
  467. this.ctx.stroke();
  468. }
  469. for (let y = 0; y < node.outputs.length; ++y) {
  470. this.ctx.beginPath();
  471. this.ctx.arc(node.x + node.width, node.y + y, 0.45, 0, 2 * Math.PI);
  472. this.ctx.fill();
  473. this.ctx.stroke();
  474. }
  475. }
  476. if (this.currentLink != null) {
  477. this.ctx.strokeStyle = "brown";
  478. let from = this.currentLink.from;
  479. let fromX = from.node.x;
  480. let fromY = from.node.y + from.io.index;
  481. if (from.io.type == "output") {
  482. fromX += from.node.width;
  483. }
  484. this.ctx.beginPath();
  485. this.ctx.moveTo(fromX, fromY);
  486. for (let el of this.currentLink.path) {
  487. this.ctx.lineTo(el.x, el.y);
  488. }
  489. this.ctx.stroke();
  490. }
  491. if (this.tooltip != null) {
  492. this.ctx.fillStyle = "black";
  493. this.ctx.strokeStyle = "grey";
  494. let metrics = this.ctx.measureText(this.tooltip.text);
  495. let textWidth = metrics.width;
  496. let textAscent = metrics.actualBoundingBoxAscent;
  497. let x = this.tooltip.x - (textWidth / 4);
  498. let y = this.tooltip.y - 1.2;
  499. this.ctx.beginPath();
  500. this.ctx.rect(
  501. x - 0.17, y - 0.2, textWidth + 0.4, 1.1);
  502. this.ctx.fill();
  503. this.ctx.stroke();
  504. this.ctx.fillStyle = "white";
  505. this.ctx.fillText(this.tooltip.text, x, y + 0.6);
  506. }
  507. if (this.selection != null) {
  508. this.ctx.fillStyle = "rgba(100, 100, 250, 0.2)";
  509. this.ctx.strokeStyle = "black";
  510. this.ctx.beginPath();
  511. this.ctx.rect(
  512. this.selection.fromX, this.selection.fromY,
  513. this.selection.toX - this.selection.fromX,
  514. this.selection.toY - this.selection.fromY);
  515. this.ctx.fill();
  516. this.ctx.stroke();
  517. }
  518. this.ctx.restore();
  519. }
  520. update() {
  521. for (let node of this.nodes) {
  522. node.tick();
  523. }
  524. for (let node of this.nodes) {
  525. node.commit();
  526. }
  527. this.requestFrame();
  528. }
  529. }
  530. let canvas;
  531. let sim;
  532. let interval = null;
  533. onMount(() => {
  534. sim = new LogicSim(canvas);
  535. for (let node of nodes) {
  536. sim.nodes.push(node);
  537. }
  538. interval = setInterval(sim.update.bind(sim), 100);
  539. });
  540. onDestroy(() => {
  541. if (interval != null) {
  542. clearInterval(interval);
  543. interval = null;
  544. }
  545. });
  546. </script>