just so much groundwork

This commit is contained in:
2025-02-05 04:00:40 -07:00
parent b3772052f5
commit 952b5dd57f
22 changed files with 1003 additions and 2081 deletions

860
bundle.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
type ClickEvent = {
mouseX: number;
mouseY: number;
}
type ClickEventHandler = (e: ClickEvent) => void;
export class Canvas {
clickables: ClickEventHandler[] = [];
constructor();
constructor(width: number, height: number);
constructor(width?: number, height?: number) {
const canvas = document.createElement('canvas');
canvas.width = width || 400;
canvas.height = height || 400;
}
}

View File

@@ -10,9 +10,10 @@
] ]
}, },
"tasks": { "tasks": {
"dev": "deno run -RWEN --allow-run --unstable dev.ts dev" "dev": "deno run -RWEN --allow-run dev.ts dev"
}, },
"imports": { "imports": {
"doodler": "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/mod.ts" "@bearmetal/doodler": "jsr:@bearmetal/doodler@^0.0.3",
"@lib/": "./lib/"
} }
} }

29
deno.lock generated
View File

@@ -1,21 +1,29 @@
{ {
"version": "4", "version": "4",
"specifiers": { "specifiers": {
"jsr:@bearmetal/doodler@^0.0.3": "0.0.3",
"jsr:@luca/esbuild-deno-loader@*": "0.11.0", "jsr:@luca/esbuild-deno-loader@*": "0.11.0",
"jsr:@std/assert@*": "1.0.10",
"jsr:@std/assert@^1.0.10": "1.0.10",
"jsr:@std/bytes@^1.0.2": "1.0.2", "jsr:@std/bytes@^1.0.2": "1.0.2",
"jsr:@std/cli@^1.0.8": "1.0.9", "jsr:@std/cli@^1.0.8": "1.0.9",
"jsr:@std/encoding@^1.0.5": "1.0.6", "jsr:@std/encoding@^1.0.5": "1.0.6",
"jsr:@std/fmt@^1.0.3": "1.0.3", "jsr:@std/fmt@^1.0.3": "1.0.3",
"jsr:@std/html@^1.0.3": "1.0.3", "jsr:@std/html@^1.0.3": "1.0.3",
"jsr:@std/http@*": "1.0.12", "jsr:@std/http@*": "1.0.12",
"jsr:@std/internal@^1.0.5": "1.0.5",
"jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/media-types@^1.1.0": "1.1.0",
"jsr:@std/net@^1.0.4": "1.0.4", "jsr:@std/net@^1.0.4": "1.0.4",
"jsr:@std/path@^1.0.6": "1.0.8", "jsr:@std/path@^1.0.6": "1.0.8",
"jsr:@std/path@^1.0.8": "1.0.8", "jsr:@std/path@^1.0.8": "1.0.8",
"jsr:@std/streams@^1.0.8": "1.0.8", "jsr:@std/streams@^1.0.8": "1.0.8",
"jsr:@std/testing@*": "1.0.8",
"npm:esbuild@*": "0.24.2" "npm:esbuild@*": "0.24.2"
}, },
"jsr": { "jsr": {
"@bearmetal/doodler@0.0.3": {
"integrity": "42c04b672f4a6bc7ebd45ad936197a2e32856364b66a9a9fe2b81a4aa45c7a08"
},
"@luca/esbuild-deno-loader@0.11.0": { "@luca/esbuild-deno-loader@0.11.0": {
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c", "integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
"dependencies": [ "dependencies": [
@@ -24,6 +32,12 @@
"jsr:@std/path@^1.0.6" "jsr:@std/path@^1.0.6"
] ]
}, },
"@std/assert@1.0.10": {
"integrity": "59b5cbac5bd55459a19045d95cc7c2ff787b4f8527c0dd195078ff6f9481fbb3",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/bytes@1.0.2": { "@std/bytes@1.0.2": {
"integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57" "integrity": "fbdee322bbd8c599a6af186a1603b3355e59a5fb1baa139f8f4c3c9a1b3e3d57"
}, },
@@ -52,6 +66,9 @@
"jsr:@std/streams" "jsr:@std/streams"
] ]
}, },
"@std/internal@1.0.5": {
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
},
"@std/media-types@1.1.0": { "@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
}, },
@@ -63,6 +80,13 @@
}, },
"@std/streams@1.0.8": { "@std/streams@1.0.8": {
"integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3" "integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3"
},
"@std/testing@1.0.8": {
"integrity": "ceef535808fb7568e91b0f8263599bd29b1c5603ffb0377227f00a8ca9fe42a2",
"dependencies": [
"jsr:@std/assert@^1.0.10",
"jsr:@std/internal"
]
} }
}, },
"npm": { "npm": {
@@ -193,5 +217,10 @@
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/EaseInOut.ts": "9eba3d8f5bf5e03220c93916cff6f0bbc24ecdf7550f21fd99e3aaf310f625b0", "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/EaseInOut.ts": "9eba3d8f5bf5e03220c93916cff6f0bbc24ecdf7550f21fd99e3aaf310f625b0",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/Map.ts": "3948648f8bdf8f1ecea83120c41211f5543c7933dbe3e49b367285a98ed50a9a", "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/timing/Map.ts": "3948648f8bdf8f1ecea83120c41211f5543c7933dbe3e49b367285a98ed50a9a",
"https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/zoomableCanvas.ts": "395f80ddaef83e2b37a2884d7fffefae80c2bcecb72269405f53899d5dfc9956" "https://git.cyborggrizzly.com/emma/doodler/raw/tag/0.1.1/zoomableCanvas.ts": "395f80ddaef83e2b37a2884d7fffefae80c2bcecb72269405f53899d5dfc9956"
},
"workspace": {
"dependencies": [
"jsr:@bearmetal/doodler@^0.0.3"
]
} }
} }

22
dev.ts
View File

@@ -4,16 +4,28 @@ import * as esbuild from "npm:esbuild";
import { denoPlugins } from "jsr:@luca/esbuild-deno-loader"; import { denoPlugins } from "jsr:@luca/esbuild-deno-loader";
import { serveDir } from "jsr:@std/http"; import { serveDir } from "jsr:@std/http";
async function* crawl(dir: string): AsyncIterable<string> {
for await (const file of Deno.readDir(dir)) {
const fullPath = dir + "/" + file.name;
if (file.isDirectory) {
yield* crawl(fullPath);
} else {
yield fullPath;
}
}
}
async function dev() { async function dev() {
const paths = []; const paths = [];
const ignoredFiles = ["bundler", "bundle", "dev"]; const ignoredFiles = ["bundler", "bundle", "dev", "test"];
for (const path of Deno.readDirSync("./")) { for await (const path of crawl("./")) {
if ( if (
path.name.endsWith(".ts") && path.endsWith(".ts") &&
!ignoredFiles.find((file) => path.name.includes(file)) !ignoredFiles.find((file) => path.includes(file))
) { ) {
paths.push(path.name); paths.push(path);
} }
} }
await build(); await build();

View File

@@ -1,16 +0,0 @@
import { Constants } from "../math/constants.ts";
import { Vector } from "doodler";
const circle = (ctx: CanvasRenderingContext2D, center: Vector, radius: number) => {
ctx.beginPath();
ctx.arc(center.x, center.y, radius, 0, Constants.TWO_PI);
}
export const drawCircle = (ctx: CanvasRenderingContext2D, center: Vector, radius: number) => {
circle(ctx, center, radius);
ctx.stroke();
}
export const fillCircle = (ctx: CanvasRenderingContext2D, center: Vector, radius: number) => {
circle(ctx, center, radius);
ctx.fill();
}

View File

@@ -1 +0,0 @@
export { drawCircle, fillCircle } from './circle.ts'

View File

@@ -1,6 +0,0 @@
export const drawLine = (ctx: CanvasRenderingContext2D, x1:number, y1:number, x2:number, y2: number) => {
ctx.beginPath();
ctx.moveTo(x1,y1);
ctx.lineTo(x2,y2);
ctx.stroke();
}

View File

@@ -27,8 +27,11 @@ export const ctx = new Proxy(
throw new Error(`Context variable '${prop}' is not defined.`); throw new Error(`Context variable '${prop}' is not defined.`);
}, },
}, },
) as Record<string, any>; ) as Record<string, unknown>;
export function getContext() { export function getContext() {
return ctx; return ctx;
} }
export function getContextItem<T>(prop: string): T {
return ctx[prop] as T;
}

View File

@@ -3,15 +3,21 @@ export class InputManager {
private mouseStates: Map<string | number, boolean> = new Map(); private mouseStates: Map<string | number, boolean> = new Map();
private mouseLocation: { x: number; y: number } = { x: 0, y: 0 }; private mouseLocation: { x: number; y: number } = { x: 0, y: 0 };
private mouseDelta: { x: number; y: number } = { x: 0, y: 0 }; private mouseDelta: { x: number; y: number } = { x: 0, y: 0 };
private keyEvents: Map<string | number, () => void> = new Map();
private mouseEvents: Map<string | number, () => void> = new Map();
constructor() { constructor() {
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
this.keyStates.set(e.key, true); this.keyStates.set(e.key, true);
this.keyEvents.get(e.key)?.call(e);
}); });
document.addEventListener("keyup", (e) => { document.addEventListener("keyup", (e) => {
this.keyStates.set(e.key, false); this.keyStates.set(e.key, false);
}); });
document.addEventListener("mousedown", (e) => { document.addEventListener("mousedown", (e) => {
this.mouseStates.set(e.button, true); this.mouseStates.set(e.button, true);
this.mouseEvents.get(e.button)?.call(e);
}); });
document.addEventListener("mouseup", (e) => { document.addEventListener("mouseup", (e) => {
this.mouseStates.set(e.button, false); this.mouseStates.set(e.button, false);
@@ -38,4 +44,18 @@ export class InputManager {
getMouseDelta() { getMouseDelta() {
return this.mouseDelta; return this.mouseDelta;
} }
onKey(key: string | number, cb: () => void) {
this.keyEvents.set(key, cb);
}
onMouse(key: string | number, cb: () => void) {
this.mouseEvents.set(key, cb);
}
offKey(key: string | number) {
this.keyEvents.delete(key);
}
offMouse(key: string | number) {
this.mouseEvents.delete(key);
}
} }

38
lib/resources.ts Normal file
View File

@@ -0,0 +1,38 @@
export class ResourceManager {
private resources: Map<string, unknown> = new Map();
private statuses: Map<string, Promise<boolean>> = new Map();
get<T>(name: string): T {
if (!this.resources.has(name)) {
throw new Error(`Resource ${name} not found`);
}
return this.resources.get(name) as T;
}
set(name: string, value: unknown) {
if (typeof (value as EventSource).addEventListener === "function") {
this.statuses.set(
name,
new Promise((resolve) => {
const onload = () => {
this.resources.set(name, value);
resolve(true);
(value as EventSource).removeEventListener("load", onload);
};
(value as EventSource).addEventListener("load", onload);
}),
);
} else {
console.warn("Resource added was not a loadable resource");
}
this.resources.set(name, value);
}
delete(name: string) {
this.resources.delete(name);
}
ready() {
return Promise.all(Array.from(this.statuses.values()));
}
}

45
main.ts
View File

@@ -0,0 +1,45 @@
import { setDefaultContext } from "./lib/context.ts";
import { InputManager } from "./lib/input.ts";
import { Doodler, Vector, ZoomableDoodler } from "@bearmetal/doodler";
import { ResourceManager } from "./lib/resources.ts";
import { addButton } from "./ui/button.ts";
import { TrackSystem } from "./track/system.ts";
import { StraightTrack } from "./track/shapes.ts";
const inputManager = new InputManager();
const resources = new ResourceManager();
const doodler = new ZoomableDoodler({
fillScreen: true,
bg: "#302040",
});
setDefaultContext({
inputManager,
doodler,
resources,
debug: true,
showEnds: true,
});
doodler.init();
addButton({
text: "Hello World!",
onClick: () => {
console.log("Hello World!");
},
at: [
new Vector(10, doodler.height - 50),
new Vector(110, doodler.height - 10),
],
style: {
fillColor: "blue",
color: "white",
},
});
const track = new TrackSystem([new StraightTrack()]);
doodler.createLayer(() => {
track.draw();
});

View File

@@ -1,8 +1,11 @@
import { Vector } from "doodler";
export const lerp = (a: number, b: number, t: number) => { export const lerp = (a: number, b: number, t: number) => {
return (a*t) + (b*(1-t)); return (a * t) + (b * (1 - t));
} };
export const map = (value: number, x1: number, y1: number, x2: number, y2: number) => export const map = (
(value - x1) * (y2 - x2) / (y1 - x1) + x2; value: number,
x1: number,
y1: number,
x2: number,
y2: number,
) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;

View File

@@ -1,7 +1,6 @@
import { Vector } from "doodler"; import { Vector } from "@bearmetal/doodler";
export class ComplexPath { export class ComplexPath {
points: Vector[] = []; points: Vector[] = [];
radius = 50; radius = 50;
@@ -22,10 +21,10 @@ export class ComplexPath {
ctx.save(); ctx.save();
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.strokeStyle = 'white'; ctx.strokeStyle = "white";
ctx.setLineDash([21, 6]) ctx.setLineDash([21, 6]);
let last = this.points[this.points.length - 1] let last = this.points[this.points.length - 1];
for (const point of this.points) { for (const point of this.points) {
ctx.beginPath(); ctx.beginPath();
@@ -39,8 +38,7 @@ export class ComplexPath {
} }
export class PathSegment { export class PathSegment {
points: [Vector, Vector, Vector, Vector] points: [Vector, Vector, Vector, Vector];
ctx?: CanvasRenderingContext2D;
length: number; length: number;
@@ -49,44 +47,23 @@ export class PathSegment {
this.length = this.calculateApproxLength(100); this.length = this.calculateApproxLength(100);
} }
setContext(ctx: CanvasRenderingContext2D) {
this.ctx = ctx;
}
draw() {
const [a, b, c, d] = this.points;
doodler.drawBezier(a, b, c, d, {
strokeColor: '#ffffff50'
})
// if (!this.ctx) return;
// const ctx = this.ctx;
// ctx.save();
// ctx.beginPath();
// ctx.moveTo(this.points[0].x, this.points[0].y);
// ctx.bezierCurveTo(
// this.points[1].x,
// this.points[1].y,
// this.points[2].x,
// this.points[2].y,
// this.points[3].x,
// this.points[3].y,
// );
// ctx.strokeStyle = '#ffffff50';
// ctx.lineWidth = 2;
// ctx.stroke();
// ctx.restore();
}
getPointAtT(t: number) { getPointAtT(t: number) {
const [a, b, c, d] = this.points; const [a, b, c, d] = this.points;
const res = a.copy(); const res = a.copy();
res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t)) res.add(Vector.add(a.copy().mult(-3), b.copy().mult(3)).mult(t));
res.add(Vector.add(Vector.add(a.copy().mult(3), b.copy().mult(-6)), c.copy().mult(3)).mult(Math.pow(t, 2))); res.add(
res.add(Vector.add(Vector.add(a.copy().mult(-1), b.copy().mult(3)), Vector.add(c.copy().mult(-3), d.copy())).mult(Math.pow(t, 3))); Vector.add(
Vector.add(a.copy().mult(3), b.copy().mult(-6)),
c.copy().mult(3),
).mult(Math.pow(t, 2)),
);
res.add(
Vector.add(
Vector.add(a.copy().mult(-1), b.copy().mult(3)),
Vector.add(c.copy().mult(-3), d.copy()),
).mult(Math.pow(t, 3)),
);
return res; return res;
} }
@@ -123,15 +100,20 @@ export class PathSegment {
points.push([i * resolution, this]); points.push([i * resolution, this]);
} }
} }
return points return points;
} }
tangent(t: number) { tangent(t: number) {
// dP(t) / dt = -3(1-t)^2 * P0 + 3(1-t)^2 * P1 - 6t(1-t) * P1 - 3t^2 * P2 + 6t(1-t) * P2 + 3t^2 * P3 // dP(t) / dt = -3(1-t)^2 * P0 + 3(1-t)^2 * P1 - 6t(1-t) * P1 - 3t^2 * P2 + 6t(1-t) * P2 + 3t^2 * P3
const [a, b, c, d] = this.points; const [a, b, c, d] = this.points;
const res = Vector.sub(b, a).mult(3 * Math.pow(1 - t, 2)); const res = Vector.sub(b, a).mult(3 * Math.pow(1 - t, 2));
res.add(Vector.add(Vector.sub(c, b).mult(6 * (1 - t) * t), Vector.sub(d, c).mult(3 * Math.pow(t, 2)))); res.add(
Vector.add(
Vector.sub(c, b).mult(6 * (1 - t) * t),
Vector.sub(d, c).mult(3 * Math.pow(t, 2)),
),
);
return res; return res;
} }
@@ -158,12 +140,12 @@ export class PathSegment {
let dist; let dist;
if (k <= 0.0) { if (k <= 0.0) {
dist = Vector.hypot2(v, a) dist = Vector.hypot2(v, a);
} else if (k >= 1.0) { } else if (k >= 1.0) {
dist = Vector.hypot2(v, b) dist = Vector.hypot2(v, b);
} }
dist = Vector.hypot2(v, d) dist = Vector.hypot2(v, d);
if (dist < distance) { if (dist < distance) {
distance = dist; distance = dist;
@@ -179,27 +161,28 @@ export class PathSegment {
calculateApproxLength(resolution = 25) { calculateApproxLength(resolution = 25) {
const stepSize = 1 / resolution; const stepSize = 1 / resolution;
const points: Vector[] = [] const points: Vector[] = [];
for (let i = 0; i <= resolution; i++) { for (let i = 0; i <= resolution; i++) {
const current = stepSize * i; const current = stepSize * i;
points.push(this.getPointAtT(current)) points.push(this.getPointAtT(current));
} }
this.length = points.reduce((acc: { prev?: Vector, length: number }, cur) => { this.length =
const prev = acc.prev; points.reduce((acc: { prev?: Vector; length: number }, cur) => {
acc.prev = cur; const prev = acc.prev;
if (!prev) return acc; acc.prev = cur;
acc.length += cur.dist(prev); if (!prev) return acc;
return acc; acc.length += cur.dist(prev);
}, { prev: undefined, length: 0 }).length return acc;
}, { prev: undefined, length: 0 }).length;
return this.length; return this.length;
} }
calculateEvenlySpacedPoints(spacing: number, resolution = 1) { calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
const points: Vector[] = [] const points: Vector[] = [];
points.push(this.points[0]); points.push(this.points[0]);
let prev = points[0]; let prev = points[0];
let distSinceLastEvenPoint = 0 let distSinceLastEvenPoint = 0;
let t = 0; let t = 0;
@@ -209,10 +192,12 @@ export class PathSegment {
const point = this.getPointAtT(t); const point = this.getPointAtT(t);
distSinceLastEvenPoint += prev.dist(point); distSinceLastEvenPoint += prev.dist(point);
if (distSinceLastEvenPoint >= spacing) { if (distSinceLastEvenPoint >= spacing) {
const overshoot = distSinceLastEvenPoint - spacing; const overshoot = distSinceLastEvenPoint - spacing;
const evenPoint = Vector.add(point, Vector.sub(point, prev).normalize().mult(overshoot)) const evenPoint = Vector.add(
point,
Vector.sub(point, prev).normalize().mult(overshoot),
);
distSinceLastEvenPoint = overshoot; distSinceLastEvenPoint = overshoot;
points.push(evenPoint); points.push(evenPoint);
prev = evenPoint; prev = evenPoint;

53
state/machine.ts Normal file
View File

@@ -0,0 +1,53 @@
export class StateMachine {
private _states: Map<string, State> = new Map();
private currentState: State;
constructor(states: State[]) {
this.currentState = states[0];
}
update(dt: number) {
this.currentState.update(dt);
}
get current() {
return this.currentState;
}
get states() {
return this._states;
}
addState(state: State) {
this.states.set(state.name, state);
}
transitionTo(state: State) {
if (this.current.canTransitionTo(state)) {
this.current.stop();
this.currentState = state;
this.current.start();
}
}
}
export abstract class State<T> {
private stateMachine: StateMachine;
protected abstract validTransitions: Set<T>;
abstract readonly name: T;
constructor(
stateMachine: StateMachine,
) {
this.stateMachine = stateMachine;
}
abstract update(dt: number): void;
abstract start(): void;
abstract stop(): void;
canTransitionTo(state: T) {
return this.validTransitions.has(state);
}
}

139
state/states.ts Normal file
View File

@@ -0,0 +1,139 @@
import { State } from "./machine.ts";
enum States {
LOAD,
RUNNING,
PAUSED,
EDIT_TRACK,
EDIT_TRAIN,
}
export class LoadState extends State<States> {
override name: States = States.LOAD;
override validTransitions: Set<States> = new Set([
States.RUNNING,
]);
override update(dt: number): void {
throw new Error("Method not implemented.");
// TODO
// Do nothing
}
override start(): void {
throw new Error("Method not implemented.");
// TODO
// load track into context
// Load trains into context
// Load resources into context
// Switch to running state
}
override stop(): void {
throw new Error("Method not implemented.");
// TODO
// Do nothing
}
}
export class RunningState extends State<States> {
override name: States = States.RUNNING;
override validTransitions: Set<States> = new Set([
States.PAUSED,
States.EDIT_TRACK,
]);
override update(dt: number): void {
throw new Error("Method not implemented.");
// TODO
// Update trains
// Update world
// Handle input
// Draw (maybe via a layer system that syncs with doodler)
// Monitor world events
}
override start(): void {
throw new Error("Method not implemented.");
// TODO
// Do nothing
}
override stop(): void {
throw new Error("Method not implemented.");
// TODO
// Do nothing
}
}
export class PausedState extends State<States> {
override name: States = States.PAUSED;
override validTransitions: Set<States> = new Set([
States.LOAD,
States.RUNNING,
States.EDIT_TRACK,
States.EDIT_TRAIN,
]);
override update(dt: number): void {
throw new Error("Method not implemented.");
// TODO
// Handle input
// Draw ui
}
override start(): void {
throw new Error("Method not implemented.");
// TODO
// Save tracks to cache
// Save trains to cache
// Save resources to cache
}
override stop(): void {
throw new Error("Method not implemented.");
// TODO
// Do nothing
}
}
export class EditTrackState extends State<States> {
override name: States = States.EDIT_TRACK;
override validTransitions: Set<States> = new Set([
States.RUNNING,
States.PAUSED,
]);
override update(dt: number): void {
throw new Error("Method not implemented.");
// TODO
// Handle input
// Draw ui
// Draw track
// Draw track points
// Draw track tangents
}
override start(): void {
throw new Error("Method not implemented.");
// TODO
// Cache trains and save
// Stash track in context
}
override stop(): void {
throw new Error("Method not implemented.");
}
}
export class EditTrainState extends State<States> {
override name: States = States.EDIT_TRAIN;
override validTransitions: Set<States> = new Set([
States.RUNNING,
States.PAUSED,
]);
override update(dt: number): void {
throw new Error("Method not implemented.");
}
override start(): void {
throw new Error("Method not implemented.");
// TODO
// Cache trains
// Stash train in context
// Draw track
// Draw train (filtered by train ID)
}
override stop(): void {
throw new Error("Method not implemented.");
}
}

38
test/bench.ts Normal file
View File

@@ -0,0 +1,38 @@
import { assert } from "jsr:@std/assert";
import { describe, it } from "jsr:@std/testing/bdd";
/**
* Tests if a function can run a given number of iterations within a target frame time.
* @param fn The function to test.
* @param iterations Number of times to run the function per frame.
* @param fps Target frames per second.
*/
export function testPerformance(
fn: () => unknown,
iterations: number,
fps: number,
) {
console.log(`Performance Test - ${iterations} iterations at ${fps} FPS`);
const frameTime = 1000 / fps;
const startTime = performance.now();
for (let i = 0; i < iterations; i++) {
fn();
}
const endTime = performance.now();
const elapsed = endTime - startTime;
console.log(
`Elapsed time: ${elapsed.toFixed(2)}ms (Target: ≤${
frameTime.toFixed(2)
}ms)`,
);
assert(
elapsed <= frameTime,
`Function took too long: ${elapsed.toFixed(2)}ms (Target: ≤${
frameTime.toFixed(2)
}ms)`,
);
// });
}

File diff suppressed because it is too large Load Diff

35
test/contextBench.test.js Normal file
View File

@@ -0,0 +1,35 @@
import {
getContextItem,
setDefaultContext,
withContext,
} from "@lib/context.ts"; // adjust path as needed
import { testPerformance } from "./bench.ts";
Deno.test("Context Benchmark", () => {
console.log("Context Benchmark - run within frame time");
testPerformance(
() => {
setDefaultContext({ a: 1 });
},
10000,
60,
);
testPerformance(
() => {
withContext({ a: 1 }, () => {
getContextItem("a");
});
},
10000,
60,
);
testPerformance(
() => {
getContextItem("a");
},
100000,
240,
);
});

14
track/shapes.ts Normal file
View File

@@ -0,0 +1,14 @@
import { Vector } from "@bearmetal/doodler";
import { TrackSegment } from "./system.ts";
export class StraightTrack extends TrackSegment {
constructor(start?: Vector) {
start = start || new Vector(100, 100);
super([
start.copy(),
start.copy().add(25, 0),
start.copy().add(75, 0),
start.copy().add(100, 0),
]);
}
}

124
track/system.ts Normal file
View File

@@ -0,0 +1,124 @@
import { Doodler, Vector } from "@bearmetal/doodler";
import { PathSegment } from "../math/path.ts";
import { getContextItem, setDefaultContext } from "../lib/context.ts";
export class TrackSystem {
private segments: Map<string, TrackSegment> = new Map();
private doodler: Doodler;
constructor(segments: TrackSegment[]) {
this.doodler = getContextItem<Doodler>("doodler");
for (const segment of segments) {
this.segments.set(segment.id, segment);
}
}
draw() {
for (const segment of this.segments.values()) {
segment.draw();
}
try {
if (getContextItem<boolean>("showEnds")) {
const ends = this.findEnds();
for (const end of ends) {
this.doodler.fillCircle(end.pos, 2, {
color: "red",
// weight: 3,
});
if (getContextItem<boolean>("debug")) {
this.doodler.line(
end.pos,
end.pos.copy().add(end.tangent.copy().mult(20)),
{
color: "blue",
// weight: 3,
},
);
}
}
}
} catch {
setDefaultContext({ showEnds: false });
}
}
findEnds() {
const ends: { pos: Vector; segment: TrackSegment; tangent: Vector }[] = [];
for (const segment of this.segments.values()) {
const [a, b, c, d] = segment.points;
{
const tangent = Vector.sub(a, b).normalize();
const pos = a.copy();
ends.push({ pos, segment, tangent });
}
{
const tangent = Vector.sub(d, c).normalize();
const pos = d.copy();
ends.push({ pos, segment, tangent });
}
}
return ends;
}
serialize() {
return this.segments.values().map((s) => s.serialize()).toArray();
}
static deserialize(data: any) {
const track = new TrackSystem([]);
for (const segment of data) {
track.segments.set(segment.id, TrackSegment.deserialize(segment));
}
return track;
}
}
export class TrackSegment extends PathSegment {
frontNeighbours: TrackSegment[] = [];
backNeighbours: TrackSegment[] = [];
track?: TrackSystem;
doodler: Doodler;
id: string;
constructor(p: [Vector, Vector, Vector, Vector], id?: string) {
super(p);
this.doodler = getContextItem<Doodler>("doodler");
this.id = id ?? crypto.randomUUID();
}
setTrack(t: TrackSystem) {
this.track = t;
}
draw() {
this.doodler.drawBezier(
this.points[0],
this.points[1],
this.points[2],
this.points[3],
{
strokeColor: "#ffffff50",
},
);
}
serialize() {
return {
p: this.points.map((p) => p.array()),
id: this.id,
bNeighbors: this.backNeighbours.map((n) => n.id),
fNeighbors: this.frontNeighbours.map((n) => n.id),
};
}
static deserialize(data: any) {
return new TrackSegment(
data.p.map((p: [number, number, number]) => new Vector(p[0], p[1], p[2])),
data.id,
);
}
}

39
ui/button.ts Normal file
View File

@@ -0,0 +1,39 @@
import { Doodler, Vector } from "@bearmetal/doodler";
import { getContext, getContextItem } from "../lib/context.ts";
export function addButton(props: {
text: string;
onClick: () => void;
style?: {
color?: string;
fillColor?: string;
strokeColor?: string;
weight?: number;
noStroke?: boolean;
noFill?: boolean;
};
at: [Vector, Vector];
}) {
const doodler = getContextItem<Doodler>("doodler");
const { text, onClick, style } = props;
const { x, y } = props.at[1].copy().sub(props.at[0]);
const id = doodler.addUIElement(
"rectangle",
props.at[0],
x,
y,
style,
);
doodler.registerClickable(props.at[0], props.at[1], onClick);
return {
id,
text,
onClick,
style,
};
}
export function removeButton(id: string, onClick: () => void) {
getContextItem<Doodler>("doodler").removeUIElement(id);
getContextItem<Doodler>("doodler").unregisterClickable(onClick);
}