pick and place editing working, saving and loading working
This commit is contained in:
parent
8dc0af650f
commit
3d4596f8fb
461
bundle.js
461
bundle.js
@ -24,6 +24,13 @@
|
||||
function getContextItem(prop) {
|
||||
return ctx[prop];
|
||||
}
|
||||
function getContextItemOrDefault(prop, defaultValue) {
|
||||
try {
|
||||
return ctx[prop];
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
function setContextItem(prop, value) {
|
||||
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
|
||||
[prop]: value
|
||||
@ -71,12 +78,12 @@
|
||||
}, 2);
|
||||
}
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.3/geometry/constants.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.4/geometry/constants.ts
|
||||
var Constants = {
|
||||
TWO_PI: Math.PI * 2
|
||||
};
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.3/geometry/vector.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.4/geometry/vector.ts
|
||||
var Vector = class _Vector {
|
||||
x;
|
||||
y;
|
||||
@ -361,7 +368,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.3/FPSCounter.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.4/FPSCounter.ts
|
||||
var FPSCounter = class {
|
||||
frameTimes = [];
|
||||
maxSamples;
|
||||
@ -389,7 +396,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.3/canvas.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.4/canvas.ts
|
||||
var Doodler = class {
|
||||
ctx;
|
||||
_canvas;
|
||||
@ -743,13 +750,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.3/timing/EaseInOut.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.4/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.3/timing/Map.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.4/timing/Map.ts
|
||||
var map = (value, x1, y1, x2, y2) => (value - x1) * (y2 - x2) / (y1 - x1) + x2;
|
||||
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.3/zoomableCanvas.ts
|
||||
// https://jsr.io/@bearmetal/doodler/0.0.4/zoomableCanvas.ts
|
||||
var ZoomableDoodler = class extends Doodler {
|
||||
scale = 1;
|
||||
dragging = false;
|
||||
@ -1014,6 +1021,16 @@
|
||||
};
|
||||
|
||||
// lib/input.ts
|
||||
function mouseButtonToString(button) {
|
||||
switch (button) {
|
||||
case 0:
|
||||
return "left";
|
||||
case 1:
|
||||
return "middle";
|
||||
case 2:
|
||||
return "right";
|
||||
}
|
||||
}
|
||||
var InputManager = class {
|
||||
keyStates = /* @__PURE__ */ new Map();
|
||||
mouseStates = /* @__PURE__ */ new Map();
|
||||
@ -1030,11 +1047,15 @@
|
||||
this.keyStates.set(e.key, false);
|
||||
});
|
||||
document.addEventListener("mousedown", (e) => {
|
||||
this.mouseStates.set(e.button, true);
|
||||
this.mouseEvents.get(e.button)?.call(e);
|
||||
const button = mouseButtonToString(e.button);
|
||||
if (!button) throw "Mouse button not found: " + e.button;
|
||||
this.mouseStates.set(button, true);
|
||||
this.mouseEvents.get(button)?.call(e);
|
||||
});
|
||||
document.addEventListener("mouseup", (e) => {
|
||||
this.mouseStates.set(e.button, false);
|
||||
const button = mouseButtonToString(e.button);
|
||||
if (!button) throw "Mouse button not found: " + e.button;
|
||||
this.mouseStates.set(button, false);
|
||||
});
|
||||
self.addEventListener("mousemove", (e) => {
|
||||
this.mouseLocation = { x: e.clientX, y: e.clientY };
|
||||
@ -1087,6 +1108,11 @@
|
||||
offMouse(key) {
|
||||
this.mouseEvents.delete(key);
|
||||
}
|
||||
onNumberKey(arg0) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.onKey(i.toString(), () => arg0(i));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// lib/resources.ts
|
||||
@ -1125,27 +1151,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
// ui/button.ts
|
||||
function addButton(props) {
|
||||
const doodler2 = getContextItem("doodler");
|
||||
const { text, onClick, style } = props;
|
||||
const { x, y } = props.at[1].copy().sub(props.at[0]);
|
||||
const id = doodler2.addUIElement(
|
||||
"rectangle",
|
||||
props.at[0],
|
||||
x,
|
||||
y,
|
||||
style
|
||||
);
|
||||
doodler2.registerClickable(props.at[0], props.at[1], onClick);
|
||||
return {
|
||||
id,
|
||||
text,
|
||||
onClick,
|
||||
style
|
||||
};
|
||||
}
|
||||
|
||||
// state/machine.ts
|
||||
var StateMachine = class {
|
||||
_states = /* @__PURE__ */ new Map();
|
||||
@ -1399,53 +1404,46 @@
|
||||
segment.setTrack(this);
|
||||
this.segments.set(segment.id, segment);
|
||||
}
|
||||
unregisterSegment(segment) {
|
||||
this.segments.delete(segment.id);
|
||||
for (const s of this.segments.values()) {
|
||||
s.backNeighbours = s.backNeighbours.filter((n) => n !== segment);
|
||||
s.frontNeighbours = s.frontNeighbours.filter((n) => n !== segment);
|
||||
}
|
||||
}
|
||||
draw(showControls = false) {
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.draw(showControls);
|
||||
}
|
||||
try {
|
||||
if (getContextItem("showEnds")) {
|
||||
const ends = this.findEnds();
|
||||
for (const end of ends) {
|
||||
this.doodler.fillCircle(end.pos, 2, {
|
||||
color: "red"
|
||||
// weight: 3,
|
||||
});
|
||||
if (getContextItem("debug")) {
|
||||
this.doodler.line(
|
||||
end.pos,
|
||||
end.pos.copy().add(end.tangent.copy().mult(20)),
|
||||
{
|
||||
color: "blue"
|
||||
// weight: 3,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setDefaultContext({ showEnds: false });
|
||||
}
|
||||
}
|
||||
ends = /* @__PURE__ */ new Map();
|
||||
endArray = [];
|
||||
findEnds() {
|
||||
const ends = [];
|
||||
for (const segment of this.segments.values()) {
|
||||
const [a, b, c, d] = segment.points;
|
||||
if (this.ends.has(segment)) continue;
|
||||
const ends = [
|
||||
{
|
||||
const tangent = Vector.sub(a, b).normalize();
|
||||
const pos = a.copy();
|
||||
ends.push({ pos, segment, tangent });
|
||||
}
|
||||
pos: segment.points[0],
|
||||
segment,
|
||||
tangent: Vector.sub(segment.points[1], segment.points[0]).normalize(),
|
||||
frontOrBack: "back"
|
||||
},
|
||||
{
|
||||
const tangent = Vector.sub(d, c).normalize();
|
||||
const pos = d.copy();
|
||||
ends.push({ pos, segment, tangent });
|
||||
pos: segment.points[3],
|
||||
segment,
|
||||
tangent: Vector.sub(segment.points[3], segment.points[2]).normalize(),
|
||||
frontOrBack: "front"
|
||||
}
|
||||
];
|
||||
this.ends.set(segment, ends);
|
||||
this.endArray.push(...ends);
|
||||
}
|
||||
return ends;
|
||||
return this.endArray;
|
||||
}
|
||||
serialize() {
|
||||
return this.segments.values().map((s) => s.serialize()).toArray();
|
||||
return JSON.stringify(
|
||||
this.segments.values().map((s) => s.serialize()).toArray()
|
||||
);
|
||||
}
|
||||
copy() {
|
||||
const track = new _TrackSystem([]);
|
||||
@ -1475,6 +1473,11 @@
|
||||
}
|
||||
return track;
|
||||
}
|
||||
translate(v) {
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.translate(v);
|
||||
}
|
||||
}
|
||||
};
|
||||
var TrackSegment = class _TrackSegment extends PathSegment {
|
||||
frontNeighbours = [];
|
||||
@ -1501,13 +1504,17 @@
|
||||
}
|
||||
);
|
||||
if (showControls) {
|
||||
this.doodler.drawCircle(this.points[1], 4, {
|
||||
color: "red",
|
||||
weight: 3
|
||||
this.doodler.fillCircle(this.points[0], 1, {
|
||||
color: "red"
|
||||
});
|
||||
this.doodler.drawCircle(this.points[2], 4, {
|
||||
color: "red",
|
||||
weight: 3
|
||||
this.doodler.fillCircle(this.points[1], 1, {
|
||||
color: "red"
|
||||
});
|
||||
this.doodler.fillCircle(this.points[2], 1, {
|
||||
color: "red"
|
||||
});
|
||||
this.doodler.fillCircle(this.points[3], 1, {
|
||||
color: "red"
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1525,18 +1532,23 @@
|
||||
this.id
|
||||
);
|
||||
}
|
||||
propagate() {
|
||||
const [_, __, p3, p4] = this.points;
|
||||
const tangent = Vector.sub(p4, p3);
|
||||
cleanCopy() {
|
||||
return new _TrackSegment(
|
||||
this.points.map((p) => p.copy())
|
||||
);
|
||||
}
|
||||
propagateTranslation(v) {
|
||||
for (const fNeighbour of this.frontNeighbours) {
|
||||
fNeighbour.receivePropagation(tangent);
|
||||
fNeighbour.receivePropagation(v);
|
||||
}
|
||||
for (const bNeighbour of this.backNeighbours) {
|
||||
bNeighbour.receivePropagation(v);
|
||||
}
|
||||
}
|
||||
lastHeading;
|
||||
receivePropagation(tangent) {
|
||||
const [p1, p2, p3, p4] = this.points;
|
||||
this.rotate(tangent);
|
||||
this.propagate();
|
||||
receivePropagation(v) {
|
||||
this.translate(v);
|
||||
this.propagateTranslation(v);
|
||||
}
|
||||
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
|
||||
rotate(angle) {
|
||||
@ -1566,6 +1578,33 @@
|
||||
data.id
|
||||
);
|
||||
}
|
||||
setPositionByPoint(pos, point) {
|
||||
if (!this.points.includes(point)) return;
|
||||
point = point.copy();
|
||||
this.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, point);
|
||||
p.set(pos);
|
||||
p.add(relativePoint);
|
||||
});
|
||||
}
|
||||
rotateAboutPoint(angle, point) {
|
||||
if (!this.points.includes(point)) return;
|
||||
point = point.copy();
|
||||
this.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, point);
|
||||
relativePoint.rotate(angle);
|
||||
p.set(Vector.add(point, relativePoint));
|
||||
});
|
||||
}
|
||||
// resetRotation() {
|
||||
// const angle = this.tangent(0).heading();
|
||||
// this.rotateAboutPoint(-angle, this.points[0]);
|
||||
// }
|
||||
translate(v) {
|
||||
this.points.forEach((p) => {
|
||||
p.add(v.x, v.y);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// track/shapes.ts
|
||||
@ -1580,6 +1619,38 @@
|
||||
]);
|
||||
}
|
||||
};
|
||||
var SBendLeft = class extends StraightTrack {
|
||||
constructor(start) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, -25);
|
||||
this.points[3].add(0, -25);
|
||||
}
|
||||
};
|
||||
var SBendRight = class extends StraightTrack {
|
||||
constructor(start) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, 25);
|
||||
this.points[3].add(0, 25);
|
||||
}
|
||||
};
|
||||
var BankLeft = class extends StraightTrack {
|
||||
constructor(start) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, -25);
|
||||
this.points[3].add(0, 25);
|
||||
}
|
||||
};
|
||||
var BankRight = class extends StraightTrack {
|
||||
constructor(start) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, 25);
|
||||
this.points[3].add(0, -25);
|
||||
}
|
||||
};
|
||||
|
||||
// state/states/EditTrackState.ts
|
||||
var EditTrackState = class extends State {
|
||||
@ -1590,55 +1661,132 @@
|
||||
]);
|
||||
heldEvents = /* @__PURE__ */ new Map();
|
||||
currentSegment;
|
||||
selectedSegment;
|
||||
ghostSegment;
|
||||
ghostRotated = false;
|
||||
closestEnd;
|
||||
update(dt) {
|
||||
const inputManager2 = getContextItem("inputManager");
|
||||
const track = getContextItem("track");
|
||||
const doodler2 = getContextItem("doodler");
|
||||
const segment = this.currentSegment;
|
||||
if (segment) {
|
||||
segment.propagate();
|
||||
if (this.selectedSegment) {
|
||||
const segment = this.selectedSegment;
|
||||
const firstPoint = segment.points[0].copy();
|
||||
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
|
||||
segment.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, firstPoint);
|
||||
p.set(mousePos);
|
||||
p.add(relativePoint);
|
||||
});
|
||||
const ends = track.findEnds();
|
||||
setContextItem("showEnds", true);
|
||||
const nearbyEnds = ends.filter((end) => {
|
||||
const dist = Vector.dist(end.pos, mousePos);
|
||||
return dist < 20 && end.segment !== segment;
|
||||
});
|
||||
let closestEnd = nearbyEnds[0];
|
||||
for (const end of nearbyEnds) {
|
||||
if (end === closestEnd) continue;
|
||||
const closestEndTangent = Vector.add(
|
||||
closestEnd.tangent.copy().mult(20),
|
||||
closestEnd.pos
|
||||
);
|
||||
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 endTangent = Vector.add(
|
||||
end.tangent.copy().rotate(Math.PI).mult(20),
|
||||
end.pos
|
||||
);
|
||||
doodler2.drawCircle(closestEndTangent, 4, { color: "red", weight: 1 });
|
||||
doodler2.drawCircle(endTangent, 4, { color: "blue", weight: 1 });
|
||||
if (endTangent.dist(mousePos) < closestEndTangent.dist(mousePos) || end.pos.dist(mousePos) < closestEnd.pos.dist(mousePos)) {
|
||||
closestEnd = end;
|
||||
}
|
||||
}
|
||||
if (closestEnd !== this.closestEnd) {
|
||||
this.closestEnd = closestEnd;
|
||||
this.ghostSegment = void 0;
|
||||
this.ghostRotated = false;
|
||||
}
|
||||
if (closestEnd) {
|
||||
doodler2.line(
|
||||
closestEnd.pos,
|
||||
Vector.add(closestEnd.pos, closestEnd.tangent.copy().mult(20)),
|
||||
{ color: "green" }
|
||||
);
|
||||
}
|
||||
if (this.closestEnd) {
|
||||
if (!this.ghostSegment) {
|
||||
this.ghostSegment = segment.copy();
|
||||
this.ghostRotated = false;
|
||||
}
|
||||
switch (this.closestEnd.frontOrBack) {
|
||||
case "front":
|
||||
this.ghostSegment.setPositionByPoint(
|
||||
this.closestEnd.pos,
|
||||
this.ghostSegment.points[0]
|
||||
);
|
||||
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
|
||||
this.closestEnd.tangent.heading(),
|
||||
this.ghostSegment.points[0]
|
||||
);
|
||||
this.ghostRotated = true;
|
||||
break;
|
||||
case "back":
|
||||
this.ghostSegment.setPositionByPoint(
|
||||
this.closestEnd.pos,
|
||||
this.ghostSegment.points[3]
|
||||
);
|
||||
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
|
||||
this.closestEnd.tangent.heading(),
|
||||
this.ghostSegment.points[3]
|
||||
);
|
||||
this.ghostRotated = true;
|
||||
break;
|
||||
}
|
||||
} else if (!this.closestEnd || !closestEnd) {
|
||||
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 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();
|
||||
const translation = new Vector(0, 0);
|
||||
if (inputManager2.getKeyState("ArrowUp")) {
|
||||
translation.y -= 1;
|
||||
}
|
||||
doodler2.fillText(
|
||||
segment.calculateApproxLength().toFixed(2),
|
||||
p2.copy().add(10, 0),
|
||||
100
|
||||
);
|
||||
if (inputManager2.getKeyState("ArrowDown")) {
|
||||
translation.y += 1;
|
||||
}
|
||||
if (inputManager2.getKeyState("ArrowLeft")) {
|
||||
translation.x -= 1;
|
||||
}
|
||||
if (inputManager2.getKeyState("ArrowRight")) {
|
||||
translation.x += 1;
|
||||
}
|
||||
if (translation.x !== 0 || translation.y !== 0) {
|
||||
track.translate(translation);
|
||||
}
|
||||
track.draw(true);
|
||||
}
|
||||
start() {
|
||||
setContextItem("trackSegments", [
|
||||
void 0,
|
||||
new StraightTrack(),
|
||||
new SBendLeft(),
|
||||
new SBendRight(),
|
||||
new BankLeft(),
|
||||
new BankRight()
|
||||
]);
|
||||
const inputManager2 = getContextItem("inputManager");
|
||||
this.heldEvents.set("e", inputManager2.offKey("e"));
|
||||
this.heldEvents.set("Escape", inputManager2.offKey("Escape"));
|
||||
@ -1655,25 +1803,50 @@
|
||||
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(" ", () => {
|
||||
if (this.selectedSegment) {
|
||||
this.selectedSegment = void 0;
|
||||
} else {
|
||||
this.selectedSegment = new StraightTrack();
|
||||
}
|
||||
});
|
||||
inputManager2.onKey("1", () => {
|
||||
this.currentSegment = track.firstSegment;
|
||||
inputManager2.onMouse("left", () => {
|
||||
const track2 = getContextItem("track");
|
||||
if (this.ghostSegment && this.closestEnd) {
|
||||
const segment = this.ghostSegment.cleanCopy();
|
||||
switch (this.closestEnd.frontOrBack) {
|
||||
case "front":
|
||||
this.closestEnd.segment.frontNeighbours.push(segment);
|
||||
segment.backNeighbours.push(this.closestEnd.segment);
|
||||
break;
|
||||
case "back":
|
||||
this.closestEnd.segment.backNeighbours.push(segment);
|
||||
segment.frontNeighbours.push(this.closestEnd.segment);
|
||||
break;
|
||||
}
|
||||
track2.registerSegment(segment);
|
||||
this.ghostSegment = void 0;
|
||||
this.closestEnd = void 0;
|
||||
} else if (this.selectedSegment) {
|
||||
track2.registerSegment(this.selectedSegment);
|
||||
this.selectedSegment = new StraightTrack();
|
||||
} else {
|
||||
this.selectedSegment = void 0;
|
||||
}
|
||||
});
|
||||
inputManager2.onNumberKey((i) => {
|
||||
console.log(i);
|
||||
const segments = getContextItem("trackSegments");
|
||||
this.selectedSegment = segments[i];
|
||||
this.ghostRotated = false;
|
||||
this.ghostSegment = void 0;
|
||||
});
|
||||
this.currentSegment = track.lastSegment;
|
||||
}
|
||||
stop() {
|
||||
const inputManager2 = getContextItem("inputManager");
|
||||
inputManager2.offKey("e");
|
||||
inputManager2.offKey("w");
|
||||
inputManager2.offKey("Escape");
|
||||
if (this.heldEvents.size > 0) {
|
||||
for (const [key, cb] of this.heldEvents) {
|
||||
@ -1684,6 +1857,7 @@
|
||||
}
|
||||
}
|
||||
setContextItem("trackCopy", void 0);
|
||||
setContextItem("trackSegments", void 0);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1790,31 +1964,36 @@
|
||||
bg: "#302040"
|
||||
});
|
||||
doodler.scale = doodler.maxScale;
|
||||
var colors = [
|
||||
"red",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"blue",
|
||||
"indigo",
|
||||
"purple",
|
||||
"violet"
|
||||
];
|
||||
setDefaultContext({
|
||||
inputManager,
|
||||
doodler,
|
||||
resources,
|
||||
debug: true,
|
||||
showEnds: true
|
||||
showEnds: true,
|
||||
colors
|
||||
});
|
||||
var state = bootstrapGameStateMachine();
|
||||
setContextItem("state", state);
|
||||
doodler.init();
|
||||
addButton({
|
||||
text: "Hello World!",
|
||||
onClick: () => {
|
||||
console.log("Hello World!");
|
||||
},
|
||||
at: [
|
||||
new Vector(10, doodler.height - 50),
|
||||
new Vector(110, doodler.height - 10)
|
||||
],
|
||||
style: {
|
||||
fillColor: "blue",
|
||||
color: "white"
|
||||
}
|
||||
});
|
||||
doodler.createLayer((_, __, dTime) => {
|
||||
state.update(dTime);
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
const track = getContextItem("track");
|
||||
localStorage.setItem("track", track.serialize());
|
||||
console.log("Saved track to local storage");
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
@ -13,6 +13,6 @@
|
||||
"dev": "deno run -RWEN --allow-run dev.ts dev"
|
||||
},
|
||||
"imports": {
|
||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@^0.0.3"
|
||||
"@bearmetal/doodler": "jsr:@bearmetal/doodler@^0.0.4"
|
||||
}
|
||||
}
|
9
deno.lock
generated
9
deno.lock
generated
@ -1,7 +1,8 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@bearmetal/doodler@^0.0.3": "0.0.3",
|
||||
"jsr:@bearmetal/doodler@*": "0.0.4",
|
||||
"jsr:@bearmetal/doodler@^0.0.4": "0.0.4",
|
||||
"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",
|
||||
@ -22,8 +23,8 @@
|
||||
"npm:esbuild@*": "0.24.2"
|
||||
},
|
||||
"jsr": {
|
||||
"@bearmetal/doodler@0.0.3": {
|
||||
"integrity": "42c04b672f4a6bc7ebd45ad936197a2e32856364b66a9a9fe2b81a4aa45c7a08"
|
||||
"@bearmetal/doodler@0.0.4": {
|
||||
"integrity": "b631083cff84994c513f70d1f09e6a9256edabcb224112c93a9ca6a87c88a389"
|
||||
},
|
||||
"@luca/esbuild-deno-loader@0.11.0": {
|
||||
"integrity": "c05a989aa7c4ee6992a27be5f15cfc5be12834cab7ff84cabb47313737c51a2c",
|
||||
@ -229,7 +230,7 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@bearmetal/doodler@^0.0.3"
|
||||
"jsr:@bearmetal/doodler@^0.0.4"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,13 @@ export function getContext() {
|
||||
export function getContextItem<T>(prop: string): T {
|
||||
return ctx[prop] as T;
|
||||
}
|
||||
export function getContextItemOrDefault<T>(prop: string, defaultValue: T): T {
|
||||
try {
|
||||
return ctx[prop] as T;
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
export function setContextItem<T>(prop: string, value: T) {
|
||||
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
|
||||
[prop]: value,
|
||||
|
58
lib/input.ts
58
lib/input.ts
@ -1,14 +1,36 @@
|
||||
import { Vector, ZoomableDoodler } from "@bearmetal/doodler";
|
||||
import { getContextItem } from "./context.ts";
|
||||
|
||||
function mouseButtonToString(button: number) {
|
||||
switch (button) {
|
||||
case 0:
|
||||
return "left";
|
||||
case 1:
|
||||
return "middle";
|
||||
case 2:
|
||||
return "right";
|
||||
}
|
||||
}
|
||||
|
||||
function mouseButtonToNumber(button: string) {
|
||||
switch (button) {
|
||||
case "left":
|
||||
return 0;
|
||||
case "middle":
|
||||
return 1;
|
||||
case "right":
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
export class InputManager {
|
||||
private keyStates: Map<string | number, boolean> = new Map();
|
||||
private mouseStates: Map<string | number, boolean> = new Map();
|
||||
private keyStates: Map<string, boolean> = new Map();
|
||||
private mouseStates: Map<string, boolean> = new Map();
|
||||
private mouseLocation: { x: number; y: number } = { x: 0, y: 0 };
|
||||
private mouseDelta: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
private keyEvents: Map<string | number, () => void> = new Map();
|
||||
private mouseEvents: Map<string | number, () => void> = new Map();
|
||||
private keyEvents: Map<string, () => void> = new Map();
|
||||
private mouseEvents: Map<string, () => void> = new Map();
|
||||
|
||||
constructor() {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
@ -19,11 +41,15 @@ export class InputManager {
|
||||
this.keyStates.set(e.key, false);
|
||||
});
|
||||
document.addEventListener("mousedown", (e) => {
|
||||
this.mouseStates.set(e.button, true);
|
||||
this.mouseEvents.get(e.button)?.call(e);
|
||||
const button = mouseButtonToString(e.button);
|
||||
if (!button) throw "Mouse button not found: " + e.button;
|
||||
this.mouseStates.set(button, true);
|
||||
this.mouseEvents.get(button)?.call(e);
|
||||
});
|
||||
document.addEventListener("mouseup", (e) => {
|
||||
this.mouseStates.set(e.button, false);
|
||||
const button = mouseButtonToString(e.button);
|
||||
if (!button) throw "Mouse button not found: " + e.button;
|
||||
this.mouseStates.set(button, false);
|
||||
});
|
||||
|
||||
self.addEventListener("mousemove", (e) => {
|
||||
@ -35,10 +61,10 @@ export class InputManager {
|
||||
});
|
||||
}
|
||||
|
||||
getKeyState(key: string | number) {
|
||||
getKeyState(key: string) {
|
||||
return this.keyStates.get(key);
|
||||
}
|
||||
getMouseState(key: string | number) {
|
||||
getMouseState(key: string) {
|
||||
return this.mouseStates.get(key);
|
||||
}
|
||||
getMouseLocation() {
|
||||
@ -65,19 +91,25 @@ export class InputManager {
|
||||
return this.mouseDelta;
|
||||
}
|
||||
|
||||
onKey(key: string | number, cb: () => void) {
|
||||
onKey(key: string, cb: () => void) {
|
||||
this.keyEvents.set(key, cb);
|
||||
}
|
||||
onMouse(key: string | number, cb: () => void) {
|
||||
onMouse(key: string, cb: () => void) {
|
||||
this.mouseEvents.set(key, cb);
|
||||
}
|
||||
|
||||
offKey(key: string | number) {
|
||||
offKey(key: string) {
|
||||
const events = this.keyEvents.get(key);
|
||||
this.keyEvents.delete(key);
|
||||
return events;
|
||||
}
|
||||
offMouse(key: string | number) {
|
||||
offMouse(key: string) {
|
||||
this.mouseEvents.delete(key);
|
||||
}
|
||||
|
||||
onNumberKey(arg0: (arg: number) => void) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.onKey(i.toString(), () => arg0(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
36
main.ts
36
main.ts
@ -1,5 +1,6 @@
|
||||
import {
|
||||
getContext,
|
||||
getContextItem,
|
||||
setContextItem,
|
||||
setDefaultContext,
|
||||
} from "./lib/context.ts";
|
||||
@ -22,33 +23,40 @@ const doodler = new ZoomableDoodler({
|
||||
// doodler.minScale = 0.1;
|
||||
(doodler as any).scale = doodler.maxScale;
|
||||
|
||||
const colors = [
|
||||
"red",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"blue",
|
||||
"indigo",
|
||||
"purple",
|
||||
"violet",
|
||||
];
|
||||
|
||||
setDefaultContext({
|
||||
inputManager,
|
||||
doodler,
|
||||
resources,
|
||||
debug: true,
|
||||
showEnds: true,
|
||||
colors,
|
||||
});
|
||||
|
||||
const state = bootstrapGameStateMachine();
|
||||
setContextItem("state", state);
|
||||
|
||||
doodler.init();
|
||||
addButton({
|
||||
text: "Hello World!",
|
||||
onClick: () => {
|
||||
console.log("Hello World!");
|
||||
},
|
||||
at: [
|
||||
new Vector(10, doodler.height - 50),
|
||||
new Vector(110, doodler.height - 10),
|
||||
],
|
||||
style: {
|
||||
fillColor: "blue",
|
||||
color: "white",
|
||||
},
|
||||
});
|
||||
|
||||
doodler.createLayer((_, __, dTime) => {
|
||||
state.update(dTime);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||
e.preventDefault();
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
localStorage.setItem("track", track.serialize());
|
||||
console.log("Saved track to local storage");
|
||||
}
|
||||
});
|
||||
|
3
math/clamp.ts
Normal file
3
math/clamp.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
@ -1,11 +1,22 @@
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
import { getContextItem, setContextItem } from "../../lib/context.ts";
|
||||
import {
|
||||
getContextItem,
|
||||
getContextItemOrDefault,
|
||||
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 {
|
||||
BankLeft,
|
||||
BankRight,
|
||||
SBendLeft,
|
||||
SBendRight,
|
||||
StraightTrack,
|
||||
} from "../../track/shapes.ts";
|
||||
import { TrackSegment } from "../../track/system.ts";
|
||||
import { clamp } from "../../math/clamp.ts";
|
||||
|
||||
export class EditTrackState extends State<States> {
|
||||
override name: States = States.EDIT_TRACK;
|
||||
@ -14,14 +25,18 @@ export class EditTrackState extends State<States> {
|
||||
States.PAUSED,
|
||||
]);
|
||||
|
||||
private heldEvents: Map<string | number, (() => void) | undefined> =
|
||||
new Map();
|
||||
private heldEvents: Map<string, (() => void) | undefined> = new Map();
|
||||
|
||||
private currentSegment?: TrackSegment;
|
||||
private selectedSegment?: TrackSegment;
|
||||
private ghostSegment?: TrackSegment;
|
||||
private ghostRotated = false;
|
||||
private closestEnd?: End;
|
||||
|
||||
override update(dt: number): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
const doodler = getContextItem<Doodler>("doodler");
|
||||
|
||||
// For moving a segment, i.e. the currently active one
|
||||
// const segment = track.lastSegment;
|
||||
@ -35,6 +50,112 @@ export class EditTrackState extends State<States> {
|
||||
// });
|
||||
// }
|
||||
|
||||
if (this.selectedSegment) {
|
||||
const segment = this.selectedSegment;
|
||||
const firstPoint = segment.points[0].copy();
|
||||
const mousePos = inputManager.getMouseLocationV();
|
||||
segment.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, firstPoint);
|
||||
p.set(mousePos);
|
||||
p.add(relativePoint);
|
||||
});
|
||||
|
||||
const ends = track.findEnds();
|
||||
setContextItem("showEnds", true);
|
||||
const nearbyEnds = ends.filter((end) => {
|
||||
const dist = Vector.dist(end.pos, mousePos);
|
||||
return dist < 20 && end.segment !== segment;
|
||||
});
|
||||
let closestEnd = nearbyEnds[0];
|
||||
for (const end of nearbyEnds) {
|
||||
if (end === closestEnd) continue;
|
||||
const closestEndTangent = Vector.add(
|
||||
closestEnd.tangent.copy().mult(20),
|
||||
closestEnd.pos,
|
||||
);
|
||||
const endTangent = Vector.add(
|
||||
end.tangent.copy().rotate(Math.PI).mult(20),
|
||||
end.pos,
|
||||
);
|
||||
doodler.drawCircle(closestEndTangent, 4, { color: "red", weight: 1 });
|
||||
doodler.drawCircle(endTangent, 4, { color: "blue", weight: 1 });
|
||||
if (
|
||||
endTangent.dist(mousePos) < closestEndTangent.dist(mousePos) ||
|
||||
end.pos.dist(mousePos) < closestEnd.pos.dist(mousePos)
|
||||
) {
|
||||
closestEnd = end;
|
||||
}
|
||||
}
|
||||
if (closestEnd !== this.closestEnd) {
|
||||
this.closestEnd = closestEnd;
|
||||
this.ghostSegment = undefined;
|
||||
this.ghostRotated = false;
|
||||
}
|
||||
if (closestEnd) {
|
||||
// doodler.drawCircle(closestEnd.pos, 4, { color: "green", weight: 1 });
|
||||
doodler.line(
|
||||
closestEnd.pos,
|
||||
Vector.add(closestEnd.pos, closestEnd.tangent.copy().mult(20)),
|
||||
{ color: "green" },
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.closestEnd
|
||||
) {
|
||||
if (!this.ghostSegment) {
|
||||
this.ghostSegment = segment.copy();
|
||||
this.ghostRotated = false;
|
||||
}
|
||||
switch (this.closestEnd.frontOrBack) {
|
||||
case "front":
|
||||
this.ghostSegment.setPositionByPoint(
|
||||
this.closestEnd.pos,
|
||||
this.ghostSegment.points[0],
|
||||
);
|
||||
// this.ghostSegment.points[0] = this.closestEnd.pos;
|
||||
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
|
||||
this.closestEnd.tangent.heading(),
|
||||
this.ghostSegment.points[0],
|
||||
);
|
||||
this.ghostRotated = true;
|
||||
break;
|
||||
case "back":
|
||||
this.ghostSegment.setPositionByPoint(
|
||||
this.closestEnd.pos,
|
||||
this.ghostSegment.points[3],
|
||||
);
|
||||
// this.ghostSegment.points[3] = this.closestEnd.pos;
|
||||
!this.ghostRotated && this.ghostSegment.rotateAboutPoint(
|
||||
this.closestEnd.tangent.heading(),
|
||||
this.ghostSegment.points[3],
|
||||
);
|
||||
this.ghostRotated = true;
|
||||
break;
|
||||
}
|
||||
// } else if (closestEnd) {
|
||||
// this.closestEnd = closestEnd;
|
||||
} else if (!this.closestEnd || !closestEnd) {
|
||||
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<string[]>("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
|
||||
// const segment = track.lastSegment;
|
||||
// if (segment) {
|
||||
@ -50,56 +171,96 @@ export class EditTrackState extends State<States> {
|
||||
|
||||
// // 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 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 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 prevp3 = p3.copy();
|
||||
// const dirToMouse = Vector.sub(mousePos, p2).normalize();
|
||||
// const angleToMouse = dirToMouse.heading();
|
||||
// const dirToP1 = Vector.sub(p2, p1).normalize();
|
||||
// const angleToP1 = dirToP1.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);
|
||||
|
||||
// const goodangle = clamp(
|
||||
// angleToMouse - angleToP1,
|
||||
// angleToP1 - .6,
|
||||
// angleToP1 + .6,
|
||||
// );
|
||||
// if (
|
||||
// // Math.abs(goodangle) < .6 &&
|
||||
// p2DistToMouse > distToP3 &&
|
||||
// p3DistToMouse > distToP4
|
||||
// ) {
|
||||
// {
|
||||
// const dirToNewP3 = dirToP1.copy().rotate(
|
||||
// goodangle / 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, dirToNewP3),
|
||||
// {
|
||||
// color: "red",
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// {
|
||||
// const dirToMouse = Vector.sub(mousePos, p3).normalize();
|
||||
// const dirToP3 = Vector.sub(p3, p2).normalize();
|
||||
// const angleToP3 = dirToP3.heading();
|
||||
// const goodangle = clamp(
|
||||
// dirToMouse.heading() - angleToP3,
|
||||
// angleToP3 - .6,
|
||||
// angleToP3 + .6,
|
||||
// );
|
||||
|
||||
// const dirToNewP4 = dirToP3.copy().rotate(
|
||||
// goodangle / 2,
|
||||
// );
|
||||
// dirToNewP4.setMag(distToP4);
|
||||
// p4.set(Vector.add(p3, dirToNewP4));
|
||||
// doodler.line(p3, Vector.add(p3, dirToNewP4), { color: "green" });
|
||||
// }
|
||||
// segment.clampLength();
|
||||
// }
|
||||
// // doodler.fillText(
|
||||
// // segment.calculateApproxLength().toFixed(2),
|
||||
// // p2.copy().add(10, 0),
|
||||
// // 100,
|
||||
// // );
|
||||
// }
|
||||
|
||||
const translation = new Vector(0, 0);
|
||||
|
||||
if (inputManager.getKeyState("ArrowUp")) {
|
||||
translation.y -= 1;
|
||||
}
|
||||
{
|
||||
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" });
|
||||
if (inputManager.getKeyState("ArrowDown")) {
|
||||
translation.y += 1;
|
||||
}
|
||||
segment.clampLength();
|
||||
if (inputManager.getKeyState("ArrowLeft")) {
|
||||
translation.x -= 1;
|
||||
}
|
||||
doodler.fillText(
|
||||
segment.calculateApproxLength().toFixed(2),
|
||||
p2.copy().add(10, 0),
|
||||
100,
|
||||
);
|
||||
if (inputManager.getKeyState("ArrowRight")) {
|
||||
translation.x += 1;
|
||||
}
|
||||
|
||||
if (translation.x !== 0 || translation.y !== 0) {
|
||||
track.translate(translation);
|
||||
}
|
||||
|
||||
track.draw(true);
|
||||
@ -109,6 +270,15 @@ export class EditTrackState extends State<States> {
|
||||
// Draw track tangents
|
||||
}
|
||||
override start(): void {
|
||||
setContextItem("trackSegments", [
|
||||
undefined,
|
||||
new StraightTrack(),
|
||||
new SBendLeft(),
|
||||
new SBendRight(),
|
||||
new BankLeft(),
|
||||
new BankRight(),
|
||||
]);
|
||||
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
this.heldEvents.set("e", inputManager.offKey("e"));
|
||||
this.heldEvents.set("Escape", inputManager.offKey("Escape"));
|
||||
@ -127,20 +297,62 @@ export class EditTrackState extends State<States> {
|
||||
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(" ", () => {
|
||||
if (this.selectedSegment) {
|
||||
this.selectedSegment = undefined;
|
||||
} else {
|
||||
this.selectedSegment = new StraightTrack();
|
||||
}
|
||||
});
|
||||
|
||||
inputManager.onKey("1", () => {
|
||||
this.currentSegment = track.firstSegment;
|
||||
inputManager.onMouse("left", () => {
|
||||
const track = getContextItem<TrackSystem>("track");
|
||||
if (this.ghostSegment && this.closestEnd) {
|
||||
const segment = this.ghostSegment.cleanCopy();
|
||||
|
||||
switch (this.closestEnd.frontOrBack) {
|
||||
case "front":
|
||||
this.closestEnd.segment.frontNeighbours.push(segment);
|
||||
segment.backNeighbours.push(this.closestEnd.segment);
|
||||
break;
|
||||
case "back":
|
||||
this.closestEnd.segment.backNeighbours.push(segment);
|
||||
segment.frontNeighbours.push(this.closestEnd.segment);
|
||||
break;
|
||||
}
|
||||
track.registerSegment(segment);
|
||||
this.ghostSegment = undefined;
|
||||
this.closestEnd = undefined;
|
||||
} else if (this.selectedSegment) {
|
||||
track.registerSegment(this.selectedSegment);
|
||||
this.selectedSegment = new StraightTrack();
|
||||
} else {
|
||||
this.selectedSegment = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
// });
|
||||
|
||||
inputManager.onNumberKey((i) => {
|
||||
console.log(i);
|
||||
const segments = getContextItem<TrackSegment[]>("trackSegments");
|
||||
this.selectedSegment = segments[i];
|
||||
this.ghostRotated = false;
|
||||
this.ghostSegment = undefined;
|
||||
});
|
||||
|
||||
this.currentSegment = track.lastSegment;
|
||||
@ -151,6 +363,7 @@ export class EditTrackState extends State<States> {
|
||||
override stop(): void {
|
||||
const inputManager = getContextItem<InputManager>("inputManager");
|
||||
inputManager.offKey("e");
|
||||
inputManager.offKey("w");
|
||||
inputManager.offKey("Escape");
|
||||
if (this.heldEvents.size > 0) {
|
||||
for (const [key, cb] of this.heldEvents) {
|
||||
@ -161,5 +374,6 @@ export class EditTrackState extends State<States> {
|
||||
}
|
||||
}
|
||||
setContextItem("trackCopy", undefined);
|
||||
setContextItem("trackSegments", undefined);
|
||||
}
|
||||
}
|
||||
|
35
test/trackSystemBench.test.ts
Normal file
35
test/trackSystemBench.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { assert } from "jsr:@std/assert";
|
||||
import { describe, it } from "jsr:@std/testing/bdd";
|
||||
import { TrackSystem } from "../track/system.ts";
|
||||
import { StraightTrack } from "../track/shapes.ts";
|
||||
import { testPerformance } from "./bench.ts";
|
||||
import { setDefaultContext } from "../lib/context.ts";
|
||||
|
||||
/**
|
||||
* Tests if a function can run a given number of iterations within a target frame time.
|
||||
* @param fn The function to test.
|
||||
* @param iterations Number of times to run the function per frame.
|
||||
* @param fps Target frames per second.
|
||||
*/
|
||||
Deno.test("Track System Benchmark", () => {
|
||||
console.log("Track System Benchmark - run within frame time");
|
||||
const mockDoodler = {
|
||||
fillCircle: () => {},
|
||||
line: () => {},
|
||||
};
|
||||
setDefaultContext({
|
||||
doodler: mockDoodler,
|
||||
});
|
||||
const mockTrack = new TrackSystem([]);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
mockTrack.registerSegment(new StraightTrack());
|
||||
}
|
||||
|
||||
testPerformance(
|
||||
() => {
|
||||
mockTrack.findEnds();
|
||||
},
|
||||
10000,
|
||||
60,
|
||||
);
|
||||
});
|
@ -12,3 +12,37 @@ export class StraightTrack extends TrackSegment {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export class SBendLeft extends StraightTrack {
|
||||
constructor(start?: Vector) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, -25);
|
||||
this.points[3].add(0, -25);
|
||||
}
|
||||
}
|
||||
export class SBendRight extends StraightTrack {
|
||||
constructor(start?: Vector) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, 25);
|
||||
this.points[3].add(0, 25);
|
||||
}
|
||||
}
|
||||
|
||||
export class BankLeft extends StraightTrack {
|
||||
constructor(start?: Vector) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, -25);
|
||||
this.points[3].add(0, 25);
|
||||
}
|
||||
}
|
||||
export class BankRight extends StraightTrack {
|
||||
constructor(start?: Vector) {
|
||||
start = start || new Vector(100, 100);
|
||||
super(start);
|
||||
this.points[2].add(0, 25);
|
||||
this.points[3].add(0, -25);
|
||||
}
|
||||
}
|
||||
|
178
track/system.ts
178
track/system.ts
@ -1,4 +1,4 @@
|
||||
import { Doodler, Vector } from "@bearmetal/doodler";
|
||||
import { Doodler, Point, Vector } from "@bearmetal/doodler";
|
||||
import { PathSegment } from "../math/path.ts";
|
||||
import { getContextItem, setDefaultContext } from "../lib/context.ts";
|
||||
|
||||
@ -25,57 +25,74 @@ export class TrackSystem {
|
||||
segment.setTrack(this);
|
||||
this.segments.set(segment.id, segment);
|
||||
}
|
||||
unregisterSegment(segment: TrackSegment) {
|
||||
this.segments.delete(segment.id);
|
||||
for (const s of this.segments.values()) {
|
||||
s.backNeighbours = s.backNeighbours.filter((n) => n !== segment);
|
||||
s.frontNeighbours = s.frontNeighbours.filter((n) => n !== segment);
|
||||
}
|
||||
}
|
||||
|
||||
draw(showControls = false) {
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.draw(showControls);
|
||||
}
|
||||
|
||||
try {
|
||||
if (getContextItem<boolean>("showEnds")) {
|
||||
const ends = this.findEnds();
|
||||
for (const end of ends) {
|
||||
this.doodler.fillCircle(end.pos, 2, {
|
||||
color: "red",
|
||||
// weight: 3,
|
||||
});
|
||||
if (getContextItem<boolean>("debug")) {
|
||||
this.doodler.line(
|
||||
end.pos,
|
||||
end.pos.copy().add(end.tangent.copy().mult(20)),
|
||||
{
|
||||
color: "blue",
|
||||
// weight: 3,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setDefaultContext({ showEnds: false });
|
||||
}
|
||||
// try {
|
||||
// if (getContextItem<boolean>("showEnds")) {
|
||||
// const ends = this.findEnds();
|
||||
// for (const end of ends) {
|
||||
// this.doodler.fillCircle(end.pos, 2, {
|
||||
// color: "red",
|
||||
// // weight: 3,
|
||||
// });
|
||||
// if (getContextItem<boolean>("debug")) {
|
||||
// this.doodler.line(
|
||||
// end.pos,
|
||||
// end.pos.copy().add(end.tangent.copy().mult(20)),
|
||||
// {
|
||||
// color: "blue",
|
||||
// // weight: 3,
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// setDefaultContext({ showEnds: false });
|
||||
// }
|
||||
}
|
||||
|
||||
ends: Map<TrackSegment, [End, End]> = new Map();
|
||||
endArray: End[] = [];
|
||||
|
||||
findEnds() {
|
||||
const ends: { pos: Vector; segment: TrackSegment; tangent: Vector }[] = [];
|
||||
for (const segment of this.segments.values()) {
|
||||
const [a, b, c, d] = segment.points;
|
||||
if (this.ends.has(segment)) continue;
|
||||
const ends: [End, End] = [
|
||||
{
|
||||
const tangent = Vector.sub(a, b).normalize();
|
||||
const pos = a.copy();
|
||||
ends.push({ pos, segment, tangent });
|
||||
}
|
||||
pos: segment.points[0],
|
||||
segment,
|
||||
tangent: Vector.sub(segment.points[1], segment.points[0]).normalize(),
|
||||
frontOrBack: "back",
|
||||
},
|
||||
{
|
||||
const tangent = Vector.sub(d, c).normalize();
|
||||
const pos = d.copy();
|
||||
ends.push({ pos, segment, tangent });
|
||||
pos: segment.points[3],
|
||||
segment,
|
||||
tangent: Vector.sub(segment.points[3], segment.points[2]).normalize(),
|
||||
frontOrBack: "front",
|
||||
},
|
||||
];
|
||||
this.ends.set(segment, ends);
|
||||
this.endArray.push(...ends);
|
||||
}
|
||||
}
|
||||
return ends;
|
||||
return this.endArray;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return this.segments.values().map((s) => s.serialize()).toArray();
|
||||
return JSON.stringify(
|
||||
this.segments.values().map((s) => s.serialize()).toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
copy() {
|
||||
@ -107,6 +124,12 @@ export class TrackSystem {
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
translate(v: Vector) {
|
||||
for (const segment of this.segments.values()) {
|
||||
segment.translate(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type VectorSet = [Vector, Vector, Vector, Vector];
|
||||
@ -142,22 +165,18 @@ export class TrackSegment extends PathSegment {
|
||||
},
|
||||
);
|
||||
if (showControls) {
|
||||
// this.doodler.drawCircle(this.points[0], 4, {
|
||||
// color: "red",
|
||||
// weight: 3,
|
||||
// });
|
||||
this.doodler.drawCircle(this.points[1], 4, {
|
||||
this.doodler.fillCircle(this.points[0], 1, {
|
||||
color: "red",
|
||||
weight: 3,
|
||||
});
|
||||
this.doodler.drawCircle(this.points[2], 4, {
|
||||
this.doodler.fillCircle(this.points[1], 1, {
|
||||
color: "red",
|
||||
});
|
||||
this.doodler.fillCircle(this.points[2], 1, {
|
||||
color: "red",
|
||||
});
|
||||
this.doodler.fillCircle(this.points[3], 1, {
|
||||
color: "red",
|
||||
weight: 3,
|
||||
});
|
||||
// this.doodler.drawCircle(this.points[3], 4, {
|
||||
// color: "red",
|
||||
// weight: 3,
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,30 +196,26 @@ export class TrackSegment extends PathSegment {
|
||||
);
|
||||
}
|
||||
|
||||
propagate() {
|
||||
const [_, __, p3, p4] = this.points;
|
||||
const tangent = Vector.sub(p4, p3);
|
||||
cleanCopy() {
|
||||
return new TrackSegment(
|
||||
this.points.map((p) => p.copy()) as VectorSet,
|
||||
);
|
||||
}
|
||||
|
||||
propagateTranslation(v: Vector) {
|
||||
for (const fNeighbour of this.frontNeighbours) {
|
||||
fNeighbour.receivePropagation(tangent);
|
||||
fNeighbour.receivePropagation(v);
|
||||
}
|
||||
for (const bNeighbour of this.backNeighbours) {
|
||||
bNeighbour.receivePropagation(v);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
receivePropagation(v: Vector) {
|
||||
this.translate(v);
|
||||
this.propagateTranslation(v);
|
||||
}
|
||||
|
||||
// TODO: this duplicates receivePropagation, but for some reason it doesn't work when called from receivePropagation
|
||||
@ -231,4 +246,35 @@ export class TrackSegment extends PathSegment {
|
||||
data.id,
|
||||
);
|
||||
}
|
||||
|
||||
setPositionByPoint(pos: Vector, point: Vector) {
|
||||
if (!this.points.includes(point)) return;
|
||||
point = point.copy();
|
||||
this.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, point);
|
||||
p.set(pos);
|
||||
p.add(relativePoint);
|
||||
});
|
||||
}
|
||||
|
||||
rotateAboutPoint(angle: number, point: Vector) {
|
||||
if (!this.points.includes(point)) return;
|
||||
point = point.copy();
|
||||
this.points.forEach((p, i) => {
|
||||
const relativePoint = Vector.sub(p, point);
|
||||
relativePoint.rotate(angle);
|
||||
p.set(Vector.add(point, relativePoint));
|
||||
});
|
||||
}
|
||||
|
||||
// resetRotation() {
|
||||
// const angle = this.tangent(0).heading();
|
||||
// this.rotateAboutPoint(-angle, this.points[0]);
|
||||
// }
|
||||
|
||||
translate(v: Point) {
|
||||
this.points.forEach((p) => {
|
||||
p.add(v.x, v.y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user