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 = new Map(); private doodler: Doodler; constructor(segments: TrackSegment[]) { this.doodler = getContextItem("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("showEnds")) { const ends = this.findEnds(); for (const end of ends) { this.doodler.fillCircle(end.pos, 2, { color: "red", // weight: 3, }); if (getContextItem("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(); 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"); 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, ); } }