diff --git a/src/App/App.tsx b/src/App/App.tsx index cac3ed8..66a2d05 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -1,11 +1,10 @@ import "../index.css"; import "./App.css"; -import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Dispatch, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { emptyState } from "@/statecharts/concrete_syntax"; import { detectConnections } from "@/statecharts/detect_connections"; -import { Conns, coupledExecution, EventDestination, exposeStatechartInputs, statechartExecution } from "@/statecharts/timed_reactive"; +import { Conns, coupledExecution, EventDestination, statechartExecution, TimedReactive } from "@/statecharts/timed_reactive"; import { RuntimeError } from "../statecharts/interpreter"; import { parseStatechart } from "../statecharts/parser"; import { BigStep, RaisedEvent } from "../statecharts/runtime_types"; @@ -13,17 +12,17 @@ import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time"; import { BottomPanel } from "./BottomPanel"; import { usePersistentState } from "./persistent_state"; import { PersistentDetails } from "./PersistentDetails"; -import { DummyPlant } from "./Plant/Dummy/Dummy"; -import { MicrowavePlant } from "./Plant/Microwave/Microwave"; -import { autoConnect, exposePlantInputs, Plant } from "./Plant/Plant"; +import { dummyPlant } from "./Plant/Dummy/Dummy"; +import { microwavePlant } from "./Plant/Microwave/Microwave"; +import { Plant } from "./Plant/Plant"; import { RTHistory } from "./RTHistory"; import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST"; import { TopPanel } from "./TopPanel/TopPanel"; -import { getKeyHandler } from "./VisualEditor/shortcut_handler"; -import { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; -import { addV2D, rotateLine90CCW, rotateLine90CW, rotatePoint90CCW, rotatePoint90CW, rotateRect90CCW, rotateRect90CW, scaleV2D, subtractV2D, Vec2D } from "@/util/geometry"; -import { HISTORY_RADIUS } from "./parameters"; -import { DigitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch"; +import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; +import { digitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch"; +import { useEditor as useEditor } from "./useEditor"; +import { InsertMode } from "./TopPanel/InsertModes"; +import { Statechart } from "@/statecharts/abstract_syntax"; export type EditHistory = { current: VisualEditorState, @@ -32,9 +31,9 @@ export type EditHistory = { } const plants: [string, Plant][] = [ - ["dummy", DummyPlant], - ["microwave", MicrowavePlant], - ["digital watch", DigitalWatchPlant], + ["dummy", dummyPlant], + ["microwave", microwavePlant], + ["digital watch", digitalWatchPlant], ] export type TraceItemError = { @@ -58,25 +57,8 @@ export type TraceState = { idx: number, }; // <-- null if there is no trace -// function getPlantState(plant: Plant, trace: TraceItem[], idx: number): T | null { -// if (idx === -1) { -// return plant.initial; -// } -// let plantState = getPlantState(plant, trace, idx-1); -// if (plantState !== null) { -// const currentConfig = trace[idx]; -// if (currentConfig.kind === "bigstep") { -// for (const o of currentConfig.outputEvents) { -// plantState = plant.reduce(o, plantState); -// } -// } -// return plantState; -// } -// return null; -// } - export function App() { - const [insertMode, setInsertMode] = useState("and"); + const [insertMode, setInsertMode] = usePersistentState("insertMode", "and"); const [editHistory, setEditHistory] = useState(null); const [trace, setTrace] = useState(null); const [time, setTime] = useState({kind: "paused", simtime: 0}); @@ -86,6 +68,10 @@ export function App() { const [zoom, setZoom] = usePersistentState("zoom", 1); const [showKeys, setShowKeys] = usePersistentState("shortcuts", true); + const [autoScroll, setAutoScroll] = usePersistentState("autoScroll", true); + const [autoConnect, setAutoConnect] = usePersistentState("autoConnect", true); + const [plantConns, setPlantConns] = usePersistentState("plantConns", {}); + const plant = plants.find(([pn, p]) => pn === plantName)![1]; const editorState = editHistory && editHistory.current; @@ -93,63 +79,6 @@ export function App() { setEditHistory(historyState => historyState && ({...historyState, current: cb(historyState.current)})); }, [setEditHistory]); - // recover editor state from URL - we need an effect here because decompression is asynchronous - useEffect(() => { - console.log('recovering state...'); - const compressedState = window.location.hash.slice(1); - if (compressedState.length === 0) { - // empty URL hash - console.log("no state to recover"); - setEditHistory(() => ({current: emptyState, history: [], future: []})); - return; - } - let compressedBuffer; - try { - compressedBuffer = Uint8Array.fromBase64(compressedState); // may throw - } catch (e) { - // probably invalid base64 - console.error("failed to recover state:", e); - setEditHistory(() => ({current: emptyState, history: [], future: []})); - return; - } - const ds = new DecompressionStream("deflate"); - const writer = ds.writable.getWriter(); - writer.write(compressedBuffer).catch(() => {}); // any promise rejections will be detected when we try to read - writer.close().catch(() => {}); - new Response(ds.readable).arrayBuffer() - .then(decompressedBuffer => { - const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer)); - setEditHistory(() => ({current: recoveredState, history: [], future: []})); - }) - .catch(e => { - // any other error: invalid JSON, or decompression failed. - console.error("failed to recover state:", e); - setEditHistory({current: emptyState, history: [], future: []}); - }); - }, []); - - // save editor state in URL - useEffect(() => { - const timeout = setTimeout(() => { - if (editorState === null) { - window.location.hash = "#"; - return; - } - const serializedState = JSON.stringify(editorState); - const stateBuffer = new TextEncoder().encode(serializedState); - const cs = new CompressionStream("deflate"); - const writer = cs.writable.getWriter(); - writer.write(stateBuffer); - writer.close(); - // todo: cancel this promise handler when concurrently starting another compression job - new Response(cs.readable).arrayBuffer().then(compressedStateBuffer => { - const compressedStateString = new Uint8Array(compressedStateBuffer).toBase64(); - window.location.hash = "#"+compressedStateString; - }); - }, 100); - return () => clearTimeout(timeout); - }, [editorState]); - const refRightSideBar = useRef(null); // parse concrete syntax always: @@ -166,177 +95,42 @@ export function App() { }] : [], ] - // append editor state to undo history - const makeCheckPoint = useCallback(() => { - setEditHistory(historyState => historyState && ({ - ...historyState, - history: [...historyState.history, historyState.current], - future: [], - })); - }, [setEditHistory]); - const onUndo = useCallback(() => { - setEditHistory(historyState => { - if (historyState === null) return null; - if (historyState.history.length === 0) { - return historyState; // no change - } - return { - current: historyState.history.at(-1)!, - history: historyState.history.slice(0,-1), - future: [...historyState.future, historyState.current], - } - }) - }, [setEditHistory]); - const onRedo = useCallback(() => { - setEditHistory(historyState => { - if (historyState === null) return null; - if (historyState.future.length === 0) { - return historyState; // no change - } - return { - current: historyState.future.at(-1)!, - history: [...historyState.history, historyState.current], - future: historyState.future.slice(0,-1), - } - }); - }, [setEditHistory]); - const onRotate = useCallback((direction: "ccw" | "cw") => { - makeCheckPoint(); - setEditHistory(historyState => { - if (historyState === null) return null; - - const selection = historyState.current.selection; - - if (selection.length === 0) { - return historyState; - } - - // determine bounding box... in a convoluted manner - let minX = -Infinity, minY = -Infinity, maxX = Infinity, maxY = Infinity; - - function addPointToBBox({x,y}: Vec2D) { - minX = Math.max(minX, x); - minY = Math.max(minY, y); - maxX = Math.min(maxX, x); - maxY = Math.min(maxY, y); - } - - for (const rt of historyState.current.rountangles) { - if (selection.some(s => s.uid === rt.uid)) { - addPointToBBox(rt.topLeft); - addPointToBBox(addV2D(rt.topLeft, rt.size)); - } - } - for (const d of historyState.current.diamonds) { - if (selection.some(s => s.uid === d.uid)) { - addPointToBBox(d.topLeft); - addPointToBBox(addV2D(d.topLeft, d.size)); - } - } - for (const arr of historyState.current.arrows) { - if (selection.some(s => s.uid === arr.uid)) { - addPointToBBox(arr.start); - addPointToBBox(arr.end); - } - } - for (const txt of historyState.current.texts) { - if (selection.some(s => s.uid === txt.uid)) { - addPointToBBox(txt.topLeft); - } - } - const historySize = {x: HISTORY_RADIUS, y: HISTORY_RADIUS}; - for (const h of historyState.current.history) { - if (selection.some(s => s.uid === h.uid)) { - addPointToBBox(h.topLeft); - addPointToBBox(addV2D(h.topLeft, scaleV2D(historySize, 2))); - } - } - - const center: Vec2D = { - x: (minX + maxX) / 2, - y: (minY + maxY) / 2, - }; - - const mapIfSelected = (shape: {uid: string}, cb: (shape:any)=>any) => { - if (selection.some(s => s.uid === shape.uid)) { - return cb(shape); - } - else { - return shape; - } - } - - return { - ...historyState, - current: { - ...historyState.current, - rountangles: historyState.current.rountangles.map(rt => mapIfSelected(rt, rt => { - return { - ...rt, - ...(direction === "ccw" - ? rotateRect90CCW(rt, center) - : rotateRect90CW(rt, center)), - } - })), - arrows: historyState.current.arrows.map(arr => mapIfSelected(arr, arr => { - return { - ...arr, - ...(direction === "ccw" - ? rotateLine90CCW(arr, center) - : rotateLine90CW(arr, center)), - }; - })), - diamonds: historyState.current.diamonds.map(d => mapIfSelected(d, d => { - return { - ...d, - ...(direction === "ccw" - ? rotateRect90CCW(d, center) - : rotateRect90CW(d, center)), - }; - })), - texts: historyState.current.texts.map(txt => mapIfSelected(txt, txt => { - return { - ...txt, - topLeft: (direction === "ccw" - ? rotatePoint90CCW(txt.topLeft, center) - : rotatePoint90CW(txt.topLeft, center)), - }; - })), - history: historyState.current.history.map(h => mapIfSelected(h, h => { - return { - ...h, - topLeft: (direction === "ccw" - ? subtractV2D(rotatePoint90CCW(addV2D(h.topLeft, historySize), center), historySize) - : subtractV2D(rotatePoint90CW(addV2D(h.topLeft, historySize), center), historySize) - ), - }; - })), - }, - } - }) - }, [setEditHistory]); + const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(editorState, setEditHistory); const scrollDownSidebar = useCallback(() => { - if (refRightSideBar.current) { + if (autoScroll && refRightSideBar.current) { const el = refRightSideBar.current; // hack: we want to scroll to the new element, but we have to wait until it is rendered... setTimeout(() => { el.scrollIntoView({block: "end", behavior: "smooth"}); }, 50); } - }, [refRightSideBar.current]); + }, [refRightSideBar.current, autoScroll]); - const plantConns = ast && ({ - inputEvents: { - ...exposeStatechartInputs(ast, "sc", (eventName: string) => "DEBUG_"+eventName), - ...exposePlantInputs(plant, "plant", (eventName: string) => "PLANT_UI_"+eventName), - }, - outputEvents: autoConnect(ast, "sc", plant, "plant"), - }) as Conns; + // const plantConns = ast && ({ + // inputEvents: { + // // all SC inputs are directly triggerable from outside + // ...exposeStatechartInputs(ast, "sc", (eventName: string) => "debug."+eventName), + + // ...Object.fromEntries(plant.uiEvents.map(e => { + // const globalName = "PLANT_UI_"+e.event; + // if (plant.inputEvents.some(f => f.event === e.event)) { + // return [globalName, {kind: "model", model: 'plant', eventName: e.event}]; + // } + // if (ast.inputEvents.some(f => f.event === e.event)) { + // return [globalName, {kind: "model", model: 'sc', eventName: e.event}]; + // } + // }).filter(entry => entry !== undefined)), + // }, + // outputEvents: {}, //autoConnect(ast, "sc", plant, "plant"), + // }) as Conns; const cE = useMemo(() => ast && coupledExecution({ sc: statechartExecution(ast), plant: plant.execution, - }, plantConns!), [ast]); + }, { + ...plantConns, + ...Object.fromEntries(ast.inputEvents.map(({event}) => ["debug."+event, ['sc',event] as [string,string]])), + }), [ast]); const onInit = useCallback(() => { if (cE === null) return; @@ -448,7 +242,7 @@ export function App() { scrollDownSidebar(); } - function onBack() { + const onBack = useCallback(() => { if (trace !== null) { setTime(() => { if (trace !== null) { @@ -464,22 +258,7 @@ export function App() { idx: trace.idx-1, }); } - } - - useEffect(() => { - console.log("Welcome to StateBuddy!"); - () => { - console.log("Goodbye!"); - } - }, []); - - useEffect(() => { - const onKeyDown = getKeyHandler(setInsertMode); - window.addEventListener("keydown", onKeyDown); - return () => { - window.removeEventListener("keydown", onKeyDown); - }; - }, []); + }, [trace]); const currentBigStep = currentTraceItem && currentTraceItem.kind === "bigstep" && currentTraceItem; const highlightActive = (currentBigStep && currentBigStep.state.sc.mode) || new Set(); @@ -490,6 +269,12 @@ export function App() { const speed = time.kind === "paused" ? 0 : time.scale; + const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1]; + + useEffect(() => { + ast && autoConnect && autoDetectConns(ast, plant, setPlantConns); + }, [ast, plant, autoConnect]); + return <> {/* Modal dialog */} @@ -523,7 +308,7 @@ export function App() { {/* Editor */}
{editorState && conns && syntaxErrors && - } + }
@@ -550,7 +335,7 @@ export function App() { input events {ast && onRaise("DEBUG_"+e,p)} + onRaise={(e,p) => onRaise("debug."+e,p)} disabled={trace===null || trace.trace[trace.idx].kind === "error"} showKeys={showKeys}/>} @@ -572,13 +357,25 @@ export function App() { )} - {plantConns && } - {currentBigStep && onRaise("PLANT_UI_"+e.name, e.param)} + {/* Render plant */} + { onRaise("plant.ui."+e.name, e.param)} />} + + connections + + {ast && ConnEditor(ast, plant, plantConns, setPlantConns)} + +
setShowExecutionTrace(e.newState === "open")}>execution trace - setShowPlantTrace(e.target.checked)}/> + setShowPlantTrace(e.target.checked)}/> + + setAutoScroll(e.target.checked)}/> +
@@ -622,11 +419,85 @@ function ShowEventDestination(dst: EventDestination) { function ShowConns({inputEvents, outputEvents}: Conns) { return
-
+ {/*
{Object.entries(inputEvents).map(([eventName, destination]) =>
{eventName} →
)}
- {Object.entries(outputEvents).map(([modelName, mapping]) => <>{Object.entries(mapping).map(([eventName, destination]) =>
{modelName}.{eventName} →
)})} + {Object.entries(outputEvents).map(([modelName, mapping]) => <>{Object.entries(mapping).map(([eventName, destination]) =>
{modelName}.{eventName} →
)})} */}
; } +import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'; +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; + +function autoDetectConns(ast: Statechart, plant: Plant, setPlantConns: Dispatch>) { + for (const {event: a} of plant.uiEvents) { + for (const {event: b} of plant.inputEvents) { + if (a === b) { + setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['plant', b]})); + break; + } + } + for (const {event: b} of ast.inputEvents) { + if (a === b) { + setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['sc', b]})); + } + } + } + for (const a of ast.outputEvents) { + for (const {event: b} of plant.inputEvents) { + if (a === b) { + setPlantConns(conns => ({...conns, ['sc.'+a]: ['plant', b]})); + } + } + } + for (const {event: a} of plant.outputEvents) { + for (const {event: b} of ast.inputEvents) { + if (a === b) { + setPlantConns(conns => ({...conns, ['plant.'+a]: ['sc', b]})); + } + } + } +} + +function ConnEditor(ast: Statechart, plant: Plant, plantConns: Conns, setPlantConns: Dispatch>) { + const plantInputs = <>{plant.inputEvents.map(e => )} + const scInputs = <>{ast.inputEvents.map(e => )}; + return <> + {/* Plant UI events can go to SC or to Plant */} + {plant.uiEvents.map(e =>
+ + +
)} + + {/* SC output events can go to Plant */} + {[...ast.outputEvents].map(e =>
+ + +
)} + + {/* Plant output events can go to Statechart */} + {[...plant.outputEvents.map(e =>
+ + +
)]} + ; +} + export default App; + diff --git a/src/App/BottomPanel.css b/src/App/BottomPanel.css index 124d58f..92c1a48 100644 --- a/src/App/BottomPanel.css +++ b/src/App/BottomPanel.css @@ -6,6 +6,5 @@ .bottom { border-top: 1px lightgrey solid; - background-color: lightyellow; - /* background-color: rgb(255, 251, 244); */ + background-color: rgb(255, 249, 235); } \ No newline at end of file diff --git a/src/App/Plant/DigitalWatch/DigitalWatch.tsx b/src/App/Plant/DigitalWatch/DigitalWatch.tsx index fcc2b61..c9b83b4 100644 --- a/src/App/Plant/DigitalWatch/DigitalWatch.tsx +++ b/src/App/Plant/DigitalWatch/DigitalWatch.tsx @@ -2,10 +2,9 @@ import { useAudioContext } from "@/App/useAudioContext"; import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor"; import { detectConnections } from "@/statecharts/detect_connections"; import { parseStatechart } from "@/statecharts/parser"; -import { BigStep, RT_Statechart } from "@/statecharts/runtime_types"; -import { statechartExecution } from "@/statecharts/timed_reactive"; +import { RT_Statechart } from "@/statecharts/runtime_types"; import { useEffect } from "react"; -import { Plant, PlantRenderProps } from "../Plant"; +import { makeStatechartPlant, PlantRenderProps } from "../Plant"; import dwatchConcreteSyntax from "./model.json"; import sndBeep from "./beep.wav"; @@ -24,12 +23,12 @@ if (dwatchErrors.length > 0) { const twoDigits = (n: number) => ("0"+n.toString()).slice(-2); -export function DigitalWatch({state, speed, raiseInput}: PlantRenderProps) { - const displayingTime = state.mode.has("265"); - const displayingAlarm = state.mode.has("266"); - const displayingChrono = state.mode.has("264"); +export function DigitalWatch({state, speed, raiseUIEvent}: PlantRenderProps) { + const displayingTime = state.mode.has("625"); + const displayingAlarm = state.mode.has("626"); + const displayingChrono = state.mode.has("624"); - const lightOn = state.mode.has("389"); + const lightOn = state.mode.has("630"); const alarm = state.environment.get("alarm"); @@ -43,9 +42,9 @@ export function DigitalWatch({state, speed, raiseInput}: PlantRenderProps { if (beep) { @@ -88,20 +87,20 @@ export function DigitalWatch({state, speed, raiseInput}: PlantRenderProps{hhmmss} raiseInput({name: "topLeftPressed"})} - onMouseUp={() => raiseInput({name: "topLeftReleased"})} + onMouseDown={() => raiseUIEvent({name: "topLeftPressed"})} + onMouseUp={() => raiseUIEvent({name: "topLeftReleased"})} /> raiseInput({name: "topRightPressed"})} - onMouseUp={() => raiseInput({name: "topRightReleased"})} + onMouseDown={() => raiseUIEvent({name: "topRightPressed"})} + onMouseUp={() => raiseUIEvent({name: "topRightReleased"})} /> raiseInput({name: "bottomLeftPressed"})} - onMouseUp={() => raiseInput({name: "bottomLeftReleased"})} + onMouseDown={() => raiseUIEvent({name: "bottomLeftPressed"})} + onMouseUp={() => raiseUIEvent({name: "bottomLeftReleased"})} /> raiseInput({name: "bottomRightPressed"})} - onMouseUp={() => raiseInput({name: "bottomRightReleased"})} + onMouseDown={() => raiseUIEvent({name: "bottomRightPressed"})} + onMouseUp={() => raiseUIEvent({name: "bottomRightReleased"})} /> {alarm && @@ -111,46 +110,17 @@ export function DigitalWatch({state, speed, raiseInput}: PlantRenderProps; } -export const DigitalWatchPlant: Plant = { - inputEvents: [ - { kind: "event", event: "displayTime" }, - { kind: "event", event: "displayChrono" }, - { kind: "event", event: "displayAlarm" }, - { kind: "event", event: "beginEdit" }, - { kind: "event", event: "endEdit" }, - { kind: "event", event: "selectNext" }, - { kind: "event", event: "incSelection" }, - { kind: "event", event: "incTime" }, - { kind: "event", event: "incAlarm" }, - { kind: "event", event: "incChrono" }, - { kind: "event", event: "resetChrono" }, - { kind: "event", event: "lightOn"}, - { kind: "event", event: "lightOff"}, - { kind: "event", event: "setAlarm", paramName: 'alarmOn'}, - { kind: "event", event: "beep", paramName: 'beep'}, - - // UI events - { kind: "event", event: "topLeftPressed" }, - { kind: "event", event: "topRightPressed" }, - { kind: "event", event: "bottomRightPressed" }, - { kind: "event", event: "bottomLeftPressed" }, - { kind: "event", event: "topLeftReleased" }, - { kind: "event", event: "topRightReleased" }, - { kind: "event", event: "bottomRightReleased" }, - { kind: "event", event: "bottomLeftReleased" }, - ], - outputEvents: [ - { kind: "event", event: "alarm" }, - - { kind: "event", event: "topLeftPressed" }, - { kind: "event", event: "topRightPressed" }, - { kind: "event", event: "bottomRightPressed" }, - { kind: "event", event: "bottomLeftPressed" }, - { kind: "event", event: "topLeftReleased" }, - { kind: "event", event: "topRightReleased" }, - { kind: "event", event: "bottomRightReleased" }, - { kind: "event", event: "bottomLeftReleased" }, - ], - execution: statechartExecution(dwatchAbstractSyntax), +export const digitalWatchPlant = makeStatechartPlant({ + ast: dwatchAbstractSyntax, render: DigitalWatch, -} + uiEvents: [ + { kind: "event", event: "topLeftPressed" }, + { kind: "event", event: "topRightPressed" }, + { kind: "event", event: "bottomRightPressed" }, + { kind: "event", event: "bottomLeftPressed" }, + { kind: "event", event: "topLeftReleased" }, + { kind: "event", event: "topRightReleased" }, + { kind: "event", event: "bottomRightReleased" }, + { kind: "event", event: "bottomLeftReleased" }, + ], +}); diff --git a/src/App/Plant/DigitalWatch/model.json b/src/App/Plant/DigitalWatch/model.json index d8689b0..2f7d052 100644 --- a/src/App/Plant/DigitalWatch/model.json +++ b/src/App/Plant/DigitalWatch/model.json @@ -1 +1 @@ -{"rountangles":[{"uid":"251","topLeft":{"x":258.8281250000001,"y":127.34374999999989},"size":{"x":2542.5781249999986,"y":1096.3281249999998},"kind":"and"},{"uid":"252","topLeft":{"x":1188.5859375,"y":404.33593750000057},"size":{"x":1173.140625,"y":793.296875},"kind":"or"},{"uid":"253","topLeft":{"x":1240.1593750000002,"y":551.365624999999},"size":{"x":1069.7749999999987,"y":602.15625},"kind":"or"},{"uid":"254","topLeft":{"x":280.3125000000002,"y":722.352081298828},"size":{"x":879.4374999999998,"y":478.2812500000001},"kind":"or"},{"uid":"417","topLeft":{"x":2392.4999999999986,"y":162.49999999999991},"size":{"x":392.49999999999994,"y":1028.7499999999995},"kind":"or"},{"uid":"255","topLeft":{"x":658.7031249999999,"y":161.6197967529296},"size":{"x":500.15625,"y":539.2500000000002},"kind":"or"},{"uid":"256","topLeft":{"x":1185.9109375000012,"y":163.97656249999932},"size":{"x":928.378124999999,"y":216.07812499999991},"kind":"or"},{"uid":"396","topLeft":{"x":1218.7499999999993,"y":183.7499999999999},"size":{"x":802.4999999999995,"y":173.74999999999994},"kind":"or"},{"uid":"257","topLeft":{"x":281.95312499999795,"y":160.07812499999966},"size":{"x":360.3124999999998,"y":281.95312499999983},"kind":"or"},{"uid":"387","topLeft":{"x":279.99999999999983,"y":463.7499999999997},"size":{"x":361.2499999999999,"y":237.49999999999991},"kind":"or"},{"uid":"258","topLeft":{"x":1476.7093749999988,"y":717.209374999999},"size":{"x":267,"y":249.125},"kind":"or"},{"uid":"418","topLeft":{"x":2482.4999999999986,"y":314.99999999999983},"size":{"x":82.49999999999997,"y":786.2499999999998},"kind":"and"},{"uid":"259","topLeft":{"x":1824.709374999999,"y":588.334374999999},"size":{"x":258.5999999999999,"y":216.8},"kind":"or"},{"uid":"399","topLeft":{"x":327.49999999999983,"y":497.49999999999966},"size":{"x":293.74999999999994,"y":188.74999999999994},"kind":"or"},{"uid":"260","topLeft":{"x":373.7500000000001,"y":804.832290649414},"size":{"x":160.9375,"y":307.75},"kind":"and"},{"uid":"405","topLeft":{"x":2142.4999999999986,"y":163.74999999999994},"size":{"x":216.2499999999999,"y":212.49999999999983},"kind":"or"},{"uid":"261","topLeft":{"x":1797.459374999999,"y":931.4343749999962},"size":{"x":241.3999999999998,"y":186.4000000000002},"kind":"or"},{"uid":"262","topLeft":{"x":710.9218749999999,"y":308.36979675293003},"size":{"x":367,"y":65.00000000000001},"kind":"and"},{"uid":"263","topLeft":{"x":1318.1328125,"y":427.21093750000045},"size":{"x":307.6249999999999,"y":67.390625},"kind":"and"},{"uid":"264","topLeft":{"x":1790.9765625000007,"y":239.7265624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"265","topLeft":{"x":1329.726562500001,"y":240.9765624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"266","topLeft":{"x":1548.476562500001,"y":239.7265624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"267","topLeft":{"x":1621.959374999999,"y":808.084374999999},"size":{"x":94,"y":136},"kind":"and"},{"uid":"268","topLeft":{"x":1850.709374999999,"y":644.334374999999},"size":{"x":78,"y":135},"kind":"and"},{"uid":"388","topLeft":{"x":373.7499999999998,"y":518.7499999999997},"size":{"x":186.24999999999997,"y":55.00000000000006},"kind":"and"},{"uid":"389","topLeft":{"x":376.2499999999998,"y":617.4999999999997},"size":{"x":177.49999999999994,"y":56.25},"kind":"and"},{"uid":"269","topLeft":{"x":342.5781249999975,"y":261.71874999999955},"size":{"x":190.00000000000006,"y":51.562499999999986},"kind":"and"},{"uid":"270","topLeft":{"x":333.82812499999795,"y":377.0312499999993},"size":{"x":194.6875,"y":44.68749999999996},"kind":"and"},{"uid":"271","topLeft":{"x":1817.459374999999,"y":961.834374999999},"size":{"x":61.99999999999994,"y":135},"kind":"and"},{"uid":"272","topLeft":{"x":1507.959374999999,"y":809.084374999999},"size":{"x":61.99999999999994,"y":135},"kind":"and"},{"uid":"406","topLeft":{"x":2169.9999999999986,"y":249.99999999999983},"size":{"x":123.74999999999997,"y":65},"kind":"and"},{"uid":"273","topLeft":{"x":1980.709374999999,"y":643.334374999999},"size":{"x":45.99999999999994,"y":136},"kind":"and"},{"uid":"274","topLeft":{"x":1931.459374999999,"y":960.834374999999},"size":{"x":45.99999999999994,"y":136},"kind":"and"}],"diamonds":[{"uid":"275","topLeft":{"x":876.99169921875,"y":827.648956298828},"size":{"x":72.99999999999997,"y":81.99999999999997}},{"uid":"276","topLeft":{"x":888.99169921875,"y":1013.3989562988279},"size":{"x":58.00000000000003,"y":77.99999999999997}},{"uid":"277","topLeft":{"x":990.03857421875,"y":522.0864562988284},"size":{"x":85.00000000000003,"y":76.99999999999997}},{"uid":"278","topLeft":{"x":739.0385742187499,"y":513.1489562988284},"size":{"x":97.00000000000003,"y":77.99999999999997}},{"uid":"279","topLeft":{"x":2225.5718749999996,"y":600.5593749999953},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"280","topLeft":{"x":2235.971875,"y":930.1593749999934},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"281","topLeft":{"x":1254.2718749999997,"y":756.9093749999988},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"411","topLeft":{"x":607.4999999999997,"y":821.2499999999995},"size":{"x":56.24999999999997,"y":217.4999999999999}}],"history":[],"arrows":[{"uid":"282","start":{"x":1962.709374999999,"y":808.334374999999},"end":{"x":1852.459374999999,"y":956.834374999999}},{"uid":"283","start":{"x":1799.459374999999,"y":1080.834374999999},"end":{"x":1674.959374999999,"y":947.084374999999}},{"uid":"284","start":{"x":1751.709374999999,"y":856.334374999999},"end":{"x":1847.709374999999,"y":753.334374999999}},{"uid":"285","start":{"x":1252.5078125,"y":450.25781250000045},"end":{"x":1308.7421875,"y":459.14843750000045}},{"uid":"286","start":{"x":1721.4093749999988,"y":593.1843750000039},"end":{"x":1820.8593749999986,"y":612.9343750000021}},{"uid":"287","start":{"x":1972.709374999999,"y":683.334374999999},"end":{"x":1932.709374999999,"y":683.334374999999}},{"uid":"288","start":{"x":1927.709374999999,"y":753.334374999999},"end":{"x":1973.709374999999,"y":751.334374999999}},{"uid":"289","start":{"x":1913.5093749999987,"y":609.9343750000007},"end":{"x":1915.309374999999,"y":638.7343750000009}},{"uid":"290","start":{"x":1923.459374999999,"y":1000.834374999999},"end":{"x":1883.459374999999,"y":1000.834374999999}},{"uid":"291","start":{"x":1878.459374999999,"y":1070.834374999999},"end":{"x":1924.459374999999,"y":1068.834374999999}},{"uid":"292","start":{"x":2018.2593749999987,"y":1079.4343749999998},"end":{"x":1980.459374999999,"y":1053.834374999999}},{"uid":"293","start":{"x":1613.959374999999,"y":848.084374999999},"end":{"x":1573.959374999999,"y":848.084374999999}},{"uid":"294","start":{"x":1568.959374999999,"y":918.084374999999},"end":{"x":1614.959374999999,"y":916.084374999999}},{"uid":"295","start":{"x":2086.5093749999987,"y":758.7343749999982},"end":{"x":2238.309374999999,"y":775.1343749999992}},{"uid":"296","start":{"x":1345.8515625000002,"y":542.0390625000005},"end":{"x":1345.6015625000002,"y":500.35156250000045}},{"uid":"297","start":{"x":1537.6760498046865,"y":754.7260375976552},"end":{"x":1536.4260498046865,"y":805.3510375976552}},{"uid":"298","start":{"x":366.9531249999975,"y":225.46874999999955},"end":{"x":406.9531249999975,"y":255.46874999999977}},{"uid":"299","start":{"x":490.7031249999976,"y":321.7187499999993},"end":{"x":489.7656249999976,"y":370.1562499999993}},{"uid":"300","start":{"x":393.20312499999795,"y":373.9062499999993},"end":{"x":392.26562499999795,"y":322.3437499999993}},{"uid":"301","start":{"x":878.5249938964844,"y":891.648956298828},"end":{"x":663.3374938964846,"y":894.648956298828}},{"uid":"302","start":{"x":882.5249938964844,"y":841.648956298828},"end":{"x":664.3374938964846,"y":841.648956298828}},{"uid":"303","start":{"x":531.0541992187502,"y":1078.648956298828},"end":{"x":883.99169921875,"y":1084.648956298828}},{"uid":"304","start":{"x":885.7749938964844,"y":1021.3989562988279},"end":{"x":665.5874938964846,"y":1021.3989562988279}},{"uid":"305","start":{"x":917.5249938964844,"y":1012.3989562988279},"end":{"x":915.5249938964844,"y":919.6489562988279}},{"uid":"306","start":{"x":324.7500000000001,"y":783.586456298828},"end":{"x":377.7500000000001,"y":802.586456298828}},{"uid":"307","start":{"x":757.9218749999999,"y":380.36979675293003},"end":{"x":756.9218749999999,"y":524.3697967529299}},{"uid":"308","start":{"x":1058.5718688964844,"y":528.0864562988284},"end":{"x":1055.5718688964844,"y":380.08645629882847}},{"uid":"309","start":{"x":832.0385742187498,"y":559.0864562988284},"end":{"x":976.03857421875,"y":561.0864562988284}},{"uid":"310","start":{"x":820.5718688964843,"y":524.0864562988284},"end":{"x":820.5718688964843,"y":380.08645629882847}},{"uid":"311","start":{"x":985.4468688964844,"y":530.2114562988284},"end":{"x":986.5718688964844,"y":378.08645629882847}},{"uid":"312","start":{"x":735.2343749999994,"y":219.1406249999999},"end":{"x":774.9218749999994,"y":301.0156249999999}},{"uid":"313","start":{"x":273.4374999999999,"y":68.74999999999993},"end":{"x":343.7499999999999,"y":120.31249999999994}},{"uid":"314","start":{"x":1254.851562500001,"y":273.9765624999991},"end":{"x":1318.289062500001,"y":270.6640624999991}},{"uid":"315","start":{"x":2228.3718749999994,"y":695.159374999997},"end":{"x":2032.3718749999994,"y":710.9593749999999}},{"uid":"316","start":{"x":2243.1718749999995,"y":619.3593749999973},"end":{"x":2032.7718749999995,"y":669.759375000001}},{"uid":"317","start":{"x":2043.0593749999994,"y":1104.4343749999957},"end":{"x":2248.709375,"y":1104.7343749999968}},{"uid":"318","start":{"x":2238.7718750000004,"y":1024.7593749999946},"end":{"x":2042.521875,"y":1031.8593749999968}},{"uid":"319","start":{"x":2253.571875,"y":948.9593749999954},"end":{"x":2044.521875,"y":962.6593749999961}},{"uid":"320","start":{"x":1478.1218749999991,"y":759.0093749999969},"end":{"x":1295.0718749999999,"y":773.3093749999966}},{"uid":"321","start":{"x":1317.471875,"y":850.909374999997},"end":{"x":1508.0218749999995,"y":868.5093749999978}},{"uid":"322","start":{"x":1289.4718750000004,"y":936.5093749999978},"end":{"x":1503.2218750000004,"y":925.3093749999957}},{"uid":"323","start":{"x":1521.249999999999,"y":496.2499999999997},"end":{"x":1518.749999999999,"y":549.9999999999997}},{"uid":"324","start":{"x":1432.499999999999,"y":186.2499999999999},"end":{"x":1431.249999999999,"y":231.2499999999999}},{"uid":"325","start":{"x":1642.4999999999989,"y":186.24999999999991},"end":{"x":1643.7499999999989,"y":233.74999999999991}},{"uid":"326","start":{"x":1888.7499999999986,"y":189.99999999999991},"end":{"x":1888.7499999999986,"y":232.49999999999991}},{"uid":"390","start":{"x":616.2499999999997,"y":633.7499999999997},"end":{"x":563.7499999999997,"y":634.9999999999997}},{"uid":"397","start":{"x":2087.4999999999986,"y":239.99999999999986},"end":{"x":2029.9999999999986,"y":241.24999999999986}},{"uid":"401","start":{"x":616.2499999999997,"y":547.4999999999997},"end":{"x":563.7499999999997,"y":546.2499999999997}},{"uid":"403","start":{"x":359.9999999999998,"y":594.9999999999997},"end":{"x":369.9999999999998,"y":562.4999999999997}},{"uid":"404","start":{"x":306.24999999999983,"y":617.4999999999997},"end":{"x":321.24999999999983,"y":568.7499999999997}},{"uid":"408","start":{"x":2191.2499999999986,"y":203.7499999999999},"end":{"x":2223.7499999999986,"y":247.4999999999999}},{"uid":"409","start":{"x":2299.9999999999986,"y":282.49999999999983},"end":{"x":2263.7499999999986,"y":323.74999999999983}},{"uid":"412","start":{"x":608.7499999999997,"y":927.4999999999994},"end":{"x":538.7499999999997,"y":927.4999999999994}},{"uid":"415","start":{"x":607.4999999999997,"y":1006.2499999999994},"end":{"x":538.7499999999997,"y":1004.9999999999994}},{"uid":"419","start":{"x":2573.7499999999986,"y":346.24999999999983},"end":{"x":2579.9999999999986,"y":404.99999999999983}},{"uid":"420","start":{"x":2468.7499999999986,"y":273.74999999999983},"end":{"x":2521.2499999999986,"y":311.24999999999983}},{"uid":"422","start":{"x":2569.9999999999986,"y":442.49999999999983},"end":{"x":2576.2499999999986,"y":501.24999999999983}},{"uid":"424","start":{"x":2569.9999999999986,"y":529.9999999999998},"end":{"x":2576.2499999999986,"y":588.7499999999998}},{"uid":"426","start":{"x":2571.2499999999986,"y":619.9999999999997},"end":{"x":2577.4999999999986,"y":678.7499999999998}},{"uid":"428","start":{"x":2572.4999999999986,"y":709.9999999999998},"end":{"x":2578.7499999999986,"y":768.7499999999998}},{"uid":"430","start":{"x":2571.2499999999986,"y":912.4999999999997},"end":{"x":2577.4999999999986,"y":971.2499999999997}},{"uid":"432","start":{"x":2573.7499999999986,"y":1002.4999999999995},"end":{"x":2579.9999999999986,"y":1061.2499999999995}},{"uid":"434","start":{"x":2569.9999999999986,"y":809.9999999999994},"end":{"x":2576.2499999999986,"y":868.7499999999993}},{"uid":"436","start":{"x":1029.9999999999993,"y":307.49999999999983},"end":{"x":983.7499999999994,"y":302.49999999999983}}],"texts":[{"uid":"327","text":"// Not editing","topLeft":{"x":1475.0703125,"y":463.57031250000045}},{"uid":"328","text":"selectNext","topLeft":{"x":1942.709374999999,"y":868.334374999999}},{"uid":"329","text":"selectNext","topLeft":{"x":1732.209374999999,"y":1031.584374999999}},{"uid":"330","text":"selectNext","topLeft":{"x":1789.709374999999,"y":818.334374999999}},{"uid":"331","text":"after 500ms","topLeft":{"x":1942.709374999999,"y":760.334374999999}},{"uid":"332","text":"after 500ms","topLeft":{"x":1943.709374999999,"y":665.334374999999}},{"uid":"333","text":"after 500ms","topLeft":{"x":1893.459374999999,"y":1077.834374999999}},{"uid":"334","text":"after 500ms","topLeft":{"x":1894.459374999999,"y":982.834374999999}},{"uid":"335","text":"after 500ms","topLeft":{"x":1583.959374999999,"y":925.084374999999}},{"uid":"336","text":"after 500ms","topLeft":{"x":1584.959374999999,"y":830.084374999999}},{"uid":"337","text":"incSelection","topLeft":{"x":2200.559374999999,"y":792.734375}},{"uid":"338","text":"endEdit","topLeft":{"x":1344.4765625000002,"y":527.1640625000005}},{"uid":"339","text":"// TimeEditor","topLeft":{"x":1823.3192749023442,"y":470.5640563964847}},{"uid":"340","text":"beep","topLeft":{"x":496.3281249999976,"y":347.9687499999993}},{"uid":"341","text":"after 10ms","topLeft":{"x":386.32812499999795,"y":350.7812499999993}},{"uid":"342","text":"// Increasing","topLeft":{"x":452.93750000000045,"y":850.2031249999999}},{"uid":"343","text":"[m >= 60] / m=0; h=(h+1)%24;","topLeft":{"x":779.5249938964846,"y":828.086456298828}},{"uid":"344","text":"incTime / s =s+1;","topLeft":{"x":746.7416992187502,"y":1072.836456298828}},{"uid":"345","text":"[s >= 60] / s=0; m=m+1;","topLeft":{"x":919.5249938964844,"y":963.6489562988277}},{"uid":"346","text":"[s < 60]","topLeft":{"x":753.7750244140627,"y":1011.8364562988279}},{"uid":"347","text":"[m < 60]","topLeft":{"x":754.6250000000002,"y":885.2031249999999}},{"uid":"348","text":"entry /\n^_timeChanged","topLeft":{"x":449.95837402343795,"y":936.8906249999995}},{"uid":"349","text":"// TimeIncreasor","topLeft":{"x":714.683349609375,"y":752.910415649414}},{"uid":"350","text":"incChrono\n/ chs = chs+1","topLeft":{"x":752.9218749999999,"y":478.3697967529299}},{"uid":"351","text":"[cs >= 60] /\ncs=0;\ncm=(cm+1)%100;","topLeft":{"x":1080.5718688964844,"y":452.08645629882835}},{"uid":"352","text":"[chs >= 100] /\nchs=0; cs=cs+1;","topLeft":{"x":903.1968688964844,"y":578.3989562988284}},{"uid":"353","text":"[chs < 100]","topLeft":{"x":820.8218994140624,"y":431.08645629882835}},{"uid":"354","text":"[cm < 60]","topLeft":{"x":980.921875,"y":422.2031250000002}},{"uid":"355","text":"// sound","topLeft":{"x":431.8749999999985,"y":404.76562499999955}},{"uid":"356","text":"// no sound","topLeft":{"x":434.0624999999984,"y":289.14062499999955}},{"uid":"357","text":"// ChronoIncreasor","topLeft":{"x":882.1093749999993,"y":197.2656249999999}},{"uid":"358","text":"// BeepRequestHandler","topLeft":{"x":437.4999999999998,"y":188.4374999999999}},{"uid":"359","text":"entry / h=9;","topLeft":{"x":122.0625,"y":183.44583129882804}},{"uid":"360","text":"entry / m=0;","topLeft":{"x":125.0625,"y":213.44583129882778}},{"uid":"361","text":"entry / s=0;","topLeft":{"x":124.0625,"y":245.44583129882773}},{"uid":"362","text":"entry / cm=0;","topLeft":{"x":119.75,"y":420.3208312988279}},{"uid":"363","text":"entry / cs=0;","topLeft":{"x":117.1875,"y":452.7583312988279}},{"uid":"364","text":"entry / chs=0;","topLeft":{"x":121.1875,"y":488.9458312988279}},{"uid":"365","text":"entry / ah=9;","topLeft":{"x":125.125,"y":299.7583312988277}},{"uid":"366","text":"entry / am=0;","topLeft":{"x":128.25,"y":330.1958312988279}},{"uid":"367","text":"entry / as=5;","topLeft":{"x":125.3125,"y":360.1958312988279}},{"uid":"368","text":"// HideHours","topLeft":{"x":1883.8968749999995,"y":711.9593749999976}},{"uid":"369","text":"// HideMinutes","topLeft":{"x":1841.7093749999995,"y":1030.084374999998}},{"uid":"370","text":"// HideSeconds","topLeft":{"x":1666.7093749999995,"y":874.7718749999981}},{"uid":"371","text":"// DisplayingTime","topLeft":{"x":1423.289062500001,"y":288.9140624999991}},{"uid":"372","text":"[inState(\"DisplayingTime\")] /\nh = (h+1)%24","topLeft":{"x":2141.571874999999,"y":707.7093750000017}},{"uid":"373","text":"[inState(\"DisplayingAlarm\")] /\nah = (ah+1)%24","topLeft":{"x":2136.8218749999996,"y":642.459374999999}},{"uid":"374","text":"incSelection","topLeft":{"x":2145.9593749999995,"y":1093.584374999998}},{"uid":"375","text":"[inState(\"DisplayingTime\")] /\nm = (m+1)%60","topLeft":{"x":2148.9218749999995,"y":1002.8093749999985}},{"uid":"376","text":"[inState(\"DisplayingAlarm\")] /\nam = (am+1)%60","topLeft":{"x":2154.721875,"y":928.3093749999966}},{"uid":"377","text":"incSelection","topLeft":{"x":1376.7718750000006,"y":758.7093749999958}},{"uid":"378","text":"[inState(\"DisplayingTime\")]\n/ s = (s+1)%60","topLeft":{"x":1407.371875,"y":827.9093749999952}},{"uid":"379","text":"[inState(\"DisplayingAlarm\")]\n/ as = (as+1)%60","topLeft":{"x":1391.6718750000007,"y":952.0093749999946}},{"uid":"380","text":"// Editing","topLeft":{"x":1303.1249999999986,"y":588.1249999999997}},{"uid":"381","text":"beginEdit","topLeft":{"x":1523.749999999999,"y":524.9999999999997}},{"uid":"382","text":"// DisplayingAlarm","topLeft":{"x":1642.039062500001,"y":287.6640624999991}},{"uid":"383","text":"// DisplayingChrono","topLeft":{"x":1884.5390625000005,"y":287.6640624999991}},{"uid":"384","text":"displayTime","topLeft":{"x":1434.999999999999,"y":209.9999999999999}},{"uid":"385","text":"displayAlarm","topLeft":{"x":1643.7499999999989,"y":216.2499999999999}},{"uid":"386","text":"displayChrono","topLeft":{"x":1892.4999999999986,"y":214.9999999999999}},{"uid":"393","text":"// light off","topLeft":{"x":464.9999999999997,"y":549.9999999999997}},{"uid":"394","text":"// light on","topLeft":{"x":466.2499999999998,"y":649.9999999999997}},{"uid":"395","text":"lightOn","topLeft":{"x":588.7499999999997,"y":618.7499999999997}},{"uid":"402","text":"lightOff","topLeft":{"x":592.4999999999997,"y":533.7499999999997}},{"uid":"407","text":"entry / alarm=false;","topLeft":{"x":121.24999999999991,"y":559.9999999999997}},{"uid":"410","text":"setAlarm(onOrOff)\n/ alarm = onOrOff","topLeft":{"x":2306.2499999999986,"y":296.24999999999983}},{"uid":"413","text":"[alarm\n &&(h==ah)\n &&(m==am)\n &&(s==as)]\n/ ^alarm","topLeft":{"x":573.7499999999997,"y":892.4999999999994}},{"uid":"416","text":"[!(alarm\n &&(h==ah)\n &&(m==am)\n &&(s==as))]","topLeft":{"x":578.7499999999997,"y":997.4999999999994}},{"uid":"421","text":"topLeftPressed / ^topLeftPressed","topLeft":{"x":2601.2499999999986,"y":369.99999999999983}},{"uid":"423","text":"topLeftReleased / ^topLeftReleased","topLeft":{"x":2597.4999999999986,"y":466.24999999999983}},{"uid":"425","text":"topRightPressed / ^topRightPressed","topLeft":{"x":2597.4999999999986,"y":553.7499999999998}},{"uid":"427","text":"topRightReleased / ^topRightReleased","topLeft":{"x":2598.7499999999986,"y":643.7499999999998}},{"uid":"429","text":"bottomRightPressed / ^bottomRightPressed","topLeft":{"x":2599.9999999999986,"y":733.7499999999998}},{"uid":"431","text":"bottomLeftPressed / ^bottomLeftPressed","topLeft":{"x":2598.7499999999986,"y":936.2499999999997}},{"uid":"433","text":"bottomLeftReleased / ^bottomLeftReleased","topLeft":{"x":2601.2499999999986,"y":1026.2499999999995}},{"uid":"435","text":"bottomRightReleased / ^bottomRightReleased","topLeft":{"x":2597.4999999999986,"y":833.7499999999994}},{"uid":"437","text":"resetChrono / chs=0; cs=0; cm=0","topLeft":{"x":1012.4999999999994,"y":281.24999999999983}}]} \ No newline at end of file +{"rountangles":[{"uid":"605","topLeft":{"x":258.8281250000001,"y":127.34374999999989},"size":{"x":2123.8281249999986,"y":1096.3281249999998},"kind":"and"},{"uid":"606","topLeft":{"x":1188.5859375,"y":404.33593750000057},"size":{"x":1173.140625,"y":793.296875},"kind":"or"},{"uid":"607","topLeft":{"x":1240.1593750000002,"y":551.365624999999},"size":{"x":1069.7749999999987,"y":602.15625},"kind":"or"},{"uid":"608","topLeft":{"x":280.3125000000002,"y":722.352081298828},"size":{"x":879.4374999999998,"y":478.2812500000001},"kind":"or"},{"uid":"610","topLeft":{"x":658.7031249999999,"y":161.6197967529296},"size":{"x":500.15625,"y":539.2500000000002},"kind":"or"},{"uid":"611","topLeft":{"x":1185.9109375000012,"y":163.97656249999932},"size":{"x":928.378124999999,"y":216.07812499999991},"kind":"or"},{"uid":"612","topLeft":{"x":1218.7499999999993,"y":183.7499999999999},"size":{"x":802.4999999999995,"y":173.74999999999994},"kind":"or"},{"uid":"613","topLeft":{"x":281.95312499999795,"y":160.07812499999966},"size":{"x":360.3124999999998,"y":281.95312499999983},"kind":"or"},{"uid":"614","topLeft":{"x":279.99999999999983,"y":463.7499999999997},"size":{"x":361.2499999999999,"y":237.49999999999991},"kind":"or"},{"uid":"615","topLeft":{"x":1476.7093749999988,"y":717.209374999999},"size":{"x":267,"y":249.125},"kind":"or"},{"uid":"617","topLeft":{"x":1824.709374999999,"y":588.334374999999},"size":{"x":258.5999999999999,"y":216.8},"kind":"or"},{"uid":"618","topLeft":{"x":327.49999999999983,"y":497.49999999999966},"size":{"x":293.74999999999994,"y":188.74999999999994},"kind":"or"},{"uid":"619","topLeft":{"x":373.7500000000001,"y":804.832290649414},"size":{"x":160.9375,"y":307.75},"kind":"and"},{"uid":"620","topLeft":{"x":2142.4999999999986,"y":163.74999999999994},"size":{"x":216.2499999999999,"y":212.49999999999983},"kind":"or"},{"uid":"621","topLeft":{"x":1797.459374999999,"y":931.4343749999962},"size":{"x":241.3999999999998,"y":186.4000000000002},"kind":"or"},{"uid":"622","topLeft":{"x":710.9218749999999,"y":308.36979675293003},"size":{"x":367,"y":65.00000000000001},"kind":"and"},{"uid":"623","topLeft":{"x":1318.1328125,"y":427.21093750000045},"size":{"x":307.6249999999999,"y":67.390625},"kind":"and"},{"uid":"624","topLeft":{"x":1790.9765625000007,"y":239.7265624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"625","topLeft":{"x":1329.726562500001,"y":240.9765624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"626","topLeft":{"x":1548.476562500001,"y":239.7265624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"627","topLeft":{"x":1621.959374999999,"y":808.084374999999},"size":{"x":94,"y":136},"kind":"and"},{"uid":"628","topLeft":{"x":1850.709374999999,"y":644.334374999999},"size":{"x":78,"y":135},"kind":"and"},{"uid":"629","topLeft":{"x":373.7499999999998,"y":518.7499999999997},"size":{"x":186.24999999999997,"y":55.00000000000006},"kind":"and"},{"uid":"630","topLeft":{"x":376.2499999999998,"y":617.4999999999997},"size":{"x":177.49999999999994,"y":56.25},"kind":"and"},{"uid":"631","topLeft":{"x":342.5781249999975,"y":261.71874999999955},"size":{"x":190.00000000000006,"y":51.562499999999986},"kind":"and"},{"uid":"632","topLeft":{"x":333.82812499999795,"y":377.0312499999993},"size":{"x":194.6875,"y":44.68749999999996},"kind":"and"},{"uid":"633","topLeft":{"x":1817.459374999999,"y":961.834374999999},"size":{"x":61.99999999999994,"y":135},"kind":"and"},{"uid":"634","topLeft":{"x":1507.959374999999,"y":809.084374999999},"size":{"x":61.99999999999994,"y":135},"kind":"and"},{"uid":"635","topLeft":{"x":2169.9999999999986,"y":249.99999999999983},"size":{"x":123.74999999999997,"y":65},"kind":"and"},{"uid":"636","topLeft":{"x":1980.709374999999,"y":643.334374999999},"size":{"x":45.99999999999994,"y":136},"kind":"and"},{"uid":"637","topLeft":{"x":1931.459374999999,"y":960.834374999999},"size":{"x":45.99999999999994,"y":136},"kind":"and"}],"diamonds":[{"uid":"638","topLeft":{"x":876.99169921875,"y":827.648956298828},"size":{"x":72.99999999999997,"y":81.99999999999997}},{"uid":"639","topLeft":{"x":888.99169921875,"y":1013.3989562988279},"size":{"x":58.00000000000003,"y":77.99999999999997}},{"uid":"640","topLeft":{"x":990.03857421875,"y":522.0864562988284},"size":{"x":85.00000000000003,"y":76.99999999999997}},{"uid":"641","topLeft":{"x":739.0385742187499,"y":513.1489562988284},"size":{"x":97.00000000000003,"y":77.99999999999997}},{"uid":"642","topLeft":{"x":2225.5718749999996,"y":600.5593749999953},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"643","topLeft":{"x":2235.971875,"y":930.1593749999934},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"644","topLeft":{"x":1254.2718749999997,"y":756.9093749999988},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"645","topLeft":{"x":607.4999999999997,"y":821.2499999999995},"size":{"x":56.24999999999997,"y":217.4999999999999}}],"history":[],"arrows":[{"uid":"646","start":{"x":1962.709374999999,"y":808.334374999999},"end":{"x":1852.459374999999,"y":956.834374999999}},{"uid":"647","start":{"x":1799.459374999999,"y":1080.834374999999},"end":{"x":1674.959374999999,"y":947.084374999999}},{"uid":"648","start":{"x":1751.709374999999,"y":856.334374999999},"end":{"x":1847.709374999999,"y":753.334374999999}},{"uid":"649","start":{"x":1252.5078125,"y":450.25781250000045},"end":{"x":1308.7421875,"y":459.14843750000045}},{"uid":"650","start":{"x":1721.4093749999988,"y":593.1843750000039},"end":{"x":1820.8593749999986,"y":612.9343750000021}},{"uid":"651","start":{"x":1972.709374999999,"y":683.334374999999},"end":{"x":1932.709374999999,"y":683.334374999999}},{"uid":"652","start":{"x":1927.709374999999,"y":753.334374999999},"end":{"x":1973.709374999999,"y":751.334374999999}},{"uid":"653","start":{"x":1913.5093749999987,"y":609.9343750000007},"end":{"x":1915.309374999999,"y":638.7343750000009}},{"uid":"654","start":{"x":1923.459374999999,"y":1000.834374999999},"end":{"x":1883.459374999999,"y":1000.834374999999}},{"uid":"655","start":{"x":1878.459374999999,"y":1070.834374999999},"end":{"x":1924.459374999999,"y":1068.834374999999}},{"uid":"656","start":{"x":2018.2593749999987,"y":1079.4343749999998},"end":{"x":1980.459374999999,"y":1053.834374999999}},{"uid":"657","start":{"x":1613.959374999999,"y":848.084374999999},"end":{"x":1573.959374999999,"y":848.084374999999}},{"uid":"658","start":{"x":1568.959374999999,"y":918.084374999999},"end":{"x":1614.959374999999,"y":916.084374999999}},{"uid":"659","start":{"x":2086.5093749999987,"y":758.7343749999982},"end":{"x":2238.309374999999,"y":775.1343749999992}},{"uid":"660","start":{"x":1345.8515625000002,"y":542.0390625000005},"end":{"x":1345.6015625000002,"y":500.35156250000045}},{"uid":"661","start":{"x":1537.6760498046865,"y":754.7260375976552},"end":{"x":1536.4260498046865,"y":805.3510375976552}},{"uid":"662","start":{"x":366.9531249999975,"y":225.46874999999955},"end":{"x":406.9531249999975,"y":255.46874999999977}},{"uid":"663","start":{"x":490.7031249999976,"y":321.7187499999993},"end":{"x":489.7656249999976,"y":370.1562499999993}},{"uid":"664","start":{"x":393.20312499999795,"y":373.9062499999993},"end":{"x":392.26562499999795,"y":322.3437499999993}},{"uid":"665","start":{"x":878.5249938964844,"y":891.648956298828},"end":{"x":663.3374938964846,"y":894.648956298828}},{"uid":"666","start":{"x":882.5249938964844,"y":841.648956298828},"end":{"x":664.3374938964846,"y":841.648956298828}},{"uid":"667","start":{"x":531.0541992187502,"y":1078.648956298828},"end":{"x":883.99169921875,"y":1084.648956298828}},{"uid":"668","start":{"x":885.7749938964844,"y":1021.3989562988279},"end":{"x":665.5874938964846,"y":1021.3989562988279}},{"uid":"669","start":{"x":917.5249938964844,"y":1012.3989562988279},"end":{"x":915.5249938964844,"y":919.6489562988279}},{"uid":"670","start":{"x":324.7500000000001,"y":783.586456298828},"end":{"x":377.7500000000001,"y":802.586456298828}},{"uid":"671","start":{"x":757.9218749999999,"y":380.36979675293003},"end":{"x":756.9218749999999,"y":524.3697967529299}},{"uid":"672","start":{"x":1058.5718688964844,"y":528.0864562988284},"end":{"x":1055.5718688964844,"y":380.08645629882847}},{"uid":"673","start":{"x":832.0385742187498,"y":559.0864562988284},"end":{"x":976.03857421875,"y":561.0864562988284}},{"uid":"674","start":{"x":820.5718688964843,"y":524.0864562988284},"end":{"x":820.5718688964843,"y":380.08645629882847}},{"uid":"675","start":{"x":985.4468688964844,"y":530.2114562988284},"end":{"x":986.5718688964844,"y":378.08645629882847}},{"uid":"676","start":{"x":735.2343749999994,"y":219.1406249999999},"end":{"x":774.9218749999994,"y":301.0156249999999}},{"uid":"677","start":{"x":273.4374999999999,"y":68.74999999999993},"end":{"x":343.7499999999999,"y":120.31249999999994}},{"uid":"678","start":{"x":1254.851562500001,"y":273.9765624999991},"end":{"x":1318.289062500001,"y":270.6640624999991}},{"uid":"679","start":{"x":2228.3718749999994,"y":695.159374999997},"end":{"x":2032.3718749999994,"y":710.9593749999999}},{"uid":"680","start":{"x":2243.1718749999995,"y":619.3593749999973},"end":{"x":2032.7718749999995,"y":669.759375000001}},{"uid":"681","start":{"x":2043.0593749999994,"y":1104.4343749999957},"end":{"x":2248.709375,"y":1104.7343749999968}},{"uid":"682","start":{"x":2238.7718750000004,"y":1024.7593749999946},"end":{"x":2042.521875,"y":1031.8593749999968}},{"uid":"683","start":{"x":2253.571875,"y":948.9593749999954},"end":{"x":2044.521875,"y":962.6593749999961}},{"uid":"684","start":{"x":1478.1218749999991,"y":759.0093749999969},"end":{"x":1295.0718749999999,"y":773.3093749999966}},{"uid":"685","start":{"x":1317.471875,"y":850.909374999997},"end":{"x":1508.0218749999995,"y":868.5093749999978}},{"uid":"686","start":{"x":1289.4718750000004,"y":936.5093749999978},"end":{"x":1503.2218750000004,"y":925.3093749999957}},{"uid":"687","start":{"x":1521.249999999999,"y":496.2499999999997},"end":{"x":1518.749999999999,"y":549.9999999999997}},{"uid":"688","start":{"x":1432.499999999999,"y":186.2499999999999},"end":{"x":1431.249999999999,"y":231.2499999999999}},{"uid":"689","start":{"x":1642.4999999999989,"y":186.24999999999991},"end":{"x":1643.7499999999989,"y":233.74999999999991}},{"uid":"690","start":{"x":1888.7499999999986,"y":189.99999999999991},"end":{"x":1888.7499999999986,"y":232.49999999999991}},{"uid":"691","start":{"x":616.2499999999997,"y":633.7499999999997},"end":{"x":563.7499999999997,"y":634.9999999999997}},{"uid":"692","start":{"x":2087.4999999999986,"y":239.99999999999986},"end":{"x":2029.9999999999986,"y":241.24999999999986}},{"uid":"693","start":{"x":616.2499999999997,"y":547.4999999999997},"end":{"x":563.7499999999997,"y":546.2499999999997}},{"uid":"694","start":{"x":359.9999999999998,"y":594.9999999999997},"end":{"x":369.9999999999998,"y":562.4999999999997}},{"uid":"695","start":{"x":306.24999999999983,"y":617.4999999999997},"end":{"x":321.24999999999983,"y":568.7499999999997}},{"uid":"696","start":{"x":2191.2499999999986,"y":203.7499999999999},"end":{"x":2223.7499999999986,"y":247.4999999999999}},{"uid":"697","start":{"x":2299.9999999999986,"y":282.49999999999983},"end":{"x":2263.7499999999986,"y":323.74999999999983}},{"uid":"698","start":{"x":608.7499999999997,"y":927.4999999999994},"end":{"x":538.7499999999997,"y":927.4999999999994}},{"uid":"699","start":{"x":607.4999999999997,"y":1006.2499999999994},"end":{"x":538.7499999999997,"y":1004.9999999999994}},{"uid":"709","start":{"x":1029.9999999999993,"y":307.49999999999983},"end":{"x":983.7499999999994,"y":302.49999999999983}}],"texts":[{"uid":"710","text":"// Not editing","topLeft":{"x":1475.0703125,"y":463.57031250000045}},{"uid":"711","text":"selectNext","topLeft":{"x":1942.709374999999,"y":868.334374999999}},{"uid":"712","text":"selectNext","topLeft":{"x":1732.209374999999,"y":1031.584374999999}},{"uid":"713","text":"selectNext","topLeft":{"x":1789.709374999999,"y":818.334374999999}},{"uid":"714","text":"after 500ms","topLeft":{"x":1942.709374999999,"y":760.334374999999}},{"uid":"715","text":"after 500ms","topLeft":{"x":1943.709374999999,"y":665.334374999999}},{"uid":"716","text":"after 500ms","topLeft":{"x":1893.459374999999,"y":1077.834374999999}},{"uid":"717","text":"after 500ms","topLeft":{"x":1894.459374999999,"y":982.834374999999}},{"uid":"718","text":"after 500ms","topLeft":{"x":1583.959374999999,"y":925.084374999999}},{"uid":"719","text":"after 500ms","topLeft":{"x":1584.959374999999,"y":830.084374999999}},{"uid":"720","text":"incSelection","topLeft":{"x":2200.559374999999,"y":792.734375}},{"uid":"721","text":"endEdit","topLeft":{"x":1344.4765625000002,"y":527.1640625000005}},{"uid":"722","text":"// TimeEditor","topLeft":{"x":1823.3192749023442,"y":470.5640563964847}},{"uid":"723","text":"beep","topLeft":{"x":496.3281249999976,"y":347.9687499999993}},{"uid":"724","text":"after 10ms","topLeft":{"x":386.32812499999795,"y":350.7812499999993}},{"uid":"725","text":"// Increasing","topLeft":{"x":452.93750000000045,"y":850.2031249999999}},{"uid":"726","text":"[m >= 60] / m=0; h=(h+1)%24;","topLeft":{"x":779.5249938964846,"y":828.086456298828}},{"uid":"727","text":"incTime / s =s+1;","topLeft":{"x":746.7416992187502,"y":1072.836456298828}},{"uid":"728","text":"[s >= 60] / s=0; m=m+1;","topLeft":{"x":919.5249938964844,"y":963.6489562988277}},{"uid":"729","text":"[s < 60]","topLeft":{"x":753.7750244140627,"y":1011.8364562988279}},{"uid":"730","text":"[m < 60]","topLeft":{"x":754.6250000000002,"y":885.2031249999999}},{"uid":"731","text":"entry /\n^_timeChanged","topLeft":{"x":449.95837402343795,"y":936.8906249999995}},{"uid":"732","text":"// TimeIncreasor","topLeft":{"x":714.683349609375,"y":752.910415649414}},{"uid":"733","text":"incChrono\n/ chs = chs+1","topLeft":{"x":752.9218749999999,"y":478.3697967529299}},{"uid":"734","text":"[cs >= 60] /\ncs=0;\ncm=(cm+1)%100;","topLeft":{"x":1080.5718688964844,"y":452.08645629882835}},{"uid":"735","text":"[chs >= 100] /\nchs=0; cs=cs+1;","topLeft":{"x":903.1968688964844,"y":578.3989562988284}},{"uid":"736","text":"[chs < 100]","topLeft":{"x":820.8218994140624,"y":431.08645629882835}},{"uid":"737","text":"[cm < 60]","topLeft":{"x":980.921875,"y":422.2031250000002}},{"uid":"738","text":"// sound","topLeft":{"x":431.8749999999985,"y":404.76562499999955}},{"uid":"739","text":"// no sound","topLeft":{"x":434.0624999999984,"y":289.14062499999955}},{"uid":"740","text":"// ChronoIncreasor","topLeft":{"x":882.1093749999993,"y":197.2656249999999}},{"uid":"741","text":"// BeepRequestHandler","topLeft":{"x":437.4999999999998,"y":188.4374999999999}},{"uid":"742","text":"entry / h=9;","topLeft":{"x":122.0625,"y":183.44583129882804}},{"uid":"743","text":"entry / m=0;","topLeft":{"x":125.0625,"y":213.44583129882778}},{"uid":"744","text":"entry / s=0;","topLeft":{"x":124.0625,"y":245.44583129882773}},{"uid":"745","text":"entry / cm=0;","topLeft":{"x":119.75,"y":420.3208312988279}},{"uid":"746","text":"entry / cs=0;","topLeft":{"x":117.1875,"y":452.7583312988279}},{"uid":"747","text":"entry / chs=0;","topLeft":{"x":121.1875,"y":488.9458312988279}},{"uid":"748","text":"entry / ah=9;","topLeft":{"x":125.125,"y":299.7583312988277}},{"uid":"749","text":"entry / am=0;","topLeft":{"x":128.25,"y":330.1958312988279}},{"uid":"750","text":"entry / as=5;","topLeft":{"x":125.3125,"y":360.1958312988279}},{"uid":"751","text":"// HideHours","topLeft":{"x":1883.8968749999995,"y":711.9593749999976}},{"uid":"752","text":"// HideMinutes","topLeft":{"x":1841.7093749999995,"y":1030.084374999998}},{"uid":"753","text":"// HideSeconds","topLeft":{"x":1666.7093749999995,"y":874.7718749999981}},{"uid":"754","text":"// DisplayingTime","topLeft":{"x":1423.289062500001,"y":288.9140624999991}},{"uid":"755","text":"[inState(\"DisplayingTime\")] /\nh = (h+1)%24","topLeft":{"x":2141.571874999999,"y":707.7093750000017}},{"uid":"756","text":"[inState(\"DisplayingAlarm\")] /\nah = (ah+1)%24","topLeft":{"x":2136.8218749999996,"y":642.459374999999}},{"uid":"757","text":"incSelection","topLeft":{"x":2145.9593749999995,"y":1093.584374999998}},{"uid":"758","text":"[inState(\"DisplayingTime\")] /\nm = (m+1)%60","topLeft":{"x":2148.9218749999995,"y":1002.8093749999985}},{"uid":"759","text":"[inState(\"DisplayingAlarm\")] /\nam = (am+1)%60","topLeft":{"x":2154.721875,"y":928.3093749999966}},{"uid":"760","text":"incSelection","topLeft":{"x":1376.7718750000006,"y":758.7093749999958}},{"uid":"761","text":"[inState(\"DisplayingTime\")]\n/ s = (s+1)%60","topLeft":{"x":1407.371875,"y":827.9093749999952}},{"uid":"762","text":"[inState(\"DisplayingAlarm\")]\n/ as = (as+1)%60","topLeft":{"x":1391.6718750000007,"y":952.0093749999946}},{"uid":"763","text":"// Editing","topLeft":{"x":1303.1249999999986,"y":588.1249999999997}},{"uid":"764","text":"beginEdit","topLeft":{"x":1523.749999999999,"y":524.9999999999997}},{"uid":"765","text":"// DisplayingAlarm","topLeft":{"x":1642.039062500001,"y":287.6640624999991}},{"uid":"766","text":"// DisplayingChrono","topLeft":{"x":1884.5390625000005,"y":287.6640624999991}},{"uid":"767","text":"displayTime","topLeft":{"x":1434.999999999999,"y":209.9999999999999}},{"uid":"768","text":"displayAlarm","topLeft":{"x":1643.7499999999989,"y":216.2499999999999}},{"uid":"769","text":"displayChrono","topLeft":{"x":1892.4999999999986,"y":214.9999999999999}},{"uid":"770","text":"// light off","topLeft":{"x":464.9999999999997,"y":549.9999999999997}},{"uid":"771","text":"// light on","topLeft":{"x":466.2499999999998,"y":649.9999999999997}},{"uid":"772","text":"lightOn","topLeft":{"x":588.7499999999997,"y":618.7499999999997}},{"uid":"773","text":"lightOff","topLeft":{"x":592.4999999999997,"y":533.7499999999997}},{"uid":"774","text":"entry / alarm=false;","topLeft":{"x":121.24999999999991,"y":559.9999999999997}},{"uid":"775","text":"setAlarm(onOrOff)\n/ alarm = onOrOff","topLeft":{"x":2306.2499999999986,"y":296.24999999999983}},{"uid":"776","text":"[alarm\n &&(h==ah)\n &&(m==am)\n &&(s==as)]\n/ ^alarm","topLeft":{"x":573.7499999999997,"y":892.4999999999994}},{"uid":"777","text":"[!(alarm\n &&(h==ah)\n &&(m==am)\n &&(s==as))]","topLeft":{"x":578.7499999999997,"y":997.4999999999994}},{"uid":"786","text":"resetChrono / chs=0; cs=0; cm=0","topLeft":{"x":1012.4999999999994,"y":281.24999999999983}}]} \ No newline at end of file diff --git a/src/App/Plant/Dummy/Dummy.tsx b/src/App/Plant/Dummy/Dummy.tsx index 917ccde..90d581a 100644 --- a/src/App/Plant/Dummy/Dummy.tsx +++ b/src/App/Plant/Dummy/Dummy.tsx @@ -8,7 +8,8 @@ export const dummyExecution: TimedReactive = { extTransition: () => [[], null], }; -export const DummyPlant: Plant = { +export const dummyPlant: Plant = { + uiEvents: [], inputEvents: [], outputEvents: [], execution: dummyExecution, diff --git a/src/App/Plant/Microwave/Microwave.tsx b/src/App/Plant/Microwave/Microwave.tsx index 93db7a2..033f22b 100644 --- a/src/App/Plant/Microwave/Microwave.tsx +++ b/src/App/Plant/Microwave/Microwave.tsx @@ -8,14 +8,24 @@ import fontDigital from "../DigitalWatch/digital-font.ttf"; import sndBell from "./bell.wav"; import sndRunning from "./running.wav"; -import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types"; +import { RT_Statechart } from "@/statecharts/runtime_types"; import { useEffect } from "react"; import "./Microwave.css"; import { useAudioContext } from "../../useAudioContext"; -import { Plant, PlantRenderProps } from "../Plant"; -import { statechartExecution } from "@/statecharts/timed_reactive"; -import { microwaveAbstractSyntax } from "./model"; +import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant"; +import { detectConnections } from "@/statecharts/detect_connections"; +import { parseStatechart } from "@/statecharts/parser"; + +import microwaveConcreteSyntax from "./model.json"; +import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor"; + +export const [microwaveAbstractSyntax, microwaveErrors] = parseStatechart(microwaveConcreteSyntax as ConcreteSyntax, detectConnections(microwaveConcreteSyntax as ConcreteSyntax)); + +if (microwaveErrors.length > 0) { + console.log({microwaveErrors}); + throw new Error("there were errors parsing microwave plant model. see console.") +} const imgs = { @@ -37,7 +47,7 @@ const DOOR_Y0 = 68; const DOOR_WIDTH = 353; const DOOR_HEIGHT = 217; -export function Microwave({state, speed, raiseInput}: PlantRenderProps) { +export function Microwave({state, speed, raiseUIEvent}: PlantRenderProps) { const [playSound, preloadAudio] = useAudioContext(speed); // preload(imgSmallClosedOff, {as: "image"}); @@ -48,9 +58,9 @@ export function Microwave({state, speed, raiseInput}: PlantRenderProps raiseInput({name: "startPressed"})} - onMouseUp={() => raiseInput({name: "startReleased"})} + onMouseDown={() => raiseUIEvent({name: "startPressed"})} + onMouseUp={() => raiseUIEvent({name: "startReleased"})} /> raiseInput({name: "stopPressed"})} - onMouseUp={() => raiseInput({name: "stopReleased"})} + onMouseDown={() => raiseUIEvent({name: "stopPressed"})} + onMouseUp={() => raiseUIEvent({name: "stopReleased"})} /> raiseInput({name: "incTimePressed"})} - onMouseUp={() => raiseInput({name: "incTimeReleased"})} + onMouseDown={() => raiseUIEvent({name: "incTimePressed"})} + onMouseUp={() => raiseUIEvent({name: "incTimeReleased"})} /> raiseInput({name: "doorMouseDown"})} - onMouseUp={() => raiseInput({name: "doorMouseUp"})} + onMouseDown={() => raiseUIEvent({name: "doorMouseDown"})} + onMouseUp={() => raiseUIEvent({name: "doorMouseUp"})} /> {timeDisplay} ; } -export const MicrowavePlant: Plant = { - inputEvents: [ - // events coming from statechart - {kind: "event", event: "setTimeDisplay", paramName: "t"}, - {kind: "event", event: "setMagnetron", paramName: "state"}, - {kind: "event", event: "ringBell"}, - - // events coming from UI: +const microwavePlantSpec: StatechartPlantSpec = { + ast: microwaveAbstractSyntax, + render: Microwave, + uiEvents: [ {kind: "event", event: "doorMouseDown"}, {kind: "event", event: "doorMouseUp"}, {kind: "event", event: "startPressed"}, - {kind: "event", event: "stopPressed"}, - {kind: "event", event: "incTimePressed"}, {kind: "event", event: "startReleased"}, + {kind: "event", event: "stopPressed"}, {kind: "event", event: "stopReleased"}, + {kind: "event", event: "incTimePressed"}, {kind: "event", event: "incTimeReleased"}, ], - outputEvents: [ - {kind: "event", event: "door", paramName: "state"}, - {kind: "event", event: "startPressed"}, - {kind: "event", event: "stopPressed"}, - {kind: "event", event: "incTimePressed"}, - {kind: "event", event: "startReleased"}, - {kind: "event", event: "stopReleased"}, - {kind: "event", event: "incTimeReleased"}, - ], - execution: statechartExecution(microwaveAbstractSyntax), - render: Microwave, } + +export const microwavePlant = makeStatechartPlant(microwavePlantSpec); diff --git a/src/App/Plant/Microwave/model.json b/src/App/Plant/Microwave/model.json new file mode 100644 index 0000000..26f5a70 --- /dev/null +++ b/src/App/Plant/Microwave/model.json @@ -0,0 +1 @@ +{"rountangles":[{"uid":"0","topLeft":{"x":53.99999999999977,"y":125.99999999999989},"size":{"x":1804.7499999999995,"y":623.9999999999997},"kind":"and"},{"uid":"2","topLeft":{"x":546.9999999999994,"y":162.99999999999983},"size":{"x":482.99999999999943,"y":554.9999999999998},"kind":"or"},{"uid":"3","topLeft":{"x":88.99999999999974,"y":159.99999999999983},"size":{"x":427.9999999999998,"y":556.9999999999998},"kind":"or"},{"uid":"1","topLeft":{"x":1432.999999999999,"y":164.9999999999999},"size":{"x":399.2499999999999,"y":553.9999999999997},"kind":"or"},{"uid":"4","topLeft":{"x":587.9999999999993,"y":229.99999999999983},"size":{"x":392.9999999999999,"y":462.99999999999983},"kind":"or"},{"uid":"5","topLeft":{"x":1063.999999999999,"y":169.9999999999999},"size":{"x":328.9999999999999,"y":549.9999999999998},"kind":"or"},{"uid":"7","topLeft":{"x":261.9999999999997,"y":539.9999999999997},"size":{"x":198.9999999999999,"y":124.99999999999994},"kind":"and"},{"uid":"8","topLeft":{"x":757.9999999999993,"y":540.9999999999997},"size":{"x":183.99999999999994,"y":108.99999999999994},"kind":"and"},{"uid":"9","topLeft":{"x":273.9999999999997,"y":287.9999999999998},"size":{"x":183.99999999999994,"y":94.99999999999994},"kind":"and"},{"uid":"10","topLeft":{"x":1157.9999999999989,"y":295.9999999999998},"size":{"x":156.99999999999994,"y":102.99999999999996},"kind":"and"},{"uid":"11","topLeft":{"x":760.9999999999993,"y":305.9999999999998},"size":{"x":173.9999999999999,"y":87.99999999999996},"kind":"and"},{"uid":"6","topLeft":{"x":1492.7499999999989,"y":385.2499999999997},"size":{"x":148.99999999999994,"y":101.24999999999991},"kind":"and"},{"uid":"12","topLeft":{"x":1162.9999999999989,"y":560.9999999999997},"size":{"x":162.99999999999997,"y":89.99999999999999},"kind":"and"},{"uid":"13","topLeft":{"x":100.49999999999993,"y":426.9999999999997},"size":{"x":181.49999999999994,"y":73.99999999999997},"kind":"and"}],"diamonds":[{"uid":"14","topLeft":{"x":686.9999999999993,"y":447.99999999999966},"size":{"x":59,"y":60}}],"history":[],"arrows":[{"uid":"15","start":{"x":405.99999999999955,"y":382.9999999999998},"end":{"x":404.99999999999955,"y":530.9999999999998}},{"uid":"16","start":{"x":308.9999999999997,"y":536.9999999999997},"end":{"x":240.99999999999972,"y":503.99999999999966}},{"uid":"17","start":{"x":307.9999999999997,"y":233.99999999999977},"end":{"x":339.99999999999966,"y":284.9999999999998}},{"uid":"18","start":{"x":231.99999999999972,"y":71.99999999999991},"end":{"x":274.9999999999997,"y":119.99999999999983}},{"uid":"19","start":{"x":588.9999999999993,"y":482.99999999999966},"end":{"x":681.9999999999993,"y":481.99999999999966}},{"uid":"20","start":{"x":820.9999999999993,"y":264.99999999999983},"end":{"x":837.9999999999993,"y":295.99999999999983}},{"uid":"21","start":{"x":659.9999999999993,"y":190.99999999999986},"end":{"x":700.9999999999993,"y":223.99999999999986}},{"uid":"22","start":{"x":720.9999999999993,"y":506.99999999999966},"end":{"x":759.9999999999993,"y":542.9999999999997}},{"uid":"23","start":{"x":716.9999999999993,"y":447.99999999999966},"end":{"x":762.9999999999993,"y":387.99999999999966}},{"uid":"24","start":{"x":1192.9999999999989,"y":227.99999999999983},"end":{"x":1235.9999999999989,"y":292.99999999999983}},{"uid":"25","start":{"x":1289.9999999999989,"y":399.9999999999997},"end":{"x":1290.9999999999989,"y":558.9999999999997}},{"uid":"26","start":{"x":1199.9999999999989,"y":557.9999999999997},"end":{"x":1198.9999999999989,"y":402.99999999999966}},{"uid":"27","start":{"x":1646.2499999999989,"y":435.49999999999966},"end":{"x":1593.2499999999989,"y":491.49999999999966}},{"uid":"28","start":{"x":1507.7499999999989,"y":343.4999999999998},"end":{"x":1526.7499999999989,"y":379.2499999999998}},{"uid":"35","start":{"x":209.9999999999999,"y":419.9999999999997},"end":{"x":265.9999999999999,"y":368.9999999999997}}],"texts":[{"uid":"36","text":"// Door closed","topLeft":{"x":365.99999999999955,"y":323.99999999999983}},{"uid":"37","text":"// Door opened","topLeft":{"x":360.9999999999993,"y":581.9999999999997}},{"uid":"38","text":"entry / ^door(\"closed\")","topLeft":{"x":188.7499999999995,"y":477.7499999999998}},{"uid":"39","text":"entry / ^door(\"open\")","topLeft":{"x":360.99999999999966,"y":631.9999999999997}},{"uid":"40","text":"doorMouseDown","topLeft":{"x":260.9999999999996,"y":527.9999999999998}},{"uid":"41","text":"doorMouseUp","topLeft":{"x":402.99999999999955,"y":474.9999999999998}},{"uid":"42","text":"setMagnetron(state)","topLeft":{"x":645.9999999999993,"y":469.99999999999955}},{"uid":"43","text":"[state==\"on\"]","topLeft":{"x":747.9999999999991,"y":526.9999999999997}},{"uid":"44","text":"[state == \"off\"]","topLeft":{"x":735.9999999999993,"y":428.9999999999997}},{"uid":"45","text":"// Magnetron off","topLeft":{"x":847.9999999999991,"y":353.9999999999998}},{"uid":"46","text":"// Magnetron on","topLeft":{"x":853.9999999999991,"y":602.9999999999995}},{"uid":"47","text":"ringBell","topLeft":{"x":1298.9999999999989,"y":485.99999999999966}},{"uid":"48","text":"after 10 ms","topLeft":{"x":1192.9999999999989,"y":479.99999999999966}},{"uid":"49","text":"// bell","topLeft":{"x":1241.9999999999989,"y":605.9999999999995}},{"uid":"50","text":"// no bell","topLeft":{"x":1234.9999999999989,"y":353.9999999999998}},{"uid":"51","text":"setTimeDisplay(value) / timeDisplay = value;","topLeft":{"x":1656.2499999999989,"y":463.4999999999998}},{"uid":"52","text":"entry / timeDisplay = 0;","topLeft":{"x":464.9999999999997,"y":91.99999999999996}},{"uid":"59","text":"doorMouseUp","topLeft":{"x":230.99999999999986,"y":404.9999999999998}},{"uid":"60","text":"// closing door","topLeft":{"x":186.9999999999999,"y":453.7499999999997}}]} \ No newline at end of file diff --git a/src/App/Plant/Microwave/model.ts b/src/App/Plant/Microwave/model.ts index 084e850..e69de29 100644 --- a/src/App/Plant/Microwave/model.ts +++ b/src/App/Plant/Microwave/model.ts @@ -1,15 +0,0 @@ -import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor"; -import { detectConnections } from "@/statecharts/detect_connections"; -import { parseStatechart } from "@/statecharts/parser"; - -// export const microwaveConcreteSyntax: ConcreteSyntax = {"rountangles":[{"uid":"25","topLeft":{"x":134.99999999999972,"y":136.9999999999999},"size":{"x":1686.9999999999995,"y":623.9999999999997},"kind":"and"},{"uid":"24","topLeft":{"x":517.9999999999994,"y":173.99999999999983},"size":{"x":482.99999999999943,"y":554.9999999999998},"kind":"or"},{"uid":"57","topLeft":{"x":1403.999999999999,"y":175.9999999999999},"size":{"x":378.99999999999983,"y":553.9999999999997},"kind":"or"},{"uid":"32","topLeft":{"x":558.9999999999993,"y":240.99999999999983},"size":{"x":392.9999999999999,"y":462.99999999999983},"kind":"or"},{"uid":"43","topLeft":{"x":1034.999999999999,"y":180.9999999999999},"size":{"x":328.9999999999999,"y":549.9999999999998},"kind":"or"},{"uid":"23","topLeft":{"x":169.99999999999972,"y":170.99999999999983},"size":{"x":317.9999999999998,"y":556.9999999999998},"kind":"or"},{"uid":"13","topLeft":{"x":232.99999999999972,"y":550.9999999999997},"size":{"x":198.9999999999999,"y":124.99999999999994},"kind":"and"},{"uid":"28","topLeft":{"x":728.9999999999993,"y":551.9999999999997},"size":{"x":183.99999999999994,"y":108.99999999999994},"kind":"and"},{"uid":"11","topLeft":{"x":244.99999999999972,"y":298.9999999999998},"size":{"x":183.99999999999994,"y":94.99999999999994},"kind":"and"},{"uid":"58","topLeft":{"x":1447.9999999999989,"y":407.9999999999997},"size":{"x":148.99999999999994,"y":113.99999999999997},"kind":"and"},{"uid":"44","topLeft":{"x":1128.9999999999989,"y":306.9999999999998},"size":{"x":156.99999999999994,"y":102.99999999999996},"kind":"and"},{"uid":"27","topLeft":{"x":731.9999999999993,"y":316.9999999999998},"size":{"x":173.9999999999999,"y":87.99999999999996},"kind":"and"},{"uid":"45","topLeft":{"x":1133.9999999999989,"y":571.9999999999997},"size":{"x":162.99999999999997,"y":89.99999999999999},"kind":"and"}],"diamonds":[{"uid":"34","topLeft":{"x":657.9999999999993,"y":458.99999999999966},"size":{"x":59,"y":60}}],"history":[],"arrows":[{"uid":"18","start":{"x":376.99999999999955,"y":393.9999999999998},"end":{"x":375.99999999999955,"y":541.9999999999998}},{"uid":"20","start":{"x":279.9999999999997,"y":547.9999999999997},"end":{"x":279.9999999999997,"y":401.99999999999966}},{"uid":"22","start":{"x":278.9999999999997,"y":244.99999999999977},"end":{"x":310.99999999999966,"y":295.9999999999998}},{"uid":"26","start":{"x":202.99999999999972,"y":82.99999999999991},"end":{"x":245.99999999999972,"y":130.99999999999983}},{"uid":"29","start":{"x":559.9999999999993,"y":493.99999999999966},"end":{"x":652.9999999999993,"y":492.99999999999966}},{"uid":"30","start":{"x":791.9999999999993,"y":275.99999999999983},"end":{"x":808.9999999999993,"y":306.99999999999983}},{"uid":"33","start":{"x":630.9999999999993,"y":201.99999999999986},"end":{"x":671.9999999999993,"y":234.99999999999986}},{"uid":"35","start":{"x":691.9999999999993,"y":517.9999999999997},"end":{"x":730.9999999999993,"y":553.9999999999997}},{"uid":"37","start":{"x":687.9999999999993,"y":458.99999999999966},"end":{"x":733.9999999999993,"y":398.99999999999966}},{"uid":"46","start":{"x":1163.9999999999989,"y":238.99999999999983},"end":{"x":1206.9999999999989,"y":303.99999999999983}},{"uid":"47","start":{"x":1260.9999999999989,"y":410.9999999999997},"end":{"x":1261.9999999999989,"y":569.9999999999997}},{"uid":"48","start":{"x":1170.9999999999989,"y":568.9999999999997},"end":{"x":1169.9999999999989,"y":413.99999999999966}},{"uid":"59","start":{"x":1600.9999999999989,"y":470.9999999999997},"end":{"x":1546.9999999999989,"y":524.9999999999997}},{"uid":"66","start":{"x":1467.9999999999989,"y":355.9999999999998},"end":{"x":1528.9999999999989,"y":402.9999999999998}}],"texts":[{"uid":"12","text":"// Door closed","topLeft":{"x":336.99999999999955,"y":334.99999999999983}},{"uid":"14","text":"// Door opened","topLeft":{"x":331.9999999999993,"y":592.9999999999997}},{"uid":"15","text":"entry / ^door(\"closed\")","topLeft":{"x":335.99999999999966,"y":364.9999999999998}},{"uid":"16","text":"entry / ^door(\"open\")","topLeft":{"x":331.99999999999966,"y":642.9999999999997}},{"uid":"19","text":"toggleDoor","topLeft":{"x":267.9999999999996,"y":463.9999999999998}},{"uid":"21","text":"toggleDoor","topLeft":{"x":373.99999999999955,"y":485.9999999999998}},{"uid":"31","text":"setMagnetron(state)","topLeft":{"x":616.9999999999993,"y":480.99999999999955}},{"uid":"36","text":"[state==\"on\"]","topLeft":{"x":718.9999999999991,"y":537.9999999999997}},{"uid":"38","text":"[state == \"off\"]","topLeft":{"x":706.9999999999993,"y":439.9999999999997}},{"uid":"39","text":"// Magnetron off","topLeft":{"x":818.9999999999991,"y":364.9999999999998}},{"uid":"40","text":"// Magnetron on","topLeft":{"x":824.9999999999991,"y":613.9999999999995}},{"uid":"49","text":"ringBell","topLeft":{"x":1269.9999999999989,"y":496.99999999999966}},{"uid":"50","text":"after 10 ms","topLeft":{"x":1163.9999999999989,"y":490.99999999999966}},{"uid":"51","text":"// bell","topLeft":{"x":1212.9999999999989,"y":616.9999999999995}},{"uid":"52","text":"// no bell","topLeft":{"x":1205.9999999999989,"y":364.9999999999998}},{"uid":"60","text":"setTimeDisplay(value) / timeDisplay = value;","topLeft":{"x":1610.9999999999989,"y":498.9999999999998}},{"uid":"65","text":"entry / timeDisplay = 0;","topLeft":{"x":435.9999999999997,"y":102.99999999999996}}]}; - - -export const microwaveConcreteSyntax: ConcreteSyntax = {"rountangles":[{"uid":"25","topLeft":{"x":53.99999999999977,"y":125.99999999999989},"size":{"x":2025.9999999999995,"y":623.9999999999997},"kind":"and"},{"uid":"57","topLeft":{"x":1432.999999999999,"y":164.9999999999999},"size":{"x":607.9999999999998,"y":553.9999999999997},"kind":"or"},{"uid":"24","topLeft":{"x":546.9999999999994,"y":162.99999999999983},"size":{"x":482.99999999999943,"y":554.9999999999998},"kind":"or"},{"uid":"23","topLeft":{"x":88.99999999999974,"y":159.99999999999983},"size":{"x":427.9999999999998,"y":556.9999999999998},"kind":"or"},{"uid":"32","topLeft":{"x":587.9999999999993,"y":229.99999999999983},"size":{"x":392.9999999999999,"y":462.99999999999983},"kind":"or"},{"uid":"43","topLeft":{"x":1063.999999999999,"y":169.9999999999999},"size":{"x":328.9999999999999,"y":549.9999999999998},"kind":"or"},{"uid":"58","topLeft":{"x":1638.9999999999989,"y":248.99999999999972},"size":{"x":148.99999999999994,"y":389.99999999999994},"kind":"and"},{"uid":"13","topLeft":{"x":261.9999999999997,"y":539.9999999999997},"size":{"x":198.9999999999999,"y":124.99999999999994},"kind":"and"},{"uid":"28","topLeft":{"x":757.9999999999993,"y":540.9999999999997},"size":{"x":183.99999999999994,"y":108.99999999999994},"kind":"and"},{"uid":"11","topLeft":{"x":273.9999999999997,"y":287.9999999999998},"size":{"x":183.99999999999994,"y":94.99999999999994},"kind":"and"},{"uid":"44","topLeft":{"x":1157.9999999999989,"y":295.9999999999998},"size":{"x":156.99999999999994,"y":102.99999999999996},"kind":"and"},{"uid":"27","topLeft":{"x":760.9999999999993,"y":305.9999999999998},"size":{"x":173.9999999999999,"y":87.99999999999996},"kind":"and"},{"uid":"45","topLeft":{"x":1162.9999999999989,"y":560.9999999999997},"size":{"x":162.99999999999997,"y":89.99999999999999},"kind":"and"},{"uid":"80","topLeft":{"x":127.99999999999993,"y":426.9999999999997},"size":{"x":108.99999999999996,"y":73.99999999999997},"kind":"and"}],"diamonds":[{"uid":"34","topLeft":{"x":686.9999999999993,"y":447.99999999999966},"size":{"x":59,"y":60}}],"history":[],"arrows":[{"uid":"18","start":{"x":405.99999999999955,"y":382.9999999999998},"end":{"x":404.99999999999955,"y":530.9999999999998}},{"uid":"20","start":{"x":308.9999999999997,"y":536.9999999999997},"end":{"x":240.99999999999972,"y":503.99999999999966}},{"uid":"22","start":{"x":307.9999999999997,"y":233.99999999999977},"end":{"x":339.99999999999966,"y":284.9999999999998}},{"uid":"26","start":{"x":231.99999999999972,"y":71.99999999999991},"end":{"x":274.9999999999997,"y":119.99999999999983}},{"uid":"29","start":{"x":588.9999999999993,"y":482.99999999999966},"end":{"x":681.9999999999993,"y":481.99999999999966}},{"uid":"30","start":{"x":820.9999999999993,"y":264.99999999999983},"end":{"x":837.9999999999993,"y":295.99999999999983}},{"uid":"33","start":{"x":659.9999999999993,"y":190.99999999999986},"end":{"x":700.9999999999993,"y":223.99999999999986}},{"uid":"35","start":{"x":720.9999999999993,"y":506.99999999999966},"end":{"x":759.9999999999993,"y":542.9999999999997}},{"uid":"37","start":{"x":716.9999999999993,"y":447.99999999999966},"end":{"x":762.9999999999993,"y":387.99999999999966}},{"uid":"46","start":{"x":1192.9999999999989,"y":227.99999999999983},"end":{"x":1235.9999999999989,"y":292.99999999999983}},{"uid":"47","start":{"x":1289.9999999999989,"y":399.9999999999997},"end":{"x":1290.9999999999989,"y":558.9999999999997}},{"uid":"48","start":{"x":1199.9999999999989,"y":557.9999999999997},"end":{"x":1198.9999999999989,"y":402.99999999999966}},{"uid":"59","start":{"x":1789.9999999999989,"y":587.9999999999997},"end":{"x":1736.9999999999989,"y":643.9999999999997}},{"uid":"66","start":{"x":1643.9999999999989,"y":200.99999999999977},"end":{"x":1672.9999999999989,"y":242.99999999999977}},{"uid":"67","start":{"x":1633.9999999999989,"y":322.99999999999983},"end":{"x":1630.9999999999989,"y":367.99999999999983}},{"uid":"70","start":{"x":1632.9999999999989,"y":405.99999999999983},"end":{"x":1629.9999999999989,"y":450.99999999999983}},{"uid":"72","start":{"x":1632.9999999999989,"y":482.99999999999983},"end":{"x":1629.9999999999989,"y":527.9999999999998}},{"uid":"74","start":{"x":1791.9999999999989,"y":454.9999999999998},"end":{"x":1788.9999999999989,"y":499.9999999999998}},{"uid":"76","start":{"x":1789.9999999999989,"y":373.9999999999998},"end":{"x":1786.9999999999989,"y":418.9999999999998}},{"uid":"78","start":{"x":1793.9999999999989,"y":288.9999999999998},"end":{"x":1790.9999999999989,"y":333.9999999999998}},{"uid":"81","start":{"x":209.9999999999999,"y":419.9999999999997},"end":{"x":265.9999999999999,"y":368.9999999999997}}],"texts":[{"uid":"12","text":"// Door closed","topLeft":{"x":365.99999999999955,"y":323.99999999999983}},{"uid":"14","text":"// Door opened","topLeft":{"x":360.9999999999993,"y":581.9999999999997}},{"uid":"15","text":"entry / ^door(\"closed\")","topLeft":{"x":364.99999999999966,"y":353.9999999999998}},{"uid":"16","text":"entry / ^door(\"open\")","topLeft":{"x":360.99999999999966,"y":631.9999999999997}},{"uid":"19","text":"doorMouseDown","topLeft":{"x":260.9999999999996,"y":527.9999999999998}},{"uid":"21","text":"doorMouseUp","topLeft":{"x":402.99999999999955,"y":474.9999999999998}},{"uid":"31","text":"setMagnetron(state)","topLeft":{"x":645.9999999999993,"y":469.99999999999955}},{"uid":"36","text":"[state==\"on\"]","topLeft":{"x":747.9999999999991,"y":526.9999999999997}},{"uid":"38","text":"[state == \"off\"]","topLeft":{"x":735.9999999999993,"y":428.9999999999997}},{"uid":"39","text":"// Magnetron off","topLeft":{"x":847.9999999999991,"y":353.9999999999998}},{"uid":"40","text":"// Magnetron on","topLeft":{"x":853.9999999999991,"y":602.9999999999995}},{"uid":"49","text":"ringBell","topLeft":{"x":1298.9999999999989,"y":485.99999999999966}},{"uid":"50","text":"after 10 ms","topLeft":{"x":1192.9999999999989,"y":479.99999999999966}},{"uid":"51","text":"// bell","topLeft":{"x":1241.9999999999989,"y":605.9999999999995}},{"uid":"52","text":"// no bell","topLeft":{"x":1234.9999999999989,"y":353.9999999999998}},{"uid":"60","text":"setTimeDisplay(value) / timeDisplay = value;","topLeft":{"x":1799.9999999999989,"y":615.9999999999998}},{"uid":"65","text":"entry / timeDisplay = 0;","topLeft":{"x":464.9999999999997,"y":91.99999999999996}},{"uid":"68","text":"startPressed / ^startPressed","topLeft":{"x":1664.9999999999989,"y":349.99999999999983}},{"uid":"71","text":"stopPressed / ^stopPressed","topLeft":{"x":1646.9999999999989,"y":433.99999999999983}},{"uid":"73","text":"incTimePressed / ^incTimePressed","topLeft":{"x":1643.9999999999989,"y":509.9999999999998}},{"uid":"75","text":"incTimeReleased / ^incTimeReleased","topLeft":{"x":1825.9999999999989,"y":477.9999999999998}},{"uid":"77","text":"stopReleased / ^stopReleased","topLeft":{"x":1823.9999999999989,"y":397.9999999999998}},{"uid":"79","text":"startReleased / ^startReleased","topLeft":{"x":1830.9999999999989,"y":311.9999999999998}},{"uid":"82","text":"doorMouseUp","topLeft":{"x":230.99999999999986,"y":404.9999999999998}},{"uid":"83","text":"// closing door","topLeft":{"x":181.9999999999999,"y":464.9999999999997}}]}; - -export const [microwaveAbstractSyntax, microwaveErrors] = parseStatechart(microwaveConcreteSyntax, detectConnections(microwaveConcreteSyntax)); - -if (microwaveErrors.length > 0) { - console.log({microwaveErrors}); - throw new Error("there were errors parsing microwave plant model. see console.") -} diff --git a/src/App/Plant/Plant.ts b/src/App/Plant/Plant.ts index d6883e7..bc47f79 100644 --- a/src/App/Plant/Plant.ts +++ b/src/App/Plant/Plant.ts @@ -1,18 +1,21 @@ import { ReactElement } from "react"; import { Statechart } from "@/statecharts/abstract_syntax"; import { EventTrigger } from "@/statecharts/label_ast"; -import { RaisedEvent } from "@/statecharts/runtime_types"; -import { Conns, TimedReactive } from "@/statecharts/timed_reactive"; +import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types"; +import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive"; export type PlantRenderProps = { state: StateType, speed: number, - raiseInput: (e: RaisedEvent) => void, + raiseUIEvent: (e: RaisedEvent) => void, }; export type Plant = { + uiEvents: EventTrigger[]; + inputEvents: EventTrigger[]; outputEvents: EventTrigger[]; + execution: TimedReactive; render: (props: PlantRenderProps) => ReactElement; } @@ -48,3 +51,19 @@ export function exposePlantInputs(plant: Plant, plantName: string, tfm = (s } return inputs } + +export type StatechartPlantSpec = { + uiEvents: EventTrigger[], + ast: Statechart, + render: (props: PlantRenderProps) => ReactElement, +} + +export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec): Plant { + return { + uiEvents, + inputEvents: ast.inputEvents, + outputEvents: [...ast.outputEvents].map(e => ({kind: "event" as const, event: e})), + execution: statechartExecution(ast), + render, + } +} diff --git a/src/App/RTHistory.tsx b/src/App/RTHistory.tsx index 83e3972..d5d1da9 100644 --- a/src/App/RTHistory.tsx +++ b/src/App/RTHistory.tsx @@ -5,7 +5,6 @@ import { formatTime } from "../util/util"; import { TimeMode, timeTravel } from "../statecharts/time"; import { TraceItem, TraceState } from "./App"; import { Environment } from "@/statecharts/environment"; -import { Conns } from "@/statecharts/timed_reactive"; type RTHistoryProps = { trace: TraceState|null, diff --git a/src/App/TopPanel/InsertModes.tsx b/src/App/TopPanel/InsertModes.tsx index aa120bb..fe1c411 100644 --- a/src/App/TopPanel/InsertModes.tsx +++ b/src/App/TopPanel/InsertModes.tsx @@ -1,10 +1,11 @@ -import { Dispatch, memo, ReactElement, SetStateAction } from "react"; +import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect } from "react"; import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo"; -import { InsertMode } from "@/App/VisualEditor/VisualEditor"; import { HistoryIcon, PseudoStateIcon, RountangleIcon } from "./Icons"; import TrendingFlatIcon from '@mui/icons-material/TrendingFlat'; +export type InsertMode = "and" | "or" | "pseudo" | "shallow" | "deep" | "transition" | "text"; + const insertModes: [InsertMode, string, ReactElement, ReactElement][] = [ ["and", "AND-states", , A], ["or", "OR-states", , O], @@ -16,6 +17,47 @@ const insertModes: [InsertMode, string, ReactElement, ReactElement][] = [ ]; export const InsertModes = memo(function InsertModes({showKeys, insertMode, setInsertMode}: {showKeys: boolean, insertMode: InsertMode, setInsertMode: Dispatch>}) { + + const onKeyDown = useCallback((e: KeyboardEvent) => { + // @ts-ignore + if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; + + if (!e.ctrlKey) { + if (e.key === "a") { + e.preventDefault(); + setInsertMode("and"); + } + if (e.key === "o") { + e.preventDefault(); + setInsertMode("or"); + } + if (e.key === "p") { + e.preventDefault(); + setInsertMode("pseudo"); + } + if (e.key === "t") { + e.preventDefault(); + setInsertMode("transition"); + } + if (e.key === "x") { + e.preventDefault(); + setInsertMode("text"); + } + if (e.key === "h") { + e.preventDefault(); + setInsertMode(oldMode => { + if (oldMode === "shallow") return "deep"; + return "shallow"; + }) + } + } + }, [setInsertMode]); + + useEffect(() => { + window.addEventListener("keydown", onKeyDown); + () => window.removeEventListener("keydown", onKeyDown); + }, [onKeyDown]); + const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden; return <>{insertModes.map(([m, hint, buttonTxt, keyInfo]) => )}; -}) \ No newline at end of file +}) diff --git a/src/App/TopPanel/SpeedControl.tsx b/src/App/TopPanel/SpeedControl.tsx new file mode 100644 index 0000000..cc6e920 --- /dev/null +++ b/src/App/TopPanel/SpeedControl.tsx @@ -0,0 +1,61 @@ +import { Dispatch, memo, SetStateAction, useCallback, useEffect } from "react"; +import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo"; +import { setRealtime, TimeMode } from "@/statecharts/time"; + +export const SpeedControl = memo(function SpeedControl({showKeys, timescale, setTimescale, setTime}: {showKeys: boolean, timescale: number, setTimescale: Dispatch>, setTime: Dispatch>}) { + + const onTimeScaleChange = useCallback((newValue: string, wallclktime: number) => { + const asFloat = parseFloat(newValue); + if (Number.isNaN(asFloat)) { + return; + } + const maxed = Math.min(asFloat, 64); + const mined = Math.max(maxed, 1/64); + setTimescale(mined); + setTime(time => { + if (time.kind === "paused") { + return time; + } + else { + return setRealtime(time, mined, wallclktime); + } + }); + }, [setTime, setTimescale]); + + const onSlower = useCallback(() => { + onTimeScaleChange((timescale/2).toString(), Math.round(performance.now())); + }, [onTimeScaleChange, timescale]); + const onFaster = useCallback(() => { + onTimeScaleChange((timescale*2).toString(), Math.round(performance.now())); + }, [onTimeScaleChange, timescale]); + + const onKeyDown = useCallback((e: KeyboardEvent) => { + if (!e.ctrlKey) { + if (e.key === "s") { + e.preventDefault(); + onSlower(); + } + if (e.key === "f") { + e.preventDefault(); + onFaster(); + } + } + }, [onSlower, onFaster]) + + useEffect(() => { + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [onKeyDown]) + + const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden; + return <> +   + S}> + + + onTimeScaleChange(e.target.value, Math.round(performance.now()))}/> + F}> + + + +}); diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx index b90f6ce..73260c9 100644 --- a/src/App/TopPanel/TopPanel.tsx +++ b/src/App/TopPanel/TopPanel.tsx @@ -1,7 +1,7 @@ import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useState } from "react"; import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types"; import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time"; -import { InsertMode } from "../VisualEditor/VisualEditor"; +import { InsertMode } from "./InsertModes"; import { About } from "../Modals/About"; import { EditHistory, TraceState } from "../App"; import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo"; @@ -20,6 +20,7 @@ import StopIcon from '@mui/icons-material/Stop'; import { InsertModes } from "./InsertModes"; import { usePersistentState } from "@/App/persistent_state"; import { RotateButtons } from "./RotateButtons"; +import { SpeedControl } from "./SpeedControl"; export type TopPanelProps = { trace: TraceState | null, @@ -79,24 +80,6 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on updateDisplayedTime(); }, [setTime, timescale, updateDisplayedTime]); - const onTimeScaleChange = useCallback((newValue: string, wallclktime: number) => { - const asFloat = parseFloat(newValue); - if (Number.isNaN(asFloat)) { - return; - } - const maxed = Math.min(asFloat, 64); - const mined = Math.max(maxed, 1/64); - setTimescale(mined); - setTime(time => { - if (time.kind === "paused") { - return time; - } - else { - return setRealtime(time, mined, wallclktime); - } - }); - }, [setTime, setTimescale]); - // timestamp of next timed transition, in simulated time const timers: Timers = config?.kind === "bigstep" && config.state.sc.environment.get("_timers") || []; const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0]; @@ -115,16 +98,10 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on } }, [nextTimedTransition, setTime]); - const onSlower = useCallback(() => { - onTimeScaleChange((timescale/2).toString(), Math.round(performance.now())); - }, [onTimeScaleChange, timescale]); - const onFaster = useCallback(() => { - onTimeScaleChange((timescale*2).toString(), Math.round(performance.now())); - }, [onTimeScaleChange, timescale]); - useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { // don't capture keyboard events when focused on an input element: + // @ts-ignore if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; if (!e.ctrlKey) { @@ -143,7 +120,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on onClear(); } if (e.key === "Tab") { - if (trace === null) { + if (config === null) { onInit(); } else { @@ -151,14 +128,6 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on } e.preventDefault(); } - if (e.key === "s") { - e.preventDefault(); - onSlower(); - } - if (e.key === "f") { - e.preventDefault(); - onFaster(); - } if (e.key === "`") { e.preventDefault(); setShowKeys(show => !show); @@ -168,23 +137,12 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on onBack(); } } - else { - // ctrl is down - if (e.key === "z") { - e.preventDefault(); - onUndo(); - } - if (e.key === "Z") { - e.preventDefault(); - onRedo(); - } - } }; window.addEventListener("keydown", onKeyDown); return () => { window.removeEventListener("keydown", onKeyDown); }; - }, [trace, config, time, onInit, timescale, onChangePaused, setShowKeys, onUndo, onRedo, onSlower, onFaster, onSkip, onBack, onClear]); + }, [config, time, onInit, onChangePaused, setShowKeys, onSkip, onBack, onClear]); return
{/* shortcuts / about */} @@ -241,14 +199,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on {/* speed */}
-   - S}> - - - onTimeScaleChange(e.target.value, Math.round(performance.now()))}/> - F}> - - +
diff --git a/src/App/TopPanel/UndoRedoButtons.tsx b/src/App/TopPanel/UndoRedoButtons.tsx index 742c74d..0a9d0e5 100644 --- a/src/App/TopPanel/UndoRedoButtons.tsx +++ b/src/App/TopPanel/UndoRedoButtons.tsx @@ -1,10 +1,30 @@ -import { memo } from "react"; +import { memo, useCallback, useEffect } from "react"; import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo"; import UndoIcon from '@mui/icons-material/Undo'; import RedoIcon from '@mui/icons-material/Redo'; export const UndoRedoButtons = memo(function UndoRedoButtons({showKeys, onUndo, onRedo, historyLength, futureLength}: {showKeys: boolean, onUndo: () => void, onRedo: () => void, historyLength: number, futureLength: number}) { + + const onKeyDown = useCallback((e: KeyboardEvent) => { + if (e.ctrlKey) { + // ctrl is down + if (e.key === "z") { + e.preventDefault(); + onUndo(); + } + if (e.key === "Z") { + e.preventDefault(); + onRedo(); + } + } + }, [onUndo, onRedo]); + + useEffect(() => { + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [onKeyDown]); + const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden; return <> Ctrl+Z}> diff --git a/src/App/TopPanel/ZoomButtons.tsx b/src/App/TopPanel/ZoomButtons.tsx index cd8ea09..8db4a8c 100644 --- a/src/App/TopPanel/ZoomButtons.tsx +++ b/src/App/TopPanel/ZoomButtons.tsx @@ -24,10 +24,12 @@ export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}: if (e.ctrlKey) { if (e.key === "+") { e.preventDefault(); + e.stopPropagation(); onZoomIn(); } if (e.key === "-") { e.preventDefault(); + e.stopPropagation(); onZoomOut(); } } diff --git a/src/App/VisualEditor/DiamondSVG.tsx b/src/App/VisualEditor/DiamondSVG.tsx index df35101..da7b289 100644 --- a/src/App/VisualEditor/DiamondSVG.tsx +++ b/src/App/VisualEditor/DiamondSVG.tsx @@ -1,5 +1,5 @@ import { Diamond, RectSide } from "@/statecharts/concrete_syntax"; -import { rountangleMinSize } from "./VisualEditor"; +import { rountangleMinSize } from "@/statecharts/concrete_syntax"; import { Vec2D } from "../../util/geometry"; import { RectHelper } from "./RectHelpers"; import { memo } from "react"; diff --git a/src/App/VisualEditor/RectHelpers.tsx b/src/App/VisualEditor/RectHelpers.tsx index 15f6cbb..d01b74c 100644 --- a/src/App/VisualEditor/RectHelpers.tsx +++ b/src/App/VisualEditor/RectHelpers.tsx @@ -1,4 +1,3 @@ -import { memo } from "react"; import { RectSide } from "../../statecharts/concrete_syntax"; import { Vec2D } from "../../util/geometry"; import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "../parameters"; diff --git a/src/App/VisualEditor/RountangleSVG.tsx b/src/App/VisualEditor/RountangleSVG.tsx index 62b0d10..744abfd 100644 --- a/src/App/VisualEditor/RountangleSVG.tsx +++ b/src/App/VisualEditor/RountangleSVG.tsx @@ -2,7 +2,7 @@ import { memo } from "react"; import { Rountangle, RectSide } from "../../statecharts/concrete_syntax"; import { ROUNTANGLE_RADIUS } from "../parameters"; import { RectHelper } from "./RectHelpers"; -import { rountangleMinSize } from "./VisualEditor"; +import { rountangleMinSize } from "@/statecharts/concrete_syntax"; import { arraysEqual } from "@/util/util"; diff --git a/src/App/VisualEditor/Selection.tsx b/src/App/VisualEditor/Selection.tsx new file mode 100644 index 0000000..f1a598c --- /dev/null +++ b/src/App/VisualEditor/Selection.tsx @@ -0,0 +1,14 @@ +import { normalizeRect, Rect2D } from "@/util/geometry"; + +export type SelectingState = Rect2D | null; + +export function Selecting(props: SelectingState) { + const normalizedRect = normalizeRect(props!); + return ; +} \ No newline at end of file diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx index 9b3b107..4505dc6 100644 --- a/src/App/VisualEditor/VisualEditor.tsx +++ b/src/App/VisualEditor/VisualEditor.tsx @@ -1,22 +1,23 @@ -import { ClipboardEvent, Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react"; -import { Statechart } from "../../statecharts/abstract_syntax"; -import { Arrow, ArrowPart, Diamond, History, Rountangle, RectSide, Text } from "../../statecharts/concrete_syntax"; -import { parseStatechart, TraceableError } from "../../statecharts/parser"; -import { ArcDirection, Rect2D, Vec2D, addV2D, arcDirection, area, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "../../util/geometry"; -import { MIN_ROUNTANGLE_SIZE } from "../parameters"; -import { getBBoxInSvgCoords } from "../../util/svg_helper"; -import { ArrowSVG } from "./ArrowSVG"; -import { RountangleSVG } from "./RountangleSVG"; -import { TextSVG } from "./TextSVG"; -import { DiamondSVG } from "./DiamondSVG"; -import { HistorySVG } from "./HistorySVG"; -import { Connections, detectConnections } from "../../statecharts/detect_connections"; - -import "./VisualEditor.css"; import { TraceState } from "@/App/App"; +import { InsertMode } from "../TopPanel/InsertModes"; import { Mode } from "@/statecharts/runtime_types"; import { arraysEqual, objectsEqual, setsEqual } from "@/util/util"; +import { Arrow, ArrowPart, Diamond, History, RectSide, Rountangle, Text } from "../../statecharts/concrete_syntax"; +import { Connections } from "../../statecharts/detect_connections"; +import { TraceableError } from "../../statecharts/parser"; +import { ArcDirection, arcDirection } from "../../util/geometry"; +import { ArrowSVG } from "./ArrowSVG"; +import { DiamondSVG } from "./DiamondSVG"; +import { HistorySVG } from "./HistorySVG"; +import { RountangleSVG } from "./RountangleSVG"; +import { TextSVG } from "./TextSVG"; +import { useCopyPaste } from "./useCopyPaste"; + +import "./VisualEditor.css"; +import { useMouse } from "./useMouse"; +import { Selecting } from "./Selection"; export type ConcreteSyntax = { rountangles: Rountangle[]; @@ -31,8 +32,6 @@ export type VisualEditorState = ConcreteSyntax & { selection: Selection; }; -type SelectingState = Rect2D | null; - export type RountangleSelectable = { // kind: "rountangle"; parts: RectSide[]; @@ -55,9 +54,6 @@ type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable | Hist export type Selection = Selectable[]; -export type InsertMode = "and"|"or"|"pseudo"|"shallow"|"deep"|"transition"|"text"; - - type VisualEditorProps = { state: VisualEditorState, setState: Dispatch<(v:VisualEditorState) => VisualEditorState>, @@ -74,17 +70,8 @@ type VisualEditorProps = { export const VisualEditor = memo(function VisualEditor({state, setState, trace, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) { - const [dragging, setDragging] = useState(false); - - window.setState = setState; - // uid's of selected rountangles const selection = state.selection || []; - const setSelection = useCallback((cb: (oldSelection: Selection) => Selection) => - setState(oldState => ({...oldState, selection: cb(oldState.selection)})),[setState]); - - // not null while the user is making a selection - const [selectingState, setSelectingState] = useState(null); const refSVG = useRef(null); @@ -102,344 +89,11 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace, }) }, [trace && trace.idx]); - const getCurrentPointer = useCallback((e: {pageX: number, pageY: number}) => { - const bbox = refSVG.current!.getBoundingClientRect(); - return { - x: (e.pageX - bbox.left)/zoom, - y: (e.pageY - bbox.top)/zoom, - } - }, [refSVG.current, zoom]); - const onMouseDown = useCallback((e: {button: number, target: any, pageX: number, pageY: number}) => { - const currentPointer = getCurrentPointer(e); - if (e.button === 2) { - makeCheckPoint(); - // ignore selection, middle mouse button always inserts - setState(state => { - const newID = state.nextID.toString(); - if (insertMode === "and" || insertMode === "or") { - // insert rountangle - return { - ...state, - rountangles: [...state.rountangles, { - uid: newID, - topLeft: currentPointer, - size: MIN_ROUNTANGLE_SIZE, - kind: insertMode, - }], - nextID: state.nextID+1, - selection: [{uid: newID, parts: ["bottom", "right"]}], - }; - } - else if (insertMode === "pseudo") { - return { - ...state, - diamonds: [...state.diamonds, { - uid: newID, - topLeft: currentPointer, - size: MIN_ROUNTANGLE_SIZE, - }], - nextID: state.nextID+1, - selection: [{uid: newID, parts: ["bottom", "right"]}], - }; - } - else if (insertMode === "shallow" || insertMode === "deep") { - return { - ...state, - history: [...state.history, { - uid: newID, - kind: insertMode, - topLeft: currentPointer, - }], - nextID: state.nextID+1, - selection: [{uid: newID, parts: ["history"]}], - } - } - else if (insertMode === "transition") { - return { - ...state, - arrows: [...state.arrows, { - uid: newID, - start: currentPointer, - end: currentPointer, - }], - nextID: state.nextID+1, - selection: [{uid: newID, parts: ["end"]}], - } - } - else if (insertMode === "text") { - return { - ...state, - texts: [...state.texts, { - uid: newID, - text: "// Double-click to edit", - topLeft: currentPointer, - }], - nextID: state.nextID+1, - selection: [{uid: newID, parts: ["text"]}], - } - } - throw new Error("unreachable, mode=" + insertMode); // shut up typescript - }); - setDragging(true); - return; - } + const {onCopy, onPaste, onCut, deleteSelection} = useCopyPaste(makeCheckPoint, state, setState, selection); - if (e.button === 0) { - // left mouse button on a shape will drag that shape (and everything else that's selected). if the shape under the pointer was not in the selection then the selection is reset to contain only that shape. - const uid = e.target?.dataset.uid; - const parts: string[] = e.target?.dataset.parts?.split(' ').filter((p:string) => p!=="") || []; - if (uid && parts.length > 0) { - makeCheckPoint(); + const {onMouseDown, selectionRect} = useMouse(makeCheckPoint, insertMode, zoom, refSVG, state, setState, deleteSelection); - // if the mouse button is pressed outside of the current selection, we reset the selection to whatever shape the mouse is on - let allPartsInSelection = true; - for (const part of parts) { - if (!(selection.find(s => s.uid === uid)?.parts || [] as string[]).includes(part)) { - allPartsInSelection = false; - break; - } - } - if (!allPartsInSelection) { - if (e.target.classList.contains("helper")) { - setSelection(() => [{uid, parts}] as Selection); - } - else { - setDragging(false); - setSelectingState({ - topLeft: currentPointer, - size: {x: 0, y: 0}, - }); - setSelection(() => []); - return; - } - } - - // start dragging - setDragging(true); - return; - } - } - - // otherwise, just start making a selection - setDragging(false); - setSelectingState({ - topLeft: currentPointer, - size: {x: 0, y: 0}, - }); - setSelection(() => []); - }, [getCurrentPointer, makeCheckPoint, insertMode, selection]); - - const onMouseMove = useCallback((e: {pageX: number, pageY: number, movementX: number, movementY: number}) => { - const currentPointer = getCurrentPointer(e); - if (dragging) { - // const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos); - const pointerDelta = {x: e.movementX/zoom, y: e.movementY/zoom}; - setState(state => ({ - ...state, - rountangles: state.rountangles.map(r => { - const parts = state.selection.find(selected => selected.uid === r.uid)?.parts || []; - if (parts.length === 0) { - return r; - } - return { - ...r, - ...transformRect(r, parts, pointerDelta), - }; - }) - .toSorted((a,b) => area(b) - area(a)), // sort: smaller rountangles are drawn on top - diamonds: state.diamonds.map(d => { - const parts = state.selection.find(selected => selected.uid === d.uid)?.parts || []; - if (parts.length === 0) { - return d; - } - return { - ...d, - ...transformRect(d, parts, pointerDelta), - } - }), - history: state.history.map(h => { - const parts = state.selection.find(selected => selected.uid === h.uid)?.parts || []; - if (parts.length === 0) { - return h; - } - return { - ...h, - topLeft: addV2D(h.topLeft, pointerDelta), - } - }), - arrows: state.arrows.map(a => { - const parts = state.selection.find(selected => selected.uid === a.uid)?.parts || []; - if (parts.length === 0) { - return a; - } - return { - ...a, - ...transformLine(a, parts, pointerDelta), - } - }), - texts: state.texts.map(t => { - const parts = state.selection.find(selected => selected.uid === t.uid)?.parts || []; - if (parts.length === 0) { - return t; - } - return { - ...t, - topLeft: addV2D(t.topLeft, pointerDelta), - } - }), - })); - setDragging(true); - } - else if (selectingState) { - setSelectingState(ss => { - const selectionSize = subtractV2D(currentPointer, ss!.topLeft); - return { - ...ss!, - size: selectionSize, - }; - }); - } - }, [getCurrentPointer, selectingState, dragging]); - - const onMouseUp = useCallback((e: {target: any, pageX: number, pageY: number}) => { - if (dragging) { - setDragging(false); - // do not persist sizes smaller than 40x40 - setState(state => { - return { - ...state, - rountangles: state.rountangles.map(r => ({ - ...r, - size: rountangleMinSize(r.size), - })), - diamonds: state.diamonds.map(d => ({ - ...d, - size: rountangleMinSize(d.size), - })) - }; - }); - } - if (selectingState) { - if (selectingState.size.x === 0 && selectingState.size.y === 0) { - const uid = e.target?.dataset.uid; - if (uid) { - const parts = e.target?.dataset.parts.split(' ').filter((p: string) => p!==""); - if (uid) { - setSelection(() => [{ - uid, - parts, - }]); - } - } - } - else { - // we were making a selection - const normalizedSS = normalizeRect(selectingState); - const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[]; - const shapesInSelection = shapes.filter(el => { - const bbox = getBBoxInSvgCoords(el, refSVG.current!); - const scaledBBox = { - topLeft: scaleV2D(bbox.topLeft, 1/zoom), - size: scaleV2D(bbox.size, 1/zoom), - } - return isEntirelyWithin(scaledBBox, normalizedSS); - }).filter(el => !el.classList.contains("corner")); - - const uidToParts = new Map(); - for (const shape of shapesInSelection) { - const uid = shape.dataset.uid; - if (uid) { - const parts: Set = uidToParts.get(uid) || new Set(); - for (const part of shape.dataset.parts?.split(' ') || []) { - parts.add(part); - } - uidToParts.set(uid, parts); - } - } - setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({ - uid, - parts: [...parts], - }))); - } - } - setSelectingState(null); // no longer making a selection - }, [dragging, selectingState, refSVG.current]); - - const deleteSelection = useCallback(() => { - setState(state => ({ - ...state, - rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)), - diamonds: state.diamonds.filter(d => !state.selection.some(ds => ds.uid === d.uid)), - history: state.history.filter(h => !state.selection.some(hs => hs.uid === h.uid)), - arrows: state.arrows.filter(a => !state.selection.some(as => as.uid === a.uid)), - texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)), - selection: [], - })); - }, [setState]); - - const onKeyDown = useCallback((e: KeyboardEvent) => { - // don't capture keyboard events when focused on an input element: - if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; - - if (e.key === "Delete") { - // delete selection - makeCheckPoint(); - deleteSelection(); - } - if (e.key === "o") { - // selected states become OR-states - setState(state => ({ - ...state, - rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "or"}) : r), - })); - } - if (e.key === "a") { - // selected states become AND-states - setState(state => ({ - ...state, - rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "and"}) : r), - })); - } - // if (e.key === "p") { - // // selected states become pseudo-states - // setSelection(selection => { - // setState(state => ({ - // ...state, - // rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "pseudo"}) : r), - // })); - // return selection; - // }); - // } - if (e.ctrlKey) { - if (e.key === "a") { - e.preventDefault(); - setDragging(false); - setState(state => ({ - ...state, - // @ts-ignore - selection: [ - ...state.rountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})), - ...state.diamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})), - ...state.arrows.map(a => ({uid: a.uid, parts: ["start", "end"]})), - ...state.texts.map(t => ({uid: t.uid, parts: ["text"]})), - ] - })) - } - } - }, [makeCheckPoint, deleteSelection, setState, setDragging]); - - useEffect(() => { - // mousemove and mouseup are global event handlers so they keep working when pointer is outside of browser window - window.addEventListener("mouseup", onMouseUp); - window.addEventListener("mousemove", onMouseMove); - window.addEventListener("keydown", onKeyDown); - return () => { - window.removeEventListener("keydown", onKeyDown); - window.removeEventListener("mousemove", onMouseMove); - window.removeEventListener("mouseup", onMouseUp); - }; - }, [selectingState, dragging]); // for visual feedback, when selecting/moving one thing, we also highlight (in green) all the things that belong to the thing we selected. const sidesToHighlight: {[key: string]: RectSide[]} = {}; @@ -487,101 +141,6 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace, } } - const onPaste = useCallback((e: ClipboardEvent) => { - const data = e.clipboardData?.getData("text/plain"); - if (data) { - let parsed; - try { - parsed = JSON.parse(data); - } - catch (e) { - return; - } - // const offset = {x: 40, y: 40}; - const offset = {x: 0, y: 0}; - setState(state => { - let nextID = state.nextID; - const copiedRountangles: Rountangle[] = parsed.rountangles.map((r: Rountangle) => ({ - ...r, - uid: (nextID++).toString(), - topLeft: addV2D(r.topLeft, offset), - } as Rountangle)); - const copiedDiamonds: Diamond[] = parsed.diamonds.map((r: Diamond) => ({ - ...r, - uid: (nextID++).toString(), - topLeft: addV2D(r.topLeft, offset), - } as Diamond)); - const copiedArrows: Arrow[] = parsed.arrows.map((a: Arrow) => ({ - ...a, - uid: (nextID++).toString(), - start: addV2D(a.start, offset), - end: addV2D(a.end, offset), - } as Arrow)); - const copiedTexts: Text[] = parsed.texts.map((t: Text) => ({ - ...t, - uid: (nextID++).toString(), - topLeft: addV2D(t.topLeft, offset), - } as Text)); - const copiedHistories: History[] = parsed.history.map((h: History) => ({ - ...h, - uid: (nextID++).toString(), - topLeft: addV2D(h.topLeft, offset), - })) - // @ts-ignore - const newSelection: Selection = [ - ...copiedRountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})), - ...copiedDiamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})), - ...copiedArrows.map(a => ({uid: a.uid, parts: ["start", "end"]})), - ...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})), - ...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})), - ]; - return { - ...state, - rountangles: [...state.rountangles, ...copiedRountangles], - diamonds: [...state.diamonds, ...copiedDiamonds], - arrows: [...state.arrows, ...copiedArrows], - texts: [...state.texts, ...copiedTexts], - history: [...state.history, ...copiedHistories], - nextID: nextID, - selection: newSelection, - }; - }); - // copyInternal(newSelection, e); // doesn't work - e.preventDefault(); - } - }, [setState]); - - const copyInternal = useCallback((state: VisualEditorState, selection: Selection, e: ClipboardEvent) => { - const uidsToCopy = new Set(selection.map(shape => shape.uid)); - const rountanglesToCopy = state.rountangles.filter(r => uidsToCopy.has(r.uid)); - const diamondsToCopy = state.diamonds.filter(d => uidsToCopy.has(d.uid)); - const historiesToCopy = state.history.filter(h => uidsToCopy.has(h.uid)); - const arrowsToCopy = state.arrows.filter(a => uidsToCopy.has(a.uid)); - const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid)); - e.clipboardData?.setData("text/plain", JSON.stringify({ - rountangles: rountanglesToCopy, - diamonds: diamondsToCopy, - history: historiesToCopy, - arrows: arrowsToCopy, - texts: textsToCopy, - })); - }, []); - - const onCopy = useCallback((e: ClipboardEvent) => { - if (selection.length > 0) { - e.preventDefault(); - copyInternal(state, selection, e); - } - }, [state, selection]); - - const onCut = useCallback((e: ClipboardEvent) => { - if (selection.length > 0) { - copyInternal(state, selection, e); - deleteSelection(); - e.preventDefault(); - } - }, [state, selection]); - const onEditText = useCallback((text: Text, newText: string) => { if (newText === "") { // delete text node @@ -616,7 +175,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace, const size = 4000*zoom; return e.preventDefault()} ref={refSVG} @@ -689,20 +248,10 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace, - {selectingState && } + {selectionRect} ; }); -export function rountangleMinSize(size: Vec2D): Vec2D { - if (size.x >= 40 && size.y >= 40) { - return size; - } - return { - x: Math.max(40, size.x), - y: Math.max(40, size.y), - }; -} - const Rountangles = memo(function Rountangles({rountangles, selection, sidesToHighlight, rountanglesToHighlight, errors, highlightActive}: {rountangles: Rountangle[], selection: Selection, sidesToHighlight: {[key: string]: RectSide[]}, rountanglesToHighlight: {[key: string]: boolean}, errors: TraceableError[], highlightActive: Mode}) { return <>{rountangles.map(rountangle => { return ; -} \ No newline at end of file diff --git a/src/App/VisualEditor/shortcut_handler.ts b/src/App/VisualEditor/shortcut_handler.ts deleted file mode 100644 index 366d7d6..0000000 --- a/src/App/VisualEditor/shortcut_handler.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Dispatch, SetStateAction } from "react"; -import { InsertMode } from "./VisualEditor"; - -export function getKeyHandler(setMode: Dispatch>) { - return function onKeyDown(e: KeyboardEvent) { - // don't capture keyboard events when focused on an input element: - if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; - - if (!e.ctrlKey) { - if (e.key === "a") { - setMode("and"); - } - if (e.key === "o") { - setMode("or"); - } - if (e.key === "p") { - setMode("pseudo"); - } - if (e.key === "t") { - setMode("transition"); - } - if (e.key === "x") { - setMode("text"); - } - if (e.key === "h") { - setMode(oldMode => { - if (oldMode === "shallow") return "deep"; - return "shallow"; - }) - } - } - } -} diff --git a/src/App/VisualEditor/useCopyPaste.ts b/src/App/VisualEditor/useCopyPaste.ts new file mode 100644 index 0000000..a39c537 --- /dev/null +++ b/src/App/VisualEditor/useCopyPaste.ts @@ -0,0 +1,135 @@ +import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concrete_syntax"; +import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react"; +import { Selection, VisualEditorState } from "./VisualEditor"; +import { addV2D } from "@/util/geometry"; + +// const offset = {x: 40, y: 40}; +const offset = {x: 0, y: 0}; + +export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorState, setState: Dispatch<(v:VisualEditorState) => VisualEditorState>, selection: Selection) { + const onPaste = useCallback((e: ClipboardEvent) => { + const data = e.clipboardData?.getData("text/plain"); + if (data) { + try { + const parsed = JSON.parse(data); + setState(state => { + try { + let nextID = state.nextID; + const copiedRountangles: Rountangle[] = parsed.rountangles.map((r: Rountangle) => ({ + ...r, + uid: (nextID++).toString(), + topLeft: addV2D(r.topLeft, offset), + } as Rountangle)); + const copiedDiamonds: Diamond[] = parsed.diamonds.map((r: Diamond) => ({ + ...r, + uid: (nextID++).toString(), + topLeft: addV2D(r.topLeft, offset), + } as Diamond)); + const copiedArrows: Arrow[] = parsed.arrows.map((a: Arrow) => ({ + ...a, + uid: (nextID++).toString(), + start: addV2D(a.start, offset), + end: addV2D(a.end, offset), + } as Arrow)); + const copiedTexts: Text[] = parsed.texts.map((t: Text) => ({ + ...t, + uid: (nextID++).toString(), + topLeft: addV2D(t.topLeft, offset), + } as Text)); + const copiedHistories: History[] = parsed.history.map((h: History) => ({ + ...h, + uid: (nextID++).toString(), + topLeft: addV2D(h.topLeft, offset), + })) + // @ts-ignore + const newSelection: Selection = [ + ...copiedRountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})), + ...copiedDiamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})), + ...copiedArrows.map(a => ({uid: a.uid, parts: ["start", "end"]})), + ...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})), + ...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})), + ]; + makeCheckPoint(); + return { + ...state, + rountangles: [...state.rountangles, ...copiedRountangles], + diamonds: [...state.diamonds, ...copiedDiamonds], + arrows: [...state.arrows, ...copiedArrows], + texts: [...state.texts, ...copiedTexts], + history: [...state.history, ...copiedHistories], + nextID: nextID, + selection: newSelection, + }; + } + catch (e) { + console.warn("error pasting data. most likely you're tying to paste nonsense. ", e); + return state; + } + }); + } + catch (e) { + console.warn("error pasting data. most likely you're tying to paste nonsense. ", e); + } + e.preventDefault(); + } + }, [setState]); + + const copyInternal = useCallback((state: VisualEditorState, selection: Selection, e: ClipboardEvent) => { + const uidsToCopy = new Set(selection.map(shape => shape.uid)); + const rountanglesToCopy = state.rountangles.filter(r => uidsToCopy.has(r.uid)); + const diamondsToCopy = state.diamonds.filter(d => uidsToCopy.has(d.uid)); + const historiesToCopy = state.history.filter(h => uidsToCopy.has(h.uid)); + const arrowsToCopy = state.arrows.filter(a => uidsToCopy.has(a.uid)); + const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid)); + e.clipboardData?.setData("text/plain", JSON.stringify({ + rountangles: rountanglesToCopy, + diamonds: diamondsToCopy, + history: historiesToCopy, + arrows: arrowsToCopy, + texts: textsToCopy, + })); + }, []); + + const onCopy = useCallback((e: ClipboardEvent) => { + if (selection.length > 0) { + e.preventDefault(); + copyInternal(state, selection, e); + } + }, [state, selection]); + + const onCut = useCallback((e: ClipboardEvent) => { + if (selection.length > 0) { + copyInternal(state, selection, e); + deleteSelection(); + e.preventDefault(); + } + }, [state, selection]); + + const deleteSelection = useCallback(() => { + setState(state => ({ + ...state, + rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)), + diamonds: state.diamonds.filter(d => !state.selection.some(ds => ds.uid === d.uid)), + history: state.history.filter(h => !state.selection.some(hs => hs.uid === h.uid)), + arrows: state.arrows.filter(a => !state.selection.some(as => as.uid === a.uid)), + texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)), + selection: [], + })); + }, [setState]); + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Delete") { + // delete selection + makeCheckPoint(); + deleteSelection(); + e.preventDefault(); + } + } + + useEffect(() => { + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }) + + return {onCopy, onPaste, onCut, deleteSelection}; +} \ No newline at end of file diff --git a/src/App/VisualEditor/useMouse.tsx b/src/App/VisualEditor/useMouse.tsx new file mode 100644 index 0000000..f234142 --- /dev/null +++ b/src/App/VisualEditor/useMouse.tsx @@ -0,0 +1,344 @@ +import { rountangleMinSize } from "@/statecharts/concrete_syntax"; +import { addV2D, area, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "@/util/geometry"; +import { getBBoxInSvgCoords } from "@/util/svg_helper"; +import { Dispatch, useCallback, useEffect, useState } from "react"; +import { MIN_ROUNTANGLE_SIZE } from "../parameters"; +import { InsertMode } from "../TopPanel/InsertModes"; +import { Selecting, SelectingState } from "./Selection"; +import { Selection, VisualEditorState } from "./VisualEditor"; + +export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoom: number, refSVG: {current: SVGSVGElement|null}, state: VisualEditorState, setState: Dispatch<(v: VisualEditorState) => VisualEditorState>, deleteSelection: () => void) { + const [dragging, setDragging] = useState(false); + + // not null while the user is making a selection + const [selectingState, setSelectingState] = useState(null); + + const selection = state.selection; + const setSelection = useCallback((cb: (oldSelection: Selection) => Selection) => + setState(oldState => ({...oldState, selection: cb(oldState.selection)})),[setState]); + + const getCurrentPointer = useCallback((e: {pageX: number, pageY: number}) => { + const bbox = refSVG.current!.getBoundingClientRect(); + return { + x: (e.pageX - bbox.left)/zoom, + y: (e.pageY - bbox.top)/zoom, + } + }, [refSVG.current, zoom]); + + const onMouseDown = useCallback((e: {button: number, target: any, pageX: number, pageY: number}) => { + const currentPointer = getCurrentPointer(e); + if (e.button === 2) { + makeCheckPoint(); + // ignore selection, middle mouse button always inserts + setState(state => { + const newID = state.nextID.toString(); + if (insertMode === "and" || insertMode === "or") { + // insert rountangle + return { + ...state, + rountangles: [...state.rountangles, { + uid: newID, + topLeft: currentPointer, + size: MIN_ROUNTANGLE_SIZE, + kind: insertMode, + }], + nextID: state.nextID+1, + selection: [{uid: newID, parts: ["bottom", "right"]}], + }; + } + else if (insertMode === "pseudo") { + return { + ...state, + diamonds: [...state.diamonds, { + uid: newID, + topLeft: currentPointer, + size: MIN_ROUNTANGLE_SIZE, + }], + nextID: state.nextID+1, + selection: [{uid: newID, parts: ["bottom", "right"]}], + }; + } + else if (insertMode === "shallow" || insertMode === "deep") { + return { + ...state, + history: [...state.history, { + uid: newID, + kind: insertMode, + topLeft: currentPointer, + }], + nextID: state.nextID+1, + selection: [{uid: newID, parts: ["history"]}], + } + } + else if (insertMode === "transition") { + return { + ...state, + arrows: [...state.arrows, { + uid: newID, + start: currentPointer, + end: currentPointer, + }], + nextID: state.nextID+1, + selection: [{uid: newID, parts: ["end"]}], + } + } + else if (insertMode === "text") { + return { + ...state, + texts: [...state.texts, { + uid: newID, + text: "// Double-click to edit", + topLeft: currentPointer, + }], + nextID: state.nextID+1, + selection: [{uid: newID, parts: ["text"]}], + } + } + throw new Error("unreachable, mode=" + insertMode); // shut up typescript + }); + setDragging(true); + return; + } + + if (e.button === 0) { + // left mouse button on a shape will drag that shape (and everything else that's selected). if the shape under the pointer was not in the selection then the selection is reset to contain only that shape. + const uid = e.target?.dataset.uid; + const parts: string[] = e.target?.dataset.parts?.split(' ').filter((p:string) => p!=="") || []; + if (uid && parts.length > 0) { + makeCheckPoint(); + + // if the mouse button is pressed outside of the current selection, we reset the selection to whatever shape the mouse is on + let allPartsInSelection = true; + for (const part of parts) { + if (!(selection.find(s => s.uid === uid)?.parts || [] as string[]).includes(part)) { + allPartsInSelection = false; + break; + } + } + if (!allPartsInSelection) { + if (e.target.classList.contains("helper")) { + setSelection(() => [{uid, parts}] as Selection); + } + else { + setDragging(false); + setSelectingState({ + topLeft: currentPointer, + size: {x: 0, y: 0}, + }); + setSelection(() => []); + return; + } + } + + // start dragging + setDragging(true); + return; + } + } + + // otherwise, just start making a selection + setDragging(false); + setSelectingState({ + topLeft: currentPointer, + size: {x: 0, y: 0}, + }); + setSelection(() => []); + }, [getCurrentPointer, makeCheckPoint, insertMode, selection]); + + const onMouseMove = useCallback((e: {pageX: number, pageY: number, movementX: number, movementY: number}) => { + const currentPointer = getCurrentPointer(e); + if (dragging) { + // const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos); + const pointerDelta = {x: e.movementX/zoom, y: e.movementY/zoom}; + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => { + const parts = state.selection.find(selected => selected.uid === r.uid)?.parts || []; + if (parts.length === 0) { + return r; + } + return { + ...r, + ...transformRect(r, parts, pointerDelta), + }; + }) + .toSorted((a,b) => area(b) - area(a)), // sort: smaller rountangles are drawn on top + diamonds: state.diamonds.map(d => { + const parts = state.selection.find(selected => selected.uid === d.uid)?.parts || []; + if (parts.length === 0) { + return d; + } + return { + ...d, + ...transformRect(d, parts, pointerDelta), + } + }), + history: state.history.map(h => { + const parts = state.selection.find(selected => selected.uid === h.uid)?.parts || []; + if (parts.length === 0) { + return h; + } + return { + ...h, + topLeft: addV2D(h.topLeft, pointerDelta), + } + }), + arrows: state.arrows.map(a => { + const parts = state.selection.find(selected => selected.uid === a.uid)?.parts || []; + if (parts.length === 0) { + return a; + } + return { + ...a, + ...transformLine(a, parts, pointerDelta), + } + }), + texts: state.texts.map(t => { + const parts = state.selection.find(selected => selected.uid === t.uid)?.parts || []; + if (parts.length === 0) { + return t; + } + return { + ...t, + topLeft: addV2D(t.topLeft, pointerDelta), + } + }), + })); + setDragging(true); + } + else if (selectingState) { + setSelectingState(ss => { + const selectionSize = subtractV2D(currentPointer, ss!.topLeft); + return { + ...ss!, + size: selectionSize, + }; + }); + } + }, [getCurrentPointer, selectingState, dragging]); + + const onMouseUp = useCallback((e: {target: any, pageX: number, pageY: number}) => { + if (dragging) { + setDragging(false); + // do not persist sizes smaller than 40x40 + setState(state => { + return { + ...state, + rountangles: state.rountangles.map(r => ({ + ...r, + size: rountangleMinSize(r.size), + })), + diamonds: state.diamonds.map(d => ({ + ...d, + size: rountangleMinSize(d.size), + })) + }; + }); + } + if (selectingState) { + if (selectingState.size.x === 0 && selectingState.size.y === 0) { + const uid = e.target?.dataset.uid; + if (uid) { + const parts = e.target?.dataset.parts.split(' ').filter((p: string) => p!==""); + if (uid) { + setSelection(() => [{ + uid, + parts, + }]); + } + } + } + else { + // we were making a selection + const normalizedSS = normalizeRect(selectingState); + const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[]; + const shapesInSelection = shapes.filter(el => { + const bbox = getBBoxInSvgCoords(el, refSVG.current!); + const scaledBBox = { + topLeft: scaleV2D(bbox.topLeft, 1/zoom), + size: scaleV2D(bbox.size, 1/zoom), + } + return isEntirelyWithin(scaledBBox, normalizedSS); + }).filter(el => !el.classList.contains("corner")); + + const uidToParts = new Map(); + for (const shape of shapesInSelection) { + const uid = shape.dataset.uid; + if (uid) { + const parts: Set = uidToParts.get(uid) || new Set(); + for (const part of shape.dataset.parts?.split(' ') || []) { + parts.add(part); + } + uidToParts.set(uid, parts); + } + } + setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({ + uid, + parts: [...parts], + }))); + } + } + setSelectingState(null); // no longer making a selection + }, [dragging, selectingState, refSVG.current]); + + const onKeyDown = useCallback((e: KeyboardEvent) => { + // don't capture keyboard events when focused on an input element: + // @ts-ignore + if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return; + + if (e.key === "o") { + // selected states become OR-states + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "or"}) : r), + })); + } + if (e.key === "a") { + // selected states become AND-states + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "and"}) : r), + })); + } + // if (e.key === "p") { + // // selected states become pseudo-states + // setSelection(selection => { + // setState(state => ({ + // ...state, + // rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "pseudo"}) : r), + // })); + // return selection; + // }); + // } + if (e.ctrlKey) { + if (e.key === "a") { + e.preventDefault(); + setDragging(false); + setState(state => ({ + ...state, + // @ts-ignore + selection: [ + ...state.rountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})), + ...state.diamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})), + ...state.arrows.map(a => ({uid: a.uid, parts: ["start", "end"]})), + ...state.texts.map(t => ({uid: t.uid, parts: ["text"]})), + ] + })) + } + } + }, [makeCheckPoint, deleteSelection, setState, setDragging]); + + useEffect(() => { + // mousemove and mouseup are global event handlers so they keep working when pointer is outside of browser window + window.addEventListener("mouseup", onMouseUp); + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("keydown", onKeyDown); + return () => { + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); + window.removeEventListener("keydown", onKeyDown); + }; + }, [selectingState, dragging]); + + return {onMouseDown, selectionRect: selectingState && }; +} diff --git a/src/App/useEditor.ts b/src/App/useEditor.ts new file mode 100644 index 0000000..3aa9435 --- /dev/null +++ b/src/App/useEditor.ts @@ -0,0 +1,225 @@ +import { addV2D, rotateLine90CCW, rotateLine90CW, rotatePoint90CCW, rotatePoint90CW, rotateRect90CCW, rotateRect90CW, scaleV2D, subtractV2D, Vec2D } from "@/util/geometry"; +import { HISTORY_RADIUS } from "./parameters"; +import { Dispatch, SetStateAction, useCallback, useEffect } from "react"; +import { EditHistory } from "./App"; +import { VisualEditorState } from "./VisualEditor/VisualEditor"; +import { emptyState } from "@/statecharts/concrete_syntax"; + +export function useEditor(editorState: VisualEditorState | null, setEditHistory: Dispatch>) { + useEffect(() => { + console.log("Welcome to StateBuddy!"); + () => { + console.log("Goodbye!"); + } + }, []); + + // recover editor state from URL - we need an effect here because decompression is asynchronous + useEffect(() => { + console.log('recovering state...'); + const compressedState = window.location.hash.slice(1); + if (compressedState.length === 0) { + // empty URL hash + console.log("no state to recover"); + setEditHistory(() => ({current: emptyState, history: [], future: []})); + return; + } + let compressedBuffer; + try { + compressedBuffer = Uint8Array.fromBase64(compressedState); // may throw + } catch (e) { + // probably invalid base64 + console.error("failed to recover state:", e); + setEditHistory(() => ({current: emptyState, history: [], future: []})); + return; + } + const ds = new DecompressionStream("deflate"); + const writer = ds.writable.getWriter(); + writer.write(compressedBuffer).catch(() => {}); // any promise rejections will be detected when we try to read + writer.close().catch(() => {}); + new Response(ds.readable).arrayBuffer() + .then(decompressedBuffer => { + const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer)); + setEditHistory(() => ({current: recoveredState, history: [], future: []})); + }) + .catch(e => { + // any other error: invalid JSON, or decompression failed. + console.error("failed to recover state:", e); + setEditHistory({current: emptyState, history: [], future: []}); + }); + }, []); + + // save editor state in URL + useEffect(() => { + const timeout = setTimeout(() => { + if (editorState === null) { + window.location.hash = "#"; + return; + } + const serializedState = JSON.stringify(editorState); + const stateBuffer = new TextEncoder().encode(serializedState); + const cs = new CompressionStream("deflate"); + const writer = cs.writable.getWriter(); + writer.write(stateBuffer); + writer.close(); + // todo: cancel this promise handler when concurrently starting another compression job + new Response(cs.readable).arrayBuffer().then(compressedStateBuffer => { + const compressedStateString = new Uint8Array(compressedStateBuffer).toBase64(); + window.location.hash = "#"+compressedStateString; + }); + }, 100); + return () => clearTimeout(timeout); + }, [editorState]); + + + // append editor state to undo history + const makeCheckPoint = useCallback(() => { + setEditHistory(historyState => historyState && ({ + ...historyState, + history: [...historyState.history, historyState.current], + future: [], + })); + }, [setEditHistory]); + const onUndo = useCallback(() => { + setEditHistory(historyState => { + if (historyState === null) return null; + if (historyState.history.length === 0) { + return historyState; // no change + } + return { + current: historyState.history.at(-1)!, + history: historyState.history.slice(0,-1), + future: [...historyState.future, historyState.current], + } + }) + }, [setEditHistory]); + const onRedo = useCallback(() => { + setEditHistory(historyState => { + if (historyState === null) return null; + if (historyState.future.length === 0) { + return historyState; // no change + } + return { + current: historyState.future.at(-1)!, + history: [...historyState.history, historyState.current], + future: historyState.future.slice(0,-1), + } + }); + }, [setEditHistory]); + const onRotate = useCallback((direction: "ccw" | "cw") => { + makeCheckPoint(); + setEditHistory(historyState => { + if (historyState === null) return null; + + const selection = historyState.current.selection; + + if (selection.length === 0) { + return historyState; + } + + // determine bounding box... in a convoluted manner + let minX = -Infinity, minY = -Infinity, maxX = Infinity, maxY = Infinity; + + function addPointToBBox({x,y}: Vec2D) { + minX = Math.max(minX, x); + minY = Math.max(minY, y); + maxX = Math.min(maxX, x); + maxY = Math.min(maxY, y); + } + + for (const rt of historyState.current.rountangles) { + if (selection.some(s => s.uid === rt.uid)) { + addPointToBBox(rt.topLeft); + addPointToBBox(addV2D(rt.topLeft, rt.size)); + } + } + for (const d of historyState.current.diamonds) { + if (selection.some(s => s.uid === d.uid)) { + addPointToBBox(d.topLeft); + addPointToBBox(addV2D(d.topLeft, d.size)); + } + } + for (const arr of historyState.current.arrows) { + if (selection.some(s => s.uid === arr.uid)) { + addPointToBBox(arr.start); + addPointToBBox(arr.end); + } + } + for (const txt of historyState.current.texts) { + if (selection.some(s => s.uid === txt.uid)) { + addPointToBBox(txt.topLeft); + } + } + const historySize = {x: HISTORY_RADIUS, y: HISTORY_RADIUS}; + for (const h of historyState.current.history) { + if (selection.some(s => s.uid === h.uid)) { + addPointToBBox(h.topLeft); + addPointToBBox(addV2D(h.topLeft, scaleV2D(historySize, 2))); + } + } + + const center: Vec2D = { + x: (minX + maxX) / 2, + y: (minY + maxY) / 2, + }; + + const mapIfSelected = (shape: {uid: string}, cb: (shape:any)=>any) => { + if (selection.some(s => s.uid === shape.uid)) { + return cb(shape); + } + else { + return shape; + } + } + + return { + ...historyState, + current: { + ...historyState.current, + rountangles: historyState.current.rountangles.map(rt => mapIfSelected(rt, rt => { + return { + ...rt, + ...(direction === "ccw" + ? rotateRect90CCW(rt, center) + : rotateRect90CW(rt, center)), + } + })), + arrows: historyState.current.arrows.map(arr => mapIfSelected(arr, arr => { + return { + ...arr, + ...(direction === "ccw" + ? rotateLine90CCW(arr, center) + : rotateLine90CW(arr, center)), + }; + })), + diamonds: historyState.current.diamonds.map(d => mapIfSelected(d, d => { + return { + ...d, + ...(direction === "ccw" + ? rotateRect90CCW(d, center) + : rotateRect90CW(d, center)), + }; + })), + texts: historyState.current.texts.map(txt => mapIfSelected(txt, txt => { + return { + ...txt, + topLeft: (direction === "ccw" + ? rotatePoint90CCW(txt.topLeft, center) + : rotatePoint90CW(txt.topLeft, center)), + }; + })), + history: historyState.current.history.map(h => mapIfSelected(h, h => { + return { + ...h, + topLeft: (direction === "ccw" + ? subtractV2D(rotatePoint90CCW(addV2D(h.topLeft, historySize), center), historySize) + : subtractV2D(rotatePoint90CW(addV2D(h.topLeft, historySize), center), historySize) + ), + }; + })), + }, + } + }) + }, [setEditHistory]); + + return {makeCheckPoint, onUndo, onRedo, onRotate}; +} \ No newline at end of file diff --git a/src/App/useSimulator.ts b/src/App/useSimulator.ts new file mode 100644 index 0000000..1f75028 --- /dev/null +++ b/src/App/useSimulator.ts @@ -0,0 +1,3 @@ +export function useSimulator() { + +} \ No newline at end of file diff --git a/src/statecharts/concrete_syntax.ts b/src/statecharts/concrete_syntax.ts index 3fb9a72..85aae51 100644 --- a/src/statecharts/concrete_syntax.ts +++ b/src/statecharts/concrete_syntax.ts @@ -1,5 +1,5 @@ import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox, subtractV2D } from "../util/geometry"; -import { ARROW_SNAP_THRESHOLD, HISTORY_RADIUS, TEXT_SNAP_THRESHOLD } from "../App/parameters"; +import { ARROW_SNAP_THRESHOLD, HISTORY_RADIUS, ROUNTANGLE_RADIUS, TEXT_SNAP_THRESHOLD } from "../App/parameters"; import { VisualEditorState } from "../App/VisualEditor/VisualEditor"; import { sides } from "@/util/geometry"; @@ -123,3 +123,14 @@ export function findNearestHistory(point: Vec2D, candidates: History[]): History } return best; } + +export function rountangleMinSize(size: Vec2D): Vec2D { + const minSize = ROUNTANGLE_RADIUS * 2; + if (size.x >= minSize && size.y >= minSize) { + return size; + } + return { + x: Math.max(minSize, size.x), + y: Math.max(minSize, size.y), + }; +} diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts index e1326d1..389c0ec 100644 --- a/src/statecharts/interpreter.ts +++ b/src/statecharts/interpreter.ts @@ -1,6 +1,6 @@ import { AbstractState, computeArena, computePath, ConcreteState, getDescendants, HistoryState, isOverlapping, OrState, StableState, Statechart, stateDescription, Transition, transitionDescription, TransitionSrcTgt } from "./abstract_syntax"; import { evalExpr } from "./actionlang_interpreter"; -import { Environment, FlatEnvironment, ScopedEnvironment } from "./environment"; +import { Environment, FlatEnvironment } from "./environment"; import { Action, EventTrigger, TransitionLabel } from "./label_ast"; import { BigStep, initialRaised, Mode, RaisedEvents, RT_Event, RT_History, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types"; diff --git a/src/statecharts/timed_reactive.ts b/src/statecharts/timed_reactive.ts index cac9e0f..c842059 100644 --- a/src/statecharts/timed_reactive.ts +++ b/src/statecharts/timed_reactive.ts @@ -11,6 +11,9 @@ export type TimedReactive = { timeAdvance: (c: RT_Config) => number, intTransition: (c: RT_Config) => [RaisedEvent[], RT_Config], extTransition: (simtime: number, c: RT_Config, e: InputEvent) => [RaisedEvent[], RT_Config], + + // inputEvents: string[], + // outputEvents: string[], } export function statechartExecution(ast: Statechart): TimedReactive { @@ -32,7 +35,10 @@ export function statechartExecution(ast: Statechart): TimedReactive { extTransition: (simtime: number, c: RT_Statechart, e: InputEvent) => { const {outputEvents, ...rest} = handleInputEvent(simtime, e, ast, c); return [outputEvents, {outputEvents, ...rest}]; - } + }, + + // inputEvents: ast.inputEvents.map(e => e.event), + // outputEvents: [...ast.outputEvents], } } @@ -53,24 +59,24 @@ export type OutputDestination = { // kind: "nowhere", // }; -export function exposeStatechartInputsOutputs(ast: Statechart, model: string): Conns { - return { - // all the coupled execution's input events become input events for the statechart - inputEvents: exposeStatechartInputs(ast, model), - outputEvents: exposeStatechartOutputs(ast, model), - } -} +// export function exposeStatechartInputsOutputs(ast: Statechart, model: string): Conns { +// return { +// // all the coupled execution's input events become input events for the statechart +// inputEvents: exposeStatechartInputs(ast, model), +// outputEvents: exposeStatechartOutputs(ast, model), +// } +// } -export function exposeStatechartInputs(ast: Statechart, model: string, tfm = (s: string) => s): {[eventName: string]: ModelDestination} { - return Object.fromEntries(ast.inputEvents.map(e => [tfm(e.event), {kind: "model", model, eventName: e.event}])); -} +// export function exposeStatechartInputs(ast: Statechart, model: string, tfm = (s: string) => s): {[eventName: string]: ModelDestination} { +// return Object.fromEntries(ast.inputEvents.map(e => [tfm(e.event), {kind: "model", model, eventName: e.event}])); +// } -export function exposeStatechartOutputs(ast: Statechart, model: string): {[modelName: string]: {[eventName: string]: EventDestination}} { - return { - // all the statechart's output events become output events of our coupled execution - [model]: Object.fromEntries([...ast.outputEvents].map(e => [e, {kind: "output", model, eventName: e}])), - }; -} +// export function exposeStatechartOutputs(ast: Statechart, model: string): {[modelName: string]: {[eventName: string]: EventDestination}} { +// return { +// // all the statechart's output events become output events of our coupled execution +// [model]: Object.fromEntries([...ast.outputEvents].map(e => [e, {kind: "output", model, eventName: e}])), +// }; +// } // export function hideStatechartOutputs(ast: Statechart, model: string) { // return { @@ -78,15 +84,23 @@ export function exposeStatechartOutputs(ast: Statechart, model: string): {[model // } // } -export type Conns = { - // inputs coming from outside are routed to the right models - inputEvents: {[eventName: string]: ModelDestination}, +// export type Conns = { +// // inputs coming from outside are routed to the right models +// inputEvents: {[eventName: string]: ModelDestination}, - // outputs coming from the models are routed to other models or to outside - outputEvents: {[modelName: string]: {[eventName: string]: EventDestination}}, -} +// // outputs coming from the models are routed to other models or to outside +// outputEvents: {[modelName: string]: {[eventName: string]: EventDestination}}, +// } -export function coupledExecution(models: {[name in keyof T]: TimedReactive}, conns: Conns): TimedReactive { + +// maps source to target. e.g.: +// { +// "sc.incTime": ["plant", "incTime"], +// "DEBUG_topRightClicked": ["sc", "topRightClicked"], +// } +export type Conns = {[eventName: string]: [string|null, string]}; + +export function coupledExecution(models: {[name in keyof T]: TimedReactive}, conns: Conns, /*inputEvents: string[], outputEvents: []*/): TimedReactive { function makeModelExtTransition(simtime: number, c: T, model: string, e: InputEvent) { const [outputEvents, newConfig] = models[model].extTransition(simtime, c[model], e); @@ -100,33 +114,33 @@ export function coupledExecution(models: {[name function processOutputs(simtime: number, events: RaisedEvent[], model: string, c: T): [RaisedEvent[], T] { if (events.length > 0) { const [event, ...rest] = events; - const destination = conns.outputEvents[model]?.[event.name]; + const destination = conns[model+'.'+event.name]; if (destination === undefined) { // ignore console.log(`${model}.${event.name} goes nowhere`); return processOutputs(simtime, rest, model, c); } - if (destination.kind === "model") { + const [destinationModel, destinationEventName] = destination; + if (destinationModel !== null) { // output event is input for another model - console.log(`${model}.${event.name} goes to ${destination.model}.${destination.eventName}`); + console.log(`${model}.${event.name} goes to ${destinationModel}.${destinationEventName}`); const inputEvent = { kind: "input" as const, - name: destination.eventName, + name: destinationEventName, param: event.param, }; - const [outputEvents, newConfig] = makeModelExtTransition(simtime, c, destination.model, inputEvent); + const [outputEvents, newConfig] = makeModelExtTransition(simtime, c, destinationModel, inputEvent); // proceed with 'rest': const [restOutputEvents, newConfig2] = processOutputs(simtime, rest, model, newConfig); return [[...outputEvents, ...restOutputEvents], newConfig2]; } - else if (destination.kind === "output") { - // kind === "output" - console.log(`${model}.${event.name} becomes ^${destination.eventName}`); + else { + // event is output event of our coupled execution + console.log(`${model}.${event.name} becomes ^${destinationEventName}`); const [outputEvents, newConfig] = processOutputs(simtime, rest, model, c); return [[event, ...outputEvents], newConfig]; } - throw new Error("unreachable"); } else { return [[], c]; @@ -146,7 +160,6 @@ export function coupledExecution(models: {[name // @ts-ignore state[modelName] = modelState; } - console.log({state}); // 2. handle all output events (models' outputs may be inputs for each other) let finalOutputs = []; for (const [modelName, outputEvents] of allOutputs) { @@ -175,21 +188,28 @@ export function coupledExecution(models: {[name throw new Error("cannot make intTransition - timeAdvance is infinity"); }, extTransition: (simtime, c, e) => { - if (!Object.hasOwn(conns.inputEvents, e.name)) { + if (!Object.hasOwn(conns, e.name)) { console.warn('input event', e.name, 'goes to nowhere'); return [[], c]; } else { - const {model, eventName} = conns.inputEvents[e.name]; - console.log('input event', e.name, 'goes to', `${model}.${eventName}`); - const inputEvent: InputEvent = { - kind: "input", - name: eventName, - param: e.param, - }; - return makeModelExtTransition(simtime, c, model, inputEvent); + const [model, eventName] = conns[e.name]; + if (model !== null) { + console.log('input event', e.name, 'goes to', `${model}.${eventName}`); + const inputEvent: InputEvent = { + kind: "input", + name: eventName, + param: e.param, + }; + return makeModelExtTransition(simtime, c, model, inputEvent); + } + else { + throw new Error("not implemented: input event becoming output event right away.") + } } }, + // inputEvents, + // outputEvents, } }