|
|
@@ -6,14 +6,16 @@ import {
|
|
|
NodeDeletion,
|
|
|
EdgeCreation,
|
|
|
EdgeUpdate,
|
|
|
+ PrimitiveDelta,
|
|
|
} from "../onion/primitive_delta";
|
|
|
|
|
|
+import {NodeState, GraphDeltaExecutor} from "../onion/delta_executor";
|
|
|
+
|
|
|
import {
|
|
|
- // CompositeDelta,
|
|
|
CompositeLevel,
|
|
|
} from "../onion/composite_delta";
|
|
|
|
|
|
-import {UUID} from "../onion/types";
|
|
|
+import {PrimitiveType, UUID} from "../onion/types";
|
|
|
|
|
|
import {
|
|
|
Version,
|
|
|
@@ -21,196 +23,88 @@ import {
|
|
|
VersionRegistry,
|
|
|
} from "../onion/version";
|
|
|
|
|
|
-// The stuff that we store in each (visible) node:
|
|
|
-interface NodeObjType {
|
|
|
- // The delta that created the node
|
|
|
- nodeCreation: NodeCreation;
|
|
|
-
|
|
|
- // Outgoing edges: Mapping from edge label to the most recent EdgeCreation or EdgeUpdate.
|
|
|
- outgoing: Map<string, [EdgeCreation|EdgeUpdate, NodeObjType]>;
|
|
|
-
|
|
|
- // For any edge that is or was incoming, we keep the delta that set, unset or deleted that edge. If this node is deleted, this deletion must depend on those deltas.
|
|
|
- incoming: Array<[EdgeCreation|EdgeUpdate|NodeDeletion, NodeObjType]>;
|
|
|
-}
|
|
|
-
|
|
|
-type EdgeObjType = null; // We don't need to store anything special in edges.
|
|
|
+export type NodeType = d3Types.d3Node<NodeState>;
|
|
|
+export type LinkType = d3Types.d3Link<null>;
|
|
|
|
|
|
-export type NodeType = d3Types.d3Node<NodeObjType>;
|
|
|
-export type LinkType = d3Types.d3Link<EdgeObjType>;
|
|
|
-
|
|
|
-export type GraphType = d3Types.d3Graph<NodeObjType,EdgeObjType>;
|
|
|
+export type GraphType = d3Types.d3Graph<NodeState,null>;
|
|
|
|
|
|
interface EditableGraphProps {
|
|
|
graph: GraphType;
|
|
|
+ graphDeltaExecutor: GraphDeltaExecutor;
|
|
|
forces: Forces;
|
|
|
version: Version;
|
|
|
getUuid: () => UUID;
|
|
|
newVersionHandler: (Version) => void;
|
|
|
- newDeltaHandler: (Delta) => void;
|
|
|
+ newDeltaHandler: (CompositeDelta) => void;
|
|
|
|
|
|
- createNode: (NodeType) => void;
|
|
|
- deleteNode: (string) => void;
|
|
|
- createLink: (LinkType) => void;
|
|
|
- deleteLink: (source: NodeType, label:string) => void;
|
|
|
+ setNextNodePosition: (x:number, y:number) => void;
|
|
|
}
|
|
|
|
|
|
interface EditableGraphState {
|
|
|
}
|
|
|
|
|
|
-export function nodeCreationToD3(nodeCreation: NodeCreation, x: number, y: number): NodeType {
|
|
|
- return {
|
|
|
- id: nodeCreation.id.value.toString(),
|
|
|
- label: nodeCreation.id.value.toString(),
|
|
|
- x, y,
|
|
|
- color: "darkturquoise",
|
|
|
- obj: {nodeCreation, outgoing: new Map(), incoming: []},
|
|
|
- };
|
|
|
-}
|
|
|
|
|
|
export class EditableGraph extends React.Component<EditableGraphProps, EditableGraphState> {
|
|
|
- graphRef: React.RefObject<Graph<NodeObjType,null>> = React.createRef<Graph<NodeObjType,null>>();
|
|
|
- mouseDownNode: d3Types.d3Node<NodeObjType> | null = null; // only while mouse button is down, does this record the d3Node that was under the cursor when the mouse went down.
|
|
|
-
|
|
|
- nextId: number = 0;
|
|
|
+ mouseDownNode: NodeType | null = null; // only while mouse button is down, does this record the d3Node that was under the cursor when the mouse went down.
|
|
|
|
|
|
readonly compositeLvl: CompositeLevel = new CompositeLevel();
|
|
|
readonly versionRegistry: VersionRegistry = new VersionRegistry();
|
|
|
- // currentVersion: Version;
|
|
|
-
|
|
|
- // constructor(props) {
|
|
|
- // super(props);
|
|
|
- // // this.currentVersion = initialVersion;
|
|
|
- // }
|
|
|
|
|
|
-
|
|
|
- mouseDownHandler = (event, {x,y}, node) => {
|
|
|
+ mouseDownHandler = (event, {x,y}, node: NodeType | undefined) => {
|
|
|
event.stopPropagation();
|
|
|
- // console.log("DOWN:", node, event);
|
|
|
if (node) {
|
|
|
this.mouseDownNode = node;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- mouseUpHandler = (event, {x,y}, node: d3Types.d3Node<NodeObjType> | undefined) => {
|
|
|
+ mouseUpHandler = (event, {x,y}, node: NodeType | undefined) => {
|
|
|
event.stopPropagation();
|
|
|
- // console.log("UP:", node, event);
|
|
|
- if (this.graphRef.current !== null) {
|
|
|
- if (event.button === 2) { // right mouse button
|
|
|
+
|
|
|
+ // Construct the delta(s) that capture the user's change:
|
|
|
+ const deltas: PrimitiveDelta[] = (() => {
|
|
|
+ if (event.button === 2) { // right mouse button
|
|
|
if (node !== undefined && this.mouseDownNode !== null) {
|
|
|
+ // right mouse button was dragged from one node to another -> create/update edge
|
|
|
const label = prompt("Edge label:", "label");
|
|
|
if (label !== null) {
|
|
|
- // Create or update edge:
|
|
|
- const source = this.mouseDownNode.obj;
|
|
|
- const target = node.obj;
|
|
|
- const previousEdgeUpdate = source.outgoing.get(label);
|
|
|
- const edgeCreationOrUpdate = (() => {
|
|
|
- if (previousEdgeUpdate !== undefined) {
|
|
|
- console.log("updating existing edge...")
|
|
|
- const [prevUpdate, prevTargetObj] = previousEdgeUpdate;
|
|
|
- // an edge with the same source and label already exists:
|
|
|
- const edgeUpdate = new EdgeUpdate(prevUpdate, target.nodeCreation);
|
|
|
- this.props.deleteLink(this.mouseDownNode, label);
|
|
|
- // replace item in array:
|
|
|
- console.log("replace item in array:");
|
|
|
- prevTargetObj.incoming.splice(target.incoming.findIndex(([delta, _])=>delta===prevUpdate), 1, [edgeUpdate, prevTargetObj]);
|
|
|
- console.log("prevTargetObj:", prevTargetObj);
|
|
|
- return edgeUpdate;
|
|
|
- }
|
|
|
- else {
|
|
|
- const edgeCreation = new EdgeCreation(source.nodeCreation, label, target.nodeCreation);
|
|
|
- return edgeCreation;
|
|
|
- }
|
|
|
- })();
|
|
|
- source.outgoing.set(label, [edgeCreationOrUpdate, target]);
|
|
|
- target.incoming.push([edgeCreationOrUpdate, source]);
|
|
|
- console.log("target:", target);
|
|
|
-
|
|
|
- // console.log("target.incoming:", target.incoming);
|
|
|
- // console.log("edgeCreationOrUpdate.conflicts:", edgeCreationOrUpdate.getConflicts());
|
|
|
-
|
|
|
- this.props.createLink({source: this.mouseDownNode, label, target: node, obj: null});
|
|
|
-
|
|
|
- const tx = this.compositeLvl.createComposite([edgeCreationOrUpdate]);
|
|
|
- const version = this.versionRegistry.createVersion(this.props.version, tx);
|
|
|
- this.props.newDeltaHandler(tx);
|
|
|
- this.props.newVersionHandler(version);
|
|
|
+ return [this.mouseDownNode.obj.setEdge(label, node.obj)];
|
|
|
}
|
|
|
}
|
|
|
- else { // right mouse button
|
|
|
- // Create node:
|
|
|
+ else {
|
|
|
+ // right mouse button clicked -> create node
|
|
|
const uuid = this.props.getUuid();
|
|
|
- const nodeCreation = new NodeCreation(uuid);
|
|
|
-
|
|
|
- console.log("createNode??")
|
|
|
- this.props.createNode(nodeCreationToD3(nodeCreation, x, y));
|
|
|
-
|
|
|
- const tx = this.compositeLvl.createComposite([nodeCreation]);
|
|
|
- const version = this.versionRegistry.createVersion(this.props.version, tx);
|
|
|
- this.props.newDeltaHandler(tx);
|
|
|
- this.props.newVersionHandler(version);
|
|
|
+ this.props.setNextNodePosition(x,y);
|
|
|
+ return [new NodeCreation(uuid)];
|
|
|
}
|
|
|
}
|
|
|
else if (event.button === 1) { // middle mouse button
|
|
|
- if (node) {
|
|
|
- // Delete node (and its incoming + outgoing edges):
|
|
|
- // console.log("incoming:", node.obj.incoming);
|
|
|
- // console.log("outgoing:", node.obj.outgoing);
|
|
|
-
|
|
|
- const outgoingEdgeDependencies: any[] = [];
|
|
|
- const incomingEdgeDependencies: any[] = [];
|
|
|
- const compositeDeltas: any[] = [];
|
|
|
-
|
|
|
- for (const [incomingEdge,sourceObj] of node.obj.incoming) {
|
|
|
- // We also depend on incoming edges that were deleted (because their source node was deleted):
|
|
|
- if (incomingEdge instanceof NodeDeletion) {
|
|
|
- console.log("edge was already deleted");
|
|
|
- incomingEdgeDependencies.push(incomingEdge);
|
|
|
- }
|
|
|
- else {
|
|
|
- if (incomingEdge.target.target === node.obj.nodeCreation) {
|
|
|
- console.log("unsetting incoming edge...");
|
|
|
- // Must set the value of every incoming edge to 'null' (with an EdgeUpdate):
|
|
|
- const edgeUnset = new EdgeUpdate(incomingEdge, null);
|
|
|
- sourceObj.outgoing.set(incomingEdge.getCreation().label, [edgeUnset,node.obj]);
|
|
|
- incomingEdgeDependencies.push(edgeUnset);
|
|
|
- compositeDeltas.push(edgeUnset);
|
|
|
- }
|
|
|
- else {
|
|
|
- console.log("edge already pointing somewhere else");
|
|
|
- // Edge is already pointing somewhere else: just include the operation as a dependency for the deletion.
|
|
|
- incomingEdgeDependencies.push(incomingEdge);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- const targetIncomings: Array<[EdgeCreation|EdgeUpdate|NodeDeletion, NodeObjType]>[] = [];
|
|
|
- for (const [outgoingEdge, targetObj] of node.obj.outgoing.values()) {
|
|
|
- targetObj.incoming.splice(targetObj.incoming.findIndex(([d,_])=>d===outgoingEdge), 1);
|
|
|
- targetIncomings.push(targetObj.incoming);
|
|
|
- outgoingEdgeDependencies.push(outgoingEdge);
|
|
|
- }
|
|
|
- const nodeDeletion = new NodeDeletion(node.obj.nodeCreation, outgoingEdgeDependencies, incomingEdgeDependencies);
|
|
|
- compositeDeltas.push(nodeDeletion);
|
|
|
- for (const targetIncoming of targetIncomings) {
|
|
|
- targetIncoming.push([nodeDeletion, node.obj]);
|
|
|
- }
|
|
|
- this.props.deleteNode(node.id);
|
|
|
- // console.log("deleteDependencies:", deleteDependencies);
|
|
|
- // console.log("compositeDeltas:", compositeDeltas);
|
|
|
- const tx = this.compositeLvl.createComposite(compositeDeltas);
|
|
|
- const version = this.versionRegistry.createVersion(this.props.version, tx);
|
|
|
- this.props.newDeltaHandler(tx);
|
|
|
- this.props.newVersionHandler(version);
|
|
|
+ if (node !== undefined) {
|
|
|
+ // middle mouse button click on node -> delete node (and incoming/outgoing edges)
|
|
|
+ return node.obj.delete();
|
|
|
}
|
|
|
}
|
|
|
+ return [];
|
|
|
+ })();
|
|
|
+
|
|
|
+ if (deltas.length > 0) {
|
|
|
+ // Let the world know that there is a new (composite) delta, and a new version:
|
|
|
+ const composite = this.compositeLvl.createComposite(deltas);
|
|
|
+ const version = this.versionRegistry.createVersion(this.props.version, composite);
|
|
|
+ this.props.newDeltaHandler(composite);
|
|
|
+ this.props.newVersionHandler(version);
|
|
|
+
|
|
|
+ // Actually update the graph state:
|
|
|
+ deltas.forEach(d => d.exec(this.props.graphDeltaExecutor));
|
|
|
}
|
|
|
+
|
|
|
this.mouseDownNode = null;
|
|
|
}
|
|
|
|
|
|
+
|
|
|
render() {
|
|
|
return (
|
|
|
<>
|
|
|
<Graph
|
|
|
- ref={this.graphRef}
|
|
|
graph={this.props.graph}
|
|
|
forces={this.props.forces}
|
|
|
mouseDownHandler={this.mouseDownHandler}
|