@webwriter/logic-circuit
Version:
Create and simulate circuits with logic gates (AND, OR, XOR, etc.).
628 lines (550 loc) • 26.9 kB
text/typescript
import { html } from 'lit';
import { LitElementWw } from '@webwriter/lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import NOTGate from './src/gates/not-gate.js';
import ANDGate from './src/gates/and-gate.js';
import Input from './src/gates/input.js';
import NANDGate from './src/gates/nand-gate.js';
import NORGate from './src/gates/nor-gate.js';
import ORGate from './src/gates/or-gate.js';
import Output from './src/gates/output.js';
import XNORGate from './src/gates/xnor-gate.js';
import XORGate from './src/gates/xor-gate.js';
import Splitter from './src/gates/splitter.js';
import Gate from './src/gates/gate.js';
import ConnectorElement from './src/connector.js';
import { getConnectorCoordinates, getMouseCoordinates, calculatePathToMouse } from './src/helper/line-helper.js';
import '@shoelace-style/shoelace/dist/themes/light.css';
import SlButton from '@shoelace-style/shoelace/dist/components/button/button.component.js';
import SlButtonGroup from '@shoelace-style/shoelace/dist/components/button-group/button-group.component.js';
import SlIconButton from '@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js';
import SlTooltip from '@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js';
import SlIcon from '@shoelace-style/shoelace/dist/components/icon/icon.component.js';
import SlPopup from '@shoelace-style/shoelace/dist/components/popup/popup.component.js';
import SlSwitch from '@shoelace-style/shoelace/dist/components/switch/switch.component.js';
import SlMenu from '@shoelace-style/shoelace/dist/components/menu/menu.component.js';
import SlMenuItem from '@shoelace-style/shoelace/dist/components/menu-item/menu-item.component.js';
import SlCheckbox from '@shoelace-style/shoelace/dist/components/checkbox/checkbox.component.js';
import SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js';
import { Styles } from './src/styles.js';
import { trash } from './src/assets/icons.js';
import { calculatePath, updateLines, resetLines, createLine} from './src/helper/line-helper.js';
import { addGate, moveGate, transferOutputToNextGate } from './src/helper/gate-helper.js';
import { gateCounter, isDropOverTrashIcon, resetGates } from './src/helper/gate-helper.js';
const workspaceWidth: number = 3000;
const workspaceHeight: number = 2000;
let workspaceOffsetX: number = -workspaceWidth / 2;
let workspaceOffsetY: number = -workspaceHeight / 2;
export default class LogicCircuit extends LitElementWw {
static shadowRootOptions = {
...LitElementWw.shadowRootOptions,
delegatesFocus: true,
};
static styles = Styles;
public static get scopedElements() {
return {
'not-gate': NOTGate,
'and-gate': ANDGate,
'input-gate': Input,
'nand-gate': NANDGate,
'nor-gate': NORGate,
'or-gate': ORGate,
'output-gate': Output,
'xnor-gate': XNORGate,
'xor-gate': XORGate,
'splitter-gate': Splitter,
'connector-element': ConnectorElement,
'sl-button': SlButton,
'sl-button-group': SlButtonGroup,
'sl-icon-button': SlIconButton,
'sl-icon': SlIcon,
'sl-tooltip': SlTooltip,
'sl-popup': SlPopup,
'sl-switch': SlSwitch,
'sl-menu': SlMenu,
'sl-menu-item': SlMenuItem,
'sl-checkbox': SlCheckbox,
'sl-input': SlInput,
};
}
accessor lineElements = [];
accessor gateElements = [];
accessor reflectGates: String = ""
accessor reflectCons: String = ""
accessor gateID: number = 0;
accessor lineID: number = 0;
accessor zoom: number = 1;
accessor dragStartX: number = 0;
accessor dragStartY: number = 0;
accessor simulate: boolean = false;
accessor allowSimulation: number = 1;
accessor notGateAllowed: number = -1;
accessor andGateAllowed: number = -1;
accessor orGateAllowed: number = -1;
accessor nandGateAllowed: number = -1;
accessor norGateAllowed: number = -1;
accessor xnorGateAllowed: number = -1;
accessor xorGateAllowed: number = -1;
accessor splitterAllowed: number = -1;
accessor isDragging: boolean = false;
accessor isDrawingLine: boolean = false;
accessor startConnector: ConnectorElement = null;
accessor endConnector: ConnectorElement = null;
accessor svgCanvas;
accessor workspaceContainer;
accessor wsDrag;
accessor simCheckbox;
public getGateElements = () => this.gateElements;
public getLineElements = () => this.lineElements;
svgPathToMouse: SVGPathElement | null = null;
render() {
return html`
<div class="container">
<div class="sidebar">
<div style=${this.notGateAllowed === 0 ? 'display: none;' : ''} class="sidebar-item">
<not-gate></not-gate>
<p class="sidebar-counter">${gateCounter(this, 'NOT')}</p>
</div>
<div style=${this.andGateAllowed === 0 ? 'display: none;' : ''} class="sidebar-item">
<and-gate></and-gate>
<p class="sidebar-counter">${gateCounter(this, 'AND')}</p>
</div>
<div style=${this.orGateAllowed === 0 ? 'display: none;' : ''} class="sidebar-item">
<or-gate></or-gate>
<p class="sidebar-counter">${gateCounter(this, 'OR')}</p>
</div>
<div style=${this.nandGateAllowed === 0 ? 'display: none;' : ''} class="sidebar-item">
<nand-gate></nand-gate>
<p class="sidebar-counter">${gateCounter(this, 'NAND')}</p>
</div>
<div style=${this.norGateAllowed === 0 ? 'display: none;' : ''} class="sidebar-item">
<nor-gate></nor-gate>
<p class="sidebar-counter">${gateCounter(this, 'NOR')}</p>
</div>
<div style=${this.xnorGateAllowed === 0 ? 'display: none;' : ''} class="sidebar-item">
<xnor-gate></xnor-gate>
<p class="sidebar-counter">${gateCounter(this, 'XNOR')}</p>
</div>
<div style=${this.xorGateAllowed === 0 ? 'display: none;' : ''} class="sidebar-item">
<xor-gate></xor-gate>
<p class="sidebar-counter">${gateCounter(this, 'XOR')}</p>
</div>
<splitter-gate></splitter-gate>
<input-gate></input-gate>
<output-gate></output-gate>
</div>
<div class="workspaceContainer" id="workspace">
<sl-checkbox id="simCheckbox" class="simulateCheckbox" -change=${() => this.simulateCircuit()}
>Simulate</sl-checkbox
>
<sl-switch class="flipSwitch" id="switch" -change=${() => this.handleFlipAllGates()}
>Show all Truthtables</sl-switch
>
<div class="trashCanIcon" style="font-size: 35px;">${trash}</div>
<div class="workspaceArea" id="workspaceDraggable">
<svg class="svgArea" id="svgCanvas"></svg>
</div>
</div>
</div>
<div part="options" class="optionsMenu">
<sl-checkbox class="optionsCheckbox" -change=${() => this.handleAllowSimulation()} checked
>Allow Simulation</sl-checkbox
>
<p></p>
<p>Limit max. number of Gates:</p>
<div class="optionsItem">
<sl-input
class="optionsCheckbox"
type="number"
size="small"
-change=${(e) => this.handleInputChange(e, 'notGateAllowed')}
.value=${this.notGateAllowed >= 0 ? this.notGateAllowed : ''}
min="0"
></sl-input>
<p>NOT-Gates</p>
</div>
<div class="optionsItem">
<sl-input
class="optionsCheckbox"
type="number"
size="small"
-change=${(e) => this.handleInputChange(e, 'andGateAllowed')}
.value=${this.andGateAllowed >= 0 ? this.andGateAllowed : ''}
min="0"
></sl-input>
<p>AND-Gates</p>
</div>
<div class="optionsItem">
<sl-input
class="optionsCheckbox"
type="number"
size="small"
-change=${(e) => this.handleInputChange(e, 'orGateAllowed')}
.value=${this.orGateAllowed >= 0 ? this.orGateAllowed : ''}
min="0"
></sl-input>
<p>OR-Gates</p>
</div>
<div class="optionsItem">
<sl-input
class="optionsCheckbox"
type="number"
size="small"
-change=${(e) => this.handleInputChange(e, 'nandGateAllowed')}
.value=${this.nandGateAllowed >= 0 ? this.nandGateAllowed : ''}
min="0"
></sl-input>
<p>NAND-Gates</p>
</div>
<div class="optionsItem">
<sl-input
class="optionsCheckbox"
type="number"
size="small"
-change=${(e) => this.handleInputChange(e, 'norGateAllowed')}
.value=${this.norGateAllowed >= 0 ? this.norGateAllowed : ''}
min="0"
></sl-input>
<p>NOR-Gates</p>
</div>
<div class="optionsItem">
<sl-input
class="optionsCheckbox"
type="number"
size="small"
-change=${(e) => this.handleInputChange(e, 'xnorGateAllowed')}
.value=${this.xnorGateAllowed >= 0 ? this.xnorGateAllowed : ''}
min="0"
></sl-input>
<p>XNOR-Gates</p>
</div>
<div class="optionsItem">
<sl-input
class="optionsCheckbox"
type="number"
size="small"
-change=${(e) => this.handleInputChange(e, 'xorGateAllowed')}
.value=${this.xorGateAllowed >= 0 ? this.xorGateAllowed : ''}
min="0"
></sl-input>
<p>XOR-Gates</p>
</div>
</div>
`;
}
updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has('allowSimulation')) {
if (this.allowSimulation === 1) {
this.simCheckbox.style.display = 'block';
} else {
this.simCheckbox.style.display = 'none';
}
}
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('contextmenu', this.handleContextMenu);
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('mousedown', this.handleMouseDown);
this.removeEventListener('mousemove', this.handleMouseMove);
this.removeEventListener('mouseup', this.handleMouseUp);
}
firstUpdated() {
this.workspaceContainer.addEventListener('drop', this.handleDrop.bind(this));
this.workspaceContainer.addEventListener('dragover', this.handleDragOver.bind(this));
this.workspaceContainer.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.workspaceContainer.addEventListener('mousemove', this.handleMouseMove.bind(this));
this.workspaceContainer.addEventListener('mouseup', this.handleMouseUp.bind(this));
this.workspaceContainer.addEventListener('mouseout', this.handleMouseOut.bind(this));
this.workspaceContainer.addEventListener('wheel', this.handleWheel.bind(this));
this.wsDrag.style.width = workspaceWidth + 'px';
this.wsDrag.style.height = workspaceHeight + 'px';
this.wsDrag.style.transform = `translate(${workspaceOffsetX}px,${workspaceOffsetY}px) scale(${this.zoom})`;
this.svgPathToMouse = document.createElementNS('http://www.w3.org/2000/svg', 'path');
this.svgPathToMouse.setAttribute('d', '');
this.svgPathToMouse.setAttribute('stroke', 'black');
this.svgPathToMouse.setAttribute('fill', 'none');
this.svgPathToMouse.setAttribute('stroke-width', '3');
this.svgPathToMouse.setAttribute('id', 'lineToMouse');
this.svgCanvas.appendChild(this.svgPathToMouse);
if(this.reflectGates.length>0){
this.reflectGates.split(",").forEach(gate=>{
addGate(this, null, gate.split("|").splice(1))
})
}
if(this.reflectCons.length>0){
this.reflectCons.split(",").forEach(con=>{
let startID: string = con.split("|")[0]
let endID: string = con.split("|")[1]
let start: any, end: any
this.shadowRoot.querySelector(".workspaceArea").childNodes.forEach(node=>{
if(node.nodeName.includes("GATE")){
setTimeout(()=>{
if(startID.includes(node.shadowRoot.querySelector("div").id)){
let gate: any = node.shadowRoot.querySelector("div")
let connectorArr: any = gate.querySelectorAll("slot")
connectorArr.forEach(slot=>{
if(slot.children[0].id === startID){
start = (slot.childNodes as NodeList).item(0)
}
})
}
if(endID.includes(node.shadowRoot.querySelector("div").id)){
let gate: any = node.shadowRoot.querySelector("div")
let connectorArr: any = gate.querySelectorAll("slot")
connectorArr.forEach(slot=>{
if(slot.children[0].id === endID){
end = (slot.childNodes as NodeList).item(0)
}
})
}
createLine(this,start,end)
}, 1)
}
})
})
}
}
handleAllowSimulation() {
if (this.allowSimulation === 0) {
this.resetCircuit();
this.simCheckbox.checked = false;
this.allowSimulation = 1;
} else {
this.allowSimulation = 0;
}
}
handleFlipAllGates() {
if ((this.shadowRoot.getElementById('switch') as SlSwitch).checked === false) {
this.gateElements.forEach((gate) => {
if (gate.gatetype !== 'INPUT' && gate.gatetype !== 'OUTPUT') {
gate.shadowRoot.getElementById('flipCheckbox').checked = false;
gate.showTruthTable = false;
}
});
} else {
this.gateElements.forEach((gate) => {
if (gate.gatetype !== 'INPUT' && gate.gatetype !== 'OUTPUT') {
gate.shadowRoot.getElementById('flipCheckbox').checked = true;
gate.showTruthTable = true;
}
});
}
}
handleMouseDown(event) {
if (event.target === this.svgCanvas) {
this.isDragging = true;
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
if (this.isDrawingLine) {
this.isDrawingLine = false;
this.startConnector = null;
this.svgPathToMouse.setAttribute('d', '');
}
this.gateElements.forEach((gate) => {
gate.hideContextMenu();
});
}
}
handleMouseMove(event) {
if (this.isDragging) {
const deltaX = event.clientX - this.dragStartX;
const deltaY = event.clientY - this.dragStartY;
workspaceOffsetX = workspaceOffsetX + deltaX;
workspaceOffsetY = workspaceOffsetY + deltaY;
this.calculateBoundaries();
this.dragStartX = event.clientX;
this.dragStartY = event.clientY;
this.transformWorkspace();
}
if (this.isDrawingLine) {
const { x: startX, y: startY } = getConnectorCoordinates(this.svgCanvas, this.startConnector, this.zoom);
const { x: endX, y: endY } = getMouseCoordinates(
this.svgCanvas,
event.clientX,
event.clientY - 4,
this.zoom
);
let path;
if (this.startConnector.type === 'output') {
path = `M ${startX} ${startY}`;
} else {
path = `M ${endX} ${endY}`;
}
const points = calculatePathToMouse(this.svgCanvas, this.startConnector, this.zoom, endX, endY);
for (let i = 1; i < points.length; i++) {
path += ` L ${points[i].x} ${points[i].y}`;
}
this.svgPathToMouse.setAttribute('d', path);
}
}
transformWorkspace() {
const workspace = this.wsDrag;
workspace.style.transform = `translate(${workspaceOffsetX}px,${workspaceOffsetY}px) scale(${this.zoom})`;
}
handleMouseUp() {
this.isDragging = false;
}
handleWheel(event) {
event.preventDefault();
this.gateElements.forEach((gate) => {
gate.hideContextMenu();
});
const delta = event.deltaY;
this.zoom -= delta * 0.001;
this.zoom = Math.min(Math.max(this.zoom, 0.5), 2.5);
this.transformWorkspace();
this.calculateBoundaries();
this.transformWorkspace();
}
handleMouseOut(event) {
updateLines(this, Gate.movedGate);
this.isDragging = false;
}
handleContextMenu(event) {
event.preventDefault();
}
handleDragOver(event) {
event.preventDefault();
const draggedGate = Gate.movedGate;
const mouseStartX = Gate.x;
const mouseStartY = Gate.y;
let deltaX = (event.clientX - mouseStartX) / this.zoom;
let deltaY = (event.clientY - mouseStartY) / this.zoom;
const moveLines = [];
this.lineElements.forEach((lineObject) => {
const startConnector = lineObject.start;
const endConnector = lineObject.end;
if (
startConnector.id === draggedGate.conIn1?.id ||
startConnector.id === draggedGate.conIn2?.id ||
startConnector.id === draggedGate.conOut?.id ||
startConnector.id === draggedGate.conOut2?.id ||
endConnector.id === draggedGate.conIn1?.id ||
endConnector.id === draggedGate.conIn2?.id ||
endConnector.id === draggedGate.conOut?.id ||
endConnector.id === draggedGate.conOut2?.id
) {
moveLines.push(lineObject);
}
});
moveLines.forEach((line) => {
const startConnector = line.start;
const endConnector = line.end;
const svgPath = line.lineSVG;
let points: { x: number; y: number }[];
if (
startConnector.id === draggedGate.conIn1?.id ||
startConnector.id === draggedGate.conIn2?.id ||
startConnector.id === draggedGate.conOut?.id ||
startConnector.id === draggedGate.conOut2?.id
) {
points = calculatePath(this.svgCanvas, startConnector, endConnector, this.zoom, deltaX, deltaY, 0, 0);
} else {
points = calculatePath(this.svgCanvas, startConnector, endConnector, this.zoom, 0, 0, deltaX, deltaY);
}
let path = `M ${points[0].x} ${points[0].y}`;
for (let i = 1; i < points.length; i++) {
path += ` L ${points[i].x} ${points[i].y}`;
}
svgPath.setAttribute('d', path);
});
const isOverTrash = isDropOverTrashIcon(this, event);
if (isOverTrash) {
this.workspaceContainer.querySelector('.trashCanIcon').classList.add('trashCanIconDragOver');
} else {
this.workspaceContainer.querySelector('.trashCanIcon').classList.remove('trashCanIconDragOver');
}
}
handleDrop(event) {
event.preventDefault();
const isOverTrash = isDropOverTrashIcon(this, event);
if (event.dataTransfer.getData('movable') === 'false') {
if (!isOverTrash) {
addGate(this, event);
}
} else if (event.dataTransfer.getData('movable') === 'true') {
const id = event.dataTransfer.getData('id');
const gateToMove = this.gateElements.find((gate) => gate.id === id) as Gate;
if (isOverTrash) {
this.handleDropTrashCan(event);
} else {
moveGate(this, event);
}
}
this.workspaceContainer.querySelector('.trashCanIcon').classList.remove('trashCanIconDragOver');
}
handleDropTrashCan(event) {
event.preventDefault();
const id = event.dataTransfer.getData('id');
const trashGate = this.gateElements.find((gate) => gate.id === id);
trashGate.deleteGate();
}
handleInputChange(event, propertyName) {
const inputValue = parseInt(event.target.value);
if (isNaN(inputValue) || inputValue < 0 || event.target.value.trim() === '') {
this[propertyName] = -1;
} else {
this[propertyName] = inputValue;
}
}
simulateCircuit() {
const simCheckbox = this.simCheckbox;
if (simCheckbox.checked) {
this.simulate = true;
this.resetCircuit();
const inputGates = this.gateElements?.filter((gate) => gate.gatetype === 'INPUT');
inputGates.forEach((gate) => {
gate.calculateOutput();
gate.updateConnectorColor();
transferOutputToNextGate(this, gate);
});
} else {
this.simulate = false;
this.resetCircuit();
}
}
resetCircuit() {
resetGates(this);
resetLines(this);
this.requestUpdate();
}
calculateBoundaries() {
if (workspaceOffsetX > (this.wsDrag.getBoundingClientRect().width - workspaceWidth) / 2 + 2) {
workspaceOffsetX = (this.wsDrag.getBoundingClientRect().width - workspaceWidth) / 2 + 2;
}
if (workspaceOffsetY > (this.wsDrag.getBoundingClientRect().height - workspaceHeight) / 2 + 2) {
workspaceOffsetY = (this.wsDrag.getBoundingClientRect().height - workspaceHeight) / 2 + 2;
}
if (
workspaceOffsetX <
-this.wsDrag.getBoundingClientRect().width +
this.workspaceContainer.getBoundingClientRect().width +
(this.wsDrag.getBoundingClientRect().width - workspaceWidth) / 2 -
2
) {
workspaceOffsetX =
-this.wsDrag.getBoundingClientRect().width +
this.workspaceContainer.getBoundingClientRect().width +
(this.wsDrag.getBoundingClientRect().width - workspaceWidth) / 2 -
2;
}
if (
workspaceOffsetY <
-this.wsDrag.getBoundingClientRect().height +
this.workspaceContainer.getBoundingClientRect().height +
(this.wsDrag.getBoundingClientRect().height - workspaceHeight) / 2 -
2
) {
workspaceOffsetY =
-this.wsDrag.getBoundingClientRect().height +
this.workspaceContainer.getBoundingClientRect().height +
(this.wsDrag.getBoundingClientRect().height - workspaceHeight) / 2 -
2;
}
}
}