import { Doodler, Vector } from "@bearmetal/doodler"; import { getContextItem, getContextItemOrDefault, 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 { BankLeft, BankRight, SBendLeft, SBendRight, StraightTrack, } from "../../track/shapes.ts"; import { TrackSegment } from "../../track/system.ts"; import { clamp } from "../../math/clamp.ts"; export class EditTrackState extends State { override name: States = States.EDIT_TRACK; override validTransitions: Set = new Set([ States.RUNNING, States.PAUSED, ]); private heldEvents: Map void) | undefined> = new Map(); private currentSegment?: TrackSegment; private selectedSegment?: TrackSegment; private ghostSegment?: TrackSegment; private ghostRotated = false; private closestEnd?: End; override update(dt: number): void { const inputManager = getContextItem("inputManager"); const track = getContextItem("track"); const doodler = getContextItem("doodler"); // 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); // }); // } if (this.selectedSegment) { const segment = this.selectedSegment; const firstPoint = segment.points[0].copy(); const mousePos = inputManager.getMouseLocationV(); segment.points.forEach((p, i) => { const relativePoint = Vector.sub(p, firstPoint); p.set(mousePos); p.add(relativePoint); }); const ends = track.findEnds(); setContextItem("showEnds", true); const nearbyEnds = ends.filter((end) => { const dist = Vector.dist(end.pos, mousePos); return dist < 20 && end.segment !== segment; }); let closestEnd = nearbyEnds[0]; for (const end of nearbyEnds) { if (end === closestEnd) continue; const closestEndTangent = Vector.add( closestEnd.tangent.copy().mult(20), closestEnd.pos, ); const endTangent = Vector.add( end.tangent.copy().rotate(Math.PI).mult(20), end.pos, ); doodler.drawCircle(closestEndTangent, 4, { color: "red", weight: 1 }); doodler.drawCircle(endTangent, 4, { color: "blue", weight: 1 }); if ( endTangent.dist(mousePos) < closestEndTangent.dist(mousePos) || end.pos.dist(mousePos) < closestEnd.pos.dist(mousePos) ) { closestEnd = end; } } if (closestEnd !== this.closestEnd) { this.closestEnd = closestEnd; this.ghostSegment = undefined; this.ghostRotated = false; } if (closestEnd) { // doodler.drawCircle(closestEnd.pos, 4, { color: "green", weight: 1 }); doodler.line( closestEnd.pos, Vector.add(closestEnd.pos, closestEnd.tangent.copy().mult(20)), { color: "green" }, ); } if ( this.closestEnd ) { if (!this.ghostSegment) { this.ghostSegment = segment.copy(); this.ghostRotated = false; } switch (this.closestEnd.frontOrBack) { case "front": this.ghostSegment.setPositionByPoint( this.closestEnd.pos, this.ghostSegment.points[0], ); // this.ghostSegment.points[0] = this.closestEnd.pos; !this.ghostRotated && this.ghostSegment.rotateAboutPoint( this.closestEnd.tangent.heading(), this.ghostSegment.points[0], ); this.ghostRotated = true; break; case "back": this.ghostSegment.setPositionByPoint( this.closestEnd.pos, this.ghostSegment.points[3], ); // this.ghostSegment.points[3] = this.closestEnd.pos; !this.ghostRotated && this.ghostSegment.rotateAboutPoint( this.closestEnd.tangent.heading(), this.ghostSegment.points[3], ); this.ghostRotated = true; break; } // } else if (closestEnd) { // this.closestEnd = closestEnd; } else if (!this.closestEnd || !closestEnd) { this.ghostSegment = undefined; this.ghostRotated = false; } this.selectedSegment?.draw(); if (this.ghostSegment) { doodler.drawWithAlpha(0.5, () => { if (!this.ghostSegment) return; this.ghostSegment.draw(); if (getContextItemOrDefault("debug", false)) { const colors = getContextItem("colors"); for ( const [i, point] of this.ghostSegment.points.entries() ?? [] ) { doodler.fillCircle(point, 4, { color: colors[i + 3] }); } } }); } } // 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); // } // 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 dirToP1 = Vector.sub(p2, p1).normalize(); // const angleToP1 = dirToP1.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); // const goodangle = clamp( // angleToMouse - angleToP1, // angleToP1 - .6, // angleToP1 + .6, // ); // if ( // // Math.abs(goodangle) < .6 && // p2DistToMouse > distToP3 && // p3DistToMouse > distToP4 // ) { // { // const dirToNewP3 = dirToP1.copy().rotate( // goodangle / 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, dirToNewP3), // { // color: "red", // }, // ); // } // { // const dirToMouse = Vector.sub(mousePos, p3).normalize(); // const dirToP3 = Vector.sub(p3, p2).normalize(); // const angleToP3 = dirToP3.heading(); // const goodangle = clamp( // dirToMouse.heading() - angleToP3, // angleToP3 - .6, // angleToP3 + .6, // ); // const dirToNewP4 = dirToP3.copy().rotate( // goodangle / 2, // ); // dirToNewP4.setMag(distToP4); // p4.set(Vector.add(p3, dirToNewP4)); // doodler.line(p3, Vector.add(p3, dirToNewP4), { color: "green" }); // } // segment.clampLength(); // } // // doodler.fillText( // // segment.calculateApproxLength().toFixed(2), // // p2.copy().add(10, 0), // // 100, // // ); // } const translation = new Vector(0, 0); if (inputManager.getKeyState("ArrowUp")) { translation.y -= 1; } if (inputManager.getKeyState("ArrowDown")) { translation.y += 1; } if (inputManager.getKeyState("ArrowLeft")) { translation.x -= 1; } if (inputManager.getKeyState("ArrowRight")) { translation.x += 1; } if (translation.x !== 0 || translation.y !== 0) { track.translate(translation); } track.draw(true); // TODO // Draw ui // Draw track points // Draw track tangents } override start(): void { setContextItem("trackSegments", [ undefined, new StraightTrack(), new SBendLeft(), new SBendRight(), new BankLeft(), new BankRight(), ]); const inputManager = getContextItem("inputManager"); this.heldEvents.set("e", inputManager.offKey("e")); this.heldEvents.set("Escape", inputManager.offKey("Escape")); inputManager.onKey("e", () => { const state = getContextItem>("state"); state.transitionTo(States.RUNNING); }); const track = getContextItem("track"); setContextItem("trackCopy", track.copy()); inputManager.onKey("Escape", () => { const trackCopy = getContextItem("trackCopy"); setContextItem("track", trackCopy); setContextItem("trackCopy", undefined); const state = getContextItem>("state"); state.transitionTo(States.RUNNING); }); inputManager.onKey(" ", () => { if (this.selectedSegment) { this.selectedSegment = undefined; } else { this.selectedSegment = new StraightTrack(); } }); inputManager.onMouse("left", () => { const track = getContextItem("track"); if (this.ghostSegment && this.closestEnd) { const segment = this.ghostSegment.cleanCopy(); switch (this.closestEnd.frontOrBack) { case "front": this.closestEnd.segment.frontNeighbours.push(segment); segment.backNeighbours.push(this.closestEnd.segment); break; case "back": this.closestEnd.segment.backNeighbours.push(segment); segment.frontNeighbours.push(this.closestEnd.segment); break; } track.registerSegment(segment); this.ghostSegment = undefined; this.closestEnd = undefined; } else if (this.selectedSegment) { track.registerSegment(this.selectedSegment); this.selectedSegment = new StraightTrack(); } else { this.selectedSegment = undefined; } }); // 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; // }); inputManager.onNumberKey((i) => { console.log(i); const segments = getContextItem("trackSegments"); this.selectedSegment = segments[i]; this.ghostRotated = false; this.ghostSegment = undefined; }); this.currentSegment = track.lastSegment; // TODO // Cache trains and save } override stop(): void { const inputManager = getContextItem("inputManager"); inputManager.offKey("e"); inputManager.offKey("w"); inputManager.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", undefined); setContextItem("trackSegments", undefined); } }