Bladeren bron

BROKEN: refactoring React components...

Joeri Exelmans 2 jaren geleden
bovenliggende
commit
8f31d2a4fe

+ 0 - 8
src/frontend/app.css

@@ -1,8 +0,0 @@
-html,body {
-    min-height: 100vh;
-
-    -webkit-user-select: none;
-    -moz-user-select: none;
-    -ms-user-select: none;
-    user-select: none;
-}

+ 35 - 562
src/frontend/app.tsx

@@ -1,564 +1,37 @@
-import { Buffer } from "buffer"; // NodeJS library
-
 import * as React from "react";
-import {Grid, Text, Title, Group, Stack, SimpleGrid, Button, Space, Textarea, Tabs, HoverCard, ActionIcon} from "@mantine/core";
-import {IconPlayerTrackPrev, IconPlayerTrackNext, IconInfoCircle} from "@tabler/icons";
-
-import {GraphState} from "../onion/graph_state"; 
-import {CompositeDelta, CompositeLevel} from "../onion/composite_delta";
-import {embed, Version, VersionRegistry} from "../onion/version";
-// import {
-//   GraphState,
-// } from "../onion/graph_state";
-
-// import {NodeCreation, NodeDeletion, EdgeCreation, EdgeUpdate} from "../onion/primitive_delta";
-import {PrimitiveDelta, PrimitiveRegistry} from "../onion/primitive_delta";
-import {PrimitiveValue, UUID} from "../onion/types";
-import {mockUuid} from "../onion/test_helpers";
-import {Delta} from "../onion/delta";
-
-import {TrivialParser} from "../parser/trivial_parser";
-
-import {d3Types, Graph} from "./graph"
-import {EditableGraph, UserEditCallback, SetNodePositionCallback, GraphType, NodeType, LinkType} from "./editable_graph";
-import {D3GraphStateUpdater} from "./d3_state";
-import {RountangleEditor} from "./rountangleEditor/RountangleEditor";
-import {
-  HistoryGraphType,
-  DependencyGraphType,
-  versionToNode,
-  setCurrentVersion,
-  appendToHistoryGraph,
-  fullDeltaId,
-  setDeltaActive,
-  setDeltaInactive,
-  deltaToDepGraphNode,
-  dependencyToDepGraphLink,
-  conflictToDepGraphLink,
-  fullVersionId,
-  addDeltaAndActivate,
-} from "./app_state";
-
-import {InsidenessParser} from "../parser/insideness_parser";
-import {visitPartialOrdering} from "../util/partial_ordering";
-
-const emptyGraph: d3Types.d3Graph<any,any> = {
-  nodes: [],
-  links: [],
-};
-
-// "physics" stuff (for graph layout)
-const graphForces = {charge: -200, center: 0.2, link: 2};
-
-const initialHistoryGraph = initialVersion => ({
-  nodes: [
-    versionToNode(initialVersion, true),
-  ],
-  links: [],
-});
-
-interface VersionedModelState {
-  version: Version; // the 'current version'
-  graph: GraphType; // the state what is displayed in the leftmost panel
-  historyGraph: HistoryGraphType; // the state of what is displayed in the middle panel
-  dependencyGraphL1: DependencyGraphType; // the state of what is displayed in the rightmost panel
-  dependencyGraphL0: DependencyGraphType; // the state of what is displayed in the rightmost panel
-  d3StateUpdater: D3GraphStateUpdater;
-  graphState: GraphState;
-  versionRegistry: VersionRegistry;
-  compositeLevel: CompositeLevel;
-}
-
-export interface VersionedModelProps {
-  title: string;
-  generateUUID: () => UUID;
-  primitiveRegistry: PrimitiveRegistry;
-
-  readonly?: boolean;
-
-  onUserEdit?: UserEditCallback;
-  setNextNodePosition: SetNodePositionCallback;
-  onUndoClicked: (parentVersion: Version, deltaToUndo: Delta) => void;
-  onRedoClicked: (childVersion: Version, deltaToRedo: Delta) => void;
-  onVersionClicked: (Version) => void;
-
-  state: VersionedModelState;
-  setState: (callback: (VersionedModelState) => VersionedModelState) => void;
-
-  defaultTab0: string;
-  defaultTab1: string;
-  tabs: string[];
-}
-
-class VersionedModel extends React.Component<VersionedModelProps, {}> {
-  readonly textarearef: React.RefObject<HTMLTextAreaElement>;
-
-  constructor(props) {
-    super(props);
-    this.textarearef = React.createRef();
-  }
-
-  render() {
-    const onMergeClicked = () => {
-      // const text = this.textarearef.current?.value;
-      // if (text) {
-      //   const lines = text.split('\n');
-
-      //   const versionIds: Buffer[] = lines.map(s => {
-      //     try {
-      //       return Buffer.from(s, 'base64');
-      //     } catch (e) {
-      //       return Buffer.alloc(0);
-      //     }
-      //   }).filter(buf => buf.length !== 0);
-
-      //   let versionsToMerge;
-      //   try {
-      //     versionsToMerge = versionIds.map((buf: Buffer) => {
-      //       return this.props.versionRegistry.lookup(buf);
-      //     });
-      //   } catch (e) {
-      //     alert("Input error:" + e.toString());
-      //     return;
-      //   }
-
-      //   const mergedVersions = this.props.versionRegistry.merge(versionsToMerge);
-
-      //   console.log("mergedVersions:", mergedVersions);
-
-      //   const add = v => {
-      //     this.props.setState(({historyGraph: prevHistoryGraph, ...rest}) => {
-      //       return {
-      //         historyGraph: appendToHistoryGraph(prevHistoryGraph, v),
-      //         ...rest,
-      //       };
-      //     });
-      //   };
-
-      //   // add all mergedVersions (and their parents) to history graph:
-      //   // this may be overkill (and not so scalable), but it does the job :)
-      //   mergedVersions.forEach(v => {
-      //     const addParents = v => {
-      //       v.parents.forEach(([v]) => {
-      //         addParents(v);
-      //       });
-      //       add(v);
-      //     }
-      //     addParents(v);
-      //   });
-      // }
-    }
-
-    function makeInfoHoverCardIcon(contents) {
-      return (
-        <HoverCard shadow="md">
-          <HoverCard.Target>
-            <ActionIcon>
-              <IconInfoCircle size={18}/>
-            </ActionIcon>
-          </HoverCard.Target>
-          <HoverCard.Dropdown>
-            {contents}
-          </HoverCard.Dropdown>
-        </HoverCard>
-      );
-    }
-
-    const makeTabs = (defaultTab, tabs) => (
-      <Tabs defaultValue={defaultTab} keepMounted={false}>
-        <Tabs.List>
-          {tabs.map(tab => {
-            if (tab === "editor")
-              return <Tabs.Tab key={tab} value="editor">Editor</Tabs.Tab>;
-            if (tab === "state")
-              return <Tabs.Tab key={tab} value="state">State</Tabs.Tab>;
-            if (tab === "history")
-              return <Tabs.Tab key={tab} value="history">History</Tabs.Tab>;
-            if (tab === "dependencyL1")
-              return <Tabs.Tab key={tab} value="dependencyL1">Deltas (L1)</Tabs.Tab>;
-            if (tab === "dependencyL0")
-              return <Tabs.Tab key={tab} value="dependencyL0">Deltas (L0)</Tabs.Tab>;
-          })}
-        </Tabs.List>
-
-        <Tabs.Panel value="state">
-          {makeInfoHoverCardIcon(<>
-            <Text>Left mouse button: Drag node around.</Text>
-            <Text>Middle mouse button: Delete node (+ incoming/outgoing edges).</Text>
-            <Text>Right mouse button: Create node or edge.</Text>
-            <Text>Mouse wheel: Zoom.</Text>
-          </>)}
-          {this.props.readonly?(
-            <Graph graph={this.props.state.graph} forces={graphForces} />
-          ):(
-            <EditableGraph
-              graph={this.props.state.graph}
-              graphState={this.props.state.graphState}
-              forces={graphForces}
-              generateUUID={this.props.generateUUID}
-              primitiveRegistry={this.props.primitiveRegistry}
-              setNextNodePosition={this.props.setNextNodePosition}
-              currentVersion={this.props.state.version}
-              onUserEdit={this.props.onUserEdit} />
-          )}
-        </Tabs.Panel>
-
-        <Tabs.Panel value="editor">
-          {makeInfoHoverCardIcon(
-            <Text>New Rountangle by Alt + Click</Text>
-          )}
-          <RountangleEditor
-            graph={this.props.state.graph}
-            generateUUID={this.props.generateUUID}
-            primitiveRegistry={this.props.primitiveRegistry}
-            onUserEdit={this.props.onUserEdit}
-            graphState={this.props.state.graphState} />
-        </Tabs.Panel>
-
-        <Tabs.Panel value="dependencyL1">
-          {makeInfoHoverCardIcon(
-            <Text>Active deltas are bold.</Text>
-          )}
-          <Graph graph={this.props.state.dependencyGraphL1} forces={graphForces} />
-        </Tabs.Panel>
-
-        <Tabs.Panel value="dependencyL0">
-          {makeInfoHoverCardIcon(
-            <Text>Active deltas are bold.</Text>
-          )}
-          <Graph graph={this.props.state.dependencyGraphL0} forces={graphForces} />
-        </Tabs.Panel>
-
-        <Tabs.Panel value="history">
-          {makeInfoHoverCardIcon(<>
-            <Text>All links are parent links.</Text>
-            <Text>Right or middle mouse button: Load version.</Text>
-          </>)}
-          <Graph graph={this.props.state.historyGraph} forces={graphForces}
-            mouseUpHandler={(e, {x, y}, node) => node ? this.props.onVersionClicked(node.obj) : undefined} />
-        </Tabs.Panel>
-      </Tabs>
-    );
-
-    const undoButtons = this.props.state.version.parents.map(([parentVersion,deltaToUndo]) => {
-      return (
-        <div key={fullVersionId(parentVersion)}>
-          <Button fullWidth={true} compact={true} leftIcon={<IconPlayerTrackPrev size={18}/>} onClick={this.props.onUndoClicked.bind(null, parentVersion, deltaToUndo)}>
-            UNDO {deltaToUndo.getDescription()}
-          </Button>
-          <Space h="xs"/>
-        </div>
-      );
-    });
-    const redoButtons = this.props.state.version.children.map(([childVersion,deltaToRedo]) => {
-      return (
-        <div key={fullVersionId(childVersion)}>
-          <Button style={{width: "100%"}} compact={true} rightIcon={<IconPlayerTrackNext size={18}/>} onClick={this.props.onRedoClicked.bind(null, childVersion, deltaToRedo)}>
-            REDO {deltaToRedo.getDescription()}
-          </Button>
-          <Space h="xs"/>
-        </div>
-      );
-    });
-
-    return (
-      <>
-        <Title order={4}>{this.props.title}</Title>
-        <Stack>
-          {makeTabs(this.props.defaultTab0, this.props.tabs)}
-          {makeTabs(this.props.defaultTab1, this.props.tabs)}
-        </Stack>
-
-        <SimpleGrid cols={2}>
-          <div>
-            {undoButtons}
-          </div>
-          <div>
-            {redoButtons}
-          </div>
-        </SimpleGrid>
-
-{/*        <Textarea size="xs" label="Versions to merge:" ref={this.textarearef}
-          autosize placeholder="Right-click on version in History Graph to add it to this textbox"></Textarea>
-        <Button onClick={() => {if (this.textarearef.current) this.textarearef.current.value = "";}}>CLEAR</Button>
-        &nbsp;
-        <Button onClick={onMergeClicked}>MERGE</Button>
-*/}      </>
-    );
-  }
-}
-
-interface AppState {
-  cs: VersionedModelState;
-  corr: VersionedModelState;
-  as: VersionedModelState;
-}
-
-export class App extends React.Component<{}, AppState> {
-  generateUUID = mockUuid();
-  primitiveRegistry = new PrimitiveRegistry();
-  parser: TrivialParser;
-
-  parsedTo: Map<Version,{corr: Version, target: Version}>;
-  renderedTo: Map<Version,{corr: Version, target: Version}>;
-
-  setCsState(callback) {
-    return this.setState(({cs, ...rest}) => ({cs: callback(cs), ...rest}));
-  }
-  setCorrState(callback) {
-    return this.setState(({corr, ...rest}) => ({corr: callback(corr), ...rest}));
-  }
-  setAsState(callback) {
-    return this.setState(({as, ...rest}) => ({as: callback(as), ...rest}));
-  }
-
-  constructor(props) {
-    super(props);
-
-    const makeModelState = (setState) => {
-      // 'Glue' callback
-      const setGraph = (callback: (GraphType) => GraphType) => {
-        setState(({graph, ...rest}) => ({
-          graph: callback(graph),
-          ...rest,
-        }));
-      };
-      const versionRegistry = new VersionRegistry();
-      const d3StateUpdater = new D3GraphStateUpdater(setGraph);
-      return {
-        version: versionRegistry.initialVersion,
-        graph: emptyGraph,
-        historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
-        dependencyGraphL1: emptyGraph,
-        dependencyGraphL0: emptyGraph,
-        compositeLevel: new CompositeLevel(),
-        versionRegistry,
-        d3StateUpdater,
-        graphState: new GraphState(),
-      };
-    }
-
-    this.state = {
-      cs: makeModelState(this.setCsState.bind(this)),
-      corr: makeModelState(this.setCorrState.bind(this)),
-      as: makeModelState(this.setAsState.bind(this)),
-    };
-
-    this.parsedTo = new Map([[this.state.cs.versionRegistry.initialVersion, {corr:this.state.corr.versionRegistry.initialVersion, target:this.state.as.versionRegistry.initialVersion}]]);
-    this.renderedTo = new Map([[this.state.as.versionRegistry.initialVersion, {corr:this.state.corr.versionRegistry.initialVersion, target:this.state.cs.versionRegistry.initialVersion}]]);
-
-    this.parser = new TrivialParser(this.primitiveRegistry, this.generateUUID);
-  }
-
-  render() {
-    const addVersionAndDeltas = ({version, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}: VersionedModelState, newVersion: Version, composite: CompositeDelta) => {
-      return {
-        version: newVersion,
-        // add new version to history graph + highlight the new version as the current version:
-        historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), version, newVersion),
-        // add the composite delta to the L1-graph + highlight it as 'active':
-        dependencyGraphL1: composite.deltas.length > 0 ? addDeltaAndActivate(dependencyGraphL1, composite) : dependencyGraphL1, // never add an empty composite
-        // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
-        dependencyGraphL0: composite.deltas.reduce(
-          (graph, delta) => {
-            return addDeltaAndActivate(graph, delta);
-          }, dependencyGraphL0),
-        ...rest,
-      };
-    };
-
-    const undoWithoutUpdatingHistoryGraph = (state, setState, deltaToUndo) => {
-      state.graphState.unexec(deltaToUndo, state.d3StateUpdater);
-      setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => ({
-        dependencyGraphL1: setDeltaInactive(prevDepGraphL1, deltaToUndo),
-        dependencyGraphL0: deltaToUndo.deltas.reduce((prevDepGraphL0, delta) => setDeltaInactive(prevDepGraphL0, delta), prevDepGraphL0),
-        ...rest,
-      }));
-    };
-    const redoWithoutUpdatingHistoryGraph = (state, setState, deltaToRedo) => {
-      state.graphState.exec(deltaToRedo, state.d3StateUpdater);
-      setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => ({
-        dependencyGraphL1: setDeltaActive(prevDepGraphL1, deltaToRedo),
-        dependencyGraphL0: deltaToRedo.deltas.reduce((prevDepGraphL0, delta) => setDeltaActive(prevDepGraphL0, delta), prevDepGraphL0),
-        ...rest,
-      }));
-    };
-
-    const onUndoClicked = (state, setState, parentVersion, deltaToUndo) => {
-      undoWithoutUpdatingHistoryGraph(state, setState, deltaToUndo);
-      setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
-        version: parentVersion,
-        historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, parentVersion),
-        ...rest,
-      }));
-    };
-    const onRedoClicked = (state, setState, childVersion, deltaToRedo) => {
-      redoWithoutUpdatingHistoryGraph(state, setState, deltaToRedo);
-      setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
-        version: childVersion,
-        historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, childVersion),
-        ...rest,
-      }));
-    };
-
-    const gotoVersion = (state, setState, chosenVersion: Version) => {
-      const path = state.version.findPathTo(chosenVersion);
-      if (path === undefined) {
-        throw new Error("Could not find path to version!");
-      }
-      for (const [linkType, delta] of path) {
-        if (linkType === 'p') {
-          undoWithoutUpdatingHistoryGraph(state, setState, delta);
-        }
-        else if (linkType === 'c') {
-          redoWithoutUpdatingHistoryGraph(state, setState, delta);
-        }
-      }
-      setState(({historyGraph, version, ...rest}) => ({
-        version: chosenVersion,
-        historyGraph: setCurrentVersion(historyGraph, version, chosenVersion),
-        ...rest,
-      }));
-    }
-
-    // Pure function
-    // Replays all deltas in a version to compute the graph state of that version.
-    function getGraphState(version: Version, additionalDeltas: PrimitiveDelta[]):GraphState {
-      const graphState = new GraphState();
-      for (const d of [...version].reverse()) {
-        graphState.exec(d);
-      }
-      for (const d of additionalDeltas) {
-        graphState.exec(d);
-      }
-      return graphState;
-    }
-
-
-    const parseOrRender = (sourceState: VersionedModelState, targetState: VersionedModelState,
-          mapping, parse: boolean) => {
-      return (sourceDeltas: PrimitiveDelta[], description: string) => {
-
-        const targetDescription = (parse ? "parse:" : "render:") + description;
-
-        const sourceParent = sourceState.version;
-        const {corr: corrParent, target: targetParent} = mapping.get(sourceParent) || (()=>{throw new Error("X")})();
-
-        // SLOW! When performance becomes a problem, look for a faster alternative
-        const sourceGraphState = getGraphState(sourceParent, []);
-        const corrGraphState = getGraphState(corrParent, []);
-        const targetGraphState = getGraphState(targetParent, []);
-
-        const {corrDeltas, targetDeltas, sourceOverrides, targetOverrides} = this.parser.propagate_change(parse, sourceDeltas, sourceGraphState, corrGraphState, targetGraphState);
-
-        // console.log({corrDeltas})
-
-        const sourceComposite = sourceState.compositeLevel.createComposite(sourceDeltas, description);
-        const newSourceVersion = sourceState.versionRegistry.createVersion(sourceState.version, sourceComposite);
-
-        const targetComposite = targetState.compositeLevel.createComposite(targetDeltas, targetDescription);
-        const newTargetVersion =  (targetComposite.deltas.length > 0) ?
-          targetState.versionRegistry.createVersion(targetParent, targetComposite)
-          : targetState.version; // do not create a new target version
-
-        const csComposite = parse ? sourceComposite : targetComposite;
-        const asComposite = parse ? targetComposite : sourceComposite;
-        const newCsVersion = parse ? newSourceVersion : newTargetVersion;
-        const newAsVersion = parse ? newTargetVersion : newSourceVersion;
-        const csOverrides = parse ? sourceOverrides : targetOverrides;
-        const asOverrides = parse ? targetOverrides : sourceOverrides;
-
-
-        const corrDeltasOrderedByDependency: PrimitiveDelta[] = [];
-        visitPartialOrdering([
-          ...sourceDeltas.map(d => sourceOverrides.get(d) || d),
-          ...targetDeltas.map(d => targetOverrides.get(d) || d),
-          ...corrDeltas],
-          (d: Delta) => d.getDependencies(),
-          (d: Delta) => corrDeltasOrderedByDependency.push(d));
-
-        // console.log({corrDeltasOrderedByDependency});
-
-        const corrComposite = this.state.corr.compositeLevel.createComposite(corrDeltasOrderedByDependency, targetDescription);
-        const newCorrVersion = this.state.corr.versionRegistry.createVersion(corrParent, corrComposite,
-          embed(
-            ["cs", newCsVersion, csOverrides],
-            ["as", newAsVersion, asOverrides],
-          ));
-
-        this.parsedTo.set(newCsVersion, {corr: newCorrVersion, target: newAsVersion});
-        this.renderedTo.set(newAsVersion, {corr: newCorrVersion, target: newCsVersion});
-
-        // Update state:
-        this.setState(({cs, corr, as}) => {
-          const result = {
-            cs: addVersionAndDeltas(cs, newCsVersion, csComposite),
-            corr: addVersionAndDeltas(corr, newCorrVersion, corrComposite),
-            as: addVersionAndDeltas(as, newAsVersion, asComposite),
-          };
-          return result;
-        });
-
-        gotoVersion(this.state.cs, this.setCsState.bind(this), newCsVersion);
-        gotoVersion(this.state.corr, this.setCorrState.bind(this), newCorrVersion);
-        gotoVersion(this.state.as, this.setAsState.bind(this), newAsVersion);
-      };
-    };
-
-    const parseCsChange = parseOrRender(this.state.cs, this.state.as, this.parsedTo, true);
-    const renderAsChange = parseOrRender(this.state.as, this.state.cs, this.renderedTo, false);
-
-    return (<>
-      <Grid grow>
-        <Grid.Col span={1}>
-          <VersionedModel title="Concrete Syntax"
-            generateUUID={this.generateUUID}
-            primitiveRegistry={this.primitiveRegistry}
-            tabs={["editor", "state", "history", "dependencyL1", "dependencyL0"]}
-            defaultTab0="editor"
-            defaultTab1="history"
-            state={this.state.cs}
-            setState={this.setCsState.bind(this)}
-            setNextNodePosition={(x,y)=>{this.state.cs.d3StateUpdater.x = x; this.state.cs.d3StateUpdater.y = y;}}
-            onUserEdit={parseCsChange}
-            onUndoClicked={onUndoClicked.bind(null, this.state.cs, this.setCsState.bind(this))}
-            onRedoClicked={onRedoClicked.bind(null, this.state.cs, this.setCsState.bind(this))}
-            onVersionClicked={version => gotoVersion(this.state.cs, this.setCsState.bind(this), version)}
-          />
-        </Grid.Col>
-        <Grid.Col span={1}>
-          <VersionedModel title="Correspondence" readonly
-            generateUUID={this.generateUUID}
-            primitiveRegistry={this.primitiveRegistry}
-            tabs={["state", "history", "dependencyL1", "dependencyL0"]}
-            defaultTab0="state"
-            defaultTab1="history"
-            setNextNodePosition={(x,y)=>{this.state.corr.d3StateUpdater.x = x; this.state.corr.d3StateUpdater.y = y;}}
-            state={this.state.corr}
-            setState={this.setCorrState.bind(this)}
-            onUndoClicked={onUndoClicked.bind(null, this.state.corr, this.setCorrState.bind(this))}
-            onRedoClicked={onRedoClicked.bind(null, this.state.corr, this.setCorrState.bind(this))}
-            onVersionClicked={version => gotoVersion(this.state.corr, this.setCorrState.bind(this), version)}
-          />
 
-        </Grid.Col>
-        <Grid.Col span={1}>
-          <VersionedModel title="Abstract Syntax"
-            generateUUID={this.generateUUID}
-            primitiveRegistry={this.primitiveRegistry}
-            tabs={["state", "history", "dependencyL1", "dependencyL0"]}
-            defaultTab0="state"
-            defaultTab1="history"
-            setNextNodePosition={(x,y)=>{this.state.as.d3StateUpdater.x = x; this.state.as.d3StateUpdater.y = y;}}
-            state={this.state.as}
-            setState={this.setAsState.bind(this)}
-            onUserEdit={renderAsChange}
-            onUndoClicked={onUndoClicked.bind(null, this.state.as, this.setAsState.bind(this))}
-            onRedoClicked={onRedoClicked.bind(null, this.state.as, this.setAsState.bind(this))}
-            onVersionClicked={version => gotoVersion(this.state.as, this.setAsState.bind(this), version)}
-          />
-        </Grid.Col>
-      </Grid>
-    </>);
-  }
-}
+import {DemoPD} from "./demo_pd";
+import {DemoCorr} from "./demo_corr";
+
+import {Stack, Group, Text, Tabs, createStyles} from "@mantine/core";
+
+// const useStyles = createStyles(theme => ({
+//   root: {
+//     display: "flex",
+//     // flex: "1",
+//   },
+//   panel: {
+//     display: "flex",
+//     flex: "1",
+//   }
+// }));
+
+export function App(props) {
+  // const {classes} = useStyles();
+
+  return <>
+    <Text>Select a demo:</Text>
+    <Tabs orientation="vertical" defaultValue="pd">
+      <Tabs.List>
+        <Tabs.Tab value="pd">Primitive Delta</Tabs.Tab>
+        <Tabs.Tab value="corr">Correspondence</Tabs.Tab>
+      </Tabs.List>
+      <Tabs.Panel value="pd">
+        <DemoPD/>
+      </Tabs.Panel>
+      <Tabs.Panel value="corr">
+        <DemoCorr/>
+      </Tabs.Panel>
+    </Tabs>
+  </>;
+}

+ 9 - 0
src/frontend/app_state.ts

@@ -9,6 +9,15 @@ export type DependencyGraphType = d3Types.d3Graph<Delta,null>;
 
 ///// ALL OF THESE ARE PURE FUNCTIONS: //////
 
+export function initialHistoryGraph(initialVersion) {
+  return {
+    nodes: [
+      versionToNode(initialVersion, true),
+    ],
+    links: [],
+  };
+}
+
 export function fullVersionId(version: Version): string {
   return version.hash.toString('base64');
 }

+ 8 - 0
src/frontend/constants.ts

@@ -0,0 +1,8 @@
+import {d3Types} from "./graph"
+
+export const emptyGraph: d3Types.d3Graph<any,any> = {
+  nodes: [],
+  links: [],
+};
+
+export const graphForces = {charge: -200, center: 0.2, link: 2};

+ 543 - 0
src/frontend/demo_corr.tsx

@@ -0,0 +1,543 @@
+import { Buffer } from "buffer"; // NodeJS library
+
+import * as React from "react";
+import {Grid, Text, Title, Group, Stack, SimpleGrid, Button, Space, Textarea, Tabs, HoverCard, ActionIcon} from "@mantine/core";
+import {IconPlayerTrackPrev, IconPlayerTrackNext, IconInfoCircle} from "@tabler/icons";
+
+import {GraphState} from "../onion/graph_state"; 
+import {CompositeDelta, CompositeLevel} from "../onion/composite_delta";
+import {embed, Version, VersionRegistry} from "../onion/version";
+import {PrimitiveDelta, PrimitiveRegistry} from "../onion/primitive_delta";
+import {PrimitiveValue, UUID} from "../onion/types";
+import {mockUuid} from "../onion/test_helpers";
+import {Delta} from "../onion/delta";
+
+import {TrivialParser} from "../parser/trivial_parser";
+
+import {d3Types, Graph} from "./graph"
+import {EditableGraph, UserEditCallback, SetNodePositionCallback, GraphType, NodeType, LinkType} from "./editable_graph";
+import {D3GraphStateUpdater} from "./d3_state";
+import {emptyGraph, graphForces} from "./constants";
+import {RountangleEditor} from "./rountangleEditor/RountangleEditor";
+import {
+  HistoryGraphType,
+  DependencyGraphType,
+  versionToNode,
+  setCurrentVersion,
+  appendToHistoryGraph,
+  fullDeltaId,
+  setDeltaActive,
+  setDeltaInactive,
+  deltaToDepGraphNode,
+  dependencyToDepGraphLink,
+  conflictToDepGraphLink,
+  fullVersionId,
+  addDeltaAndActivate,
+  initialHistoryGraph,
+} from "./app_state";
+
+import {visitPartialOrdering} from "../util/partial_ordering";
+
+interface VersionedModelState {
+  version: Version; // the 'current version'
+  graph: GraphType; // the state what is displayed in the leftmost panel
+  historyGraph: HistoryGraphType; // the state of what is displayed in the middle panel
+  dependencyGraphL1: DependencyGraphType; // the state of what is displayed in the rightmost panel
+  dependencyGraphL0: DependencyGraphType; // the state of what is displayed in the rightmost panel
+  graphState: GraphState;
+  versionRegistry: VersionRegistry;
+  compositeLevel: CompositeLevel;
+}
+
+export interface VersionedModelProps {
+  title: string;
+  generateUUID: () => UUID;
+  primitiveRegistry: PrimitiveRegistry;
+
+  readonly?: boolean;
+
+  onUserEdit?: UserEditCallback;
+  setNextNodePosition: SetNodePositionCallback;
+  onUndoClicked: (parentVersion: Version, deltaToUndo: Delta) => void;
+  onRedoClicked: (childVersion: Version, deltaToRedo: Delta) => void;
+  onVersionClicked: (Version) => void;
+
+  state: VersionedModelState;
+  setState: (callback: (VersionedModelState) => VersionedModelState) => void;
+
+  defaultTab0: string;
+  defaultTab1: string;
+  tabs: string[];
+}
+
+class VersionedModel extends React.Component<VersionedModelProps, {}> {
+  readonly textarearef: React.RefObject<HTMLTextAreaElement>;
+
+  constructor(props) {
+    super(props);
+    this.textarearef = React.createRef();
+  }
+
+  render() {
+    const onMergeClicked = () => {
+      // const text = this.textarearef.current?.value;
+      // if (text) {
+      //   const lines = text.split('\n');
+
+      //   const versionIds: Buffer[] = lines.map(s => {
+      //     try {
+      //       return Buffer.from(s, 'base64');
+      //     } catch (e) {
+      //       return Buffer.alloc(0);
+      //     }
+      //   }).filter(buf => buf.length !== 0);
+
+      //   let versionsToMerge;
+      //   try {
+      //     versionsToMerge = versionIds.map((buf: Buffer) => {
+      //       return this.props.versionRegistry.lookup(buf);
+      //     });
+      //   } catch (e) {
+      //     alert("Input error:" + e.toString());
+      //     return;
+      //   }
+
+      //   const mergedVersions = this.props.versionRegistry.merge(versionsToMerge);
+
+      //   console.log("mergedVersions:", mergedVersions);
+
+      //   const add = v => {
+      //     this.props.setState(({historyGraph: prevHistoryGraph, ...rest}) => {
+      //       return {
+      //         historyGraph: appendToHistoryGraph(prevHistoryGraph, v),
+      //         ...rest,
+      //       };
+      //     });
+      //   };
+
+      //   // add all mergedVersions (and their parents) to history graph:
+      //   // this may be overkill (and not so scalable), but it does the job :)
+      //   mergedVersions.forEach(v => {
+      //     const addParents = v => {
+      //       v.parents.forEach(([v]) => {
+      //         addParents(v);
+      //       });
+      //       add(v);
+      //     }
+      //     addParents(v);
+      //   });
+      // }
+    }
+
+    function makeInfoHoverCardIcon(contents) {
+      return (
+        <HoverCard shadow="md">
+          <HoverCard.Target>
+            <ActionIcon>
+              <IconInfoCircle size={18}/>
+            </ActionIcon>
+          </HoverCard.Target>
+          <HoverCard.Dropdown>
+            {contents}
+          </HoverCard.Dropdown>
+        </HoverCard>
+      );
+    }
+
+    const makeTabs = (defaultTab, tabs) => (
+      <Tabs defaultValue={defaultTab} keepMounted={false}>
+        <Tabs.List>
+          {tabs.map(tab => {
+            if (tab === "editor")
+              return <Tabs.Tab key={tab} value="editor">Editor</Tabs.Tab>;
+            if (tab === "state")
+              return <Tabs.Tab key={tab} value="state">State</Tabs.Tab>;
+            if (tab === "history")
+              return <Tabs.Tab key={tab} value="history">History</Tabs.Tab>;
+            if (tab === "dependencyL1")
+              return <Tabs.Tab key={tab} value="dependencyL1">Deltas (L1)</Tabs.Tab>;
+            if (tab === "dependencyL0")
+              return <Tabs.Tab key={tab} value="dependencyL0">Deltas (L0)</Tabs.Tab>;
+          })}
+        </Tabs.List>
+
+        <Tabs.Panel value="state">
+          {makeInfoHoverCardIcon(<>
+            <Text>Left mouse button: Drag node around.</Text>
+            <Text>Middle mouse button: Delete node (+ incoming/outgoing edges).</Text>
+            <Text>Right mouse button: Create node or edge.</Text>
+            <Text>Mouse wheel: Zoom.</Text>
+          </>)}
+          {this.props.readonly?(
+            <Graph graph={this.props.state.graph} forces={graphForces} />
+          ):(
+            <EditableGraph
+              graph={this.props.state.graph}
+              graphState={this.props.state.graphState}
+              forces={graphForces}
+              generateUUID={this.props.generateUUID}
+              primitiveRegistry={this.props.primitiveRegistry}
+              setNextNodePosition={this.props.setNextNodePosition}
+              onUserEdit={this.props.onUserEdit} />
+          )}
+        </Tabs.Panel>
+
+        <Tabs.Panel value="editor">
+          {makeInfoHoverCardIcon(
+            <Text>New Rountangle by Alt + Click</Text>
+          )}
+          <RountangleEditor
+            graph={this.props.state.graph}
+            generateUUID={this.props.generateUUID}
+            primitiveRegistry={this.props.primitiveRegistry}
+            onUserEdit={this.props.onUserEdit}
+            graphState={this.props.state.graphState} />
+        </Tabs.Panel>
+
+        <Tabs.Panel value="dependencyL1">
+          {makeInfoHoverCardIcon(
+            <Text>Active deltas are bold.</Text>
+          )}
+          <Graph graph={this.props.state.dependencyGraphL1} forces={graphForces} />
+        </Tabs.Panel>
+
+        <Tabs.Panel value="dependencyL0">
+          {makeInfoHoverCardIcon(
+            <Text>Active deltas are bold.</Text>
+          )}
+          <Graph graph={this.props.state.dependencyGraphL0} forces={graphForces} />
+        </Tabs.Panel>
+
+        <Tabs.Panel value="history">
+          {makeInfoHoverCardIcon(<>
+            <Text>All links are parent links.</Text>
+            <Text>Right or middle mouse button: Load version.</Text>
+          </>)}
+          <Graph graph={this.props.state.historyGraph} forces={graphForces}
+            mouseUpHandler={(e, {x, y}, node) => node ? this.props.onVersionClicked(node.obj) : undefined} />
+        </Tabs.Panel>
+      </Tabs>
+    );
+
+    const undoButtons = this.props.state.version.parents.map(([parentVersion,deltaToUndo]) => {
+      return (
+        <div key={fullVersionId(parentVersion)}>
+          <Button fullWidth={true} compact={true} leftIcon={<IconPlayerTrackPrev size={18}/>} onClick={this.props.onUndoClicked.bind(null, parentVersion, deltaToUndo)}>
+            UNDO {deltaToUndo.getDescription()}
+          </Button>
+          <Space h="xs"/>
+        </div>
+      );
+    });
+    const redoButtons = this.props.state.version.children.map(([childVersion,deltaToRedo]) => {
+      return (
+        <div key={fullVersionId(childVersion)}>
+          <Button style={{width: "100%"}} compact={true} rightIcon={<IconPlayerTrackNext size={18}/>} onClick={this.props.onRedoClicked.bind(null, childVersion, deltaToRedo)}>
+            REDO {deltaToRedo.getDescription()}
+          </Button>
+          <Space h="xs"/>
+        </div>
+      );
+    });
+
+    return (
+      <>
+        <Title order={4}>{this.props.title}</Title>
+        <Stack>
+          {makeTabs(this.props.defaultTab0, this.props.tabs)}
+          {makeTabs(this.props.defaultTab1, this.props.tabs)}
+        </Stack>
+
+        <SimpleGrid cols={2}>
+          <div>
+            {undoButtons}
+          </div>
+          <div>
+            {redoButtons}
+          </div>
+        </SimpleGrid>
+
+{/*        <Textarea size="xs" label="Versions to merge:" ref={this.textarearef}
+          autosize placeholder="Right-click on version in History Graph to add it to this textbox"></Textarea>
+        <Button onClick={() => {if (this.textarearef.current) this.textarearef.current.value = "";}}>CLEAR</Button>
+        &nbsp;
+        <Button onClick={onMergeClicked}>MERGE</Button>
+*/}      </>
+    );
+  }
+}
+
+interface DemoCorrState {
+  cs: VersionedModelState;
+  corr: VersionedModelState;
+  as: VersionedModelState;
+}
+
+export class DemoCorr extends React.Component<{}, DemoCorrState> {
+  generateUUID = mockUuid();
+  primitiveRegistry = new PrimitiveRegistry();
+  parser: TrivialParser;
+
+  parsedTo: Map<Version,{corr: Version, target: Version}>;
+  renderedTo: Map<Version,{corr: Version, target: Version}>;
+
+  setCsState(callback) {
+    return this.setState(({cs, ...rest}) => ({cs: callback(cs), ...rest}));
+  }
+  setCorrState(callback) {
+    return this.setState(({corr, ...rest}) => ({corr: callback(corr), ...rest}));
+  }
+  setAsState(callback) {
+    return this.setState(({as, ...rest}) => ({as: callback(as), ...rest}));
+  }
+
+  constructor(props) {
+    super(props);
+
+    const makeModelState = (setState) => {
+      // 'Glue' callback
+      const setGraph = (callback: (GraphType) => GraphType) => {
+        setState(({graph, ...rest}) => ({
+          graph: callback(graph),
+          ...rest,
+        }));
+      };
+      const versionRegistry = new VersionRegistry();
+      const d3StateUpdater = new D3GraphStateUpdater(setGraph);
+      return {
+        version: versionRegistry.initialVersion,
+        graph: emptyGraph,
+        historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
+        dependencyGraphL1: emptyGraph,
+        dependencyGraphL0: emptyGraph,
+        compositeLevel: new CompositeLevel(),
+        versionRegistry,
+        // d3StateUpdater,
+        graphState: new GraphState(d3StateUpdater),
+      };
+    }
+
+    this.state = {
+      cs: makeModelState(this.setCsState.bind(this)),
+      corr: makeModelState(this.setCorrState.bind(this)),
+      as: makeModelState(this.setAsState.bind(this)),
+    };
+
+    this.parsedTo = new Map([[this.state.cs.versionRegistry.initialVersion, {corr:this.state.corr.versionRegistry.initialVersion, target:this.state.as.versionRegistry.initialVersion}]]);
+    this.renderedTo = new Map([[this.state.as.versionRegistry.initialVersion, {corr:this.state.corr.versionRegistry.initialVersion, target:this.state.cs.versionRegistry.initialVersion}]]);
+
+    this.parser = new TrivialParser(this.primitiveRegistry, this.generateUUID);
+  }
+
+  render() {
+    const addVersionAndDeltas = ({version, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}: VersionedModelState, newVersion: Version, composite: CompositeDelta) => {
+      return {
+        version: newVersion,
+        // add new version to history graph + highlight the new version as the current version:
+        historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), version, newVersion),
+        // add the composite delta to the L1-graph + highlight it as 'active':
+        dependencyGraphL1: composite.deltas.length > 0 ? addDeltaAndActivate(dependencyGraphL1, composite) : dependencyGraphL1, // never add an empty composite
+        // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
+        dependencyGraphL0: composite.deltas.reduce(
+          (graph, delta) => {
+            return addDeltaAndActivate(graph, delta);
+          }, dependencyGraphL0),
+        ...rest,
+      };
+    };
+
+    const undoWithoutUpdatingHistoryGraph = (state, setState, deltaToUndo) => {
+      state.graphState.unexec(deltaToUndo);
+      setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => ({
+        dependencyGraphL1: setDeltaInactive(prevDepGraphL1, deltaToUndo),
+        dependencyGraphL0: deltaToUndo.deltas.reduce((prevDepGraphL0, delta) => setDeltaInactive(prevDepGraphL0, delta), prevDepGraphL0),
+        ...rest,
+      }));
+    };
+    const redoWithoutUpdatingHistoryGraph = (state, setState, deltaToRedo) => {
+      state.graphState.exec(deltaToRedo);
+      setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => ({
+        dependencyGraphL1: setDeltaActive(prevDepGraphL1, deltaToRedo),
+        dependencyGraphL0: deltaToRedo.deltas.reduce((prevDepGraphL0, delta) => setDeltaActive(prevDepGraphL0, delta), prevDepGraphL0),
+        ...rest,
+      }));
+    };
+
+    const onUndoClicked = (state, setState, parentVersion, deltaToUndo) => {
+      undoWithoutUpdatingHistoryGraph(state, setState, deltaToUndo);
+      setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+        version: parentVersion,
+        historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, parentVersion),
+        ...rest,
+      }));
+    };
+    const onRedoClicked = (state, setState, childVersion, deltaToRedo) => {
+      redoWithoutUpdatingHistoryGraph(state, setState, deltaToRedo);
+      setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+        version: childVersion,
+        historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, childVersion),
+        ...rest,
+      }));
+    };
+
+    const gotoVersion = (state, setState, chosenVersion: Version) => {
+      const path = state.version.findPathTo(chosenVersion);
+      if (path === undefined) {
+        throw new Error("Could not find path to version!");
+      }
+      for (const [linkType, delta] of path) {
+        if (linkType === 'p') {
+          undoWithoutUpdatingHistoryGraph(state, setState, delta);
+        }
+        else if (linkType === 'c') {
+          redoWithoutUpdatingHistoryGraph(state, setState, delta);
+        }
+      }
+      setState(({historyGraph, version, ...rest}) => ({
+        version: chosenVersion,
+        historyGraph: setCurrentVersion(historyGraph, version, chosenVersion),
+        ...rest,
+      }));
+    }
+
+    // Pure function
+    // Replays all deltas in a version to compute the graph state of that version.
+    function getGraphState(version: Version, additionalDeltas: PrimitiveDelta[]):GraphState {
+      const graphState = new GraphState();
+      for (const d of [...version].reverse()) {
+        graphState.exec(d);
+      }
+      for (const d of additionalDeltas) {
+        graphState.exec(d);
+      }
+      return graphState;
+    }
+
+
+    const parseOrRender = (sourceState: VersionedModelState, targetState: VersionedModelState,
+          mapping, parse: boolean) => {
+      return (sourceDeltas: PrimitiveDelta[], description: string) => {
+
+        const targetDescription = (parse ? "parse:" : "render:") + description;
+
+        const sourceParent = sourceState.version;
+        const {corr: corrParent, target: targetParent} = mapping.get(sourceParent) || (()=>{throw new Error("X")})();
+
+        // SLOW! When performance becomes a problem, look for a faster alternative
+        const sourceGraphState = getGraphState(sourceParent, []);
+        const corrGraphState = getGraphState(corrParent, []);
+        const targetGraphState = getGraphState(targetParent, []);
+
+        const {corrDeltas, targetDeltas, sourceOverrides, targetOverrides} = this.parser.propagate_change(parse, sourceDeltas, sourceGraphState, corrGraphState, targetGraphState);
+
+        // console.log({corrDeltas})
+
+        const sourceComposite = sourceState.compositeLevel.createComposite(sourceDeltas, description);
+        const newSourceVersion = sourceState.versionRegistry.createVersion(sourceState.version, sourceComposite);
+
+        const targetComposite = targetState.compositeLevel.createComposite(targetDeltas, targetDescription);
+        const newTargetVersion =  (targetComposite.deltas.length > 0) ?
+          targetState.versionRegistry.createVersion(targetParent, targetComposite)
+          : targetState.version; // do not create a new target version
+
+        const csComposite = parse ? sourceComposite : targetComposite;
+        const asComposite = parse ? targetComposite : sourceComposite;
+        const newCsVersion = parse ? newSourceVersion : newTargetVersion;
+        const newAsVersion = parse ? newTargetVersion : newSourceVersion;
+        const csOverrides = parse ? sourceOverrides : targetOverrides;
+        const asOverrides = parse ? targetOverrides : sourceOverrides;
+
+
+        const corrDeltasOrderedByDependency: PrimitiveDelta[] = [];
+        visitPartialOrdering([
+          ...sourceDeltas.map(d => sourceOverrides.get(d) || d),
+          ...targetDeltas.map(d => targetOverrides.get(d) || d),
+          ...corrDeltas],
+          (d: Delta) => d.getDependencies(),
+          (d: Delta) => corrDeltasOrderedByDependency.push(d));
+
+        // console.log({corrDeltasOrderedByDependency});
+
+        const corrComposite = this.state.corr.compositeLevel.createComposite(corrDeltasOrderedByDependency, targetDescription);
+        const newCorrVersion = this.state.corr.versionRegistry.createVersion(corrParent, corrComposite,
+          embed(
+            ["cs", newCsVersion, csOverrides],
+            ["as", newAsVersion, asOverrides],
+          ));
+
+        this.parsedTo.set(newCsVersion, {corr: newCorrVersion, target: newAsVersion});
+        this.renderedTo.set(newAsVersion, {corr: newCorrVersion, target: newCsVersion});
+
+        // Update state:
+        this.setState(({cs, corr, as}) => {
+          const result = {
+            cs: addVersionAndDeltas(cs, newCsVersion, csComposite),
+            corr: addVersionAndDeltas(corr, newCorrVersion, corrComposite),
+            as: addVersionAndDeltas(as, newAsVersion, asComposite),
+          };
+          return result;
+        });
+
+        gotoVersion(this.state.cs, this.setCsState.bind(this), newCsVersion);
+        gotoVersion(this.state.corr, this.setCorrState.bind(this), newCorrVersion);
+        gotoVersion(this.state.as, this.setAsState.bind(this), newAsVersion);
+      };
+    };
+
+    const parseCsChange = parseOrRender(this.state.cs, this.state.as, this.parsedTo, true);
+    const renderAsChange = parseOrRender(this.state.as, this.state.cs, this.renderedTo, false);
+
+    return (<>
+      <Grid grow>
+        <Grid.Col span={1}>
+          <VersionedModel title="Concrete Syntax"
+            generateUUID={this.generateUUID}
+            primitiveRegistry={this.primitiveRegistry}
+            tabs={["editor", "state", "history", "dependencyL1", "dependencyL0"]}
+            defaultTab0="editor"
+            defaultTab1="history"
+            state={this.state.cs}
+            setState={this.setCsState.bind(this)}
+            setNextNodePosition={(x,y)=>{this.state.cs.d3StateUpdater.x = x; this.state.cs.d3StateUpdater.y = y;}}
+            onUserEdit={parseCsChange}
+            onUndoClicked={onUndoClicked.bind(null, this.state.cs, this.setCsState.bind(this))}
+            onRedoClicked={onRedoClicked.bind(null, this.state.cs, this.setCsState.bind(this))}
+            onVersionClicked={version => gotoVersion(this.state.cs, this.setCsState.bind(this), version)}
+          />
+        </Grid.Col>
+        <Grid.Col span={1}>
+          <VersionedModel title="Correspondence" readonly
+            generateUUID={this.generateUUID}
+            primitiveRegistry={this.primitiveRegistry}
+            tabs={["state", "history", "dependencyL1", "dependencyL0"]}
+            defaultTab0="state"
+            defaultTab1="history"
+            setNextNodePosition={(x,y)=>{this.state.corr.d3StateUpdater.x = x; this.state.corr.d3StateUpdater.y = y;}}
+            state={this.state.corr}
+            setState={this.setCorrState.bind(this)}
+            onUndoClicked={onUndoClicked.bind(null, this.state.corr, this.setCorrState.bind(this))}
+            onRedoClicked={onRedoClicked.bind(null, this.state.corr, this.setCorrState.bind(this))}
+            onVersionClicked={version => gotoVersion(this.state.corr, this.setCorrState.bind(this), version)}
+          />
+
+        </Grid.Col>
+        <Grid.Col span={1}>
+          <VersionedModel title="Abstract Syntax"
+            generateUUID={this.generateUUID}
+            primitiveRegistry={this.primitiveRegistry}
+            tabs={["state", "history", "dependencyL1", "dependencyL0"]}
+            defaultTab0="state"
+            defaultTab1="history"
+            setNextNodePosition={(x,y)=>{this.state.as.d3StateUpdater.x = x; this.state.as.d3StateUpdater.y = y;}}
+            state={this.state.as}
+            setState={this.setAsState.bind(this)}
+            onUserEdit={renderAsChange}
+            onUndoClicked={onUndoClicked.bind(null, this.state.as, this.setAsState.bind(this))}
+            onRedoClicked={onRedoClicked.bind(null, this.state.as, this.setAsState.bind(this))}
+            onVersionClicked={version => gotoVersion(this.state.as, this.setAsState.bind(this), version)}
+          />
+        </Grid.Col>
+      </Grid>
+    </>);
+  }
+}

+ 96 - 0
src/frontend/demo_pd.tsx

@@ -0,0 +1,96 @@
+import * as React from "react";
+import {Title, Grid} from "@mantine/core";
+
+import {PrimitiveDelta, PrimitiveRegistry} from "../onion/primitive_delta";
+import {VersionRegistry} from "../onion/version";
+import {CompositeLevel} from "../onion/composite_delta";
+import {GraphState} from "../onion/graph_state"; 
+import {mockUuid} from "../onion/test_helpers";
+import {D3GraphStateUpdater} from "./d3_state";
+import {d3Types, Graph} from "./graph"
+import {EditableGraph, UserEditCallback, SetNodePositionCallback, GraphType, NodeType, LinkType} from "./editable_graph";
+import {emptyGraph, graphForces} from "./constants";
+import {
+  HistoryGraphType,
+  DependencyGraphType,
+  fullDeltaId,
+  setDeltaActive,
+  setDeltaInactive,
+  deltaToDepGraphNode,
+  dependencyToDepGraphLink,
+  conflictToDepGraphLink,
+  fullVersionId,
+  addDeltaAndActivate,
+  initialHistoryGraph,
+} from "./app_state";
+
+interface DemoPDProps {
+}
+
+interface DemoPDState {
+  graph: GraphType;
+  dependencyGraphL0: DependencyGraphType;
+  history: HistoryGraphType;
+}
+
+export class DemoPD extends React.Component<DemoPDProps, DemoPDState> {
+  readonly primitiveRegistry = new PrimitiveRegistry();
+  readonly d3Listener = new D3GraphStateUpdater(
+      callback => this.setState(({graph, ...rest}) => ({graph: callback(graph), ...rest}))
+    );
+  readonly graphState = new GraphState(this.d3Listener);
+  readonly generateUUID = mockUuid();
+  readonly versionRegistry = new VersionRegistry();
+  readonly compositeLvl = new CompositeLevel();
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      graph: emptyGraph,
+      dependencyGraphL0: emptyGraph,
+      history: initialHistoryGraph(this.versionRegistry.initialVersion),
+    };
+  }
+
+  render() {
+    const onUserEdit = (deltas: PrimitiveDelta[], description: string) => {
+      // const composite = this.compositeLvl.createComposite(deltas);
+      // const version = this.versionRegistry.
+      deltas.forEach(d => {
+        this.graphState.exec(d);
+        this.setState(({dependencyGraphL0, ...rest}) => ({dependencyGraphL0: addDeltaAndActivate(dependencyGraphL0, d), ...rest}));
+      });
+    }
+
+    return <>
+        <Title order={4}>State</Title>
+        <EditableGraph
+          graph={this.state.graph}
+          primitiveRegistry={this.primitiveRegistry}
+          graphState={this.graphState}
+          forces={graphForces}
+          generateUUID={this.generateUUID}
+          setNextNodePosition={(x,y) => {this.d3Listener.x = x; this.d3Listener.y = y}}
+          onUserEdit={onUserEdit}
+        />
+
+        <Grid grow>
+          <Grid.Col span={1}>
+            <Title order={4}>Deltas</Title>
+            <Graph
+             graph={this.state.dependencyGraphL0}
+             forces={graphForces}
+            />
+          </Grid.Col>
+          <Grid.Col span={1}>
+            <Title order={4}>History</Title>
+            <Graph
+              graph={this.state.history}
+              forces={graphForces}
+            />
+          </Grid.Col>
+        </Grid>
+      </>;
+  }
+}

+ 1 - 8
src/frontend/editable_graph.tsx

@@ -37,7 +37,6 @@ export interface EditableGraphProps {
   generateUUID: () => UUID;
   setNextNodePosition: SetNodePositionCallback;
   onUserEdit?: UserEditCallback;
-  currentVersion: Version;
 }
 
 interface EditableGraphState {
@@ -112,9 +111,6 @@ export class EditableGraph extends React.Component<EditableGraphProps, EditableG
       else if (event.button === 1) { // middle mouse button
         if (mouseUpNode !== undefined) {
           // middle mouse button click on node -> delete node (and incoming/outgoing edges)
-          // if (mouseUpNode.obj.type === "node") {
-          //   return [getDeltasForDelete(this.props.primitiveRegistry, (mouseUpNode.obj as INodeState).creation, this.props.currentVersion), "deleteNode"];
-          // }
           return [mouseUpNode.obj.getDeltasForDelete(this.props.primitiveRegistry), "deleteNode"];
         }
       }
@@ -122,10 +118,7 @@ export class EditableGraph extends React.Component<EditableGraphProps, EditableG
     })();
 
     if (deltas.length > 0) {
-      // Let the world know that there is a new (composite) delta, and a new version:
-      // const composite = this.props.compositeLvl.createComposite(deltas);
-      // const version = this.props.versionRegistry.createVersionUnsafe(this.props.version, composite);
-
+      // Let the world know that there was a user edit:
       this.props.onUserEdit?.(deltas, description);
     }
 

+ 0 - 5
src/frontend/graph.css

@@ -1,8 +1,3 @@
-.graph {
-  background-color: #eee;
-  height: 400px;
-}
-
 .graphNode {
   stroke: #000;
   stroke-width: 1.5px;

+ 3 - 5
src/frontend/graph.tsx

@@ -301,8 +301,6 @@ export class Graph<NodeType,LinkType> extends React.Component<GraphProps<NodeTyp
   render() {
     const { graph } = this.props;
 
-    const height = 400;
-
     const clientToSvgCoords = (event, callback) => {
       // console.log(event);
       // Translate mouse event to SVG coordinates:
@@ -319,10 +317,10 @@ export class Graph<NodeType,LinkType> extends React.Component<GraphProps<NodeTyp
 
     return (
       <svg
-        className="graph"
+        className="canvas"
         ref={this.refSVG}
-        style={{width: "100%", height}}
-        viewBox={`${-height/2/this.state.zoom} ${-height/2/this.state.zoom} ${height/this.state.zoom} ${height/this.state.zoom}`}
+        viewBox={`${-200/this.state.zoom} ${-200/this.state.zoom} ${400/this.state.zoom} ${400/this.state.zoom}`}
+        // preserveAspectRatio="xMidYMid meet"
         onMouseDown={e => this.props.mouseDownHandler ? clientToSvgCoords(e, this.props.mouseDownHandler) : null}
         onMouseUp={e => this.props.mouseUpHandler ? clientToSvgCoords(e, this.props.mouseUpHandler) : null}
         onContextMenu={e => e.preventDefault()}

+ 15 - 0
src/frontend/index.css

@@ -0,0 +1,15 @@
+html,body, #root {
+    /*min-height: 100vh;*/
+
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+
+.canvas {
+  background-color: #eee;
+  width: 100%;
+  height: 400px;
+  vertical-align:top;
+}

+ 1 - 1
src/frontend/index.tsx

@@ -2,7 +2,7 @@ import * as React from 'react';
 import {createRoot} from 'react-dom/client';
 import './rountangleEditor/RountangleEditor.css';
 import './graph.css';
-import './app.css';
+import './index.css';
 
 import {App} from "./app";
 

+ 2 - 2
src/frontend/rountangleEditor/RountangleEditor.css

@@ -1,6 +1,6 @@
 .re-background {
-    background: #eee;
-    height: 400px;
+    /*background: #eee;*/
+    /*height: 100px;*/
     position: relative;
     font-family: sans-serif;
 }

+ 1 - 1
src/frontend/rountangleEditor/RountangleEditor.tsx

@@ -203,7 +203,7 @@ export class RountangleEditor extends React.Component<RountangleEditorProps, {}>
     render() {
         return(
             <div
-                   className={'re-background'}
+                   className="re-background canvas"
                    onPointerUp={this.onPointerUp}
                    ref={this.canvasRef}
                >

+ 158 - 0
src/frontend/versioned_model.tsx

@@ -0,0 +1,158 @@
+interface VersionedModelState {
+  version: Version; // the 'current version'
+  graph: GraphType; // the state what is displayed in the leftmost panel
+  historyGraph: HistoryGraphType; // the state of what is displayed in the middle panel
+  dependencyGraphL1: DependencyGraphType; // the state of what is displayed in the rightmost panel
+  dependencyGraphL0: DependencyGraphType; // the state of what is displayed in the rightmost panel
+}
+
+export interface VersionedModelProps {
+  title: string;
+  readonly?: boolean;
+  
+  generateUUID: () => UUID;
+  primitiveRegistry: PrimitiveRegistry;
+  versionRegistry: VersionRegistry;
+  compositeLevel: CompositeLevel;
+  d3Updater: D3GraphStateUpdater;
+  graphState: GraphState;
+
+
+  onUserEdit?: UserEditCallback;
+  onUndoClicked: (parentVersion: Version, deltaToUndo: Delta) => void;
+  onRedoClicked: (childVersion: Version, deltaToRedo: Delta) => void;
+  onVersionClicked: (Version) => void;
+
+  state: VersionedModelState;
+  setState: (callback: (VersionedModelState) => VersionedModelState) => void;
+}
+
+// All of these are pure functions:
+
+function getReactComponents(props: VersionedModelProps) {
+
+  const addVersionAndDeltas = (newVersion: Version, composite: CompositeDelta) => {
+    props.setState(({version, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => ({
+      version: newVersion,
+      // add new version to history graph + highlight the new version as the current version:
+      historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), version, newVersion),
+      // add the composite delta to the L1-graph + highlight it as 'active':
+      dependencyGraphL1: composite.deltas.length > 0 ? addDeltaAndActivate(dependencyGraphL1, composite) : dependencyGraphL1, // never add an empty composite
+      // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
+      dependencyGraphL0: composite.deltas.reduce(
+        (graph, delta) => {
+          return addDeltaAndActivate(graph, delta);
+        }, dependencyGraphL0),
+      ...rest,
+    }));
+  };
+  const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
+    props.graphState.unexec(deltaToUndo);
+    props.setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => ({
+      dependencyGraphL1: setDeltaInactive(prevDepGraphL1, deltaToUndo),
+      dependencyGraphL0: deltaToUndo.deltas.reduce((prevDepGraphL0, delta) => setDeltaInactive(prevDepGraphL0, delta), prevDepGraphL0),
+      ...rest,
+    }));
+  };
+  const redoWithoutUpdatingHistoryGraph = (deltaToRedo) => {
+    props.graphState.exec(deltaToRedo);
+    props.setState(({dependencyGraphL0: prevDepGraphL0, dependencyGraphL1: prevDepGraphL1, ...rest}) => ({
+      dependencyGraphL1: setDeltaActive(prevDepGraphL1, deltaToRedo),
+      dependencyGraphL0: deltaToRedo.deltas.reduce((prevDepGraphL0, delta) => setDeltaActive(prevDepGraphL0, delta), prevDepGraphL0),
+      ...rest,
+    }));
+  };
+  const undo = (parentVersion, deltaToUndo) => {
+    undoWithoutUpdatingHistoryGraph(deltaToUndo);
+    props.setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+      version: parentVersion,
+      historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, parentVersion),
+      ...rest,
+    }));
+  };
+  const redo = (childVersion, deltaToRedo) => {
+    redoWithoutUpdatingHistoryGraph(deltaToRedo);
+    props.setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+      version: childVersion,
+      historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, childVersion),
+      ...rest,
+    }));
+  };
+  const gotoVersion = (chosenVersion: Version) => {
+    const path = props.state.version.findPathTo(chosenVersion);
+    if (path === undefined) {
+      throw new Error("Could not find path to version!");
+    }
+    for (const [linkType, delta] of path) {
+      if (linkType === 'p') {
+        undoWithoutUpdatingHistoryGraph(state, setState, delta);
+      }
+      else if (linkType === 'c') {
+        redoWithoutUpdatingHistoryGraph(state, setState, delta);
+      }
+    }
+    props.setState(({historyGraph, version, ...rest}) => ({
+      version: chosenVersion,
+      historyGraph: setCurrentVersion(historyGraph, version, chosenVersion),
+      ...rest,
+    }));
+  };
+
+
+  const graphStateComponent = props.readonly ? 
+    <Graph graph={props.state.graph} forces={graphForces} />
+    : <EditableGraph
+      graph={props.state.graph}
+      graphState={props.graphState}
+      forces={graphForces}
+      generateUUID={props.generateUUID}
+      primitiveRegistry={props.primitiveRegistry}
+      setNextNodePosition={(x,y) => {props.d3Updater.x = x; props.d3Updater.y = y;}}
+      onUserEdit={props.onUserEdit} />;
+
+  const depGraphL1Component = <Graph graph={props.state.dependencyGraphL1} forces={graphForces} />;
+  const depGraphL0Component = <Graph graph={props.state.dependencyGraphL0} forces={graphForces} />;
+
+  const historyComponent = <Graph graph={props.state.historyGraph} forces={graphForces}
+      mouseUpHandler={(e, {x, y}, node) => node ? props.onVersionClicked(node.obj) : undefined} />;
+
+
+  const undoButtons = props.state.version.parents.map(([parentVersion,deltaToUndo]) => {
+    return (
+      <div key={fullVersionId(parentVersion)}>
+        <Button fullWidth={true} compact={true} leftIcon={<IconPlayerTrackPrev size={18}/>} onClick={props.onUndoClicked.bind(null, parentVersion, deltaToUndo)}>
+          UNDO {deltaToUndo.getDescription()}
+        </Button>
+        <Space h="xs"/>
+      </div>
+    );
+  });
+  const redoButtons = props.state.version.children.map(([childVersion,deltaToRedo]) => {
+    return (
+      <div key={fullVersionId(childVersion)}>
+        <Button style={{width: "100%"}} compact={true} rightIcon={<IconPlayerTrackNext size={18}/>} onClick={props.onRedoClicked.bind(null, childVersion, deltaToRedo)}>
+          REDO {deltaToRedo.getDescription()}
+        </Button>
+        <Space h="xs"/>
+      </div>
+    );
+  });
+
+
+  return {
+    components: {
+      graphStateComponent,
+      depGraphL1Component,
+      depGraphL0Component,
+      historyComponent,
+      undoButtons,
+      redoButtons,
+    },
+    callbacks: {
+      gotoVersion,
+      addVersionAndDeltas,
+      undo,
+      redo,
+    },
+  };
+}

+ 46 - 40
src/onion/graph_state.ts

@@ -307,42 +307,48 @@ export class GraphState {
   readonly nodes: Map<PrimitiveValue, NodeState> = new Map();
   readonly values: Map<PrimitiveValue, ValueState> = new Map();
 
-  exec(delta: Delta, listener: GraphStateListener = DUMMY) {
+  private readonly listener: GraphStateListener;
+
+  constructor(listener: GraphStateListener = DUMMY) {
+    this.listener = listener;
+  }
+
+  exec(delta: Delta) {
     if (delta instanceof CompositeDelta) {
-      delta.deltas.forEach(d => this.exec(d, listener));
+      delta.deltas.forEach(d => this.exec(d));
     }
     else if (delta instanceof NodeCreation) {
-      this.execNodeCreation(delta, listener);
+      this.execNodeCreation(delta);
     }
     else if (delta instanceof NodeDeletion) {
-      this.execNodeDeletion(delta, listener);
+      this.execNodeDeletion(delta);
     }
     else if (delta instanceof EdgeCreation) {
-      this.execEdgeCreation(delta, listener);
+      this.execEdgeCreation(delta);
     }
     else if (delta instanceof EdgeUpdate) {
-      this.execEdgeUpdate(delta, listener);
+      this.execEdgeUpdate(delta);
     }
     else {
       throw new Error("Assertion failed: Unexpected delta type");
     }
   }
-  unexec(delta: Delta, listener: GraphStateListener = DUMMY) {
+  unexec(delta: Delta) {
     if (delta instanceof CompositeDelta) {
       // must un-exec them in reverse order:
-      delta.deltas.reduceRight((_, currentDelta) => {this.unexec(currentDelta, listener); return null;}, null);
+      delta.deltas.reduceRight((_, currentDelta) => {this.unexec(currentDelta); return null;}, null);
     }
     else if (delta instanceof NodeCreation) {
-      this.unexecNodeCreation(delta, listener);
+      this.unexecNodeCreation(delta);
     }
     else if (delta instanceof NodeDeletion) {
-      this.unexecNodeDeletion(delta, listener);
+      this.unexecNodeDeletion(delta);
     }
     else if (delta instanceof EdgeCreation) {
-      this.unexecEdgeCreation(delta, listener);
+      this.unexecEdgeCreation(delta);
     }
     else if (delta instanceof EdgeUpdate) {
-      this.unexecEdgeUpdate(delta, listener);
+      this.unexecEdgeUpdate(delta);
     }
     else {
       throw new Error("Assertion failed: Unexpected delta type");
@@ -366,19 +372,19 @@ export class GraphState {
     return vs;
   }
 
-  execNodeCreation(delta: NodeCreation, listener: GraphStateListener) {
+  execNodeCreation(delta: NodeCreation) {
     // console.log("execNodeCreation", delta)
     const nodeState = new NodeState(delta);
     this.nodes.set(delta.id.value, nodeState);
-    listener.createNode(nodeState);
+    this.listener.createNode(nodeState);
   }
-  unexecNodeCreation(delta: NodeCreation, listener: GraphStateListener) {
+  unexecNodeCreation(delta: NodeCreation) {
     // console.log("unexecNodeCreation", delta)
     this.nodes.delete(delta.id.value);
-    listener.deleteNode(delta.id.value);
+    this.listener.deleteNode(delta.id.value);
   }
 
-  execNodeDeletion(delta: NodeDeletion, listener: GraphStateListener) {
+  execNodeDeletion(delta: NodeDeletion) {
     // console.log("execNodeDeletion", delta)
     const id = delta.creation.id.value;
     const nodeState = this.nodes.get(id);
@@ -390,38 +396,38 @@ export class GraphState {
       const target = outgoingEdgeOperation.target.getTarget();
       const targetState = this._getEdgeTargetState(target);
       if (targetState !== undefined) {
-        targetState.replaceIncoming(outgoingEdgeOperation, delta, listener);
+        targetState.replaceIncoming(outgoingEdgeOperation, delta, this.listener);
         const outgoingEdgeCreation = outgoingEdgeOperation.getCreation();
         const sourceId = outgoingEdgeCreation.source.id.value;
         const label = outgoingEdgeCreation.label;
-        listener.deleteLink(sourceId, label);
+        this.listener.deleteLink(sourceId, label);
       }
     }
-    listener.deleteNode(id);    
+    this.listener.deleteNode(id);    
   }
-  unexecNodeDeletion(delta: NodeDeletion, listener: GraphStateListener) {
+  unexecNodeDeletion(delta: NodeDeletion) {
     // restore outgoing links
     const id = delta.creation.id.value;
     const nodeState = this.nodes.get(id);
     if (nodeState === undefined) {
       throw new Error("Assertion failed: deleted node does not exist")
     }
-    listener.createNode(nodeState);
+    this.listener.createNode(nodeState);
     // For every outgoing edge of deleted node, restore in the target node the incoming edge operation by whatever was there before
     for (const outgoingEdgeOperation of nodeState.outgoing.values()) {
       const target = outgoingEdgeOperation.target.getTarget();
       const targetState = this._getEdgeTargetState(target);
       if (targetState !== undefined) {
-        targetState.replaceIncoming(delta, outgoingEdgeOperation, listener);
+        targetState.replaceIncoming(delta, outgoingEdgeOperation, this.listener);
         const outgoingEdgeCreation = outgoingEdgeOperation.getCreation();
         const sourceId = outgoingEdgeCreation.source.id.value;
         const label = outgoingEdgeCreation.label;
-        targetState.createLinkTo(sourceId, label, listener);
+        targetState.createLinkTo(sourceId, label, this.listener);
       }
     }
   }
 
-  execEdgeCreation(delta: EdgeCreation, listener: GraphStateListener) {
+  execEdgeCreation(delta: EdgeCreation) {
     // console.log("execEdgeCreation", delta)
     const sourceId = delta.source.id.value;
     const target = delta.target.getTarget();
@@ -439,11 +445,11 @@ export class GraphState {
     }
     sourceState.outgoing.set(label, delta);
     sourceState.outgoingStates.set(label, targetState);
-    targetState.addIncoming(delta, listener);
+    targetState.addIncoming(delta, this.listener);
     targetState.incomingStates.push([label, sourceState]);
-    targetState.createLinkTo(sourceId, label, listener);
+    targetState.createLinkTo(sourceId, label, this.listener);
   }
-  unexecEdgeCreation(delta: EdgeCreation, listener: GraphStateListener) {
+  unexecEdgeCreation(delta: EdgeCreation) {
     const sourceId = delta.source.id.value;
     const target = delta.target.getTarget();
     if (target === null) {
@@ -460,12 +466,12 @@ export class GraphState {
     }
     sourceState.outgoing.delete(label);
     sourceState.outgoingStates.delete(label);
-    targetState.removeIncoming(delta, listener);
+    targetState.removeIncoming(delta, this.listener);
     targetState.incomingStates.splice(targetState.incomingStates.findIndex(([l,s]) => l===label && s===sourceState), 1);
-    listener.deleteLink(sourceId, label);
+    this.listener.deleteLink(sourceId, label);
   }
 
-  execEdgeUpdate(delta: EdgeUpdate, listener: GraphStateListener) {
+  execEdgeUpdate(delta: EdgeUpdate) {
     // console.log("execEdgeUpdate", delta)
     // Delete link to old target
     const edgeCreation = delta.getCreation();
@@ -483,9 +489,9 @@ export class GraphState {
       const oldTargetState = this._getEdgeTargetState(oldTarget);
       // Delete from old target's incoming edges:
       if (oldTargetState !== undefined) {
-        oldTargetState.replaceIncoming(overwrittenEdgeOperation, delta, listener);
+        oldTargetState.replaceIncoming(overwrittenEdgeOperation, delta, this.listener);
         oldTargetState.incomingStates.splice(oldTargetState.incomingStates.findIndex(([l,s]) => l===label && s===sourceState), 1);
-        listener.deleteLink(sourceId, label);
+        this.listener.deleteLink(sourceId, label);
       }
     }
     // Create link to new target (if there is a target)
@@ -496,10 +502,10 @@ export class GraphState {
       // Add to new target's incoming edges
       if (newTargetState !== undefined) {
         if (newTarget !== oldTarget) { // if newTarget === oldTarget, the 'delta' is already part of 'incoming'
-          newTargetState.addIncoming(delta, listener);
+          newTargetState.addIncoming(delta, this.listener);
         }
         newTargetState.incomingStates.push([label, sourceState]);
-        newTargetState.createLinkTo(sourceId, label, listener);
+        newTargetState.createLinkTo(sourceId, label, this.listener);
         sourceState.outgoingStates.set(label, newTargetState);
       }
     } else {
@@ -507,7 +513,7 @@ export class GraphState {
     }
     sourceState.outgoing.set(label, delta);
   }
-  unexecEdgeUpdate(delta: EdgeUpdate, listener: GraphStateListener) {
+  unexecEdgeUpdate(delta: EdgeUpdate) {
     // console.log("execEdgeUpdate", delta)
     // Delete link to old target
     const edgeCreation = delta.getCreation();
@@ -525,9 +531,9 @@ export class GraphState {
       const newTargetState = this._getEdgeTargetState(newTarget);
       // Add to new target's incoming edges
       if (newTargetState !== undefined) {
-        newTargetState.removeIncoming(delta, listener);
+        newTargetState.removeIncoming(delta, this.listener);
         newTargetState.incomingStates.splice(newTargetState.incomingStates.findIndex(([l,s]) => l===label && s===sourceState), 1);
-        listener.deleteLink(sourceId, label);
+        this.listener.deleteLink(sourceId, label);
       }
     }
     // Restore link to old target
@@ -537,9 +543,9 @@ export class GraphState {
       const oldTargetState = this._getEdgeTargetState(oldTarget);
       // Delete from old target's incoming edges:
       if (oldTargetState !== undefined) {
-        oldTargetState.replaceIncoming(delta, overwrittenEdgeOperation, listener);
+        oldTargetState.replaceIncoming(delta, overwrittenEdgeOperation, this.listener);
         oldTargetState.incomingStates.push([label, sourceState]);
-        oldTargetState.createLinkTo(sourceId, label, listener);
+        oldTargetState.createLinkTo(sourceId, label, this.listener);
         sourceState.outgoingStates.set(label, oldTargetState);
       }
     } else {