Joeri Exelmans 2 anni fa
parent
commit
1a1e8a051c

+ 35 - 67
src/frontend/correspondence.ts

@@ -16,103 +16,71 @@ function getGraphState(version: Version): GraphState {
 }
 
 export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
-  const {initialState, getReducer: getReducerOrig} = newVersionedModel({readonly: true, generateUUID, primitiveRegistry});
+  const {initialState, getCurrentVersion, getReducer: getReducerOrig, getReactComponents} = newVersionedModel({readonly: true, generateUUID, primitiveRegistry});
 
   const parser = new TrivialParser(primitiveRegistry, generateUUID);
 
   // Mapping from correspondence model version to CS and AS model version.
-  const corrMap: Map<Version,{csVersion:Version,asVersion:Version}> = new Map([
+  const corrMap: Map<Version, {csVersion: Version, asVersion: Version}> = new Map([
     [initialState.version, {csVersion: cs.initialState.version, asVersion: as.initialState.version}]
   ]);
 
-  function getReducer([state, setState], csReducer, asReducer) {
+  function getReducer(setCorrState, csReducer, asReducer) {
     const {
-      getReactComponents,
-      callbacks: {
-        gotoVersion: gotoVersionOrig,
-        createAndGotoNewVersion,
-        undo,
-        redo,
-      },
-    } = getReducerOrig([state, setState]);
+      gotoVersion: gotoVersionOrig,
+      createAndGotoNewVersion,
+      undo,
+      redo,
+    } = getReducerOrig(setCorrState);
 
     const parse = (csDeltas, description: string) => {
-      const [csGS, corrGS, asGS] = [csReducer.state.version, state.version, asReducer.state.version].map(v => getGraphState(v));
+      const csCurrentVersion = cs.getCurrentVersion();
+      const asCurrentVersion = as.getCurrentVersion();
+
+      const [csGS, corrGS, asGS] = [csCurrentVersion, getCurrentVersion(), asCurrentVersion].map(v => getGraphState(v));
 
       const {corrDeltas, asDeltas} = parser.parse(csDeltas, csGS, corrGS, asGS);
 
-      const csVersionPromise = new Promise<Version>(resolve => {
-        // createAndGotoNewVersion will create a new Version in a callback passed to React's setState.
-        // Therefore, we can only get the newly created Version by passing a callback ourselves ('resolve') that will be called when the new Version is created.
-        csReducer.callbacks.createAndGotoNewVersion(csDeltas, description, resolve);
-      });
-      const corrVersionPromise = new Promise<Version>(resolve => {
-        // See comment about 'createAndGotoNewVersion' above.
-        createAndGotoNewVersion(corrDeltas, description, resolve);
-      });
-      const asVersionPromise = new Promise<Version>(resolve => {
-        if (asDeltas.length > 0) {
-          // See comment about 'createAndGotoNewVersion' above.
-          asReducer.callbacks.createAndGotoNewVersion(asDeltas, "parse:"+description, resolve);
-        } else {
-          resolve(asReducer.state.version);
-        }
-      });
-
-      // Remember which Corr version embeds which CS/AS versions:
-      Promise.all([csVersionPromise, corrVersionPromise, asVersionPromise]).then(([csVersion, corrVersion, asVersion]) => {
-        corrMap.set(corrVersion, {csVersion, asVersion});        
-      });
+      const csVersion = csReducer.createAndGotoNewVersion(csDeltas, description);
+      const corrVersion = createAndGotoNewVersion(corrDeltas, description);
+      const asVersion = asDeltas.length > 0 ? asReducer.createAndGotoNewVersion(asDeltas, "parse:"+description) : asCurrentVersion;
+
+      corrMap.set(corrVersion, {csVersion, asVersion});        
     };
     const render = (asDeltas, description: string) => {
-      const [csGS, corrGS, asGS] = [csReducer.state.version, state.version, asReducer.state.version].map(v => getGraphState(v));
+      const csCurrentVersion = cs.getCurrentVersion();
+      const asCurrentVersion = as.getCurrentVersion();
+
+      const [csGS, corrGS, asGS] = [csCurrentVersion, getCurrentVersion(), asCurrentVersion].map(v => getGraphState(v));
       
       const {corrDeltas, csDeltas} = parser.render(asDeltas, csGS, corrGS, asGS);
 
-      const csVersionPromise = new Promise<Version>(resolve => {
-        if (csDeltas.length > 0) {
-          // See comment about 'createAndGotoNewVersion' above.
-          csReducer.callbacks.createAndGotoNewVersion(csDeltas, "render:"+description, resolve);
-        } else {
-          resolve(csReducer.state.version);
-        }
-      });
-      const corrVersionPromise = new Promise<Version>(resolve => {
-        // See comment about 'createAndGotoNewVersion' above.
-        createAndGotoNewVersion(corrDeltas, description, resolve);
-      });
-      const asVersionPromise = new Promise<Version>(resolve => {
-        // See comment about 'createAndGotoNewVersion' above.
-        asReducer.callbacks.createAndGotoNewVersion(asDeltas, description, resolve);
-      });
-
-      // Remember which Corr version embeds which CS/AS versions:
-      Promise.all([csVersionPromise, corrVersionPromise, asVersionPromise]).then(([csVersion, corrVersion, asVersion]) => {
-        corrMap.set(corrVersion, {csVersion, asVersion});        
-      });
+      const csVersion = csDeltas.length > 0 ? csReducer.createAndGotoNewVersion(csDeltas, "render:"+description) : csCurrentVersion;
+      const corrVersion = createAndGotoNewVersion(corrDeltas, description);
+      const asVersion = asReducer.createAndGotoNewVersion(asDeltas, description);
+
+      corrMap.set(corrVersion, {csVersion, asVersion});
     };
     const gotoVersion = (corrVersion: Version) => {
       const {csVersion, asVersion} = corrMap.get(corrVersion)!;
-      csReducer.callbacks.gotoVersion(csVersion);
+      csReducer.gotoVersion(csVersion);
       gotoVersionOrig(corrVersion);
-      asReducer.callbacks.gotoVersion(asVersion);
+      asReducer.gotoVersion(asVersion);
     };
 
     return {
-      state,
-      getReactComponents,
-      callbacks: {
-        parse,
-        render,
-        gotoVersion,
-        undo,
-        redo,
-      },
+      parse,
+      render,
+      gotoVersion,
+      undo,
+      redo,
     };
   }
 
   return {
     initialState,
+    getCurrentVersion,
     getReducer,
+    getReactComponents,
   };
 }

+ 0 - 1
src/frontend/d3_state.ts

@@ -25,7 +25,6 @@ export class D3GraphStateUpdater implements GraphStateListener {
   }
 
   createNode(ns: INodeState) {
-    console.log('create node')
     this.setGraph(prevGraph => ({
       nodes: [...prevGraph.nodes, {
         id: nodeNodeId(ns.creation.id.value),

+ 22 - 18
src/frontend/demo_corr.tsx

@@ -4,7 +4,7 @@ import {SimpleGrid, Text, Title, Group, Stack, Button, Space, Textarea, Tabs, Ho
 import {PrimitiveRegistry} from "../onion/primitive_delta";
 import {mockUuid} from "../onion/test_helpers";
 
-import {newVersionedModel} from "./versioned_model";
+import {newVersionedModel, VersionedModelState} from "./versioned_model";
 import {newCorrespondence} from "./correspondence";
 
 export function getDemoCorr() {
@@ -19,26 +19,30 @@ export function getDemoCorr() {
 
   // returns functional react component
   return function() {
-    const asReducer = as.getReducer(React.useState(as.initialState));
-    const csReducer = cs.getReducer(React.useState(cs.initialState));
-    const corrReducer = corr.getReducer(React.useState(corr.initialState), csReducer, asReducer);
+    const [asState, setAsState] = React.useState<VersionedModelState>(as.initialState);
+    const [csState, setCsState] = React.useState<VersionedModelState>(cs.initialState);
+    const [corrState, setCorrState] = React.useState<VersionedModelState>(corr.initialState);
 
-    const csComponents = csReducer.getReactComponents({
-      onUserEdit: corrReducer.callbacks.parse,
-      onUndoClicked: csReducer.callbacks.undo,
-      onRedoClicked: csReducer.callbacks.redo,
-      onVersionClicked: csReducer.callbacks.gotoVersion,
+    const asReducer = as.getReducer(setAsState);
+    const csReducer = cs.getReducer(setCsState);
+    const corrReducer = corr.getReducer(setCorrState, csReducer, asReducer);
+
+    const csComponents = cs.getReactComponents(csState, {
+      onUserEdit: corrReducer.parse,
+      onUndoClicked: csReducer.undo,
+      onRedoClicked: csReducer.redo,
+      onVersionClicked: csReducer.gotoVersion,
     });
-    const corrComponents = corrReducer.getReactComponents({
-      onUndoClicked: corrReducer.callbacks.gotoVersion,
-      onRedoClicked: corrReducer.callbacks.gotoVersion,
-      onVersionClicked: corrReducer.callbacks.gotoVersion,
+    const corrComponents = corr.getReactComponents(corrState, {
+      onUndoClicked: corrReducer.gotoVersion,
+      onRedoClicked: corrReducer.gotoVersion,
+      onVersionClicked: corrReducer.gotoVersion,
     });
-    const asComponents = asReducer.getReactComponents({
-      onUserEdit: corrReducer.callbacks.render,
-      onUndoClicked: asReducer.callbacks.undo,
-      onRedoClicked: asReducer.callbacks.redo,
-      onVersionClicked: asReducer.callbacks.gotoVersion,
+    const asComponents = as.getReactComponents(asState, {
+      onUserEdit: corrReducer.render,
+      onUndoClicked: asReducer.undo,
+      onRedoClicked: asReducer.redo,
+      onVersionClicked: asReducer.gotoVersion,
     });
 
     const csTabs = ["editor", "state", "history", "dependencyL1", "dependencyL0"];

+ 18 - 19
src/frontend/demo_pd.tsx

@@ -34,36 +34,35 @@ export function getDemoPD() {
       };
     }
 
-    return <>{ globalState.map(([branchName, branchState, {getReducer}], i) => {
+    return <>{ globalState.map(([branchName, branchState, {getCurrentVersion, getReducer, getReactComponents}], i) => {
         const setBranchState = getSetBranchState(i);
 
-        const {callbacks, getReactComponents} = getReducer([branchState, setBranchState]);
+        const reducer = getReducer(setBranchState);
 
-        const components = getReactComponents({
-          onUserEdit: callbacks.createAndGotoNewVersion,
-          onUndoClicked: callbacks.undo,
-          onRedoClicked: callbacks.redo,
-          onVersionClicked: callbacks.gotoVersion,
+        const components = getReactComponents(branchState, {
+          onUserEdit: reducer.createAndGotoNewVersion,
+          onUndoClicked: reducer.undo,
+          onRedoClicked: reducer.redo,
+          onVersionClicked: reducer.gotoVersion,
         });
 
-        const mergeClicked = () => {
-          const [mergeWithBranchName, mergeWithBranchState, {getReducer: mergeWithReducer}] = globalState[i-1];
-          const {callbacks: mergeWithCallbacks} = mergeWithReducer([mergeWithBranchState, getSetBranchState(i-1)]);
-          console.log({mergeWithCallbacks})
+        const unionClicked = () => {
+          const [mergeWithBranchName, mergeWithBranchState, {getReducer: getOtherReducer}] = globalState[i-1];
+          const otherReducer = getOtherReducer(getSetBranchState(i-1));
           const addRecursive = ([version, delta, _]) => {
             if (version.parents.length > 0) {
               addRecursive(version.parents[0])
             }
-            mergeWithCallbacks.addDeltasAndVersion(delta.deltas, delta.getDescription(), version.hash);
+            otherReducer.addDeltasAndVersion(delta.deltas, delta.getDescription(), version.hash);
           }
-          addRecursive(branchState.version.parents[0]);
+          addRecursive(getCurrentVersion().parents[0]);
           setGlobalState(prevGlobalState => [
             ...prevGlobalState.slice(0,i),
             ...prevGlobalState.slice(i+1),
           ]);
         }
 
-        const branchClicked = () => {
+        const cloneClicked = () => {
           const newBranchName = prompt("Branch name: (ESC to cancel)", "branch");
           if (newBranchName === null) {
             return;
@@ -83,10 +82,10 @@ export function getDemoPD() {
 
           const setNewBranchState = getSetBranchState(i+1);
 
-          const {callbacks} = newModel.getReducer([newBranchState, setNewBranchState]);
-          const compositeDeltas = [...branchState.version].reverse();
+          const reducer = newModel.getReducer(setNewBranchState);
+          const compositeDeltas = [...getCurrentVersion()].reverse();
           compositeDeltas.forEach((c: any) => {
-            callbacks.createAndGotoNewVersion(c.deltas, c.getDescription());
+            reducer.createAndGotoNewVersion(c.deltas, c.getDescription());
           });
         }
 
@@ -103,9 +102,9 @@ export function getDemoPD() {
               {components.historyComponent}
               <Space h="md"/>
               <SimpleGrid cols={4} spacing="xs">
-                <Button onClick={mergeClicked} compact disabled={i===0} leftIcon={<Icons.IconChevronUp/>}>Merge </Button>
+                <Button onClick={unionClicked} compact disabled={i===0} leftIcon={<Icons.IconChevronUp/>}>Union </Button>
                 {components.undoRedoButtons}
-                <Button onClick={branchClicked} compact rightIcon={<Icons.IconChevronDown/>}>Branch</Button>
+                <Button onClick={cloneClicked} compact rightIcon={<Icons.IconChevronDown/>}>Clone</Button>
               </SimpleGrid>
             </div>
             <div>

+ 0 - 5
src/frontend/rountangleEditor/RountangleEditor.tsx

@@ -165,11 +165,6 @@ export class RountangleEditor extends React.Component<RountangleEditorProps, {}>
             default: assertNever(action);
         }
         if (deltas.length > 0) {
-            // Let the world know that there is a new (composite) delta, and a new version:
-            // const composite = this.props.compositeLvl.createCompositeWithCustomDescription(deltas, action.tag);
-            // AR: WARNING: "createVersionUnsafe" is probably not safe?!
-            // JE: differenc between safe and unsafe: unsafe does not check some assertions, but it is ok in this case, by JE does not know why anymore.
-            // const version = this.props.versionRegistry.createVersionUnsafe(this.props.version, composite);
             this.props.onUserEdit?.(deltas, action.tag);
         }
 

+ 128 - 109
src/frontend/versioned_model.tsx

@@ -58,14 +58,21 @@ function makeOverlayHelpIcon(background, helpIcon) {
   );
 }
 
+// Basically everything we need to construct the React components for:
+//  - Graph state (+ optionally, a Rountangle Editor)
+//  - History graph (+undo/redo buttons)
+//  - Delta graph
+// , their state, and callbacks for updating their state.
 export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
   const versionRegistry = new VersionRegistry();
   const graphState = new GraphState();
   const compositeLevel = new CompositeLevel();
 
+  // SVG coordinates to be used when adding a new node
   let x = 0;
   let y = 0;
 
+
   const initialState: VersionedModelState = {
     version: versionRegistry.initialVersion,
     graph: emptyGraph,
@@ -74,12 +81,21 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
     dependencyGraphL0: emptyGraph,
   }
 
+  // The "current version" is both part of the React state (for rendering undo/redo buttons) and a local variable here, such that we can get the current version (synchronously), even outside of a setState-callback.
+  let currentVersion = versionRegistry.initialVersion;
+  function getCurrentVersion() {
+    return currentVersion;
+  }
+
   // This function may only be called from a functional React component.
-  // It creates the state, and a number of state update functions.
-  function getReducer([state, setState]) {
+  // Given setState callback, returns:
+  //  - Callback functions for updating the state
+  //  - A callback that constructs all React components (to be used in React render function)
+  function getReducer(setState) {
     const setGraph = callback =>
       setState(({graph, ...rest}) => ({graph: callback(graph), ...rest}));
 
+    // Create and add a new version, and its deltas, without changing the current version
     const addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => {
       let composite;
       try {
@@ -108,21 +124,21 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
         });        
       }
     };
-    const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, newVersionCallback?: (Version)=>void) => {
+    const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string): Version => {
       const composite = compositeLevel.createComposite(deltas, description);
+      const newVersion = versionRegistry.createVersion(currentVersion, composite);
+      currentVersion = newVersion;
 
       // update graph state:
       const d3Updater = new D3GraphStateUpdater(setGraph, x, y);
       graphState.exec(composite, d3Updater);
 
       // update rest of state:
-      setState(({version: curVersion, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => {
-        const newVersion = versionRegistry.createVersion(curVersion, composite);
-        newVersionCallback?.(newVersion);
+      setState(({version: oldVersion, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => {
         return {
           version: newVersion,
           // add new version to history graph + highlight the new version as the current version:
-          historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), curVersion, newVersion),
+          historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), oldVersion, 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':
@@ -133,6 +149,8 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
           ...rest,
         };
       });
+
+      return newVersion;
     };
     const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
       const d3Updater = new D3GraphStateUpdater(setGraph, x, y);
@@ -154,6 +172,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
     };
     const undo = (parentVersion, deltaToUndo) => {
       undoWithoutUpdatingHistoryGraph(deltaToUndo);
+      currentVersion = parentVersion;
       setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
         version: parentVersion,
         historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, parentVersion),
@@ -162,6 +181,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
     };
     const redo = (childVersion, deltaToRedo) => {
       redoWithoutUpdatingHistoryGraph(deltaToRedo);
+      currentVersion = childVersion;
       setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
         version: childVersion,
         historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, childVersion),
@@ -169,7 +189,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
       }));
     };
     const gotoVersion = (chosenVersion: Version) => {
-      const path = state.version.findPathTo(chosenVersion);
+      const path = currentVersion.findPathTo(chosenVersion);
       if (path === undefined) {
         throw new Error("Could not find path to version!");
       }
@@ -181,130 +201,129 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
           redoWithoutUpdatingHistoryGraph(delta);
         }
       }
-      setState(({historyGraph, version, ...rest}) => ({
+      currentVersion = chosenVersion;
+      setState(({historyGraph, version: oldVersion, ...rest}) => ({
         version: chosenVersion,
-        historyGraph: setCurrentVersion(historyGraph, version, chosenVersion),
+        historyGraph: setCurrentVersion(historyGraph, oldVersion, chosenVersion),
         ...rest,
       }));
     };
 
-    const getReactComponents = (callbacks: VersionedModelCallbacks) => {
-      const graphStateComponent = makeOverlayHelpIcon(readonly ? 
-        <Graph graph={state.graph} forces={graphForces} />
-        : <EditableGraph
-            graph={state.graph}
-            graphState={graphState}
-            forces={graphForces}
-            generateUUID={generateUUID}
-            primitiveRegistry={primitiveRegistry}
-            setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
-            onUserEdit={callbacks.onUserEdit}
-          />, readonly ? HelpIcons.graphEditorReadonly : HelpIcons.graphEditor);
-
-      const depGraphL1Component = makeOverlayHelpIcon(
-        <Graph graph={state.dependencyGraphL1} forces={graphForces} />,
-        HelpIcons.depGraph);
-      const depGraphL0Component = makeOverlayHelpIcon(
-        <Graph graph={state.dependencyGraphL0} forces={graphForces} />,
-        HelpIcons.depGraph);
-
-      const historyComponent = makeOverlayHelpIcon(
-        <Graph graph={state.historyGraph} forces={graphForces}
-          mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />,
-        HelpIcons.historyGraph);
+    return {
+      addDeltasAndVersion,
+      gotoVersion,
+      createAndGotoNewVersion,
+      undo,
+      redo,
+    };
+  }
 
-      const rountangleEditor = makeOverlayHelpIcon(
-        <RountangleEditor
+  function getReactComponents(state: VersionedModelState, callbacks: VersionedModelCallbacks) {
+    const graphStateComponent = makeOverlayHelpIcon(readonly ? 
+      <Graph graph={state.graph} forces={graphForces} />
+      : <EditableGraph
           graph={state.graph}
+          graphState={graphState}
+          forces={graphForces}
           generateUUID={generateUUID}
           primitiveRegistry={primitiveRegistry}
-          graphState={graphState}
+          setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
           onUserEdit={callbacks.onUserEdit}
-        />,
-        HelpIcons.rountangleEditor);
+        />, readonly ? HelpIcons.graphEditorReadonly : HelpIcons.graphEditor);
 
-      const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => {
-        if (parentsOrChildren.length === 0) {
-          return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} disabled>{text}</Mantine.Button>;
-        }
-        if (parentsOrChildren.length === 1) {
-          return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} onClick={callback?.bind(null, parentsOrChildren[0][0], parentsOrChildren[0][1])}>{text}</Mantine.Button>;
-        }
-        return <Mantine.Menu shadow="md" position="bottom-start" trigger="hover" offset={0} transitionDuration={0}>
-            <Mantine.Menu.Target>
-              <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
-            </Mantine.Menu.Target>
-            <Mantine.Menu.Dropdown>
-              {/*<Mantine.Menu.Label>{text}</Mantine.Menu.Label>*/}
-              {parentsOrChildren.map(([parentOrChildVersion,deltaToUndoOrRedo]) =>
-                <Mantine.Menu.Item key={fullDeltaId(deltaToUndoOrRedo)} onClick={callback?.bind(null, parentOrChildVersion, deltaToUndoOrRedo)}>{deltaToUndoOrRedo.getDescription()}</Mantine.Menu.Item>)}
-            </Mantine.Menu.Dropdown>
-          </Mantine.Menu>;
+    const depGraphL1Component = makeOverlayHelpIcon(
+      <Graph graph={state.dependencyGraphL1} forces={graphForces} />,
+      HelpIcons.depGraph);
+    const depGraphL0Component = makeOverlayHelpIcon(
+      <Graph graph={state.dependencyGraphL0} forces={graphForces} />,
+      HelpIcons.depGraph);
 
-      }
-      const undoButton = makeUndoOrRedoButton(state.version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
-      const redoButton = makeUndoOrRedoButton(state.version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);
+    const historyComponent = makeOverlayHelpIcon(
+      <Graph graph={state.historyGraph} forces={graphForces}
+        mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />,
+      HelpIcons.historyGraph);
 
-      const undoRedoButtons = <>
-        {undoButton}
-        {redoButton}
-      </>;
+    const rountangleEditor = makeOverlayHelpIcon(
+      <RountangleEditor
+        graph={state.graph}
+        generateUUID={generateUUID}
+        primitiveRegistry={primitiveRegistry}
+        graphState={graphState}
+        onUserEdit={callbacks.onUserEdit}
+      />,
+      HelpIcons.rountangleEditor);
 
-      const stackedUndoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => {
-        return (
-          <div key={fullVersionId(parentVersion)}>
-            <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconPlayerTrackPrev size={18}/>} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}>
-              UNDO {deltaToUndo.getDescription()}
-            </Mantine.Button>
-            <Mantine.Space h="xs"/>
-          </div>
-        );
-      });
-      const stackedRedoButtons = state.version.children.map(([childVersion,deltaToRedo]) => {
-        return (
-          <div key={fullVersionId(childVersion)}>
-            <Mantine.Button style={{width: "100%"}} compact={true} rightIcon={<Icons.IconPlayerTrackNext size={18}/>} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}>
-              REDO {deltaToRedo.getDescription()}
-            </Mantine.Button>
-            <Mantine.Space h="xs"/>
-          </div>
-        );
-      });
-      const stackedUndoRedoButtons = (
-        <Mantine.SimpleGrid cols={2}>
-          <div>{stackedUndoButtons}</div>
-          <div>{stackedRedoButtons}</div>
-        </Mantine.SimpleGrid>
-      );
+    const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => {
+      if (parentsOrChildren.length === 0) {
+        return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} disabled>{text}</Mantine.Button>;
+      }
+      if (parentsOrChildren.length === 1) {
+        return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} onClick={callback?.bind(null, parentsOrChildren[0][0], parentsOrChildren[0][1])}>{text}</Mantine.Button>;
+      }
+      return <Mantine.Menu shadow="md" position="bottom-start" trigger="hover" offset={0} transitionDuration={0}>
+          <Mantine.Menu.Target>
+            <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
+          </Mantine.Menu.Target>
+          <Mantine.Menu.Dropdown>
+            {/*<Mantine.Menu.Label>{text}</Mantine.Menu.Label>*/}
+            {parentsOrChildren.map(([parentOrChildVersion,deltaToUndoOrRedo]) =>
+              <Mantine.Menu.Item key={fullDeltaId(deltaToUndoOrRedo)} onClick={callback?.bind(null, parentOrChildVersion, deltaToUndoOrRedo)}>{deltaToUndoOrRedo.getDescription()}</Mantine.Menu.Item>)}
+          </Mantine.Menu.Dropdown>
+        </Mantine.Menu>;
 
-      return {
-        graphStateComponent,
-        rountangleEditor,
-        depGraphL1Component,
-        depGraphL0Component,
-        historyComponent,
-        undoButton,
-        redoButton,
-        undoRedoButtons,
-        stackedUndoRedoButtons,
-      };
     }
+    const undoButton = makeUndoOrRedoButton(state.version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
+    const redoButton = makeUndoOrRedoButton(state.version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);
+
+    const undoRedoButtons = <>
+      {undoButton}
+      {redoButton}
+    </>;
+
+    const stackedUndoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => {
+      return (
+        <div key={fullVersionId(parentVersion)}>
+          <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconPlayerTrackPrev size={18}/>} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}>
+            UNDO {deltaToUndo.getDescription()}
+          </Mantine.Button>
+          <Mantine.Space h="xs"/>
+        </div>
+      );
+    });
+    const stackedRedoButtons = state.version.children.map(([childVersion,deltaToRedo]) => {
+      return (
+        <div key={fullVersionId(childVersion)}>
+          <Mantine.Button style={{width: "100%"}} compact={true} rightIcon={<Icons.IconPlayerTrackNext size={18}/>} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}>
+            REDO {deltaToRedo.getDescription()}
+          </Mantine.Button>
+          <Mantine.Space h="xs"/>
+        </div>
+      );
+    });
+    const stackedUndoRedoButtons = (
+      <Mantine.SimpleGrid cols={2}>
+        <div>{stackedUndoButtons}</div>
+        <div>{stackedRedoButtons}</div>
+      </Mantine.SimpleGrid>
+    );
 
     return {
-      state,
-      getReactComponents,
-      callbacks: {
-        addDeltasAndVersion,
-        gotoVersion,
-        createAndGotoNewVersion,
-        undo,
-        redo,
-      },
+      graphStateComponent,
+      rountangleEditor,
+      depGraphL1Component,
+      depGraphL0Component,
+      historyComponent,
+      undoButton,
+      redoButton,
+      undoRedoButtons,
+      stackedUndoRedoButtons,
     };
   }
 
   return {
     initialState,
+    getCurrentVersion,
     getReducer,
+    getReactComponents,
   };
 }

+ 0 - 1
src/onion/composite_delta.ts

@@ -119,7 +119,6 @@ export class CompositeLevel {
       // Conflicts are symmetric, so newly created conflicts are also added to the other:
       compositeConflict.conflicts.push(composite);
     }
-    console.log(this.containedBy)
     return composite;
   }
 }

+ 0 - 1
src/onion/graph_state.ts

@@ -314,7 +314,6 @@ export class GraphState {
   // }
 
   exec(delta: Delta, listener: GraphStateListener = DUMMY) {
-    console.log('exec...')
     if (delta instanceof CompositeDelta) {
       delta.deltas.forEach(d => this.exec(d, listener));
     }