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

Partially refactored the node types editor.

parent c98725f3
No related branches found
No related tags found
No related merge requests found
Pipeline #56978 passed
......@@ -47,9 +47,9 @@ export class Graph
private idToNode: Map<number, Node>;
private idToLink: Map<number, Link>;
private nextNodeId = 0;
private nextLinkId = 0;
private nextObjectGroupId = 0;
protected nextNodeId = 0;
protected nextLinkId = 0;
protected nextObjectGroupId = 0;
/**
* Creates a new Graph object.
......@@ -184,6 +184,14 @@ export class Graph
return this.idToNode.get(id);
}
public nodeType(id: number): NodeType {
return this.idToObjectGroup.get(id);
}
public link(id: number): Link {
return this.idToLink.get(id);
}
private addNode(node: Node) {
if (this.idToNode.has(node.id)) {
node.id = ++this.nextNodeId;
......@@ -283,12 +291,16 @@ export class Graph
}
const nodeType = this.idToObjectGroup.get(id);
this.idToObjectGroup.delete(id);
for (const node of this.nodes) {
if (node.type.id === nodeType.id) {
node.type = this.objectGroups[0];
}
}
this.objectGroups = this.objectGroups.filter(
(group) => group.id !== id
);
return true;
}
......
......@@ -59,6 +59,7 @@ function NodeDetails({
Object.assign(referenceData, { [key]: value });
onNodeDataChange(
// Generate changes for individual nodes if multiple nodes are selected
selectedNodes.map((node) => {
const update = Object.fromEntries(
Object.entries(referenceData).filter(
......
import React from "react";
import { ReactNode } from "react";
import { NodeType } from "../../common/graph/nodetype";
import React, { useState } from "react";
import { NodeType, NodeTypeData } from "../../common/graph/nodetype";
import "./nodetypeentry.css";
import { DynamicGraph } from "../graph";
type propTypes = {
graph: DynamicGraph;
type: NodeType;
onChange: { (): void };
onSelectAll: (type: NodeType) => void;
type NodeTypeEntryProps = {
nodeType: NodeType;
onNodeTypeDelete: (id: number[]) => void;
onNodeTypeDataChange: (
requests: NodeTypeData[],
createCheckpoint?: boolean
) => void;
onNodeTypeSelect: (type: NodeType) => void;
enableDelete: boolean;
};
type stateTypes = {
temporaryColor: string;
};
export class NodeTypeEntry extends React.Component<propTypes, stateTypes> {
debounceTimeout: NodeJS.Timeout;
debounceArgs: any[];
debounceFunc: any;
constructor(props: propTypes) {
super(props);
this.deleteType = this.deleteType.bind(this);
this.handleTextChange = this.handleTextChange.bind(this);
this.handleColorChange = this.handleColorChange.bind(this);
this.state = {
temporaryColor: undefined,
};
}
private deleteType() {
//this.props.type.delete();
}
function NodeTypeEntry({
nodeType,
onNodeTypeDataChange,
onNodeTypeDelete,
onNodeTypeSelect,
enableDelete,
}: NodeTypeEntryProps) {
const [tmpColor, setTmpColor] = useState<string>(undefined);
private isValidColor(color: string): boolean {
const isValidColor = (color: string) => {
if (color.length <= 0) {
return false;
}
// Source: https://stackoverflow.com/a/8027444
return /^#([0-9A-F]{3}){1,2}$/i.test(color);
}
private handleColorChange(event: any) {
const newColor = event.target.value;
if (this.isValidColor(newColor)) {
// Is actual change?
if (this.props.type.color !== newColor) {
// Update proper color
this.props.type.color = newColor;
// this.props.type.graph.storeCurrentData( // TODO: Reimplement
// "Changed color of type [" + this.props.type + "]"
// );
}
// Reset temporary value
this.setState({
temporaryColor: undefined,
});
};
const referenceData: NodeTypeData = {
id: nodeType.id,
color: nodeType.color,
name: nodeType.name,
};
const handleDataChange = function <ValueType>(
key: keyof NodeTypeData,
value: ValueType
) {
// if (!changed) { TODO: Move to nodetypeseditor
// setChanged(true);
// }
Object.assign(referenceData, { [key]: value });
onNodeTypeDataChange([referenceData], false);
};
const handleColorChange = (color: string) => {
if (isValidColor(color)) {
handleDataChange("color", color);
setTmpColor(undefined);
} else {
// Only set as temporary value
this.setState({
temporaryColor: newColor,
});
}
this.props.onChange();
}
/**
* Generic function for handeling a changing text input and applying the new value to the node type.
* @param event Change event of text input.
* @param property Property to give new value.
*/
private handleTextChange(event: any, property: string) {
const newValue = event.target.value;
// Actual change?
if ((this.props.type as any)[property] == newValue) {
return;
setTmpColor(color);
}
(this.props.type as any)[property] = newValue;
this.props.onChange();
// Save change, but debounce, so it doesn't trigger too quickly
this.props.onChange();
// this.debounce(
// (property: string) => {
// // this.props.type.graph.storeCurrentData( // TODO: Reimplement
// // "Changed " + property + " of type [" + this.props.type + "]"
// // );
// this.props.onChange();
// },
// 500,
// property
// );
}
// debounce(func: any, wait: number, ...args: any[]) {
// // It works, don't question it
// const later = () => {
// this.debounceTimeout = null;
// this.debounceFunc(...this.debounceArgs);
// };
//
// clearTimeout(this.debounceTimeout);
// if (
// this.debounceArgs !== undefined &&
// args[0] !== this.debounceArgs[0] &&
// this.debounceFunc !== undefined
// ) {
// this.debounceFunc(...this.debounceArgs);
// }
//
// this.debounceArgs = args;
// this.debounceFunc = func;
// this.debounceTimeout = setTimeout(later, wait);
// }
render(): ReactNode {
return (
<li className="node-type">
<input
className="node-type-name"
type={"text"}
value={this.props.type.name}
onChange={(event) => this.handleTextChange(event, "name")}
/>
<input
className="node-type-color"
type={"text"}
value={
this.state.temporaryColor !== undefined
? this.state.temporaryColor
: this.props.type.color
}
onChange={(event) => this.handleColorChange(event)}
/>
<button onClick={() => this.props.onSelectAll(this.props.type)}>
Select nodes
};
return (
<li className="node-type">
<input
className="node-type-name"
type={"text"}
value={nodeType.name}
onChange={(event) =>
handleDataChange("name", event.target.value)
}
/>
<input
className="node-type-color"
type={"text"}
value={tmpColor !== undefined ? tmpColor : nodeType.color}
onChange={(event) => handleColorChange(event.target.value)}
/>
<button onClick={() => onNodeTypeSelect(nodeType)}>
Select nodes
</button>
{enableDelete && (
<button onClick={() => onNodeTypeDelete([nodeType.id])}>
Delete
</button>
{this.props.graph &&
this.props.graph.objectGroups.length > 1 ? (
<button onClick={this.deleteType}>Delete</button>
) : (
""
)}
</li>
);
}
)}
</li>
);
}
export default NodeTypeEntry;
import React from "react";
import { ReactNode } from "react";
import "./nodetypeseditor.css";
import { NodeTypeEntry } from "./nodetypeentry";
import { NodeType } from "../../common/graph/nodetype";
import { DynamicGraph } from "../graph";
import NodeTypeEntry from "./nodetypeentry";
import { NodeType, NodeTypeData } from "../../common/graph/nodetype";
type propTypes = {
graph: DynamicGraph;
onChange: { (): void };
onSelectAll: (type: NodeType) => void;
type NodeTypesEditorProps = {
nodeTypes: NodeType[];
onNodeTypeSelect: (type: NodeType) => void;
onNodeTypeCreation: () => NodeType;
onNodeTypeDelete: (id: number[]) => void;
onNodeTypeDataChange: (
requests: NodeTypeData[],
createCheckpoint?: boolean
) => void;
};
export class NodeTypesEditor extends React.Component<propTypes> {
constructor(props: propTypes) {
super(props);
this.addType = this.addType.bind(this);
}
private addType() {
this.props.graph.createObjectGroup();
}
render(): ReactNode {
if (this.props.graph === undefined) {
return "No graph selected.";
}
return (
<div id="node-types-editor">
<h3>Node types</h3>
<ul>
{this.props.graph.objectGroups.map((type) => (
<NodeTypeEntry
onChange={this.props.onChange}
key={type.id}
type={type}
graph={this.props.graph}
onSelectAll={this.props.onSelectAll}
/>
))}
</ul>
<button onClick={this.addType}>Add type</button>
</div>
);
}
function NodeTypesEditor({
nodeTypes,
onNodeTypeSelect,
onNodeTypeCreation,
onNodeTypeDelete,
onNodeTypeDataChange,
}: NodeTypesEditorProps) {
return (
<div id="node-types-editor">
<h3>Node types</h3>
<ul>
{nodeTypes.map((nodeType) => (
<NodeTypeEntry
key={nodeType.id}
nodeType={nodeType}
onNodeTypeSelect={onNodeTypeSelect}
onNodeTypeDelete={onNodeTypeDelete}
onNodeTypeDataChange={onNodeTypeDataChange}
enableDelete={nodeTypes.length > 1}
/>
))}
</ul>
<button onClick={onNodeTypeCreation}>Add type</button>
</div>
);
}
export default NodeTypesEditor;
......@@ -4,11 +4,11 @@ import "./sidepanel.css";
import HistoryNavigator from "./historynavigator";
import { DynamicGraph } from "../graph";
import NodeDetails from "./nodedetails";
import { NodeTypesEditor } from "./nodetypeseditor";
import NodeTypesEditor from "./nodetypeseditor";
import Settings from "./settings";
import Instructions from "./instructions";
import { Node } from "../../common/graph/node";
import { NodeType } from "../../common/graph/nodetype";
import { NodeType, NodeTypeData } from "../../common/graph/nodetype";
import { EditorSettings, NodeDataChangeRequest } from "../editor";
interface SidepanelProps {
......@@ -17,6 +17,12 @@ interface SidepanelProps {
onRedo: () => void;
onUndo: () => void;
onNodeTypeSelect: (type: NodeType) => void;
onNodeTypeCreation: () => NodeType;
onNodeTypeDelete: (id: number[]) => void;
onNodeTypeDataChange: (
requests: NodeTypeData[],
createCheckpoint?: boolean
) => void;
onSettingsChange: (settings: EditorSettings) => void;
onNodeDataChange: (
requests: NodeDataChangeRequest[],
......@@ -34,6 +40,9 @@ function Sidepanel({
onUndo,
onRedo,
onNodeTypeSelect,
onNodeTypeCreation,
onNodeTypeDelete,
onNodeTypeDataChange,
onSettingsChange,
onNodeDataChange,
onSave,
......@@ -61,11 +70,11 @@ function Sidepanel({
/>
<hr />
<NodeTypesEditor
onChange={() =>
console.log("Refactor onChange for nodetypes editor!")
} // TODO: Refactor
graph={graph}
onSelectAll={onNodeTypeSelect}
nodeTypes={graph.objectGroups}
onNodeTypeSelect={onNodeTypeSelect}
onNodeTypeCreation={onNodeTypeCreation}
onNodeTypeDelete={onNodeTypeDelete}
onNodeTypeDataChange={onNodeTypeDataChange}
/>
<hr />
<Settings settings={settings} onSettingsChange={onSettingsChange} />
......
......@@ -13,7 +13,7 @@ import { Node, NodeProperties } from "../common/graph/node";
import { SpaceManager } from "./components/spacemanager";
import SelectLayer from "./components/selectlayer";
import { Coordinate2D, GraphData, SimGraphData } from "../common/graph/graph";
import { NodeType } from "../common/graph/nodetype";
import { NodeType, NodeTypeData } from "../common/graph/nodetype";
import { GraphRenderer2D } from "./renderer";
import * as Config from "../config";
import Sidepanel from "./components/sidepanel";
......@@ -80,6 +80,10 @@ export class Editor extends React.PureComponent<any, stateTypes> {
this.saveSpace = this.saveSpace.bind(this);
this.forceUpdate = this.forceUpdate.bind(this);
this.handleNodeTypeSelect = this.handleNodeTypeSelect.bind(this);
this.handleNodeTypeDataChange =
this.handleNodeTypeDataChange.bind(this);
this.handleNodeTypeDeletion = this.handleNodeTypeDeletion.bind(this);
this.handleNodeTypeCreation = this.handleNodeTypeCreation.bind(this);
this.handleBoxSelect = this.handleBoxSelect.bind(this);
this.selectNodes = this.selectNodes.bind(this);
this.handleNodeDataChange = this.handleNodeDataChange.bind(this);
......@@ -259,6 +263,34 @@ export class Editor extends React.PureComponent<any, stateTypes> {
this.setState({ graph: graph });
}
private handleNodeTypeDataChange(
nodeTypeData: NodeTypeData[],
createCheckpoint = true
) {
if (nodeTypeData.length == 0) {
return;
}
// Create a shallow copy of the graph object to trigger an update over setState
const graph = Object.assign(new DynamicGraph(), this.state.graph);
// Modify node type
for (const request of nodeTypeData) {
const node = graph.nodeType(request.id);
Object.assign(node, request);
}
// Create checkpoint
if (createCheckpoint) {
graph.createCheckpoint(
`Modified ${nodeTypeData.length} node(s) data.`
);
}
// Push shallow copy to state
this.setState({ graph: graph });
}
private handleNodeCreation(
position?: Coordinate2D,
createCheckpoint = true
......@@ -326,6 +358,29 @@ export class Editor extends React.PureComponent<any, stateTypes> {
this.setState({ graph: graph });
}
private handleNodeTypeCreation(createCheckpoint = true): NodeType {
const graph = Object.assign(new DynamicGraph(), this.state.graph);
const nodeType = graph.createObjectGroup();
if (createCheckpoint) {
graph.createCheckpoint("Created new node type.");
}
this.setState({ graph: graph });
return nodeType;
}
private handleNodeTypeDeletion(ids: number[], createCheckpoint = true) {
const graph = Object.assign(new DynamicGraph(), this.state.graph);
ids.forEach((id) => graph.deleteNodeType(id));
if (createCheckpoint) {
graph.createCheckpoint(`Deleted ${ids.length} node type(s).`);
}
this.setState({ graph: graph });
}
private loadGraphFromCheckpoint(checkpoint: Checkpoint<SimGraphData>) {
const graph = new DynamicGraph();
graph.fromSerializedObject(checkpoint.data);
......@@ -425,6 +480,9 @@ export class Editor extends React.PureComponent<any, stateTypes> {
onUndo={this.handleUndo}
onRedo={this.handleRedo}
onNodeTypeSelect={this.handleNodeTypeSelect}
onNodeTypeCreation={this.handleNodeTypeCreation}
onNodeTypeDelete={this.handleNodeTypeDeletion}
onNodeTypeDataChange={this.handleNodeTypeDataChange}
onSettingsChange={(settings) =>
this.setState({ settings: settings })
}
......
......@@ -42,7 +42,7 @@ export class DynamicGraph extends Common.Graph {
public createObjectGroup(data?: NodeTypeData): NodeType {
if (data == undefined) {
data = {
id: 0,
id: ++this.nextObjectGroupId,
name: "Unnamed",
color: "#000000",
};
......
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