Forráskód Böngészése

Begin refactor of 'single_model' module

Joeri Exelmans 1 éve
szülő
commit
876ac7bcd7

+ 28 - 53
src/frontend/demos/demo_le.tsx

@@ -6,8 +6,9 @@ import {PrimitiveRegistry, PrimitiveDelta} from "onion/primitive_delta";
 import {mockUuid} from "onion/test_helpers";
 import {PrimitiveValue} from "onion/types";
 import {INodeState, IValueState} from "onion/graph_state";
+import {Version} from "onion/version";
 
-import {newVersionedModel, VersionedModelState} from "../versioned_model/single_model";
+import {newOnion} from "../versioned_model/single_model2";
 import {MergeView} from "../versioned_model/merge_view";
 import {InfoHoverCard} from "../info_hover_card";
 import {OnionContext} from "../onion_context";
@@ -17,7 +18,7 @@ import {Actionblock, Resultblock} from "./blocks";
 export const demo_LE_description =
     <>
         <Title order={4}>
-            List Editor
+            List Editorcom
         </Title>
         <Text>
             This demo was not mentioned in our (submitted) paper, and was added in response to a question from one of the reviewers. The reviewer wanted to know if we can support data structures other than objects diagrams with primitive values in the objects' slots.
@@ -28,32 +29,30 @@ export const demo_LE_description =
     </>;
 
 export function getDemoLE() {
-    const model = newVersionedModel({readonly: true});
-
     // We manually create our own OnionContext, because we need access to it before we even create our React component.
     const primitiveRegistry = new PrimitiveRegistry();
     const generateUUID = mockUuid();
     const listNodeId = generateUUID();
+    const onion = newOnion({readonly: true, primitiveRegistry});
 
     let nextVal = 42;
 
     // returns functional react component
     return function () {
-        const [modelState, setModelState] = React.useState<VersionedModelState>(model.initialState);
+        const {state, reducer, components} = onion.useOnion(reducer => ({}));
         const [pseudoDelete, setPseudoDelete] = React.useState<boolean>(true);
-        const modelReducer = model.getReducer(setModelState);
 
         React.useEffect(() => {
             // idempotent:
             const listNodeCreation = primitiveRegistry.newNodeCreation(listNodeId);
-            modelReducer.createAndGotoNewVersion([
+            reducer.createAndGotoNewVersion([
                 listNodeCreation,
                 primitiveRegistry.newEdgeCreation(listNodeCreation, "next", listNodeCreation),
                 primitiveRegistry.newEdgeCreation(listNodeCreation, "prev", listNodeCreation),
-            ], "createList", model.initialState.version);
+            ], "createList", onion.initialState.version);
         }, []);
 
-        const listNode = model.graphState.nodes.get(listNodeId.value) as INodeState;
+        const listNode = onion.graphState.nodes.get(listNodeId.value) as INodeState;
 
         // Inserts list item after prevNode and before nextNode.
         function insertBetween(prevNode: INodeState, nextNode: INodeState, val: PrimitiveValue) {
@@ -62,22 +61,22 @@ export function getDemoLE() {
                 throw new Error("Assertion failed");
             }
 
-            model.graphState.pushState(); // we will go back to this checkpoint
+            onion.graphState.pushState(); // we will go back to this checkpoint
 
             const newItemCreation = primitiveRegistry.newNodeCreation(generateUUID());
-            model.graphState.exec(newItemCreation);
+            onion.graphState.exec(newItemCreation);
             const updateNext = prevNode.getDeltasForSetEdge(primitiveRegistry, "next", newItemCreation);
-            updateNext.forEach(d => model.graphState.exec(d));
+            updateNext.forEach(d => onion.graphState.exec(d));
             const updatePrev = nextNode.getDeltasForSetEdge(primitiveRegistry, "prev", newItemCreation);
-            updatePrev.forEach(d => model.graphState.exec(d));
+            updatePrev.forEach(d => onion.graphState.exec(d));
 
-            model.graphState.exec(primitiveRegistry.newEdgeCreation(newItemCreation, "value", val));
-            model.graphState.exec(primitiveRegistry.newEdgeCreation(newItemCreation, "prev", prevNode.creation));
-            model.graphState.exec(primitiveRegistry.newEdgeCreation(newItemCreation, "next", nextNode.creation));
+            onion.graphState.exec(primitiveRegistry.newEdgeCreation(newItemCreation, "value", val));
+            onion.graphState.exec(primitiveRegistry.newEdgeCreation(newItemCreation, "prev", prevNode.creation));
+            onion.graphState.exec(primitiveRegistry.newEdgeCreation(newItemCreation, "next", nextNode.creation));
 
-            const deltas = model.graphState.popState();
+            const deltas = onion.graphState.popState();
 
-            modelReducer.createAndGotoNewVersion(deltas, "insert"+JSON.stringify(val));
+            reducer.createAndGotoNewVersion(deltas, "insert"+JSON.stringify(val));
         }
 
         function insertBefore(node: INodeState, val: PrimitiveValue) {
@@ -94,34 +93,24 @@ export function getDemoLE() {
             const prevNode = node.getOutgoingEdges().get("prev") as INodeState;
             const nextNode = node.getOutgoingEdges().get("next") as INodeState;
 
-            model.graphState.pushState(); // we will go back to this checkpoint
+            onion.graphState.pushState(); // we will go back to this checkpoint
 
-            prevNode.getDeltasForSetEdge(primitiveRegistry, "next", nextNode.creation).forEach(d => model.graphState.exec(d));
-            nextNode.getDeltasForSetEdge(primitiveRegistry, "prev", prevNode.creation).forEach(d => model.graphState.exec(d));
-            node.getDeltasForDelete(primitiveRegistry).forEach(d => model.graphState.exec(d));
+            prevNode.getDeltasForSetEdge(primitiveRegistry, "next", nextNode.creation).forEach(d => onion.graphState.exec(d));
+            nextNode.getDeltasForSetEdge(primitiveRegistry, "prev", prevNode.creation).forEach(d => onion.graphState.exec(d));
+            node.getDeltasForDelete(primitiveRegistry).forEach(d => onion.graphState.exec(d));
 
-            const deltas = model.graphState.popState();
+            const deltas = onion.graphState.popState();
 
-            modelReducer.createAndGotoNewVersion(deltas, "delete"+JSON.stringify((node.getOutgoingEdges().get("value") as IValueState).value));
+            reducer.createAndGotoNewVersion(deltas, "delete"+JSON.stringify((node.getOutgoingEdges().get("value") as IValueState).value));
         }
 
         // Alternative implementation of deletion - just add a property 'deleted' to the deleted node.
         function deleteItemAlt(node: INodeState) {
             const deltas = node.getDeltasForSetEdge(primitiveRegistry, "deleted", true);
 
-            modelReducer.createAndGotoNewVersion(deltas, "delete"+JSON.stringify((node.getOutgoingEdges().get("value") as IValueState).value));
+            reducer.createAndGotoNewVersion(deltas, "delete"+JSON.stringify((node.getOutgoingEdges().get("value") as IValueState).value));
         }
 
-        const modelComponents = model.getReactComponents(modelState, setModelState, {
-            onUserEdit: (deltas, description) => {
-                const newVersion = modelReducer.createAndGotoNewVersion(deltas, description);
-            },
-            onUndoClicked: modelReducer.undo,
-            onRedoClicked: modelReducer.redo,
-            onVersionClicked: modelReducer.gotoVersion,
-            onMerge: modelReducer.appendVersions,
-        });
-
         const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
 
         // Recursively renders list elements as a vertical stack
@@ -181,35 +170,21 @@ export function getDemoLE() {
                         <Stack>
                             <Title order={5}>History</Title>
                             <Center>
-                                {modelComponents.undoRedoButtons}
+                                {components.undoRedoButtons}
                                 <Space w="sm"/>
                                 <InfoHoverCard>
                                     {undoButtonHelpText}
                                 </InfoHoverCard>
                             </Center>
-                            {modelComponents.historyComponentWithMerge}
+                            {components.historyComponentWithMerge}
                             <Title order={5}>Deltas</Title>
-                            {modelComponents.makeTabs("deltaL1", ["deltaL1", "deltaL0"])}
+                            {components.makeTabs("deltaL1", ["deltaL1", "deltaL0"])}
                         </Stack>
                     </div>
                     <div>
                         <Stack>
                             <Title order={5}>Graph State (read-only)</Title>
-                            {modelComponents.graphStateComponent}
-                            <div>
-                            Nodes:
-                            <ul>
-                                {modelState.graph.nodes.map(n => {
-                                    return <li key={n.id}>{n.id}</li>;
-                                })}
-                            </ul>
-                            Edges:
-                            <ul>
-                                {modelState.graph.links.map(e => {
-                                    return <li key={e.source.id+"-"+e.label+"->"+e.target.id}>{e.source.id} ---{e.label}--&gt; {e.target.id}</li>
-                                })}
-                            </ul>
-                            </div>
+                            {components.graphStateComponent}
                         </Stack>
                     </div>
                 </SimpleGrid>

+ 50 - 54
src/frontend/demos/demo_live.tsx

@@ -4,14 +4,14 @@ import {Button, Divider, Group, Image, SimpleGrid, Space, Text, Title, TextInput
 
 import { Graphviz } from 'graphviz-react';
 
-import {newVersionedModel, undoButtonHelpText, VersionedModelState,} from '../versioned_model/single_model';
+import {newOnion, undoButtonHelpText, VersionedModelState,} from '../versioned_model/single_model2';
 import {InfoHoverCard, InfoHoverCardOverlay} from "../info_hover_card";
 import {OnionContext} from "../onion_context";
 
 import {mockUuid} from "onion/test_helpers";
 import {PrimitiveRegistry, PrimitiveDelta} from "onion/primitive_delta";
 import {INodeState, IValueState} from "onion/graph_state";
-import {Version, VersionRegistry} from "onion/version";
+import {Version} from "onion/version";
 import {GraphState} from "onion/graph_state"; 
 
 export const demo_Live_description = <>
@@ -29,24 +29,38 @@ const getStateName = s => (s.getOutgoingEdges().get("name") as IValueState).valu
 export function getDemoLive() {
   const primitiveRegistry = new PrimitiveRegistry();
   const generateUUID = mockUuid();
-  const versionRegistry = new VersionRegistry();
-  const model = newVersionedModel({readonly: true}, versionRegistry);
+  const onion = newOnion({readonly: true, primitiveRegistry});
 
   // To efficiently create a delta in the context of the design model only
   const designModelGraphState = new GraphState();
 
   // Mapping from run-time model to design model version
-  const rt2d = new Map([[versionRegistry.initialVersion, versionRegistry.initialVersion]]);
+  const rt2d = new Map([[onion.versionRegistry.initialVersion, onion.versionRegistry.initialVersion]]);
 
 
   const modelId = generateUUID();
 
   return function DemoSem() {
 
-    const [modelState, setDesignModelState] = React.useState<VersionedModelState>(model.initialState);
+    const {state, reducer, components} = onion.useOnion(reducer => ({
+      onUndoClicked: (parentVersion, deltaToUndo) => {
+        gotoMatchingDesignVersion(parentVersion);
+        reducer.undo(parentVersion, deltaToUndo);
+      },
+      onRedoClicked: (childVersion, deltaToRedo) => {
+        gotoMatchingDesignVersion(childVersion);
+        reducer.redo(childVersion, deltaToRedo);
+      },
+      onVersionClicked: (version: Version) => {
+        gotoMatchingDesignVersion(version);
+        reducer.gotoVersion(version);
+      },
+      onMerge: reducer.appendVersions,
+      onImport: (json: Array<any>) => reducer.importVersion(json, primitiveRegistry),
+    }));
 
     const gotoMatchingDesignVersion = (rtVersion: Version) => {
-      const currentDesignVersion = rt2d.get(modelState.version)!;
+      const currentDesignVersion = rt2d.get(state.version)!;
       const newDesignVersion = rt2d.get(rtVersion)!;
       const path = currentDesignVersion.findPathTo(newDesignVersion)!;
       for (const [linkType, delta] of path) {
@@ -59,16 +73,15 @@ export function getDemoLive() {
       }
     }
 
-    const modelReducer = model.getReducer(setDesignModelState);
 
     // We start out with the 'createList' delta already having occurred:
     React.useEffect(() => {
         // idempotent:
         const modelCreation = primitiveRegistry.newNodeCreation(modelId);
-        const newVersion = modelReducer.createAndGotoNewVersion([
+        const newVersion = reducer.createAndGotoNewVersion([
             modelCreation,
             primitiveRegistry.newEdgeCreation(modelCreation, "type", "DesignModel"),
-        ], "createDesignModel", model.initialState.version);
+        ], "createDesignModel", onion.initialState.version);
         rt2d.set(newVersion, newVersion);
         gotoMatchingDesignVersion(newVersion);
     }, []);
@@ -160,28 +173,11 @@ export function getDemoLive() {
     };
 
     // Whenever the current version changes, we calculate a bunch of values that are needed in the UI and in its callbacks
-    const runtimeStuff = React.useMemo(() => getRuntimeModelStuff(model.graphState),
-      [modelState.version]); // memoize the values for each version
+    const runtimeStuff = React.useMemo(() => getRuntimeModelStuff(onion.graphState),
+      [state.version]); // memoize the values for each version
 
     const designStuff = React.useMemo(() => getDesignModelStuff(designModelGraphState),
-      [rt2d.get(modelState.version)]); // memoize the values for each version
-
-
-    const modelComponents = model.getReactComponents(modelState, setDesignModelState, {
-      onUndoClicked: (parentVersion, deltaToUndo) => {
-        gotoMatchingDesignVersion(parentVersion);
-        modelReducer.undo(parentVersion, deltaToUndo);
-      },
-      onRedoClicked: (childVersion, deltaToRedo) => {
-        gotoMatchingDesignVersion(childVersion);
-        modelReducer.redo(childVersion, deltaToRedo);
-      },
-      onVersionClicked: (version: Version) => {
-        gotoMatchingDesignVersion(version);
-        modelReducer.gotoVersion(version);
-      },
-      onMerge: modelReducer.appendVersions,
-    });
+      [rt2d.get(state.version)]); // memoize the values for each version
 
     const [addStateName, setAddStateName] = React.useState<string>("A");
     const [addTransitionSrc, setAddTransitionSrc] = React.useState<string|null>(null);
@@ -197,35 +193,35 @@ export function getDemoLive() {
 
     const editDesignModel = callback => {
       if (runtimeStuff.modelNode !== null) {
-        const currentDesignVersion = rt2d.get(modelState.version)!;
+        const currentDesignVersion = rt2d.get(state.version)!;
 
         // perform edit on run-time model:
-        const {compositeLabel, deltas} = execTransaction(model.graphState, runtimeStuff.modelNode, callback);
+        const {compositeLabel, deltas} = execTransaction(onion.graphState, runtimeStuff.modelNode, callback);
 
         // see if the edit can be applied also on the design model:
         let newDesignVersion;
         try {
-          newDesignVersion = modelReducer.addDeltasAndVersion(deltas, "design:"+compositeLabel, currentDesignVersion.hash)!; // may throw
+          newDesignVersion = reducer.addDeltasAndVersion(deltas, "design:"+compositeLabel, currentDesignVersion.hash)!; // may throw
         }
         catch (e) {
           // alert("could not apply this exact same delta on the design model - probably there's a missing dependency:\n" + e.toString() + "\nTherefore, I will attempt to create a new delta with the same /intent/ on the design model.");
 
           const {compositeLabel, deltas} = execTransaction(designModelGraphState, designStuff.modelNode, callback);
-          newDesignVersion = modelReducer.addDeltasAndVersion(deltas, "design':"+compositeLabel, currentDesignVersion.hash)!; // won't throw this time
+          newDesignVersion = reducer.addDeltasAndVersion(deltas, "design':"+compositeLabel, currentDesignVersion.hash)!; // won't throw this time
         }
 
-        const newRtVersion = modelReducer.createAndGotoNewVersion(deltas, "design:"+compositeLabel);
+        const newRtVersion = reducer.createAndGotoNewVersion(deltas, "design:"+compositeLabel);
         rt2d.set(newRtVersion, newDesignVersion);
         rt2d.set(newDesignVersion, newDesignVersion);
         gotoMatchingDesignVersion(newRtVersion);
       }
     }
     const editRuntimeModel = callback => {
-      const currentDesignVersion = rt2d.get(modelState.version)!;
+      const currentDesignVersion = rt2d.get(state.version)!;
 
       // perform edit on run-time model:
-      const {compositeLabel, deltas} = execTransaction(model.graphState, runtimeStuff.modelNode, callback);
-      const newRtVersion = modelReducer.createAndGotoNewVersion(deltas, "runtime:"+compositeLabel);
+      const {compositeLabel, deltas} = execTransaction(onion.graphState, runtimeStuff.modelNode, callback);
+      const newRtVersion = reducer.createAndGotoNewVersion(deltas, "runtime:"+compositeLabel);
 
       // the run-time change did not change the design model:
       rt2d.set(newRtVersion, currentDesignVersion);
@@ -272,7 +268,7 @@ export function getDemoLive() {
       }
     }
 
-    function addState() {
+    function onAddState() {
       editDesignModel((graphState, modelNode) => {
         const nodeId = generateUUID();
         const nodeCreation = primitiveRegistry.newNodeCreation(nodeId);
@@ -280,7 +276,7 @@ export function getDemoLive() {
         graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "State"));
         graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "name", addStateName));
 
-        // modelNode.getDeltasForSetEdge(primitiveRegistry, "contains-"+JSON.stringify(nodeId.value), nodeCreation).forEach(d => graphState.exec(d));
+        modelNode.getDeltasForSetEdge(primitiveRegistry, "contains-"+JSON.stringify(nodeId.value), nodeCreation).forEach(d => graphState.exec(d));
 
         // Bonus feature: Auto-increment state names
         if (addStateName.match(/[A-Z]/i)) {
@@ -290,7 +286,7 @@ export function getDemoLive() {
         return {compositeLabel: "addState:"+addStateName};
       });
     }
-    function addTransition() {
+    function onAddTransition() {
       editDesignModel((graphState, modelNode) => {
         const nodeId = generateUUID();
         const nodeCreation = primitiveRegistry.newNodeCreation(nodeId);
@@ -300,7 +296,7 @@ export function getDemoLive() {
         graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "src", findState(graphState, addTransitionSrc!)!.creation));
         graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "tgt", findState(graphState, addTransitionTgt!)!.creation));
 
-        // modelNode.getDeltasForSetEdge(primitiveRegistry, "contains-"+JSON.stringify(nodeId.value), nodeCreation).forEach(d => graphState.exec(d));
+        modelNode.getDeltasForSetEdge(primitiveRegistry, "contains-"+JSON.stringify(nodeId.value), nodeCreation).forEach(d => graphState.exec(d));
 
         return {compositeLabel: "addTransition:"+addTransitionSrc+"--"+addTransitionEvent+"->"+addTransitionTgt};
       });
@@ -344,12 +340,12 @@ export function getDemoLive() {
         editRuntimeModel((graphState) => {
           const runtimeId = generateUUID();
           const nodeCreation = primitiveRegistry.newNodeCreation(runtimeId);
-          model.graphState.exec(nodeCreation);
-          model.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "RuntimeModel"));
-          model.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "design", runtimeStuff.modelNode!.creation));
+          graphState.exec(nodeCreation);
+          graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "type", "RuntimeModel"));
+          graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "design", runtimeStuff.modelNode!.creation));
           // overwrite 'initial' pointer with its current value:
-          runtimeStuff.modelNode!.getDeltasForSetEdge(primitiveRegistry, "initial", runtimeStuff.initial!.creation).forEach(d => model.graphState.exec(d));
-          model.graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "current", runtimeStuff.initial!.creation));
+          runtimeStuff.modelNode!.getDeltasForSetEdge(primitiveRegistry, "initial", runtimeStuff.initial!.creation).forEach(d => graphState.exec(d));
+          graphState.exec(primitiveRegistry.newEdgeCreation(nodeCreation, "current", runtimeStuff.initial!.creation));
           return {compositeLabel: "initialize"};
         });
       }
@@ -387,7 +383,7 @@ export function getDemoLive() {
         <SimpleGrid cols={2}>
           <Stack>
             <Group grow>
-              {modelComponents.undoRedoButtons}
+              {components.undoRedoButtons}
             </Group>
             <Divider label="(Projectional) Model Editor" labelPosition="centered" />
             {
@@ -401,7 +397,7 @@ export function getDemoLive() {
             <Paper shadow="xs" p="xs" withBorder>
             <Group grow>
               <TextInput value={addStateName} onChange={e => setAddStateName(e.currentTarget.value)} label="State Name" withAsterisk/>
-              <Button onClick={addState} disabled={addStateName===""||runtimeStuff.modelNode===null||runtimeStuff.states.some(([name]) => name ===addStateName)} leftIcon={<Icons.IconPlus/>} color="green">State</Button>
+              <Button onClick={onAddState} disabled={addStateName===""||runtimeStuff.modelNode===null||runtimeStuff.states.some(([name]) => name ===addStateName)} leftIcon={<Icons.IconPlus/>} color="green">State</Button>
             </Group>
             </Paper>
               <Select disabled={runtimeStuff.modelNode===null} searchable clearable label="Initial State" data={runtimeStuff.states.map(([stateName]) => ({value:stateName, label:stateName}))} value={runtimeStuff.initialStateName} onChange={onInitialStateChange}/>
@@ -418,7 +414,7 @@ export function getDemoLive() {
               <Select searchable withAsterisk clearable label="Source" data={runtimeStuff.states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionSrc} onChange={setAddTransitionSrc}/>
               <Select searchable withAsterisk clearable label="Target" data={runtimeStuff.states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionTgt} onChange={setAddTransitionTgt}/>
               <TextInput value={addTransitionEvent} onChange={e => setAddTransitionEvent(e.currentTarget.value)}  label="Event" />
-              <Button disabled={addTransitionSrc === null || addTransitionTgt === null} onClick={addTransition} leftIcon={<Icons.IconPlus/>} color="green">Transition</Button>
+              <Button disabled={addTransitionSrc === null || addTransitionTgt === null} onClick={onAddTransition} leftIcon={<Icons.IconPlus/>} color="green">Transition</Button>
             </Group>
             </Paper>
             <div style={{minHeight: 26}}>
@@ -466,10 +462,10 @@ export function getDemoLive() {
             </InfoHoverCardOverlay>
           </Stack>
           <Stack>
-            {modelComponents.makeTabs("state", ["state", "merge", "historyGraphviz", "deltaL1", "deltaL0"])}
-            {modelComponents.makeTabs("deltaL1", ["state", "merge", "historyGraphviz", "deltaL1", "deltaL1Graphviz", "deltaL0", "deltaL0Graphviz"])}
-            Run-time model version: {modelState.version.hash.toString('hex').substring(0,8)}<br/>
-            DesignModel version: {rt2d.get(modelState.version)!.hash.toString('hex').substring(0,8)}
+            {components.makeTabs("state", ["state", "merge", "historyGraphviz", "deltaL1", "deltaL0"])}
+            {components.makeTabs("deltaL1", ["state", "merge", "historyGraphviz", "deltaL1", "deltaL1Graphviz", "deltaL0", "deltaL0Graphviz"])}
+            Run-time model version: {state.version.hash.toString('hex').substring(0,8)}<br/>
+            DesignModel version: {rt2d.get(state.version)!.hash.toString('hex').substring(0,8)}
           </Stack>
         </SimpleGrid>
       </OnionContext.Provider>

+ 28 - 1
src/frontend/versioned_model/merge_view.tsx

@@ -4,9 +4,12 @@ import * as Icons from "@tabler/icons";
 
 import {Version} from "onion/version";
 import {Delta} from "onion/delta";
+import {CompositeDelta} from "onion/composite_delta";
+import {DeltaParser} from "onion/delta_parser";
 import {GraphView} from "./graph_view";
 import {fullVersionId, HistoryGraphState, historyGraphReducer} from "../d3graph/reducers/history_graph";
 import {InfoHoverCardOverlay} from "../info_hover_card";
+import {OnionContext, OnionContextType} from "../onion_context";
 
 const inputColor = 'seashell';
 const outputColor = 'lightblue';
@@ -27,7 +30,7 @@ export const historyGraphHelpText = <>
   </Mantine.Text>
 </>;
 
-export function MergeView({history, setHistory, forces, versionRegistry, onMerge, onGoto}) {
+export function MergeView({history, setHistory, forces, versionRegistry, onMerge, onGoto, primitiveRegistry, compositeLevel, addDeltasAndVersion}) {
   const [inputs, setInputs] = React.useState<Version[]>([]);
   const [outputs, setOutputs] = React.useState<Version[]>([]);
 
@@ -104,6 +107,30 @@ export function MergeView({history, setHistory, forces, versionRegistry, onMerge
       <Mantine.Button compact leftIcon={<Icons.IconNavigation/>} disabled={inputs.length!==1} onClick={() => {
         onGoto(inputs[0]);
       }}>Goto</Mantine.Button>
+      <Mantine.Button compact leftIcon={<Icons.IconDatabaseImport/>} onClick={() => {
+        let parsed;
+        while (true) {
+          const toImport = prompt("Versions to import (JSON)", "[]");
+          if (toImport === null) {
+            return; // 'cancel'
+          }
+          try {
+            parsed = JSON.parse(toImport);
+            break;
+          } catch (e) {
+            alert("Invalid JSON");
+          }
+          // ask again ...
+        }
+
+        const parser = new DeltaParser(primitiveRegistry, compositeLevel);
+        let parentVersion = versionRegistry.initialVersion;
+        for (const d of parsed) {
+          const composite = parser.loadDelta(d) as CompositeDelta;
+          parentVersion = addDeltasAndVersion(composite.deltas, composite.getDescription(), parentVersion.hash)!;
+        }
+      }}
+      >Import</Mantine.Button>
       <Mantine.Tooltip label="Copied to clipboard!" opened={showTooltip !== null} withArrow>
         <Mantine.Button compact leftIcon={<Icons.IconDatabaseExport/>} disabled={inputs.length!==1} onClick={() => {
           const type = "application/json";

+ 3 - 1
src/frontend/versioned_model/single_model.tsx

@@ -246,7 +246,9 @@ export function newVersionedModel({readonly}, vReg?: VersionRegistry) {
       forces={defaultGraphForces}
       versionRegistry={versionRegistry}
       onMerge={outputs => callbacks.onMerge?.(outputs)}
-      onGoto={version => callbacks.onVersionClicked?.(version)} />
+      onGoto={version => callbacks.onVersionClicked?.(version)}
+      {...{primitiveRegistry: null, compositeLevel, addDeltasAndVersion:()=>{}}}
+      />
 
     const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
       <RountangleEditor

+ 386 - 0
src/frontend/versioned_model/single_model2.tsx

@@ -0,0 +1,386 @@
+import * as React from "react";
+import * as Mantine from "@mantine/core";
+import * as Icons from "@tabler/icons";
+
+import {D3OnionGraphData, D3GraphUpdater} from "../d3graph/reducers/onion_graph";
+import {D3GraphEditable, UserEditCallback} from "../d3graph/d3graph_editable";
+
+import {
+  DeltaGraphState,
+  fullDeltaId,
+  deltaGraphReducer,
+} from "../d3graph/reducers/delta_graph";
+
+import {
+  HistoryGraphState,
+  initialHistoryGraph,
+  fullVersionId,
+  historyGraphReducer,
+} from "../d3graph/reducers/history_graph";
+
+import * as helpText from "./help_text";
+import {MergeView} from "./merge_view";
+import {GraphView} from "./graph_view";
+
+import {D3Graph, emptyGraph, defaultGraphForces} from "../d3graph/d3graph";
+import {RountangleEditor} from "../rountangleEditor/RountangleEditor";
+import {InfoHoverCardOverlay} from "../info_hover_card";
+import {OnionContext, OnionContextType} from "../onion_context";
+
+import {Version, VersionRegistry} from "onion/version";
+import {PrimitiveDelta, PrimitiveRegistry} from "onion/primitive_delta";
+import {PrimitiveValue, UUID} from "onion/types";
+import {CompositeDelta, CompositeLevel} from "onion/composite_delta";
+import {GraphState} from "onion/graph_state"; 
+import {Delta} from "onion/delta";
+import {DeltaParser} from "onion/delta_parser";
+
+export const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
+
+export interface VersionedModelState {
+  version: Version; // the 'current version'
+  graph: D3OnionGraphData; // the state what is displayed in the leftmost panel
+  historyGraph: HistoryGraphState; // the state of what is displayed in the middle panel
+  deltaGraphL1: DeltaGraphState; // the state of what is displayed in the rightmost panel
+  deltaGraphL0: DeltaGraphState; // the state of what is displayed in the rightmost panel
+}
+
+interface VersionedModelCallbacks {
+  onUserEdit?: UserEditCallback;
+  onUndoClicked?: (parentVersion: Version, deltaToUndo: Delta) => void;
+  onRedoClicked?: (childVersion: Version, deltaToRedo: Delta) => void;
+  onVersionClicked?: (Version) => void;
+  onMerge?: (outputs: Version[]) => void;
+  onImport?: (json: Array<any>) => void;
+}
+
+// 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 newOnion({readonly, primitiveRegistry}) {
+  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,
+    historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
+    deltaGraphL1: emptyGraph,
+    deltaGraphL0: 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;
+  }
+
+  function useOnion(overridenCallbacks: (any) => VersionedModelCallbacks) {
+    const [state, setState] = React.useState<VersionedModelState>(initialState);
+
+    // Reducer
+
+    // Create and add a new version, and its deltas, without changing the current version
+    const addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => {
+      const composite = compositeLevel.createComposite(deltas, description);
+
+      const parentVersion = versionRegistry.lookupOptional(parentHash);
+      if (parentVersion !== undefined) {
+        const newVersion = versionRegistry.createVersion(parentVersion, composite);
+
+        setState(({historyGraph, deltaGraphL1, deltaGraphL0, ...rest}) => {
+          return {
+            // add new version to history graph:
+            historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version: newVersion}),
+            // add the composite delta to the L1-graph + highlight it as 'active':
+            deltaGraphL1: composite.deltas.length > 0 ? deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta: composite, active: false}) : deltaGraphL1, // never add an empty composite
+            // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
+            deltaGraphL0: composite.deltas.reduce(
+              (graph, delta) => deltaGraphReducer(graph, {type: 'addDelta', delta, active: false}),
+              deltaGraphL0),
+            ...rest,
+          };
+        });
+
+        return newVersion;
+      }
+    };
+    const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, parentVersion: Version = currentVersion): Version => {
+      const newVersion = addDeltasAndVersion(deltas, description, parentVersion.hash) as Version;
+      gotoVersion(newVersion);
+      return newVersion;
+    };
+    const appendVersions = (versions: Version[]) => {
+      setState(({historyGraph, ...rest}) => {
+        const ordered: Version[] = [];
+        const makeSureParentVersionsAreThere = version => {
+          for (const [parent] of version.parents) {
+            if (! (historyGraph.nodes.some(n => n.obj === parent) || ordered.includes(parent))) {
+              makeSureParentVersionsAreThere(parent);
+              ordered.push(parent);
+            }
+          }
+        }
+        for (const v of versions) {
+          makeSureParentVersionsAreThere(v);
+          ordered.push(v);
+        }
+        return {
+          historyGraph: ordered.reduce((historyGraph, version) => historyGraphReducer(historyGraph, {type: 'addVersion', version}), historyGraph),
+          ...rest,
+        };
+      });
+    }
+
+    // helper
+    const setGraph = callback =>
+      setState(({graph, ...rest}) => ({graph: callback(graph), ...rest}));
+
+    const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
+      const d3Updater = new D3GraphUpdater(setGraph, x, y);
+      graphState.unexec(deltaToUndo, d3Updater);
+      setState(({deltaGraphL0: prevDeltaGraphL0, deltaGraphL1: prevDeltaGraphL1, ...rest}) => ({
+        deltaGraphL1: deltaGraphReducer(prevDeltaGraphL1, {type: 'setDeltaInactive', delta: deltaToUndo}),
+        deltaGraphL0: deltaToUndo.deltas.reduce((prevDeltaGraphL0, delta) => deltaGraphReducer(prevDeltaGraphL0, {type: 'setDeltaInactive', delta}),prevDeltaGraphL0),
+        ...rest,
+      }));
+    };
+    const redoWithoutUpdatingHistoryGraph = (deltaToRedo) => {
+      const d3Updater = new D3GraphUpdater(setGraph, x, y);
+      graphState.exec(deltaToRedo, d3Updater);
+      setState(({deltaGraphL0: prevDeltaGraphL0, deltaGraphL1: prevDeltaGraphL1, ...rest}) => ({
+        deltaGraphL1: deltaGraphReducer(prevDeltaGraphL1, {type: 'setDeltaActive', delta: deltaToRedo}),
+        deltaGraphL0: deltaToRedo.deltas.reduce((prevDeltaGraphL0, delta) => deltaGraphReducer(prevDeltaGraphL0, {type: 'setDeltaActive', delta}),
+          prevDeltaGraphL0),
+        ...rest,
+      }));
+    };
+    const undo = (parentVersion, deltaToUndo) => {
+      undoWithoutUpdatingHistoryGraph(deltaToUndo);
+      currentVersion = parentVersion;
+      setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+        version: parentVersion,
+        historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph,
+            {type: 'highlightVersion', version: prevVersion, bold: false}),
+            {type: 'highlightVersion', version: parentVersion, bold: true}),
+        ...rest,
+      }));
+    };
+    const redo = (childVersion, deltaToRedo) => {
+      redoWithoutUpdatingHistoryGraph(deltaToRedo);
+      currentVersion = childVersion;
+      setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
+        version: childVersion,
+        historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph,
+          {type: 'highlightVersion', version: prevVersion, bold: false}),
+          {type: 'highlightVersion', version: childVersion, bold: true}),
+        ...rest,
+      }));
+    };
+    const gotoVersion = (chosenVersion: Version) => {
+      const path = currentVersion.findPathTo(chosenVersion);
+      if (path === undefined) {
+        throw new Error("Could not find path to version!");
+      }
+      for (const [linkType, delta] of path) {
+        if (linkType === 'p') {
+          undoWithoutUpdatingHistoryGraph(delta);
+        }
+        else if (linkType === 'c') {
+          redoWithoutUpdatingHistoryGraph(delta);
+        }
+      }
+      currentVersion = chosenVersion;
+      setState(({historyGraph, version: oldVersion, ...rest}) => ({
+        version: chosenVersion,
+        historyGraph: historyGraphReducer(historyGraphReducer(historyGraph,
+          {type: 'highlightVersion', version: oldVersion, bold: false}),
+          {type: 'highlightVersion', version: chosenVersion, bold: true}),
+        ...rest,
+      }));
+    };
+
+    const reducer = {
+      addDeltasAndVersion,
+      gotoVersion,
+      createAndGotoNewVersion,
+      appendVersions,
+      undo,
+      redo,
+    };
+
+    // Components
+
+    const defaultCallbacks = {
+      onUserEdit: createAndGotoNewVersion,
+      onUndoClicked: undo,
+      onRedoClicked: redo,
+      onVersionClicked: gotoVersion,
+      onMerge: appendVersions,
+    };
+
+    const callbacks = Object.assign({}, defaultCallbacks, overridenCallbacks(reducer));
+
+    const graphStateComponent = readonly ? 
+          <GraphView graphData={state.graph} help={helpText.graphEditorReadonly} mouseUpHandler={()=>{}} />
+        : <InfoHoverCardOverlay contents={helpText.graphEditor}>
+            <D3GraphEditable
+              graph={state.graph}
+              graphState={graphState}
+              forces={defaultGraphForces}
+              setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
+              onUserEdit={callbacks.onUserEdit}
+            />
+          </InfoHoverCardOverlay>;
+
+    // Serialize delta
+    const onDeltaClick = (e, {x,y}, node) => {
+      if (node) {
+        alert(JSON.stringify(node.obj.serialize(), null, 2));
+      }
+    }
+    const deltaGraphL0Component = <GraphView graphData={state.deltaGraphL0} help={helpText.deltaGraph} mouseUpHandler={onDeltaClick} />;
+    const deltaGraphL1Component = <GraphView graphData={state.deltaGraphL1} help={helpText.deltaGraph} mouseUpHandler={onDeltaClick} />;
+    const historyComponent = <GraphView graphData={state.historyGraph} help={helpText.historyGraph}
+      mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />;
+
+    const historyComponentWithMerge = <MergeView
+      history={state.historyGraph}
+      setHistory={callback => setState(({historyGraph, ...rest})=>({historyGraph: callback(historyGraph), ...rest}))}
+      forces={defaultGraphForces}
+      versionRegistry={versionRegistry}
+      onMerge={outputs => callbacks.onMerge?.(outputs)}
+      onGoto={version => callbacks.onVersionClicked?.(version)}
+      {...{primitiveRegistry, compositeLevel, addDeltasAndVersion}}
+      />
+
+    const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
+      <RountangleEditor
+        graph={state.graph}
+        graphState={graphState}
+        onUserEdit={callbacks.onUserEdit}
+      />,
+    </InfoHoverCardOverlay>;
+
+    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}
+      <Mantine.Space w="sm"/>
+      {redoButton}
+    </>;
+
+    const stackedUndoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => {
+      return (
+        <div key={fullVersionId(parentVersion)}>
+          <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconChevronLeft 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.IconChevronRight 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 makeTabs = (defaultTab: string, tabs: string[]) => {
+      return <Mantine.Tabs defaultValue={defaultTab} keepMounted={false}>
+        <Mantine.Tabs.List grow>
+          {tabs.map(tab => ({
+            editor: <Mantine.Tabs.Tab key={tab} value={tab}>Editor</Mantine.Tabs.Tab>,
+            state:  <Mantine.Tabs.Tab key={tab} value={tab}>State</Mantine.Tabs.Tab>,
+            history: <Mantine.Tabs.Tab key={tab} value={tab}>History</Mantine.Tabs.Tab>,
+            merge: <Mantine.Tabs.Tab key={tab} value={tab}>History</Mantine.Tabs.Tab>,
+            deltaL1: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L1)</Mantine.Tabs.Tab>,
+            deltaL0: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L0)</Mantine.Tabs.Tab>,
+          }[tab]))}
+        </Mantine.Tabs.List>
+        <Mantine.Tabs.Panel value="state">
+          {graphStateComponent}
+        </Mantine.Tabs.Panel>
+        <Mantine.Tabs.Panel value="editor">
+          {rountangleEditor}
+        </Mantine.Tabs.Panel>
+        <Mantine.Tabs.Panel value="deltaL1">
+          {deltaGraphL1Component}
+        </Mantine.Tabs.Panel>
+        <Mantine.Tabs.Panel value="deltaL0">
+          {deltaGraphL0Component}
+        </Mantine.Tabs.Panel>
+        <Mantine.Tabs.Panel value="history">
+          {historyComponent}
+        </Mantine.Tabs.Panel>
+        <Mantine.Tabs.Panel value="merge">
+          {historyComponentWithMerge}
+        </Mantine.Tabs.Panel>
+      </Mantine.Tabs>;
+    }
+
+
+    return {
+      state,
+      reducer,
+      components: {
+        graphStateComponent,
+        rountangleEditor,
+        deltaGraphL1Component,
+        deltaGraphL0Component,
+        historyComponent,
+        historyComponentWithMerge,
+        undoButton,
+        redoButton,
+        undoRedoButtons,
+        stackedUndoRedoButtons,
+        makeTabs,
+      },
+    };
+  }
+
+  return {
+    initialState,
+    graphState,
+    versionRegistry,
+    getCurrentVersion,
+    useOnion,
+  }
+}