diff --git a/bundle.js b/bundle.js index 0a66da6..1573d30 100644 --- a/bundle.js +++ b/bundle.js @@ -71,65 +71,6 @@ }, 2); } - // lib/input.ts - var InputManager = class { - keyStates = /* @__PURE__ */ new Map(); - mouseStates = /* @__PURE__ */ new Map(); - mouseLocation = { x: 0, y: 0 }; - mouseDelta = { x: 0, y: 0 }; - keyEvents = /* @__PURE__ */ new Map(); - mouseEvents = /* @__PURE__ */ new Map(); - constructor() { - document.addEventListener("keydown", (e) => { - this.keyStates.set(e.key, true); - this.keyEvents.get(e.key)?.call(e); - }); - document.addEventListener("keyup", (e) => { - this.keyStates.set(e.key, false); - }); - document.addEventListener("mousedown", (e) => { - this.mouseStates.set(e.button, true); - this.mouseEvents.get(e.button)?.call(e); - }); - document.addEventListener("mouseup", (e) => { - this.mouseStates.set(e.button, false); - }); - self.addEventListener("mousemove", (e) => { - this.mouseLocation = { x: e.clientX, y: e.clientY }; - this.mouseDelta = { - x: e.movementX, - y: e.movementY - }; - }); - } - getKeyState(key) { - return this.keyStates.get(key); - } - getMouseState(key) { - return this.mouseStates.get(key); - } - getMouseLocation() { - return this.mouseLocation; - } - getMouseDelta() { - return this.mouseDelta; - } - onKey(key, cb) { - this.keyEvents.set(key, cb); - } - onMouse(key, cb) { - this.mouseEvents.set(key, cb); - } - offKey(key) { - const events = this.keyEvents.get(key); - this.keyEvents.delete(key); - return events; - } - offMouse(key) { - this.mouseEvents.delete(key); - } - }; - // https://jsr.io/@bearmetal/doodler/0.0.3/geometry/constants.ts var Constants = { TWO_PI: Math.PI * 2 @@ -1072,6 +1013,82 @@ } }; + // lib/input.ts + var InputManager = class { + keyStates = /* @__PURE__ */ new Map(); + mouseStates = /* @__PURE__ */ new Map(); + mouseLocation = { x: 0, y: 0 }; + mouseDelta = { x: 0, y: 0 }; + keyEvents = /* @__PURE__ */ new Map(); + mouseEvents = /* @__PURE__ */ new Map(); + constructor() { + document.addEventListener("keydown", (e) => { + this.keyStates.set(e.key, true); + this.keyEvents.get(e.key)?.call(e); + }); + document.addEventListener("keyup", (e) => { + this.keyStates.set(e.key, false); + }); + document.addEventListener("mousedown", (e) => { + this.mouseStates.set(e.button, true); + this.mouseEvents.get(e.button)?.call(e); + }); + document.addEventListener("mouseup", (e) => { + this.mouseStates.set(e.button, false); + }); + self.addEventListener("mousemove", (e) => { + this.mouseLocation = { x: e.clientX, y: e.clientY }; + this.mouseDelta = { + x: e.movementX, + y: e.movementY + }; + }); + } + getKeyState(key) { + return this.keyStates.get(key); + } + getMouseState(key) { + return this.mouseStates.get(key); + } + getMouseLocation() { + if (getContextItem("doodler") instanceof ZoomableDoodler) { + return getContextItem("doodler").screenToWorld( + this.mouseLocation.x, + this.mouseLocation.y + ); + } + return this.mouseLocation; + } + getMouseLocationV() { + if (getContextItem("doodler") instanceof ZoomableDoodler) { + return new Vector( + getContextItem("doodler").screenToWorld( + this.mouseLocation.x, + this.mouseLocation.y + ) + ); + } + return new Vector(this.mouseLocation); + } + getMouseDelta() { + return this.mouseDelta; + } + onKey(key, cb) { + this.keyEvents.set(key, cb); + } + onMouse(key, cb) { + this.mouseEvents.set(key, cb); + } + offKey(key) { + const events = this.keyEvents.get(key); + this.keyEvents.delete(key); + return events; + } + offMouse(key) { + this.mouseEvents.delete(key); + } + }; + // lib/resources.ts var ResourceManager = class { resources = /* @__PURE__ */ new Map(); @@ -1186,114 +1203,15 @@ } }; - // state/states/EditTrackState.ts - var EditTrackState = class extends State { - name = 3 /* EDIT_TRACK */; - validTransitions = /* @__PURE__ */ new Set([ - 1 /* RUNNING */, - 2 /* PAUSED */ - ]); - heldEvents = /* @__PURE__ */ new Map(); - update(dt) { - const inputManager2 = getContextItem("inputManager"); - const track = getContextItem("track"); - const firstSegment = track.firstSegment; - if (firstSegment) { - const firstPoint = firstSegment.points[0].copy(); - const { x, y } = inputManager2.getMouseLocation(); - firstSegment.points.forEach((p, i) => { - const relativePoint = Vector.sub(p, firstPoint); - p.set(x, y); - p.add(relativePoint); - }); - } - track.draw(true); - } - start() { - const inputManager2 = getContextItem("inputManager"); - this.heldEvents.set("e", inputManager2.offKey("e")); - this.heldEvents.set("Escape", inputManager2.offKey("Escape")); - inputManager2.onKey("e", () => { - const state2 = getContextItem("state"); - state2.transitionTo(1 /* RUNNING */); - }); - const track = getContextItem("track"); - setContextItem("trackCopy", track.copy()); - inputManager2.onKey("Escape", () => { - const trackCopy = getContextItem("trackCopy"); - setContextItem("track", trackCopy); - const state2 = getContextItem("state"); - state2.transitionTo(1 /* RUNNING */); - }); - } - stop() { - if (this.heldEvents.size > 0) { - for (const [key, cb] of this.heldEvents) { - if (cb) { - getContextItem("inputManager").onKey(key, cb); - } - this.heldEvents.delete(key); - } - } - } - }; - - // state/states/PausedState.ts - var PausedState = class extends State { - name = 2 /* PAUSED */; - validTransitions = /* @__PURE__ */ new Set([ - 0 /* LOAD */, - 1 /* RUNNING */, - 3 /* EDIT_TRACK */, - 4 /* EDIT_TRAIN */ - ]); - update(dt) { - throw new Error("Method not implemented."); - } - start() { - throw new Error("Method not implemented."); - } - stop() { - throw new Error("Method not implemented."); - } - }; - - // state/states/RunningState.ts - var RunningState = class extends State { - name = 1 /* RUNNING */; - validTransitions = /* @__PURE__ */ new Set([ - 2 /* PAUSED */, - 3 /* EDIT_TRACK */ - ]); - update(dt) { - const ctx2 = getContext(); - ctx2.track.draw(); - for (const train of ctx2.trains) { - train.draw(); - } - } - start() { - } - stop() { - } - }; - - // inputs.ts - function bootstrapInputs() { - const inputManager2 = getContextItem("inputManager"); - inputManager2.onKey("e", () => { - const state2 = getContextItem("state"); - state2.transitionTo(3 /* EDIT_TRACK */); - }); - } - // math/path.ts var PathSegment = class { points; length; + startingLength; constructor(points) { this.points = points; this.length = this.calculateApproxLength(100); + this.startingLength = Math.round(this.length); } getPointAtT(t) { const [a, b, c, d] = this.points; @@ -1401,7 +1319,7 @@ }, { prev: void 0, length: 0 }).length; return this.length; } - calculateEvenlySpacedPoints(spacing, resolution = 1) { + calculateEvenlySpacedPoints(spacing, resolution = 1, targetLength) { const points = []; points.push(this.points[0]); let prev = points[0]; @@ -1424,8 +1342,41 @@ } prev = point; } + if (targetLength && points.length < targetLength) { + while (points.length < targetLength) { + t += 1 / div; + const point = this.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; + } + } return points; } + calculateSubdividedPoints(numberOfPoints) { + const points = []; + for (let i = 0; i < numberOfPoints; i++) { + const point = this.getPointAtT(i / numberOfPoints); + points.push(point); + } + return points; + } + clampLength() { + const curveLength = this.startingLength; + const points = this.calculateEvenlySpacedPoints(1, 1, curveLength + 1); + if (points.length >= curveLength) { + this.points[3].set(points[curveLength]); + } + } }; // track/system.ts @@ -1441,6 +1392,13 @@ get firstSegment() { return this.segments.values().next().value; } + get lastSegment() { + return this.segments.values().toArray().pop(); + } + registerSegment(segment) { + segment.setTrack(this); + this.segments.set(segment.id, segment); + } draw(showControls = false) { for (const segment of this.segments.values()) { segment.draw(showControls); @@ -1567,6 +1525,41 @@ this.id ); } + propagate() { + const [_, __, p3, p4] = this.points; + const tangent = Vector.sub(p4, p3); + for (const fNeighbour of this.frontNeighbours) { + fNeighbour.receivePropagation(tangent); + } + } + lastHeading; + receivePropagation(tangent) { + const [p1, p2, p3, p4] = this.points; + this.rotate(tangent); + this.propagate(); + } + // TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation + rotate(angle) { + 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) { return new _TrackSegment( data.p.map((p) => new Vector(p[0], p[1], p[2])), @@ -1588,6 +1581,161 @@ } }; + // state/states/EditTrackState.ts + var EditTrackState = class extends State { + name = 3 /* EDIT_TRACK */; + validTransitions = /* @__PURE__ */ new Set([ + 1 /* RUNNING */, + 2 /* PAUSED */ + ]); + heldEvents = /* @__PURE__ */ new Map(); + currentSegment; + update(dt) { + const inputManager2 = getContextItem("inputManager"); + const track = getContextItem("track"); + const doodler2 = getContextItem("doodler"); + const segment = this.currentSegment; + if (segment) { + segment.propagate(); + const mousePos = inputManager2.getMouseLocationV(); + const p1 = segment.points[0]; + const p2 = segment.points[1]; + const p3 = segment.points[2]; + const p4 = segment.points[3]; + const prevp3 = p3.copy(); + const dirToMouse = Vector.sub(mousePos, p2).normalize(); + const angleToMouse = dirToMouse.heading(); + const angleToP1 = Vector.sub(p2, p1).heading(); + const p2DistToMouse = Vector.dist(p2, mousePos); + const p3DistToMouse = Vector.dist(p3, mousePos); + const distToP3 = Vector.dist(p2, p3); + const distToP4 = Vector.dist(prevp3, p4); + if (Math.abs(angleToMouse - angleToP1) < 0.6 && p2DistToMouse > distToP3 && p3DistToMouse > distToP4) { + { + const dirToNewP3 = dirToMouse.copy().rotate( + -(angleToMouse - angleToP1) / 2 + ); + dirToNewP3.setMag(distToP3); + p3.set(Vector.add(p2, dirToNewP3)); + doodler2.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" }); + doodler2.line(p2, Vector.add(p2, dirToMouse.mult(100)), { + color: "red" + }); + } + { + const dirToMouse2 = Vector.sub(mousePos, p3).normalize(); + dirToMouse2.setMag(distToP4); + p4.set(Vector.add(p3, dirToMouse2)); + doodler2.line(p3, Vector.add(p3, dirToMouse2), { color: "green" }); + } + segment.clampLength(); + } + doodler2.fillText( + segment.calculateApproxLength().toFixed(2), + p2.copy().add(10, 0), + 100 + ); + } + track.draw(true); + } + start() { + const inputManager2 = getContextItem("inputManager"); + this.heldEvents.set("e", inputManager2.offKey("e")); + this.heldEvents.set("Escape", inputManager2.offKey("Escape")); + inputManager2.onKey("e", () => { + const state2 = getContextItem("state"); + state2.transitionTo(1 /* RUNNING */); + }); + const track = getContextItem("track"); + setContextItem("trackCopy", track.copy()); + inputManager2.onKey("Escape", () => { + const trackCopy = getContextItem("trackCopy"); + setContextItem("track", trackCopy); + setContextItem("trackCopy", void 0); + const state2 = getContextItem("state"); + state2.transitionTo(1 /* RUNNING */); + }); + inputManager2.onKey("w", () => { + const track2 = getContextItem("track"); + const segment = track2.lastSegment; + if (!segment) return; + const n = new StraightTrack(segment.points[3]); + const t = segment.tangent(1).heading(); + n.rotate(t); + segment.frontNeighbours.push(n); + track2.registerSegment(n); + this.currentSegment = n; + }); + inputManager2.onKey("1", () => { + this.currentSegment = track.firstSegment; + }); + this.currentSegment = track.lastSegment; + } + stop() { + const inputManager2 = getContextItem("inputManager"); + inputManager2.offKey("e"); + inputManager2.offKey("Escape"); + if (this.heldEvents.size > 0) { + for (const [key, cb] of this.heldEvents) { + if (cb) { + getContextItem("inputManager").onKey(key, cb); + } + this.heldEvents.delete(key); + } + } + setContextItem("trackCopy", void 0); + } + }; + + // state/states/PausedState.ts + var PausedState = class extends State { + name = 2 /* PAUSED */; + validTransitions = /* @__PURE__ */ new Set([ + 0 /* LOAD */, + 1 /* RUNNING */, + 3 /* EDIT_TRACK */, + 4 /* EDIT_TRAIN */ + ]); + update(dt) { + throw new Error("Method not implemented."); + } + start() { + throw new Error("Method not implemented."); + } + stop() { + throw new Error("Method not implemented."); + } + }; + + // state/states/RunningState.ts + var RunningState = class extends State { + name = 1 /* RUNNING */; + validTransitions = /* @__PURE__ */ new Set([ + 2 /* PAUSED */, + 3 /* EDIT_TRACK */ + ]); + update(dt) { + const ctx2 = getContext(); + ctx2.track.draw(); + for (const train of ctx2.trains) { + train.draw(); + } + } + start() { + } + stop() { + } + }; + + // inputs.ts + function bootstrapInputs() { + const inputManager2 = getContextItem("inputManager"); + inputManager2.onKey("e", () => { + const state2 = getContextItem("state"); + state2.transitionTo(3 /* EDIT_TRACK */); + }); + } + // state/states/LoadState.ts var LoadState = class extends State { name = 0 /* LOAD */; @@ -1641,6 +1789,7 @@ fillScreen: true, bg: "#302040" }); + doodler.scale = doodler.maxScale; setDefaultContext({ inputManager, doodler, diff --git a/lib/input.ts b/lib/input.ts index 7928fee..a4b5f72 100644 --- a/lib/input.ts +++ b/lib/input.ts @@ -1,3 +1,6 @@ +import { Vector, ZoomableDoodler } from "@bearmetal/doodler"; +import { getContextItem } from "./context.ts"; + export class InputManager { private keyStates: Map = new Map(); private mouseStates: Map = new Map(); @@ -39,8 +42,25 @@ export class InputManager { return this.mouseStates.get(key); } getMouseLocation() { + if (getContextItem("doodler") instanceof ZoomableDoodler) { + return getContextItem("doodler").screenToWorld( + this.mouseLocation.x, + this.mouseLocation.y, + ); + } return this.mouseLocation; } + getMouseLocationV() { + if (getContextItem("doodler") instanceof ZoomableDoodler) { + return new Vector( + getContextItem("doodler").screenToWorld( + this.mouseLocation.x, + this.mouseLocation.y, + ), + ); + } + return new Vector(this.mouseLocation); + } getMouseDelta() { return this.mouseDelta; } diff --git a/main.ts b/main.ts index 5a15a8b..12f62b3 100644 --- a/main.ts +++ b/main.ts @@ -19,6 +19,8 @@ const doodler = new ZoomableDoodler({ fillScreen: true, bg: "#302040", }); +// doodler.minScale = 0.1; +(doodler as any).scale = doodler.maxScale; setDefaultContext({ inputManager, diff --git a/math/path.ts b/math/path.ts index bff925d..e03c2aa 100644 --- a/math/path.ts +++ b/math/path.ts @@ -41,10 +41,12 @@ export class PathSegment { points: [Vector, Vector, Vector, Vector]; length: number; + startingLength: number; constructor(points: [Vector, Vector, Vector, Vector]) { this.points = points; this.length = this.calculateApproxLength(100); + this.startingLength = Math.round(this.length); } getPointAtT(t: number) { @@ -177,7 +179,11 @@ export class PathSegment { return this.length; } - calculateEvenlySpacedPoints(spacing: number, resolution = 1) { + calculateEvenlySpacedPoints( + spacing: number, + resolution = 1, + targetLength?: number, + ) { const points: Vector[] = []; points.push(this.points[0]); @@ -206,6 +212,46 @@ export class PathSegment { prev = point; } + if (targetLength && points.length < targetLength) { + while (points.length < targetLength) { + t += 1 / div; + const point = this.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; + } + } + return points; } + + calculateSubdividedPoints(numberOfPoints: number) { + const points: Vector[] = []; + + for (let i = 0; i < numberOfPoints; i++) { + const point = this.getPointAtT(i / numberOfPoints); + points.push(point); + } + + return points; + } + + clampLength() { + const curveLength = this.startingLength; + const points = this.calculateEvenlySpacedPoints(1, 1, curveLength + 1); + if (points.length >= curveLength) { + this.points[3].set(points[curveLength]); + } + } } diff --git a/state/states/EditTrackState.ts b/state/states/EditTrackState.ts index 9d7d2c5..4fa8de3 100644 --- a/state/states/EditTrackState.ts +++ b/state/states/EditTrackState.ts @@ -1,9 +1,11 @@ -import { Vector } from "@bearmetal/doodler"; +import { Doodler, Vector } from "@bearmetal/doodler"; import { getContextItem, setContextItem } from "../../lib/context.ts"; import { InputManager } from "../../lib/input.ts"; import { TrackSystem } from "../../track/system.ts"; import { State, StateMachine } from "../machine.ts"; import { States } from "./index.ts"; +import { StraightTrack } from "../../track/shapes.ts"; +import { TrackSegment } from "../../track/system.ts"; export class EditTrackState extends State { override name: States = States.EDIT_TRACK; @@ -15,19 +17,89 @@ export class EditTrackState extends State { private heldEvents: Map void) | undefined> = new Map(); + private currentSegment?: TrackSegment; + override update(dt: number): void { const inputManager = getContextItem("inputManager"); const track = getContextItem("track"); - const firstSegment = track.firstSegment; - if (firstSegment) { - const firstPoint = firstSegment.points[0].copy(); - const { x, y } = inputManager.getMouseLocation(); - firstSegment.points.forEach((p, i) => { - const relativePoint = Vector.sub(p, firstPoint); - p.set(x, y); - p.add(relativePoint); - }); + // For moving a segment, i.e. the currently active one + // const segment = track.lastSegment; + // if (segment) { + // const firstPoint = segment.points[0].copy(); + // const { x, y } = inputManager.getMouseLocation(); + // segment.points.forEach((p, i) => { + // const relativePoint = Vector.sub(p, firstPoint); + // p.set(x, y); + // p.add(relativePoint); + // }); + // } + + // manipulate only end of segment while maintaining length + // const segment = track.lastSegment; + // if (segment) { + // const p3 = segment.points[2]; + // const p4 = segment.points[3]; + // let curveLength = Math.round(segment.calculateApproxLength()); + // this.startingLength = this.startingLength ?? curveLength; + // curveLength = this.startingLength; + // const { x, y } = inputManager.getMouseLocation(); + // p4.set(x, y); + // const points = segment.calculateEvenlySpacedPoints(1); + // if (points.length > curveLength) p4.set(points[curveLength - 1]); + + // // doodler.fillText(curveLength.toFixed(2), p3.copy().add(10, 0), 100); + // } + const doodler = getContextItem("doodler"); + + // Adjust angles until tangent points to mouse + const segment = this.currentSegment; + if (segment) { + segment.propagate(); + + const mousePos = inputManager.getMouseLocationV(); + const p1 = segment.points[0]; + const p2 = segment.points[1]; + const p3 = segment.points[2]; + const p4 = segment.points[3]; + + const prevp3 = p3.copy(); + const dirToMouse = Vector.sub(mousePos, p2).normalize(); + const angleToMouse = dirToMouse.heading(); + const angleToP1 = Vector.sub(p2, p1).heading(); + const p2DistToMouse = Vector.dist(p2, mousePos); + const p3DistToMouse = Vector.dist(p3, mousePos); + const distToP3 = Vector.dist(p2, p3); + const distToP4 = Vector.dist(prevp3, p4); + if ( + Math.abs(angleToMouse - angleToP1) < .6 && + p2DistToMouse > distToP3 && + p3DistToMouse > distToP4 + ) { + { + const dirToNewP3 = dirToMouse.copy().rotate( + -(angleToMouse - angleToP1) / 2, + ); + dirToNewP3.setMag(distToP3); + p3.set(Vector.add(p2, dirToNewP3)); + doodler.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" }); + doodler.line(p2, Vector.add(p2, dirToMouse.mult(100)), { + color: "red", + }); + } + { + const dirToMouse = Vector.sub(mousePos, p3).normalize(); + dirToMouse.setMag(distToP4); + p4.set(Vector.add(p3, dirToMouse)); + doodler.line(p3, Vector.add(p3, dirToMouse), { color: "green" }); + } + segment.clampLength(); + } + doodler.fillText( + segment.calculateApproxLength().toFixed(2), + p2.copy().add(10, 0), + 100, + ); } track.draw(true); @@ -50,14 +122,36 @@ export class EditTrackState extends State { inputManager.onKey("Escape", () => { const trackCopy = getContextItem("trackCopy"); setContextItem("track", trackCopy); + setContextItem("trackCopy", undefined); const state = getContextItem>("state"); state.transitionTo(States.RUNNING); }); + + inputManager.onKey("w", () => { + const track = getContextItem("track"); + const segment = track.lastSegment; + if (!segment) return; + const n = new StraightTrack(segment.points[3]); + const t = segment.tangent(1).heading(); + n.rotate(t); + segment.frontNeighbours.push(n); + track.registerSegment(n); + this.currentSegment = n; + }); + + inputManager.onKey("1", () => { + this.currentSegment = track.firstSegment; + }); + + this.currentSegment = track.lastSegment; + // TODO // Cache trains and save - // Stash track in context } override stop(): void { + const inputManager = getContextItem("inputManager"); + inputManager.offKey("e"); + inputManager.offKey("Escape"); if (this.heldEvents.size > 0) { for (const [key, cb] of this.heldEvents) { if (cb) { @@ -66,5 +160,6 @@ export class EditTrackState extends State { this.heldEvents.delete(key); } } + setContextItem("trackCopy", undefined); } } diff --git a/track/system.ts b/track/system.ts index ee628c6..1d3611c 100644 --- a/track/system.ts +++ b/track/system.ts @@ -17,6 +17,15 @@ export class TrackSystem { 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); @@ -168,6 +177,54 @@ export class TrackSegment extends PathSegment { ); } + 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])),