trainsim/track/system.ts

235 lines
6.1 KiB
TypeScript

import { Doodler, Vector } from "@bearmetal/doodler";
import { PathSegment } from "../math/path.ts";
import { getContextItem, setDefaultContext } from "../lib/context.ts";
export class TrackSystem {
private segments: Map<string, TrackSegment> = new Map();
private doodler: Doodler;
constructor(segments: TrackSegment[]) {
this.doodler = getContextItem<Doodler>("doodler");
for (const segment of segments) {
this.segments.set(segment.id, segment);
}
}
get firstSegment() {
return this.segments.values().next().value;
}
get lastSegment() {
return this.segments.values().toArray().pop();
}
registerSegment(segment: TrackSegment) {
segment.setTrack(this);
this.segments.set(segment.id, segment);
}
draw(showControls = false) {
for (const segment of this.segments.values()) {
segment.draw(showControls);
}
try {
if (getContextItem<boolean>("showEnds")) {
const ends = this.findEnds();
for (const end of ends) {
this.doodler.fillCircle(end.pos, 2, {
color: "red",
// weight: 3,
});
if (getContextItem<boolean>("debug")) {
this.doodler.line(
end.pos,
end.pos.copy().add(end.tangent.copy().mult(20)),
{
color: "blue",
// weight: 3,
},
);
}
}
}
} catch {
setDefaultContext({ showEnds: false });
}
}
findEnds() {
const ends: { pos: Vector; segment: TrackSegment; tangent: Vector }[] = [];
for (const segment of this.segments.values()) {
const [a, b, c, d] = segment.points;
{
const tangent = Vector.sub(a, b).normalize();
const pos = a.copy();
ends.push({ pos, segment, tangent });
}
{
const tangent = Vector.sub(d, c).normalize();
const pos = d.copy();
ends.push({ pos, segment, tangent });
}
}
return ends;
}
serialize() {
return this.segments.values().map((s) => s.serialize()).toArray();
}
copy() {
const track = new TrackSystem([]);
for (const segment of this.segments.values()) {
track.segments.set(segment.id, segment.copy());
}
return track;
}
static deserialize(data: any) {
if (data.length === 0) return undefined;
const track = new TrackSystem([]);
const neighborMap = new Map<string, [string[], string[]]>();
for (const segment of data) {
track.segments.set(segment.id, TrackSegment.deserialize(segment));
}
for (const segment of track.segments.values()) {
segment.setTrack(track);
const neighbors = neighborMap.get(segment.id);
if (neighbors) {
segment.backNeighbours = neighbors[1].map((id) =>
track.segments.get(id)
).filter((s) => s) as TrackSegment[];
segment.frontNeighbours = neighbors[0].map((id) =>
track.segments.get(id)
).filter((s) => s) as TrackSegment[];
}
}
return track;
}
}
type VectorSet = [Vector, Vector, Vector, Vector];
export class TrackSegment extends PathSegment {
frontNeighbours: TrackSegment[] = [];
backNeighbours: TrackSegment[] = [];
track?: TrackSystem;
doodler: Doodler;
id: string;
constructor(p: VectorSet, id?: string) {
super(p);
this.doodler = getContextItem<Doodler>("doodler");
this.id = id ?? crypto.randomUUID();
}
setTrack(t: TrackSystem) {
this.track = t;
}
draw(showControls = false) {
this.doodler.drawBezier(
this.points[0],
this.points[1],
this.points[2],
this.points[3],
{
strokeColor: "#ffffff50",
},
);
if (showControls) {
// this.doodler.drawCircle(this.points[0], 4, {
// color: "red",
// weight: 3,
// });
this.doodler.drawCircle(this.points[1], 4, {
color: "red",
weight: 3,
});
this.doodler.drawCircle(this.points[2], 4, {
color: "red",
weight: 3,
});
// this.doodler.drawCircle(this.points[3], 4, {
// color: "red",
// weight: 3,
// });
}
}
serialize() {
return {
p: this.points.map((p) => p.array()),
id: this.id,
bNeighbors: this.backNeighbours.map((n) => n.id),
fNeighbors: this.frontNeighbours.map((n) => n.id),
};
}
copy() {
return new TrackSegment(
this.points.map((p) => p.copy()) as VectorSet,
this.id,
);
}
propagate() {
const [_, __, p3, p4] = this.points;
const tangent = Vector.sub(p4, p3);
for (const fNeighbour of this.frontNeighbours) {
fNeighbour.receivePropagation(tangent);
}
}
lastHeading?: number;
receivePropagation(tangent: Vector) {
const [p1, p2, p3, p4] = this.points;
// const angle = tangent.heading() - (this.lastHeading ?? 0);
// this.lastHeading = tangent.heading();
// const newP2 = Vector.add(p1, tangent);
// const p2ToP3 = Vector.sub(p3, p2);
// p2ToP3.rotate(angle);
// p3.set(Vector.add(newP2, p2ToP3));
// const p2Top4 = Vector.sub(p4, p2);
// p2Top4.rotate(angle);
// p4.set(Vector.add(newP2, p2Top4));
// p2.set(newP2);
this.rotate(tangent);
this.propagate();
}
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
rotate(angle: number | Vector) {
const [p1, p2, p3, p4] = this.points;
let newP2;
if (angle instanceof Vector) {
const tan = angle;
angle = tan.heading() - (this.lastHeading ?? 0);
this.lastHeading = tan.heading();
newP2 = Vector.add(p1, tan);
} else {
const p1ToP2 = Vector.sub(p2, p1);
p1ToP2.rotate(angle);
newP2 = Vector.add(p1, p1ToP2);
}
const p2ToP3 = Vector.sub(p3, p2);
p2ToP3.rotate(angle);
p3.set(Vector.add(newP2, p2ToP3));
const p2Top4 = Vector.sub(p4, p2);
p2Top4.rotate(angle);
p4.set(Vector.add(newP2, p2Top4));
p2.set(newP2);
}
static deserialize(data: any) {
return new TrackSegment(
data.p.map((p: [number, number, number]) => new Vector(p[0], p[1], p[2])),
data.id,
);
}
}