import { PathSegment } from "./math/path.ts"; import { Vector } from "doodler"; import { Train } from "./train/train.ts"; export class Track extends PathSegment { editable = false; next: Track; prev: Track; id: string; constructor( points: [Vector, Vector, Vector, Vector], next?: Track, prev?: Track, ) { super(points); this.id = crypto.randomUUID(); this.next = next || this; this.prev = prev || this; } // followTrack(train: Train): [Vector, number] { // const predict = train.velocity.copy(); // predict.normalize(); // predict.mult(1); // const predictpos = Vector.add(train.position, predict) // // const leading = train.leadingPoint; // // let closest = this.points[0]; // // let closestDistance = this.getClosestPoint(leading); // let [closest, closestDistance, closestT] = this.getClosestPoint(predictpos); // // deno-lint-ignore no-this-alias // let mostValid: Track = this; // if (this.next !== this) { // const [point, distance, t] = this.next.getClosestPoint(predictpos); // if (distance < closestDistance) { // closest = point; // closestDistance = distance; // mostValid = this.next; // closestT = t; // } // } // if (this.prev !== this) { // const [point, distance, t] = this.next.getClosestPoint(predictpos); // if (distance < closestDistance) { // closest = point; // closestDistance = distance; // mostValid = this.next; // closestT = t; // } // } // train.currentTrack = mostValid; // train.arrive(closest); // // if (predictpos.dist(closest) > 2) train.arrive(closest); // return [closest, closestT]; // } getNearestPoint(p: Vector) { let [closest, closestDistance] = this.getClosestPoint(p); if (this.next !== this) { const [point, distance, t] = this.next.getClosestPoint(p); if (distance < closestDistance) { closest = point; closestDistance = distance; } } if (this.prev !== this) { const [point, distance, t] = this.next.getClosestPoint(p); if (distance < closestDistance) { closest = point; closestDistance = distance; } } return closest; } getAllPointsInRange(v: Vector, r: number) { const points: [number, PathSegment][] = this.getPointsWithinRadius(v, r) .concat( this.next.getPointsWithinRadius(v, r), this.prev.getPointsWithinRadius(v, r), ); return points; } draw(): void { super.draw(); if (this.editable) { const [a, b, c, d] = this.points; doodler.line(a, b); doodler.line(c, d); } } setNext(t: Track) { this.next = t; this.next.points[0] = this.points[3]; } setPrev(t: Track) { this.prev = t; this.prev.points[3] = this.points[0]; } } export class Spline { segments: T[] = []; ctx?: CanvasRenderingContext2D; evenPoints: Vector[]; pointSpacing: number; get points() { return Array.from(new Set(this.segments.flatMap((s) => s.points))); } nodes: IControlNode[]; constructor(segs: T[]) { this.segments = segs; this.pointSpacing = 1; this.evenPoints = this.calculateEvenlySpacedPoints(1); this.nodes = []; for (let i = 0; i < this.points.length; i += 3) { const node: IControlNode = { anchor: this.points[i], controls: [ this.points.at(i - 1)!, this.points[(i + 1) % this.points.length], ], mirrored: false, tangent: true, }; this.nodes.push(node); } } setContext(ctx: CanvasRenderingContext2D) { this.ctx = ctx; for (const segment of this.segments) { segment.setContext(ctx); } } draw() { for (const segment of this.segments) { segment.draw(); } } calculateEvenlySpacedPoints(spacing: number, resolution = 1) { this.pointSpacing = 1; // return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution)); const points: Vector[] = []; points.push(this.segments[0].points[0]); let prev = points[0]; let distSinceLastEvenPoint = 0; for (const seg of this.segments) { let t = 0; const div = Math.ceil(seg.length * resolution * 10); while (t < 1) { t += 1 / div; const point = seg.getPointAtT(t); distSinceLastEvenPoint += prev.dist(point); if (distSinceLastEvenPoint >= spacing) { const overshoot = distSinceLastEvenPoint - spacing; const evenPoint = Vector.add( point, Vector.sub(point, prev).normalize().mult(overshoot), ); distSinceLastEvenPoint = overshoot; points.push(evenPoint); prev = evenPoint; } prev = point; } } this.evenPoints = points; return points; } followEvenPoints(t: number) { if (t < 0) t += this.evenPoints.length; const i = Math.floor(t); const a = this.evenPoints[i]; const b = this.evenPoints[(i + 1) % this.evenPoints.length]; return Vector.lerp(a, b, t % 1); } calculateApproxLength() { for (const s of this.segments) { s.calculateApproxLength(); } } toggleNodeTangent(p: Vector) { const node = this.nodes.find((n) => n.anchor === p); node && (node.tangent = !node.tangent); } toggleNodeMirrored(p: Vector) { const node = this.nodes.find((n) => n.anchor === p); node && (node.mirrored = !node.mirrored); } handleNodeEdit(p: Vector, movement: { x: number; y: number }) { const node = this.nodes.find((n) => n.anchor === p || n.controls.includes(p) ); if (!node || !(node.mirrored || node.tangent)) return; if (node.anchor !== p) { if (node.mirrored || node.tangent) { const mover = node.controls.find((e) => e !== p)!; const v = Vector.sub(node.anchor, p); if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag()); mover.set(Vector.add(v, node.anchor)); } } else { for (const control of node.controls) { control.add(movement.x, movement.y); } } } } export const generateSquareTrack = () => { const first = new Track([ new Vector(20, 40), new Vector(20, 100), new Vector(20, 300), new Vector(20, 360), ]); const second = new Track([ first.points[3], new Vector(20, 370), new Vector(30, 380), new Vector(40, 380), ]); const third = new Track([ second.points[3], new Vector(100, 380), new Vector(300, 380), new Vector(360, 380), ]); const fourth = new Track([ third.points[3], new Vector(370, 380), new Vector(380, 370), new Vector(380, 360), ]); const fifth = new Track([ fourth.points[3], new Vector(380, 300), new Vector(380, 100), new Vector(380, 40), ]); const sixth = new Track([ fifth.points[3], new Vector(380, 30), new Vector(370, 20), new Vector(360, 20), ]); const seventh = new Track([ sixth.points[3], new Vector(300, 20), new Vector(100, 20), new Vector(40, 20), ]); const eighth = new Track([ seventh.points[3], new Vector(30, 20), new Vector(20, 30), first.points[0], ]); const tracks = [first, second, third, fourth, fifth, sixth, seventh, eighth]; for (const [i, track] of tracks.entries()) { track.next = tracks[(i + 1) % tracks.length]; track.prev = tracks.at(i - 1)!; } // first.next = second; // first.prev = eighth; // second.next = third; // second.prev = first; // third. return new Spline([ first, second, third, fourth, fifth, sixth, seventh, eighth, ]); }; export const loadFromJson = () => { const json = JSON.parse(localStorage.getItem("railPath") || ""); if (!json) return generateSquareTrack(); const segments: Track[] = []; for (const { points } of json.segments) { segments.push( new Track( points.map((p: { x: number; y: number }) => new Vector(p.x, p.y)), ), ); } for (const [i, s] of segments.entries()) { s.setNext(segments[(i + 1) % segments.length]); s.setPrev(segments.at(i - 1)!); } return new Spline(segments); }; export interface IControlNode { anchor: Vector; controls: [Vector, Vector]; tangent: boolean; mirrored: boolean; }