Browse Source

Implemented manual rendering in case of missing geometry

Joeri Exelmans 2 years ago
parent
commit
6c73080a58
4 changed files with 145 additions and 66 deletions
  1. 92 44
      src/frontend/correspondence.tsx
  2. 12 5
      src/frontend/demo_bm.tsx
  3. 12 8
      src/frontend/demo_corr.tsx
  4. 29 9
      src/parser/trivial_parser.ts

+ 92 - 44
src/frontend/correspondence.tsx

@@ -3,17 +3,26 @@ import * as Mantine from "@mantine/core";
 import * as Icons from "@tabler/icons";
 
 import {newVersionedModel, VersionedModelState} from "./versioned_model";
+import {emptyGraph, graphForces} from "./constants";
+import {ManualRendererProps} from "./manual_renderer";
+import {D3GraphStateUpdater} from "./d3_state";
 import {TrivialParser} from "../parser/trivial_parser";
 import {Version} from "../onion/version";
 import {GraphState} from "../onion/graph_state"; 
 import {CompositeDelta} from "../onion/composite_delta";
+import {PrimitiveDelta} from "../onion/primitive_delta";
 
 // Pure function
 // Replays all deltas in a version to compute the graph state of that version.
-function getGraphState(version: Version): GraphState {
+function getGraphState(version: Version, listener?): GraphState {
   const graphState = new GraphState();
   for (const d of [...version].reverse()) {
-    graphState.exec(d);
+    if (listener !== undefined) {
+      graphState.exec(d, listener);
+    }
+    else {
+      graphState.exec(d);
+    }
   }
   return graphState;
 }
@@ -42,40 +51,40 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
       redo,
     } = getReducerOrig(setCorrState);
 
-    const parse = (csDeltas, description: string) => {
-      const csParentVersion = cs.getCurrentVersion();
-      const corrParentVersion = parseMap.get(csParentVersion)!;
-      const asParentVersion = corrMap.get(corrParentVersion)!.asVersion;
+    // const parse = (csDeltas, description: string) => {
+    //   const csParentVersion = cs.getCurrentVersion();
+    //   const corrParentVersion = parseMap.get(csParentVersion)!;
+    //   const asParentVersion = corrMap.get(corrParentVersion)!.asVersion;
 
-      const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(getGraphState);
+    //   const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(getGraphState);
 
-      const {corrDeltas, asDeltas} = parser.parse(csDeltas, csGS, corrGS, asGS);
+    //   const {corrDeltas, asDeltas} = parser.parse(csDeltas, csGS, corrGS, asGS); // may throw MissingInformation
 
-      const csVersion = csReducer.createAndGotoNewVersion(csDeltas, description);
-      const corrVersion = createAndGotoNewVersion(corrDeltas, "cs:"+description, corrParentVersion);
-      const asVersion = asDeltas.length > 0 ? asReducer.createAndGotoNewVersion(asDeltas, "parse:"+description, asParentVersion) : asParentVersion;
+    //   const csVersion = csReducer.createAndGotoNewVersion(csDeltas, description);
+    //   const corrVersion = createAndGotoNewVersion(corrDeltas, "cs:"+description, corrParentVersion);
+    //   const asVersion = asDeltas.length > 0 ? asReducer.createAndGotoNewVersion(asDeltas, "parse:"+description, asParentVersion) : asParentVersion;
 
-      corrMap.set(corrVersion, {csVersion, asVersion});
-      parseMap.set(csVersion, corrVersion);
-      renderMap.set(asVersion, corrVersion);
-    };
-    const render = (asDeltas, description: string) => {
-      const asParentVersion = as.getCurrentVersion();
-      const corrParentVersion = renderMap.get(asParentVersion)!;
-      const csParentVersion = corrMap.get(corrParentVersion)!.csVersion;
+    //   corrMap.set(corrVersion, {csVersion, asVersion});
+    //   parseMap.set(csVersion, corrVersion);
+    //   renderMap.set(asVersion, corrVersion);
+    // };
+    // const render = (asDeltas, description: string) => {
+    //   const asParentVersion = as.getCurrentVersion();
+    //   const corrParentVersion = renderMap.get(asParentVersion)!;
+    //   const csParentVersion = corrMap.get(corrParentVersion)!.csVersion;
 
-      const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(getGraphState);
+    //   const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(getGraphState);
       
-      const {corrDeltas, csDeltas} = parser.render(asDeltas, csGS, corrGS, asGS);
+    //   const {corrDeltas, csDeltas} = parser.render(asDeltas, csGS, corrGS, asGS);
 
-      const csVersion = csDeltas.length > 0 ? csReducer.createAndGotoNewVersion(csDeltas, "render:"+description) : csParentVersion;
-      const corrVersion = createAndGotoNewVersion(corrDeltas, "as:"+description);
-      const asVersion = asReducer.createAndGotoNewVersion(asDeltas, description);
+    //   const csVersion = csDeltas.length > 0 ? csReducer.createAndGotoNewVersion(csDeltas, "render:"+description) : csParentVersion;
+    //   const corrVersion = createAndGotoNewVersion(corrDeltas, "as:"+description);
+    //   const asVersion = asReducer.createAndGotoNewVersion(asDeltas, description);
 
-      corrMap.set(corrVersion, {csVersion, asVersion});
-      parseMap.set(csVersion, corrVersion);
-      renderMap.set(asVersion, corrVersion);
-    };
+    //   corrMap.set(corrVersion, {csVersion, asVersion});
+    //   parseMap.set(csVersion, corrVersion);
+    //   renderMap.set(asVersion, corrVersion);
+    // };
     const parseExistingVersion = (csVersion: Version) => {
       for (const [csParentVersion, compositeDelta] of csVersion.parents) {
 
@@ -97,7 +106,7 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
           continue;
         }
 
-        const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(getGraphState);
+        const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(v => getGraphState(v));
 
         const {corrDeltas, asDeltas} = parser.parse(csDeltas, csGS, corrGS, asGS);
         const corrVersion = createAndGotoNewVersion(corrDeltas, "cs:"+description, corrParentVersion);
@@ -108,14 +117,14 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
         renderMap.set(asVersion, corrVersion);
       }
     };
-    const renderExistingVersion = (asVersion: Version) => {
+    const renderExistingVersion = (asVersion: Version, manualRenderCallback) => {
       for (const [asParentVersion, compositeDelta] of asVersion.parents) {
 
         const asDeltas = (compositeDelta as CompositeDelta).deltas;
         const description = compositeDelta.getDescription();
 
         if (!renderMap.has(asParentVersion)) {
-          renderExistingVersion(asParentVersion);
+          renderExistingVersion(asParentVersion, manualRenderCallback);
         }
 
         const corrParentVersion = renderMap.get(asParentVersion);
@@ -129,15 +138,44 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
           continue;
         }
 
-        const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(getGraphState);
+        const [csGS, corrGS, asGS] = [csParentVersion, corrParentVersion, asParentVersion].map(v => getGraphState(v));
 
-        const {corrDeltas, csDeltas} = parser.render(asDeltas, csGS, corrGS, asGS);
-        const corrVersion = createAndGotoNewVersion(corrDeltas, "as:"+description, corrParentVersion);
-        const csVersion = csDeltas.length > 0 ? csReducer.createAndGotoNewVersion(csDeltas, "parse:"+description, csParentVersion) : asParentVersion;
+        const {corrDeltas, csDeltas, complete} = parser.render(asDeltas, csGS, corrGS, asGS);
 
-        corrMap.set(corrVersion, {csVersion, asVersion});
-        parseMap.set(csVersion, corrVersion);
-        renderMap.set(asVersion, corrVersion);
+        function finishRender({corrDeltas, csDeltas}) {
+          const corrVersion = createAndGotoNewVersion(corrDeltas, "as:"+description, corrParentVersion);
+          const csVersion = csDeltas.length > 0 ? csReducer.createAndGotoNewVersion(csDeltas, "parse:"+description, csParentVersion) : csParentVersion;
+
+          corrMap.set(corrVersion, {csVersion, asVersion});
+          parseMap.set(csVersion, corrVersion);
+          renderMap.set(asVersion, corrVersion);          
+        }
+
+
+        if (complete) {
+          finishRender({corrDeltas, csDeltas});
+        }
+        else {
+          function getD3State(version: Version, additionalDeltas: PrimitiveDelta[] = []) {
+            let graph = emptyGraph;
+            const setGraph = callback => (graph = callback(graph));
+            const d3Updater = new D3GraphStateUpdater(setGraph, 0, 0);
+            const graphState = getGraphState(version, d3Updater);
+            for (const d of additionalDeltas) {
+              graphState.exec(d, d3Updater);
+            }
+            return {graph, graphState};
+          }
+
+          const {graph: asGraph} = getD3State(asVersion);
+          const {graph: csParentGraph, graphState: csParentGraphState} = getD3State(csParentVersion, csDeltas);
+
+          manualRenderCallback({asGraph, csParentGraph, csParentGraphState, asDeltasToRender: asDeltas})
+          .then(additionalCsDeltas => {
+            finishRender({corrDeltas, csDeltas: csDeltas.concat(additionalCsDeltas)});
+          })
+          .catch();
+        }
       }
     };
     const gotoVersion = (corrVersion: Version) => {
@@ -146,6 +184,18 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
       gotoVersionOrig(corrVersion);
       asReducer.gotoVersion(asVersion);
     };
+    const getManualRenderCallback = (setManualRendererState) => {
+      return props => {
+        return new Promise((resolve, reject) => {
+          setManualRendererState({
+            ...props,
+            resolve,
+            reject,
+          });
+        })
+        .finally(() => setManualRendererState(null));
+      }
+    }
     const getParseButton = (csVersion, dir: "left"|"right" = "right") => {
       return <Mantine.Button compact
           disabled={parseMap.has(csVersion)}
@@ -154,10 +204,10 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
           leftIcon={dir === "left" ? <Icons.IconChevronsLeft/>: null}
         >Parse</Mantine.Button>;
     };
-    const getRenderButton = (asVersion, dir: "left"|"right" = "left") => {
+    const getRenderButton = (asVersion, setManualRendererState, dir: "left"|"right" = "left") => {
       return <Mantine.Button compact
           disabled={renderMap.has(asVersion)}
-          onClick={() => renderExistingVersion(asVersion)}
+          onClick={() => renderExistingVersion(asVersion, getManualRenderCallback(setManualRendererState))}
           rightIcon={dir === "right" ? <Icons.IconChevronsRight/>: null}
           leftIcon={dir === "left" ? <Icons.IconChevronsLeft/>: null}
         >Render</Mantine.Button>;
@@ -176,13 +226,13 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
         return <Mantine.Group>{sw}{button}</Mantine.Group>;
       }
     };
-    const getCaptionWithRenderButton = (autoRenderState, setAutoRenderState, asVersion, dir: "left"|"right" = "left") => {
+    const getCaptionWithRenderButton = (autoRenderState, setAutoRenderState, asVersion, setManualRendererState, dir: "left"|"right" = "left") => {
       const sw = <Mantine.Switch label="Auto"
         labelPosition={dir}
         checked={autoRenderState}
         onChange={(event) => setAutoRenderState(event.currentTarget.checked)}
       />
-      const button = getRenderButton(asVersion, dir);
+      const button = getRenderButton(asVersion, setManualRendererState, dir);
       if (dir === "left") {
         return <Mantine.Group>{button}{sw}</Mantine.Group>;
       }
@@ -192,8 +242,6 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
     };
 
     return {
-      parse,
-      render,
       parseExistingVersion,
       renderExistingVersion,
       gotoVersion,

+ 12 - 5
src/frontend/demo_bm.tsx

@@ -1,11 +1,12 @@
 import * as React from "react";
-import {SimpleGrid, Text, Title, Group, Stack, Button, Space, Textarea, Tabs, HoverCard, ActionIcon, Center, Switch, Grid} from "@mantine/core";
+import {SimpleGrid, Text, Title, Group, Stack, Button, Space, Textarea, Tabs, HoverCard, ActionIcon, Center, Switch, Grid, Modal} from "@mantine/core";
 
 import {PrimitiveRegistry} from "../onion/primitive_delta";
 import {mockUuid} from "../onion/test_helpers";
 
 import {newVersionedModel, VersionedModelState} from "./versioned_model";
 import {newCorrespondence} from "./correspondence";
+import {newManualRenderer, ManualRendererProps} from "./manual_renderer";
 
 export function getDemoBM() {
   const generateUUID = mockUuid();
@@ -19,6 +20,8 @@ export function getDemoBM() {
   const corr1 = newCorrespondence({cs: cs1, as, ...commonStuff});
   const corr2 = newCorrespondence({cs: cs2, as, ...commonStuff});
 
+  const {getModalManualRenderer} = newManualRenderer(commonStuff);
+
   // returns functional react component
   return function() {
     const [asState, setAsState] = React.useState<VersionedModelState>(as.initialState);
@@ -27,6 +30,8 @@ export function getDemoBM() {
     const [corr1State, setCorr1State] = React.useState<VersionedModelState>(corr1.initialState);
     const [corr2State, setCorr2State] = React.useState<VersionedModelState>(corr2.initialState);
 
+    const [manualRendererState, setManualRendererState] = React.useState<null | ManualRendererProps>(null);
+
     const [autoParse1, setAutoParse1] = React.useState<boolean>(true);
     const [autoParse2, setAutoParse2] = React.useState<boolean>(true);
     const [autoRender1, setAutoRender1] = React.useState<boolean>(false);
@@ -42,10 +47,10 @@ export function getDemoBM() {
       onUserEdit: (deltas, description) => {
         const newVersion = asReducer.createAndGotoNewVersion(deltas, description);
         if (autoRender1) {
-          corr1Reducer.renderExistingVersion(newVersion);
+          corr1Reducer.renderExistingVersion(newVersion, setManualRendererState);
         }
         if (autoRender2) {
-          corr2Reducer.renderExistingVersion(newVersion);
+          corr2Reducer.renderExistingVersion(newVersion, setManualRendererState);
         }
       },
       onUndoClicked: asReducer.undo,
@@ -96,6 +101,8 @@ export function getDemoBM() {
     }
 
     return (<div style={{minWidth:2200}}>
+      {getModalManualRenderer(manualRendererState)}
+
       <Grid columns={5} grow>
         <Grid.Col span={1}>
           {centeredCaption("Concrete Syntax 1")}
@@ -122,8 +129,8 @@ export function getDemoBM() {
         </Grid.Col>
         <Grid.Col span={1}>
           <Group position="apart">
-            {corr1Reducer.getCaptionWithRenderButton(autoRender1, setAutoRender1, as.getCurrentVersion())}
-            {corr2Reducer.getCaptionWithRenderButton(autoRender2, setAutoRender2, as.getCurrentVersion(), "right")}
+            {corr1Reducer.getCaptionWithRenderButton(autoRender1, setAutoRender1, as.getCurrentVersion(), setManualRendererState)}
+            {corr2Reducer.getCaptionWithRenderButton(autoRender2, setAutoRender2, as.getCurrentVersion(), setManualRendererState, "right")}
           </Group>
         </Grid.Col>
         <Grid.Col span={1}>

+ 12 - 8
src/frontend/demo_corr.tsx

@@ -6,6 +6,7 @@ import {mockUuid} from "../onion/test_helpers";
 
 import {newVersionedModel, VersionedModelState} from "./versioned_model";
 import {newCorrespondence} from "./correspondence";
+import {newManualRenderer, ManualRendererProps} from "./manual_renderer";
 import {makeInfoHoverCardIcon} from "./help_icons";
 
 export function getDemoCorr() {
@@ -18,6 +19,8 @@ export function getDemoCorr() {
   const cs = newVersionedModel({readonly: false, ...commonStuff});
   const corr = newCorrespondence({cs, as, ...commonStuff});
 
+  const {getModalManualRenderer} = newManualRenderer(commonStuff);
+
   // returns functional react component
   return function() {
     const [asState, setAsState] = React.useState<VersionedModelState>(as.initialState);
@@ -27,16 +30,17 @@ export function getDemoCorr() {
     const [autoParse, setAutoParse] = React.useState<boolean>(true);
     const [autoRender, setAutoRender] = React.useState<boolean>(false);
 
+    const [manualRendererState, setManualRendererState] = React.useState<null | ManualRendererProps>(null);
+
     const asReducer = as.getReducer(setAsState);
     const csReducer = cs.getReducer(setCsState);
     const corrReducer = corr.getReducer(setCorrState, csReducer, asReducer);
 
     const csComponents = cs.getReactComponents(csState, {
       onUserEdit: (deltas, description) => {
+        const newVersion = csReducer.createAndGotoNewVersion(deltas, description);
         if (autoParse) {
-          corrReducer.parse(deltas, description);
-        } else {
-          csReducer.createAndGotoNewVersion(deltas, description);
+          corrReducer.parseExistingVersion(newVersion);
         }
       },
       onUndoClicked: csReducer.undo,
@@ -50,11 +54,9 @@ export function getDemoCorr() {
     });
     const asComponents = as.getReactComponents(asState, {
       onUserEdit: (deltas, description) => {
+        const newVersion = asReducer.createAndGotoNewVersion(deltas, description);
         if (autoRender) {
-          corrReducer.render(deltas, description);
-        }
-        else {
-          asReducer.createAndGotoNewVersion(deltas, description);
+          corrReducer.renderExistingVersion(newVersion, setManualRendererState);
         }
       },
       onUndoClicked: asReducer.undo,
@@ -69,6 +71,8 @@ export function getDemoCorr() {
     const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
 
     return (<div style={{minWidth:1300}}>
+      {getModalManualRenderer(manualRendererState)}
+
       <SimpleGrid cols={3}>
         <div>
           <Group position="apart">
@@ -109,7 +113,7 @@ export function getDemoCorr() {
         <div>
           <Group position="apart">
             {corrReducer.getCaptionWithRenderButton(
-              autoRender, setAutoRender, as.getCurrentVersion())}
+              autoRender, setAutoRender, as.getCurrentVersion(), setManualRendererState)}
             <Title order={4}>Abstract Syntax</Title>
           </Group>
           <Space h="sm" />

+ 29 - 9
src/parser/trivial_parser.ts

@@ -23,8 +23,7 @@ import {
 
 import {getDeltasForDelete} from "../onion/delete_node";
 
-export class ParseError extends Error {
-}
+export class ParseError extends Error {}
 
 export interface Geometry2DRect {
   x: number;
@@ -39,7 +38,6 @@ const geometryLabels = ["x", "y", "width", "height"];
 const isInside = (a: Geometry2DRect, b: Geometry2DRect): boolean => 
     a.x > b.x && a.y > b.y && a.x+a.w < b.x+b.w && a.y+a.h < b.y+b.h;
 
-
 // A parser that creates an AS-node for every CS-node, with a Corr-node in between.
 export class TrivialParser  {
   readonly getUuid: () => UUID;
@@ -78,15 +76,14 @@ export class TrivialParser  {
       }
     }
 
+    let complete = true; // is parsing/rendering complete, or do we need manual adjustment? (for missing geometry information)
+
     try {
       for (const sourceDelta of sourceDeltas) {
         // We'll update the sourceState, in-place, for every delta that we are parsing/rendering
         applyToState(sourceState, sourceDelta);
 
         if (sourceDelta instanceof NodeCreation) {
-          if (!parse) {
-            throw new ParseError("Cannot render node creation (missing geometry information)");
-          }
 
           // Creations are easy, and never cause conflicts:
           const sourceCreation = sourceDelta; // alias for readability :)
@@ -102,6 +99,23 @@ export class TrivialParser  {
 
           corrDeltas.push(corrCreation, corr2Source, corr2Target);
           targetDeltas.push(targetCreation);
+
+          if (!parse) {
+            // generate a Rountangle with some default geometry:
+            const edges: [string,PrimitiveValue][] = [
+              ["type", "Rountangle"],
+              ["label", "New Rountangle"],
+              ["x", 0],
+              ["y", 0],
+              ["z-index", 0],
+              ["width", 100],
+              ["height", 60],
+            ];
+            targetDeltas.push(...edges.map(([edgeLabel, value]) =>
+              this.primitiveRegistry.newEdgeCreation(targetCreation, edgeLabel, value)));
+
+            complete = false; // actual geometry of new rountangle to be determined by user
+          }
         }
         else if (sourceDelta instanceof NodeDeletion) {
           const sourceDeletion = sourceDelta; // alias for readability :)
@@ -159,7 +173,10 @@ export class TrivialParser  {
                 }
               }
             }
-            throw new ParseError("Cannot render edge creation / edge update (missing geometry information");
+
+            // parent edge updated: need to update geometry: (manually)
+            complete = false;
+            continue;
           }
 
           const edgeCreation = (sourceDelta as (EdgeCreation | EdgeUpdate)).getCreation();
@@ -330,6 +347,7 @@ export class TrivialParser  {
       targetDeltas,
       sourceOverrides,
       targetOverrides,
+      complete,
     };
     // console.log(result);
     return result;
@@ -341,9 +359,10 @@ export class TrivialParser  {
       targetDeltas,
       sourceOverrides: csOverrides,
       targetOverrides: asOverrides,
+      complete,
     } = this.propagate_change(true, csDeltas, csState, corrState, asState);
 
-    return { corrDeltas, asDeltas: targetDeltas, csOverrides, asOverrides };
+    return { corrDeltas, asDeltas: targetDeltas, csOverrides, asOverrides, complete };
   }
 
   render(asDeltas: PrimitiveDelta[], csState: GraphState, corrState: GraphState, asState: GraphState) {
@@ -352,8 +371,9 @@ export class TrivialParser  {
       targetDeltas,
       sourceOverrides: asOverrides,
       targetOverrides: csOverrides,
+      complete,
     } = this.propagate_change(false, asDeltas, asState, corrState, csState);
 
-    return { corrDeltas, csDeltas: targetDeltas, csOverrides, asOverrides };
+    return { corrDeltas, csDeltas: targetDeltas, csOverrides, asOverrides, complete };
   }
 }