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
200 changes: 200 additions & 0 deletions src/Components/Derived/Node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { colors } from "@mui/material";

Check failure on line 1 in src/Components/Derived/Node.ts

View workflow job for this annotation

GitHub Actions / Run eslint

'colors' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 1 in src/Components/Derived/Node.ts

View workflow job for this annotation

GitHub Actions / Run eslint

Unable to resolve path to module '@mui/material'
import Circle, { CircleOptions, defaultShapeOptions } from "../Circle";
import Line from "../Line";
import Text from "../Text"
import { Color, MeshBasicMaterial } from "three";

Check failure on line 5 in src/Components/Derived/Node.ts

View workflow job for this annotation

GitHub Actions / Run eslint

`three` import should occur before import of `@mui/material`
import { LineMaterial } from "three-fatline";

Check failure on line 6 in src/Components/Derived/Node.ts

View workflow job for this annotation

GitHub Actions / Run eslint

`three-fatline` import should occur before import of `../Circle`

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) {
if (node.adjacencyList[index].weight !== undefined) {
node.adjacencyList[index].weight! += value;

Check warning on line 147 in src/Components/Derived/Node.ts

View workflow job for this annotation

GitHub Actions / Run eslint

Forbidden non-null assertion
} else {
node.adjacencyList[index].weight = value;
}
node.adjacencyList[index].line.setLabel(node.adjacencyList[index].weight?.toString()!);

Check warning on line 151 in src/Components/Derived/Node.ts

View workflow job for this annotation

GitHub Actions / Run eslint

Forbidden non-null assertion

Check failure on line 151 in src/Components/Derived/Node.ts

View workflow job for this annotation

GitHub Actions / Run eslint

Optional chain expressions can return undefined by design - using a non-null assertion is unsafe and wrong
}
}

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;
106 changes: 106 additions & 0 deletions src/Components/Derived/OperationButtonPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { LineMaterial } from "three-fatline";

Check failure on line 1 in src/Components/Derived/OperationButtonPanel.ts

View workflow job for this annotation

GitHub Actions / Run eslint

'LineMaterial' is defined but never used. Allowed unused vars must match /^_/u
import Core from "../../Core";

Check failure on line 2 in src/Components/Derived/OperationButtonPanel.ts

View workflow job for this annotation

GitHub Actions / Run eslint

'Core' is defined but never used. Allowed unused vars must match /^_/u
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 15 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