Compare commits
2 commits
64aab1a6df
...
456b85bf8f
| Author | SHA1 | Date | |
|---|---|---|---|
| 456b85bf8f | |||
| 8b0726ef01 |
22 changed files with 261 additions and 144 deletions
|
|
@ -47,7 +47,8 @@ details:has(+ details) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar input {
|
.toolbar input {
|
||||||
height: 22px;
|
height: 26px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.toolbar div {
|
.toolbar div {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,11 @@ import { useEditor } from "./hooks/useEditor";
|
||||||
import { useSimulator } from "./hooks/useSimulator";
|
import { useSimulator } from "./hooks/useSimulator";
|
||||||
import { useUrlHashState } from "../hooks/useUrlHashState";
|
import { useUrlHashState } from "../hooks/useUrlHashState";
|
||||||
import { plants } from "./plants";
|
import { plants } from "./plants";
|
||||||
import { emptyState } from "@/statecharts/concrete_syntax";
|
import { initialEditorState } from "@/statecharts/concrete_syntax";
|
||||||
import { ModalOverlay } from "./Overlays/ModalOverlay";
|
import { ModalOverlay } from "./Overlays/ModalOverlay";
|
||||||
import { FindReplace } from "./BottomPanel/FindReplace";
|
import { FindReplace } from "./BottomPanel/FindReplace";
|
||||||
import { useCustomMemo } from "@/hooks/useCustomMemo";
|
import { useCustomMemo } from "@/hooks/useCustomMemo";
|
||||||
|
import { usePersistentState } from "@/hooks/usePersistentState";
|
||||||
|
|
||||||
export type EditHistory = {
|
export type EditHistory = {
|
||||||
current: VisualEditorState,
|
current: VisualEditorState,
|
||||||
|
|
@ -51,6 +52,8 @@ export function App() {
|
||||||
const [editHistory, setEditHistory] = useState<EditHistory|null>(null);
|
const [editHistory, setEditHistory] = useState<EditHistory|null>(null);
|
||||||
const [modal, setModal] = useState<ReactElement|null>(null);
|
const [modal, setModal] = useState<ReactElement|null>(null);
|
||||||
|
|
||||||
|
const [[findText, replaceText], setFindReplaceText] = usePersistentState("findReplaceTxt", ["", ""]);
|
||||||
|
|
||||||
const {commitState, replaceState, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
|
const {commitState, replaceState, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
|
||||||
|
|
||||||
const editorState = editHistory && editHistory.current;
|
const editorState = editHistory && editHistory.current;
|
||||||
|
|
@ -67,9 +70,17 @@ export function App() {
|
||||||
([prevState, prevConns], [nextState, nextConns]) => {
|
([prevState, prevConns], [nextState, nextConns]) => {
|
||||||
if ((prevState === null) !== (nextState === null)) return false;
|
if ((prevState === null) !== (nextState === null)) return false;
|
||||||
if ((prevConns === null) !== (nextConns === null)) return false;
|
if ((prevConns === null) !== (nextConns === null)) return false;
|
||||||
|
if (prevConns === null) {
|
||||||
|
return nextConns === null;
|
||||||
|
}
|
||||||
|
if (prevState === null) {
|
||||||
|
return nextState === null;
|
||||||
|
}
|
||||||
|
if (nextConns === null) return false;
|
||||||
|
if (nextState === null) return false;
|
||||||
// the following check is much cheaper than re-rendering everything that depends on
|
// the following check is much cheaper than re-rendering everything that depends on
|
||||||
return connectionsEqual(prevConns!, nextConns!)
|
return connectionsEqual(prevConns, nextConns)
|
||||||
&& reducedConcreteSyntaxEqual(prevState!, nextState!);
|
&& reducedConcreteSyntaxEqual(prevState, nextState);
|
||||||
});
|
});
|
||||||
const ast = parsed && parsed[0];
|
const ast = parsed && parsed[0];
|
||||||
|
|
||||||
|
|
@ -78,7 +89,7 @@ export function App() {
|
||||||
const persist = useUrlHashState<VisualEditorState | AppState & {editorState: VisualEditorState}>(
|
const persist = useUrlHashState<VisualEditorState | AppState & {editorState: VisualEditorState}>(
|
||||||
recoveredState => {
|
recoveredState => {
|
||||||
if (recoveredState === null) {
|
if (recoveredState === null) {
|
||||||
setEditHistory(() => ({current: emptyState, history: [], future: []}));
|
setEditHistory(() => ({current: initialEditorState, history: [], future: []}));
|
||||||
}
|
}
|
||||||
// we support two formats
|
// we support two formats
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -161,7 +172,7 @@ export function App() {
|
||||||
<div className="stackHorizontal" style={{flexGrow:1, overflow: "auto"}}>
|
<div className="stackHorizontal" style={{flexGrow:1, overflow: "auto"}}>
|
||||||
|
|
||||||
{/* top-to-bottom: top bar, editor */}
|
{/* top-to-bottom: top bar, editor */}
|
||||||
<div className="stackVertical" style={{flexGrow:1, overflow: "auto"}}>
|
<div className="stackVertical" style={{flexGrow:1, overflow: "hidden"}}>
|
||||||
{/* Top bar */}
|
{/* Top bar */}
|
||||||
<div
|
<div
|
||||||
className="shadowBelow"
|
className="shadowBelow"
|
||||||
|
|
@ -174,12 +185,12 @@ export function App() {
|
||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
<div style={{flexGrow: 1, overflow: "auto"}}>
|
<div style={{flexGrow: 1, overflow: "auto"}}>
|
||||||
{editorState && conns && syntaxErrors &&
|
{editorState && conns && syntaxErrors &&
|
||||||
<VisualEditor {...{state: editorState, commitState, replaceState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, ...appState}}/>}
|
<VisualEditor {...{state: editorState, commitState, replaceState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, ...appState, findText:findText}}/>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{appState.showFindReplace &&
|
{editorState && appState.showFindReplace &&
|
||||||
<div>
|
<div style={{}}>
|
||||||
<FindReplace setCS={setEditorState} hide={() => setters.setShowFindReplace(false)}/>
|
<FindReplace findText={findText} replaceText={replaceText} setFindReplaceText={setFindReplaceText} cs={editorState} setCS={setEditorState} hide={() => setters.setShowFindReplace(false)}/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
.errorStatus {
|
.statusBar {
|
||||||
/* background-color: rgb(230,0,0); */
|
background-color: var(--statusbar-bg-color);
|
||||||
background-color: var(--error-color);
|
color: var(--statusbar-fg-color);
|
||||||
color: var(--background-color);
|
}
|
||||||
|
|
||||||
|
.statusBar.error {
|
||||||
|
background-color: var(--statusbar-error-bg-color);
|
||||||
|
color: var(--statusbar-error-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBar summary:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--statusbar-bg-color) 60%, var(--background-color));
|
||||||
|
}
|
||||||
|
.statusBar.error summary:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--statusbar-error-bg-color) 70%, var(--text-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBar a {
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.greeter {
|
.greeter {
|
||||||
/* border-top: 1px var(--separator-color) solid; */
|
/* border-top: 1px var(--separator-color) solid; */
|
||||||
background-color: var(--greeter-bg-color);
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import "./BottomPanel.css";
|
||||||
import { PersistentDetailsLocalStorage } from "../Components/PersistentDetails";
|
import { PersistentDetailsLocalStorage } from "../Components/PersistentDetails";
|
||||||
import { Logo } from "@/App/Logo/Logo";
|
import { Logo } from "@/App/Logo/Logo";
|
||||||
import { AppState } from "../App";
|
import { AppState } from "../App";
|
||||||
import { FindReplace } from "./FindReplace";
|
|
||||||
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
||||||
import { Setters } from "../makePartialSetter";
|
import { Setters } from "../makePartialSetter";
|
||||||
|
|
||||||
|
|
@ -14,9 +13,10 @@ import gitRev from "@/git-rev.txt";
|
||||||
|
|
||||||
export function BottomPanel(props: {errors: TraceableError[], setEditorState: Dispatch<(state: VisualEditorState) => VisualEditorState>} & AppState & Setters<AppState>) {
|
export function BottomPanel(props: {errors: TraceableError[], setEditorState: Dispatch<(state: VisualEditorState) => VisualEditorState>} & AppState & Setters<AppState>) {
|
||||||
const [greeting, setGreeting] = useState(
|
const [greeting, setGreeting] = useState(
|
||||||
<div className="greeter" style={{textAlign:'center'}}>
|
<div className="greeter" style={{textAlign:'center'}} onClick={() => setGreeting(<></>)}>
|
||||||
<span style={{fontSize: 18, fontStyle: 'italic'}}>
|
<span style={{fontSize: 18, fontStyle: 'italic'}}>
|
||||||
Welcome to <Logo/>
|
Welcome to
|
||||||
|
<Logo width={250} height="auto" style={{verticalAlign: 'middle'}}/>
|
||||||
</span>
|
</span>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
||||||
|
|
@ -26,16 +26,18 @@ export function BottomPanel(props: {errors: TraceableError[], setEditorState: Di
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <div className="toolbar bottom">
|
return <div className="bottom">
|
||||||
|
{greeting}
|
||||||
{/* {props.showFindReplace &&
|
{/* {props.showFindReplace &&
|
||||||
<div>
|
<div>
|
||||||
<FindReplace setCS={props.setEditorState} hide={() => props.setShowFindReplace(false)}/>
|
<FindReplace setCS={props.setEditorState} hide={() => props.setShowFindReplace(false)}/>
|
||||||
</div>
|
</div>
|
||||||
} */}
|
} */}
|
||||||
<div className={"statusBar" + props.errors.length ? " error" : ""}>
|
<div className={"stackHorizontal statusBar" + (props.errors.length ? " error" : "")}>
|
||||||
|
<div style={{flexGrow:1}}>
|
||||||
<PersistentDetailsLocalStorage initiallyOpen={false} localStorageKey="errorsExpanded">
|
<PersistentDetailsLocalStorage initiallyOpen={false} localStorageKey="errorsExpanded">
|
||||||
<summary>{props.errors.length} errors</summary>
|
<summary>{props.errors.length} errors</summary>
|
||||||
<div style={{maxHeight: '25vh', overflow: 'auto'}}>
|
<div style={{maxHeight: '20vh', overflow: 'auto'}}>
|
||||||
{props.errors.map(({message, shapeUid})=>
|
{props.errors.map(({message, shapeUid})=>
|
||||||
<div>
|
<div>
|
||||||
{shapeUid}: {message}
|
{shapeUid}: {message}
|
||||||
|
|
@ -43,6 +45,16 @@ export function BottomPanel(props: {errors: TraceableError[], setEditorState: Di
|
||||||
</div>
|
</div>
|
||||||
</PersistentDetailsLocalStorage>
|
</PersistentDetailsLocalStorage>
|
||||||
</div>
|
</div>
|
||||||
{greeting}
|
<div style={{display: 'flex', alignItems: 'center'}}>
|
||||||
|
switch to
|
||||||
|
{location.host === "localhost:3000" ?
|
||||||
|
<a href={`https://deemz.org/public/statebuddy/${location.hash}`}>production</a>
|
||||||
|
: <a href={`http://localhost:3000/${location.hash}`}>development</a>
|
||||||
|
}
|
||||||
|
mode
|
||||||
|
|
|
||||||
|
Rev: <a title={"git"} href={`https://deemz.org/git/research/statebuddy/commit/${gitRev}`}>{gitRev.slice(0,8)}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,82 @@
|
||||||
import { Dispatch, useCallback, useEffect } from "react";
|
import { Dispatch, FormEvent, SetStateAction, useCallback } from "react";
|
||||||
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
||||||
import { usePersistentState } from "@/hooks/usePersistentState";
|
|
||||||
|
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
import SwapVertIcon from '@mui/icons-material/SwapVert';
|
||||||
import { useShortcuts } from "@/hooks/useShortcuts";
|
|
||||||
|
|
||||||
type FindReplaceProps = {
|
type FindReplaceProps = {
|
||||||
|
findText: string,
|
||||||
|
replaceText: string,
|
||||||
|
setFindReplaceText: Dispatch<SetStateAction<[string, string]>>,
|
||||||
|
cs: VisualEditorState,
|
||||||
setCS: Dispatch<(oldState: VisualEditorState) => VisualEditorState>,
|
setCS: Dispatch<(oldState: VisualEditorState) => VisualEditorState>,
|
||||||
// setModal: (modal: null) => void;
|
|
||||||
hide: () => void,
|
hide: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FindReplace({setCS, hide}: FindReplaceProps) {
|
export function FindReplace({findText, replaceText, setFindReplaceText, cs, setCS, hide}: FindReplaceProps) {
|
||||||
const [findTxt, setFindText] = usePersistentState("findTxt", "");
|
|
||||||
const [replaceTxt, setReplaceTxt] = usePersistentState("replaceTxt", "");
|
|
||||||
|
|
||||||
const onReplace = useCallback(() => {
|
const onReplace = useCallback(() => {
|
||||||
setCS(cs => {
|
setCS(cs => {
|
||||||
return {
|
return {
|
||||||
...cs,
|
...cs,
|
||||||
texts: cs.texts.map(txt => ({
|
texts: cs.texts.map(txt => ({
|
||||||
...txt,
|
...txt,
|
||||||
text: txt.text.replaceAll(findTxt, replaceTxt)
|
text: txt.text.replaceAll(findText, replaceText)
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [findTxt, replaceTxt]);
|
}, [findText, replaceText, setCS]);
|
||||||
|
|
||||||
useShortcuts([
|
|
||||||
{keys: ["Enter"], action: onReplace},
|
|
||||||
])
|
|
||||||
|
|
||||||
const onSwap = useCallback(() => {
|
const onSwap = useCallback(() => {
|
||||||
setReplaceTxt(findTxt);
|
setFindReplaceText(([findText, replaceText]) => [replaceText, findText]);
|
||||||
setFindText(replaceTxt);
|
}, [findText, replaceText]);
|
||||||
}, [findTxt, replaceTxt]);
|
|
||||||
|
|
||||||
return <div className="toolbar toolbarGroup" style={{display: 'flex'}}>
|
const onSubmit = useCallback((e: FormEvent<HTMLFormElement>) => {
|
||||||
<input placeholder="find" value={findTxt} onChange={e => setFindText(e.target.value)} style={{width:300}}/>
|
e.preventDefault();
|
||||||
<button tabIndex={-1} onClick={onSwap}><SwapHorizIcon fontSize="small"/></button>
|
e.stopPropagation();
|
||||||
<input tabIndex={0} placeholder="replace" value={replaceTxt} onChange={(e => setReplaceTxt(e.target.value))} style={{width:300}}/>
|
onReplace();
|
||||||
|
// onSwap();
|
||||||
<button onClick={onReplace}>replace all</button>
|
}, [findText, replaceText, onSwap, onReplace]);
|
||||||
<button onClick={hide} style={{marginLeft: 'auto'}}><CloseIcon fontSize="small"/></button>
|
|
||||||
</div>;
|
const n = findText === "" ? 0 : cs.texts.reduce((count, txt) => count+(txt.text.indexOf(findText) !== -1 ? 1: 0), 0);
|
||||||
|
|
||||||
|
return <form onSubmit={onSubmit}>
|
||||||
|
<div className="toolbar toolbarGroup" style={{display: 'flex', flexDirection: 'row'}}>
|
||||||
|
<div style={{flexGrow:1, display: 'flex', flexDirection: 'column'}}>
|
||||||
|
<input placeholder="find"
|
||||||
|
title="old text"
|
||||||
|
value={findText}
|
||||||
|
onChange={e => setFindReplaceText(([_, replaceText]) => [e.target.value, replaceText])}
|
||||||
|
style={{flexGrow: 1, minWidth: 20}}/>
|
||||||
|
<br/>
|
||||||
|
<input tabIndex={0} placeholder="replace"
|
||||||
|
title="new text"
|
||||||
|
value={replaceText}
|
||||||
|
onChange={(e => setFindReplaceText(([findText, _]) => [findText, e.target.value]))}
|
||||||
|
style={{flexGrow: 1, minWidth: 20}}/>
|
||||||
|
</div>
|
||||||
|
<div style={{flex: '0 0 content', display: 'flex', justifyItems: 'flex-start', flexDirection: 'column'}}>
|
||||||
|
<div style={{display: 'flex'}}>
|
||||||
|
<button
|
||||||
|
type="button" // <-- prevent form submission on click
|
||||||
|
title="swap find/replace fields"
|
||||||
|
onClick={onSwap}
|
||||||
|
style={{flexGrow: 1}}>
|
||||||
|
<SwapVertIcon fontSize="small"/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button" // <-- prevent form submission on click
|
||||||
|
title="hide find & replace"
|
||||||
|
onClick={hide}
|
||||||
|
style={{flexGrow: 1 }}>
|
||||||
|
<CloseIcon fontSize="small"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="submit"
|
||||||
|
disabled={n===0}
|
||||||
|
title="replace all occurrences in model"
|
||||||
|
value={`replace all (${n})`}
|
||||||
|
style={{height: 26}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>;
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { SVGAttributes, SVGProps } from "react";
|
||||||
|
|
||||||
// i couldn't find a better way to make the text in the logo adapt to light/dark mode...
|
// i couldn't find a better way to make the text in the logo adapt to light/dark mode...
|
||||||
export function Logo() {
|
export function Logo(props: SVGAttributes<SVGElement>) {
|
||||||
return <svg style={{maxWidth: '100%'}} width="424.29" height="105.72" version="1.1" viewBox="0 0 424.29 105.72">
|
return <svg style={{maxWidth: '100%'}} width="424.29" height="105.72" version="1.1" viewBox="0 0 424.29 105.72" {...props} >
|
||||||
<style>{`
|
<style>{`
|
||||||
.logoText {
|
.logoText {
|
||||||
fill: var(--text-color);
|
fill: var(--text-color);
|
||||||
|
|
|
||||||
|
|
@ -70,14 +70,15 @@ export const ShowInputEvents = memo(function ShowInputEvents({inputEvents, onRai
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const shortcutSpec = raiseHandlers.map((handler, i) => {
|
// less painful and more readable than figuring out the equivalent of range(n) in JS:
|
||||||
|
// (btw, useShortcuts must always be called with an array of the same size)
|
||||||
|
useShortcuts([0,1,2,3,4,5,6,7,8,9].map(i => {
|
||||||
const n = (i+1) % 10;
|
const n = (i+1) % 10;
|
||||||
return {
|
return {
|
||||||
keys: [n.toString()],
|
keys: [n.toString()],
|
||||||
action: handler,
|
action: raiseHandlers[n] || (() => {}),
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
useShortcuts(shortcutSpec);
|
|
||||||
|
|
||||||
const KeyInfo = KeyInfoVisible; // always show keyboard shortcuts on input events, we can't expect the user to remember them
|
const KeyInfo = KeyInfoVisible; // always show keyboard shortcuts on input events, we can't expect the user to remember them
|
||||||
|
|
||||||
|
|
@ -107,7 +108,6 @@ export const ShowInputEvents = memo(function ShowInputEvents({inputEvents, onRai
|
||||||
</div>;
|
</div>;
|
||||||
})
|
})
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
console.log('onRaise changed:', prevProps.onRaise === nextProps.onRaise, prevProps.onRaise, nextProps.onRaise);
|
|
||||||
return prevProps.onRaise === nextProps.onRaise
|
return prevProps.onRaise === nextProps.onRaise
|
||||||
&& prevProps.disabled === nextProps.disabled
|
&& prevProps.disabled === nextProps.disabled
|
||||||
&& jsonDeepEqual(prevProps.inputEvents, nextProps.inputEvents);
|
&& jsonDeepEqual(prevProps.inputEvents, nextProps.inputEvents);
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ export const SideBar = memo(function SideBar({showExecutionTrace, showConnection
|
||||||
<button title="see in trace (below)" className={activeProperty === i ? "active" : ""} onClick={() => setActiveProperty(i)}>
|
<button title="see in trace (below)" className={activeProperty === i ? "active" : ""} onClick={() => setActiveProperty(i)}>
|
||||||
<VisibilityIcon fontSize="small"/>
|
<VisibilityIcon fontSize="small"/>
|
||||||
</button>
|
</button>
|
||||||
<input type="text" style={{width:'calc(100% - 90px)'}} value={property} onChange={e => setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/>
|
<input type="text" style={{width:'calc(100% - 90px)'}} value={property} onChange={e => setProperties(properties => properties.toSpliced(i, 1, e.target.value))} placeholder='write MTL property...'/>
|
||||||
<button title="delete this property" onClick={() => setProperties(properties => properties.toSpliced(i, 1))}>
|
<button title="delete this property" onClick={() => setProperties(properties => properties.toSpliced(i, 1))}>
|
||||||
<DeleteOutlineIcon fontSize="small"/>
|
<DeleteOutlineIcon fontSize="small"/>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -246,11 +246,5 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="toolbarGroup">
|
|
||||||
{location.host === "localhost:3000" ?
|
|
||||||
<a href={`https://deemz.org/public/statebuddy/${location.hash}`}>production</a>
|
|
||||||
: <a href={`http://localhost:3000/${location.hash}`}>development</a>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: ArrowPart
|
||||||
data-parts="start end" />
|
data-parts="start end" />
|
||||||
|
|
||||||
{props.error && <text
|
{props.error && <text
|
||||||
className="error"
|
className="errorHover"
|
||||||
x={(start.x + end.x) / 2 + 5}
|
x={(start.x + end.x) / 2 + 5}
|
||||||
y={(start.y + end.y) / 2}
|
y={(start.y + end.y) / 2}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rou
|
||||||
<text x={10} y={20} className="uid">{props.rountangle.uid}</text>
|
<text x={10} y={20} className="uid">{props.rountangle.uid}</text>
|
||||||
|
|
||||||
{props.error &&
|
{props.error &&
|
||||||
<text className="error" x={10} y={40} data-uid={uid} data-parts="left top right bottom">{props.error}</text>}
|
<text className="errorHover" x={30} y={20} data-uid={uid} data-parts="left top right bottom">{props.error}</text>}
|
||||||
|
|
||||||
<RectHelper uid={uid} size={minSize}
|
<RectHelper uid={uid} size={minSize}
|
||||||
selected={props.selected}
|
selected={props.selected}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,46 @@
|
||||||
import { TextDialog } from "@/App/Modals/TextDialog";
|
import { TextDialog } from "@/App/Modals/TextDialog";
|
||||||
import { TraceableError } from "../../statecharts/parser";
|
import { TraceableError } from "../../statecharts/parser";
|
||||||
import {Text} from "../../statecharts/concrete_syntax";
|
import {Text} from "../../statecharts/concrete_syntax";
|
||||||
import { Dispatch, memo, ReactElement, SetStateAction } from "react";
|
import { Dispatch, memo, ReactElement, SetStateAction, SVGTextElementAttributes } from "react";
|
||||||
import { jsonDeepEqual } from "@/util/util";
|
import { jsonDeepEqual } from "@/util/util";
|
||||||
|
|
||||||
export const TextSVG = memo(function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
|
export const FragmentedText = function FragmentedText({start, end, text, highlightClassName, uid, parts, ...rest}: {start: number, end: number, text: string, highlightClassName: string, uid: string, parts: string} & SVGTextElementAttributes<SVGTextElement>) {
|
||||||
const commonProps = {
|
if (start !== -1 && start !== end) {
|
||||||
"data-uid": props.text.uid,
|
return <text data-uid={uid} data-parts={parts} {...rest}>
|
||||||
"data-parts": "text",
|
{text.slice(0, start)}
|
||||||
textAnchor: "middle" as "middle",
|
<tspan className={highlightClassName} data-uid={uid} data-parts="text">
|
||||||
className: "draggableText"
|
{text.slice(start, end)}
|
||||||
+ (props.selected ? " selected":"")
|
|
||||||
+ (props.highlight ? " highlight":""),
|
|
||||||
style: {whiteSpace: "preserve"},
|
|
||||||
}
|
|
||||||
|
|
||||||
let textNode;
|
|
||||||
if (props.error?.data?.location) {
|
|
||||||
const {start,end} = props.error.data.location;
|
|
||||||
textNode = <><text {...commonProps}>
|
|
||||||
{props.text.text.slice(0, start.offset)}
|
|
||||||
<tspan className="error" data-uid={props.text.uid} data-parts="text">
|
|
||||||
{props.text.text.slice(start.offset, end.offset)}
|
|
||||||
{start.offset === end.offset && <>_</>}
|
|
||||||
</tspan>
|
</tspan>
|
||||||
{props.text.text.slice(end.offset)}
|
{text.slice(end)}
|
||||||
</text>
|
</text>;
|
||||||
<text className="error errorHover" y={20} textAnchor="middle">{props.error.message}</text></>;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
textNode = <text {...commonProps}>{props.text.text}</text>;
|
return <text data-uid={uid} data-parts={parts} {...rest}>
|
||||||
|
{text}
|
||||||
|
</text>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextSVG = memo(function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>, findText: string}) {
|
||||||
|
|
||||||
|
const className = "draggableText"
|
||||||
|
+ (props.selected ? " selected":"")
|
||||||
|
+ (props.highlight ? " highlight":"")
|
||||||
|
+ (props.error ? " error":"");
|
||||||
|
|
||||||
|
const found = props.text.text.indexOf(props.findText);
|
||||||
|
const start = (found >= 0) ? found : -1
|
||||||
|
const end = (found >= 0) ? found + props.findText.length : -1;
|
||||||
|
|
||||||
|
const textNode = <FragmentedText
|
||||||
|
{...{start, end}}
|
||||||
|
text={props.text.text}
|
||||||
|
textAnchor="middle"
|
||||||
|
className={className}
|
||||||
|
highlightClassName="findText"
|
||||||
|
uid={props.text.uid}
|
||||||
|
parts="text"
|
||||||
|
/>;
|
||||||
|
|
||||||
return <g
|
return <g
|
||||||
key={props.text.uid}
|
key={props.text.uid}
|
||||||
|
|
@ -44,6 +54,9 @@ export const TextSVG = memo(function TextSVG(props: {text: Text, error: Traceabl
|
||||||
}}>
|
}}>
|
||||||
{textNode}
|
{textNode}
|
||||||
<text className="draggableText helper" textAnchor="middle" data-uid={props.text.uid} data-parts="text" style={{whiteSpace: "preserve"}}>{props.text.text}</text>
|
<text className="draggableText helper" textAnchor="middle" data-uid={props.text.uid} data-parts="text" style={{whiteSpace: "preserve"}}>{props.text.text}</text>
|
||||||
|
{props.error &&
|
||||||
|
<text className="errorHover" y={-20} textAnchor="middle">{props.error.message}</text>
|
||||||
|
}
|
||||||
</g>;
|
</g>;
|
||||||
}, (prevProps, newProps) => {
|
}, (prevProps, newProps) => {
|
||||||
return jsonDeepEqual(prevProps.text, newProps)
|
return jsonDeepEqual(prevProps.text, newProps)
|
||||||
|
|
@ -52,4 +65,5 @@ export const TextSVG = memo(function TextSVG(props: {text: Text, error: Traceabl
|
||||||
&& prevProps.setModal === newProps.setModal
|
&& prevProps.setModal === newProps.setModal
|
||||||
&& prevProps.error === newProps.error
|
&& prevProps.error === newProps.error
|
||||||
&& prevProps.selected === newProps.selected
|
&& prevProps.selected === newProps.selected
|
||||||
|
&& prevProps.findText === newProps.findText
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -112,10 +112,6 @@ line.selected, circle.selected {
|
||||||
stroke-width: 4px;
|
stroke-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableText.selected, .draggableText.selected:hover {
|
|
||||||
fill: var(--accent-border-color);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
text.helper {
|
text.helper {
|
||||||
fill: rgba(0,0,0,0);
|
fill: rgba(0,0,0,0);
|
||||||
stroke: rgba(0,0,0,0);
|
stroke: rgba(0,0,0,0);
|
||||||
|
|
@ -126,24 +122,41 @@ text.helper:hover {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableText, .draggableText.highlight {
|
.draggableText {
|
||||||
paint-order: stroke;
|
paint-order: stroke;
|
||||||
fill: var(--text-color);
|
fill: var(--text-color);
|
||||||
stroke: var(--background-color);
|
stroke: var(--background-color);
|
||||||
stroke-width: 4px;
|
stroke-width: 4px;
|
||||||
stroke-linecap: butt;
|
text-anchor: middle;
|
||||||
stroke-linejoin: miter;
|
white-space: preserve;
|
||||||
stroke-opacity: 1;
|
|
||||||
fill-opacity:1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableText.highlight:not(.selected) {
|
|
||||||
fill: var(--associated-color);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draggableText.selected {
|
.draggableText.selected {
|
||||||
fill: var(--accent-border-color);
|
fill: var(--accent-border-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.draggableText.highlight:not(.selected) {
|
||||||
|
fill: var(--associated-color);
|
||||||
|
}
|
||||||
|
.draggableText.error:not(.selected) {
|
||||||
|
fill: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
text.errorHover {
|
||||||
|
display: none;
|
||||||
|
paint-order: stroke;
|
||||||
|
fill: var(--error-color);
|
||||||
|
stroke: var(--background-color);
|
||||||
|
stroke-width: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: preserve;
|
||||||
|
}
|
||||||
|
g:hover > text.errorHover {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
tspan.findText {
|
||||||
|
fill: var(--foundtext-color);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight:not(.selected):not(text) {
|
.highlight:not(.selected):not(text) {
|
||||||
|
|
@ -152,7 +165,7 @@ text.helper:hover {
|
||||||
fill: none;
|
fill: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.error {
|
.arrow.error:not(.selected) {
|
||||||
stroke: var(--error-color);
|
stroke: var(--error-color);
|
||||||
}
|
}
|
||||||
.arrow.fired {
|
.arrow.fired {
|
||||||
|
|
@ -172,19 +185,13 @@ text.helper:hover {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* .errorHover {
|
||||||
text.error, tspan.error {
|
|
||||||
fill: var(--error-color);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorHover {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
g:hover > .errorHover {
|
g:hover > .errorHover {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
} */
|
||||||
|
|
||||||
text.uid {
|
text.uid {
|
||||||
fill: var(--separator-color);
|
fill: var(--separator-color);
|
||||||
|
|
|
||||||
|
|
@ -52,9 +52,10 @@ type VisualEditorProps = {
|
||||||
highlightTransitions: string[],
|
highlightTransitions: string[],
|
||||||
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
||||||
zoom: number;
|
zoom: number;
|
||||||
|
findText: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VisualEditor = memo(function VisualEditor({state, commitState, replaceState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, zoom}: VisualEditorProps) {
|
export const VisualEditor = memo(function VisualEditor({state, commitState, replaceState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, zoom, findText}: VisualEditorProps) {
|
||||||
|
|
||||||
// While dragging, the editor is in a temporary state (a state that is not committed to the edit history). If the temporary state is not null, then this state will be what you see.
|
// While dragging, the editor is in a temporary state (a state that is not committed to the edit history). If the temporary state is not null, then this state will be what you see.
|
||||||
// const [temporaryState, setTemporaryState] = useState<VisualEditorState | null>(null);
|
// const [temporaryState, setTemporaryState] = useState<VisualEditorState | null>(null);
|
||||||
|
|
@ -199,8 +200,6 @@ export const VisualEditor = memo(function VisualEditor({state, commitState, repl
|
||||||
</marker>
|
</marker>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
{(rootErrors.length>0) && <text className="error" x={5} y={20}>{rootErrors.join(' ')}</text>}
|
|
||||||
|
|
||||||
<Rountangles rountangles={state.rountangles} {...{selection, sidesToHighlight, rountanglesToHighlight, errors, highlightActive}}/>
|
<Rountangles rountangles={state.rountangles} {...{selection, sidesToHighlight, rountanglesToHighlight, errors, highlightActive}}/>
|
||||||
<Diamonds diamonds={state.diamonds} {...{selection, sidesToHighlight, rountanglesToHighlight, errors}}/>
|
<Diamonds diamonds={state.diamonds} {...{selection, sidesToHighlight, rountanglesToHighlight, errors}}/>
|
||||||
|
|
||||||
|
|
@ -235,7 +234,9 @@ export const VisualEditor = memo(function VisualEditor({state, commitState, repl
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Texts texts={state.texts} {...{selection, textsToHighlight, errors, onEditText, setModal}}/>
|
<Texts texts={state.texts} {...{selection, textsToHighlight, errors, onEditText, setModal, findText}}/>
|
||||||
|
|
||||||
|
{(rootErrors.length>0) && <text className="errorHover" x={5} y={20} style={{display:'inline'}}>{rootErrors.join('\n')}</text>}
|
||||||
|
|
||||||
{selectionRect}
|
{selectionRect}
|
||||||
</svg>;
|
</svg>;
|
||||||
|
|
@ -282,7 +283,7 @@ const Diamonds = memo(function Diamonds({diamonds, selection, sidesToHighlight,
|
||||||
&& arraysEqual(p.errors, n.errors);
|
&& arraysEqual(p.errors, n.errors);
|
||||||
});
|
});
|
||||||
|
|
||||||
const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, onEditText, setModal}: {texts: Text[], selection: Selection, textsToHighlight: {[key: string]: boolean}, errors: TraceableError[], onEditText: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
|
const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, onEditText, setModal, findText}: {texts: Text[], selection: Selection, textsToHighlight: {[key: string]: boolean}, errors: TraceableError[], onEditText: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>, findText: string}) {
|
||||||
return <>{texts.map(txt => {
|
return <>{texts.map(txt => {
|
||||||
return <TextSVG
|
return <TextSVG
|
||||||
key={txt.uid}
|
key={txt.uid}
|
||||||
|
|
@ -292,6 +293,7 @@ const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, o
|
||||||
highlight={textsToHighlight.hasOwnProperty(txt.uid)}
|
highlight={textsToHighlight.hasOwnProperty(txt.uid)}
|
||||||
onEdit={onEditText}
|
onEdit={onEditText}
|
||||||
setModal={setModal}
|
setModal={setModal}
|
||||||
|
findText={findText}
|
||||||
/>
|
/>
|
||||||
})}</>;
|
})}</>;
|
||||||
}, (p, n) => {
|
}, (p, n) => {
|
||||||
|
|
@ -300,6 +302,7 @@ const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, o
|
||||||
&& objectsEqual(p.textsToHighlight, n.textsToHighlight)
|
&& objectsEqual(p.textsToHighlight, n.textsToHighlight)
|
||||||
&& arraysEqual(p.errors, n.errors)
|
&& arraysEqual(p.errors, n.errors)
|
||||||
&& p.onEditText === n.onEditText
|
&& p.onEditText === n.onEditText
|
||||||
&& p.setModal === n.setModal;
|
&& p.setModal === n.setModal
|
||||||
|
&& p.findText === n.findText;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ export function useMouse(
|
||||||
...t,
|
...t,
|
||||||
topLeft: addV2D(t.topLeft, pointerDelta),
|
topLeft: addV2D(t.topLeft, pointerDelta),
|
||||||
}
|
}
|
||||||
}),
|
}).toSorted((a,b) => a.topLeft.y - b.topLeft.y),
|
||||||
}));
|
}));
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,6 @@ export function useSimulator(ast: Statechart|null, plant: Plant<any, UniversalPl
|
||||||
};
|
};
|
||||||
}, [cE, currentTraceItem, time, appendNewConfig]);
|
}, [cE, currentTraceItem, time, appendNewConfig]);
|
||||||
|
|
||||||
console.log({onRaise});
|
|
||||||
|
|
||||||
// timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
|
// timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log('time effect:', time, currentTraceItem);
|
// console.log('time effect:', time, currentTraceItem);
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ import { App } from "./App/App";
|
||||||
|
|
||||||
const elem = document.getElementById("root")!;
|
const elem = document.getElementById("root")!;
|
||||||
const app = (
|
const app = (
|
||||||
// <StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
// </StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Dispatch, SetStateAction, useCallback, useState } from "react";
|
||||||
// like useState, but it is persisted in localStorage
|
// like useState, but it is persisted in localStorage
|
||||||
// important: values must be JSON-(de-)serializable
|
// important: values must be JSON-(de-)serializable
|
||||||
export function usePersistentState<T>(key: string, initial: T): [T, Dispatch<SetStateAction<T>>] {
|
export function usePersistentState<T>(key: string, initial: T): [T, Dispatch<SetStateAction<T>>] {
|
||||||
|
console.log('usePersistentState ---', key);
|
||||||
const [state, setState] = useState(() => {
|
const [state, setState] = useState(() => {
|
||||||
const recovered = localStorage.getItem(key);
|
const recovered = localStorage.getItem(key);
|
||||||
let parsed;
|
let parsed;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ html, body {
|
||||||
--light-accent-color: light-dark(rgba(0,0,255,0.2), rgba(78, 186, 248, 0.377));
|
--light-accent-color: light-dark(rgba(0,0,255,0.2), rgba(78, 186, 248, 0.377));
|
||||||
--accent-border-color: light-dark(blue, rgb(64, 185, 255));
|
--accent-border-color: light-dark(blue, rgb(64, 185, 255));
|
||||||
--accent-opaque-color: light-dark(#ccccff, #305b73);
|
--accent-opaque-color: light-dark(#ccccff, #305b73);
|
||||||
|
|
||||||
|
--statusbar-bg-color: light-dark(rgb(225, 229, 235), rgb(48, 48, 68));
|
||||||
|
--statusbar-fg-color: light-dark(rgb(0, 0, 0), white);
|
||||||
|
--statusbar-error-bg-color: light-dark(rgb(163, 0, 0), rgb(255, 82, 82));
|
||||||
|
--statusbar-error-fg-color: light-dark(white, black);
|
||||||
|
|
||||||
--separator-color: light-dark(lightgrey, rgb(58, 58, 58));
|
--separator-color: light-dark(lightgrey, rgb(58, 58, 58));
|
||||||
--inactive-bg-color: light-dark(#f7f7f7, rgb(29, 29, 29));
|
--inactive-bg-color: light-dark(#f7f7f7, rgb(29, 29, 29));
|
||||||
--inactive-fg-color: light-dark(grey, rgb(70, 70, 70));
|
--inactive-fg-color: light-dark(grey, rgb(70, 70, 70));
|
||||||
|
|
@ -40,6 +46,7 @@ html, body {
|
||||||
--input-event-hover-bg-color: light-dark(rgb(195, 224, 176), rgb(59, 88, 40));
|
--input-event-hover-bg-color: light-dark(rgb(195, 224, 176), rgb(59, 88, 40));
|
||||||
--input-event-active-bg-color: light-dark(rgb(176, 204, 158), rgb(77, 117, 53));
|
--input-event-active-bg-color: light-dark(rgb(176, 204, 158), rgb(77, 117, 53));
|
||||||
--output-event-bg-color: light-dark(rgb(230, 249, 255), rgb(28, 83, 104));
|
--output-event-bg-color: light-dark(rgb(230, 249, 255), rgb(28, 83, 104));
|
||||||
|
--foundtext-color: light-dark(rgb(0, 189, 164), rgb(202, 149, 218));
|
||||||
|
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
|
@ -50,16 +57,16 @@ input {
|
||||||
border: 1px solid var(--separator-color);
|
border: 1px solid var(--separator-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button, input[type="submit"] {
|
||||||
background-color: var(--button-bg-color);
|
background-color: var(--button-bg-color);
|
||||||
border: 1px var(--separator-color) solid;
|
border: 1px var(--separator-color) solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not(:disabled):hover {
|
button:not(:disabled):hover, input[type="submit"]:not(:disabled):hover {
|
||||||
background-color: var(--light-accent-color);
|
background-color: var(--light-accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled, input[type="submit"]:disabled {
|
||||||
background-color: var(--inactive-bg-color);
|
background-color: var(--inactive-bg-color);
|
||||||
color: var(--inactive-fg-color);
|
color: var(--inactive-fg-color);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,23 @@ export type ConcreteSyntax = {
|
||||||
export type RectSide = "left" | "top" | "right" | "bottom";
|
export type RectSide = "left" | "top" | "right" | "bottom";
|
||||||
export type ArrowPart = "start" | "end";
|
export type ArrowPart = "start" | "end";
|
||||||
|
|
||||||
export const emptyState: VisualEditorState = {
|
export const initialEditorState: VisualEditorState = {
|
||||||
rountangles: [], texts: [], arrows: [], diamonds: [], history: [], nextID: 0, selection: [],
|
rountangles: [{
|
||||||
|
uid:"0",
|
||||||
|
topLeft:{x:76.25,y:122.5},
|
||||||
|
size:{x:133.75,y:103.75},
|
||||||
|
kind:"and"
|
||||||
|
}],
|
||||||
|
diamonds:[],
|
||||||
|
history:[],
|
||||||
|
arrows:[{
|
||||||
|
uid:"39",
|
||||||
|
start:{x:85,y:67.5},
|
||||||
|
end:{x:116.25,y:116.25}
|
||||||
|
}],
|
||||||
|
texts:[],
|
||||||
|
nextID: 1,
|
||||||
|
selection: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// used to find which rountangle an arrow connects to (src/tgt)
|
// used to find which rountangle an arrow connects to (src/tgt)
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
||||||
else if (state.initial.length === 0) {
|
else if (state.initial.length === 0) {
|
||||||
errors.push({
|
errors.push({
|
||||||
shapeUid: state.uid,
|
shapeUid: state.uid,
|
||||||
message: "no initial state",
|
message: "needs initial state",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,8 +216,8 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
||||||
|
|
||||||
// step 3: figure out labels
|
// step 3: figure out labels
|
||||||
|
|
||||||
const textsSorted = concreteSyntax.texts.toSorted((a,b) => a.topLeft.y - b.topLeft.y);
|
// ASSUMPTION: text is sorted by y-coordinate
|
||||||
for (const text of textsSorted) {
|
for (const text of concreteSyntax.texts) {
|
||||||
let parsed: ParsedText;
|
let parsed: ParsedText;
|
||||||
try {
|
try {
|
||||||
parsed = cachedParseLabel(text.text); // may throw
|
parsed = cachedParseLabel(text.text); // may throw
|
||||||
|
|
@ -226,7 +226,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
||||||
if (e instanceof SyntaxError) {
|
if (e instanceof SyntaxError) {
|
||||||
errors.push({
|
errors.push({
|
||||||
shapeUid: text.uid,
|
shapeUid: text.uid,
|
||||||
message: e.message,
|
message: 'parser: ' + e.message,
|
||||||
data: e,
|
data: e,
|
||||||
});
|
});
|
||||||
parsed = {
|
parsed = {
|
||||||
|
|
@ -248,7 +248,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
||||||
// triggers
|
// triggers
|
||||||
if (parsed.trigger.kind === "event") {
|
if (parsed.trigger.kind === "event") {
|
||||||
if (src.kind === "pseudo") {
|
if (src.kind === "pseudo") {
|
||||||
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have event trigger"});
|
errors.push({shapeUid: text.uid, message: "cannot have trigger"});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const {event} = parsed.trigger;
|
const {event} = parsed.trigger;
|
||||||
|
|
@ -262,7 +262,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
||||||
}
|
}
|
||||||
else if (parsed.trigger.kind === "after") {
|
else if (parsed.trigger.kind === "after") {
|
||||||
if (src.kind === "pseudo") {
|
if (src.kind === "pseudo") {
|
||||||
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have after-trigger"});
|
errors.push({shapeUid: text.uid, message: "cannot have trigger"});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
src.timers.push(parsed.trigger.durationMs);
|
src.timers.push(parsed.trigger.durationMs);
|
||||||
|
|
@ -274,7 +274,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
||||||
}
|
}
|
||||||
else if (parsed.trigger.kind === "triggerless") {
|
else if (parsed.trigger.kind === "triggerless") {
|
||||||
if (src.kind !== "pseudo") {
|
if (src.kind !== "pseudo") {
|
||||||
errors.push({shapeUid: text.uid, message: "triggerless transitions only allowed on pseudo-states"});
|
errors.push({shapeUid: text.uid, message: "needs trigger"});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +296,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co
|
||||||
else {
|
else {
|
||||||
errors.push({
|
errors.push({
|
||||||
shapeUid: text.uid,
|
shapeUid: text.uid,
|
||||||
message: "states can only have entry/exit triggers",
|
message: "must belong to transition",
|
||||||
data: {start: {offset: 0}, end: {offset: text.text.length}},
|
data: {start: {offset: 0}, end: {offset: text.text.length}},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
todo.txt
3
todo.txt
|
|
@ -79,6 +79,9 @@ TODO
|
||||||
https://pub.dev/packages/ploeg_tree_layout
|
https://pub.dev/packages/ploeg_tree_layout
|
||||||
- local variable scopes
|
- local variable scopes
|
||||||
|
|
||||||
|
Breaking changes (after mosis25 is finished):
|
||||||
|
- state names start with # (not with //)
|
||||||
|
|
||||||
Publish StateBuddy paper(s):
|
Publish StateBuddy paper(s):
|
||||||
compare CS approach to other tools, not only YAKINDU
|
compare CS approach to other tools, not only YAKINDU
|
||||||
z
|
z
|
||||||
Loading…
Add table
Add a link
Reference in a new issue