import { PathSegment } from "./math/path.ts"; import { Vector } from "doodler"; import { Train } from "./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; }