Very rudimentary track editor... and headache spaghetti
This commit is contained in:
parent
791ba42ceb
commit
8dc0af650f
471
bundle.js
471
bundle.js
@ -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,
|
||||||
|
20
lib/input.ts
20
lib/input.ts
@ -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;
|
||||||
}
|
}
|
||||||
|
2
main.ts
2
main.ts
@ -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,
|
||||||
|
48
math/path.ts
48
math/path.ts
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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])),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user