Recalculation on track edit end Changes rendering of ties to be evenly spaced Fixes ghost and held track rendering
421 lines
14 KiB
TypeScript
421 lines
14 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;
|
|
|
|
layers: number[] = [];
|
|
|
|
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],
|
|
);
|
|
const ghostEndTangent = this.ghostSegment.tangent(1);
|
|
// this.ghostSegment.points[3] = this.closestEnd.pos;
|
|
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
|
|
this.closestEnd.tangent.heading() - ghostEndTangent.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;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// TODO
|
|
// Draw ui
|
|
// Draw track points
|
|
// Draw track tangents
|
|
}
|
|
override start(): void {
|
|
const doodler = getContextItem<Doodler>("doodler");
|
|
this.layers.push(
|
|
doodler.createLayer(() => {
|
|
this.selectedSegment?.draw(false, true);
|
|
if (this.ghostSegment) {
|
|
doodler.drawWithAlpha(0.5, () => {
|
|
if (!this.ghostSegment) return;
|
|
this.ghostSegment.draw(false, true);
|
|
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] });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
track.draw(true);
|
|
}),
|
|
);
|
|
|
|
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) => {
|
|
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
|
|
|
|
// const trackCount = 2000;
|
|
// for (let i = 0; i < trackCount; i++) {
|
|
// const seg = new StraightTrack();
|
|
// track.registerSegment(seg);
|
|
// }
|
|
}
|
|
redoBuffer: TrackSegment[] = [];
|
|
override stop(): void {
|
|
for (const layer of this.layers) {
|
|
getContextItem<Doodler>("doodler").deleteLayer(layer);
|
|
}
|
|
|
|
const track = getContextItem<TrackSystem>("track");
|
|
track.recalculateAll();
|
|
|
|
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);
|
|
}
|
|
}
|