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

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