731 lines
19 KiB
TypeScript
731 lines
19 KiB
TypeScript
import { Doodler, Point, Vector } from "@bearmetal/doodler";
|
|
import { ComplexPath, PathSegment } from "../math/path.ts";
|
|
import { getContextItem, setDefaultContext } from "../lib/context.ts";
|
|
import { clamp } from "../math/clamp.ts";
|
|
|
|
export class TrackSystem {
|
|
private _segments: Map<string, TrackSegment> = new Map();
|
|
private doodler: Doodler;
|
|
|
|
constructor(segments: TrackSegment[]) {
|
|
this.doodler = getContextItem<Doodler>("doodler");
|
|
for (const segment of segments) {
|
|
this._segments.set(segment.id, segment);
|
|
}
|
|
}
|
|
|
|
getSegment(id: string) {
|
|
return this._segments.get(id);
|
|
}
|
|
|
|
get firstSegment() {
|
|
return this._segments.values().next().value;
|
|
}
|
|
|
|
get lastSegment() {
|
|
return this._segments.values().toArray().pop();
|
|
}
|
|
|
|
optimize(percent: number) {
|
|
console.log("Optimizing track", percent * 100 / 4);
|
|
for (const segment of this._segments.values()) {
|
|
segment.recalculateRailPoints(Math.round(percent * 100 / 4));
|
|
}
|
|
}
|
|
|
|
recalculateAll() {
|
|
for (const segment of this._segments.values()) {
|
|
segment.update();
|
|
segment.length = segment.calculateApproxLength();
|
|
}
|
|
}
|
|
|
|
registerSegment(segment: TrackSegment) {
|
|
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);
|
|
}
|
|
const ends = this.ends.get(segment);
|
|
this.ends.delete(segment);
|
|
this.endArray = this.endArray.filter((e) => !ends?.includes(e));
|
|
}
|
|
|
|
draw(showControls = false) {
|
|
for (const [i, segment] of this._segments.entries()) {
|
|
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 });
|
|
// }
|
|
}
|
|
|
|
ends: Map<TrackSegment, [End, End]> = new Map();
|
|
endArray: End[] = [];
|
|
|
|
findEnds() {
|
|
for (const segment of this._segments.values()) {
|
|
if (this.ends.has(segment)) continue;
|
|
const ends: [End, End] = [
|
|
{
|
|
pos: segment.points[0],
|
|
segment,
|
|
tangent: Vector.sub(segment.points[1], segment.points[0]).normalize(),
|
|
frontOrBack: "back",
|
|
},
|
|
{
|
|
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 this.endArray;
|
|
}
|
|
|
|
serialize() {
|
|
return JSON.stringify(
|
|
this._segments.values().map((s) => s.serialize()).toArray(),
|
|
);
|
|
}
|
|
|
|
copy() {
|
|
const track = new TrackSystem([]);
|
|
for (const segment of this._segments.values()) {
|
|
track._segments.set(segment.id, segment.copy());
|
|
}
|
|
return track;
|
|
}
|
|
|
|
static deserialize(data: SerializedTrackSegment[]) {
|
|
if (data.length === 0) return new TrackSystem([]);
|
|
const track = new TrackSystem([]);
|
|
const neighborMap = new Map<string, [string[], string[]]>();
|
|
|
|
for (const segment of data) {
|
|
track._segments.set(segment.id, TrackSegment.deserialize(segment));
|
|
neighborMap.set(segment.id, [segment.fNeighbors, segment.bNeighbors]);
|
|
}
|
|
|
|
for (const segment of track._segments.values()) {
|
|
segment.setTrack(track);
|
|
const neighbors = neighborMap.get(segment.id);
|
|
if (neighbors) {
|
|
segment.backNeighbours = neighbors[1].map((id) =>
|
|
track._segments.get(id)
|
|
).filter((s) => s) as TrackSegment[];
|
|
segment.frontNeighbours = neighbors[0].map((id) =>
|
|
track._segments.get(id)
|
|
).filter((s) => s) as TrackSegment[];
|
|
}
|
|
}
|
|
|
|
return track;
|
|
}
|
|
|
|
translate(v: Vector) {
|
|
for (const segment of this._segments.values()) {
|
|
segment.translate(v);
|
|
}
|
|
}
|
|
|
|
private _path?: Spline<TrackSegment>;
|
|
|
|
get path() {
|
|
if (!this._path) {
|
|
this._path = this.generatePath();
|
|
}
|
|
return this._path;
|
|
}
|
|
|
|
generatePath() {
|
|
if (!this.firstSegment) throw new Error("No first segment");
|
|
const flags = { looping: false };
|
|
const rightOnlyPath = [
|
|
this.firstSegment.copy(),
|
|
...this.findRightPath(
|
|
this.firstSegment,
|
|
new Set([this.firstSegment.id]),
|
|
flags,
|
|
),
|
|
];
|
|
|
|
rightOnlyPath.forEach((s, i, arr) => {
|
|
if (i === 0) return;
|
|
const prev = arr[i - 1];
|
|
s.points[0] = prev.points[3];
|
|
s.prev = prev;
|
|
prev.next = s;
|
|
});
|
|
if (flags.looping) {
|
|
const first = rightOnlyPath[0];
|
|
const last = rightOnlyPath[rightOnlyPath.length - 1];
|
|
first.points[0] = last.points[3];
|
|
last.points[3] = first.points[0];
|
|
first.prev = last;
|
|
last.next = first;
|
|
}
|
|
|
|
return new Spline<TrackSegment>(rightOnlyPath);
|
|
}
|
|
|
|
*findRightPath(
|
|
start: TrackSegment,
|
|
seen: Set<string>,
|
|
flags: { looping: boolean },
|
|
): Generator<TrackSegment> {
|
|
if (start.frontNeighbours.length === 0) {
|
|
return;
|
|
}
|
|
let rightMost = start.frontNeighbours[0];
|
|
for (const segment of start.frontNeighbours) {
|
|
if (segment.id === rightMost.id) continue;
|
|
const rotatedSegment = segment.copy();
|
|
rotatedSegment.rotateAboutPoint(
|
|
rotatedSegment.tangent(0).heading(),
|
|
rotatedSegment.points[0],
|
|
);
|
|
const rotatedRightMost = rightMost.copy();
|
|
rotatedRightMost.rotateAboutPoint(
|
|
rotatedRightMost.tangent(0).heading(),
|
|
rotatedRightMost.points[0],
|
|
);
|
|
if (rotatedSegment.points[3].y > rotatedRightMost.points[3].y) {
|
|
rightMost = segment;
|
|
}
|
|
}
|
|
if (seen.has(rightMost.id)) {
|
|
if (seen.values().next().value === rightMost.id) {
|
|
flags.looping = true;
|
|
}
|
|
return;
|
|
}
|
|
seen.add(rightMost.id);
|
|
yield rightMost.copy();
|
|
yield* this.findRightPath(rightMost, seen, flags);
|
|
}
|
|
*findLeftPath(
|
|
start: TrackSegment,
|
|
seen: Set<string>,
|
|
flags: { looping: boolean },
|
|
): Generator<TrackSegment> {
|
|
if (start.frontNeighbours.length === 0) {
|
|
return;
|
|
}
|
|
let leftMost = start.frontNeighbours[0];
|
|
for (const segment of start.frontNeighbours) {
|
|
if (segment.id === leftMost.id) continue;
|
|
const rotatedSegment = segment.copy();
|
|
rotatedSegment.rotateAboutPoint(
|
|
rotatedSegment.tangent(0).heading(),
|
|
rotatedSegment.points[0],
|
|
);
|
|
const rotatedLeftMost = leftMost.copy();
|
|
rotatedLeftMost.rotateAboutPoint(
|
|
rotatedLeftMost.tangent(0).heading(),
|
|
rotatedLeftMost.points[0],
|
|
);
|
|
if (rotatedSegment.points[3].y < rotatedLeftMost.points[3].y) {
|
|
leftMost = segment;
|
|
}
|
|
}
|
|
if (seen.has(leftMost.id)) {
|
|
if (seen.values().next().value === leftMost.id) {
|
|
flags.looping = true;
|
|
}
|
|
return;
|
|
}
|
|
seen.add(leftMost.id);
|
|
yield leftMost.copy();
|
|
yield* this.findLeftPath(leftMost, seen, flags);
|
|
}
|
|
}
|
|
|
|
type VectorSet = [Vector, Vector, Vector, Vector];
|
|
|
|
export class TrackSegment extends PathSegment {
|
|
frontNeighbours: TrackSegment[] = [];
|
|
backNeighbours: TrackSegment[] = [];
|
|
|
|
track?: TrackSystem;
|
|
|
|
doodler: Doodler;
|
|
normalPoints: Vector[] = [];
|
|
antiNormalPoints: Vector[] = [];
|
|
evenPoints: [Vector, number][] = [];
|
|
|
|
aabb!: AABB;
|
|
|
|
private trackGuage = 12;
|
|
|
|
constructor(p: VectorSet, id?: string) {
|
|
super(p);
|
|
this.doodler = getContextItem<Doodler>("doodler");
|
|
this.id = id ?? crypto.randomUUID();
|
|
this.update();
|
|
}
|
|
|
|
updateAABB() {
|
|
let minX = Infinity;
|
|
let maxX = -Infinity;
|
|
let minY = Infinity;
|
|
let maxY = -Infinity;
|
|
|
|
[...this.normalPoints, ...this.antiNormalPoints].forEach((p) => {
|
|
minX = Math.min(minX, p.x);
|
|
maxX = Math.max(maxX, p.x);
|
|
minY = Math.min(minY, p.y);
|
|
maxY = Math.max(maxY, p.y);
|
|
});
|
|
|
|
const width = maxX - minX;
|
|
const height = maxY - minY;
|
|
if (width < this.trackGuage) {
|
|
const extra = (this.trackGuage - width) / 2;
|
|
minX -= extra;
|
|
maxX += extra;
|
|
}
|
|
if (height < this.trackGuage) {
|
|
const extra = (this.trackGuage - height) / 2;
|
|
minY -= extra;
|
|
maxY += extra;
|
|
}
|
|
this.aabb = {
|
|
pos: new Vector(minX, minY),
|
|
x: minX,
|
|
y: minY,
|
|
width: maxX - minX,
|
|
height: maxY - minY,
|
|
center: new Vector(minX, minY).add(
|
|
new Vector(maxX - minX, maxY - minY).div(2),
|
|
),
|
|
};
|
|
}
|
|
|
|
recalculateRailPoints(resolution = 60) {
|
|
this.normalPoints = [];
|
|
this.antiNormalPoints = [];
|
|
for (let i = 0; i <= resolution; i++) {
|
|
const t = i / resolution;
|
|
const normal = this.tangent(t).rotate(Math.PI / 2);
|
|
normal.setMag(this.trackGuage / 2);
|
|
const p = this.getPointAtT(t);
|
|
this.normalPoints.push(p.copy().add(normal));
|
|
this.antiNormalPoints.push(p.copy().add(normal.rotate(Math.PI)));
|
|
}
|
|
}
|
|
recalculateTiePoints() {
|
|
const spacing = Math.ceil(this.length / 10);
|
|
this.evenPoints = this.calculateEvenlySpacedPoints(this.length / spacing);
|
|
}
|
|
|
|
update() {
|
|
this.recalculateRailPoints();
|
|
this.recalculateTiePoints();
|
|
this.updateAABB();
|
|
}
|
|
|
|
setTrack(t: TrackSystem) {
|
|
this.track = t;
|
|
}
|
|
|
|
override draw(showControls = false, recalculateRailPoints = false) {
|
|
// if (showControls) {
|
|
// this.doodler.drawBezier(
|
|
// this.points[0],
|
|
// this.points[1],
|
|
// this.points[2],
|
|
// this.points[3],
|
|
// {
|
|
// strokeColor: "#ffffff50",
|
|
// },
|
|
// );
|
|
// }
|
|
if (showControls) {
|
|
this.doodler.deferDrawing(() => {
|
|
this.doodler.fillCircle(this.points[0], 1, {
|
|
color: "red",
|
|
});
|
|
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",
|
|
});
|
|
});
|
|
}
|
|
|
|
// const spacing = Math.ceil(this.length / 10);
|
|
// const points = this.calculateEvenlySpacedPoints(this.length / spacing);
|
|
for (let i = 0; i < this.evenPoints.length - 1; i++) {
|
|
// const t = i / ties;
|
|
// const p = this.getPointAtT(t);
|
|
const [p, t] = this.evenPoints[i];
|
|
// this.doodler.drawCircle(p, 2, {
|
|
// color: "red",
|
|
// weight: 3,
|
|
// });
|
|
this.doodler.drawRotated(p, t, () => {
|
|
this.doodler.line(p, p.copy().add(0, 10), {
|
|
color: "#291b17",
|
|
weight: 4,
|
|
});
|
|
this.doodler.line(p, p.copy().add(0, -10), {
|
|
color: "#291b17",
|
|
weight: 4,
|
|
});
|
|
// this.doodler.line(p.copy().add(-6, 5), p.copy().add(6, 5), {
|
|
// color: "grey",
|
|
// weight: 1,
|
|
// });
|
|
// this.doodler.line(p.copy().add(-6, -5), p.copy().add(6, -5), {
|
|
// color: "grey",
|
|
// weight: 1,
|
|
// });
|
|
});
|
|
}
|
|
|
|
if (recalculateRailPoints) {
|
|
this.recalculateRailPoints();
|
|
}
|
|
this.doodler.deferDrawing(
|
|
() => {
|
|
this.doodler.drawLine(this.normalPoints, {
|
|
color: "grey",
|
|
weight: 1.5,
|
|
});
|
|
this.doodler.drawLine(this.antiNormalPoints, {
|
|
color: "grey",
|
|
weight: 1.5,
|
|
});
|
|
},
|
|
);
|
|
// this.doodler.drawCircle(p, 2, {
|
|
// color: "red",
|
|
// weight: 3,
|
|
// });
|
|
const debug = getContextItem("debug");
|
|
if (debug.track) {
|
|
this.doodler.drawRect(this.aabb.pos, this.aabb.width, this.aabb.height, {
|
|
color: "lime",
|
|
});
|
|
this.doodler.drawCircle(this.aabb.center, 2, {
|
|
color: "cyan",
|
|
});
|
|
}
|
|
}
|
|
|
|
serialize(): SerializedTrackSegment {
|
|
return {
|
|
p: this.points.map((p) => p.array()),
|
|
id: this.id,
|
|
bNeighbors: this.backNeighbours.map((n) => n.id),
|
|
fNeighbors: this.frontNeighbours.map((n) => n.id),
|
|
};
|
|
}
|
|
|
|
copy() {
|
|
return new TrackSegment(
|
|
this.points.map((p) => p.copy()) as VectorSet,
|
|
this.id,
|
|
);
|
|
}
|
|
|
|
cleanCopy() {
|
|
return new TrackSegment(
|
|
this.points.map((p) => p.copy()) as VectorSet,
|
|
);
|
|
}
|
|
|
|
propagateTranslation(v: Vector) {
|
|
for (const fNeighbour of this.frontNeighbours) {
|
|
fNeighbour.receivePropagation(v);
|
|
}
|
|
for (const bNeighbour of this.backNeighbours) {
|
|
bNeighbour.receivePropagation(v);
|
|
}
|
|
}
|
|
|
|
lastHeading?: number;
|
|
|
|
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
|
|
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])),
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
interface PathPoint {
|
|
p: Vector;
|
|
segmentId: string;
|
|
tangent: Vector;
|
|
}
|
|
|
|
export class Spline<T extends PathSegment = PathSegment> {
|
|
segments: T[] = [];
|
|
ctx?: CanvasRenderingContext2D;
|
|
|
|
evenPoints: PathPoint[];
|
|
_pointSpacing: number;
|
|
get pointSpacing() {
|
|
return this._pointSpacing;
|
|
}
|
|
set pointSpacing(value: number) {
|
|
this._pointSpacing = value;
|
|
this.evenPoints = this.calculateEvenlySpacedPoints(value);
|
|
}
|
|
|
|
get points() {
|
|
return Array.from(new Set(this.segments.flatMap((s) => s.points)));
|
|
}
|
|
|
|
nodes: IControlNode[];
|
|
|
|
looped = false;
|
|
|
|
constructor(segs: T[]) {
|
|
this.segments = segs;
|
|
if (this.segments.at(-1)?.next === this.segments[0]) {
|
|
this.looped = true;
|
|
}
|
|
this._pointSpacing = 1;
|
|
this.evenPoints = this.calculateEvenlySpacedPoints(this._pointSpacing);
|
|
this.nodes = [];
|
|
// for (let i = 0; i < this.points.length; i += 3) {
|
|
// const node: IControlNode = {
|
|
// anchor: this.points[i],
|
|
// controls: [
|
|
// this.points.at(i - 1)!,
|
|
// this.points[(i + 1) % this.points.length],
|
|
// ],
|
|
// mirrored: false,
|
|
// tangent: true,
|
|
// };
|
|
// this.nodes.push(node);
|
|
// }
|
|
}
|
|
|
|
// setContext(ctx: CanvasRenderingContext2D) {
|
|
// this.ctx = ctx;
|
|
// for (const segment of this.segments) {
|
|
// segment.setContext(ctx);
|
|
// }
|
|
// }
|
|
|
|
draw() {
|
|
for (const segment of this.segments) {
|
|
// segment.draw();
|
|
const doodler = getContextItem<Doodler>("doodler");
|
|
doodler.drawWithAlpha(0.5, () => {
|
|
doodler.drawBezier(...segment.points, { color: "red" });
|
|
});
|
|
}
|
|
}
|
|
|
|
calculateEvenlySpacedPoints(spacing: number, resolution = 1): PathPoint[] {
|
|
// this._pointSpacing = 1;
|
|
// return this.segments.flatMap(s => s.calculateEvenlySpacedPoints(spacing, resolution));
|
|
const points: PathPoint[] = [];
|
|
|
|
points.push({
|
|
p: this.segments[0].points[0],
|
|
segmentId: this.segments[0].id,
|
|
tangent: this.segments[0].tangent(0),
|
|
});
|
|
let prev = points[0].p;
|
|
let distSinceLastEvenPoint = 0;
|
|
for (const seg of this.segments) {
|
|
let t = 0;
|
|
|
|
const div = Math.ceil(seg.length * resolution * 10);
|
|
while (t < 1) {
|
|
t += 1 / div;
|
|
const point = seg.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({
|
|
p: evenPoint,
|
|
segmentId: seg.id,
|
|
tangent: seg.tangent(t),
|
|
});
|
|
prev = evenPoint;
|
|
continue;
|
|
}
|
|
|
|
prev = point;
|
|
}
|
|
}
|
|
|
|
// this.evenPoints = points;
|
|
|
|
return points;
|
|
}
|
|
|
|
followEvenPoints(t: number): PathPoint {
|
|
if (this.looped) {
|
|
if (t < 0) t += this.evenPoints.length;
|
|
const i = Math.floor(t) % this.evenPoints.length;
|
|
const a = this.evenPoints[i];
|
|
const b = this.evenPoints[(i + 1) % this.evenPoints.length];
|
|
return {
|
|
p: Vector.lerp(a.p, b.p, t % 1),
|
|
segmentId: b.segmentId,
|
|
tangent: b.tangent,
|
|
};
|
|
}
|
|
t = clamp(t, 0, this.evenPoints.length - 1);
|
|
const i = clamp(Math.floor(t), 0, this.evenPoints.length - 1);
|
|
const a = this.evenPoints[clamp(i, 0, this.evenPoints.length - 1)];
|
|
const b = this
|
|
.evenPoints[
|
|
clamp((i + 1) % this.evenPoints.length, 0, this.evenPoints.length - 1)
|
|
];
|
|
return {
|
|
p: Vector.lerp(a.p, b.p, t % 1),
|
|
segmentId: b.segmentId,
|
|
tangent: b.tangent,
|
|
};
|
|
}
|
|
|
|
calculateApproxLength() {
|
|
for (const s of this.segments) {
|
|
s.calculateApproxLength();
|
|
}
|
|
}
|
|
|
|
toggleNodeTangent(p: Vector) {
|
|
const node = this.nodes.find((n) => n.anchor === p);
|
|
|
|
node && (node.tangent = !node.tangent);
|
|
}
|
|
toggleNodeMirrored(p: Vector) {
|
|
const node = this.nodes.find((n) => n.anchor === p);
|
|
|
|
node && (node.mirrored = !node.mirrored);
|
|
}
|
|
handleNodeEdit(p: Vector, movement: { x: number; y: number }) {
|
|
const node = this.nodes.find((n) =>
|
|
n.anchor === p || n.controls.includes(p)
|
|
);
|
|
if (!node || !(node.mirrored || node.tangent)) return;
|
|
|
|
if (node.anchor !== p) {
|
|
if (node.mirrored || node.tangent) {
|
|
const mover = node.controls.find((e) => e !== p)!;
|
|
const v = Vector.sub(node.anchor, p);
|
|
if (!node.mirrored) v.setMag(Vector.sub(node.anchor, mover).mag());
|
|
mover.set(Vector.add(v, node.anchor));
|
|
}
|
|
} else {
|
|
for (const control of node.controls) {
|
|
control.add(movement.x, movement.y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export interface IControlNode {
|
|
anchor: Vector;
|
|
controls: [Vector, Vector];
|
|
tangent: boolean;
|
|
mirrored: boolean;
|
|
}
|