Very rudimentary track editor... and headache spaghetti

This commit is contained in:
Emmaline Autumn 2025-02-08 05:30:16 -07:00
parent 791ba42ceb
commit 8dc0af650f
6 changed files with 542 additions and 173 deletions

471
bundle.js
View File

@ -71,65 +71,6 @@
}, 2); }, 2);
} }
// lib/input.ts
var InputManager = class {
keyStates = /* @__PURE__ */ new Map();
mouseStates = /* @__PURE__ */ new Map();
mouseLocation = { x: 0, y: 0 };
mouseDelta = { x: 0, y: 0 };
keyEvents = /* @__PURE__ */ new Map();
mouseEvents = /* @__PURE__ */ new Map();
constructor() {
document.addEventListener("keydown", (e) => {
this.keyStates.set(e.key, true);
this.keyEvents.get(e.key)?.call(e);
});
document.addEventListener("keyup", (e) => {
this.keyStates.set(e.key, false);
});
document.addEventListener("mousedown", (e) => {
this.mouseStates.set(e.button, true);
this.mouseEvents.get(e.button)?.call(e);
});
document.addEventListener("mouseup", (e) => {
this.mouseStates.set(e.button, false);
});
self.addEventListener("mousemove", (e) => {
this.mouseLocation = { x: e.clientX, y: e.clientY };
this.mouseDelta = {
x: e.movementX,
y: e.movementY
};
});
}
getKeyState(key) {
return this.keyStates.get(key);
}
getMouseState(key) {
return this.mouseStates.get(key);
}
getMouseLocation() {
return this.mouseLocation;
}
getMouseDelta() {
return this.mouseDelta;
}
onKey(key, cb) {
this.keyEvents.set(key, cb);
}
onMouse(key, cb) {
this.mouseEvents.set(key, cb);
}
offKey(key) {
const events = this.keyEvents.get(key);
this.keyEvents.delete(key);
return events;
}
offMouse(key) {
this.mouseEvents.delete(key);
}
};
// https://jsr.io/@bearmetal/doodler/0.0.3/geometry/constants.ts // https://jsr.io/@bearmetal/doodler/0.0.3/geometry/constants.ts
var Constants = { var Constants = {
TWO_PI: Math.PI * 2 TWO_PI: Math.PI * 2
@ -1072,6 +1013,82 @@
} }
}; };
// lib/input.ts
var InputManager = class {
keyStates = /* @__PURE__ */ new Map();
mouseStates = /* @__PURE__ */ new Map();
mouseLocation = { x: 0, y: 0 };
mouseDelta = { x: 0, y: 0 };
keyEvents = /* @__PURE__ */ new Map();
mouseEvents = /* @__PURE__ */ new Map();
constructor() {
document.addEventListener("keydown", (e) => {
this.keyStates.set(e.key, true);
this.keyEvents.get(e.key)?.call(e);
});
document.addEventListener("keyup", (e) => {
this.keyStates.set(e.key, false);
});
document.addEventListener("mousedown", (e) => {
this.mouseStates.set(e.button, true);
this.mouseEvents.get(e.button)?.call(e);
});
document.addEventListener("mouseup", (e) => {
this.mouseStates.set(e.button, false);
});
self.addEventListener("mousemove", (e) => {
this.mouseLocation = { x: e.clientX, y: e.clientY };
this.mouseDelta = {
x: e.movementX,
y: e.movementY
};
});
}
getKeyState(key) {
return this.keyStates.get(key);
}
getMouseState(key) {
return this.mouseStates.get(key);
}
getMouseLocation() {
if (getContextItem("doodler") instanceof ZoomableDoodler) {
return getContextItem("doodler").screenToWorld(
this.mouseLocation.x,
this.mouseLocation.y
);
}
return this.mouseLocation;
}
getMouseLocationV() {
if (getContextItem("doodler") instanceof ZoomableDoodler) {
return new Vector(
getContextItem("doodler").screenToWorld(
this.mouseLocation.x,
this.mouseLocation.y
)
);
}
return new Vector(this.mouseLocation);
}
getMouseDelta() {
return this.mouseDelta;
}
onKey(key, cb) {
this.keyEvents.set(key, cb);
}
onMouse(key, cb) {
this.mouseEvents.set(key, cb);
}
offKey(key) {
const events = this.keyEvents.get(key);
this.keyEvents.delete(key);
return events;
}
offMouse(key) {
this.mouseEvents.delete(key);
}
};
// lib/resources.ts // lib/resources.ts
var ResourceManager = class { var ResourceManager = class {
resources = /* @__PURE__ */ new Map(); resources = /* @__PURE__ */ new Map();
@ -1186,114 +1203,15 @@
} }
}; };
// state/states/EditTrackState.ts
var EditTrackState = class extends State {
name = 3 /* EDIT_TRACK */;
validTransitions = /* @__PURE__ */ new Set([
1 /* RUNNING */,
2 /* PAUSED */
]);
heldEvents = /* @__PURE__ */ new Map();
update(dt) {
const inputManager2 = getContextItem("inputManager");
const track = getContextItem("track");
const firstSegment = track.firstSegment;
if (firstSegment) {
const firstPoint = firstSegment.points[0].copy();
const { x, y } = inputManager2.getMouseLocation();
firstSegment.points.forEach((p, i) => {
const relativePoint = Vector.sub(p, firstPoint);
p.set(x, y);
p.add(relativePoint);
});
}
track.draw(true);
}
start() {
const inputManager2 = getContextItem("inputManager");
this.heldEvents.set("e", inputManager2.offKey("e"));
this.heldEvents.set("Escape", inputManager2.offKey("Escape"));
inputManager2.onKey("e", () => {
const state2 = getContextItem("state");
state2.transitionTo(1 /* RUNNING */);
});
const track = getContextItem("track");
setContextItem("trackCopy", track.copy());
inputManager2.onKey("Escape", () => {
const trackCopy = getContextItem("trackCopy");
setContextItem("track", trackCopy);
const state2 = getContextItem("state");
state2.transitionTo(1 /* RUNNING */);
});
}
stop() {
if (this.heldEvents.size > 0) {
for (const [key, cb] of this.heldEvents) {
if (cb) {
getContextItem("inputManager").onKey(key, cb);
}
this.heldEvents.delete(key);
}
}
}
};
// state/states/PausedState.ts
var PausedState = class extends State {
name = 2 /* PAUSED */;
validTransitions = /* @__PURE__ */ new Set([
0 /* LOAD */,
1 /* RUNNING */,
3 /* EDIT_TRACK */,
4 /* EDIT_TRAIN */
]);
update(dt) {
throw new Error("Method not implemented.");
}
start() {
throw new Error("Method not implemented.");
}
stop() {
throw new Error("Method not implemented.");
}
};
// state/states/RunningState.ts
var RunningState = class extends State {
name = 1 /* RUNNING */;
validTransitions = /* @__PURE__ */ new Set([
2 /* PAUSED */,
3 /* EDIT_TRACK */
]);
update(dt) {
const ctx2 = getContext();
ctx2.track.draw();
for (const train of ctx2.trains) {
train.draw();
}
}
start() {
}
stop() {
}
};
// inputs.ts
function bootstrapInputs() {
const inputManager2 = getContextItem("inputManager");
inputManager2.onKey("e", () => {
const state2 = getContextItem("state");
state2.transitionTo(3 /* EDIT_TRACK */);
});
}
// math/path.ts // math/path.ts
var PathSegment = class { var PathSegment = class {
points; points;
length; length;
startingLength;
constructor(points) { constructor(points) {
this.points = points; this.points = points;
this.length = this.calculateApproxLength(100); this.length = this.calculateApproxLength(100);
this.startingLength = Math.round(this.length);
} }
getPointAtT(t) { getPointAtT(t) {
const [a, b, c, d] = this.points; const [a, b, c, d] = this.points;
@ -1401,7 +1319,7 @@
}, { prev: void 0, length: 0 }).length; }, { prev: void 0, length: 0 }).length;
return this.length; return this.length;
} }
calculateEvenlySpacedPoints(spacing, resolution = 1) { calculateEvenlySpacedPoints(spacing, resolution = 1, targetLength) {
const points = []; const points = [];
points.push(this.points[0]); points.push(this.points[0]);
let prev = points[0]; let prev = points[0];
@ -1424,8 +1342,41 @@
} }
prev = point; prev = point;
} }
if (targetLength && points.length < targetLength) {
while (points.length < targetLength) {
t += 1 / div;
const point = this.getPointAtT(t);
distSinceLastEvenPoint += prev.dist(point);
if (distSinceLastEvenPoint >= spacing) {
const overshoot = distSinceLastEvenPoint - spacing;
const evenPoint = Vector.add(
point,
Vector.sub(point, prev).normalize().mult(overshoot)
);
distSinceLastEvenPoint = overshoot;
points.push(evenPoint);
prev = evenPoint;
}
prev = point;
}
}
return points; return points;
} }
calculateSubdividedPoints(numberOfPoints) {
const points = [];
for (let i = 0; i < numberOfPoints; i++) {
const point = this.getPointAtT(i / numberOfPoints);
points.push(point);
}
return points;
}
clampLength() {
const curveLength = this.startingLength;
const points = this.calculateEvenlySpacedPoints(1, 1, curveLength + 1);
if (points.length >= curveLength) {
this.points[3].set(points[curveLength]);
}
}
}; };
// track/system.ts // track/system.ts
@ -1441,6 +1392,13 @@
get firstSegment() { get firstSegment() {
return this.segments.values().next().value; return this.segments.values().next().value;
} }
get lastSegment() {
return this.segments.values().toArray().pop();
}
registerSegment(segment) {
segment.setTrack(this);
this.segments.set(segment.id, segment);
}
draw(showControls = false) { draw(showControls = false) {
for (const segment of this.segments.values()) { for (const segment of this.segments.values()) {
segment.draw(showControls); segment.draw(showControls);
@ -1567,6 +1525,41 @@
this.id this.id
); );
} }
propagate() {
const [_, __, p3, p4] = this.points;
const tangent = Vector.sub(p4, p3);
for (const fNeighbour of this.frontNeighbours) {
fNeighbour.receivePropagation(tangent);
}
}
lastHeading;
receivePropagation(tangent) {
const [p1, p2, p3, p4] = this.points;
this.rotate(tangent);
this.propagate();
}
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
rotate(angle) {
const [p1, p2, p3, p4] = this.points;
let newP2;
if (angle instanceof Vector) {
const tan = angle;
angle = tan.heading() - (this.lastHeading ?? 0);
this.lastHeading = tan.heading();
newP2 = Vector.add(p1, tan);
} else {
const p1ToP2 = Vector.sub(p2, p1);
p1ToP2.rotate(angle);
newP2 = Vector.add(p1, p1ToP2);
}
const p2ToP3 = Vector.sub(p3, p2);
p2ToP3.rotate(angle);
p3.set(Vector.add(newP2, p2ToP3));
const p2Top4 = Vector.sub(p4, p2);
p2Top4.rotate(angle);
p4.set(Vector.add(newP2, p2Top4));
p2.set(newP2);
}
static deserialize(data) { static deserialize(data) {
return new _TrackSegment( return new _TrackSegment(
data.p.map((p) => new Vector(p[0], p[1], p[2])), data.p.map((p) => new Vector(p[0], p[1], p[2])),
@ -1588,6 +1581,161 @@
} }
}; };
// state/states/EditTrackState.ts
var EditTrackState = class extends State {
name = 3 /* EDIT_TRACK */;
validTransitions = /* @__PURE__ */ new Set([
1 /* RUNNING */,
2 /* PAUSED */
]);
heldEvents = /* @__PURE__ */ new Map();
currentSegment;
update(dt) {
const inputManager2 = getContextItem("inputManager");
const track = getContextItem("track");
const doodler2 = getContextItem("doodler");
const segment = this.currentSegment;
if (segment) {
segment.propagate();
const mousePos = inputManager2.getMouseLocationV();
const p1 = segment.points[0];
const p2 = segment.points[1];
const p3 = segment.points[2];
const p4 = segment.points[3];
const prevp3 = p3.copy();
const dirToMouse = Vector.sub(mousePos, p2).normalize();
const angleToMouse = dirToMouse.heading();
const angleToP1 = Vector.sub(p2, p1).heading();
const p2DistToMouse = Vector.dist(p2, mousePos);
const p3DistToMouse = Vector.dist(p3, mousePos);
const distToP3 = Vector.dist(p2, p3);
const distToP4 = Vector.dist(prevp3, p4);
if (Math.abs(angleToMouse - angleToP1) < 0.6 && p2DistToMouse > distToP3 && p3DistToMouse > distToP4) {
{
const dirToNewP3 = dirToMouse.copy().rotate(
-(angleToMouse - angleToP1) / 2
);
dirToNewP3.setMag(distToP3);
p3.set(Vector.add(p2, dirToNewP3));
doodler2.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" });
doodler2.line(p2, Vector.add(p2, dirToMouse.mult(100)), {
color: "red"
});
}
{
const dirToMouse2 = Vector.sub(mousePos, p3).normalize();
dirToMouse2.setMag(distToP4);
p4.set(Vector.add(p3, dirToMouse2));
doodler2.line(p3, Vector.add(p3, dirToMouse2), { color: "green" });
}
segment.clampLength();
}
doodler2.fillText(
segment.calculateApproxLength().toFixed(2),
p2.copy().add(10, 0),
100
);
}
track.draw(true);
}
start() {
const inputManager2 = getContextItem("inputManager");
this.heldEvents.set("e", inputManager2.offKey("e"));
this.heldEvents.set("Escape", inputManager2.offKey("Escape"));
inputManager2.onKey("e", () => {
const state2 = getContextItem("state");
state2.transitionTo(1 /* RUNNING */);
});
const track = getContextItem("track");
setContextItem("trackCopy", track.copy());
inputManager2.onKey("Escape", () => {
const trackCopy = getContextItem("trackCopy");
setContextItem("track", trackCopy);
setContextItem("trackCopy", void 0);
const state2 = getContextItem("state");
state2.transitionTo(1 /* RUNNING */);
});
inputManager2.onKey("w", () => {
const track2 = getContextItem("track");
const segment = track2.lastSegment;
if (!segment) return;
const n = new StraightTrack(segment.points[3]);
const t = segment.tangent(1).heading();
n.rotate(t);
segment.frontNeighbours.push(n);
track2.registerSegment(n);
this.currentSegment = n;
});
inputManager2.onKey("1", () => {
this.currentSegment = track.firstSegment;
});
this.currentSegment = track.lastSegment;
}
stop() {
const inputManager2 = getContextItem("inputManager");
inputManager2.offKey("e");
inputManager2.offKey("Escape");
if (this.heldEvents.size > 0) {
for (const [key, cb] of this.heldEvents) {
if (cb) {
getContextItem("inputManager").onKey(key, cb);
}
this.heldEvents.delete(key);
}
}
setContextItem("trackCopy", void 0);
}
};
// state/states/PausedState.ts
var PausedState = class extends State {
name = 2 /* PAUSED */;
validTransitions = /* @__PURE__ */ new Set([
0 /* LOAD */,
1 /* RUNNING */,
3 /* EDIT_TRACK */,
4 /* EDIT_TRAIN */
]);
update(dt) {
throw new Error("Method not implemented.");
}
start() {
throw new Error("Method not implemented.");
}
stop() {
throw new Error("Method not implemented.");
}
};
// state/states/RunningState.ts
var RunningState = class extends State {
name = 1 /* RUNNING */;
validTransitions = /* @__PURE__ */ new Set([
2 /* PAUSED */,
3 /* EDIT_TRACK */
]);
update(dt) {
const ctx2 = getContext();
ctx2.track.draw();
for (const train of ctx2.trains) {
train.draw();
}
}
start() {
}
stop() {
}
};
// inputs.ts
function bootstrapInputs() {
const inputManager2 = getContextItem("inputManager");
inputManager2.onKey("e", () => {
const state2 = getContextItem("state");
state2.transitionTo(3 /* EDIT_TRACK */);
});
}
// state/states/LoadState.ts // state/states/LoadState.ts
var LoadState = class extends State { var LoadState = class extends State {
name = 0 /* LOAD */; name = 0 /* LOAD */;
@ -1641,6 +1789,7 @@
fillScreen: true, fillScreen: true,
bg: "#302040" bg: "#302040"
}); });
doodler.scale = doodler.maxScale;
setDefaultContext({ setDefaultContext({
inputManager, inputManager,
doodler, doodler,

View File

@ -1,3 +1,6 @@
import { Vector, ZoomableDoodler } from "@bearmetal/doodler";
import { getContextItem } from "./context.ts";
export class InputManager { export class InputManager {
private keyStates: Map<string | number, boolean> = new Map(); private keyStates: Map<string | number, boolean> = new Map();
private mouseStates: Map<string | number, boolean> = new Map(); private mouseStates: Map<string | number, boolean> = new Map();
@ -39,8 +42,25 @@ export class InputManager {
return this.mouseStates.get(key); return this.mouseStates.get(key);
} }
getMouseLocation() { getMouseLocation() {
if (getContextItem("doodler") instanceof ZoomableDoodler) {
return getContextItem<ZoomableDoodler>("doodler").screenToWorld(
this.mouseLocation.x,
this.mouseLocation.y,
);
}
return this.mouseLocation; return this.mouseLocation;
} }
getMouseLocationV() {
if (getContextItem("doodler") instanceof ZoomableDoodler) {
return new Vector(
getContextItem<ZoomableDoodler>("doodler").screenToWorld(
this.mouseLocation.x,
this.mouseLocation.y,
),
);
}
return new Vector(this.mouseLocation);
}
getMouseDelta() { getMouseDelta() {
return this.mouseDelta; return this.mouseDelta;
} }

View File

@ -19,6 +19,8 @@ const doodler = new ZoomableDoodler({
fillScreen: true, fillScreen: true,
bg: "#302040", bg: "#302040",
}); });
// doodler.minScale = 0.1;
(doodler as any).scale = doodler.maxScale;
setDefaultContext({ setDefaultContext({
inputManager, inputManager,

View File

@ -41,10 +41,12 @@ export class PathSegment {
points: [Vector, Vector, Vector, Vector]; points: [Vector, Vector, Vector, Vector];
length: number; length: number;
startingLength: number;
constructor(points: [Vector, Vector, Vector, Vector]) { constructor(points: [Vector, Vector, Vector, Vector]) {
this.points = points; this.points = points;
this.length = this.calculateApproxLength(100); this.length = this.calculateApproxLength(100);
this.startingLength = Math.round(this.length);
} }
getPointAtT(t: number) { getPointAtT(t: number) {
@ -177,7 +179,11 @@ export class PathSegment {
return this.length; return this.length;
} }
calculateEvenlySpacedPoints(spacing: number, resolution = 1) { calculateEvenlySpacedPoints(
spacing: number,
resolution = 1,
targetLength?: number,
) {
const points: Vector[] = []; const points: Vector[] = [];
points.push(this.points[0]); points.push(this.points[0]);
@ -206,6 +212,46 @@ export class PathSegment {
prev = point; prev = point;
} }
if (targetLength && points.length < targetLength) {
while (points.length < targetLength) {
t += 1 / div;
const point = this.getPointAtT(t);
distSinceLastEvenPoint += prev.dist(point);
if (distSinceLastEvenPoint >= spacing) {
const overshoot = distSinceLastEvenPoint - spacing;
const evenPoint = Vector.add(
point,
Vector.sub(point, prev).normalize().mult(overshoot),
);
distSinceLastEvenPoint = overshoot;
points.push(evenPoint);
prev = evenPoint;
}
prev = point;
}
}
return points; return points;
} }
calculateSubdividedPoints(numberOfPoints: number) {
const points: Vector[] = [];
for (let i = 0; i < numberOfPoints; i++) {
const point = this.getPointAtT(i / numberOfPoints);
points.push(point);
}
return points;
}
clampLength() {
const curveLength = this.startingLength;
const points = this.calculateEvenlySpacedPoints(1, 1, curveLength + 1);
if (points.length >= curveLength) {
this.points[3].set(points[curveLength]);
}
}
} }

View File

@ -1,9 +1,11 @@
import { Vector } from "@bearmetal/doodler"; import { Doodler, Vector } from "@bearmetal/doodler";
import { getContextItem, setContextItem } from "../../lib/context.ts"; import { getContextItem, setContextItem } 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";
import { State, StateMachine } from "../machine.ts"; import { State, StateMachine } from "../machine.ts";
import { States } from "./index.ts"; import { States } from "./index.ts";
import { StraightTrack } from "../../track/shapes.ts";
import { TrackSegment } from "../../track/system.ts";
export class EditTrackState extends State<States> { export class EditTrackState extends State<States> {
override name: States = States.EDIT_TRACK; override name: States = States.EDIT_TRACK;
@ -15,19 +17,89 @@ export class EditTrackState extends State<States> {
private heldEvents: Map<string | number, (() => void) | undefined> = private heldEvents: Map<string | number, (() => void) | undefined> =
new Map(); new Map();
private currentSegment?: TrackSegment;
override update(dt: number): void { override update(dt: number): void {
const inputManager = getContextItem<InputManager>("inputManager"); const inputManager = getContextItem<InputManager>("inputManager");
const track = getContextItem<TrackSystem>("track"); const track = getContextItem<TrackSystem>("track");
const firstSegment = track.firstSegment; // For moving a segment, i.e. the currently active one
if (firstSegment) { // const segment = track.lastSegment;
const firstPoint = firstSegment.points[0].copy(); // if (segment) {
const { x, y } = inputManager.getMouseLocation(); // const firstPoint = segment.points[0].copy();
firstSegment.points.forEach((p, i) => { // const { x, y } = inputManager.getMouseLocation();
const relativePoint = Vector.sub(p, firstPoint); // segment.points.forEach((p, i) => {
p.set(x, y); // const relativePoint = Vector.sub(p, firstPoint);
p.add(relativePoint); // p.set(x, y);
}); // p.add(relativePoint);
// });
// }
// manipulate only end of segment while maintaining length
// const segment = track.lastSegment;
// if (segment) {
// const p3 = segment.points[2];
// const p4 = segment.points[3];
// let curveLength = Math.round(segment.calculateApproxLength());
// this.startingLength = this.startingLength ?? curveLength;
// curveLength = this.startingLength;
// const { x, y } = inputManager.getMouseLocation();
// p4.set(x, y);
// const points = segment.calculateEvenlySpacedPoints(1);
// if (points.length > curveLength) p4.set(points[curveLength - 1]);
// // doodler.fillText(curveLength.toFixed(2), p3.copy().add(10, 0), 100);
// }
const doodler = getContextItem<Doodler>("doodler");
// Adjust angles until tangent points to mouse
const segment = this.currentSegment;
if (segment) {
segment.propagate();
const mousePos = inputManager.getMouseLocationV();
const p1 = segment.points[0];
const p2 = segment.points[1];
const p3 = segment.points[2];
const p4 = segment.points[3];
const prevp3 = p3.copy();
const dirToMouse = Vector.sub(mousePos, p2).normalize();
const angleToMouse = dirToMouse.heading();
const angleToP1 = Vector.sub(p2, p1).heading();
const p2DistToMouse = Vector.dist(p2, mousePos);
const p3DistToMouse = Vector.dist(p3, mousePos);
const distToP3 = Vector.dist(p2, p3);
const distToP4 = Vector.dist(prevp3, p4);
if (
Math.abs(angleToMouse - angleToP1) < .6 &&
p2DistToMouse > distToP3 &&
p3DistToMouse > distToP4
) {
{
const dirToNewP3 = dirToMouse.copy().rotate(
-(angleToMouse - angleToP1) / 2,
);
dirToNewP3.setMag(distToP3);
p3.set(Vector.add(p2, dirToNewP3));
doodler.line(p2, Vector.add(p2, dirToNewP3), { color: "blue" });
doodler.line(p2, Vector.add(p2, dirToMouse.mult(100)), {
color: "red",
});
}
{
const dirToMouse = Vector.sub(mousePos, p3).normalize();
dirToMouse.setMag(distToP4);
p4.set(Vector.add(p3, dirToMouse));
doodler.line(p3, Vector.add(p3, dirToMouse), { color: "green" });
}
segment.clampLength();
}
doodler.fillText(
segment.calculateApproxLength().toFixed(2),
p2.copy().add(10, 0),
100,
);
} }
track.draw(true); track.draw(true);
@ -50,14 +122,36 @@ export class EditTrackState extends State<States> {
inputManager.onKey("Escape", () => { inputManager.onKey("Escape", () => {
const trackCopy = getContextItem<TrackSystem>("trackCopy"); const trackCopy = getContextItem<TrackSystem>("trackCopy");
setContextItem("track", trackCopy); setContextItem("track", trackCopy);
setContextItem("trackCopy", undefined);
const state = getContextItem<StateMachine<States>>("state"); const state = getContextItem<StateMachine<States>>("state");
state.transitionTo(States.RUNNING); state.transitionTo(States.RUNNING);
}); });
inputManager.onKey("w", () => {
const track = getContextItem<TrackSystem>("track");
const segment = track.lastSegment;
if (!segment) return;
const n = new StraightTrack(segment.points[3]);
const t = segment.tangent(1).heading();
n.rotate(t);
segment.frontNeighbours.push(n);
track.registerSegment(n);
this.currentSegment = n;
});
inputManager.onKey("1", () => {
this.currentSegment = track.firstSegment;
});
this.currentSegment = track.lastSegment;
// TODO // TODO
// Cache trains and save // Cache trains and save
// Stash track in context
} }
override stop(): void { override stop(): void {
const inputManager = getContextItem<InputManager>("inputManager");
inputManager.offKey("e");
inputManager.offKey("Escape");
if (this.heldEvents.size > 0) { if (this.heldEvents.size > 0) {
for (const [key, cb] of this.heldEvents) { for (const [key, cb] of this.heldEvents) {
if (cb) { if (cb) {
@ -66,5 +160,6 @@ export class EditTrackState extends State<States> {
this.heldEvents.delete(key); this.heldEvents.delete(key);
} }
} }
setContextItem("trackCopy", undefined);
} }
} }

View File

@ -17,6 +17,15 @@ export class TrackSystem {
return this.segments.values().next().value; return this.segments.values().next().value;
} }
get lastSegment() {
return this.segments.values().toArray().pop();
}
registerSegment(segment: TrackSegment) {
segment.setTrack(this);
this.segments.set(segment.id, segment);
}
draw(showControls = false) { draw(showControls = false) {
for (const segment of this.segments.values()) { for (const segment of this.segments.values()) {
segment.draw(showControls); segment.draw(showControls);
@ -168,6 +177,54 @@ export class TrackSegment extends PathSegment {
); );
} }
propagate() {
const [_, __, p3, p4] = this.points;
const tangent = Vector.sub(p4, p3);
for (const fNeighbour of this.frontNeighbours) {
fNeighbour.receivePropagation(tangent);
}
}
lastHeading?: number;
receivePropagation(tangent: Vector) {
const [p1, p2, p3, p4] = this.points;
// const angle = tangent.heading() - (this.lastHeading ?? 0);
// this.lastHeading = tangent.heading();
// const newP2 = Vector.add(p1, tangent);
// const p2ToP3 = Vector.sub(p3, p2);
// p2ToP3.rotate(angle);
// p3.set(Vector.add(newP2, p2ToP3));
// const p2Top4 = Vector.sub(p4, p2);
// p2Top4.rotate(angle);
// p4.set(Vector.add(newP2, p2Top4));
// p2.set(newP2);
this.rotate(tangent);
this.propagate();
}
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
rotate(angle: number | Vector) {
const [p1, p2, p3, p4] = this.points;
let newP2;
if (angle instanceof Vector) {
const tan = angle;
angle = tan.heading() - (this.lastHeading ?? 0);
this.lastHeading = tan.heading();
newP2 = Vector.add(p1, tan);
} else {
const p1ToP2 = Vector.sub(p2, p1);
p1ToP2.rotate(angle);
newP2 = Vector.add(p1, p1ToP2);
}
const p2ToP3 = Vector.sub(p3, p2);
p2ToP3.rotate(angle);
p3.set(Vector.add(newP2, p2ToP3));
const p2Top4 = Vector.sub(p4, p2);
p2Top4.rotate(angle);
p4.set(Vector.add(newP2, p2Top4));
p2.set(newP2);
}
static deserialize(data: any) { static deserialize(data: any) {
return new TrackSegment( return new TrackSegment(
data.p.map((p: [number, number, number]) => new Vector(p[0], p[1], p[2])), data.p.map((p: [number, number, number]) => new Vector(p[0], p[1], p[2])),