فهرست منبع

Merge branch 'manual-render'

Joeri Exelmans 2 سال پیش
والد
کامیت
1c9e51e0bf

+ 126 - 52
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,36 +117,103 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
         renderMap.set(asVersion, corrVersion);
       }
     };
-    const renderExistingVersion = (asVersion: Version) => {
-      for (const [asParentVersion, compositeDelta] of asVersion.parents) {
-
-        const asDeltas = (compositeDelta as CompositeDelta).deltas;
-        const description = compositeDelta.getDescription();
+    const renderExistingVersion = async (asVersion: Version, setManualRendererState) => {
+      const [asParentVersion, compositeDelta] = asVersion.parents[0];
 
-        if (!renderMap.has(asParentVersion)) {
-          renderExistingVersion(asParentVersion);
-        }
+      const asDeltas = (compositeDelta as CompositeDelta).deltas;
+      const description = compositeDelta.getDescription();
 
+      const render = () => {
         const corrParentVersion = renderMap.get(asParentVersion);
         if (corrParentVersion === undefined) {
           console.log("Cannot parse - no parent CORR version");
-          continue;
+          return;
         }
         const csParentVersion = corrMap.get(corrParentVersion)?.csVersion;
         if (csParentVersion === undefined) {
           console.log("Cannot parse - no parent CS version");
-          continue;
+          return;
         }
 
-        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});
+          return Promise.resolve();
+        }
+        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, graphState: asGraphState} = getD3State(asVersion);
+          const {graph: csGraph, graphState: csGraphState} = getD3State(csParentVersion, csDeltas);
+
+          const csToAs = new Map();
+          const asToCs = new Map();
+
+          for (const corrNode of corrGS.nodes.values()) {
+            const csNode = corrNode.getOutgoingEdges().get("cs");
+            if (csNode?.type !== "node") {
+              continue; // corrNode is not a correspondence node
+            }
+            const asNode = corrNode.getOutgoingEdges().get("as");
+            if (asNode?.type !== "node") {
+              continue; // corrNode is not a correspondence node
+            }
+            csToAs.set(csNode.creation.id.value, asNode.creation.id.value);
+            asToCs.set(asNode.creation.id.value, csNode.creation.id.value);
+          }
+
+          return new Promise((resolve: (csDeltas: PrimitiveDelta[]) => void, reject: () => void) => {
+            setManualRendererState({
+              asGraph,
+              csGraph,
+              asGraphState,
+              csGraphState,
+              csToAs,
+              asToCs,
+              asDeltasToRender: asDeltas,
+              done: resolve,
+              cancel: reject,
+            });
+          })
+          .then((additionalCsDeltas: PrimitiveDelta[]) => {
+            finishRender({
+              corrDeltas: corrDeltas.concat(additionalCsDeltas),
+              csDeltas: csDeltas.concat(additionalCsDeltas),
+            });
+          })
+          .catch()
+          .finally(() => setManualRendererState(null));
+        }
+      }
+
+      if (!renderMap.has(asParentVersion)) {
+        return renderExistingVersion(asParentVersion, setManualRendererState)
+        .then(render);
+      }
+      else {
+        return render();
       }
     };
     const gotoVersion = (corrVersion: Version) => {
@@ -154,10 +230,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, setManualRendererState)}
           rightIcon={dir === "right" ? <Icons.IconChevronsRight/>: null}
           leftIcon={dir === "left" ? <Icons.IconChevronsLeft/>: null}
         >Render</Mantine.Button>;
@@ -176,13 +252,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 +268,6 @@ export function newCorrespondence({generateUUID, primitiveRegistry, cs, as}) {
     };
 
     return {
-      parse,
-      render,
       parseExistingVersion,
       renderExistingVersion,
       gotoVersion,

+ 1 - 1
src/frontend/d3_state.ts

@@ -28,7 +28,7 @@ export class D3GraphStateUpdater implements GraphStateListener {
     this.setGraph(prevGraph => ({
       nodes: [...prevGraph.nodes, {
         id: nodeNodeId(ns.creation.id.value),
-        label: ns.creation.id.value.toString(),
+        label: JSON.stringify(ns.creation.id.value),
         x: this.x,
         y: this.y,
         color: "darkturquoise",

+ 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" />

+ 10 - 27
src/frontend/help_icons.tsx

@@ -17,30 +17,13 @@ export function makeInfoHoverCardIcon(contents) {
   );
 }
 
-export const graphEditor = makeInfoHoverCardIcon(<>
-  <Text><b>Left-Drag</b>: Drag Node</Text>
-  <Text><b>Middle-Click</b>: Delete Node</Text>
-  <Text><b>Right-Click</b>: Create Node or Edge</Text>
-  <Text><b>Wheel</b>: Zoom</Text>
-</>);
-
-export const graphEditorReadonly = makeInfoHoverCardIcon(<>
-  <Text><b>Left-Drag</b>: Drag Node</Text>
-  <Text><b>Wheel</b>: Zoom</Text>
-</>);
-
-export const rountangleEditor = makeInfoHoverCardIcon(<>
-  <Text><b>Alt + Left-Click</b>: Create/Delete Rountangle</Text>
-  <Text><b>Left-Drag</b>: Move Rountangle / Pan Canvas</Text>
-</>);
-
-export const depGraph = makeInfoHoverCardIcon(<>
-  <Text><b>Left-Drag</b>: Drag Node</Text>
-  <Text>Active deltas are <b>bold</b>.</Text>
-</>);
-
-export const historyGraph = makeInfoHoverCardIcon(<>
-  <Text><b>Left-Drag</b>: Drag Node</Text>
-  <Text><b>Right-Click</b>: Goto Version</Text>
-  <Text>Current version is <b>bold</b>.</Text>
-</>);
+export function makeOverlayHelpIcon(background, overlayText) {
+  return (
+    <div style={{position:"relative"}}>
+      {background}
+      <div style={{position: "absolute", top: 0, right: 0}}>
+        {makeInfoHoverCardIcon(overlayText)}
+      </div>
+    </div>
+  );
+}

+ 245 - 0
src/frontend/manual_renderer.tsx

@@ -0,0 +1,245 @@
+import * as React from "react";
+import {IconChevronLeft, IconChevronRight, IconAlertCircle, IconCircleCheck, IconArrowsHorizontal, IconInfoCircle} from "@tabler/icons";
+import {SimpleGrid, Group, Grid, Stack, Title, Button, Text, Modal, Center, List, Space, Table} from "@mantine/core";
+
+import {RountangleEditor} from "./rountangleEditor/RountangleEditor";
+import {Version} from "../onion/version";
+import {emptyGraph, graphForces} from "./constants";
+import {makeOverlayHelpIcon} from "./help_icons";
+import {d3Types, Graph} from "./graph";
+import {PrimitiveDelta, EdgeCreation, EdgeUpdate, PrimitiveRegistry, NodeCreation, NodeDeletion} from "../onion/primitive_delta";
+import {GraphType} from "./editable_graph";
+import {UUID, PrimitiveValue} from "../onion/types";
+import {GraphState, INodeState} from "../onion/graph_state";
+import {Geometry2DRect, getGeometry, isInside} from "../parser/trivial_parser";
+import {D3GraphStateUpdater} from "./d3_state";
+
+export interface ManualRendererProps {
+  asGraph: GraphType;
+  csGraph: GraphType;
+
+  asGraphState: GraphState;
+  csGraphState: GraphState;
+
+  csToAs: Map<PrimitiveValue, PrimitiveValue>;
+  asToCs: Map<PrimitiveValue, PrimitiveValue>;
+
+  asDeltasToRender: PrimitiveDelta[];
+
+  done: (csDeltas: PrimitiveDelta[]) => void;
+  cancel: () => void;
+}
+
+// Replay all deltas in version to get graph state (+ D3 graph state)
+function getD3GraphState(version: Version, setGraph) {
+  const graphState = new GraphState();
+  const d3Updater = new D3GraphStateUpdater(setGraph, 0, 0);
+  for (const d of [...version].reverse()) {
+    graphState.exec(d, d3Updater)
+  }
+  return graphState;
+}
+
+function getInsidenesses(csGraphState: GraphState) {
+  // get every CS node and its rountangle-geometry:
+  const geometries: Array<[INodeState, Geometry2DRect]> = [];
+  for (const csNode of csGraphState.nodes.values()) {
+    const geometry = getGeometry(csGraphState, csNode.creation.id.value);
+    if (geometry === undefined) continue;
+    geometries.push([csNode, geometry]);
+  }
+
+  // sort by surface area, from small to large:
+  geometries.sort(([_, geomA], [__, geomB]) => (geomA.w * geomA.h) - (geomB.w * geomB.h));
+
+  // array of pairs (a,b), meaning a is inside b.
+  const insidenesses: Array<[PrimitiveValue, PrimitiveValue]> = [];
+
+  for (const [csNode1, geom1] of geometries) {
+    for (const [csNode2, geom2] of geometries) {
+      if (isInside(geom1, geom2)) {
+        insidenesses.push([csNode1.creation.id.value, csNode2.creation.id.value]);
+        break; // we're only interested in the smallest other rountangle that we're inside
+      }
+    }
+  }
+  return insidenesses;
+}
+
+export function newManualRenderer({generateUUID, primitiveRegistry}) {
+
+  // Return functional React component
+  function ManualRenderer(props: ManualRendererProps) {
+    const [csGraph, setCsGraph] = React.useState<GraphType>(props.csGraph);
+    const [csGraphState] = React.useState<GraphState>(props.csGraphState);
+    const [csDeltas, setCsDeltas] = React.useState<PrimitiveDelta[]>([]);
+
+    // Is the parent-hierarchy in CS consistent with AS?
+    function findInconsistencies(csGraphState, asGraphState) {
+      const inconsistencies: string[] = [];
+      const insidenesses = getInsidenesses(csGraphState);
+      // For every insideness in CS, there must be a parent-link in AS:
+      for (const [insideCs, outsideCs] of insidenesses) {
+        const childAs = asGraphState.nodes.get(props.csToAs.get(insideCs)!);
+        const parentAs = asGraphState.nodes.get(props.csToAs.get(outsideCs)!);
+        if (childAs.getOutgoingEdges().get("hasParent") !== parentAs) {
+          inconsistencies.push("CS rountangle " + JSON.stringify(insideCs) + " must not be inside " + JSON.stringify(outsideCs));
+        }
+      }
+      // For every parent-link in AS, there must be an "insideness" in CS:
+      for (const childAs of asGraphState.nodes.values()) {
+        const parentAs = childAs.getOutgoingEdges().get("hasParent");
+        if (parentAs !== undefined) {
+          const insideCsId = props.asToCs.get(childAs.creation.id.value)!;
+          const outsideCsId = props.asToCs.get(parentAs.creation.id.value)!
+          const insideGeometry = getGeometry(csGraphState, insideCsId)!;
+          const outsideGeometry = getGeometry(csGraphState, outsideCsId)!;
+          if (!isInside(insideGeometry, outsideGeometry)) {
+            inconsistencies.push("CS rountangle " + JSON.stringify(insideCsId) + " must be inside " + JSON.stringify(outsideCsId));
+          }
+        }
+      }
+      return inconsistencies;
+    }
+
+    const [inconsistencies, setInconsistencies] = React.useState<string[]>(findInconsistencies(props.csGraphState, props.asGraphState));
+
+    const [undone, setUndone] = React.useState<PrimitiveDelta[]>([]);
+
+    const changelog = props.asDeltasToRender.filter(d => !(d instanceof NodeDeletion)).map(d => {
+      if (d instanceof NodeCreation) {
+        return "Newly created rountangle " + JSON.stringify(props.asToCs.get(d.id.value)!) + " needs geometry";
+      }
+      else if (d instanceof EdgeCreation) {
+        return "Rountangle " + JSON.stringify(props.asToCs.get(d.source.id.value)!) + " needs to be moved inside rountangle " + JSON.stringify((props.asToCs.get((d.target.getTarget() as NodeCreation).id.value)!));
+      }
+      else if (d instanceof EdgeUpdate) {
+        const oldTarget = d.overwrites.target.getTarget();
+        const newTarget = d.target.getTarget();
+        const sourceId = d.getCreation().source.id.value;
+        if (oldTarget === null && newTarget !== null) {
+          return "Rountangle " + JSON.stringify(props.asToCs.get(sourceId)!) + " needs to be moved inside rountangle " + JSON.stringify(props.asToCs.get((newTarget as NodeCreation).id.value));
+        }
+        else if (oldTarget !== null && newTarget === null) {
+          return "Rountangle " + JSON.stringify(props.asToCs.get(sourceId)!) + " is no longer inside rountangle " + JSON.stringify(props.asToCs.get((oldTarget as NodeCreation).id.value)!);
+        }
+        else if (oldTarget !== null && newTarget !== null) {
+          return "Rountangle " + JSON.stringify(props.asToCs.get(sourceId)!) + " is no longer inside rountangle " + JSON.stringify(props.asToCs.get((oldTarget as NodeCreation).id.value)!) + " but instead inside rountangle " + JSON.stringify(props.asToCs.get((newTarget as NodeCreation).id.value)!);
+        }
+      }
+    });
+
+    return <>
+      <Stack>
+      <List icon={<IconInfoCircle/>}>
+      {changelog.map((line, i) => <List.Item key={i}>{line}</List.Item>)}
+      </List>
+      <Group>
+        <Stack>
+          <Group position="apart">
+            <Title order={5}>Concrete Syntax (please adjust geometries)</Title>
+            <Group>
+              <Button disabled={csDeltas.length === 0} onClick={() => {
+                const csDelta = csDeltas[csDeltas.length-1];
+                setCsDeltas(csDeltas.slice(0,-1));
+                setUndone(undone.concat(csDelta));
+                csGraphState.unexec(csDelta, new D3GraphStateUpdater(setCsGraph, 0, 0));
+              }} compact leftIcon={<IconChevronLeft/>}>Undo</Button>
+              <Button disabled={undone.length === 0} onClick={() => {
+                const csDelta = undone[undone.length-1];
+                setCsDeltas(csDeltas.concat(csDelta));
+                setUndone(undone.slice(0,-1));
+                csGraphState.exec(csDelta, new D3GraphStateUpdater(setCsGraph, 0, 0));
+              }} compact rightIcon={<IconChevronRight/>}>Redo</Button>
+            </Group>
+          </Group>
+          {makeOverlayHelpIcon(
+            <RountangleEditor
+              graph={csGraph}
+              generateUUID={generateUUID}
+              primitiveRegistry={primitiveRegistry}
+              graphState={csGraphState}
+              onUserEdit={(deltas: PrimitiveDelta[], description: string) => {
+                // the user is not allowed to create/remove rountangles, so we only respond to EdgeUpdates
+                const filteredDeltas = deltas.filter(d => d instanceof EdgeUpdate);
+                setCsDeltas(prevCsDeltas => prevCsDeltas.concat(filteredDeltas));
+                setUndone([]);
+                filteredDeltas.forEach(d => csGraphState.exec(d, new D3GraphStateUpdater(setCsGraph, 0, 0)));
+                setInconsistencies(findInconsistencies(csGraphState, props.asGraphState));
+              }}
+            />,
+            <Text><b>Left-Drag</b>: Move/Resize Rountangle / Pan Canvas</Text>
+          )}
+        </Stack>
+        <Stack align="flex-start">
+          <Stack style={{height:"100%"}}>
+          <Table>
+            <caption>ID-Mapping</caption>
+            <tbody>
+            {[...props.csToAs.entries()].map(([csId, asId], i) => (
+              <tr key={i}>
+                <td>{JSON.stringify(csId)}</td>
+                <td><IconArrowsHorizontal stroke={1.2}/></td>
+                <td>{JSON.stringify(asId)}</td>
+              </tr>
+            ))}
+            </tbody>
+          </Table>
+          </Stack>
+        </Stack>
+        <Stack>
+          <Title order={5}>Abstract Syntax (read-only)</Title>
+          {makeOverlayHelpIcon(<Graph
+            graph={props.asGraph}
+            forces={graphForces}
+          />, <Text><b>Left-Drag</b>: Drag Node</Text>)}
+        </Stack>
+      </Group>
+      <Group position="apart">
+        {inconsistencies.length > 0 ?
+          <List icon={<IconAlertCircle color="red"/>}>
+            {inconsistencies.map(inconsistencyText => <List.Item key={inconsistencyText}>{inconsistencyText}</List.Item>)}
+          </List>
+        :
+          <List icon={<IconCircleCheck color="green"/>}>
+            <List.Item color="green">No inconsistencies</List.Item>
+          </List>
+        }
+        <Group position="right">
+          <Button variant="subtle" onClick={() => props.cancel()}>Cancel</Button>
+          <Button onClick={() => props.done(csDeltas)} disabled={inconsistencies.length > 0}>Done</Button>
+        </Group>
+      </Group>
+      </Stack>
+    </>;
+  }
+
+  const getModalManualRenderer = (manualRendererState: null | ManualRendererProps) => {
+    return <Modal
+      opened={manualRendererState !== null}
+      onClose={() => manualRendererState?.cancel?.()}
+      title="Rendering: Missing Geometry Information"
+      size="auto"
+      centered
+    >
+      {manualRendererState === null ? <></> :
+        <ManualRenderer
+          asGraph={manualRendererState.asGraph}
+          csGraph={manualRendererState.csGraph}
+          asGraphState={manualRendererState.asGraphState}
+          csGraphState={manualRendererState.csGraphState}
+          csToAs={manualRendererState.csToAs}
+          asToCs={manualRendererState.asToCs}
+          asDeltasToRender={manualRendererState.asDeltasToRender}
+          done={manualRendererState.done}
+          cancel={manualRendererState.cancel}
+        />}
+    </Modal>;
+  }
+
+  return {
+    ManualRenderer,
+    getModalManualRenderer,
+  }
+}
+

+ 3 - 3
src/frontend/rountangleEditor/RountangleActions.ts

@@ -1,9 +1,9 @@
 import {PrimitiveValue} from "../../onion/types";
 
-interface CreateRountangle  {tag: 'createRountangle',  id: PrimitiveValue, name: string, posX: number, posY: number, posZ: number, width: number, height: number}
+interface CreateRountangle  {tag: 'createRountangle',  id: PrimitiveValue, /*name: string,*/ posX: number, posY: number, posZ: number, width: number, height: number}
 interface MoveRountangle    {tag: 'moveRountangle',    id: PrimitiveValue, newPosX: number, newPosY: number}
 interface ResizeRountangle  {tag: 'resizeRountangle',  id: PrimitiveValue, width: number, height: number}
-interface RenameRountangle  {tag: 'renameRountangle',  id: PrimitiveValue, newName: string}
+// interface RenameRountangle  {tag: 'renameRountangle',  id: PrimitiveValue, newName: string}
 interface ChangeRountangleZ {tag: 'changeRountangleZ', id: PrimitiveValue, newPosZ: number}
 interface DeleteRountangle  {tag: 'deleteRountangle',  id: PrimitiveValue}
 
@@ -11,7 +11,7 @@ export type RountangleAction =
     Readonly<CreateRountangle>
     | Readonly<MoveRountangle>
     | Readonly<ResizeRountangle>
-    | Readonly<RenameRountangle>
+    // | Readonly<RenameRountangle>
     | Readonly<ChangeRountangleZ>
     | Readonly<DeleteRountangle>
 ;

+ 14 - 14
src/frontend/rountangleEditor/RountangleComponent.tsx

@@ -26,7 +26,7 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
             || this.props.posZ       !== nextProps.posZ
             || this.props.width      !== nextProps.width
             || this.props.height     !== nextProps.height
-            || this.props.name       !== nextProps.name
+            // || this.props.name       !== nextProps.name
             || this.state.dragging   !== nextState.dragging
             || this.state.movementsX !== nextState.movementsX
             || this.state.movementsY !== nextState.movementsY
@@ -98,16 +98,16 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
         }
     }
 
-    onDoubleClick = (event: React.MouseEvent) => {
-        const newRountangleName = prompt('Rename', this.props.name);
-        if (newRountangleName && newRountangleName !== this.props.name) {
-            this.props.dispatch({
-                tag: 'renameRountangle',
-                id: this.props.id,
-                newName: newRountangleName
-            });
-        }
-    }
+    // onDoubleClick = (event: React.MouseEvent) => {
+    //     const newRountangleName = prompt('Rename', this.props.name);
+    //     if (newRountangleName && newRountangleName !== this.props.name) {
+    //         this.props.dispatch({
+    //             tag: 'renameRountangle',
+    //             id: this.props.id,
+    //             newName: newRountangleName
+    //         });
+    //     }
+    // }
 
     onResize = (deltaHeight: number, deltaWidth: number) => {
         if (deltaHeight !== 0 || deltaWidth !== 0) {
@@ -154,13 +154,13 @@ export class RountangleComponent extends React.Component<RountangleProps, Rounta
                     onPointerDown={this.onPointerDown}
                     onPointerMove={this.onPointerMove}
                     onPointerUp={this.onPointerUp}
-                    onDoubleClick={this.onDoubleClick}
+                    // onDoubleClick={this.onDoubleClick}
                 >
                     <div
-                        className={'re-rountangle-name'}
+                        className={'re-rountangle-id'}
                         title={this.props.id.toString()}
                     >
-                        <span>{this.props.name}</span>
+                        <span>ID<sub>CS</sub>: {JSON.stringify(this.props.id)}</span>
                     </div>
                 </foreignObject>
                 <RountangleResizeHandleComponent

+ 6 - 0
src/frontend/rountangleEditor/RountangleEditor.css

@@ -33,6 +33,12 @@
     flex-direction: column;
     pointer-events: none;
 }
+.re-rountangle-id {
+    position: absolute;
+    left: 5px;
+    top: 2px;
+    font-size: 0.8em;
+}
 .re-rountangle-resize-handle {
     fill: deeppink;
     cursor: nwse-resize;

+ 17 - 16
src/frontend/rountangleEditor/RountangleEditor.tsx

@@ -9,7 +9,7 @@ import {assert, assertNever} from "../../util/assert";
 import {d3Types} from "../graph";
 
 export interface Rountangle {
-    readonly name:   string;
+    // readonly name:   string;
     readonly posX:   number;
     readonly posY:   number;
     readonly posZ:   number;
@@ -22,7 +22,7 @@ export function isRountangle(d3node: d3Types.d3Node<INodeState | IValueState>) {
     const nodeState = d3node.obj as unknown as INodeState;
     const outgoing = nodeState.getOutgoingEdges();
     if (!(outgoing.get('type')?.asTarget() === 'Rountangle')) return false;
-    if (!(typeof outgoing.get('label')?.asTarget()   === "string")) return false;
+    // if (!(typeof outgoing.get('label')?.asTarget()   === "string")) return false;
     if (!(typeof outgoing.get('x')?.asTarget()       === "number")) return false;
     if (!(typeof outgoing.get('y')?.asTarget()       === "number")) return false;
     if (!(typeof outgoing.get('z-index')?.asTarget() === "number")) return false;
@@ -41,7 +41,7 @@ export function graphStateToRountangle(d3node: d3Types.d3Node<INodeState | IValu
     const nodeState = d3node.obj as INodeState;
     const outgoing = nodeState.getOutgoingEdges();
     return [nodeState.creation.id.value, {
-        name: outgoing.get("label")!.asTarget() as string,
+        // name: outgoing.get("label")!.asTarget() as string,
         posX: outgoing.get("x")!.asTarget() as number,
         posY: outgoing.get("y")!.asTarget() as number,
         posZ: outgoing.get("z-index")!.asTarget() as number,
@@ -149,7 +149,7 @@ export class RountangleEditor extends React.Component<RountangleEditorProps, Rou
                 deltas.push(createRountangleNodeDelta);
                 const edgeSpec: [string,PrimitiveValue][] = [
                     ["type","Rountangle"],
-                    ["label",action.name],
+                    // ["label",action.name],
                     ["x",action.posX],
                     ["y",action.posY],
                     ["z-index",action.posZ],
@@ -179,12 +179,12 @@ export class RountangleEditor extends React.Component<RountangleEditorProps, Rou
                     deltas.push(...this.createValueDeltas(nodeState, [["width",action.width],["height",action.height]]));
                 }
                 break;
-            case 'renameRountangle':
-                nodeState = this.props.graphState.nodes.get(action.id);
-                if (nodeState !== undefined) {
-                    deltas.push(...this.createValueDeltas(nodeState, [["label",action.newName]]));
-                }
-                break;
+            // case 'renameRountangle':
+            //     nodeState = this.props.graphState.nodes.get(action.id);
+            //     if (nodeState !== undefined) {
+            //         deltas.push(...this.createValueDeltas(nodeState, [["label",action.newName]]));
+            //     }
+            //     break;
             case 'changeRountangleZ':
                 // this.setState( (state,_) => ({
                 //     ...state,
@@ -258,18 +258,19 @@ export class RountangleEditor extends React.Component<RountangleEditorProps, Rou
             const cursorPoint = this.clickToSVGPos(event.clientX, event.clientY);
 
             if (cursorPoint) {
-                const newRountangleName = prompt('Name', 'New Rountangle');
-                if (newRountangleName) {
+                // const newRountangleName = prompt('Name', 'New Rountangle');
+                // if (newRountangleName) {
                     this.dispatch({
                         tag: 'createRountangle',
-                        id: this.props.generateUUID().value.toString(),
+                        id: this.props.generateUUID().value,
                         posX: cursorPoint.x,
                         posY: cursorPoint.y,
                         posZ: 10,
                         width: 100,
                         height: 66,
-                        name: newRountangleName});
-                }
+                        // name: newRountangleName,
+                    });
+                // }
             }
         }
     }
@@ -328,7 +329,7 @@ export class RountangleEditor extends React.Component<RountangleEditorProps, Rou
                             return <RountangleComponent
                                 key={id.toString()}
                                 id={id}
-                                name={rountangle.name}
+                                // name={rountangle.name}
                                 posX={rountangle.posX}
                                 posY={rountangle.posY}
                                 posZ={rountangle.posZ}

+ 34 - 19
src/frontend/versioned_model.tsx

@@ -23,7 +23,7 @@ import {
 import {d3Types, Graph} from "./graph"
 import {emptyGraph, graphForces} from "./constants";
 import {RountangleEditor} from "./rountangleEditor/RountangleEditor";
-import * as HelpIcons from "./help_icons";
+import {makeOverlayHelpIcon} from "./help_icons";
 
 import {embed, Version, VersionRegistry} from "../onion/version";
 import {PrimitiveDelta, PrimitiveRegistry} from "../onion/primitive_delta";
@@ -32,6 +32,32 @@ import {CompositeDelta, CompositeLevel} from "../onion/composite_delta";
 import {GraphState} from "../onion/graph_state"; 
 import {Delta} from "../onion/delta";
 
+const helpText = {
+  graphEditor: <>
+    <Mantine.Text><b>Left-Drag</b>: Drag Node</Mantine.Text>
+    <Mantine.Text><b>Middle-Click</b>: Delete Node</Mantine.Text>
+    <Mantine.Text><b>Right-Click</b>: Create Node or Edge</Mantine.Text>
+    <Mantine.Text><b>Wheel</b>: Zoom</Mantine.Text>
+  </>,
+  graphEditorReadonly: <>
+    <Mantine.Text><b>Left-Drag</b>: Drag Node</Mantine.Text>
+    <Mantine.Text><b>Wheel</b>: Zoom</Mantine.Text>
+  </>,
+  rountangleEditor: <>
+    <Mantine.Text><b>Alt + Left-Click</b>: Create/Delete Rountangle</Mantine.Text>
+    <Mantine.Text><b>Left-Drag</b>: Move/Resize Rountangle / Pan Canvas</Mantine.Text>
+  </>,
+  depGraph: <>
+    <Mantine.Text><b>Left-Drag</b>: Drag Node</Mantine.Text>
+    <Mantine.Text>Active deltas are <b>bold</b>.</Mantine.Text>
+  </>,
+  historyGraph: <>
+    <Mantine.Text><b>Left-Drag</b>: Drag Node</Mantine.Text>
+    <Mantine.Text><b>Right-Click</b>: Goto Version</Mantine.Text>
+    <Mantine.Text>Current version is <b>bold</b>.</Mantine.Text>
+  </>,
+};
+
 export interface VersionedModelState {
   version: Version; // the 'current version'
   graph: GraphType; // the state what is displayed in the leftmost panel
@@ -47,17 +73,6 @@ interface VersionedModelCallbacks {
   onVersionClicked?: (Version) => void;
 }
 
-function makeOverlayHelpIcon(background, helpIcon) {
-  return (
-    <div style={{position:"relative"}}>
-      {background}
-      <div style={{position: "absolute", top: 0, right: 0}}>
-        {helpIcon}
-      </div>
-    </div>
-  );
-}
-
 // Basically everything we need to construct the React components for:
 //  - Graph state (+ optionally, a Rountangle Editor)
 //  - History graph (+undo/redo buttons)
@@ -208,19 +223,19 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
           primitiveRegistry={primitiveRegistry}
           setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
           onUserEdit={callbacks.onUserEdit}
-        />, readonly ? HelpIcons.graphEditorReadonly : HelpIcons.graphEditor);
+        />, readonly ? helpText.graphEditorReadonly : helpText.graphEditor);
 
     const depGraphL1Component = makeOverlayHelpIcon(
       <Graph graph={state.dependencyGraphL1} forces={graphForces} />,
-      HelpIcons.depGraph);
+      helpText.depGraph);
     const depGraphL0Component = makeOverlayHelpIcon(
       <Graph graph={state.dependencyGraphL0} forces={graphForces} />,
-      HelpIcons.depGraph);
+      helpText.depGraph);
 
     const historyComponent = makeOverlayHelpIcon(
       <Graph graph={state.historyGraph} forces={graphForces}
         mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />,
-      HelpIcons.historyGraph);
+      helpText.historyGraph);
 
     const rountangleEditor = makeOverlayHelpIcon(
       <RountangleEditor
@@ -230,7 +245,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
         graphState={graphState}
         onUserEdit={callbacks.onUserEdit}
       />,
-      HelpIcons.rountangleEditor);
+      helpText.rountangleEditor);
 
     const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => {
       if (parentsOrChildren.length === 0) {
@@ -263,7 +278,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
     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)}>
+          <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"/>
@@ -273,7 +288,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
     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)}>
+          <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"/>

+ 67 - 57
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;
@@ -36,9 +35,23 @@ export interface Geometry2DRect {
 const geometryLabels = ["x", "y", "width", "height"];
 
 // Whether a is inside of b
-const isInside = (a: Geometry2DRect, b: Geometry2DRect): boolean => 
+export 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;
 
+export function getGeometry(sourceState: GraphState, nodeId: PrimitiveValue): Geometry2DRect | undefined {
+  const node = sourceState.nodes.get(nodeId);
+  if (node !== undefined) {
+    try {
+      return {
+        x: (node.getOutgoingEdges().get("x")!.asTarget()) as number,
+        y: (node.getOutgoingEdges().get("y")!.asTarget()) as number,
+        w: (node.getOutgoingEdges().get("width")!.asTarget()) as number,
+        h: (node.getOutgoingEdges().get("height")!.asTarget()) as number,
+      };
+    }
+    catch(e) {}
+  }
+}
 
 // A parser that creates an AS-node for every CS-node, with a Corr-node in between.
 export class TrivialParser  {
@@ -52,7 +65,6 @@ export class TrivialParser  {
     this.getUuid = getUuid;
   }
 
-
   // We can use pretty much the same code for both parsing and rendering :)
   propagate_change(parse: boolean, sourceDeltas: PrimitiveDelta[], sourceState: GraphState, corrState: GraphState, targetState: GraphState) {
     const targetDeltas: Delta[] = []; // deltas that are only part of target model
@@ -66,7 +78,7 @@ export class TrivialParser  {
 
     // In order to parse, the CS/CORR/AS-state may be altered in-place.
     // Whenever the state is altered, a callback must be pushed to this array that undoes the change.
-    const revertState: (()=>void)[] = [];
+    // const revertState: (()=>void)[] = [];
 
     const applyToState = (state: GraphState, delta: PrimitiveDelta | PrimitiveDelta[]) => {
       if (Array.isArray(delta)) {
@@ -74,19 +86,18 @@ export class TrivialParser  {
       }
       else {
         state.exec(delta);
-        revertState.push(() => state.unexec(delta));
+        // revertState.push(() => state.unexec(delta));
       }
     }
 
+    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,42 +113,49 @@ 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", ""],
+              ["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 :)
           const sourceCreation = sourceDeletion.creation; // the NodeCreation of the deleted cs node
+          const sourceId = sourceCreation.id.value;
 
-          // edge from corrspondence node to CS node:
-          const corr2Source = sourceCreation.incomingEdges.find(delta => delta instanceof EdgeCreation && delta.label === corr2SourceLabel);
-          if (corr2Source === undefined || !(corr2Source instanceof EdgeCreation)) {
-            throw new Error("Assertion failed: Must be able to find corr -> " + corr2SourceLabel + " edge.");
-          }
-
-          // creation of correspondence node:
-          const corrCreation = corr2Source.source;
-
-          const corr2Target = corrCreation.outgoingEdges.find(delta => delta instanceof EdgeCreation && delta.label === corr2TargetLabel);
-          if (corr2Target === undefined || !(corr2Target instanceof EdgeCreation)) {
-            throw new Error("Assertion failed: Must be able to find corr -> " + corr2TargetLabel + " edge.");
-          }
-
-          const targetCreation = corr2Target.target.getTarget() as NodeCreation;
+          // Follow 'cs'/'as' edges in correspondence model to find the corresponding node:
+          const sourceNodeState = corrState.nodes.get(sourceId)!;
+          const [_, corrNodeState] = sourceNodeState.getIncomingEdges().find(([label, nodeState]) => label === corr2SourceLabel)!;
+          const targetNodeState = corrNodeState.getOutgoingEdges().get(corr2TargetLabel) as INodeState;
 
-          // deletion of correspondence node, and its outgoing ('cs', 'as') edges.
-          const corrDeletion = corrState.nodes.get(corrCreation.id.value)!.getDeltasForDelete(this.primitiveRegistry);
+          // Deletion of correspondence node, and its outgoing ('cs', 'as') edges:
+          const corrDeletion = corrNodeState.getDeltasForDelete(this.primitiveRegistry);
 
           // The following operations on corrState depend on 'corrDeletion'. That's why update corrState now. We'll undo these changes later.
           applyToState(corrState, corrDeletion);
 
-          // We create 2 (conflicting) asDeletions:
+          // We create 2 asDeletions:
           // one that is to be used only in the AS model, unaware of any CORR-stuff, and one that is to be used in the CORR model.
-          const targetDeletion = targetState.nodes.get(targetCreation.id.value)!.getDeltasForDelete(this.primitiveRegistry);
-          const targetDeletion1 = corrState.nodes.get(targetCreation.id.value)!.getDeltasForDelete(this.primitiveRegistry);
+          const targetDeletion = targetState.nodes.get(targetNodeState.creation.id.value)!.getDeltasForDelete(this.primitiveRegistry);
+          const targetDeletion1 = targetNodeState.getDeltasForDelete(this.primitiveRegistry);
 
           applyToState(corrState, targetDeletion1);
 
           // We already have the deletion in the CS model, so we only need to create another one to be used in the CORR model:
-          const sourceDeletion1 = corrState.nodes.get(sourceCreation.id.value)!.getDeltasForDelete(this.primitiveRegistry);
+          const sourceDeletion1 = sourceNodeState.getDeltasForDelete(this.primitiveRegistry);
 
           applyToState(corrState, sourceDeletion1);
 
@@ -159,26 +177,15 @@ 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();
           const label = edgeCreation.label;
 
-          function getGeometry(nodeId: PrimitiveValue): Geometry2DRect | undefined {
-            const node = sourceState.nodes.get(nodeId);
-            if (node !== undefined) {
-              try {
-                return {
-                  x: (node.getOutgoingEdges().get("x")!.asTarget()) as number,
-                  y: (node.getOutgoingEdges().get("y")!.asTarget()) as number,
-                  w: (node.getOutgoingEdges().get("width")!.asTarget()) as number,
-                  h: (node.getOutgoingEdges().get("height")!.asTarget()) as number,
-                };
-              }
-              catch(e) {}
-            }
-          }
           function findCorrespondingAsNode(csNode: INodeState, reverse: boolean = false) {
             const pair = csNode.getIncomingEdges().find(([label]) => label===(reverse?"as":"cs"));
             if (pair === undefined) {
@@ -194,7 +201,7 @@ export class TrivialParser  {
 
           if (geometryLabels.includes(label)) {
             const updatedNodeId = edgeCreation.source.id.value;
-            const updatedGeometry = getGeometry(updatedNodeId);
+            const updatedGeometry = getGeometry(sourceState, updatedNodeId);
             if (updatedGeometry !== undefined) {
               const updatedSurface = updatedGeometry.w * updatedGeometry.h;
               const updatedAsNode = findCorrespondingAsNode(corrState.nodes.get(updatedNodeId) as INodeState);
@@ -209,7 +216,7 @@ export class TrivialParser  {
                   if (otherNodeState.creation === edgeCreation.source) {
                     continue; // don't compare with ourselves
                   }
-                  const otherGeometry = getGeometry(otherNodeId);
+                  const otherGeometry = getGeometry(sourceState, otherNodeId);
                   if (otherGeometry !== undefined) {
                     const inside = isInside(geometry, otherGeometry);
                     if (inside) {
@@ -224,7 +231,7 @@ export class TrivialParser  {
                 if (smallestParent !== null) {
                   const existingLink = asNodeState.getOutgoingEdges().get("hasParent");
                   if (existingLink !== smallestParent) {
-                    console.log("updated geometry is on inside...");
+                    // console.log("updated geometry is on inside...");
                     const asParentLink = asNodeState.getDeltasForSetEdge(this.primitiveRegistry, "hasParent", smallestParent.asTarget());
                     applyToState(corrState, asParentLink);
                     targetDeltas.push(...asParentLink);
@@ -238,10 +245,10 @@ export class TrivialParser  {
               if (otherAsNodeState !== undefined && otherAsNodeState.type === "node") {
                 const otherCsNodeState = findCorrespondingAsNode(otherAsNodeState, true);
                 const otherNodeId = otherCsNodeState.creation.id.value;
-                const otherGeometry = getGeometry(otherNodeId);
+                const otherGeometry = getGeometry(sourceState, otherNodeId);
                 if (otherGeometry === undefined || !isInside(updatedGeometry, otherGeometry)) {
                   // parent relation no longer holds
-                  console.log("deleting outgoing link...")
+                  // console.log("deleting outgoing link...")
                   // CORRECT: we'll find updatedAsNode's new parent in step 2.
                   const deleteLink = updatedAsNode.getDeltasForSetEdge(this.primitiveRegistry, "hasParent", null); // deletes the edge
                   applyToState(corrState, deleteLink);
@@ -252,7 +259,7 @@ export class TrivialParser  {
               for (const [_, otherAsNodeState] of updatedAsNode.getIncomingEdges().filter(([label, ns]) => label === "hasParent")) {
                 const otherCsNodeState = findCorrespondingAsNode(otherAsNodeState, true);
                 const otherNodeId = otherCsNodeState.creation.id.value;
-                const otherGeometry = getGeometry(otherNodeId);
+                const otherGeometry = getGeometry(sourceState, otherNodeId);
                 if (otherGeometry === undefined) {
                   throw new Error("Assertion failed: The Corresponding CS node of an AS node that is target of 'hasParent' has no geometry.");
                 }
@@ -276,7 +283,7 @@ export class TrivialParser  {
                 if (otherNodeState.creation === edgeCreation.source) {
                   continue; // don't compare with ourselves
                 }
-                const otherGeometry = getGeometry(otherNodeId);
+                const otherGeometry = getGeometry(sourceState, otherNodeId);
                 if (otherGeometry !== undefined) {
                   findAndSetNewParent(updatedGeometry, updatedAsNode);
                   const outside = isInside(otherGeometry, updatedGeometry);
@@ -290,14 +297,14 @@ export class TrivialParser  {
                         return Infinity;
                       }
                       const otherCurrentParentCs = findCorrespondingAsNode(otherCurrentParent as INodeState, true);
-                      const otherCurrentParentGeometry = getGeometry(otherCurrentParentCs.creation.id.value);
+                      const otherCurrentParentGeometry = getGeometry(sourceState, otherCurrentParentCs.creation.id.value);
                       if (otherCurrentParentGeometry === undefined) {
                         return Infinity;
                       }
                       return otherCurrentParentGeometry.w * otherCurrentParentGeometry.h;
                     })();
                     if (updatedSurface < otherCurrentParentSurface) {
-                      console.log("updated geometry is on outside...");
+                      // console.log("updated geometry is on outside...");
                       const asParentLink = otherAsNode.getDeltasForSetEdge(this.primitiveRegistry, "hasParent", updatedAsNode.asTarget());
                       applyToState(corrState, asParentLink);
                       targetDeltas.push(...asParentLink);
@@ -312,7 +319,7 @@ export class TrivialParser  {
     }
     finally {
       // Rollback all changes that were made (in-place) to the CS/CORR/AS-state
-      revertState.reduceRight((_,callback) => {callback(); return null;}, null);
+      // revertState.reduceRight((_,callback) => {callback(); return null;}, null);
     }
 
     const corrDeltasOrderedByDependency: PrimitiveDelta[] = [];
@@ -330,6 +337,7 @@ export class TrivialParser  {
       targetDeltas,
       sourceOverrides,
       targetOverrides,
+      complete,
     };
     // console.log(result);
     return result;
@@ -341,9 +349,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 +361,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 };
   }
 }