|
|
@@ -0,0 +1,748 @@ |
|
|
|
<canvas bind:this={canvas}></canvas> |
|
|
|
|
|
|
|
<style> |
|
|
|
canvas { |
|
|
|
width: 100vw; |
|
|
|
height: 100vh; |
|
|
|
position: absolute; |
|
|
|
top: 0px; |
|
|
|
left: 0px; |
|
|
|
} |
|
|
|
</style> |
|
|
|
|
|
|
|
<script> |
|
|
|
import {onMount} from 'svelte'; |
|
|
|
|
|
|
|
class Link { |
|
|
|
constructor(from, index) { |
|
|
|
this.from = from; |
|
|
|
this.index = index; |
|
|
|
this.current = false; |
|
|
|
this.next = false; |
|
|
|
|
|
|
|
this.connections = []; |
|
|
|
} |
|
|
|
|
|
|
|
connect(node, index) { |
|
|
|
node.inputs[index].links.push(this); |
|
|
|
this.connections.push({node, index}); |
|
|
|
} |
|
|
|
|
|
|
|
disconnect(node, index) { |
|
|
|
for (let i = this.connections.length - 1; i >= 0; --i) { |
|
|
|
let conn = this.connections[i]; |
|
|
|
if (conn.node == node && conn.index == index) { |
|
|
|
this.connections.splice(i, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
destroy() { |
|
|
|
for (let conn of this.connections) { |
|
|
|
for (let i = conn.node.inputs[conn.index].links.length - 1; i >= 0; --i) { |
|
|
|
if (conn.node.inputs[conn.index].links[i] == this) { |
|
|
|
conn.node.inputs[conn.index].links.splice(i, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
commit() { |
|
|
|
this.current = this.next; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class Input { |
|
|
|
constructor(x, y) { |
|
|
|
this.name = "IN"; |
|
|
|
this.x = x; |
|
|
|
this.y = y; |
|
|
|
this.inputs = []; |
|
|
|
this.outputs = [{name: "Out", link: new Link(this, 0)}]; |
|
|
|
|
|
|
|
this.width = 3; |
|
|
|
this.height = 1; |
|
|
|
|
|
|
|
this.lit = false; |
|
|
|
} |
|
|
|
|
|
|
|
activate() { |
|
|
|
this.lit = !this.lit; |
|
|
|
} |
|
|
|
|
|
|
|
tick() { |
|
|
|
if (this.lit) { |
|
|
|
this.outputs[0].link.next = true; |
|
|
|
} else { |
|
|
|
this.outputs[0].link.next = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
commit() { |
|
|
|
this.outputs[0].link.commit(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class NotGate { |
|
|
|
constructor(x, y) { |
|
|
|
this.name = "NOT"; |
|
|
|
this.x = x; |
|
|
|
this.y = y; |
|
|
|
this.inputs = [{name: "In", links: []}]; |
|
|
|
this.outputs = [{name: "Out", link: new Link(this, 0)}]; |
|
|
|
|
|
|
|
this.width = 4; |
|
|
|
this.height = 1; |
|
|
|
|
|
|
|
this.lit = false; |
|
|
|
} |
|
|
|
|
|
|
|
tick() { |
|
|
|
this.outputs[0].link.next = true; |
|
|
|
for (let link of this.inputs[0].links) { |
|
|
|
if (link.current) { |
|
|
|
this.outputs[0].link.next = false; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
commit() { |
|
|
|
this.outputs[0].link.commit(); |
|
|
|
this.lit = this.outputs[0].link.current; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class Diode { |
|
|
|
constructor(x, y) { |
|
|
|
this.name = "DIODE"; |
|
|
|
this.x = x; |
|
|
|
this.y = y; |
|
|
|
this.inputs = [{name: "In", links: []}]; |
|
|
|
this.outputs = [{name: "Out", link: new Link(this, 0)}]; |
|
|
|
|
|
|
|
this.width = 4; |
|
|
|
this.height = 1; |
|
|
|
|
|
|
|
this.lit = false; |
|
|
|
} |
|
|
|
|
|
|
|
tick() { |
|
|
|
this.outputs[0].link.next = false; |
|
|
|
for (let link of this.inputs[0].links) { |
|
|
|
if (link.current) { |
|
|
|
this.outputs[0].link.next = true; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
commit() { |
|
|
|
this.outputs[0].link.commit(); |
|
|
|
this.lit = this.outputs[0].link.current; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class Lamp { |
|
|
|
constructor(x, y) { |
|
|
|
this.name = "LAMP"; |
|
|
|
this.x = x; |
|
|
|
this.y = y; |
|
|
|
this.inputs = [{name: "In", links: []}]; |
|
|
|
this.outputs = []; |
|
|
|
|
|
|
|
this.width = 4; |
|
|
|
this.height = 1; |
|
|
|
|
|
|
|
this.lit = false; |
|
|
|
this.nextLit = false; |
|
|
|
} |
|
|
|
|
|
|
|
tick() { |
|
|
|
this.nextLit = false; |
|
|
|
for (let link of this.inputs[0].links) { |
|
|
|
if (link.current) { |
|
|
|
this.nextLit = true; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
commit() { |
|
|
|
this.lit = this.nextLit; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
class LogicSim { |
|
|
|
constructor(can) { |
|
|
|
this.scale = 40; |
|
|
|
this.can = can; |
|
|
|
this.ctx = can.getContext("2d"); |
|
|
|
this.nodes = []; |
|
|
|
this.frameRequested = false; |
|
|
|
this.clickCancelled = false; |
|
|
|
this.currentTouch = null; |
|
|
|
this.x = 0; |
|
|
|
this.y = 0; |
|
|
|
|
|
|
|
this.tooltip = null; |
|
|
|
this.selectedNodes = []; |
|
|
|
this.mouseMoveStart = null; |
|
|
|
this.currentLink = null; |
|
|
|
this.selection = null; |
|
|
|
|
|
|
|
this.requestFrame(); |
|
|
|
|
|
|
|
window.addEventListener("resize", () => { |
|
|
|
this.requestFrame(); |
|
|
|
}); |
|
|
|
|
|
|
|
window.addEventListener("keydown", evt => { |
|
|
|
this.onKeyDown(evt.key); |
|
|
|
}); |
|
|
|
|
|
|
|
can.addEventListener("wheel", evt => { |
|
|
|
this.onScroll(evt.deltaY / 10); |
|
|
|
}); |
|
|
|
|
|
|
|
can.addEventListener("mousedown", evt => { |
|
|
|
this.onMouseDown(evt.offsetX, evt.offsetY, evt.buttons, evt); |
|
|
|
}); |
|
|
|
|
|
|
|
can.addEventListener("mouseup", evt => { |
|
|
|
this.onMouseUp(); |
|
|
|
}); |
|
|
|
|
|
|
|
can.addEventListener("mousemove", evt => { |
|
|
|
this.onMouseMove(evt.offsetX, evt.offsetY, evt.movementX, evt.movementY, evt.buttons); |
|
|
|
}); |
|
|
|
|
|
|
|
can.addEventListener("click", evt => { |
|
|
|
this.onClick(evt.offsetX, evt.offsetY); |
|
|
|
}); |
|
|
|
|
|
|
|
can.addEventListener("touchstart", evt => { |
|
|
|
if (this.currentTouch != null || evt.changedTouches.length == 0) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
let relevantTouch = evt.changedTouches[0]; |
|
|
|
this.currentTouch = relevantTouch; |
|
|
|
this.onMouseDown(relevantTouch.clientX, relevantTouch.clientY, 1, evt); |
|
|
|
}); |
|
|
|
|
|
|
|
can.addEventListener("touchend", evt => { |
|
|
|
if (this.currentTouch == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
let relevantTouch = null; |
|
|
|
for (let touch of evt.changedTouches) { |
|
|
|
if (touch.identifier == this.currentTouch.identifier) { |
|
|
|
relevantTouch = touch; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (relevantTouch == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
this.currentTouch = null; |
|
|
|
tis.onMouseUp(); |
|
|
|
}); |
|
|
|
|
|
|
|
can.addEventListener("touchmove", evt => { |
|
|
|
if (this.currentTouch == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
let relevantTouch = null; |
|
|
|
for (let touch of evt.changedTouches) { |
|
|
|
if (touch.identifier == this.currentTouch.identifier) { |
|
|
|
relevantTouch = touch; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (relevantTouch == null) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
let deltaX = this.currentTouch.clientX - relevantTouch.clientX; |
|
|
|
let deltaY = this.currentTouch.clientY - relevantTouch.clientY; |
|
|
|
this.currentTouch = relevantTouch; |
|
|
|
|
|
|
|
this.onMouseMove(relevantTouch.clientX, relevantTouch.clientY, -deltaX, -deltaY, 1); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
onKeyDown(key) { |
|
|
|
let responded = true; |
|
|
|
if (key == "Escape" || key == "q") { |
|
|
|
this.tooltip = null; |
|
|
|
this.selectedNodes = []; |
|
|
|
this.mouseMoveStart = null; |
|
|
|
this.currentLink = null; |
|
|
|
this.selection = null; |
|
|
|
this.requestFrame(); |
|
|
|
} else if (key == "Delete") { |
|
|
|
for (let node of this.selectedNodes) { |
|
|
|
this.deleteNode(node); |
|
|
|
} |
|
|
|
this.selectedNodes = []; |
|
|
|
} else if (key == "Enter") { |
|
|
|
for (let node of this.selectedNodes) { |
|
|
|
if (node.activate) { |
|
|
|
node.activate(); |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
responded = false; |
|
|
|
} |
|
|
|
|
|
|
|
if (responded) { |
|
|
|
evt.preventDefault(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
onScroll(delta) { |
|
|
|
this.scale -= delta * (this.scale / 40); |
|
|
|
if (this.scale > 200) { |
|
|
|
this.scale = 200; |
|
|
|
} else if (this.scale < 10) { |
|
|
|
this.scale = 10; |
|
|
|
} |
|
|
|
|
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
|
|
|
|
onMouseDown(offsetX, offsetY, buttons, keys) { |
|
|
|
this.clickCancelled = false; |
|
|
|
let [x, y] = this.coordsFromScreenPos(offsetX, offsetY); |
|
|
|
let node = this.getNodeAt(x, y); |
|
|
|
this.mouseMoveStart = {x, y, node}; |
|
|
|
|
|
|
|
if (keys.shiftKey && buttons == 1) { |
|
|
|
this.selectedNodes = []; |
|
|
|
this.selection = {fromX: x, fromY: y, toX: x, toY: y}; |
|
|
|
this.tooltip = null; |
|
|
|
this.requestFrame(); |
|
|
|
} else if (node != null && buttons == 1) { |
|
|
|
if (this.selectedNodes.indexOf(node) == -1) { |
|
|
|
this.selectedNodes = [node]; |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} else if (buttons == 1) { |
|
|
|
this.selectedNodes = []; |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
onMouseUp() { |
|
|
|
this.mouseMoveStart = null; |
|
|
|
|
|
|
|
if (this.selection) { |
|
|
|
this.selectedNodes = []; |
|
|
|
let [x1, y1] = [this.selection.fromX, this.selection.fromY]; |
|
|
|
let [x2, y2] = [this.selection.toX, this.selection.toY]; |
|
|
|
|
|
|
|
// Make sure we actually have a rect from top-left to bottom-right |
|
|
|
let tmp; |
|
|
|
if (x1 > x2) { |
|
|
|
tmp = x1; |
|
|
|
x1 = x2; |
|
|
|
x2 = tmp; |
|
|
|
} |
|
|
|
if (y1 > y2) { |
|
|
|
tmp = y1; |
|
|
|
y1 = y2; |
|
|
|
y2 = tmp; |
|
|
|
} |
|
|
|
|
|
|
|
for (let node of this.nodes) { |
|
|
|
let nodeX1 = node.x; |
|
|
|
let nodeY1 = node.y - 0.5; |
|
|
|
let nodeX2 = nodeX1 + node.width; |
|
|
|
let nodeY2 = nodeY1 + node.height; |
|
|
|
|
|
|
|
if (x1 < nodeX2 && x2 > nodeX1 && y1 < nodeY2 && y2 > nodeY1) { |
|
|
|
this.selectedNodes.push(node); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.selection = null; |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
onMouseMove(offsetX, offsetY, movementX, movementY, buttons) { |
|
|
|
let [x, y] = this.coordsFromScreenPos(offsetX, offsetY); |
|
|
|
let dx = 0, dy = 0; |
|
|
|
if (this.mouseMoveStart) { |
|
|
|
dx = x - this.mouseMoveStart.x; |
|
|
|
dy = y - this.mouseMoveStart.y; |
|
|
|
} |
|
|
|
|
|
|
|
if ( |
|
|
|
buttons == 1 && this.selection == null && |
|
|
|
this.mouseMoveStart != null && this.currentLink == null) { |
|
|
|
if (this.mouseMoveStart.node == null) { |
|
|
|
this.x -= movementX / this.scale; |
|
|
|
this.y -= movementY / this.scale; |
|
|
|
this.requestFrame(); |
|
|
|
} else { |
|
|
|
let nodeDX = Math.round(dx); |
|
|
|
let nodeDY = Math.round(dy); |
|
|
|
|
|
|
|
if (nodeDX != 0 || nodeDY != 0) { |
|
|
|
this.clickCancelled = true; |
|
|
|
this.mouseMoveStart.x += nodeDX; |
|
|
|
this.mouseMoveStart.y += nodeDY; |
|
|
|
|
|
|
|
for (let node of this.selectedNodes) { |
|
|
|
node.x += nodeDX; |
|
|
|
node.y += nodeDY; |
|
|
|
} |
|
|
|
|
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let node = this.getNodeAt(x, y); |
|
|
|
let io = null; |
|
|
|
if (node != null) { |
|
|
|
io = this.getNodeIOAt(node, x, y); |
|
|
|
} |
|
|
|
|
|
|
|
if (io == null && this.tooltip == null) { |
|
|
|
// Do nothing |
|
|
|
} else if (io == null && this.tooltip != null) { |
|
|
|
this.tooltip = null; |
|
|
|
this.requestFrame(); |
|
|
|
} else { |
|
|
|
this.tooltip = { |
|
|
|
text: io.io.name, |
|
|
|
x, y, |
|
|
|
}; |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.selection != null) { |
|
|
|
this.selection.toX = x; |
|
|
|
this.selection.toY = y; |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.currentLink != null) { |
|
|
|
let p = this.currentLink.path[this.currentLink.path.length - 1]; |
|
|
|
p.x = x; |
|
|
|
p.y = y; |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
onClick(offsetX, offsetY) { |
|
|
|
if (this.clickCancelled) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
let [x, y] = this.coordsFromScreenPos(offsetX, offsetY); |
|
|
|
let node = this.getNodeAt(x, y); |
|
|
|
let io = null; |
|
|
|
if (node != null) { |
|
|
|
io = this.getNodeIOAt(node, x, y); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.currentLink != null) { |
|
|
|
if (io) { |
|
|
|
let fromNode = this.currentLink.from.node; |
|
|
|
let fromIO = this.currentLink.from.io; |
|
|
|
if (fromIO.type == "input" && io.type == "output") { |
|
|
|
io.io.link.connect(fromNode, fromIO.index); |
|
|
|
} else if (fromIO.type == "output" && io.type == "input") { |
|
|
|
fromIO.io.link.connect(node, io.index); |
|
|
|
} |
|
|
|
|
|
|
|
if (fromIO.type != io.type) { |
|
|
|
this.currentLink = null; |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} else { |
|
|
|
let path = this.currentLink.path; |
|
|
|
path[path.length - 1].x = Math.round(path[path.length - 1].x); |
|
|
|
path[path.length - 1].y = Math.round(path[path.length - 1].y); |
|
|
|
path.push({x, y}); |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} else if (io != null) { |
|
|
|
this.currentLink = {from: {node, io}, path: [{x, y}]}; |
|
|
|
} else { |
|
|
|
if (node && node.activate) { |
|
|
|
node.activate(); |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
deleteNode(node) { |
|
|
|
for (let i = 0; i < node.inputs.length; ++i) { |
|
|
|
let input = node.inputs[i]; |
|
|
|
for (let link of input.links) { |
|
|
|
link.disconnect(node, i); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
for (let out of node.outputs) { |
|
|
|
out.link.destroy(); |
|
|
|
} |
|
|
|
|
|
|
|
this.nodes.splice(this.nodes.indexOf(node), 1); |
|
|
|
} |
|
|
|
|
|
|
|
coordsFromScreenPos(screenX, screenY) { |
|
|
|
return [ |
|
|
|
(screenX - this.can.offsetWidth / 2) / this.scale + this.x, |
|
|
|
(screenY - this.can.offsetHeight / 2) / this.scale + this.y, |
|
|
|
]; |
|
|
|
} |
|
|
|
|
|
|
|
getNodeAt(x, y) { |
|
|
|
for (let i = this.nodes.length - 1; i >= 0; --i) { |
|
|
|
let node = this.nodes[i]; |
|
|
|
let nodeX1 = node.x - 0.5; |
|
|
|
let nodeY1 = node.y - 0.5; |
|
|
|
let nodeX2 = node.x + node.width + 0.5; |
|
|
|
let nodeY2 = node.y - 0.5 + node.height; |
|
|
|
|
|
|
|
if ( |
|
|
|
x >= nodeX1 && x <= nodeX2 && |
|
|
|
y >= nodeY1 && y <= nodeY2) { |
|
|
|
return node; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
getNodeIOAt(node, x, y) { |
|
|
|
for (let n = 0; n < node.inputs.length; ++n) { |
|
|
|
let dx = node.x - x; |
|
|
|
let dy = node.y + n - y; |
|
|
|
let dist = Math.abs(Math.sqrt(dx * dx + dy * dy)); |
|
|
|
if (dist < 0.5) { |
|
|
|
return {type: "input", io: node.inputs[n], index: n}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
for (let n = 0; n < node.outputs.length; ++n) { |
|
|
|
let dx = node.x + node.width - x; |
|
|
|
let dy = node.y + n - y; |
|
|
|
let dist = Math.abs(Math.sqrt(dx * dx + dy * dy)); |
|
|
|
if (dist < 0.5) { |
|
|
|
return {type: "output", io: node.outputs[n], index: n}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
requestFrame() { |
|
|
|
if (this.frameRequested) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
this.frameRequested = true; |
|
|
|
window.requestAnimationFrame(() => { |
|
|
|
this.frameRequested = false; |
|
|
|
this.draw(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
addNodeAtCursor(node) { |
|
|
|
node.x = Math.round(this.x); |
|
|
|
node.y = Math.round(this.y); |
|
|
|
this.nodes.push(node); |
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
|
|
|
|
draw() { |
|
|
|
let pixelRatio = 1; |
|
|
|
if (window.devicePixelRatio != null) { |
|
|
|
pixelRatio = window.devicePixelRatio; |
|
|
|
} |
|
|
|
|
|
|
|
this.can.width = this.can.offsetWidth * pixelRatio; |
|
|
|
this.can.height = this.can.offsetHeight * pixelRatio; |
|
|
|
let canWidth = this.can.width / this.scale; |
|
|
|
let canHeight = this.can.height / this.scale; |
|
|
|
this.ctx.save(); |
|
|
|
|
|
|
|
this.ctx.translate(this.can.width / 2, this.can.height / 2); |
|
|
|
this.ctx.scale(this.scale * pixelRatio, this.scale * pixelRatio); |
|
|
|
this.ctx.translate(-this.x, -this.y); |
|
|
|
|
|
|
|
this.ctx.strokeStyle = "rgba(0, 0, 0, 0.2)"; |
|
|
|
this.ctx.lineWidth = 1 / this.scale; |
|
|
|
|
|
|
|
let gridStartX = Math.floor(this.x - canWidth / 2); |
|
|
|
let gridStartY = Math.floor(this.y - canHeight / 2); |
|
|
|
|
|
|
|
for (let x = gridStartX; x < canWidth + gridStartX + 1; x += 1) { |
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.moveTo(x, gridStartY - canHeight - 1); |
|
|
|
this.ctx.lineTo(x, gridStartY + canHeight + 1); |
|
|
|
this.ctx.stroke(); |
|
|
|
} |
|
|
|
|
|
|
|
for (let y = gridStartY; y < canHeight + gridStartY + 1; y += 1) { |
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.moveTo(gridStartX - canWidth - 1, y); |
|
|
|
this.ctx.lineTo(gridStartX + canWidth + 1, y); |
|
|
|
this.ctx.stroke(); |
|
|
|
} |
|
|
|
|
|
|
|
this.ctx.font = "0.8px Arial"; |
|
|
|
this.ctx.lineWidth = 0.1; |
|
|
|
|
|
|
|
for (let node of this.nodes) { |
|
|
|
for (let output of node.outputs) { |
|
|
|
let link = output.link; |
|
|
|
|
|
|
|
if (link.current) { |
|
|
|
this.ctx.strokeStyle = "red"; |
|
|
|
} else { |
|
|
|
this.ctx.strokeStyle = "brown"; |
|
|
|
} |
|
|
|
|
|
|
|
for (let conn of link.connections) { |
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.moveTo(link.from.x + link.from.width, link.from.y + link.index); |
|
|
|
this.ctx.lineTo(conn.node.x, conn.node.y + conn.index); |
|
|
|
this.ctx.stroke(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.ctx.strokeStyle = "black"; |
|
|
|
this.ctx.lineWidth = 0.1; |
|
|
|
this.ctx.setLineDash([0.2, 0.1]); |
|
|
|
for (let node of this.selectedNodes) { |
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.strokeRect(node.x - 0.1, node.y - 0.5 - 0.1, node.width + 0.2, node.height + 0.2); |
|
|
|
} |
|
|
|
this.ctx.lineWidth = 0.1; |
|
|
|
this.ctx.setLineDash([]); |
|
|
|
|
|
|
|
for (let node of this.nodes) { |
|
|
|
if (node.lit) { |
|
|
|
this.ctx.fillStyle = "red"; |
|
|
|
} else { |
|
|
|
this.ctx.fillStyle = "brown"; |
|
|
|
} |
|
|
|
|
|
|
|
this.ctx.strokeStyle = "black"; |
|
|
|
|
|
|
|
let metrics = this.ctx.measureText(node.name); |
|
|
|
let textWidth = metrics.width; |
|
|
|
this.ctx.fillRect(node.x, node.y - 0.5, node.width, node.height); |
|
|
|
|
|
|
|
this.ctx.fillStyle = "white"; |
|
|
|
this.ctx.fillText( |
|
|
|
node.name, |
|
|
|
node.x + node.width / 2 - textWidth / 2, |
|
|
|
node.y - 0.5 + 0.8); |
|
|
|
|
|
|
|
this.ctx.fillStyle = "black"; |
|
|
|
this.ctx.strokeStyle = "white"; |
|
|
|
|
|
|
|
for (let y = 0; y < node.inputs.length; ++y) { |
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.arc(node.x, node.y + y, 0.45, 0, 2 * Math.PI); |
|
|
|
this.ctx.fill(); |
|
|
|
this.ctx.stroke(); |
|
|
|
} |
|
|
|
|
|
|
|
for (let y = 0; y < node.outputs.length; ++y) { |
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.arc(node.x + node.width, node.y + y, 0.45, 0, 2 * Math.PI); |
|
|
|
this.ctx.fill(); |
|
|
|
this.ctx.stroke(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (this.currentLink != null) { |
|
|
|
this.ctx.strokeStyle = "black"; |
|
|
|
|
|
|
|
let from = this.currentLink.from; |
|
|
|
let fromX = from.node.x; |
|
|
|
let fromY = from.node.y + from.io.index; |
|
|
|
if (from.io.type == "output") { |
|
|
|
fromX += from.node.width; |
|
|
|
} |
|
|
|
|
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.moveTo(fromX, fromY); |
|
|
|
for (let el of this.currentLink.path) { |
|
|
|
this.ctx.lineTo(el.x, el.y); |
|
|
|
} |
|
|
|
|
|
|
|
this.ctx.stroke(); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.tooltip != null) { |
|
|
|
this.ctx.fillStyle = "black"; |
|
|
|
this.ctx.strokeStyle = "grey"; |
|
|
|
|
|
|
|
let metrics = this.ctx.measureText(this.tooltip.text); |
|
|
|
let textWidth = metrics.width; |
|
|
|
let textAscent = metrics.actualBoundingBoxAscent; |
|
|
|
let x = this.tooltip.x - (textWidth / 4); |
|
|
|
let y = this.tooltip.y - 1.2; |
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.rect( |
|
|
|
x - 0.17, y - 0.2, textWidth + 0.4, 1.1); |
|
|
|
this.ctx.fill(); |
|
|
|
this.ctx.stroke(); |
|
|
|
|
|
|
|
this.ctx.fillStyle = "white"; |
|
|
|
this.ctx.fillText(this.tooltip.text, x, y + 0.6); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.selection != null) { |
|
|
|
this.ctx.fillStyle = "rgba(100, 100, 250, 0.2)"; |
|
|
|
this.ctx.strokeStyle = "black"; |
|
|
|
this.ctx.beginPath(); |
|
|
|
this.ctx.rect( |
|
|
|
this.selection.fromX, this.selection.fromY, |
|
|
|
this.selection.toX - this.selection.fromX, |
|
|
|
this.selection.toY - this.selection.fromY); |
|
|
|
this.ctx.fill(); |
|
|
|
this.ctx.stroke(); |
|
|
|
} |
|
|
|
|
|
|
|
this.ctx.restore(); |
|
|
|
} |
|
|
|
|
|
|
|
update() { |
|
|
|
for (let node of this.nodes) { |
|
|
|
node.tick(); |
|
|
|
} |
|
|
|
|
|
|
|
for (let node of this.nodes) { |
|
|
|
node.commit(); |
|
|
|
} |
|
|
|
|
|
|
|
this.requestFrame(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
let canvas; |
|
|
|
let sim; |
|
|
|
|
|
|
|
onMount(() => { |
|
|
|
sim = new LogicSim(canvas); |
|
|
|
}); |
|
|
|
</script> |