highlight fired transitions

This commit is contained in:
Joeri Exelmans 2025-10-19 11:27:32 +02:00
parent b8bc977a8e
commit a10bf9acc8
6 changed files with 80 additions and 34 deletions

View file

@ -19,13 +19,10 @@ import { BottomPanel } from "./BottomPanel";
export function App() {
const [mode, setMode] = useState<InsertMode>("and");
const [ast, setAST] = useState<Statechart>(emptyStatechart);
const [errors, setErrors] = useState<TraceableError[]>([]);
const [rt, setRT] = useState<BigStep[]>([]);
const [rtIdx, setRTIdx] = useState<number|undefined>();
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
const refRightSideBar = useRef<HTMLDivElement>(null);
@ -54,17 +51,22 @@ export function App() {
function appendNewConfig(inputEvent: string, simtime: number, config: BigStepOutput) {
setRT([...rt.slice(0, rtIdx!+1), {inputEvent, simtime, ...config}]);
setRTIdx(rtIdx!+1);
// console.log('new config:', config);
console.log('new config:', config);
if (refRightSideBar.current) {
const el = refRightSideBar.current;
console.log('scrolling to:', el);
setTimeout(() => {
el.scrollIntoView({block: "end", behavior: "smooth"});
}, 100);
}
}
useEffect(() => {
console.log("Welcome to StateBuddy!");
() => {
console.log("Goodbye!");
}
}, []);
useEffect(() => {
let timeout: NodeJS.Timeout | undefined;
if (rtIdx !== undefined) {
@ -108,6 +110,8 @@ export function App() {
return state && state.parent?.kind !== "and";
})) || new Set();
const highlightTransitions = (rtIdx === undefined) ? [] : rt[rtIdx].firedTransitions;
return <Stack sx={{height:'100vh'}}>
{/* Top bar */}
<Box
@ -125,7 +129,7 @@ export function App() {
<Stack direction="row" sx={{height:'calc(100vh - 64px)'}}>
{/* main */}
<Box sx={{flexGrow:1, overflow:'auto'}}>
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive}}/>
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions}}/>
</Box>
{/* right sidebar */}
<Box

View file

@ -3,18 +3,25 @@ import { ArcDirection, euclideanDistance } from "./geometry";
import { CORNER_HELPER_RADIUS } from "./parameters";
export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: string[]; highlight: boolean; arc: ArcDirection; }) {
export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: string[]; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) {
const { start, end, uid } = props.arrow;
const radius = euclideanDistance(start, end) / 1.6;
const largeArc = "1";
const arcOrLine = props.arc === "no" ? "L" :
let largeArc = "1";
let arcOrLine = props.arc === "no" ? "L" :
`A ${radius} ${radius} 0 ${largeArc} ${props.arc === "ccw" ? "0" : "1"}`;
if (props.initialMarker) {
// largeArc = "0";
arcOrLine = `A ${radius*2} ${radius*2} 0 0 1`
}
return <g>
<path
className={"arrow"
+ (props.selected.length === 2 ? " selected" : "")
+ (props.errors.length > 0 ? " error" : "")
+ (props.highlight ? " highlight" : "")}
+ (props.highlight ? " highlight" : "")
+ (props.fired ? " fired" : "")
}
markerStart={props.initialMarker ? 'url(#initialMarker)' : undefined}
markerEnd='url(#arrowEnd)'
d={`M ${start.x} ${start.y}
${arcOrLine}

View file

@ -46,7 +46,11 @@
/* fill-opacity: 0.2; */
/* stroke: rgb(100, 149, 237); */
/* stroke: */
filter: drop-shadow( 0px 0px 6px rgba(128, 72, 0, 0.856));
stroke: rgb(192, 125, 0);
fill:rgb(255, 251, 244);
/* fill: lightgrey; */
/* color: white; */
filter: drop-shadow( 0px 0px 3px rgba(192, 125, 0, 0.856));
/* stroke-width: 3px; */
}
@ -99,9 +103,16 @@ circle.helper:hover:not(:active) {
stroke-width: 3px;
}
.arrow::marker {
fill: content-stroke;
}
#arrowEnd {
fill: context-stroke;
}
#initialMarker {
fill: context-stroke;
}
.arrow:hover {
cursor: grab;
@ -157,6 +168,11 @@ text.helper:hover {
.arrow.error {
stroke: rgb(230,0,0);
}
.arrow.fired {
stroke: rgb(192, 125, 0);
stroke-width: 3px;
}
text.error, tspan.error {
fill: rgb(230,0,0);
font-weight: 600;

View file

@ -68,9 +68,10 @@ type VisualEditorProps = {
setErrors: Dispatch<SetStateAction<TraceableError[]>>,
mode: InsertMode,
highlightActive: Set<string>,
highlightTransitions: string[],
};
export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highlightActive}: VisualEditorProps) {
export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highlightActive, highlightTransitions}: VisualEditorProps) {
const [historyState, setHistoryState] = useState<HistoryState>({current: emptyState, history: [], future: []});
const state = historyState.current;
@ -679,11 +680,8 @@ export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highligh
}
}
const active = rt?.mode || new Set();
console.log(highlightActive);
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
return <svg width="4000px" height="4000px"
@ -700,6 +698,16 @@ export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highligh
onCut={onCut}
>
<defs>
<marker
id="initialMarker"
viewBox="0 0 9 9"
refX="4.5"
refY="4.5"
markerWidth="9"
markerHeight="9"
markerUnits="userSpaceOnUse">
<circle cx={4.5} cy={4.5} r={4.5}/>
</marker>
<marker
id="arrowEnd"
viewBox="0 0 10 10"
@ -709,7 +717,7 @@ export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highligh
markerHeight="12"
orient="auto-start-reverse"
markerUnits="userSpaceOnUse">
<path d="M 0 0 L 10 5 L 0 10 z" />
<path d="M 0 0 L 10 5 L 0 10 z"/>
</marker>
</defs>
@ -752,6 +760,7 @@ export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highligh
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0]!.uid !== undefined) {
arc = arcDirection(sides[0]!.part, sides[1]!.part);
}
const initialMarker = sides && sides[0] === undefined && sides[1] !== undefined;
return <ArrowSVG
key={arrow.uid}
arrow={arrow}
@ -760,7 +769,9 @@ export function VisualEditor({ast, setAST, rt, errors, setErrors, mode, highligh
.filter(({shapeUid}) => shapeUid === arrow.uid)
.map(({message}) => message)}
highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)}
fired={highlightTransitions.includes(arrow.uid)}
arc={arc}
initialMarker={initialMarker}
/>;
}
)}

View file

@ -94,10 +94,10 @@ export function exitActions(simtime: number, state: ConcreteState, {enteredState
// recursively enter the given state's default state
export function enterDefault(simtime: number, state: ConcreteState, rt: ActionScope): EnteredScope {
let actionScope = rt;
let {firedTransitions, ...actionScope} = rt;
// execute entry actions
actionScope = entryActions(simtime, state, actionScope);
({firedTransitions, ...actionScope} = entryActions(simtime, state, {firedTransitions, ...actionScope}));
// enter children...
let enteredStates = new Set([state.uid]);
@ -105,7 +105,7 @@ export function enterDefault(simtime: number, state: ConcreteState, rt: ActionSc
// enter every child
for (const child of state.children) {
let enteredChildren;
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
({enteredStates: enteredChildren, firedTransitions, ...actionScope} = enterDefault(simtime, child, {firedTransitions, ...actionScope}));
enteredStates = enteredStates.union(enteredChildren);
}
}
@ -115,14 +115,16 @@ export function enterDefault(simtime: number, state: ConcreteState, rt: ActionSc
if (state.initial.length > 1) {
console.warn(state.uid + ': multiple initial states, only entering one of them');
}
const [arrowUid, toEnter] = state.initial[0];
firedTransitions = [...firedTransitions, arrowUid];
let enteredChildren;
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, state.initial[0][1], actionScope));
({enteredStates: enteredChildren, firedTransitions, ...actionScope} = enterDefault(simtime, toEnter, {firedTransitions, ...actionScope}));
enteredStates = enteredStates.union(enteredChildren);
}
// console.warn(state.uid + ': no initial state');
}
return {enteredStates, ...actionScope};
return {enteredStates, firedTransitions, ...actionScope};
}
// recursively enter the given state and, if children need to be entered, preferrably those occurring in 'toEnter' will be entered. If no child occurs in 'toEnter', the default child will be entered.
@ -281,14 +283,14 @@ export function handleInputEvent(simtime: number, event: RT_Event, statechart: S
return handleInternalEvents(simtime, statechart, {mode, environment, history, ...raised});
}
export function handleInternalEvents(simtime: number, statechart: Statechart, {mode, environment, history, ...raised}: RT_Statechart & RaisedEvents): BigStepOutput {
while (raised.internalEvents.length > 0) {
const [internalEvent, ...rest] = raised.internalEvents;
({mode, environment, ...raised} = handleEvent(simtime,
{kind: "input", ...internalEvent}, // internal event becomes input event
statechart, statechart.root, {mode, environment, history, internalEvents: rest, outputEvents: raised.outputEvents}));
export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents): BigStepOutput {
while (internalEvents.length > 0) {
const [nextEvent, ...remainingEvents] = internalEvents;
({internalEvents, ...rest} = handleEvent(simtime,
{kind: "input", ...nextEvent}, // internal event becomes input event
statechart, statechart.root, {internalEvents: remainingEvents, ...rest}));
}
return {mode, environment, history, outputEvents: raised.outputEvents};
return rest;
}
export function fireTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, history, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
@ -309,12 +311,14 @@ export function fireTransition(simtime: number, t: Transition, ts: Map<string, T
// assuming we've already exited the source state of the transition, now enter the target state
// IF however, the target is a pseudo-state, DON'T enter it (pseudo-states are NOT states), instead fire the first pseudo-outgoing transition.
export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, history, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, history, firedTransitions, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
// exec transition actions
for (const action of label.actions) {
({environment, history, ...raised} = execAction(action, {environment, history, ...raised}));
({environment, history, firedTransitions, ...raised} = execAction(action, {environment, history, firedTransitions, ...raised}));
}
firedTransitions = [...firedTransitions, t.uid];
if (t.tgt.kind === "pseudo") {
const outgoing = ts.get(t.tgt.uid) || [];
for (const nextT of outgoing) {
@ -323,7 +327,7 @@ export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: M
if (evalExpr(nextLabel.guard, environment)) {
console.log('fire', transitionDescription(nextT));
// found ourselves an enabled transition
return fireSecondHalfOfTransition(simtime, nextT, ts, nextLabel, arena, {mode, environment, history, ...raised});
return fireSecondHalfOfTransition(simtime, nextT, ts, nextLabel, arena, {mode, environment, history, firedTransitions, ...raised});
}
}
}
@ -346,11 +350,11 @@ export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: M
// enter tgt
let enteredStates;
({enteredStates, environment, history, ...raised} = enterStates(simtime, state, toEnter, {environment, history, ...raised}));
({enteredStates, environment, history, ...raised} = enterStates(simtime, state, toEnter, {environment, history, firedTransitions, ...raised}));
const enteredMode = mode.union(enteredStates);
// console.log({enteredMode});
return {mode: enteredMode, environment, history, ...raised};
return {mode: enteredMode, environment, history, firedTransitions, ...raised};
}
}

View file

@ -101,6 +101,7 @@ export type RT_Statechart = {
export type BigStepOutput = RT_Statechart & {
outputEvents: RaisedEvent[],
firedTransitions: string[],
};
export type BigStep = {
@ -117,6 +118,7 @@ export type RaisedEvent = {
export type RaisedEvents = {
internalEvents: RaisedEvent[];
outputEvents: RaisedEvent[];
firedTransitions: string[]; // list of UIDs
};
// export type Timers = Map<string, number>; // transition uid -> timestamp
@ -124,6 +126,8 @@ export type RaisedEvents = {
export const initialRaised: RaisedEvents = {
internalEvents: [],
outputEvents: [],
firedTransitions: [],
};
export type Timers = [number, TimerElapseEvent][];