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);
|
||||
}
|
||||
|
||||
// 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
|
||||
var Constants = {
|
||||
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
|
||||
var ResourceManager = class {
|
||||
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
|
||||
var PathSegment = class {
|
||||
points;
|
||||
length;
|
||||
startingLength;
|
||||
constructor(points) {
|
||||
this.points = points;
|
||||
this.length = this.calculateApproxLength(100);
|
||||
this.startingLength = Math.round(this.length);
|
||||
}
|
||||
getPointAtT(t) {
|
||||
const [a, b, c, d] = this.points;
|
||||
@ -1401,7 +1319,7 @@
|
||||
}, { prev: void 0, length: 0 }).length;
|
||||
return this.length;
|
||||
}
|
||||
calculateEvenlySpacedPoints(spacing, resolution = 1) {
|
||||
calculateEvenlySpacedPoints(spacing, resolution = 1, targetLength) {
|
||||
const points = [];
|
||||
points.push(this.points[0]);
|
||||
let prev = points[0];
|
||||
@ -1424,8 +1342,41 @@
|
||||
}
|
||||
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;
|
||||
}
|
||||
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
|
||||
@ -1441,6 +1392,13 @@
|
||||
get firstSegment() {
|
||||
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) {
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.draw(showControls);
|
||||
@ -1567,6 +1525,41 @@
|
||||
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) {
|
||||
return new _TrackSegment(
|
||||
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
|
||||
var LoadState = class extends State {
|
||||
name = 0 /* LOAD */;
|
||||
@ -1641,6 +1789,7 @@
|
||||
fillScreen: true,
|
||||
bg: "#302040"
|
||||
});
|
||||
doodler.scale = doodler.maxScale;
|
||||
setDefaultContext({
|
||||
inputManager,
|
||||
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 {
|
||||
private keyStates: 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);
|
||||
}
|
||||
getMouseLocation() {
|
||||
if (getContextItem("doodler") instanceof ZoomableDoodler) {
|
||||
return getContextItem<ZoomableDoodler>("doodler").screenToWorld(
|
||||
this.mouseLocation.x,
|
||||
this.mouseLocation.y,
|
||||
);
|
||||
}
|
||||
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() {
|
||||
return this.mouseDelta;
|
||||
}
|
||||
|
2
main.ts
2
main.ts
@ -19,6 +19,8 @@ const doodler = new ZoomableDoodler({
|
||||
fillScreen: true,
|
||||
bg: "#302040",
|
||||
});
|
||||
// doodler.minScale = 0.1;
|
||||
(doodler as any).scale = doodler.maxScale;
|
||||
|
||||
setDefaultContext({
|
||||
inputManager,
|
||||
|
48
math/path.ts
48
math/path.ts
@ -41,10 +41,12 @@ export class PathSegment {
|
||||
points: [Vector, Vector, Vector, Vector];
|
||||
|
||||
length: number;
|
||||
startingLength: number;
|
||||
|
||||
constructor(points: [Vector, Vector, Vector, Vector]) {
|
||||
this.points = points;
|
||||
this.length = this.calculateApproxLength(100);
|
||||
this.startingLength = Math.round(this.length);
|
||||
}
|
||||
|
||||
getPointAtT(t: number) {
|
||||
@ -177,7 +179,11 @@ export class PathSegment {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
calculateEvenlySpacedPoints(spacing: number, resolution = 1) {
|
||||
calculateEvenlySpacedPoints(
|
||||
spacing: number,
|
||||
resolution = 1,
|
||||
targetLength?: number,
|
||||
) {
|
||||
const points: Vector[] = [];
|
||||
|
||||
points.push(this.points[0]);
|
||||
@ -206,6 +212,46 @@ export class PathSegment {
|
||||
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;
|
||||
}
|
||||
|
||||
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 { InputManager } from "../../lib/input.ts";
|
||||
import { TrackSystem } from "../../track/system.ts";
|
||||
import { State, StateMachine } from "../machine.ts";
|
||||
import { States } from "./index.ts";
|
||||
import { StraightTrack } from "../../track/shapes.ts";
|
||||
import { TrackSegment } from "../../track/system.ts";
|
||||
|
||||
export class EditTrackState extends State<States> {
|
||||
override name: States = States.EDIT_TRACK;
|
||||
@ -15,19 +17,89 @@ export class EditTrackState extends State<States> {
|
||||
private heldEvents: Map<string | number, (() => void) | undefined> =
|
||||
new Map();
|
||||
|
||||
private currentSegment?: TrackSegment;
|
||||
|
||||
override update(dt: number): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
|
||||
const firstSegment = track.firstSegment;
|
||||
if (firstSegment) {
|
||||
const firstPoint = firstSegment.points[0].copy();
|
||||
const { x, y } = inputManager.getMouseLocation();
|
||||
firstSegment.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, firstPoint);
|
||||
p.set(x, y);
|
||||
p.add(relativePoint);
|
||||
});
|
||||
// For moving a segment, i.e. the currently active one
|
||||
// const segment = track.lastSegment;
|
||||
// if (segment) {
|
||||
// const firstPoint = segment.points[0].copy();
|
||||
// const { x, y } = inputManager.getMouseLocation();
|
||||
// segment.points.forEach((p, i) => {
|
||||
// const relativePoint = Vector.sub(p, firstPoint);
|
||||
// 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);
|
||||
@ -50,14 +122,36 @@ export class EditTrackState extends State<States> {
|
||||
inputManager.onKey("Escape", () => {
|
||||
const trackCopy = getContextItem<TrackSystem>("trackCopy");
|
||||
setContextItem("track", trackCopy);
|
||||
setContextItem("trackCopy", undefined);
|
||||
const state = getContextItem<StateMachine<States>>("state");
|
||||
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
|
||||
// Cache trains and save
|
||||
// Stash track in context
|
||||
}
|
||||
override stop(): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
inputManager.offKey("e");
|
||||
inputManager.offKey("Escape");
|
||||
if (this.heldEvents.size > 0) {
|
||||
for (const [key, cb] of this.heldEvents) {
|
||||
if (cb) {
|
||||
@ -66,5 +160,6 @@ export class EditTrackState extends State<States> {
|
||||
this.heldEvents.delete(key);
|
||||
}
|
||||
}
|
||||
setContextItem("trackCopy", undefined);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,15 @@ export class TrackSystem {
|
||||
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) {
|
||||
for (const segment of this.segments.values()) {
|
||||
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) {
|
||||
return new TrackSegment(
|
||||
data.p.map((p: [number, number, number]) => new Vector(p[0], p[1], p[2])),
|
||||
|
Loading…
x
Reference in New Issue
Block a user