Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Made node component #80

Merged
merged 11 commits into from
Jul 3, 2024
246 changes: 246 additions & 0 deletions src/Components/Derived/Node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import { Color, MeshBasicMaterial } from "three";
import { LineMaterial } from "three-fatline";
import Circle, { CircleOptions, defaultShapeOptions } from "../Circle";
import Line from "../Line";
import Text from "../Text";

export type NodeOptions = CircleOptions & {
label?: string;
};

const defaultNodeOptions: NodeOptions = {
...defaultShapeOptions,
label: "",
};

type Edge = {
node: Node;
line: Line;
weight?: number;
};

class Node extends Circle {
adjacencyList: Edge[];
label?: Text;

constructor(
x = 0,
y = 0,
radius = 5,
adjacencyList: Edge[] = [],
options?: NodeOptions
) {
super(x, y, radius, options);
const { label } = { ...defaultNodeOptions, ...options };
if (label) {
this.label = new Text(label, {
position: [0, 0],
fontSize: this.calculateFontSize(label, radius),
anchorX: "center",
anchorY: "middle",
responsiveScale: false,
});
this.add(this.label);
}
this.adjacencyList = [...adjacencyList];
}

/**
* Help function to avoid node label from going outside of the node
*
* @param text - Node label
* @param radius - Radius of the node
* @returns FontSize which will keep the node label within the node
*/
private calculateFontSize(text: string, radius: number): number {
const maxDiameter = radius * 2;
let fontSize = radius;

while (fontSize > 0 && text.length * fontSize * 0.6 > maxDiameter) {
fontSize -= 0.25;
}

return fontSize;
}

isAdjacentTo(node: Node): boolean {
return this.adjacencyList.map((edge) => edge.node).includes(node);
}

/**
* Adds an edge from this node to the other node given and updates adjacencyList accordingly.
* Calculations are made in order to have the edge go to/from the circle arc.
*
* @param other - Node to connect with edge
* @param directed - Booleean for whether the edge is directed (directed edges will also have curve)
* @param value - Number for eeight/value of the edge
*/
connectTo(other: Node, directed = false, value?: number): void {
if (!this.isAdjacentTo(other) && !(!directed && other.isAdjacentTo(this))) {
const dx = other.position.x - this.position.x;
const dy = other.position.y - this.position.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const cos = Math.abs(dx) / dist;
const sin = Math.abs(dy) / dist;

if (dx >= 0 && dy >= 0) {
const outlineX1 = cos * this.radius;
const outlineY1 = sin * this.radius;
const outlineX2 = other.position.x - cos * other.radius;
const outlineY2 = other.position.y - sin * other.radius;

const line = new Line(
[outlineX1, outlineY1],
[outlineX2 - this.position.x, outlineY2 - this.position.y],
{
arrowhead: directed,
curve: directed ? 2 : 0,
label: value !== undefined ? value.toString() : "",
}
);
this.adjacencyList.push({ node: other, line: line, weight: value });
this.add(line);
} else if (dx >= 0 && dy < 0) {
const outlineX1 = cos * this.radius;
const outlineY1 = -sin * this.radius;
const outlineX2 = other.position.x - cos * other.radius;
const outlineY2 = other.position.y + sin * other.radius;

const line = new Line(
[outlineX1, outlineY1],
[outlineX2 - this.position.x, outlineY2 - this.position.y],
{
arrowhead: directed,
curve: directed ? 2 : 0,
label: value !== undefined ? value.toString() : "",
}
);
this.adjacencyList.push({ node: other, line: line, weight: value });
this.add(line);
} else if (dx < 0 && dy < 0) {
const outlineX1 = -cos * this.radius;
const outlineY1 = -sin * this.radius;
const outlineX2 = other.position.x + cos * other.radius;
const outlineY2 = other.position.y + sin * other.radius;

const line = new Line(
[outlineX1, outlineY1],
[outlineX2 - this.position.x, outlineY2 - this.position.y],
{
arrowhead: directed,
curve: directed ? 2 : 0,
label: value !== undefined ? value.toString() : "",
}
);
this.adjacencyList.push({ node: other, line: line, weight: value });
this.add(line);
} else {
const outlineX1 = -cos * this.radius;
const outlineY1 = sin * this.radius;
const outlineX2 = other.position.x + cos * other.radius;
const outlineY2 = other.position.y - sin * other.radius;

const line = new Line(
[outlineX1, outlineY1],
[outlineX2 - this.position.x, outlineY2 - this.position.y],
{
arrowhead: directed,
curve: directed ? 2 : 0,
label: value !== undefined ? value.toString() : "",
}
);
this.adjacencyList.push({ node: other, line: line, weight: value });
this.add(line);
}

if (!directed) {
other.connectTo(this, false);
}
}
}

disconnectFrom(other: Node): void {
const index = this.adjacencyList.map((edge) => edge.node).indexOf(other);
if (index > -1) {
const directed = this.adjacencyList[index].line.arrowhead;
this.remove(this.adjacencyList[index].line);
this.adjacencyList.splice(index, 1);
if (!directed) {
other.disconnectFrom(this);
}
}
}

getEdgeWeight(other: Node): number | undefined {
const index = this.adjacencyList.map((edge) => edge.node).indexOf(other);
if (index > -1) {
return this.adjacencyList[index].weight;
} else {
return undefined;
}
}

static addEdgeWeight(node: Node, other: Node, value: number): void {
const index = node.adjacencyList.map((edge) => edge.node).indexOf(other);
if (index > -1) {
const edge = node.adjacencyList[index];
if (edge.weight !== undefined) {
edge.weight += value;
} else {
edge.weight = value;
}
edge.line.setLabel(edge.weight.toString());
}
}

static setEdgeWeight(node: Node, other: Node, value: number): void {
const index = node.adjacencyList.map((edge) => edge.node).indexOf(other);
if (index > -1) {
node.adjacencyList[index].weight = value;
node.adjacencyList[index].line.setLabel(value.toString());
}
}

setLabel(label: string): void {
if (this.label !== undefined) {
this.label.setText(label);
this.label.setFontSize(this.calculateFontSize(label, this.radius));
} else {
this.label = new Text(label, {
position: [0, 0],
fontSize: this.calculateFontSize(label, this.radius),
anchorX: "center",
anchorY: "middle",
responsiveScale: false,
});
this.add(this.label);
}
}

static setColor(node: Node, color: number): void {
node.material = new MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.5,
});
}

static setEdgeColor(node: Node, other: Node, color: number): void {
const index = node.adjacencyList.map((edge) => edge.node).indexOf(other);
if (index > -1) {
(node.adjacencyList[index].line.material as LineMaterial).color =
new Color(color);
}
}

getEdge(other: Node): Edge | null {
const index = this.adjacencyList.map((edge) => edge.node).indexOf(other);
if (index > -1) {
return this.adjacencyList[index];
} else {
return null;
}
}
}

export default Node;
129 changes: 129 additions & 0 deletions src/Components/Derived/OperationButtonPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import Button from "../Button";
import { GuiComponent } from "../interfaces";
import Node from "./Node";

class OperationButtonPanel implements GuiComponent {
htmlElement: HTMLElement;
counter: number;
operationList: {
type: "setColor" | "setEdgeColor" | "setEdgeWeight" | "addEdgeWeight";
args: any[];

Check warning on line 10 in src/Components/Derived/OperationButtonPanel.ts

View workflow job for this annotation

GitHub Actions / Run eslint

Unexpected any. Specify a different type
}[];
reverseOperationList: (() => void)[];
backButton: Button;
fourthButton: Button;

constructor(
operationList: {
type: "setColor" | "setEdgeColor" | "setEdgeWeight" | "addEdgeWeight";
args: any[];

Check warning on line 19 in src/Components/Derived/OperationButtonPanel.ts

View workflow job for this annotation

GitHub Actions / Run eslint

Unexpected any. Specify a different type
}[]
) {
const panelWrapper = document.createElement("div");
panelWrapper.className = "panel-wrapper";
this.htmlElement = panelWrapper;

this.counter = 0;
this.operationList = operationList;
this.reverseOperationList = [];
this.backButton = new Button({ label: "<" });
this.backButton.addObserver(() => this.reversePreviousOperation());
this.backButton.htmlElement;
this.fourthButton = new Button({ label: ">" });
this.fourthButton.addObserver(() => this.executeNextOperation());

this.htmlElement.appendChild(this.backButton.htmlElement);
this.htmlElement.appendChild(this.fourthButton.htmlElement);
}

/**
* Executes the next operation when ">"-button is clicked.
* Also automatically computes the inverse and adds it to reverseOperationlist.
*/
executeNextOperation(): void {
if (this.counter < this.operationList.length) {
const nextOperation = this.operationList[this.counter];
if (nextOperation.type === "setColor") {
if (this.counter >= this.reverseOperationList.length) {
const color = nextOperation.args[0].material.color.getHex();
this.reverseOperationList.push(() => {
Node.setColor(nextOperation.args[0], color);
});
}
Node.setColor(nextOperation.args[0], nextOperation.args[1]);
this.counter++;
} else if (nextOperation.type === "setEdgeColor") {
if (this.counter >= this.reverseOperationList.length) {
const color = nextOperation.args[0]
.getEdge(nextOperation.args[1])
.line.material.color.getHex();
this.reverseOperationList.push(() => {
Node.setEdgeColor(
nextOperation.args[0],
nextOperation.args[1],
color
);
});
}
Node.setEdgeColor(
nextOperation.args[0],
nextOperation.args[1],
nextOperation.args[2]
);
this.counter++;
} else if (nextOperation.type === "setEdgeWeight") {
if (this.counter >= this.reverseOperationList.length) {
const prevEdgeWeight = nextOperation.args[0].getEdgeWeight(
nextOperation.args[1]
);
this.reverseOperationList.push(() => {
Node.setEdgeWeight(
nextOperation.args[0],
nextOperation.args[1],
prevEdgeWeight
);
});
}
Node.setEdgeWeight(
nextOperation.args[0],
nextOperation.args[1],
nextOperation.args[2]
);
this.counter++;
} else if (nextOperation.type === "addEdgeWeight") {
if (this.counter >= this.reverseOperationList.length) {
this.reverseOperationList.push(() => {
Node.addEdgeWeight(
nextOperation.args[0],
nextOperation.args[1],
-nextOperation.args[2]
);
});
}
Node.addEdgeWeight(
nextOperation.args[0],
nextOperation.args[1],
nextOperation.args[2]
);
this.counter++;
}
} else {
console.log("No more operations to perform");
}
}

/**
* Reverses the previous operation when "<"-button is clicked.
*/
reversePreviousOperation(): void {
if (this.counter > 0) {
this.counter--;
const prevOperation = this.reverseOperationList[this.counter];
prevOperation();
} else {
console.log("No earlier operations to perform");
}
}
}

export default OperationButtonPanel;
Loading
Loading