From f161b127e43dfbd5af83e41c6fc9087a2e95133c Mon Sep 17 00:00:00 2001 From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de> Date: Fri, 9 Sep 2022 14:46:04 +0200 Subject: [PATCH] Prepared editor main component for the new data structure --- src/common/graph/node.ts | 22 +-- src/common/history.ts | 5 +- src/editor/editor.tsx | 362 ++++++++++++++++----------------------- src/editor/graph.ts | 6 +- 4 files changed, 163 insertions(+), 232 deletions(-) diff --git a/src/common/graph/node.ts b/src/common/graph/node.ts index c3b8171..31a84be 100644 --- a/src/common/graph/node.ts +++ b/src/common/graph/node.ts @@ -52,10 +52,13 @@ export interface GraphNode extends NodeProperties { index?: number; x?: number; y?: number; + z?: number; vx?: number; vy?: number; + vz?: number; fx?: number; fy?: number; + fz?: number; } export class Node @@ -77,10 +80,13 @@ export class Node public index?: number; public x?: number; public y?: number; + public z?: number; public vx?: number; public vy?: number; + public vz?: number; public fx?: number; public fy?: number; + public fz?: number; constructor(graph?: Graph) { super(0, graph); @@ -89,12 +95,7 @@ export class Node } public setType(typeId: number) { - // Is it even different? - if (this.type.id === typeId) { - return; - } - - const newType = this.graph.getType(typeId); + const newType = this.graph.nameToObjectGroup.get(typeId); // TODO // Exists? if (newType === undefined) { @@ -102,15 +103,6 @@ export class Node } this.type = newType; - - // Store change - this.graph.storeCurrentData( - "Set type [" + - newType.toString() + - "] for [" + - this.toString() + - "]" - ); } public delete() { diff --git a/src/common/history.ts b/src/common/history.ts index 6e18bc9..2249bc0 100644 --- a/src/common/history.ts +++ b/src/common/history.ts @@ -14,13 +14,14 @@ export class History<HistoryDataType> { constructor( data: SerializableItem<unknown, HistoryDataType>, - maxCheckpoints = 20 + maxCheckpoints = 20, + initialMessage = "New History" ) { this.data = data; this.maxCheckpoints = maxCheckpoints; this.checkpoints = []; this.currentCheckpoint = -1; - this.checkpoint("New History"); + this.checkpoint(initialMessage); } checkpoint(description: string) { diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index 5858bf5..8ef5ba1 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Graph } from "./graph"; +import { DynamicGraph } from "./graph"; import { loadGraphJson } from "../common/datasets"; import { NodeDetails } from "./components/nodedetails"; import { SpaceSelect } from "./components/spaceselect"; @@ -12,7 +12,8 @@ import { Link } from "../common/graph/link"; import { NodeTypesEditor } from "./components/nodetypeseditor"; import { SpaceManager } from "./components/spacemanager"; import { SelectLayer } from "./components/selectlayer"; -import { NodeType } from "../structures/graph/nodetype"; +import { GraphData } from "../common/graph/graph"; +import { NodeType } from "../common/graph/nodetype"; type propTypes = { spaceId: string; @@ -21,7 +22,7 @@ type stateTypes = { /** * Graph structure holding the basic information. */ - graph: Graph; + graph: DynamicGraph; /** * Should labels on nodes be rendered, or none at all. @@ -71,8 +72,8 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { private maxDistanceToConnect = 15; private defaultWarmupTicks = 100; private warmupTicks = 100; - private renderer: any; - private graphContainer: any; + private renderer: React.RefObject<any>; + private graphContainer: React.RefObject<HTMLDivElement>; /** * True, if the graph was the target of the most recent click event. @@ -87,12 +88,11 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { this.loadSpace = this.loadSpace.bind(this); this.extractPositions = this.extractPositions.bind(this); this.handleNodeClick = this.handleNodeClick.bind(this); - this.onHistoryChange = this.onHistoryChange.bind(this); + this.onGraphDataChange = this.onGraphDataChange.bind(this); this.handleEngineStop = this.handleEngineStop.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyUp = this.handleKeyUp.bind(this); this.forceUpdate = this.forceUpdate.bind(this); - this.isHighlighted = this.isHighlighted.bind(this); this.handleNodeCanvasObject = this.handleNodeCanvasObject.bind(this); this.handleLinkCanvasObject = this.handleLinkCanvasObject.bind(this); this.handleBackgroundClick = this.handleBackgroundClick.bind(this); @@ -111,7 +111,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { graph: undefined, visibleLabels: true, connectOnDrag: false, - selectedNodes: undefined, + selectedNodes: [], // TODO: Why was undefined allowed here? keys: {}, graphWidth: 1000, }; @@ -141,27 +141,23 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { * @param data Serialized graph data. * @returns True, if successful. */ - public loadGraph(data: any): boolean { + public loadGraph(data: GraphData): boolean { console.log("Starting to load new graph ..."); console.log(data); // Create graph - const newGraph = Graph.parse(data); + const graph = new DynamicGraph(); + graph.fromSerializedObject(data); this.warmupTicks = this.defaultWarmupTicks; // Should only run for each newly initialized graph, but then never again - // Is valid and parsed successfully? - if (newGraph === undefined) { - return false; - } - // Set as new state - console.log(newGraph); + console.log(graph); this.setState({ - graph: newGraph, + graph: graph, }); - newGraph.onChangeCallbacks.push(this.onHistoryChange); + graph.onChangeCallbacks.push(this.onGraphDataChange); // Subscribe to global events document.onkeydown = this.handleKeyDown; @@ -212,26 +208,15 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { * Deletes all nodes currently selected. Handles store points accordingly of the number of deleted nodes. */ private deleteSelectedNodes() { - if (this.selectedNodes === undefined) { - return; // Nothing to delete - } - - if (this.selectedNodes.length == 1) { - this.selectedNodes[0].delete(); - return; - } + const selectedNodes = this.state.selectedNodes; - // Delete multiple connected nodes - const count: number = this.selectedNodes.length; - try { - // Disable storing temporarily to create just one big change. - this.state.graph.disableStoring(); - this.selectedNodes.forEach((node: Node) => node.delete()); - } finally { - this.state.graph.enableStoring(); - this.state.graph.storeCurrentData( - "Deleted " + count + " nodes and all connected links" - ); + if (selectedNodes.length == 1) { + selectedNodes[0].delete(); + selectedNodes.pop(); + this.selectNodes(selectedNodes); + } else { + selectedNodes.forEach((node: Node) => node.delete()); + this.deselect(); } } @@ -270,17 +255,14 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { * Handler for background click event on force graph. Adds new node by default. * @param event Click event. */ - private handleBackgroundClick(event: any, position: clickPosition) { + private handleBackgroundClick(event: MouseEvent, position: clickPosition) { 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 placeholderNode: Node = { - id: undefined, - x: position.graph.x, - y: position.graph.y, - } as unknown as Node; - const nearestNode = - this.state.graph.getClosestOtherNode(placeholderNode); + const nearestNode = this.state.graph.getClosestNode( + position.graph.x, + position.graph.y + ); if (nearestNode !== undefined && nearestNode.distance < 4) { this.handleNodeClick(nearestNode.node); return; @@ -293,66 +275,33 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { } // Add new node - const newNode = new Node(); - - newNode.name = "Unnamed"; - (newNode as any).x = position.graph.x; - (newNode as any).y = position.graph.y; - (newNode as any).vx = 0; - (newNode as any).vy = 0; - - newNode.add(this.state.graph); - this.forceUpdate(); + 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.state.keys["Shift"]) { // Simply add to current selection of shift is pressed - this.toggleNodeSelection(newNode); + this.toggleNodeSelection(node); } else { - this.selectNode(newNode); + this.selectNode(node); } } /** * Propagates the changed state of the graph. */ - private onHistoryChange() { - if (this.selectedNodes === undefined) { - this.selectNode(undefined); - this.forceUpdate(); - return; - } - - const nodes: Node[] = this.selectedNodes.map((node: Node) => - this.state.graph.getNode(node.id) + private onGraphDataChange() { + const nodes: Node[] = this.state.selectedNodes.map((node: Node) => + this.state.graph.node(node.id) ); this.selectNodes(nodes); - this.forceUpdate(); - } - - /** - * Should a given element be highlighted in rendering or not. - * @param element Element that should, or should not be highlighted. - * @returns True, if element should be highlighted. - */ - private isHighlighted(element: GraphElement): boolean { - if (this.selectedNodes == undefined || element == undefined) { - // Default to false if nothing selected. - return false; - } - - if (element.node) { - // Is one of nodes - return this.selectedNodes.includes(element as Node); - } else if (element.link) { - // Is link - // Is it one of the adjacent links? - return this.selectedNodes.some((node: Node) => - node.links.find(element.equals) - ); - } else { - return false; - } + this.forceUpdate(); // TODO } /** @@ -363,13 +312,17 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { private extractPositions(event: any): clickPosition { return { graph: this.renderer.current.screen2GraphCoords( - event.layerX, + event.layerX, // TODO: Replace layerx/layery non standard properties and fix typing event.layerY ), window: { x: event.clientX, y: event.clientY }, }; } + private deselect() { + this.setState({ selectedNodes: [] }); + } + /** * Selects a single node, or clears selection if given undefined. * @param node Single node to select, or undefined. @@ -388,38 +341,36 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { }); } - /** - * Makes sure to always offer a valid format of the selected nodes. Is either undefined or contains at least one valid node. An empty array is never returned. - */ - private get selectedNodes(): Node[] { - if (this.state.selectedNodes === undefined) { - return undefined; - } - - // Remove undefines - let selectedNodes = this.state.selectedNodes.filter( - (n: Node) => n !== undefined - ); - - // Remove duplicates - selectedNodes = [...new Set(selectedNodes)]; - - if (selectedNodes.length > 0) { - return selectedNodes; - } - - return undefined; - } + // /** + // * Makes sure to always offer a valid format of the selected nodes. Is either undefined or contains at least one valid node. An empty array is never returned. + // */ + // private get selectedNodes(): Node[] { + // // TODO: Here are a lot of things that should not be possible by design + // + // // Remove undefines + // let selectedNodes = this.state.selectedNodes.filter( + // (n: Node) => n !== undefined + // ); + // + // // Remove duplicates + // selectedNodes = [...new Set(selectedNodes)]; + // + // if (selectedNodes.length > 0) { + // return selectedNodes; + // } + // + // return undefined; + // } private handleNodeClick(node: Node) { this.graphInFocus = true; if (this.state.keys["Control"]) { // Connect to clicked node as parent while control is pressed - if (this.selectedNodes == undefined) { + if (this.state.selectedNodes.length == 0) { // Have no node connected, so select this.selectNode(node); - } else if (!this.selectedNodes.includes(node)) { + } else if (!this.state.selectedNodes.includes(node)) { // Already have *other* node/s selected, so connect this.connectSelectionToNode(node); } @@ -429,43 +380,26 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { // By default, simply select node this.selectNode(node); } - this.forceUpdate(); + this.forceUpdate(); // TODO: Remove? } private connectSelectionToNode(node: Node) { - if (this.selectedNodes === undefined) { - return; - } - - if (this.selectedNodes.length == 1) { - node.connect(this.selectedNodes[0]); + if (this.state.selectedNodes.length == 0) { return; } - // More than one new link => custom save point handling - try { - this.state.graph.disableStoring(); - this.selectedNodes.forEach((selectedNode: Node) => + if (this.state.selectedNodes.length == 1) { + node.connect(this.state.selectedNodes[0]); + } else { + this.state.selectedNodes.forEach((selectedNode: Node) => node.connect(selectedNode) ); - } finally { - this.state.graph.enableStoring(); - this.state.graph.storeCurrentData( - "Added " + - this.selectedNodes.length + - " links on [" + - node.toString() + - "]" - ); } } private toggleNodeSelection(node: Node) { // Convert selection to array as basis - let selection = this.selectedNodes; - if (selection === undefined) { - selection = []; - } + let selection = this.state.selectedNodes; // Add/Remove node if (selection.includes(node)) { @@ -478,32 +412,24 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { this.selectNodes(selection); } - private handleNodeCanvasObject(node: Node, ctx: any, globalScale: any) { + private handleNodeCanvasObject( + node: Node, + ctx: CanvasRenderingContext2D, + globalScale: number + ) { + // TODO: Refactor + // add ring just for highlighted nodes - if (this.isHighlighted(node)) { + if (this.state.selectedNodes.includes(node)) { // Outer circle ctx.beginPath(); - ctx.arc( - (node as any).x, - (node as any).y, - 4 * 0.7, - 0, - 2 * Math.PI, - false - ); + ctx.arc(node.x, node.y, 4 * 0.7, 0, 2 * Math.PI, false); ctx.fillStyle = "white"; ctx.fill(); // Inner circle ctx.beginPath(); - ctx.arc( - (node as any).x, - (node as any).y, - 4 * 0.3, - 0, - 2 * Math.PI, - false - ); + ctx.arc(node.x, node.y, 4 * 0.3, 0, 2 * Math.PI, false); ctx.fillStyle = node.type.color; ctx.fill(); } @@ -516,8 +442,8 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { ctx.drawImage( img, - (node as any).x - imageSize / 2, - (node as any).y - imageSize / 2, + node.x - imageSize / 2, + node.y - imageSize / 2, imageSize, imageSize ); @@ -529,47 +455,52 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { * If this nodes is considered highlighted => Draw label * If this node is a neighbor of a selected node => Draw label */ - const isNodeRelatedToSelection: boolean = - this.selectedNodes === undefined || - this.isHighlighted(node) || - this.selectedNodes.some((selectedNode: Node) => - selectedNode.neighbors.includes(node) - ); - - if (this.state.visibleLabels && isNodeRelatedToSelection) { - const label = node.name; - const fontSize = 11 / globalScale; - ctx.font = `${fontSize}px Sans-Serif`; - const textWidth = ctx.measureText(label).width; - const bckgDimensions = [textWidth, fontSize].map( - (n) => n + fontSize * 0.2 - ); // some padding - - const nodeHeightOffset = imageSize / 3 + bckgDimensions[1]; - ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; - ctx.fillRect( - (node as any).x - bckgDimensions[0] / 2, - (node as any).y - bckgDimensions[1] / 2 + nodeHeightOffset, - ...bckgDimensions - ); - - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillStyle = "white"; - ctx.fillText( - label, - (node as any).x, - (node as any).y + nodeHeightOffset - ); - } + // TODO: Reenable node label rendering + // const isNodeRelatedToSelection: boolean = + // this.state.selectedNodes.length != 0 || + // this.isHighlighted(node) || + // this.selectedNodes.some((selectedNode: Node) => + // selectedNode.neighbors.includes(node) + // ); + // + // if (this.state.visibleLabels && isNodeRelatedToSelection) { + // const label = node.name; + // const fontSize = 11 / globalScale; + // ctx.font = `${fontSize}px Sans-Serif`; + // const textWidth = ctx.measureText(label).width; + // const bckgDimensions = [textWidth, fontSize].map( + // (n) => n + fontSize * 0.2 + // ); // some padding + // + // const nodeHeightOffset = imageSize / 3 + bckgDimensions[1]; + // ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; + // ctx.fillRect( + // (node as any).x - bckgDimensions[0] / 2, + // (node as any).y - bckgDimensions[1] / 2 + nodeHeightOffset, + // ...bckgDimensions + // ); + // + // ctx.textAlign = "center"; + // ctx.textBaseline = "middle"; + // ctx.fillStyle = "white"; + // ctx.fillText( + // label, + // (node as any).x, + // (node as any).y + nodeHeightOffset + // ); + // } // TODO: Render label as always visible } - private handleLinkCanvasObject(link: any, ctx: any, globalScale: any): any { + private handleLinkCanvasObject( + link: Link, + ctx: CanvasRenderingContext2D, + globalScale: number + ) { // Links already initialized? if (link.source.x === undefined) { - return undefined; + return; } // Draw gradient link @@ -581,11 +512,15 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { ); // Have reversed colors // Color at source node referencing the target node and vice versa - gradient.addColorStop("0", link.target.type.color); - gradient.addColorStop("1", link.source.type.color); + gradient.addColorStop(0, link.target.type.color); + gradient.addColorStop(1, link.source.type.color); let lineWidth = 0.5; - if (this.isHighlighted(link)) { + if ( + this.state.selectedNodes.some((node: Node) => + node.links.find(link.equals) + ) + ) { lineWidth = 2; } lineWidth /= globalScale; // Scale with zoom @@ -596,8 +531,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { ctx.strokeStyle = gradient; ctx.lineWidth = lineWidth; ctx.stroke(); - - return undefined; } private handleNodeTypeSelect(type: NodeType) { @@ -610,7 +543,10 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { private handleNodeDrag(node: Node) { this.graphInFocus = true; - if (!this.selectedNodes || !this.selectedNodes.includes(node)) { + if ( + !this.state.selectedNodes || + !this.state.selectedNodes.includes(node) + ) { this.selectNode(node); } @@ -619,7 +555,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { return; } - const closest = this.state.graph.getClosestOtherNode(node); + const closest = this.state.graph.getClosestNode(node.x, node.y, node); // Is close enough for new link? if (closest.distance > this.maxDistanceToConnect) { @@ -639,11 +575,11 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { /** * Processes right-click event on graph elements by deleting them. */ - private handleElementRightClick(element: GraphElement) { + private handleElementRightClick(element: GraphElement<unknown, unknown>) { this.graphInFocus = true; element.delete(); - this.forceUpdate(); + this.forceUpdate(); // TODO: Necessary? } private handleEngineStop() { @@ -653,7 +589,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { } this.warmupTicks = 0; // Only warm up once, so stop warming up after the first freeze - this.state.graph.storeCurrentData("Initial state", false); this.forceUpdate(); } @@ -663,7 +598,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { return; } - this.selectNodes(selectedNodes.concat(this.selectedNodes)); + this.selectNodes(selectedNodes.concat(this.state.selectedNodes)); } render(): React.ReactNode { @@ -690,10 +625,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { <ForceGraph2D ref={this.renderer} width={this.state.graphWidth} - graphData={{ - nodes: this.state.graph.data.nodes, - links: this.state.graph.links, - }} + graphData={this.state.graph} onNodeClick={this.handleNodeClick} autoPauseRedraw={false} cooldownTicks={0} @@ -729,13 +661,15 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> { <HistoryNavigator spaceId="space" historyObject={this.state.graph} - onChange={this.onHistoryChange} + onChange={this.onGraphDataChange} /> <hr /> <NodeDetails - selectedNodes={this.selectedNodes} + selectedNodes={this.state.selectedNodes} allTypes={ - this.state.graph ? this.state.graph.types : [] + this.state.graph + ? this.state.graph.objectGroups + : [] } onChange={this.forceUpdate} /> diff --git a/src/editor/graph.ts b/src/editor/graph.ts index 3c5bbc3..4a74b5f 100644 --- a/src/editor/graph.ts +++ b/src/editor/graph.ts @@ -14,7 +14,11 @@ export class DynamicGraph extends Common.Graph { constructor(data?: GraphContent) { super(data); this.onChangeCallbacks = []; - this.history = new History<SimGraphData>(this); + this.history = new History<SimGraphData>( + this, + 20, + "Created new graph." + ); } /** -- GitLab