train following

This commit is contained in:
Emmaline Autumn 2025-02-17 20:58:43 -07:00
parent 20e6174658
commit 7b244526b9
8 changed files with 156 additions and 15 deletions

View File

@ -14,8 +14,9 @@
] ]
}, },
"imports": { "imports": {
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-c", "@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-e",
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.4",
"vite": "npm:vite@^6.0.1" "vite": "npm:vite@^6.0.1"
} },
"nodeModulesDir": "auto"
} }

8
deno.lock generated
View File

@ -1,7 +1,7 @@
{ {
"version": "4", "version": "4",
"specifiers": { "specifiers": {
"jsr:@bearmetal/doodler@0.0.5-c": "0.0.5-c", "jsr:@bearmetal/doodler@0.0.5-e": "0.0.5-e",
"jsr:@std/assert@*": "1.0.10", "jsr:@std/assert@*": "1.0.10",
"jsr:@std/assert@^1.0.10": "1.0.10", "jsr:@std/assert@^1.0.10": "1.0.10",
"jsr:@std/internal@^1.0.5": "1.0.5", "jsr:@std/internal@^1.0.5": "1.0.5",
@ -13,8 +13,8 @@
"npm:web-ext@*": "8.4.0" "npm:web-ext@*": "8.4.0"
}, },
"jsr": { "jsr": {
"@bearmetal/doodler@0.0.5-c": { "@bearmetal/doodler@0.0.5-e": {
"integrity": "34b0db85af1393b1b01622915963a8b33ee923c14b381afe9c771efd3d631cf1" "integrity": "70bd19397deac3b8a2ff6641b5df99bd1881581258c1c9ef3dab1170cf348430"
}, },
"@std/assert@1.0.10": { "@std/assert@1.0.10": {
"integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3", "integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
@ -2103,7 +2103,7 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@bearmetal/doodler@0.0.5-c", "jsr:@bearmetal/doodler@0.0.5-e",
"npm:@deno/vite-plugin@^1.0.4", "npm:@deno/vite-plugin@^1.0.4",
"npm:vite@^6.0.1" "npm:vite@^6.0.1"
] ]

View File

@ -47,6 +47,7 @@ const _fullDebug: Debug = {
car: false, car: false,
bogies: false, bogies: false,
angles: false, angles: false,
aabb: false,
}; };
const storedDebug = JSON.parse(localStorage.getItem("debug") || "0"); const storedDebug = JSON.parse(localStorage.getItem("debug") || "0");

View File

@ -404,6 +404,7 @@ export class EditTrackState extends State<States> {
const inputManager = getContextItem<InputManager>("inputManager"); const inputManager = getContextItem<InputManager>("inputManager");
inputManager.offKey("e"); inputManager.offKey("e");
inputManager.offKey("w"); inputManager.offKey("w");
inputManager.offKey("z");
inputManager.offKey("Escape"); inputManager.offKey("Escape");
inputManager.offMouse("left"); inputManager.offMouse("left");
if (this.heldEvents.size > 0) { if (this.heldEvents.size > 0) {

View File

@ -1,4 +1,4 @@
import { Doodler } from "@bearmetal/doodler"; import { Doodler, Point, Vector, ZoomableDoodler } from "@bearmetal/doodler";
import { getContext, getContextItem } from "../../lib/context.ts"; import { getContext, getContextItem } from "../../lib/context.ts";
import { InputManager } from "../../lib/input.ts"; import { InputManager } from "../../lib/input.ts";
import { TrackSystem } from "../../track/system.ts"; import { TrackSystem } from "../../track/system.ts";
@ -19,8 +19,20 @@ export class RunningState extends State<States> {
layers: number[] = []; layers: number[] = [];
activeTrain: Train | undefined;
override update(dt: number): void { override update(dt: number): void {
const ctx = getContext() as { trains: Train[]; track: TrackSystem }; const ctx = getContext() as { trains: Train[]; track: TrackSystem };
const doodler = getContextItem<ZoomableDoodler>(
"doodler",
);
if (this.activeTrain) {
// (doodler as any).origin = doodler.worldToScreen(
// doodler.width - this.activeTrain.aabb.center.x,
// doodler.height - this.activeTrain.aabb.center.y,
// );
doodler.centerCameraOn(this.activeTrain.aabb.center);
}
// const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem }; // const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem };
// TODO // TODO
// Update trains // Update trains
@ -67,6 +79,7 @@ export class RunningState extends State<States> {
new LargeLadyTender(), new LargeLadyTender(),
]); ]);
ctx.trains.push(train); ctx.trains.push(train);
this.activeTrain = train;
// const trainCount = 1000; // const trainCount = 1000;
// for (let i = 0; i < trainCount; i++) { // for (let i = 0; i < trainCount; i++) {
// const train = new Train(track.path, [new LargeLady(), new Tender()]); // const train = new Train(track.path, [new LargeLady(), new Tender()]);

View File

@ -3,7 +3,7 @@ import { Train, TrainCar } from "./train.ts";
import { getContextItem } from "../lib/context.ts"; import { getContextItem } from "../lib/context.ts";
import { ResourceManager } from "../lib/resources.ts"; import { ResourceManager } from "../lib/resources.ts";
import { debug } from "node:console"; import { debug } from "node:console";
import { averageAngles } from "../math/lerp.ts"; import { averageAngles, lerpAngle } from "../math/lerp.ts";
export class LargeLady extends TrainCar { export class LargeLady extends TrainCar {
scale = 1; scale = 1;
@ -68,8 +68,12 @@ export class LargeLady extends TrainCar {
}, },
}, },
]; ];
this.leading = 23;
} }
drawAngle?: number;
override draw(): void { override draw(): void {
const doodler = getContextItem<Doodler>("doodler"); const doodler = getContextItem<Doodler>("doodler");
for (const b of this.bogies) { for (const b of this.bogies) {
@ -95,11 +99,13 @@ export class LargeLady extends TrainCar {
if (debug.bogies) return; if (debug.bogies) return;
const difAngle = Vector.sub(a.pos, b.pos).heading(); const difAngle = Vector.sub(a.pos, b.pos).heading();
if (this.drawAngle == undefined) this.drawAngle = b.angle + Math.PI;
const origin = b.pos.copy().add(new Vector(33, 0).rotate(difAngle)); const origin = b.pos.copy().add(new Vector(33, 0).rotate(difAngle));
const angle = b.angle; const angle = b.angle;
const avgAngle = averageAngles(difAngle, angle) + Math.PI; const avgAngle = averageAngles(difAngle, angle) + Math.PI;
this.drawAngle = lerpAngle(this.drawAngle, avgAngle, .1);
doodler.drawRotated(origin, avgAngle, () => { doodler.drawRotated(origin, this.drawAngle, () => {
this.sprite this.sprite
? doodler.drawSprite( ? doodler.drawSprite(
this.img, this.img,
@ -121,7 +127,7 @@ export class LargeLady extends TrainCar {
// doodler.drawCircle(origin, 4, { color: "blue" }); // doodler.drawCircle(origin, 4, { color: "blue" });
doodler.deferDrawing(() => { doodler.deferDrawing(() => {
doodler.drawRotated(origin, avgAngle + Math.PI, () => { doodler.drawRotated(origin, this.drawAngle! + Math.PI, () => {
doodler.drawSprite( doodler.drawSprite(
this.img, this.img,
new Vector(133, 0), new Vector(133, 0),
@ -146,7 +152,7 @@ export class LargeLadyTender extends TrainCar {
height: 23, height: 23,
}); });
this.leading = 10; this.leading = 19;
} }
override draw(): void { override draw(): void {

View File

@ -4,6 +4,15 @@ import { Spline, TrackSegment, TrackSystem } from "../track/system.ts";
import { Debuggable } from "../lib/debuggable.ts"; import { Debuggable } from "../lib/debuggable.ts";
import { lerp, lerpAngle, map } from "../math/lerp.ts"; import { lerp, lerpAngle, map } from "../math/lerp.ts";
type TrainAABB = {
pos: Vector;
x: number;
y: number;
width: number;
height: number;
center: Vector;
};
export class Train extends Debuggable { export class Train extends Debuggable {
nodes: Vector[] = []; nodes: Vector[] = [];
@ -12,10 +21,12 @@ export class Train extends Debuggable {
path: Spline<TrackSegment>; path: Spline<TrackSegment>;
t: number; t: number;
spacing = 5; spacing = 0;
speed = 0; speed = 0;
aabb!: TrainAABB;
get segments() { get segments() {
return Array.from( return Array.from(
new Set(this.cars.flatMap((c) => c.segments.values().toArray())), new Set(this.cars.flatMap((c) => c.segments.values().toArray())),
@ -25,7 +36,7 @@ export class Train extends Debuggable {
constructor(track: Spline<TrackSegment>, cars: TrainCar[], t = 0) { constructor(track: Spline<TrackSegment>, cars: TrainCar[], t = 0) {
super("train", "path"); super("train", "path");
this.path = track; this.path = track;
this.path.pointSpacing = 5; this.path.pointSpacing = 4;
this.cars = cars; this.cars = cars;
this.t = this.cars.reduce((acc, c) => acc + c.length, 0) + this.t = this.cars.reduce((acc, c) => acc + c.length, 0) +
(this.cars.length - 1) * this.spacing; (this.cars.length - 1) * this.spacing;
@ -51,6 +62,8 @@ export class Train extends Debuggable {
// } // }
// } // }
// } // }
this.updateAABB();
} }
move(dTime: number) { move(dTime: number) {
@ -72,10 +85,27 @@ export class Train extends Debuggable {
// car.draw(); // car.draw();
currentOffset += car.moveAlongPath(this.t - currentOffset) + currentOffset += car.moveAlongPath(this.t - currentOffset) +
this.spacing / this.path.pointSpacing + (this.spacing / this.path.pointSpacing);
car.leading / this.path.pointSpacing;
} }
// this.draw(); // this.draw();
this.updateAABB();
}
updateAABB() {
const minX = Math.min(...this.cars.map((c) => c.aabb.x));
const maxX = Math.max(...this.cars.map((c) => c.aabb.x + c.aabb.width));
const minY = Math.min(...this.cars.map((c) => c.aabb.y));
const maxY = Math.max(...this.cars.map((c) => c.aabb.y + c.aabb.height));
this.aabb = {
pos: new Vector(minX, minY),
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
center: new Vector(minX, minY).add(
new Vector(maxX - minX, maxY - minY).div(2),
),
};
} }
// draw() { // draw() {
@ -133,6 +163,16 @@ export class Train extends Debuggable {
}); });
} }
} }
if (debug.aabb) {
doodler.deferDrawing(() => {
doodler.drawRect(this.aabb.pos, this.aabb.width, this.aabb.height, {
color: "orange",
});
doodler.drawCircle(this.aabb.center, 2, {
color: "lime",
});
});
}
} }
real2Track(length: number) { real2Track(length: number) {
@ -164,6 +204,8 @@ export class TrainCar extends Debuggable {
train?: Train; train?: Train;
aabb!: TrainAABB;
constructor( constructor(
length: number, length: number,
trailing: number, trailing: number,
@ -191,6 +233,8 @@ export class TrainCar extends Debuggable {
length: trailing, length: trailing,
}, },
]; ];
this.updateAABB();
} }
get length() { get length() {
@ -209,6 +253,59 @@ export class TrainCar extends Debuggable {
} }
} }
updateAABB() {
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
this.bogies.forEach((bogie, index) => {
// Unit vector in the direction the bogie is facing.
const u = new Vector(Math.cos(bogie.angle), Math.sin(bogie.angle));
// Perpendicular vector (to thicken the rectangle).
const v = new Vector(-Math.sin(bogie.angle), Math.cos(bogie.angle));
// For the first bogie, extend in the opposite direction by this.leading.
let front = bogie.pos.copy();
if (index === 0) {
front = front.sub(u.copy().rotate(Math.PI).mult(this.leading));
}
// Rear point is at bogie.pos plus the bogie length.
const rear = bogie.pos.copy().add(
u.copy().rotate(Math.PI).mult(bogie.length),
);
// Calculate half the height to offset from the center line.
const halfHeight = this.imgHeight / 2;
// Calculate the four corners of the rectangle.
const corners = [
front.copy().add(v.copy().mult(halfHeight)),
front.copy().add(v.copy().mult(-halfHeight)),
rear.copy().add(v.copy().mult(halfHeight)),
rear.copy().add(v.copy().mult(-halfHeight)),
];
// Update the overall AABB limits.
corners.forEach((corner) => {
minX = Math.min(minX, corner.x);
minY = Math.min(minY, corner.y);
maxX = Math.max(maxX, corner.x);
maxY = Math.max(maxY, corner.y);
});
});
this.aabb = {
pos: new Vector(minX, minY),
center: new Vector(minX, minY).add(
new Vector(maxX - minX, maxY - minY).div(2),
),
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
moveAlongPath(t: number, initial = false): number { moveAlongPath(t: number, initial = false): number {
if (!this.train) return 0; if (!this.train) return 0;
let offset = this.leading / this.train.path.pointSpacing; let offset = this.leading / this.train.path.pointSpacing;
@ -224,6 +321,7 @@ export class TrainCar extends Debuggable {
} }
this.segments.add(a.segmentId); this.segments.add(a.segmentId);
} }
this.updateAABB();
return offset; return offset;
} }
@ -294,6 +392,26 @@ export class TrainCar extends Debuggable {
} }
}); });
}); });
if (debug.aabb) {
doodler.deferDrawing(() => {
doodler.drawRect(this.aabb.pos, this.aabb.width, this.aabb.height, {
color: "white",
weight: .5,
});
doodler.drawCircle(this.aabb.center, 2, {
color: "yellow",
});
doodler.fillText(
this.aabb.width.toFixed(1).toString(),
this.aabb.center.copy().add(10, 10),
100,
{
color: "white",
},
);
});
}
} }
if (debug.angles) { if (debug.angles) {

View File

@ -23,5 +23,6 @@ declare global {
path: boolean; path: boolean;
bogies: boolean; bogies: boolean;
angles: boolean; angles: boolean;
aabb: boolean;
}; };
} }