Separates game loop from doodler draw loop. Each state is in charge of registering and unregistering layers

This commit is contained in:
Emmaline Autumn 2025-02-13 06:07:42 -07:00
parent 43a5268ed5
commit 3befb69f51
10 changed files with 421 additions and 130 deletions

63
GameLoop.ts Normal file
View File

@ -0,0 +1,63 @@
import { Doodler } from "@bearmetal/doodler";
import { StateMachine } from "./state/machine.ts";
import { getContextItem } from "./lib/context.ts";
export class GameLoop<T> {
lastTime: number;
running: boolean;
targetFps: number;
constructor(targetFps: number = 60) {
this.lastTime = performance.now();
this.running = false;
this.targetFps = targetFps;
}
async start(state: StateMachine<T>) {
if (this.running) return;
this.running = true;
this.lastTime = performance.now();
while (this.running) {
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
this.lastTime = currentTime;
try {
// Wait for state update to complete before continuing
await state.update(deltaTime);
} catch (error) {
console.error("Error in game loop:", error);
this.stop();
break;
}
// Use setTimeout to prevent immediate loop continuation
// and allow other tasks to run
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
stop() {
this.running = false;
}
}
// // Usage example:
// const gameState = {
// update: async (deltaTime) => {
// console.log(`Updating with delta time: ${deltaTime.toFixed(3)}s`);
// // Simulate some async work
// await new Promise(resolve => setTimeout(resolve, 16)); // ~60fps
// }
// };
// // Create and start the loop
// const loop = new GameLoop();
// loop.start(gameState);
// // Stop the loop after 5 seconds (example)
// setTimeout(() => {
// loop.stop();
// console.log('Loop stopped');
// }, 5000);

234
bundle.js
View File

@ -78,12 +78,12 @@
}, 2);
}
// https://jsr.io/@bearmetal/doodler/0.0.5-a/geometry/constants.ts
// https://jsr.io/@bearmetal/doodler/0.0.5-b/geometry/constants.ts
var Constants = {
TWO_PI: Math.PI * 2
};
// https://jsr.io/@bearmetal/doodler/0.0.5-a/geometry/vector.ts
// https://jsr.io/@bearmetal/doodler/0.0.5-b/geometry/vector.ts
var Vector = class _Vector {
x;
y;
@ -368,7 +368,7 @@
}
};
// https://jsr.io/@bearmetal/doodler/0.0.5-a/FPSCounter.ts
// https://jsr.io/@bearmetal/doodler/0.0.5-b/FPSCounter.ts
var FPSCounter = class {
frameTimes = [];
maxSamples;
@ -396,7 +396,7 @@
}
};
// https://jsr.io/@bearmetal/doodler/0.0.5-a/canvas.ts
// https://jsr.io/@bearmetal/doodler/0.0.5-b/canvas.ts
var Doodler = class {
ctx;
_canvas;
@ -488,9 +488,14 @@
// Layer management
createLayer(layer) {
this.layers.push(layer);
return this.layers.length - 1;
}
deleteLayer(layer) {
this.layers = this.layers.filter((l) => l !== layer);
if (typeof layer === "number") {
this.layers = this.layers.filter((_, i) => i !== layer);
} else {
this.layers = this.layers.filter((l) => l !== layer);
}
}
moveLayer(layer, index) {
let temp = this.layers.filter((l) => l !== layer);
@ -759,13 +764,13 @@
}
};
// https://jsr.io/@bearmetal/doodler/0.0.5-a/timing/EaseInOut.ts
// https://jsr.io/@bearmetal/doodler/0.0.5-b/timing/EaseInOut.ts
var easeInOut = (x) => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
// https://jsr.io/@bearmetal/doodler/0.0.5-a/timing/Map.ts
// https://jsr.io/@bearmetal/doodler/0.0.5-b/timing/Map.ts
var map = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
// https://jsr.io/@bearmetal/doodler/0.0.5-a/zoomableCanvas.ts
// https://jsr.io/@bearmetal/doodler/0.0.5-b/zoomableCanvas.ts
var ZoomableDoodler = class extends Doodler {
scale = 1;
dragging = false;
@ -1167,6 +1172,12 @@
update(dt, ctx2) {
this.currentState?.update(dt, ctx2);
}
optimizePerformance(percent) {
const ctx2 = getContext();
if (percent < 0.5) {
ctx2.track.optimize(percent);
}
}
get current() {
return this.currentState;
}
@ -1420,6 +1431,12 @@
get lastSegment() {
return this.segments.values().toArray().pop();
}
optimize(percent) {
console.log("Optimizing track", percent * 100 / 4);
for (const segment of this.segments.values()) {
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
}
}
registerSegment(segment) {
segment.setTrack(this);
this.segments.set(segment.id, segment);
@ -1508,9 +1525,14 @@
}
generatePath() {
if (!this.firstSegment) throw new Error("No first segment");
const flags = { looping: true };
const rightOnlyPath = [
this.firstSegment.copy(),
...this.findRightPath(this.firstSegment, /* @__PURE__ */ new Set([this.firstSegment.id]))
...this.findRightPath(
this.firstSegment,
/* @__PURE__ */ new Set([this.firstSegment.id]),
flags
)
];
rightOnlyPath.forEach((s, i, arr) => {
if (i === 0) return;
@ -1519,9 +1541,17 @@
s.prev = prev;
prev.next = s;
});
if (flags.looping) {
const first = rightOnlyPath[0];
const last = rightOnlyPath[rightOnlyPath.length - 1];
first.points[0] = last.points[3];
last.points[3] = first.points[0];
first.prev = last;
last.next = first;
}
return new Spline(rightOnlyPath);
}
*findRightPath(start, seen) {
*findRightPath(start, seen, flags) {
if (start.frontNeighbours.length === 0) {
return;
}
@ -1542,12 +1572,17 @@
rightMost = segment;
}
}
if (seen.has(rightMost.id)) return;
if (seen.has(rightMost.id)) {
if (seen.values().next().value === rightMost.id) {
flags.looping = true;
}
return;
}
seen.add(rightMost.id);
yield rightMost.copy();
yield* this.findRightPath(rightMost, seen);
yield* this.findRightPath(rightMost, seen, flags);
}
*findLeftPath(start, seen) {
*findLeftPath(start, seen, flags) {
if (start.frontNeighbours.length === 0) {
return;
}
@ -1568,10 +1603,15 @@
leftMost = segment;
}
}
if (seen.has(leftMost.id)) return;
if (seen.has(leftMost.id)) {
if (seen.values().next().value === leftMost.id) {
flags.looping = true;
}
return;
}
seen.add(leftMost.id);
yield leftMost.copy();
yield* this.findLeftPath(leftMost, seen);
yield* this.findLeftPath(leftMost, seen, flags);
}
};
var TrackSegment = class _TrackSegment extends PathSegment {
@ -1579,10 +1619,25 @@
backNeighbours = [];
track;
doodler;
normalPoints = [];
antiNormalPoints = [];
constructor(p, id) {
super(p);
this.doodler = getContextItem("doodler");
this.id = id ?? crypto.randomUUID();
this.recalculateRailPoints();
}
recalculateRailPoints(resolution = 100) {
this.normalPoints = [];
this.antiNormalPoints = [];
for (let i = 0; i <= resolution; i++) {
const t = i / resolution;
const normal = this.tangent(t).rotate(Math.PI / 2);
normal.setMag(6);
const p = this.getPointAtT(t);
this.normalPoints.push(p.copy().add(normal));
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
}
}
setTrack(t) {
this.track = t;
@ -1619,21 +1674,16 @@
});
});
}
const lineResolution = 100;
const normalPoints = [];
const antiNormalPoints = [];
for (let i = 0; i <= lineResolution; i++) {
const t = i / lineResolution;
const normal = this.tangent(t).rotate(Math.PI / 2);
normal.setMag(6);
const p = this.getPointAtT(t);
normalPoints.push(p.copy().add(normal));
antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
}
this.doodler.deferDrawing(
() => {
this.doodler.drawLine(normalPoints, { color: "grey" });
this.doodler.drawLine(antiNormalPoints, { color: "grey" });
this.doodler.drawLine(this.normalPoints, {
color: "grey",
weight: 1.5
});
this.doodler.drawLine(this.antiNormalPoints, {
color: "grey",
weight: 1.5
});
}
);
}
@ -1737,21 +1787,12 @@
looped = false;
constructor(segs) {
this.segments = segs;
if (this.segments.at(-1)?.next === this.segments[0]) {
this.looped = true;
}
this.pointSpacing = 1;
this.evenPoints = this.calculateEvenlySpacedPoints(1);
this.nodes = [];
for (let i = 0; i < this.points.length; i += 3) {
const node = {
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;
@ -1761,7 +1802,10 @@
// }
draw() {
for (const segment of this.segments) {
segment.draw();
const doodler2 = getContextItem("doodler");
doodler2.drawWithAlpha(0.5, () => {
doodler2.drawBezier(...segment.points, { color: "red" });
});
}
}
calculateEvenlySpacedPoints(spacing, resolution = 1) {
@ -1934,6 +1978,7 @@
ghostSegment;
ghostRotated = false;
closestEnd;
layers = [];
update(dt) {
const inputManager2 = getContextItem("inputManager");
const track = getContextItem("track");
@ -2015,19 +2060,6 @@
this.ghostSegment = void 0;
this.ghostRotated = false;
}
this.selectedSegment?.draw();
if (this.ghostSegment) {
doodler2.drawWithAlpha(0.5, () => {
if (!this.ghostSegment) return;
this.ghostSegment.draw();
if (getContextItemOrDefault("debug", false)) {
const colors2 = getContextItem("colors");
for (const [i, point] of this.ghostSegment.points.entries() ?? []) {
doodler2.fillCircle(point, 4, { color: colors2[i + 3] });
}
}
});
}
}
const translation = new Vector(0, 0);
if (inputManager2.getKeyState("ArrowUp")) {
@ -2045,9 +2077,27 @@
if (translation.x !== 0 || translation.y !== 0) {
track.translate(translation);
}
track.draw(true);
}
start() {
const doodler2 = getContextItem("doodler");
this.layers.push(
doodler2.createLayer(() => {
this.selectedSegment?.draw();
if (this.ghostSegment) {
doodler2.drawWithAlpha(0.5, () => {
if (!this.ghostSegment) return;
this.ghostSegment.draw();
if (getContextItemOrDefault("debug", false)) {
const colors2 = getContextItem("colors");
for (const [i, point] of this.ghostSegment.points.entries() ?? []) {
doodler2.fillCircle(point, 4, { color: colors2[i + 3] });
}
}
});
}
track.draw(true);
})
);
setContextItem("trackSegments", [
void 0,
new StraightTrack(),
@ -2129,6 +2179,9 @@
}
redoBuffer = [];
stop() {
for (const layer of this.layers) {
getContextItem("doodler").deleteLayer(layer);
}
const inputManager2 = getContextItem("inputManager");
inputManager2.offKey("e");
inputManager2.offKey("w");
@ -2577,16 +2630,27 @@
2 /* PAUSED */,
3 /* EDIT_TRACK */
]);
layers = [];
update(dt) {
const ctx2 = getContext();
const input = getContextItem("inputManager");
ctx2.track.draw();
for (const train of ctx2.trains) {
train.move(dt);
train.draw();
}
}
start() {
const doodler2 = getContextItem("doodler");
this.layers.push(
doodler2.createLayer(() => {
const track2 = getContextItem("track");
track2.draw();
}),
doodler2.createLayer(() => {
const trains = getContextItem("trains");
for (const train of trains) {
train.draw();
}
})
);
const inputManager2 = getContextItem("inputManager");
const track = getContextItem("track");
const ctx2 = getContext();
@ -2608,6 +2672,9 @@
});
}
stop() {
for (const layer of this.layers) {
getContextItem("doodler").deleteLayer(layer);
}
}
};
@ -2631,6 +2698,7 @@
validTransitions = /* @__PURE__ */ new Set([
1 /* RUNNING */
]);
layers = [];
update() {
}
start() {
@ -2648,6 +2716,13 @@
resources2.ready().then(() => {
this.stateMachine.transitionTo(1 /* RUNNING */);
});
const doodler2 = getContextItem("doodler");
this.layers.push(doodler2.createLayer((_, __, dTime) => {
doodler2.clearRect(new Vector(0, 0), doodler2.width, doodler2.height);
doodler2.fillRect(new Vector(0, 0), doodler2.width, doodler2.height, {
color: "#302040"
});
}));
}
stop() {
}
@ -2675,6 +2750,39 @@
return stateMachine;
}
// GameLoop.ts
var GameLoop = class {
lastTime;
running;
targetFps;
constructor(targetFps = 60) {
this.lastTime = performance.now();
this.running = false;
this.targetFps = targetFps;
}
async start(state2) {
if (this.running) return;
this.running = true;
this.lastTime = performance.now();
while (this.running) {
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1e3;
this.lastTime = currentTime;
try {
await state2.update(deltaTime);
} catch (error) {
console.error("Error in game loop:", error);
this.stop();
break;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
stop() {
this.running = false;
}
};
// main.ts
var inputManager = new InputManager();
var resources = new ResourceManager();
@ -2704,9 +2812,6 @@
var state = bootstrapGameStateMachine();
setContextItem("state", state);
doodler.init();
doodler.createLayer((_, __, dTime) => {
state.update(dTime);
});
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
@ -2718,12 +2823,19 @@
setInterval(() => {
const doodler2 = getContextItem("doodler");
const frameRate = doodler2.fps;
if (frameRate < 0.5) return;
let fpsEl = document.getElementById("fps");
if (!fpsEl) {
fpsEl = document.createElement("div");
fpsEl.id = "fps";
document.body.appendChild(fpsEl);
}
const fPerc = frameRate / 60;
if (fPerc < 0.6) {
state.optimizePerformance(fPerc);
}
fpsEl.textContent = frameRate.toFixed(1) + " fps";
}, 1e3);
var gameLoop = new GameLoop();
gameLoop.start(state);
})();

View File

@ -13,6 +13,6 @@
"dev": "deno run -RWEN --allow-run dev.ts dev"
},
"imports": {
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-a"
"@bearmetal/doodler": "jsr:@bearmetal/doodler@0.0.5-b"
}
}

8
deno.lock generated
View File

@ -1,7 +1,7 @@
{
"version": "4",
"specifiers": {
"jsr:@bearmetal/doodler@0.0.5-a": "0.0.5-a",
"jsr:@bearmetal/doodler@0.0.5-b": "0.0.5-b",
"jsr:@luca/esbuild-deno-loader@*": "0.11.0",
"jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1",
"jsr:@std/assert@*": "1.0.10",
@ -25,8 +25,8 @@
"@bearmetal/doodler@0.0.4": {
"integrity": "b631083cff84994c513f70d1f09e6a9256edabcb224112c93a9ca6a87c88a389"
},
"@bearmetal/doodler@0.0.5-a": {
"integrity": "c59d63f071623ad4c7588e24b464874786759e56a6b12782689251a5cf3a1c08"
"@bearmetal/doodler@0.0.5-b": {
"integrity": "94f265ea21162f943291526800de7f3f6560634a4fe762a38cd73892685b6742"
},
"@luca/esbuild-deno-loader@0.11.0": {
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
@ -232,7 +232,7 @@
},
"workspace": {
"dependencies": [
"jsr:@bearmetal/doodler@0.0.5-a"
"jsr:@bearmetal/doodler@0.0.5-b"
]
}
}

17
main.ts
View File

@ -11,8 +11,9 @@ import { ResourceManager } from "./lib/resources.ts";
import { addButton } from "./ui/button.ts";
import { TrackSystem } from "./track/system.ts";
import { StraightTrack } from "./track/shapes.ts";
import { StateMachine } from "./state/machine.ts";
import { State, StateMachine } from "./state/machine.ts";
import { bootstrapGameStateMachine } from "./state/states/index.ts";
import { GameLoop } from "./GameLoop.ts";
const inputManager = new InputManager();
const resources = new ResourceManager();
@ -50,9 +51,9 @@ setContextItem("state", state);
doodler.init();
doodler.createLayer((_, __, dTime) => {
state.update(dTime);
});
// doodler.createLayer((_, __, dTime) => {
// state.update(dTime);
// });
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
@ -66,11 +67,19 @@ document.addEventListener("keydown", (e) => {
setInterval(() => {
const doodler = getContextItem<Doodler>("doodler");
const frameRate = doodler.fps;
if (frameRate < 0.5) return;
let fpsEl = document.getElementById("fps");
if (!fpsEl) {
fpsEl = document.createElement("div");
fpsEl.id = "fps";
document.body.appendChild(fpsEl);
}
const fPerc = frameRate / 60;
if (fPerc < 0.6) {
state.optimizePerformance(fPerc);
}
fpsEl.textContent = frameRate.toFixed(1) + " fps";
}, 1000);
const gameLoop = new GameLoop();
gameLoop.start(state);

View File

@ -1,3 +1,7 @@
import { getContext } from "../lib/context.ts";
import { TrackSystem } from "../track/system.ts";
import { Train } from "../train.old.ts";
export class StateMachine<T> {
private _states: Map<T, State<T>> = new Map();
private currentState?: State<T>;
@ -6,6 +10,13 @@ export class StateMachine<T> {
this.currentState?.update(dt, ctx);
}
optimizePerformance(percent: number) {
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
if (percent < 0.5) {
ctx.track.optimize(percent);
}
}
get current() {
return this.currentState;
}

View File

@ -33,6 +33,8 @@ export class EditTrackState extends State<States> {
private ghostRotated = false;
private closestEnd?: End;
layers: number[] = [];
override update(dt: number): void {
const inputManager = getContextItem<InputManager>("inputManager");
const track = getContextItem<TrackSystem>("track");
@ -138,22 +140,6 @@ export class EditTrackState extends State<States> {
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<string[]>("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
@ -263,13 +249,34 @@ export class EditTrackState extends State<States> {
track.translate(translation);
}
track.draw(true);
// TODO
// Draw ui
// Draw track points
// Draw track tangents
}
override start(): void {
const doodler = getContextItem<Doodler>("doodler");
this.layers.push(
doodler.createLayer(() => {
this.selectedSegment?.draw();
if (this.ghostSegment) {
doodler.drawWithAlpha(0.5, () => {
if (!this.ghostSegment) return;
this.ghostSegment.draw();
if (getContextItemOrDefault("debug", false)) {
const colors = getContextItem<string[]>("colors");
for (
const [i, point] of this.ghostSegment.points.entries() ?? []
) {
doodler.fillCircle(point, 4, { color: colors[i + 3] });
}
}
});
}
track.draw(true);
}),
);
setContextItem("trackSegments", [
undefined,
new StraightTrack(),
@ -376,9 +383,19 @@ export class EditTrackState extends State<States> {
// TODO
// Cache trains and save
// const trackCount = 2000;
// for (let i = 0; i < trackCount; i++) {
// const seg = new StraightTrack();
// track.registerSegment(seg);
// }
}
redoBuffer: TrackSegment[] = [];
override stop(): void {
for (const layer of this.layers) {
getContextItem<Doodler>("doodler").deleteLayer(layer);
}
const inputManager = getContextItem<InputManager>("inputManager");
inputManager.offKey("e");
inputManager.offKey("w");

View File

@ -1,5 +1,6 @@
import { Doodler, Vector } from "@bearmetal/doodler";
import { bootstrapInputs } from "../../inputs.ts";
import { setContextItem } from "../../lib/context.ts";
import { getContextItem, setContextItem } from "../../lib/context.ts";
import { InputManager } from "../../lib/input.ts";
import { ResourceManager } from "../../lib/resources.ts";
import { StraightTrack } from "../../track/shapes.ts";
@ -13,6 +14,8 @@ export class LoadState extends State<States> {
States.RUNNING,
]);
layers: number[] = [];
override update(): void {
// noop
}
@ -37,6 +40,14 @@ export class LoadState extends State<States> {
resources.ready().then(() => {
this.stateMachine.transitionTo(States.RUNNING);
});
const doodler = getContextItem<Doodler>("doodler");
this.layers.push(doodler.createLayer((_, __, dTime) => {
doodler.clearRect(new Vector(0, 0), doodler.width, doodler.height);
doodler.fillRect(new Vector(0, 0), doodler.width, doodler.height, {
color: "#302040",
});
}));
}
override stop(): void {
// noop

View File

@ -1,3 +1,4 @@
import { Doodler } from "@bearmetal/doodler";
import { getContext, getContextItem } from "../../lib/context.ts";
import { InputManager } from "../../lib/input.ts";
import { TrackSystem } from "../../track/system.ts";
@ -14,26 +15,40 @@ export class RunningState extends State<States> {
States.PAUSED,
States.EDIT_TRACK,
]);
layers: number[] = [];
override update(dt: number): void {
const ctx = getContext() as { trains: Train[]; track: TrackSystem };
// const ctx = getContext() as { trains: DotFollower[]; track: TrackSystem };
const input = getContextItem<InputManager>("inputManager");
// TODO
// Update trains
// Update world
// Handle input
// Draw (maybe via a layer system that syncs with doodler)
ctx.track.draw();
for (const train of ctx.trains) {
// if (input.getKeyState("ArrowUp")) {
// train.acceleration.x += 10;
// }
train.move(dt);
train.draw();
}
// Monitor world events
for (const train of ctx.trains) {
train.move(dt);
}
}
override start(): void {
const doodler = getContextItem<Doodler>("doodler");
this.layers.push(
doodler.createLayer(() => {
const track = getContextItem<TrackSystem>("track");
track.draw();
}),
doodler.createLayer(() => {
const trains = getContextItem<Train[]>("trains");
for (const train of trains) {
// if (input.getKeyState("ArrowUp")) {
// train.acceleration.x += 10;
// }
train.draw();
}
}),
);
// noop
const inputManager = getContextItem<InputManager>("inputManager");
const track = getContextItem<TrackSystem>("track");
@ -46,7 +61,7 @@ export class RunningState extends State<States> {
const train = new Train(track.path, [new RedEngine(), new Tender()]);
ctx.trains.push(train);
});
// const trainCount = 1000;
// const trainCount = 2000;
// for (let i = 0; i < trainCount; i++) {
// const train = new Train(track.path, [new RedEngine(), new Tender()]);
// ctx.trains.push(train);
@ -66,6 +81,8 @@ export class RunningState extends State<States> {
});
}
override stop(): void {
// noop
for (const layer of this.layers) {
getContextItem<Doodler>("doodler").deleteLayer(layer);
}
}
}

View File

@ -22,6 +22,13 @@ export class TrackSystem {
return this.segments.values().toArray().pop();
}
optimize(percent: number) {
console.log("Optimizing track", percent * 100 / 4);
for (const segment of this.segments.values()) {
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
}
}
registerSegment(segment: TrackSegment) {
segment.setTrack(this);
this.segments.set(segment.id, segment);
@ -147,9 +154,14 @@ export class TrackSystem {
generatePath() {
if (!this.firstSegment) throw new Error("No first segment");
const flags = { looping: true };
const rightOnlyPath = [
this.firstSegment.copy(),
...this.findRightPath(this.firstSegment, new Set([this.firstSegment.id])),
...this.findRightPath(
this.firstSegment,
new Set([this.firstSegment.id]),
flags,
),
];
rightOnlyPath.forEach((s, i, arr) => {
@ -159,6 +171,14 @@ export class TrackSystem {
s.prev = prev;
prev.next = s;
});
if (flags.looping) {
const first = rightOnlyPath[0];
const last = rightOnlyPath[rightOnlyPath.length - 1];
first.points[0] = last.points[3];
last.points[3] = first.points[0];
first.prev = last;
last.next = first;
}
return new Spline<TrackSegment>(rightOnlyPath);
}
@ -166,6 +186,7 @@ export class TrackSystem {
*findRightPath(
start: TrackSegment,
seen: Set<string>,
flags: { looping: boolean },
): Generator<TrackSegment> {
if (start.frontNeighbours.length === 0) {
return;
@ -187,14 +208,20 @@ export class TrackSystem {
rightMost = segment;
}
}
if (seen.has(rightMost.id)) return;
if (seen.has(rightMost.id)) {
if (seen.values().next().value === rightMost.id) {
flags.looping = true;
}
return;
}
seen.add(rightMost.id);
yield rightMost.copy();
yield* this.findRightPath(rightMost, seen);
yield* this.findRightPath(rightMost, seen, flags);
}
*findLeftPath(
start: TrackSegment,
seen: Set<string>,
flags: { looping: boolean },
): Generator<TrackSegment> {
if (start.frontNeighbours.length === 0) {
return;
@ -216,10 +243,15 @@ export class TrackSystem {
leftMost = segment;
}
}
if (seen.has(leftMost.id)) return;
if (seen.has(leftMost.id)) {
if (seen.values().next().value === leftMost.id) {
flags.looping = true;
}
return;
}
seen.add(leftMost.id);
yield leftMost.copy();
yield* this.findLeftPath(leftMost, seen);
yield* this.findLeftPath(leftMost, seen, flags);
}
}
@ -232,11 +264,27 @@ export class TrackSegment extends PathSegment {
track?: TrackSystem;
doodler: Doodler;
normalPoints: Vector[] = [];
antiNormalPoints: Vector[] = [];
constructor(p: VectorSet, id?: string) {
super(p);
this.doodler = getContextItem<Doodler>("doodler");
this.id = id ?? crypto.randomUUID();
this.recalculateRailPoints();
}
recalculateRailPoints(resolution = 100) {
this.normalPoints = [];
this.antiNormalPoints = [];
for (let i = 0; i <= resolution; i++) {
const t = i / resolution;
const normal = this.tangent(t).rotate(Math.PI / 2);
normal.setMag(6);
const p = this.getPointAtT(t);
this.normalPoints.push(p.copy().add(normal));
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
}
}
setTrack(t: TrackSystem) {
@ -299,21 +347,17 @@ export class TrackSegment extends PathSegment {
// });
});
}
const lineResolution = 100;
const normalPoints: Vector[] = [];
const antiNormalPoints: Vector[] = [];
for (let i = 0; i <= lineResolution; i++) {
const t = i / lineResolution;
const normal = this.tangent(t).rotate(Math.PI / 2);
normal.setMag(6);
const p = this.getPointAtT(t);
normalPoints.push(p.copy().add(normal));
antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
}
this.doodler.deferDrawing(
() => {
this.doodler.drawLine(normalPoints, { color: "grey" });
this.doodler.drawLine(antiNormalPoints, { color: "grey" });
this.doodler.drawLine(this.normalPoints, {
color: "grey",
weight: 1.5,
});
this.doodler.drawLine(this.antiNormalPoints, {
color: "grey",
weight: 1.5,
});
},
);
// this.doodler.drawCircle(p, 2, {
@ -438,21 +482,24 @@ export class Spline<T extends PathSegment = PathSegment> {
constructor(segs: T[]) {
this.segments = segs;
if (this.segments.at(-1)?.next === this.segments[0]) {
this.looped = true;
}
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);
}
// 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) {
@ -464,7 +511,11 @@ export class Spline<T extends PathSegment = PathSegment> {
draw() {
for (const segment of this.segments) {
segment.draw();
// segment.draw();
const doodler = getContextItem<Doodler>("doodler");
doodler.drawWithAlpha(0.5, () => {
doodler.drawBezier(...segment.points, { color: "red" });
});
}
}