Better track shapes, rotation in track editing

This commit is contained in:
Emmaline Autumn 2025-02-19 13:16:09 -07:00
parent 7b244526b9
commit 03e0b1afcb
11 changed files with 154 additions and 34 deletions

2
.gitignore vendored
View File

@ -26,3 +26,5 @@ dist-ssr
# Packed devtools
devtools.zip
temp.*

View File

@ -2,8 +2,12 @@ import { getContextItem } from "./lib/context.ts";
import { InputManager } from "./lib/input.ts";
import { StateMachine } from "./state/machine.ts";
import { States } from "./state/states/index.ts";
import { TrackSystem } from "./track/system.ts";
export function bootstrapInputs() {
addEventListener("keydown", (e) => {
e.preventDefault();
});
const inputManager = getContextItem<InputManager>("inputManager");
inputManager.onKey("e", () => {
const state = getContextItem<StateMachine<States>>("state");
@ -14,4 +18,22 @@ export function bootstrapInputs() {
localStorage.removeItem("track");
}
});
inputManager.onKey("c", () => {
if (inputManager.getKeyState("Control")) {
const currentTrack = localStorage.getItem("track");
navigator.clipboard.writeText(currentTrack ?? "[]");
}
});
addEventListener("paste", async (e) => {
let data = e.clipboardData?.getData("text/plain");
if (!data) return;
try {
// data = data.trim().replace(/^"|"$/g, "").replace(/\\"/g, '"');
console.log(data);
const track = TrackSystem.deserialize(JSON.parse(data));
localStorage.setItem("track", track.serialize());
} catch (e) {
console.error(e);
}
});
}

View File

@ -1,14 +1,33 @@
import { ZoomableDoodler } from "@bearmetal/doodler";
import { TrackSegment } from "../track/system.ts";
import { Train, TrainCar } from "../train/train.ts";
import { InputManager } from "./input.ts";
import { ResourceManager } from "./resources.ts";
interface ContextMap {
inputManager: InputManager;
doodler: ZoomableDoodler;
resources: ResourceManager;
debug: Debug;
colors: [
string,
string,
string,
string,
string,
string,
string,
string,
...string[],
];
[key: string]: unknown;
}
type ContextStore = Record<string, any>;
const defaultContext: ContextStore = {};
const contextStack: ContextStore[] = [defaultContext];
const debug = JSON.parse(localStorage.getItem("debug") || "false");
export function setDefaultContext(context: ContextStore) {
Object.assign(defaultContext, context);
}
@ -38,6 +57,10 @@ export const ctx = new Proxy(
export function getContext() {
return ctx;
}
export function getContextItem<K extends keyof ContextMap>(
prop: string,
): ContextMap[K];
export function getContextItem<T>(prop: string): T;
export function getContextItem<T>(prop: string): T {
return ctx[prop] as T;
}

View File

@ -27,7 +27,7 @@ setTimeout(() => {
.imageSmoothingEnabled = false;
}, 0);
// doodler.minScale = 0.1;
(doodler as any).scale = 3.14;
// (doodler as any).scale = 3.14;
const colors = [
"red",

View File

@ -14,6 +14,8 @@ import {
SBendLeft,
SBendRight,
StraightTrack,
WideBankLeft,
WideBankRight,
} from "../../track/shapes.ts";
import { TrackSegment } from "../../track/system.ts";
import { clamp } from "../../math/clamp.ts";
@ -61,6 +63,7 @@ export class EditTrackState extends State<States> {
p.set(mousePos);
p.add(relativePoint);
});
segment.recalculateTiePoints();
const ends = track.findEnds();
setContextItem("showEnds", true);
@ -109,18 +112,21 @@ export class EditTrackState extends State<States> {
this.ghostRotated = false;
}
switch (this.closestEnd.frontOrBack) {
case "front":
case "front": {
this.ghostSegment.setPositionByPoint(
this.closestEnd.pos,
this.ghostSegment.points[0],
);
// this.ghostSegment.points[0] = this.closestEnd.pos;
const ghostEndTangent = this.ghostSegment.tangent(0);
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
this.closestEnd.tangent.heading(),
this.closestEnd.tangent.heading() - ghostEndTangent.heading(),
this.ghostSegment.points[0],
);
this.ghostRotated = true;
break;
}
case "back": {
this.ghostSegment.setPositionByPoint(
this.closestEnd.pos,
@ -136,6 +142,8 @@ export class EditTrackState extends State<States> {
break;
}
}
this.ghostSegment.recalculateTiePoints();
// } else if (closestEnd) {
// this.closestEnd = closestEnd;
} else if (!this.closestEnd || !closestEnd) {
@ -286,6 +294,8 @@ export class EditTrackState extends State<States> {
new SBendRight(),
new BankLeft(),
new BankRight(),
new WideBankLeft(),
new WideBankRight(),
]);
const inputManager = getContextItem<InputManager>("inputManager");
@ -306,14 +316,6 @@ export class EditTrackState extends State<States> {
state.transitionTo(States.RUNNING);
});
inputManager.onKey(" ", () => {
if (this.selectedSegment) {
this.selectedSegment = undefined;
} else {
this.selectedSegment = new StraightTrack();
}
});
inputManager.onMouse("left", () => {
const track = getContextItem<TrackSystem>("track");
if (this.ghostSegment && this.closestEnd) {
@ -383,6 +385,18 @@ export class EditTrackState extends State<States> {
}
});
inputManager.onKey("r", () => {
if (!this.selectedSegment) return;
const segment = this.selectedSegment;
let angle = Math.PI / 12;
segment.rotate(angle);
});
inputManager.onKey("R", () => {
if (!this.selectedSegment) return;
const segment = this.selectedSegment;
let angle = -Math.PI / 12;
segment.rotate(angle);
});
// TODO
// Cache trains and save
@ -405,6 +419,8 @@ export class EditTrackState extends State<States> {
inputManager.offKey("e");
inputManager.offKey("w");
inputManager.offKey("z");
inputManager.offKey("r");
inputManager.offKey("R");
inputManager.offKey("Escape");
inputManager.offMouse("left");
if (this.heldEvents.size > 0) {

View File

@ -71,15 +71,18 @@ export class RunningState extends State<States> {
// const path = track.path;
// const follower = new DotFollower(path, path.points[0].copy());
// ctx.trains.push(follower);
// const train = new Train(track.path, [new LargeLady(), new Tender()]);
// ctx.trains.push(train);
});
const train = new Train(track.path, [
new LargeLady(),
new LargeLadyTender(),
]);
ctx.trains.push(train);
this.activeTrain = train;
});
// const train = new Train(track.path, [
// new LargeLady(),
// new LargeLadyTender(),
// ]);
// ctx.trains.push(train);
// this.activeTr0ain = train;
// const trainCount = 1000;
// for (let i = 0; i < trainCount; i++) {
// const train = new Train(track.path, [new LargeLady(), new Tender()]);

View File

@ -18,9 +18,9 @@ export class SBendLeft extends TrackSegment {
start = start || new Vector(100, 100);
super([
start,
start.copy().add(60, 0),
start.copy().add(90, -25),
start.copy().add(150, -25),
start.copy().add(80, 0),
start.copy().add(120, -25),
start.copy().add(200, -25),
]);
}
}
@ -29,9 +29,9 @@ export class SBendRight extends TrackSegment {
start = start || new Vector(100, 100);
super([
start,
start.copy().add(60, 0),
start.copy().add(90, 25),
start.copy().add(150, 25),
start.copy().add(80, 0),
start.copy().add(120, 25),
start.copy().add(200, 25),
]);
}
}
@ -62,6 +62,58 @@ export class BankLeft extends TrackSegment {
]);
}
}
export class WideBankLeft extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 70.4;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(-Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(-Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}
export class WideBankRight extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
const p1 = start.copy();
const p2 = start.copy();
const p3 = start.copy();
const p4 = start.copy();
const scale = 70.4;
p2.add(new Vector(1, 0).mult(scale));
p3.set(p2);
const dirToP3 = Vector.fromAngle(Math.PI / 12).mult(scale);
p3.add(dirToP3);
p4.set(p3);
const dirToP4 = Vector.fromAngle(Math.PI / 6).mult(scale);
p4.add(dirToP4);
super([
p1,
p2,
p3,
p4,
]);
}
}
export class BankRight extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);

View File

@ -287,9 +287,7 @@ export class TrackSegment extends PathSegment {
this.doodler = getContextItem<Doodler>("doodler");
this.id = id ?? crypto.randomUUID();
this.recalculateRailPoints();
const spacing = Math.ceil(this.length / 10);
this.evenPoints = this.calculateEvenlySpacedPoints(this.length / spacing);
this.recalculateTiePoints();
}
recalculateRailPoints(resolution = 60) {
@ -304,6 +302,10 @@ export class TrackSegment extends PathSegment {
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
}
}
recalculateTiePoints() {
const spacing = Math.ceil(this.length / 10);
this.evenPoints = this.calculateEvenlySpacedPoints(this.length / spacing);
}
setTrack(t: TrackSystem) {
this.track = t;
@ -431,6 +433,7 @@ export class TrackSegment extends PathSegment {
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);
@ -467,7 +470,7 @@ export class TrackSegment extends PathSegment {
}
rotateAboutPoint(angle: number, point: Vector) {
if (!this.points.includes(point)) return;
// if (!this.points.includes(point)) return;
point = point.copy();
this.points.forEach((p, i) => {
const relativePoint = Vector.sub(p, point);

View File

@ -103,7 +103,7 @@ export class LargeLady extends TrainCar {
const origin = b.pos.copy().add(new Vector(33, 0).rotate(difAngle));
const angle = b.angle;
const avgAngle = averageAngles(difAngle, angle) + Math.PI;
this.drawAngle = lerpAngle(this.drawAngle, avgAngle, .1);
this.drawAngle = lerpAngle(this.drawAngle, avgAngle, .2);
doodler.drawRotated(origin, this.drawAngle, () => {
this.sprite

View File

@ -23,7 +23,7 @@ export class Train extends Debuggable {
spacing = 0;
speed = 0;
speed = 5;
aabb!: TrainAABB;

View File

@ -1 +0,0 @@
[{"p":[[633.178123792903,100.31612523465073,0],[699.1781237929027,100.31612523465073,0],[762.9292283279814,117.39818221141712,0],[820.0869049777544,150.39818221141715,0]],"id":"83b9fc8c-778e-4e5e-a3db-1199863a2e13","bNeighbors":["a149432a-e04e-481a-ada2-c05a30ffb4c0"],"fNeighbors":["dc3ec17d-f28e-40fe-ba68-8a4ae6b2c117"]},{"p":[[820.0869049777544,150.39818221141715,0],[877.2445816275272,183.398182211417,0],[923.9136291858395,230.06722976972924,0],[956.9136291858396,287.2249064195022,0]],"id":"dc3ec17d-f28e-40fe-ba68-8a4ae6b2c117","bNeighbors":["83b9fc8c-778e-4e5e-a3db-1199863a2e13"],"fNeighbors":["1b6409da-5c9d-4b1d-b460-fcc5e40eaee4"]},{"p":[[956.9136291858396,287.2249064195022,0],[989.9136291858396,344.3825830692749,0],[1006.9956861626059,408.13368760435344,0],[1006.9956861626064,474.1336876043541,0]],"id":"1b6409da-5c9d-4b1d-b460-fcc5e40eaee4","bNeighbors":["dc3ec17d-f28e-40fe-ba68-8a4ae6b2c117"],"fNeighbors":["24861245-ca99-4f97-9e0a-5828773fcf7a"]},{"p":[[1006.9956861626064,474.1336876043541,0],[1006.9956861626068,540.1336876043545,0],[989.9136291858408,603.884792139433,0],[956.9136291858412,661.042468789207,0]],"id":"24861245-ca99-4f97-9e0a-5828773fcf7a","bNeighbors":["1b6409da-5c9d-4b1d-b460-fcc5e40eaee4"],"fNeighbors":["98ad61a1-f2e7-4c53-9caa-022faa15ce68"]},{"p":[[956.9136291858412,661.042468789207,0],[923.9136291858417,718.2001454389806,0],[877.2445816275301,764.8691929972932,0],[820.0869049777573,797.8691929972947,0]],"id":"98ad61a1-f2e7-4c53-9caa-022faa15ce68","bNeighbors":["24861245-ca99-4f97-9e0a-5828773fcf7a"],"fNeighbors":["7f11837f-f043-468f-b1cb-254236501199"]},{"p":[[820.0869049777573,797.8691929972947,0],[762.9292283279847,830.8691929972961,0],[699.1781237929068,847.9512499740634,0],[633.1781237929063,847.9512499740653,0]],"id":"7f11837f-f043-468f-b1cb-254236501199","bNeighbors":["98ad61a1-f2e7-4c53-9caa-022faa15ce68"],"fNeighbors":["d89abe0b-7d84-4921-b690-23cdb6ec7c4a"]},{"p":[[633.1781237929063,847.9512499740653,0],[567.1781237929059,847.9512499740671,0],[503.4270192578273,830.8691929973024,0],[446.26934260805274,797.869192997304,0]],"id":"d89abe0b-7d84-4921-b690-23cdb6ec7c4a","bNeighbors":["7f11837f-f043-468f-b1cb-254236501199"],"fNeighbors":["3fb143a5-3d6e-409c-88b1-c20e9005a14f"]},{"p":[[446.26934260805274,797.869192997304,0],[389.1116659582787,764.869192997306,0],[342.44261839996534,718.2001454389954,0],[309.4426183999627,661.0424687892232,0]],"id":"3fb143a5-3d6e-409c-88b1-c20e9005a14f","bNeighbors":["d89abe0b-7d84-4921-b690-23cdb6ec7c4a"],"fNeighbors":["de299c76-bdbd-4b59-9514-722e3292ed49"]},{"p":[[309.4426183999627,661.0424687892232,0],[276.4426183999604,603.8847921394515,0],[259.36056142319165,540.1336876043738,0],[259.36056142318864,474.13368760437345,0]],"id":"de299c76-bdbd-4b59-9514-722e3292ed49","bNeighbors":["3fb143a5-3d6e-409c-88b1-c20e9005a14f"],"fNeighbors":["e573e278-4039-425f-89f5-245f96fe9096"]},{"p":[[259.36056142318864,474.13368760437345,0],[259.36056142318563,408.13368760437334,0],[276.4426183999492,344.38258306929436,0],[309.44261839994635,287.22490641951936,0]],"id":"e573e278-4039-425f-89f5-245f96fe9096","bNeighbors":["de299c76-bdbd-4b59-9514-722e3292ed49"],"fNeighbors":["87a06884-75e3-4802-ba38-3be366bac467"]},{"p":[[309.44261839994635,287.22490641951936,0],[342.4426183999434,230.06722976974456,0],[389.111665958253,183.39818221143025,0],[446.26934260802403,150.39818221142656,0]],"id":"87a06884-75e3-4802-ba38-3be366bac467","bNeighbors":["e573e278-4039-425f-89f5-245f96fe9096"],"fNeighbors":["a149432a-e04e-481a-ada2-c05a30ffb4c0"]},{"p":[[446.26934260802403,150.39818221142656,0],[503.42701925779517,117.3981822114228,0],[567.1781237928724,100.31612523465262,0],[633.1781237928726,100.31612523464773,0]],"id":"a149432a-e04e-481a-ada2-c05a30ffb4c0","bNeighbors":["87a06884-75e3-4802-ba38-3be366bac467"],"fNeighbors":["83b9fc8c-778e-4e5e-a3db-1199863a2e13"]}]