Skip to content
Snippets Groups Projects
Commit 93d1a6cb authored by Matthias Konitzny's avatar Matthias Konitzny :fire:
Browse files

Many changes to node creation and deletion. Also reworked renderer focus.

parent 26374b19
No related branches found
No related tags found
No related merge requests found
......@@ -77,7 +77,6 @@ export class Graph
this.idToLink.set(link.id, link);
});
this.connectElementsToGraph();
this.updateNodeData();
this.initializeIdGeneration();
}
......@@ -92,24 +91,19 @@ export class Graph
}
private initializeIdGeneration() {
this.nextNodeId = Math.max(...this.nodes.map((node) => node.id)) + 1;
const ids = this.nodes
.map((node) => node.id)
.filter((id) => typeof id == "number");
// TODO: Prevent ids from being -Infinity if ids is empty
//this.nextNodeId = Math.max(...this.nodes.map((node) => node.id)) + 1;
this.nextNodeId = Math.max(...ids) + 1; // TODO: Remove non-numeric ids from dataset and disable check
this.nextLinkId = Math.max(...this.links.map((link) => link.id)) + 1;
this.nextObjectGroupId = Math.max(
...this.objectGroups.map((group) => group.id)
);
}
/**
* Sets the correct graph object for all the graph elements in data.
*/
private connectElementsToGraph() {
this.nodes.forEach((n) => (n.graph = this));
this.links.forEach((l) => {
l.graph = this;
});
this.objectGroups.forEach((t) => (t.graph = this));
}
public toJSONSerializableObject(): GraphData {
return {
nodes: this.nodes.map((node) => node.toJSONSerializableObject()),
......@@ -146,6 +140,7 @@ export class Graph
data.links.forEach((link) => this.createLink(link.source, link.target));
this.updateNodeData();
this.initializeIdGeneration();
return this;
}
......@@ -209,7 +204,7 @@ export class Graph
}
public createNode(data?: NodeData | SimNodeData): Node {
const node = new Node(this);
const node = new Node();
node.fromSerializedObject(data);
node.type = this.nameToObjectGroup.get(data.type);
node.neighbors = [];
......@@ -234,7 +229,7 @@ export class Graph
return;
}
const link = new Link(sourceNode, targetNode, this);
const link = new Link(sourceNode, targetNode);
sourceNode.links.push(link);
targetNode.links.push(link);
this.addLink(link);
......@@ -242,7 +237,7 @@ export class Graph
}
public createObjectGroup(name?: string, color?: string): NodeType {
const group = new NodeType(name, color, this);
const group = new NodeType(name, color);
this.addObjectGroup(group);
return group;
}
......
import { Graph } from "./graph";
import { SerializableItem } from "../serializableitem";
export class GraphElement<JSONType, HistoryType> extends SerializableItem<
JSONType,
HistoryType
> {
public graph: Graph;
constructor(id = -1, graph: Graph = undefined) {
constructor(id = -1) {
super(id);
this.equals = this.equals.bind(this);
this.graph = graph;
}
/**
* Removes element from its parent graph.
* @returns True, if successful.
*/
public delete() {
throw new Error('Function "delete()" has not been implemented.');
}
public isInitialized(): boolean {
......
import { GraphElement } from "./graphelement";
import { Node } from "./node";
import { NodeType } from "./nodetype";
import { Graph } from "./graph";
export interface LinkData {
source: number;
......@@ -35,8 +34,8 @@ export class Link
// These parameters will be added by the force graph implementation
public index?: number;
constructor(source?: Node, target?: Node, graph?: Graph) {
super(0, graph);
constructor(source?: Node, target?: Node) {
super(0);
this.equals = this.equals.bind(this);
......@@ -60,10 +59,6 @@ export class Link
return this.target.id;
}
public delete() {
return this.graph.deleteLink(this.id);
}
/**
* Determines if the given node is part of the link structure.
* @param node Node to check for.
......
......@@ -88,40 +88,12 @@ export class Node
public fy?: number;
public fz?: number;
constructor(graph?: Graph) {
super(0, graph);
constructor() {
super(0);
this.neighbors = [];
this.links = [];
}
public setType(typeId: number) {
const newType = this.graph.nameToObjectGroup.get(String(typeId)); // TODO
// Exists?
if (newType === undefined) {
return;
}
this.type = newType;
}
public delete() {
return this.graph.deleteNode(this.id);
}
/**
* Connects this node to a given node. Only works if they are in the same graph.
* @param node Other node to connect.
* @returns The created link, if successful, otherwise undefined.
*/
public connect(node: Node): Link {
if (this.graph !== node.graph) {
throw new Error("The connected nodes are not on the same graph!");
}
return this.graph.createLink(this.id, node.id);
}
public toJSONSerializableObject(): NodeData {
return {
id: this.id,
......
import { GraphElement } from "./graphelement";
import { Graph } from "./graph";
export interface NodeTypeData {
id: number;
......@@ -15,8 +14,8 @@ export class NodeType
public name: string;
public color: string;
constructor(name?: string, color?: string, graph?: Graph) {
super(0, graph);
constructor(name?: string, color?: string) {
super(0);
this.name = name;
this.color = color;
}
......@@ -34,10 +33,6 @@ export class NodeType
return this;
}
public delete() {
return this.graph.deleteNodeType(this.name); // TODO: Change to id
}
public toString(): string {
return this.name;
}
......
......@@ -8,11 +8,11 @@ interface InstructionsProps {
function Instructions({ connectOnDragEnabled }: InstructionsProps) {
return (
<ul className={"instructions"}>
<li>Click background to create node</li>
<li>Click background to deselect all node</li>
<li>
SHIFT+Click and drag on background to add nodes to selection
</li>
<li>CTRL+Click background to clear selection</li>
<li>CTRL+Click background to create a new node</li>
<li>Click node to select and edit</li>
<li>SHIFT+Click node to add or remove from selection</li>
<li>CTRL+Click another node to connect</li>
......
......@@ -8,11 +8,12 @@ import { Node, NodeProperties } from "../common/graph/node";
import { SpaceManager } from "./components/spacemanager";
import SelectLayer from "./components/selectlayer";
import { GraphData } from "../common/graph/graph";
import { Coordinate2D, GraphData } from "../common/graph/graph";
import { NodeType } from "../common/graph/nodetype";
import { GraphRenderer2D } from "./renderer";
import * as Config from "../config";
import Sidepanel from "./components/sidepanel";
import { Link } from "../common/graph/link";
export interface NodeDataChangeRequest extends NodeProperties {
id: number;
......@@ -76,6 +77,10 @@ export class Editor extends React.PureComponent<any, stateTypes> {
this.handleBoxSelect = this.handleBoxSelect.bind(this);
this.selectNodes = this.selectNodes.bind(this);
this.handleNodeDataChange = this.handleNodeDataChange.bind(this);
this.handleNodeCreation = this.handleNodeCreation.bind(this);
this.handleNodeDeletion = this.handleNodeDeletion.bind(this);
this.handleLinkCreation = this.handleLinkCreation.bind(this);
this.handleLinkDeletion = this.handleLinkDeletion.bind(this);
document.addEventListener("keydown", (e) => {
this.keyPressed(e.key);
......@@ -217,6 +222,38 @@ export class Editor extends React.PureComponent<any, stateTypes> {
this.setState({ graph: graph });
}
private handleNodeCreation(position?: Coordinate2D): Node {
const graph = Object.assign(new DynamicGraph(), this.state.graph);
const node = graph.createNode(undefined, position.x, position.y, 0, 0);
this.setState({
graph: graph,
selectedNodes: [node],
});
return node;
}
private handleNodeDeletion(id: number) {
const graph = Object.assign(new DynamicGraph(), this.state.graph);
graph.deleteNode(id);
this.setState({ graph: graph });
}
private handleLinkCreation(source: number, target: number): Link {
const graph = Object.assign(new DynamicGraph(), this.state.graph);
const link = graph.createLink(source, target);
this.setState({ graph: graph });
return link;
}
private handleLinkDeletion(id: number) {
const graph = Object.assign(new DynamicGraph(), this.state.graph);
graph.deleteLink(id);
this.setState({ graph: graph });
}
render(): React.ReactNode {
return (
<div id="ks-editor">
......@@ -246,6 +283,10 @@ export class Editor extends React.PureComponent<any, stateTypes> {
graph={this.state.graph}
width={this.state.graphWidth}
onNodeSelectionChanged={this.selectNodes}
onNodeCreation={this.handleNodeCreation}
onNodeDeletion={this.handleNodeDeletion}
onLinkCreation={this.handleLinkCreation}
onLinkDeletion={this.handleLinkDeletion}
selectedNodes={this.state.selectedNodes}
settings={this.state.settings}
/>
......
......@@ -8,16 +8,8 @@ import { GraphContent, GraphData, SimGraphData } from "../common/graph/graph";
export class DynamicGraph extends Common.Graph {
public history: History<SimGraphData>;
// Callbacks
public onChangeCallbacks: { (data: DynamicGraph): void }[];
constructor(data?: GraphContent) {
super(data);
this.onChangeCallbacks = [];
super.deleteNode = super.deleteNode.bind(this);
super.deleteLink = super.deleteLink.bind(this);
super.deleteNodeType = super.deleteNodeType.bind(this);
if (data != undefined) {
this.history = new History<SimGraphData>(
......@@ -39,36 +31,6 @@ export class DynamicGraph extends Common.Graph {
return this;
}
/**
* Calls all registered callbacks for the onChange event.
* @private
*/
private triggerOnChange() {
this.onChangeCallbacks.forEach((fn) => fn(this));
}
/**
* Triggers change event on data-redo.
*/
protected onRedo() {
if (this.history.hasRedoCheckpoints()) {
const checkpoint = this.history.redo();
this.fromSerializedObject(checkpoint.data);
this.triggerOnChange();
}
}
/**
* Triggers change event on data-undo.
*/
protected onUndo() {
if (this.history.hasUndoCheckpoints()) {
const checkpoint = this.history.undo();
this.fromSerializedObject(checkpoint.data);
this.triggerOnChange();
}
}
public createObjectGroup(name?: string, color?: string): NodeType {
if (name == undefined) {
name = "Unnamed";
......@@ -77,7 +39,6 @@ export class DynamicGraph extends Common.Graph {
color = "#000000";
}
const objectGroup = super.createObjectGroup(name, color);
this.triggerOnChange();
return objectGroup;
}
......@@ -103,26 +64,6 @@ export class DynamicGraph extends Common.Graph {
return super.createNode(data);
}
private delete(id: string | number, fn: (id: string | number) => boolean) {
if (fn(id)) {
this.triggerOnChange();
return true;
}
return false;
}
public deleteNodeType(id: string): boolean {
return this.delete(id, super.deleteNodeType);
}
public deleteNode(id: number): boolean {
return this.delete(id, super.deleteNode);
}
public deleteLink(id: number): boolean {
return this.delete(id, super.deleteLink);
}
getLink(
sourceId: number,
targetId: number,
......
......@@ -4,7 +4,6 @@ import { DynamicGraph } from "./graph";
import { Node } from "../common/graph/node";
import { ForceGraph2D } from "react-force-graph";
import { Link } from "../common/graph/link";
import { GraphElement } from "../common/graph/graphelement";
import { Coordinate2D } from "../common/graph/graph";
export class GraphRenderer2D extends React.PureComponent<
......@@ -19,7 +18,7 @@ export class GraphRenderer2D extends React.PureComponent<
/**
* True, if the graph was the target of the most recent click event.
*/
private graphInFocus = false; // TODO: Remove?
private graphInFocus = false;
/**
* True for each key, that is currently considered pressed. If key has not been pressed yet, it will not exist as dict-key.
*/
......@@ -30,6 +29,10 @@ export class GraphRenderer2D extends React.PureComponent<
width: PropTypes.number.isRequired,
onNodeClicked: PropTypes.func,
onNodeSelectionChanged: PropTypes.func,
onNodeCreation: PropTypes.func,
onNodeDeletion: PropTypes.func,
onLinkCreation: PropTypes.func,
onLinkDeletion: PropTypes.func,
/**
* Collection of all currently selected nodes. Can also be undefined or empty.
*/
......@@ -45,7 +48,6 @@ export class GraphRenderer2D extends React.PureComponent<
this.handleNodeClick = this.handleNodeClick.bind(this);
this.handleEngineStop = this.handleEngineStop.bind(this);
this.handleNodeDrag = this.handleNodeDrag.bind(this);
this.handleElementRightClick = this.handleElementRightClick.bind(this);
this.screen2GraphCoords = this.screen2GraphCoords.bind(this);
this.handleNodeCanvasObject = this.handleNodeCanvasObject.bind(this);
this.handleLinkCanvasObject = this.handleLinkCanvasObject.bind(this);
......@@ -59,10 +61,6 @@ export class GraphRenderer2D extends React.PureComponent<
this.keys[e.key] = false;
this.handleShortcutEvents(e.key);
});
document.addEventListener(
"mousedown",
(e) => (this.graphInFocus = false)
);
this.state = {
selectedNodes: [], // TODO: Why was undefined allowed here?
......@@ -76,22 +74,6 @@ export class GraphRenderer2D extends React.PureComponent<
this.warmupTicks = this.defaultWarmupTicks;
}
/**
* Deletes all nodes currently selected. Handles store points accordingly of the number of deleted nodes.
*/
private deleteSelectedNodes() {
const selectedNodes = this.state.selectedNodes;
if (selectedNodes.length == 1) {
selectedNodes[0].delete();
selectedNodes.pop();
this.props.onNodeSelectionChanged(selectedNodes);
} else {
selectedNodes.forEach((node: Node) => node.delete());
this.props.onNodeSelectionChanged([]);
}
}
/**
* Triggers actions that correspond with certain shortcuts.
*
......@@ -104,13 +86,14 @@ export class GraphRenderer2D extends React.PureComponent<
key === "Delete" &&
this.graphInFocus // Only delete if 2d-graph is the focused element
) {
this.deleteSelectedNodes();
this.props.selectedNodes.forEach((node: Node) =>
this.props.onNodeDeletion(node.id)
);
this.props.onNodeSelectionChanged([]);
}
}
private handleNodeClick(node: Node) {
this.graphInFocus = true;
if (this.keys["Control"]) {
// Connect to clicked node as parent while control is pressed
if (this.props.selectedNodes.length == 0) {
......@@ -136,8 +119,6 @@ export class GraphRenderer2D extends React.PureComponent<
event: MouseEvent,
position: { graph: Coordinate2D; window: Coordinate2D }
) {
this.graphInFocus = true;
// Is there really no node there? Trying to prevent small error, where this event is triggered, even if there is a node.
const nearestNode = this.props.graph.getClosestNode(
position.graph.x,
......@@ -148,41 +129,18 @@ export class GraphRenderer2D extends React.PureComponent<
return;
}
// Just deselect if control key is pressed
if (this.keys["Control"]) {
this.props.onNodeSelectionChanged([]);
return;
}
// Add new node
const node = this.state.graph.createNode(
undefined,
position.graph.x,
position.graph.y,
0,
0
);
this.forceUpdate(); // TODO: Remove?
// Select newly created node
if (this.keys["Shift"]) {
// Simply add to current selection of shift is pressed
this.toggleNodeSelection(node);
// Request new node
this.props.onNodeCreation({
x: position.graph.x,
y: position.graph.y,
});
} else {
this.props.onNodeSelectionChanged([node]);
// Just deselect
this.props.onNodeSelectionChanged([]);
}
}
/**
* Processes right-click event on graph elements by deleting them.
*/
private handleElementRightClick(element: GraphElement<unknown, unknown>) {
this.graphInFocus = true;
element.delete();
this.forceUpdate(); // TODO: Necessary?
}
/**
* Propagates the changed state of the graph.
*/
......@@ -200,10 +158,10 @@ export class GraphRenderer2D extends React.PureComponent<
}
if (this.props.selectedNodes.length == 1) {
node.connect(this.state.selectedNodes[0]);
this.props.onLinkCreation(node.id, this.props.selectedNodes[0].id);
} else {
this.props.selectedNodes.forEach((selectedNode: Node) =>
node.connect(selectedNode)
this.props.onLinkCreation(node.id, selectedNode.id)
);
}
}
......@@ -233,8 +191,6 @@ export class GraphRenderer2D extends React.PureComponent<
}
private handleNodeDrag(node: Node) {
this.graphInFocus = true;
// if (!this.props.selectedNodes.includes(node)) {
// this.props.onNodeSelectionChanged([...this.props.selectedNodes, node]);
// }
......@@ -257,8 +213,7 @@ export class GraphRenderer2D extends React.PureComponent<
}
// Add link
node.connect(closest.node); // TODO: Change must propagate
// this.forceUpdate(); TODO: Remove?
this.props.onLinkCreation(node.id, closest.id);
}
private handleNodeCanvasObject(
......@@ -419,30 +374,40 @@ export class GraphRenderer2D extends React.PureComponent<
render() {
return (
<ForceGraph2D
ref={this.forceGraph}
width={this.props.width}
graphData={this.props.graph}
onNodeClick={this.handleNodeClick}
autoPauseRedraw={false}
cooldownTicks={0}
warmupTicks={this.warmupTicks}
onEngineStop={this.handleEngineStop}
nodeCanvasObject={this.handleNodeCanvasObject}
nodeCanvasObjectMode={() => "after"}
linkCanvasObject={this.handleLinkCanvasObject}
linkCanvasObjectMode={() => "replace"}
nodeColor={(node: Node) => node.type.color}
onNodeDrag={this.handleNodeDrag}
onLinkRightClick={this.handleElementRightClick}
onNodeRightClick={this.handleElementRightClick}
onBackgroundClick={(event: any) =>
this.handleBackgroundClick(
event,
this.extractPositions(event)
)
}
/>
<div
tabIndex={0} // This is needed to receive focus events
onFocus={() => (this.graphInFocus = true)}
onBlur={() => (this.graphInFocus = false)}
>
<ForceGraph2D
ref={this.forceGraph}
width={this.props.width}
graphData={this.props.graph}
onNodeClick={this.handleNodeClick}
autoPauseRedraw={false}
cooldownTicks={0}
warmupTicks={this.warmupTicks}
onEngineStop={this.handleEngineStop}
nodeCanvasObject={this.handleNodeCanvasObject}
nodeCanvasObjectMode={() => "after"}
linkCanvasObject={this.handleLinkCanvasObject}
linkCanvasObjectMode={() => "replace"}
nodeColor={(node: Node) => node.type.color}
onNodeDrag={this.handleNodeDrag}
onLinkRightClick={(link: Link) =>
this.props.onLinkDeletion(link.id)
}
onNodeRightClick={(node: Node) =>
this.props.onNodeDeletion(node.id)
}
onBackgroundClick={(event: any) =>
this.handleBackgroundClick(
event,
this.extractPositions(event)
)
}
/>
</div>
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment