瀏覽代碼

Completed first version of live modeling demo

Joeri Exelmans 2 年之前
父節點
當前提交
1e30266215
共有 4 個文件被更改,包括 74 次插入29 次删除
  1. 1 1
      dist/index.html
  2. 5 5
      src/frontend/app.tsx
  3. 66 23
      src/frontend/demos/demo_sem.tsx
  4. 2 0
      src/frontend/info_hover_card.tsx

+ 1 - 1
dist/index.html

@@ -6,6 +6,6 @@
   </head>
   <body>
     <div id="root"></div>
-    <script type="module" src="./bundle.js"></script>
+    <script src="./bundle.js"></script>
   </body>
 </html>

+ 5 - 5
src/frontend/app.tsx

@@ -13,7 +13,7 @@ import {demo_BM_description, getDemoBM} from "./demos/demo_bm";
 import {demo_Editor_description, getDemoEditor} from "./demos/demo_editor";
 import {demo_Welcome_description, Welcome} from "./demos/demo_welcome";
 import {demo_LE_description, getDemoLE} from "./demos/demo_le";
-import {demo_Sem_description, getDemoSem} from "./demos/demo_sem";
+import {demo_Live_description, getDemoLive} from "./demos/demo_live";
 
 export function getApp() {
     const DemoEditor = getDemoEditor();
@@ -21,7 +21,7 @@ export function getApp() {
     const DemoCorr = getDemoCorr();
     const DemoBM = getDemoBM();
     const DemoLE = getDemoLE();
-    const DemoSem = getDemoSem();
+    const DemoLive = getDemoLive();
 
     return function App(props) {
         React.useEffect(() => {
@@ -50,7 +50,7 @@ export function getApp() {
                                     <Tabs.Tab style={tabStyle} value="corr">Correspondence</Tabs.Tab>
                                     <Tabs.Tab style={tabStyle} value="bm">Blended Modeling</Tabs.Tab>
                                     <Tabs.Tab style={tabStyle} value="le">List Editor</Tabs.Tab>
-                                    <Tabs.Tab style={tabStyle} value="sem">Semantics</Tabs.Tab>
+                                    <Tabs.Tab style={tabStyle} value="sem">Live Modeling</Tabs.Tab>
                                 </Tabs.List>
                                 <Divider my="md"/>
                                 <div style={{overflow: 'hidden', paddingLeft: '5px'}}>
@@ -76,7 +76,7 @@ export function getApp() {
                                                 {demo_LE_description}
                                             </Tabs.Panel>
                                             <Tabs.Panel value="sem" style={{height: '100%'}}>
-                                                {demo_Sem_description}
+                                                {demo_Live_description}
                                             </Tabs.Panel>
                                         </div>
                                     </ScrollArea>
@@ -117,7 +117,7 @@ export function getApp() {
                                         </Tabs.Panel>
                                         <Tabs.Panel value="sem">
                                             {/* DemoLE comes with its own OnionContext provider */}
-                                            <DemoSem/>
+                                            <DemoLive/>
                                         </Tabs.Panel>
                                         <Anchor href="https://msdl.uantwerpen.be/git/jexelmans/onioncollab"
                                                 target="_blank"

+ 66 - 23
src/frontend/demos/demo_sem.tsx

@@ -1,22 +1,28 @@
 import * as React from 'react';
 import * as Icons from '@tabler/icons';
-import {Button, Divider, Group, Image, SimpleGrid, Space, Text, Title, TextInput, Select, Stack} from '@mantine/core';
+import {Button, Divider, Group, Image, SimpleGrid, Space, Text, Title, TextInput, Select, Stack, Paper} from '@mantine/core';
 
 import { Graphviz } from 'graphviz-react';
 
 import {newVersionedModel, undoButtonHelpText, VersionedModelState,} from '../versioned_model/single_model';
+import {InfoHoverCard} 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";
 
-export const demo_Sem_description = <>
+export const demo_Live_description = <>
   <Title order={4}>
-    Semantics
+    Live Modeling
   </Title>
+  <Text>This demo allows you to edit a Finite State Automaton (FSA) via a projectional editor, and to execute it via an interpreter.
+  </Text>
+  <Text>The model can be edited during its execution (called <em>live modeling</em> in literature). Live modeling can be done collaboratively as well. This works because the abstract syntax and the execution state are just versioned graphs.
+  </Text>
 </>;
 
-export function getDemoSem() {
+export function getDemoLive() {
   const primitiveRegistry = new PrimitiveRegistry();
   const generateUUID = mockUuid();
 
@@ -95,6 +101,7 @@ export function getDemoSem() {
 
       setDotGraph(`
         digraph {
+          rankdir="LR";
           ${states.map(([name])=>name
             + (name === initialStateName ? `[color=blue, style=filled, fontcolor=white]`:``)
             + (name === currentStateName ? `[shape=doublecircle]`:`[shape=circle]`)
@@ -117,8 +124,8 @@ export function getDemoSem() {
     });
 
     const [addStateName, setAddStateName] = React.useState<string>("A");
-    const [addTransitionSrc, setAddTransitionSrc] = React.useState<string>("");
-    const [addTransitionTgt, setAddTransitionTgt] = React.useState<string>("");
+    const [addTransitionSrc, setAddTransitionSrc] = React.useState<string|null>(null);
+    const [addTransitionTgt, setAddTransitionTgt] = React.useState<string|null>(null);
     const [addTransitionEvent, setAddTransitionEvent] = React.useState<string>("e");
 
     function addState() {
@@ -230,6 +237,19 @@ export function getDemoSem() {
       }
     }
 
+    function onExecuteStep(srcName, tgtName, label, transition: INodeState) {
+      if (runtimeModelNode !== null) {
+        designModel.graphState.pushState();
+        runtimeModelNode.getDeltasForSetEdge(primitiveRegistry, "current", getState(tgtName) || null).forEach(d => designModel.graphState.exec(d));
+        const deltas = designModel.graphState.popState();
+        designModelReducer.createAndGotoNewVersion(deltas, "executeStep:"+transitionKey([srcName, tgtName, label]));
+        setCurrentState(tgtName);
+      }
+    }
+
+    const transitionKey = ([srcName, tgtName, label]) =>
+      srcName+'--('+label+')-->'+tgtName;
+
     return <>
       <OnionContext.Provider value={{generateUUID, primitiveRegistry}}>
         <SimpleGrid cols={2}>
@@ -237,44 +257,67 @@ export function getDemoSem() {
             <Group grow>
               {designModelComponents.undoRedoButtons}
             </Group>
-            <Group>
+            <Divider my="xs" label="Design Model Editor" labelPosition="center" />
+            <Group grow>
+            <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===""||states.some(([name]) => name ===addStateName)} leftIcon={<Icons.IconPlus/>}>Add State</Button>
+              <Button onClick={addState} disabled={addStateName===""||states.some(([name]) => name ===addStateName)} leftIcon={<Icons.IconPlus/>} color="green">Add</Button>
             </Group>
-            <Group>
-              <Select searchable clearable label="Source" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionSrc} onChange={setAddTransitionSrc}/>
-              <Select searchable clearable label="Target" data={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/>}>Add Transition</Button>
+            </Paper>
+            {/*<Paper shadow="xs" p="xs" withBorder>*/}
+              <Select searchable clearable label="Initial State" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={initialState} onChange={onInitialStateChange}/>
+            {/*</Paper>*/}
             </Group>
             <Group>
               {
                 states.map(([stateName, stateNodeState]) => {
-                  return <Button key={stateName} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(stateName, stateNodeState)}>Delete State {stateName}</Button>
+                  return <Button color="red" key={stateName} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(stateName, stateNodeState)}>State {stateName}</Button>
                 })
               }
             </Group>
+            <Paper shadow="xs" p="xs" withBorder>
+            <Group grow>
+              <Select searchable clearable label="Source" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={addTransitionSrc} onChange={setAddTransitionSrc}/>
+              <Select searchable clearable label="Target" data={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">Add</Button>
+            </Group>
+            </Paper>
             <Group>
               {
                 transitions.map(([srcName, tgtName, label, tNodeState]) => {
                   const key = srcName+'--('+label+')-->'+tgtName;
-                  return <Button key={key} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(key, tNodeState)}>Delete Transition {key}</Button>
+                  return <Button color="red" key={key} leftIcon={<Icons.IconX/>} onClick={() => onDeleteState(key, tNodeState)}>Transition {key}</Button>
                 })
               }
             </Group>
-            <Group>
-              <Select searchable clearable label="Initial State" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={initialState} onChange={onInitialStateChange}/>
-              <Button disabled={initialState === null || runtimeModelNode !== null} onClick={onInitialize} leftIcon={<Icons.IconPlayerPlay/>}>Initialize Execution</Button>
-            </Group>
-            <Group>
-              <Select disabled={runtimeModelNode === null} searchable clearable label="Current State" data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={currentState} onChange={onCurrentStateChange}/>
-              <Button disabled={runtimeModelNode === null} onClick={onAbort} leftIcon={<Icons.IconPlayerStop/>}>Abort Execution</Button>
+            <Divider my="xs" label="Run-time Execution" labelPosition="center" />
+            <Group grow>
+              <Button disabled={initialState === null || runtimeModelNode !== null} onClick={onInitialize} leftIcon={<Icons.IconPlayerPlay/>}>Init</Button>
+              <Button color="red" disabled={runtimeModelNode === null} onClick={onAbort} leftIcon={<Icons.IconPlayerStop/>}>Abort</Button>
+              <Select disabled={currentState === null} data={transitions.filter(([srcName]) => srcName === currentState).map(t => {
+                  // @ts-ignore:
+                  const key = transitionKey(t);
+                  return {
+                    value: key,
+                    label: key,
+                  }
+              })} label="Execute Step" onChange={key => {
+                const t = transitions.find(t =>
+                  // @ts-ignore:
+                  transitionKey(t) == key);
+                onExecuteStep(...t!);
+              }}/>
+              <Select disabled={runtimeModelNode === null} searchable clearable label={<Group>Current State <InfoHoverCard>
+                <Text>Current state updates when executing an execution step, but can also be overridden at any point in time ("god event").</Text>
+              </InfoHoverCard></Group>} data={states.map(([stateName]) => ({value:stateName, label:stateName}))} value={currentState} onChange={onCurrentStateChange}/>
             </Group>
             <Graphviz dot={dotGraph} options={{fit:false}} className="canvas"/>
             <Text>Powered by GraphViz and WebAssembly.</Text>
           </Stack>
           <Stack>
-            {designModelComponents.makeTabs("merge", ["state", "merge", "deltaL1", "deltaL0"])}
+            {designModelComponents.makeTabs("state", ["state", "merge", "deltaL1", "deltaL0"])}
             {designModelComponents.makeTabs("deltaL1", ["state", "merge", "deltaL1", "deltaL0"])}
           </Stack>
         </SimpleGrid>

+ 2 - 0
src/frontend/info_hover_card.tsx

@@ -2,6 +2,7 @@ import * as React from "react";
 import {ActionIcon, HoverCard} from "@mantine/core";
 import {IconInfoCircle} from "@tabler/icons";
 
+// Creates an 'i' symbol with a hovercard dropdown.
 export function InfoHoverCard({children}) {
   return (
     <HoverCard shadow="md" width={300}>
@@ -17,6 +18,7 @@ export function InfoHoverCard({children}) {
   );
 }
 
+// This is used to display the 'i' symbol on the top-right corner of the graph (SVG) view
 export function InfoHoverCardOverlay({children, contents}) {
   return (
     <div style={{position:"relative"}}>