diff --git a/GameLoop.ts b/GameLoop.ts new file mode 100644 index 0000000..d6ee9ef --- /dev/null +++ b/GameLoop.ts @@ -0,0 +1,63 @@ +import { Doodler } from "@bearmetal/doodler"; +import { StateMachine } from "./state/machine.ts"; +import { getContextItem } from "./lib/context.ts"; + +export class GameLoop { + lastTime: number; + running: boolean; + targetFps: number; + + constructor(targetFps: number = 60) { + this.lastTime = performance.now(); + this.running = false; + this.targetFps = targetFps; + } + + async start(state: StateMachine) { + 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); diff --git a/bundle.js b/bundle.js index 8af9caa..5892c5b 100644 --- a/bundle.js +++ b/bundle.js @@ -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); })(); diff --git a/deno.json b/deno.json index 6fadef5..cabc7e9 100644 --- a/deno.json +++ b/deno.json @@ -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" } } \ No newline at end of file diff --git a/deno.lock b/deno.lock index 88cbca4..a0975ed 100644 --- a/deno.lock +++ b/deno.lock @@ -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" ] } } diff --git a/main.ts b/main.ts index 45db8aa..7b4710b 100644 --- a/main.ts +++ b/main.ts @@ -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"); 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); diff --git a/state/machine.ts b/state/machine.ts index 1da69e8..6015050 100644 --- a/state/machine.ts +++ b/state/machine.ts @@ -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 { private _states: Map> = new Map(); private currentState?: State; @@ -6,6 +10,13 @@ export class StateMachine { 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; } diff --git a/state/states/EditTrackState.ts b/state/states/EditTrackState.ts index 19cd24c..47220da 100644 --- a/state/states/EditTrackState.ts +++ b/state/states/EditTrackState.ts @@ -33,6 +33,8 @@ export class EditTrackState extends State { private ghostRotated = false; private closestEnd?: End; + layers: number[] = []; + override update(dt: number): void { const inputManager = getContextItem("inputManager"); const track = getContextItem("track"); @@ -138,22 +140,6 @@ export class EditTrackState extends State { 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 @@ -263,13 +249,34 @@ export class EditTrackState extends State { track.translate(translation); } - track.draw(true); // TODO // Draw ui // Draw track points // Draw track tangents } override start(): void { + const doodler = getContextItem("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("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 { // 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").deleteLayer(layer); + } + const inputManager = getContextItem("inputManager"); inputManager.offKey("e"); inputManager.offKey("w"); diff --git a/state/states/LoadState.ts b/state/states/LoadState.ts index 1265c01..278758c 100644 --- a/state/states/LoadState.ts +++ b/state/states/LoadState.ts @@ -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.RUNNING, ]); + layers: number[] = []; + override update(): void { // noop } @@ -37,6 +40,14 @@ export class LoadState extends State { resources.ready().then(() => { this.stateMachine.transitionTo(States.RUNNING); }); + + const doodler = getContextItem("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 diff --git a/state/states/RunningState.ts b/state/states/RunningState.ts index 06d7a6a..7574b13 100644 --- a/state/states/RunningState.ts +++ b/state/states/RunningState.ts @@ -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.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"); // 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"); + this.layers.push( + doodler.createLayer(() => { + const track = getContextItem("track"); + track.draw(); + }), + doodler.createLayer(() => { + const trains = getContextItem("trains"); + for (const train of trains) { + // if (input.getKeyState("ArrowUp")) { + // train.acceleration.x += 10; + // } + train.draw(); + } + }), + ); + // noop const inputManager = getContextItem("inputManager"); const track = getContextItem("track"); @@ -46,7 +61,7 @@ export class RunningState extends State { 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 { }); } override stop(): void { - // noop + for (const layer of this.layers) { + getContextItem("doodler").deleteLayer(layer); + } } } diff --git a/track/system.ts b/track/system.ts index 02beb10..b9258ca 100644 --- a/track/system.ts +++ b/track/system.ts @@ -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(rightOnlyPath); } @@ -166,6 +186,7 @@ export class TrackSystem { *findRightPath( start: TrackSegment, seen: Set, + flags: { looping: boolean }, ): Generator { 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, + flags: { looping: boolean }, ): Generator { 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"); 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 { 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 { draw() { for (const segment of this.segments) { - segment.draw(); + // segment.draw(); + const doodler = getContextItem("doodler"); + doodler.drawWithAlpha(0.5, () => { + doodler.drawBezier(...segment.points, { color: "red" }); + }); } }