ProzillaOS/packages/apps/logic-sim/core/inputHandler.ts
2024-06-16 21:46:44 +02:00

513 lines
No EOL
13 KiB
TypeScript

import { CONTROLLER, PIN, WIRE } from "../../../../config/apps/logicSim.config";
import { Vector2 } from "../../../math/vector2";
import { Chip } from "./chips/chip";
import { Circuit } from "./circuit";
import { ControlledPin } from "./pins/controlledPin";
import { Pin } from "./pins/pin";
import { State } from "./_utils/state";
import { Wire } from "./wires/wire";
export class InputHandler {
circuit!: Circuit;
canvas!: HTMLCanvasElement;
mousePosition = Vector2.ZERO;
isPlacing = false;
snapping = false;
placingOffset = Vector2.ZERO;
previousPlacement!: Vector2 | null;
placingWire!: Wire | null;
placingChip!: Chip | null;
placingPin!: ControlledPin | null;
constructor(circuit: Circuit) {
Object.assign(this, { circuit });
}
setMousePosition(event: MouseEvent) {
const rect = this.canvas.getBoundingClientRect();
this.mousePosition.x = event.clientX - rect.left;
this.mousePosition.y = event.clientY - rect.top;
}
init() {
this.canvas = this.circuit.canvas;
this.mousePosition = Vector2.ZERO;
// TO DO: add support for touch devices
this.canvas.addEventListener("mousemove", this.onMouseMove);
this.canvas.addEventListener("mouseup", this.onMouseUp);
this.canvas.addEventListener("contextmenu", this.onMouseUp);
this.canvas.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
cleanup() {
this.canvas.removeEventListener("mousemove", this.onMouseMove);
this.canvas.removeEventListener("mouseup", this.onMouseUp);
this.canvas.removeEventListener("contextmenu", this.onMouseUp);
this.canvas.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
reset() {
this.placingWire = null;
this.placingChip = null;
this.placingPin = null;
this.previousPlacement = null;
this.placingOffset = Vector2.ZERO;
this.isPlacing = false;
}
onMouseMove = (event?: MouseEvent) => {
if (event != null)
this.setMousePosition(event);
if (this.placingWire != null) {
this.updateWirePlacement();
return;
}
if (this.placingChip != null) {
this.updateChipPlacement();
return;
}
const isHoveringPinHandle = (pin: ControlledPin) => {
const top = pin.position.y - CONTROLLER.radius;
const bottom = pin.position.y + CONTROLLER.radius;
return (this.mousePosition.y > top && this.mousePosition.y < bottom);
};
if (this.placingPin != null) {
let invalidPlacement = this.mousePosition.x > CONTROLLER.handleTrackWidth && this.mousePosition.x < this.circuit.size.x - CONTROLLER.handleTrackWidth;
if (invalidPlacement) {
this.cancelPinPlacement();
return;
}
if (this.placingPin.isInput) {
this.circuit.inputPins.forEach((pin, index) => {
if (invalidPlacement || index == this.circuit.inputPins.length - 1)
return;
if (isHoveringPinHandle(pin))
invalidPlacement = true;
});
} else {
this.circuit.outputPins.forEach((pin, index) => {
if (invalidPlacement || index == this.circuit.outputPins.length - 1)
return;
if (isHoveringPinHandle(pin))
invalidPlacement = true;
});
}
if (invalidPlacement) {
this.cancelPinPlacement();
} else {
this.updatePinPlacement();
}
} else {
if (this.mousePosition.x < CONTROLLER.handleTrackWidth) {
let invalidPlacement = false;
this.circuit.inputPins.forEach((pin) => {
if (invalidPlacement)
return;
if (isHoveringPinHandle(pin))
invalidPlacement = true;
});
if (!invalidPlacement)
this.startPinPlacement(true);
} else if (this.mousePosition.x > this.circuit.size.x - CONTROLLER.handleTrackWidth) {
let invalidPlacement = false;
this.circuit.outputPins.forEach((pin) => {
if (invalidPlacement)
return;
if (isHoveringPinHandle(pin))
invalidPlacement = true;
});
if (!invalidPlacement)
this.startPinPlacement(false);
}
}
};
onClickPin(pin: Pin) {
if (this.placingWire != null) {
this.endWirePlacement(pin);
} else {
this.startWirePlacement(pin);
}
}
onMouseUp = (event: MouseEvent) => {
event.preventDefault();
this.setMousePosition(event);
if (event.button === 2) {
if (this.placingWire != null)
this.cancelWirePlacement();
if (this.placingChip != null)
this.cancelChipPlacement();
} else if (event.button === 0) {
let eventComplete = false;
this.circuit.inputPins.forEach((pin: Pin) => {
if (this.mousePosition.getDistance(pin.position.x - CONTROLLER.pinOffset, pin.position.y) <= CONTROLLER.radius) {
pin.setState(State.invert(pin.state));
eventComplete = true;
} else if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) {
this.onClickPin(pin);
eventComplete = true;
}
});
if (eventComplete)
return;
this.circuit.outputPins.forEach((pin: Pin) => {
if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) {
this.onClickPin(pin);
eventComplete = true;
}
});
if (eventComplete)
return;
this.circuit.chips.forEach((chip) => {
chip.inputPins.concat(chip.outputPins).forEach((pin) => {
if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) {
this.onClickPin(pin);
eventComplete = true;
}
});
});
if (eventComplete)
return;
if (this.placingWire != null)
this.anchorWirePlacement();
if (this.placingChip != null)
this.endChipPlacement();
if (this.placingPin != null)
this.endPinPlacement();
}
};
onMouseDown = (event: MouseEvent) => {
event.preventDefault();
this.setMousePosition(event);
if (event.button !== 0 || this.isPlacing)
return;
this.circuit.chips.forEach((chip, index) => {
if (!this.isPlacing && this.circuit.isPointInsideRect(chip, this.mousePosition)) {
let hoveringPin = false;
chip.inputPins.concat(chip.outputPins).forEach((pin) => {
if (pin.position.getDistance(this.mousePosition.x, this.mousePosition.y) <= PIN.radius)
hoveringPin = true;
});
if (!hoveringPin)
this.editChipPlacement(chip, index);
}
});
};
onKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case "Shift":
event.preventDefault();
this.snapping = true;
this.onMouseMove();
break;
case "Backspace":
case "Delete":
case "Escape":
event.preventDefault();
if (this.placingWire != null)
this.cancelWirePlacement();
if (this.placingChip != null)
this.cancelChipPlacement();
break;
}
};
onKeyUp = (event: KeyboardEvent) => {
switch (event.key) {
case "Shift":
event.preventDefault();
this.snapping = false;
this.onMouseMove();
break;
}
};
startWirePlacement(pin: Pin) {
const isInputPin = pin.isPointingRight;
// Start wire placement
const inputPin = isInputPin ? pin : undefined;
const outputPin = !isInputPin ? pin : undefined;
const anchorPoint = this.mousePosition.clone;
this.placingWire = new Wire(this.circuit, "red", inputPin, outputPin, [anchorPoint]);
if (!isInputPin)
this.placingWire.placedBackwards = true;
this.circuit.wires.push(this.placingWire);
}
snapWireHorizontally(lastAnchorPoint: Vector2, previousAnchorPoint: Vector2) {
lastAnchorPoint.x = this.mousePosition.x;
lastAnchorPoint.y = previousAnchorPoint.y;
// Snapping horizontal wire to other wires
let anchorPoints: Vector2[] = [];
this.circuit.wires.forEach((wire, index) => {
if (index < this.circuit.wires.length - 1)
anchorPoints = anchorPoints.concat(wire.anchorPoints);
});
let closestPositionX: number | undefined;
let closestDistance: number | undefined;
anchorPoints.forEach((point) => {
const distance = Math.abs(this.mousePosition.x - point.x);
if (closestDistance == null || closestDistance > distance) {
closestPositionX = point.x;
closestDistance = distance;
}
});
if (closestDistance != null && closestPositionX != null && closestDistance < WIRE.snappingSensitivity)
lastAnchorPoint.x = closestPositionX;
}
snapWireVertically(lastAnchorPoint: Vector2, previousAnchorPoint: Vector2) {
lastAnchorPoint.x = previousAnchorPoint.x;
lastAnchorPoint.y = this.mousePosition.y;
// Snapping vertical wire to pins
let pins: Pin[];
if (!this.placingWire?.placedBackwards) {
pins = this.circuit.outputPins;
this.circuit.chips.forEach((chip) => {
pins = pins.concat(chip.inputPins);
});
} else {
pins = this.circuit.inputPins;
this.circuit.chips.forEach((chip) => {
pins = pins.concat(chip.outputPins);
});
}
let closestPositionY: number | undefined;
let closestDistance: number | undefined;
pins.forEach((pin) => {
const distance = Math.abs(this.mousePosition.y - pin.position.y);
if (closestDistance == null || closestDistance > distance) {
closestPositionY = pin.position.y;
closestDistance = distance;
}
});
if (closestDistance != null && closestPositionY != null && closestDistance < WIRE.snappingSensitivity)
lastAnchorPoint.y = closestPositionY;
}
updateWirePlacement() {
const anchorCount = this.placingWire?.anchorPoints.length;
if (anchorCount == null) return;
const lastAnchorPoint = this.placingWire?.anchorPoints[anchorCount - 1];
if (lastAnchorPoint == null) return;
if (!this.snapping) {
lastAnchorPoint.x = this.mousePosition.x;
lastAnchorPoint.y = this.mousePosition.y;
} else {
// Wire snapping
let previousAnchorPoint: Vector2 | undefined;
if (anchorCount >= 2) {
previousAnchorPoint = this.placingWire?.anchorPoints[anchorCount - 2];
} else if (!this.placingWire?.placedBackwards) {
previousAnchorPoint = this.placingWire?.inputPin.position;
} else {
previousAnchorPoint = this.placingWire?.outputPin.position;
}
if (previousAnchorPoint == null) return;
const deltaX = Math.abs(this.mousePosition.x - previousAnchorPoint.x);
const deltaY = Math.abs(this.mousePosition.y - previousAnchorPoint.y);
if (deltaX > deltaY) {
this.snapWireHorizontally(lastAnchorPoint, previousAnchorPoint);
} else {
this.snapWireVertically(lastAnchorPoint, previousAnchorPoint);
}
}
}
anchorWirePlacement() {
this.placingWire?.anchorPoints.push(this.mousePosition.clone);
}
cancelWirePlacement() {
this.placingWire = null;
this.isPlacing = false;
this.circuit.wires.pop();
}
endWirePlacement(pin: Pin) {
const isInputPin = pin.isPointingRight;
if (this.placingWire == null) return;
let correctPlacement = false;
if (!isInputPin && !this.placingWire.placedBackwards) {
this.placingWire.outputPin = pin;
correctPlacement = true;
} else if (isInputPin && this.placingWire.placedBackwards) {
this.placingWire.inputPin = pin;
correctPlacement = true;
}
if (correctPlacement) {
this.placingWire.anchorPoints.pop();
this.placingWire.inputPin.addOutputWire(this.placingWire);
this.placingWire.inputPin.update();
this.placingWire = null;
this.isPlacing = false;
}
}
startChipPlacement(chip: Chip) {
const newChip = new Chip(this.circuit, chip.name, chip.color, false, chip.inputCount, chip.outputCount);
newChip.setLogic(chip.logic);
newChip.position = new Vector2(
this.mousePosition.x - newChip.size.x / 2,
this.mousePosition.y - newChip.size.y / 2
);
this.placingChip = newChip;
this.isPlacing = true;
this.circuit.chips.push(newChip);
}
editChipPlacement(chip: Chip, index: number) {
this.placingOffset = new Vector2(
(chip.position.x + chip.size.x / 2) - this.mousePosition.x,
(chip.position.y + chip.size.y / 2) - this.mousePosition.y
);
this.previousPlacement = chip.position.clone;
this.circuit.chips.push(this.circuit.chips.splice(index, 1)[0]);
this.placingChip = chip;
this.isPlacing = true;
}
updateChipPlacement() {
if (this.placingChip == null) return;
this.placingChip.position.x = this.mousePosition.x - this.placingChip.size.x / 2 + this.placingOffset.x;
this.placingChip.position.y = this.mousePosition.y - this.placingChip.size.y / 2 + this.placingOffset.y;
}
cancelChipPlacement() {
if (this.placingChip == null) return;
if (this.previousPlacement != null) {
this.placingChip.position = this.previousPlacement;
this.previousPlacement = null;
} else {
this.circuit.chips.pop();
}
this.placingChip = null;
this.isPlacing = false;
}
endChipPlacement() {
this.placingChip = null;
this.isPlacing = false;
this.placingOffset = Vector2.ZERO;
}
startPinPlacement(isInput: boolean) {
const newPin = new ControlledPin(this.circuit, "PIN", isInput);
newPin.position.x = CONTROLLER.handleTrackWidth + CONTROLLER.pinOffset + CONTROLLER.radius;
newPin.position.y = this.mousePosition.y;
if (isInput) {
this.circuit.inputPins.push(newPin);
} else {
newPin.position.x = this.circuit.size.x - newPin.position.x;
this.circuit.outputPins.push(newPin);
}
this.placingPin = newPin;
}
updatePinPlacement() {
if (this.placingPin != null)
this.placingPin.position.y = this.mousePosition.y;
}
cancelPinPlacement() {
if (this.placingPin?.isInput) {
this.circuit.inputPins.pop();
} else {
this.circuit.outputPins.pop();
}
this.placingPin = null;
}
endPinPlacement() {
this.placingPin = null;
}
isPlacingPin(pin: Pin, index: number) {
if (!pin.isControlled || this.placingPin == null || this.placingPin.isInput != pin.isInput)
return false;
if (pin.isInput) {
return index == this.circuit.inputPins.length - 1;
} else {
return index == this.circuit.outputPins.length - 1;
}
}
}