513 lines
No EOL
13 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
} |