235 lines
6.1 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}
|