trainsim/state/states/EditTrackState.ts
2025-02-10 04:41:23 -07:00

400 lines
13 KiB
TypeScript

import { Doodler, Vector } from "@bearmetal/doodler";
import {
getContextItem,
getContextItemOrDefault,
setContextItem,
} from "../../lib/context.ts";
import { InputManager } from "../../lib/input.ts";
import { TrackSystem } from "../../track/system.ts";
import { State, StateMachine } from "../machine.ts";
import { States } from "./index.ts";
import {
BankLeft,
BankRight,
SBendLeft,
SBendRight,
StraightTrack,
} from "../../track/shapes.ts";
import { TrackSegment } from "../../track/system.ts";
import { clamp } from "../../math/clamp.ts";
export class EditTrackState extends State<States> {
override name: States = States.EDIT_TRACK;
override validTransitions: Set<States> = new Set([
States.RUNNING,
States.PAUSED,
]);
private heldEvents: Map<string, (() => void) | undefined> = new Map();
private currentSegment?: TrackSegment;
private selectedSegment?: TrackSegment;
private ghostSegment?: TrackSegment;
private ghostRotated = false;
private closestEnd?: End;
override update(dt: number): void {
const inputManager = getContextItem<InputManager>("inputManager");
const track = getContextItem<TrackSystem>("track");
const doodler = getContextItem<Doodler>("doodler");
// For moving a segment, i.e. the currently active one
// const segment = track.lastSegment;
// if (segment) {
// const firstPoint = segment.points[0].copy();
// const { x, y } = inputManager.getMouseLocation();
// segment.points.forEach((p, i) => {
// const relativePoint = Vector.sub(p, firstPoint);
// p.set(x, y);
// p.add(relativePoint);
// });
// }
if (this.selectedSegment) {
const segment = this.selectedSegment;
const firstPoint = segment.points[0].copy();
const mousePos = inputManager.getMouseLocationV();
segment.points.forEach((p, i) => {
const relativePoint = Vector.sub(p, firstPoint);
p.set(mousePos);
p.add(relativePoint);
});
const ends = track.findEnds();
setContextItem("showEnds", true);
const nearbyEnds = ends.filter((end) => {
const dist = Vector.dist(end.pos, mousePos);
return dist < 20 && end.segment !== segment;
});
let closestEnd = nearbyEnds[0];
for (const end of nearbyEnds) {
if (end === closestEnd) continue;
const closestEndTangent = Vector.add(
closestEnd.tangent.copy().mult(20),
closestEnd.pos,
);
const endTangent = Vector.add(
end.tangent.copy().rotate(Math.PI).mult(20),
end.pos,
);
doodler.drawCircle(closestEndTangent, 4, { color: "red", weight: 1 });
doodler.drawCircle(endTangent, 4, { color: "blue", weight: 1 });
if (
endTangent.dist(mousePos) < closestEndTangent.dist(mousePos) ||
end.pos.dist(mousePos) < closestEnd.pos.dist(mousePos)
) {
closestEnd = end;
}
}
if (closestEnd !== this.closestEnd) {
this.closestEnd = closestEnd;
this.ghostSegment = undefined;
this.ghostRotated = false;
}
if (closestEnd) {
// doodler.drawCircle(closestEnd.pos, 4, { color: "green", weight: 1 });
doodler.line(
closestEnd.pos,
Vector.add(closestEnd.pos, closestEnd.tangent.copy().mult(20)),
{ color: "green" },
);
}
if (
this.closestEnd
) {
if (!this.ghostSegment) {
this.ghostSegment = segment.copy();
this.ghostRotated = false;
}
switch (this.closestEnd.frontOrBack) {
case "front":
this.ghostSegment.setPositionByPoint(
this.closestEnd.pos,
this.ghostSegment.points[0],
);
// this.ghostSegment.points[0] = this.closestEnd.pos;
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
this.closestEnd.tangent.heading(),
this.ghostSegment.points[0],
);
this.ghostRotated = true;
break;
case "back":
this.ghostSegment.setPositionByPoint(
this.closestEnd.pos,
this.ghostSegment.points[3],
);
// this.ghostSegment.points[3] = this.closestEnd.pos;
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
this.closestEnd.tangent.heading(),
this.ghostSegment.points[3],
);
this.ghostRotated = true;
break;
}
// } else if (closestEnd) {
// this.closestEnd = closestEnd;
} else if (!this.closestEnd || !closestEnd) {
this.ghostSegment = undefined;
this.ghostRotated = false;
}
this.selectedSegment?.draw();
if (this.ghostSegment) {
doodler.drawWithAlpha(0.5, () => {
if (!this.ghostSegment) return;
this.ghostSegment.draw();
if (getContextItemOrDefault("debug", false)) {
const colors = getContextItem<string[]>("colors");
for (
const [i, point] of this.ghostSegment.points.entries() ?? []
) {
doodler.fillCircle(point, 4, { color: colors[i + 3] });
}
}
});
}
}
// manipulate only end of segment while maintaining length
// const segment = track.lastSegment;
// if (segment) {
// const p3 = segment.points[2];
// const p4 = segment.points[3];
// let curveLength = Math.round(segment.calculateApproxLength());
// this.startingLength = this.startingLength ?? curveLength;
// curveLength = this.startingLength;
// const { x, y } = inputManager.getMouseLocation();
// p4.set(x, y);
// const points = segment.calculateEvenlySpacedPoints(1);
// if (points.length > curveLength) p4.set(points[curveLength - 1]);
// // doodler.fillText(curveLength.toFixed(2), p3.copy().add(10, 0), 100);
// }
// Adjust angles until tangent points to mouse
// const segment = this.currentSegment;
// if (segment) {
// segment.propagate();
// const mousePos = inputManager.getMouseLocationV();
// const p1 = segment.points[0];
// const p2 = segment.points[1];
// const p3 = segment.points[2];
// const p4 = segment.points[3];
// const prevp3 = p3.copy();
// const dirToMouse = Vector.sub(mousePos, p2).normalize();
// const angleToMouse = dirToMouse.heading();
// const dirToP1 = Vector.sub(p2, p1).normalize();
// const angleToP1 = dirToP1.heading();
// const p2DistToMouse = Vector.dist(p2, mousePos);
// const p3DistToMouse = Vector.dist(p3, mousePos);
// const distToP3 = Vector.dist(p2, p3);
// const distToP4 = Vector.dist(prevp3, p4);
// const goodangle = clamp(
// angleToMouse - angleToP1,
// angleToP1 - .6,
// angleToP1 + .6,
// );
// if (
// // Math.abs(goodangle) < .6 &&
// p2DistToMouse > distToP3 &&
// p3DistToMouse > distToP4
// ) {
// {
// const dirToNewP3 = dirToP1.copy().rotate(
// goodangle / 2,
// );
// dirToNewP3.setMag(distToP3);
// p3.set(Vector.add(p2, dirToNewP3));
// doodler.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" });
// doodler.line(
// p2,
// Vector.add(p2, dirToNewP3),
// {
// color: "red",
// },
// );
// }
// {
// const dirToMouse = Vector.sub(mousePos, p3).normalize();
// const dirToP3 = Vector.sub(p3, p2).normalize();
// const angleToP3 = dirToP3.heading();
// const goodangle = clamp(
// dirToMouse.heading() - angleToP3,
// angleToP3 - .6,
// angleToP3 + .6,
// );
// const dirToNewP4 = dirToP3.copy().rotate(
// goodangle / 2,
// );
// dirToNewP4.setMag(distToP4);
// p4.set(Vector.add(p3, dirToNewP4));
// doodler.line(p3, Vector.add(p3, dirToNewP4), { color: "green" });
// }
// segment.clampLength();
// }
// // doodler.fillText(
// // segment.calculateApproxLength().toFixed(2),
// // p2.copy().add(10, 0),
// // 100,
// // );
// }
const translation = new Vector(0, 0);
if (inputManager.getKeyState("ArrowUp")) {
translation.y -= 1;
}
if (inputManager.getKeyState("ArrowDown")) {
translation.y += 1;
}
if (inputManager.getKeyState("ArrowLeft")) {
translation.x -= 1;
}
if (inputManager.getKeyState("ArrowRight")) {
translation.x += 1;
}
if (translation.x !== 0 || translation.y !== 0) {
track.translate(translation);
}
track.draw(true);
// TODO
// Draw ui
// Draw track points
// Draw track tangents
}
override start(): void {
setContextItem("trackSegments", [
undefined,
new StraightTrack(),
new SBendLeft(),
new SBendRight(),
new BankLeft(),
new BankRight(),
]);
const inputManager = getContextItem<InputManager>("inputManager");
this.heldEvents.set("e", inputManager.offKey("e"));
this.heldEvents.set("Escape", inputManager.offKey("Escape"));
inputManager.onKey("e", () => {
const state = getContextItem<StateMachine<States>>("state");
state.transitionTo(States.RUNNING);
});
const track = getContextItem<TrackSystem>("track");
setContextItem("trackCopy", track.copy());
inputManager.onKey("Escape", () => {
const trackCopy = getContextItem<TrackSystem>("trackCopy");
setContextItem("track", trackCopy);
setContextItem("trackCopy", undefined);
const state = getContextItem<StateMachine<States>>("state");
state.transitionTo(States.RUNNING);
});
inputManager.onKey(" ", () => {
if (this.selectedSegment) {
this.selectedSegment = undefined;
} else {
this.selectedSegment = new StraightTrack();
}
});
inputManager.onMouse("left", () => {
const track = getContextItem<TrackSystem>("track");
if (this.ghostSegment && this.closestEnd) {
const segment = this.ghostSegment.cleanCopy();
switch (this.closestEnd.frontOrBack) {
case "front":
this.closestEnd.segment.frontNeighbours.push(segment);
segment.backNeighbours.push(this.closestEnd.segment);
break;
case "back":
this.closestEnd.segment.backNeighbours.push(segment);
segment.frontNeighbours.push(this.closestEnd.segment);
break;
}
track.registerSegment(segment);
this.ghostSegment = undefined;
this.closestEnd = undefined;
} else if (this.selectedSegment) {
track.registerSegment(this.selectedSegment.cleanCopy());
// this.selectedSegment = new StraightTrack();
} else {
this.selectedSegment = undefined;
}
});
// inputManager.onKey("w", () => {
// const track = getContextItem<TrackSystem>("track");
// const segment = track.lastSegment;
// if (!segment) return;
// const n = new StraightTrack(segment.points[3]);
// const t = segment.tangent(1).heading();
// n.rotate(t);
// segment.frontNeighbours.push(n);
// track.registerSegment(n);
// this.currentSegment = n;
// });
// inputManager.onKey("1", () => {
// this.currentSegment = track.firstSegment;
// });
inputManager.onNumberKey((i) => {
console.log(i);
const segments = getContextItem<TrackSegment[]>("trackSegments");
this.selectedSegment = segments[i];
this.ghostRotated = false;
this.ghostSegment = undefined;
});
// this.currentSegment = track.lastSegment;
inputManager.onKey("z", () => {
if (inputManager.getKeyState("Control")) {
const segment = track.lastSegment;
if (!segment) return;
this.redoBuffer.push(segment);
if (this.redoBuffer.length > 100) {
this.redoBuffer.shift();
}
track.unregisterSegment(segment);
}
});
inputManager.onKey("y", () => {
if (inputManager.getKeyState("Control")) {
const segment = this.redoBuffer.pop();
if (!segment) return;
track.registerSegment(segment);
}
});
// TODO
// Cache trains and save
}
redoBuffer: TrackSegment[] = [];
override stop(): void {
const inputManager = getContextItem<InputManager>("inputManager");
inputManager.offKey("e");
inputManager.offKey("w");
inputManager.offKey("Escape");
inputManager.offMouse("left");
if (this.heldEvents.size > 0) {
for (const [key, cb] of this.heldEvents) {
if (cb) {
getContextItem<InputManager>("inputManager").onKey(key, cb);
}
this.heldEvents.delete(key);
}
}
setContextItem("trackCopy", undefined);
setContextItem("trackSegments", undefined);
}
}