ソースを参照

More refactoring + WIP: Branching/merging demo

Joeri Exelmans 2 年 前
コミット
4dba3a26bd

+ 32 - 23
src/frontend/app.tsx

@@ -1,9 +1,9 @@
 import * as React from "react";
+import {Stack, Group, Text, Title, Tabs, Space, createStyles} from "@mantine/core";
 
 import {getDemoPD} from "./demo_pd";
 import {getDemoCorr} from "./demo_corr";
-
-import {Stack, Group, Text, Title, Tabs, createStyles} from "@mantine/core";
+import {getDemoBM} from "./demo_bm";
 
 // const useStyles = createStyles(theme => ({
 //   root: {
@@ -16,27 +16,36 @@ import {Stack, Group, Text, Title, Tabs, createStyles} from "@mantine/core";
 //   }
 // }));
 
-const DemoPD = getDemoPD();
-const DemoCorr = getDemoCorr();
 
-export function App(props) {
-  // const {classes} = useStyles();
+export function getApp() {
+  const DemoPD = getDemoPD();
+  const DemoCorr = getDemoCorr();
+  const DemoBM = getDemoBM();
+
+  return function App(props) {
+    // const {classes} = useStyles();
 
-  return <>
-    <Tabs orientation="vertical">
-      <Stack>
-        <Text order={5}>Pick a demo:</Text>
-        <Tabs.List>
-          <Tabs.Tab value="pd">Primitive Delta</Tabs.Tab>
-          <Tabs.Tab value="corr">Correspondence</Tabs.Tab>
-        </Tabs.List>
-      </Stack>
-      <Tabs.Panel value="pd">
-        <DemoPD/>
-      </Tabs.Panel>
-      <Tabs.Panel value="corr">
-        <DemoCorr/>
-      </Tabs.Panel>
-    </Tabs>
-  </>;
+    return <>
+      <Tabs orientation="vertical">
+        <Stack>
+          <Text order={5}>Pick a demo:</Text>
+          <Tabs.List>
+            <Tabs.Tab value="pd">Primitive Delta</Tabs.Tab>
+            <Tabs.Tab value="corr">Correspondence</Tabs.Tab>
+            <Tabs.Tab value="bm">Blended Modeling</Tabs.Tab>
+          </Tabs.List>
+        </Stack>
+        <Space w="md"/>
+        <Tabs.Panel value="pd">
+          <DemoPD/>
+        </Tabs.Panel>
+        <Tabs.Panel value="corr">
+          <DemoCorr/>
+        </Tabs.Panel>
+        <Tabs.Panel value="bm">
+          <DemoBM/>
+        </Tabs.Panel>
+      </Tabs>
+    </>;
+  }
 }

+ 4 - 2
src/frontend/app_state.ts

@@ -7,6 +7,8 @@ import {d3Types} from "./graph";
 export type HistoryGraphType = d3Types.d3Graph<Version|null,Delta>;
 export type DependencyGraphType = d3Types.d3Graph<Delta,null>;
 
+// This module contains functions for updating the state of the 'Graph' React/D3 component.
+
 ///// ALL OF THESE ARE PURE FUNCTIONS: //////
 
 export function initialHistoryGraph(initialVersion) {
@@ -159,7 +161,7 @@ export function setDeltaInactive(prevDepGraph: DependencyGraphType, delta: Delta
     }),
   }
 }
-export function addDeltaAndActivate(prevDepGraph: DependencyGraphType, delta: Delta): DependencyGraphType {
+export function addDeltaAndActivate(prevDepGraph: DependencyGraphType, delta: Delta, active: boolean = true): DependencyGraphType {
   if (prevDepGraph.nodes.some(node => node.id === fullDeltaId(delta))) {
     // We already have this delta (remember that delta's are identified by the hash of their contents, so it is possible that different people concurrently create the same deltas, e.g., by deleting the same node concurrently)
     // Also, when re-doing after undoing, it is possible that a delta is 'added' that we already have.
@@ -167,7 +169,7 @@ export function addDeltaAndActivate(prevDepGraph: DependencyGraphType, delta: De
   }
   return {
     // add one extra node that represents the new delta:
-    nodes: prevDepGraph.nodes.concat(deltaToDepGraphNode(delta, /*highlight: */ true)),
+    nodes: prevDepGraph.nodes.concat(deltaToDepGraphNode(delta, /*highlight: */ active)),
     // for every dependency and conflict, add a link:
     links: prevDepGraph.links.concat(
         ...delta.getTypedDependencies().map(([dep,depSummary]) => dependencyToDepGraphLink(delta,dep,depSummary)),

+ 111 - 0
src/frontend/correspondence.ts

@@ -0,0 +1,111 @@
+import * as React from "react";
+
+import {newVersionedModel} from "./versioned_model";
+import {TrivialParser} from "../parser/trivial_parser";
+import {Version} from "../onion/version";
+import {GraphState} from "../onion/graph_state"; 
+
+// Pure function
+// Replays all deltas in a version to compute the graph state of that version.
+function getGraphState(version: Version): GraphState {
+  const graphState = new GraphState();
+  for (const d of [...version].reverse()) {
+    graphState.exec(d);
+  }
+  return graphState;
+}
+
+export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
+  // const cs = newVersionedModel({readonly, generateUUID, primitiveRegistry});
+  const {initialState, getReducer: getReducerOrig} = newVersionedModel({readonly: true, generateUUID, primitiveRegistry});
+
+  const parser = new TrivialParser(primitiveRegistry, generateUUID);
+
+  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) {
+    const {
+      getReactComponents,
+      callbacks: {
+        gotoVersion: gotoVersionOrig,
+        createAndGotoNewVersion,
+        undo,
+        redo,
+      },
+    } = getReducerOrig([state, setState]);
+
+    const parse = (csDeltas, description: string) => {
+      const [csGS, corrGS, asGS] = [csReducer.state.version, state.version, asReducer.state.version].map(v => getGraphState(v));
+
+      const {corrDeltas, asDeltas} = parser.parse(csDeltas, csGS, corrGS, asGS);
+
+      const csVersionPromise = new Promise<Version>(resolve => {
+        csReducer.callbacks.createAndGotoNewVersion(csDeltas, description, resolve);
+      });
+      const corrVersionPromise = new Promise<Version>(resolve => {
+        createAndGotoNewVersion(corrDeltas, description, resolve);
+      });
+      const asVersionPromise = new Promise<Version>(resolve => {
+        if (asDeltas.length > 0) {
+          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 render = (asDeltas, description: string) => {
+      const [csGS, corrGS, asGS] = [csReducer.state.version, state.version, asReducer.state.version].map(v => getGraphState(v));
+      
+      const {corrDeltas, csDeltas} = parser.render(asDeltas, csGS, corrGS, asGS);
+
+      const csVersionPromise = new Promise<Version>(resolve => {
+        if (asDeltas.length > 0) {
+          csReducer.callbacks.createAndGotoNewVersion(csDeltas, "render:"+description, resolve);
+        } else {
+          resolve(csReducer.state.version);
+        }
+      });
+      const corrVersionPromise = new Promise<Version>(resolve => {
+        createAndGotoNewVersion(corrDeltas, description, resolve);
+      });
+      const asVersionPromise = new Promise<Version>(resolve => {
+        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 gotoVersion = (corrVersion: Version) => {
+      const {csVersion, asVersion} = corrMap.get(corrVersion)!;
+      csReducer.callbacks.gotoVersion(csVersion);
+      gotoVersionOrig(corrVersion);
+      asReducer.callbacks.gotoVersion(asVersion);
+    };
+
+    return {
+      state,
+      getReactComponents,
+      callbacks: {
+        parse,
+        render,
+        gotoVersion,
+        undo,
+        redo,
+      },
+    };
+  }
+
+  return {
+    initialState,
+    getReducer,
+  };
+}

+ 130 - 0
src/frontend/demo_bm.tsx

@@ -0,0 +1,130 @@
+import * as React from "react";
+import {Grid, Text, Title, Group, Stack, Button, Space, Textarea, Tabs, HoverCard, ActionIcon} from "@mantine/core";
+
+import {PrimitiveRegistry} from "../onion/primitive_delta";
+import {mockUuid} from "../onion/test_helpers";
+
+import {newVersionedModel} from "./versioned_model";
+import {newCorrespondence} from "./correspondence";
+
+export function getDemoBM() {
+  const generateUUID = mockUuid();
+  const primitiveRegistry = new PrimitiveRegistry();
+
+  const commonStuff = {generateUUID, primitiveRegistry};
+
+  const as = newVersionedModel({readonly: false, ...commonStuff});
+
+  const cs1 = newVersionedModel({readonly: false, ...commonStuff});
+  const cs2 = newVersionedModel({readonly: false, ...commonStuff});
+
+  const corr1 = newCorrespondence({cs: cs1, as, ...commonStuff});
+  const corr2 = newCorrespondence({cs: cs2, as, ...commonStuff});
+
+  // returns functional react component
+  return function() {
+
+    // const asState = as.useState();
+
+    // const cs1State = cs1.useState();
+    // const cs2State = cs2.useState();
+
+    // const corr1State = corr1.useState({csState: cs1State, asState});
+    // const corr2State = corr2.useState({csState: cs2State, asState});
+
+    // const asComponents = asState.getReactComponents({
+    //   onUserEdit: (...args) => {
+    //     corr1State.callbacks.render(...args);
+    //     corr2State.callbacks.render(...args);
+    //   },
+    //   onUndoClicked: asState.callbacks.undo,
+    //   onRedoClicked: asState.callbacks.redo,
+    //   onVersionClicked: asState.callbacks.gotoVersion,
+    // });
+
+    // const cs1Components = cs1State.getReactComponents({
+    //   onUserEdit: corr1State.callbacks.parse,
+    //   onUndoClicked: cs1State.callbacks.undo,
+    //   onRedoClicked: cs1State.callbacks.redo,
+    //   onVersionClicked: cs1State.callbacks.gotoVersion,
+    // });
+    // const cs2Components = cs2State.getReactComponents({
+    //   onUserEdit: corr2State.callbacks.parse,
+    //   onUndoClicked: cs2State.callbacks.undo,
+    //   onRedoClicked: cs2State.callbacks.redo,
+    //   onVersionClicked: cs2State.callbacks.gotoVersion,
+    // });
+
+    // const corr1Components = corr1State.getReactComponents({
+    //   onUndoClicked: corr1State.callbacks.gotoVersion,
+    //   onRedoClicked: corr1State.callbacks.gotoVersion,
+    //   onVersionClicked: corr1State.callbacks.gotoVersion,
+    // });
+    // const corr2Components = corr2State.getReactComponents({
+    //   onUndoClicked: corr2State.callbacks.gotoVersion,
+    //   onRedoClicked: corr2State.callbacks.gotoVersion,
+    //   onVersionClicked: corr2State.callbacks.gotoVersion,
+    // });
+
+    // const csTabs = ["editor", "state", "history", "dependencyL1", "dependencyL0"];
+    // const asTabs = ["state", "history", "dependencyL1", "dependencyL0"];
+
+    // const makeTabs = (components, defaultTab, tabs) => {
+    //   return <Tabs defaultValue={defaultTab} keepMounted={false}>
+    //     <Tabs.List>
+    //       {tabs.map(tab => ({
+    //         editor: <Tabs.Tab key={tab} value={tab}>Editor</Tabs.Tab>,
+    //         state:  <Tabs.Tab key={tab} value={tab}>State</Tabs.Tab>,
+    //         history: <Tabs.Tab key={tab} value={tab}>History</Tabs.Tab>,
+    //         dependencyL1: <Tabs.Tab key={tab} value={tab}>Deltas (L1)</Tabs.Tab>,
+    //         dependencyL0: <Tabs.Tab key={tab} value={tab}>Deltas (L0)</Tabs.Tab>,
+    //       }[tab]))}
+    //     </Tabs.List>
+    //     <Tabs.Panel value="state">
+    //       {components.graphStateComponent}
+    //     </Tabs.Panel>
+    //     <Tabs.Panel value="editor">
+    //       {components.rountangleEditor}
+    //     </Tabs.Panel>
+    //     <Tabs.Panel value="dependencyL1">
+    //       {components.depGraphL1Component}
+    //     </Tabs.Panel>
+    //     <Tabs.Panel value="dependencyL0">
+    //       {components.depGraphL0Component}
+    //     </Tabs.Panel>
+    //     <Tabs.Panel value="history">
+    //       {components.historyComponent}
+    //     </Tabs.Panel>
+    //   </Tabs>;
+    // };
+
+    // return (
+    //   <Grid grow columns={12}>
+    //     <Grid.Col span={4}>
+    //       <Title order={4}>Concrete Syntax 1</Title>
+    //       <Stack>
+    //         {makeTabs(cs1Components, "state", csTabs)}
+    //         {makeTabs(cs1Components, "history", csTabs)}
+    //         {corr1Components.undoRedoButtons}
+    //       </Stack>
+    //     </Grid.Col>
+    //     <Grid.Col span={4}>
+    //       <Title order={4}>Abstract Syntax</Title>
+    //       <Stack>
+    //         {makeTabs(asComponents, "state", asTabs)}
+    //         {makeTabs(asComponents, "history", asTabs)}
+    //       </Stack>
+    //     </Grid.Col>
+    //     <Grid.Col span={4}>
+    //       <Title order={4}>Concrete Syntax 2</Title>
+    //       <Stack>
+    //         {makeTabs(cs2Components, "state", csTabs)}
+    //         {makeTabs(cs2Components, "history", csTabs)}
+    //       </Stack>
+    //         {corr2Components.undoRedoButtons}
+    //     </Grid.Col>
+    //   </Grid>
+    // );
+    return <></>;
+  }
+}

+ 49 - 130
src/frontend/demo_corr.tsx

@@ -1,29 +1,11 @@
 import * as React from "react";
-import {Grid, Text, Title, Group, Stack, Button, Space, Textarea, Tabs, HoverCard, ActionIcon} from "@mantine/core";
+import {SimpleGrid, Text, Title, Group, Stack, Button, Space, Textarea, Tabs, HoverCard, ActionIcon, Center} from "@mantine/core";
 
 import {PrimitiveRegistry} from "../onion/primitive_delta";
 import {mockUuid} from "../onion/test_helpers";
 
-import {embed, Version, VersionRegistry} from "../onion/version";
-import {GraphState} from "../onion/graph_state"; 
-import {TrivialParser} from "../parser/trivial_parser";
-import {visitPartialOrdering} from "../util/partial_ordering";
-
-import {
-  newVersionedModel,
-  VersionedModelState,
-} from "./versioned_model";
-import * as HelpIcons from "./help_icons";
-
-// Pure function
-// Replays all deltas in a version to compute the graph state of that version.
-function getGraphState(version: Version): GraphState {
-  const graphState = new GraphState();
-  for (const d of [...version].reverse()) {
-    graphState.exec(d);
-  }
-  return graphState;
-}
+import {newVersionedModel} from "./versioned_model";
+import {newCorrespondence} from "./correspondence";
 
 export function getDemoCorr() {
   const generateUUID = mockUuid();
@@ -31,36 +13,48 @@ export function getDemoCorr() {
 
   const commonStuff = {generateUUID, primitiveRegistry};
 
-  const cs = newVersionedModel({readonly: false, ...commonStuff});
-  const corr = newVersionedModel({readonly: true, ...commonStuff});
   const as = newVersionedModel({readonly: false, ...commonStuff});
-
-  const parser = new TrivialParser(primitiveRegistry, generateUUID);
-
-  const corrToCs: Map<Version,Version> = new Map([
-    [corr.initialState.version, cs.initialState.version]
-  ]);
-  const corrToAs: Map<Version,Version> = new Map([
-    [corr.initialState.version, as.initialState.version]
-  ]);
+  const cs = newVersionedModel({readonly: false, ...commonStuff});
+  const corr = newCorrespondence({cs, as, ...commonStuff});
 
   // 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 csComponents = csReducer.getReactComponents({
+      onUserEdit: corrReducer.callbacks.parse,
+      onUndoClicked: csReducer.callbacks.undo,
+      onRedoClicked: csReducer.callbacks.redo,
+      onVersionClicked: csReducer.callbacks.gotoVersion,
+    });
+    const corrComponents = corrReducer.getReactComponents({
+      onUndoClicked: corrReducer.callbacks.gotoVersion,
+      onRedoClicked: corrReducer.callbacks.gotoVersion,
+      onVersionClicked: corrReducer.callbacks.gotoVersion,
+    });
+    const asComponents = asReducer.getReactComponents({
+      onUserEdit: corrReducer.callbacks.render,
+      onUndoClicked: asReducer.callbacks.undo,
+      onRedoClicked: asReducer.callbacks.redo,
+      onVersionClicked: asReducer.callbacks.gotoVersion,
+    });
+
+    const csTabs = ["editor", "state", "history", "dependencyL1", "dependencyL0"];
+    const corrTabs = ["state", "history", "dependencyL1", "dependencyL0"];
+    const asTabs = ["state", "history", "dependencyL1", "dependencyL0"];
+
     const makeTabs = (components, defaultTab, tabs) => {
       return <Tabs defaultValue={defaultTab} keepMounted={false}>
         <Tabs.List>
-          {tabs.map(tab => {
-            if (tab === "editor")
-              return <Tabs.Tab key={tab} value={tab}>Editor</Tabs.Tab>;
-            if (tab === "state")
-              return <Tabs.Tab key={tab} value={tab}>State</Tabs.Tab>;
-            if (tab === "history")
-              return <Tabs.Tab key={tab} value={tab}>History</Tabs.Tab>;
-            if (tab === "dependencyL1")
-              return <Tabs.Tab key={tab} value={tab}>Deltas (L1)</Tabs.Tab>;
-            if (tab === "dependencyL0")
-              return <Tabs.Tab key={tab} value={tab}>Deltas (L0)</Tabs.Tab>;
-          })}
+          {tabs.map(tab => ({
+            editor: <Tabs.Tab key={tab} value={tab}>Editor</Tabs.Tab>,
+            state:  <Tabs.Tab key={tab} value={tab}>State</Tabs.Tab>,
+            history: <Tabs.Tab key={tab} value={tab}>History</Tabs.Tab>,
+            dependencyL1: <Tabs.Tab key={tab} value={tab}>Deltas (L1)</Tabs.Tab>,
+            dependencyL0: <Tabs.Tab key={tab} value={tab}>Deltas (L0)</Tabs.Tab>,
+          }[tab]))}
         </Tabs.List>
         <Tabs.Panel value="state">
           {components.graphStateComponent}
@@ -80,106 +74,31 @@ export function getDemoCorr() {
       </Tabs>;
     }
 
-    const gotoCorrVersion = (corrVersion: Version) => {
-      corrCallbacks.gotoVersion(corrVersion);
-      csCallbacks.gotoVersion(corrToCs.get(corrVersion)!);
-      asCallbacks.gotoVersion(corrToAs.get(corrVersion)!);
-    }
-
-
-    const [csState, setCsState] = React.useState<VersionedModelState>(cs.initialState);
-    const [corrState, setCorrState] = React.useState<VersionedModelState>(corr.initialState);
-    const [asState, setAsState] = React.useState<VersionedModelState>(as.initialState);
-
-    const csTabs = ["editor", "state", "history", "dependencyL1", "dependencyL0"];
-    const csCallbacks = cs.getCallbacks({state: csState, setState: setCsState});
-    const csComponents = cs.getReactComponents({
-      state: csState,
-      callbacks: {
-        onUserEdit: (csDeltas, description: string) => {
-          const [csGS, corrGS, asGS] = [csState.version, corrState.version, asState.version].map(v => getGraphState(v));
-
-          const {corrDeltas, asDeltas} = parser.parse(csDeltas, csGS, corrGS, asGS);
-
-          const newCsVersion = csCallbacks.createAndGotoNewVersion(csDeltas, description);
-          const newCorrVersion = corrCallbacks.createAndGotoNewVersion(corrDeltas, description);
-          const newAsVersion = asCallbacks.createAndGotoNewVersion(asDeltas, "parse:"+description);
-
-          corrToCs.set(newCorrVersion, newCsVersion);
-          corrToAs.set(newCorrVersion, newAsVersion);
-        },
-        onUndoClicked: csCallbacks.undo,
-        onRedoClicked: csCallbacks.redo,
-        onVersionClicked: csCallbacks.gotoVersion,
-      },
-    });
-
-    const corrTabs = ["state", "history", "dependencyL1", "dependencyL0"];
-    const corrCallbacks = corr.getCallbacks({state: corrState, setState: setCorrState});
-    const corrComponents = corr.getReactComponents({
-      state: corrState,
-      callbacks: {
-        onUserEdit: corrCallbacks.createAndGotoNewVersion,
-        onUndoClicked: gotoCorrVersion,
-        onRedoClicked: gotoCorrVersion,
-        onVersionClicked: gotoCorrVersion,
-      },
-    });
-
-    const asTabs = ["state", "history", "dependencyL1", "dependencyL0"];
-    const asCallbacks = as.getCallbacks({state: asState, setState: setAsState});
-    const asComponents = as.getReactComponents({
-      state: asState,
-      callbacks: {
-        onUserEdit: (asDeltas, description: string) => {
-          const [csGS, corrGS, asGS] = [csState.version, corrState.version, asState.version].map(v => getGraphState(v));
-          
-          const {corrDeltas, csDeltas} = parser.render(asDeltas, csGS, corrGS, asGS);
-
-          const newCsVersion = csCallbacks.createAndGotoNewVersion(csDeltas, "render:"+description);
-          const newCorrVersion = corrCallbacks.createAndGotoNewVersion(corrDeltas, description);
-          const newAsVersion = asCallbacks.createAndGotoNewVersion(asDeltas, description);
-
-          corrToCs.set(newCorrVersion, newCsVersion);
-          corrToAs.set(newCorrVersion, newAsVersion);
-        },
-        onUndoClicked: asCallbacks.undo,
-        onRedoClicked: asCallbacks.redo,
-        onVersionClicked: asCallbacks.gotoVersion,
-      },
-    });
-
-    return (
-      <Grid grow columns={12}>
-        <Grid.Col span={4}>
+    return (<>
+      <SimpleGrid cols={3}>
+        <div>
           <Title order={4}>Concrete Syntax</Title>
           <Stack>
             {makeTabs(csComponents, "editor", csTabs)}
             {makeTabs(csComponents, "history", csTabs)}
-            {/*{csComponents.undoRedoButtons}*/}
           </Stack>
-        </Grid.Col>
-        <Grid.Col span={4}>
+        </div>
+        <div>
           <Title order={4}>Correspondence</Title>
           <Stack>
             {makeTabs(corrComponents, "state", corrTabs)}
             {makeTabs(corrComponents, "history", corrTabs)}
           </Stack>
-        </Grid.Col>
-        <Grid.Col span={4}>
+        </div>
+        <div>
           <Title order={4}>Abstract Syntax</Title>
           <Stack>
             {makeTabs(asComponents, "state", asTabs)}
             {makeTabs(asComponents, "history", asTabs)}
-            {/*{asComponents.undoRedoButtons}*/}
           </Stack>
-        </Grid.Col>
-        <Grid.Col span={2}/>
-        <Grid.Col span={8}>
-          {corrComponents.undoRedoButtons}
-        </Grid.Col>
-        <Grid.Col span={2}/>
-      </Grid>
-    );
+        </div>
+      </SimpleGrid>
+      <Center>{corrComponents.undoRedoButtons}</Center>
+    </>);
   }
 }

+ 99 - 45
src/frontend/demo_pd.tsx

@@ -1,28 +1,9 @@
 import * as React from "react";
-import {Title, Grid} from "@mantine/core";
+import * as Icons from "@tabler/icons";
+import {Title, Text, Grid, Button, SimpleGrid, Group, Center, ScrollArea, Card, CloseButton, Divider, Space} 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 {PrimitiveRegistry} from "../onion/primitive_delta";
 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";
 
 import {
   newVersionedModel,
@@ -35,33 +16,106 @@ export function getDemoPD() {
 
   const model = newVersionedModel({readonly: false, generateUUID, primitiveRegistry});
 
+  const initialState: [string, VersionedModelState, any][] = [
+    ["master", model.initialState, model],
+  ];
+
   return function() {
-    const [state, setState] = React.useState<VersionedModelState>(model.initialState);
+    const [globalState, setGlobalState] = React.useState<[string, VersionedModelState, any][]>(initialState);
+
+    const getSetBranchState = i => {
+      return callback => {
+        setGlobalState(prevGlobalState => {
+          const copy = prevGlobalState.slice();
+          const [branchName, prevBranchState, m] = copy[i];
+          copy[i] = [branchName, callback(prevBranchState), m];
+          return copy;
+        });
+      };
+    }
 
-    const callbacks = model.getCallbacks({state, setState});
-    const components = model.getReactComponents({state,
-      callbacks: {
+    return <>{ globalState.map(([branchName, branchState, {getReducer}], i) => {
+        const setBranchState = getSetBranchState(i);
+
+        const {callbacks, getReactComponents} = getReducer([branchState, setBranchState]);
+
+        const components = getReactComponents({
           onUserEdit: callbacks.createAndGotoNewVersion,
           onUndoClicked: callbacks.undo,
           onRedoClicked: callbacks.redo,
           onVersionClicked: callbacks.gotoVersion,
-      }});
-
-    return <>
-      <Title order={4}>State</Title>
-      {components.graphStateComponent}
-
-      <Grid grow>
-        <Grid.Col span={1}>
-          <Title order={4}>Deltas</Title>
-          {components.depGraphL0Component}
-        </Grid.Col>
-        <Grid.Col span={1}>
-          <Title order={4}>History</Title>
-          {components.historyComponent}
-        </Grid.Col>
-      </Grid>
-      {components.undoRedoButtons}
-    </>;
+        });
+
+        const mergeClicked = () => {
+          const [mergeWithBranchName, mergeWithBranchState, {getReducer: mergeWithReducer}] = globalState[i-1];
+          const {callbacks: mergeWithCallbacks} = mergeWithReducer([mergeWithBranchState, getSetBranchState(i-1)]);
+          console.log({mergeWithCallbacks})
+          const addRecursive = ([version, delta, _]) => {
+            if (version.parents.length > 0) {
+              addRecursive(version.parents[0])
+            }
+            mergeWithCallbacks.addDeltasAndVersion(delta.deltas, delta.getDescription(), version.hash);
+          }
+          addRecursive(branchState.version.parents[0]);
+          setGlobalState(prevGlobalState => [
+            ...prevGlobalState.slice(0,i),
+            ...prevGlobalState.slice(i+1),
+          ]);
+        }
+
+        const branchClicked = () => {
+          const newBranchName = prompt("Branch name: (ESC to cancel)", "branch");
+          if (newBranchName === null) {
+            return;
+          }
+          if (globalState.some(([existingBranchName]) => existingBranchName === newBranchName)) {
+            alert("Branch with this name already exists!");
+            return;
+          }
+          const newModel = newVersionedModel({readonly: false, generateUUID, primitiveRegistry});
+          const newBranchState = newModel.initialState;
+
+          setGlobalState(prevGlobalState => [
+            ...prevGlobalState.slice(0,i+1),
+            [newBranchName, newBranchState, newModel],
+            ...prevGlobalState.slice(i+1),
+          ]);
+
+          const setNewBranchState = getSetBranchState(i+1);
+
+          const {callbacks} = newModel.getReducer([newBranchState, setNewBranchState]);
+          const compositeDeltas = [...branchState.version].reverse();
+          compositeDeltas.forEach((c: any) => {
+            callbacks.createAndGotoNewVersion(c.deltas, c.getDescription());
+          });
+        }
+
+        return <div key={branchName}>
+          <Divider my="sm" label={"branch: "+branchName} labelPosition="center" />
+            {/*{<CloseButton/>}*/}
+          <SimpleGrid cols={3}>
+            <div>
+              <Text>State</Text>
+              {components.graphStateComponent}
+            </div>
+            <div>
+              <Text>History</Text>
+              {components.historyComponent}
+              <Space h="md"/>
+              <SimpleGrid cols={4} spacing="xs">
+                <Button onClick={mergeClicked} compact disabled={i===0} leftIcon={<Icons.IconChevronUp/>}>Merge </Button>
+                {components.undoRedoButtons}
+                <Button onClick={branchClicked} compact rightIcon={<Icons.IconChevronDown/>}>Branch</Button>
+              </SimpleGrid>
+            </div>
+            <div>
+              <Text>Deltas</Text>
+              {components.depGraphL0Component}
+            </div>
+          </SimpleGrid>
+          <Space h="md"/>
+        </div>
+      })
+    }</>;
   }
-}
+}

+ 1 - 1
src/frontend/index.css

@@ -10,6 +10,6 @@ html,body, #root {
 .canvas {
   background-color: #eee;
   width: 100%;
-  height: 400px;
+  height: 350px;
   vertical-align:top;
 }

+ 2 - 1
src/frontend/index.tsx

@@ -4,10 +4,11 @@ import './rountangleEditor/RountangleEditor.css';
 import './graph.css';
 import './index.css';
 
-import {App} from "./app";
+import {getApp} from "./app";
 
 const container = document.getElementById('root');
 const root = createRoot(container!);
+const App = getApp();
 
 root.render(
   <React.StrictMode>

+ 169 - 103
src/frontend/versioned_model.tsx

@@ -58,7 +58,6 @@ function makeOverlayHelpIcon(background, helpIcon) {
   );
 }
 
-
 export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
   const versionRegistry = new VersionRegistry();
   const graphState = new GraphState();
@@ -67,31 +66,73 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
   let x = 0;
   let y = 0;
 
-  function getCallbacks({state, setState}) {
+  const initialState: VersionedModelState = {
+    version: versionRegistry.initialVersion,
+    graph: emptyGraph,
+    historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
+    dependencyGraphL1: emptyGraph,
+    dependencyGraphL0: emptyGraph,
+  }
+
+  // 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]) {
     const setGraph = callback =>
       setState(({graph, ...rest}) => ({graph: callback(graph), ...rest}));
 
-    const addVersionAndDeltas = (newVersion: Version, composite: CompositeDelta) => {
-      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 addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => {
+      let composite;
+      try {
+        composite = compositeLevel.createComposite(deltas, description);
+      } catch(e) {
+        return;
+      }
+
+      const parentVersion = versionRegistry.lookupOptional(parentHash);
+      if (parentVersion !== undefined) {
+        const newVersion = versionRegistry.createVersion(parentVersion, composite);
+
+        setState(({historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => {
+          return {
+            // add new version to history graph + highlight the new version as the current version:
+            historyGraph: appendToHistoryGraph(historyGraph, newVersion),
+            // add the composite delta to the L1-graph + highlight it as 'active':
+            dependencyGraphL1: composite.deltas.length > 0 ? addDeltaAndActivate(dependencyGraphL1, composite, false) : 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, false);
+              }, dependencyGraphL0),
+            ...rest,
+          };
+        });        
+      }
     };
-    const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, embeddings = new Map()) => {
+    const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, newVersionCallback?: (Version)=>void) => {
       const composite = compositeLevel.createComposite(deltas, description);
-      const version = versionRegistry.createVersion(state.version, composite, embeddings);
-      addVersionAndDeltas(version, composite);
-      gotoVersion(version);
-      return version;
+
+      // 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);
+        return {
+          version: newVersion,
+          // add new version to history graph + highlight the new version as the current version:
+          historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), curVersion, 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) => {
       const d3Updater = new D3GraphStateUpdater(setGraph, x, y);
@@ -146,99 +187,124 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
         ...rest,
       }));
     };
-    return {
-      gotoVersion,
-      createAndGotoNewVersion,
-      addVersionAndDeltas,
-      undo,
-      redo,
-    };
-  }
 
-  // this function may only be called from a functional react component!
-  function getReactComponents({state, callbacks}: {state: VersionedModelState, callbacks: VersionedModelCallbacks}) {
-    const graphStateComponent = makeOverlayHelpIcon(readonly ? 
-      <Graph graph={state.graph} forces={graphForces} />
-      : <EditableGraph
+    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);
+
+      const rountangleEditor = makeOverlayHelpIcon(
+        <RountangleEditor
           graph={state.graph}
-          graphState={graphState}
-          forces={graphForces}
           generateUUID={generateUUID}
           primitiveRegistry={primitiveRegistry}
-          setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
+          graphState={graphState}
           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);
-
-    const rountangleEditor = makeOverlayHelpIcon(
-      <RountangleEditor
-        graph={state.graph}
-        generateUUID={generateUUID}
-        primitiveRegistry={primitiveRegistry}
-        graphState={graphState}
-        onUserEdit={callbacks.onUserEdit}
-      />,
-      HelpIcons.rountangleEditor);
-
-    const undoButtons = 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 redoButtons = 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>
+        />,
+        HelpIcons.rountangleEditor);
+
+      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 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>
       );
-    });
-    const undoRedoButtons = (
-      <Mantine.SimpleGrid cols={2}>
-        <div>{undoButtons}</div>
-        <div>{redoButtons}</div>
-      </Mantine.SimpleGrid>
-    );
+
+      return {
+        graphStateComponent,
+        rountangleEditor,
+        depGraphL1Component,
+        depGraphL0Component,
+        historyComponent,
+        undoButton,
+        redoButton,
+        undoRedoButtons,
+        stackedUndoRedoButtons,
+      };
+    }
 
     return {
-      graphStateComponent,
-      rountangleEditor,
-      depGraphL1Component,
-      depGraphL0Component,
-      historyComponent,
-      // undoButtons,
-      // redoButtons,
-      undoRedoButtons,
+      state,
+      getReactComponents,
+      callbacks: {
+        addDeltasAndVersion,
+        gotoVersion,
+        createAndGotoNewVersion,
+        undo,
+        redo,
+      },
     };
   }
 
   return {
-    initialState: {
-      version: versionRegistry.initialVersion,
-      graph: emptyGraph,
-      historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
-      dependencyGraphL1: emptyGraph,
-      dependencyGraphL0: emptyGraph,
-    },
-    getCallbacks,
-    getReactComponents,
+    initialState,
+    getReducer,
   };
 }

+ 2 - 2
src/onion/composite_delta.ts

@@ -65,7 +65,7 @@ export class CompositeLevel {
 
     for (const delta of deltas) {
       if (this.containedBy.has(delta)) {
-        throw new Error("Assertion failed: delta already part of another composite");
+        throw new Error("Assertion failed: delta " + delta.getDescription() + " already part of another composite");
       }
       for (const [dependency, dependencyType] of delta.getTypedDependencies()) {
         if (!deltas.includes(dependency)) {
@@ -119,7 +119,7 @@ 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;
   }
 }