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

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