diff --git a/src/common/graph/graph.ts b/src/common/graph/graph.ts index 80715515d0b428e1784b30565e4348ac9e9b5a73..9b9a61515a8077e4480afd9dec7346f84af04476 100644 --- a/src/common/graph/graph.ts +++ b/src/common/graph/graph.ts @@ -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; } diff --git a/src/common/graph/graphelement.ts b/src/common/graph/graphelement.ts index 6e0fe1ef4e4d4580915e2deeec12caca3b34ef31..432905d96c7e0a223e8c6ac1b9e97a51cd738963 100644 --- a/src/common/graph/graphelement.ts +++ b/src/common/graph/graphelement.ts @@ -1,25 +1,12 @@ -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 { diff --git a/src/common/graph/link.ts b/src/common/graph/link.ts index a69469f09963e573df66a864c461edab5c7e743f..0aaa37e0397f784ed38c944d4c6d3370403b3585 100644 --- a/src/common/graph/link.ts +++ b/src/common/graph/link.ts @@ -1,7 +1,6 @@ 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. diff --git a/src/common/graph/node.ts b/src/common/graph/node.ts index 78de57b1040d1b7e9fc3567ab572b307fdcb3ab5..b0c553b99678a74f87b33c712217038712b165c7 100644 --- a/src/common/graph/node.ts +++ b/src/common/graph/node.ts @@ -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, diff --git a/src/common/graph/nodetype.ts b/src/common/graph/nodetype.ts index 39862e69c939a4e2d5e35f8b0341ae1d148780ee..45a5353f7036f5cc164d8029ba94c7081fbffd96 100644 --- a/src/common/graph/nodetype.ts +++ b/src/common/graph/nodetype.ts @@ -1,5 +1,4 @@ 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; } diff --git a/src/editor/components/instructions.tsx b/src/editor/components/instructions.tsx index 4b3c3d92bc3b91a3e20b358bb9c8995f528662f1..be8e78d4ed27a715c21ccec649cada60db4dc168 100644 --- a/src/editor/components/instructions.tsx +++ b/src/editor/components/instructions.tsx @@ -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> diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index e38f72a3b0ca886286f8582a3909d445de83fce0..35bb633ac2fb089510e2612189d71d53275f81a4 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -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} /> diff --git a/src/editor/graph.ts b/src/editor/graph.ts index d55cbd38f58abf67677221365700edde456c77e2..5ef66529fd0053b2fd0b04fb5131cc415d465898 100644 --- a/src/editor/graph.ts +++ b/src/editor/graph.ts @@ -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, diff --git a/src/editor/renderer.tsx b/src/editor/renderer.tsx index b7795d9806a46d5cddbdf30152fd328878b64218..c6c6ae7b890d233696e39f45e05d21ce3b843af9 100644 --- a/src/editor/renderer.tsx +++ b/src/editor/renderer.tsx @@ -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> ); } }