can define multiple properties. can see detailed view of chosen property.

This commit is contained in:
Joeri Exelmans 2025-11-06 16:25:48 +01:00
parent 1660b06064
commit c7e661eb61
31 changed files with 502 additions and 307 deletions

120
bun.lock
View file

@ -6,75 +6,37 @@
"dependencies": {
"@fontsource/roboto": "^5.2.8",
"@mui/icons-material": "^7.3.4",
"react": "^19",
"react-dom": "^19",
"react": "^19.2.0",
"react-dom": "^19.2.0",
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/bun": "1.3.1",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
"@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="],
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="],
"@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="],
"@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
"@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
"@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="],
"@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="],
"@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="],
"@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="],
"@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="],
"@emotion/styled": ["@emotion/styled@11.14.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", "react": ">=16.8.0" } }, "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw=="],
"@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
"@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="],
"@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="],
"@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
"@fontsource/roboto": ["@fontsource/roboto@5.2.8", "", {}, "sha512-oh9g4Cg3loVMz9MWeKWfDI+ooxxG1aRVetkiKIb2ESS2rrryGecQ/y4pAj4z5A5ebyw450dYRi/c4k/I3UBhHA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@mui/core-downloads-tracker": ["@mui/core-downloads-tracker@7.3.4", "", {}, "sha512-BIktMapG3r4iXwIhYNpvk97ZfYWTreBBQTWjQKbNbzI64+ULHfYavQEX2w99aSWHS58DvXESWIgbD9adKcUOBw=="],
"@mui/icons-material": ["@mui/icons-material@7.3.4", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "@mui/material": "^7.3.4", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9n6Xcq7molXWYb680N2Qx+FRW8oT6j/LXF5PZFH3ph9X/Rct0B/BlLAsFI7iL9ySI6LVLuQIVtrLiPT82R7OZw=="],
@ -93,80 +55,32 @@
"@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
"@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
"@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="],
"@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
"@types/react": ["@types/react@19.2.0", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/react-dom": ["@types/react-dom@19.2.0", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg=="],
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
"@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="],
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
"cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
@ -177,23 +91,11 @@
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
"stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
}

3
global.d.ts vendored
View file

@ -4,4 +4,5 @@ declare module '*.png';
declare module '*.ttf';
declare module '*.wav';
declare module '*.opus';
declare module '*.webp';
declare module '*.webp';
declare module '*.wasm';

View file

@ -13,12 +13,13 @@
"dependencies": {
"@fontsource/roboto": "^5.2.8",
"@mui/icons-material": "^7.3.4",
"react": "^19",
"react-dom": "^19"
// "argus-wasm": "git+https://deemz.org/git/joeri/argus-wasm.git#a4491b3433d48aa1f941bd5ad37b36f819d3b2ac",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/bun": "latest"
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/bun": "1.3.1"
}
}

View file

@ -74,9 +74,20 @@ details:has(+ details) {
display: inline-block;
}
button {
background-color: #fcfcfc;
border: 1px lightgrey solid;
}
button:not(:disabled):hover {
background-color: rgba(0, 0, 255, 0.2);
}
button.active {
border: solid blue 2px;
border: solid blue 1px;
background-color: rgba(0,0,255,0.2);
/* margin-right: 1px; */
/* margin-left: 0; */
color: black;
}
@ -110,3 +121,21 @@ div.stackHorizontal {
display: flex;
flex-direction: row;
}
div.status {
display: inline-block;
vertical-align: middle;
background-color: grey;
border-radius: 50%;
height: 12px;
width: 12px;
}
div.status.violated {
background-color: var(--error-color);
}
div.status.satisfied {
background-color: forestgreen;
}

View file

@ -3,26 +3,33 @@ import "./App.css";
import { Dispatch, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
import AddIcon from '@mui/icons-material/Add';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { Statechart } from "@/statecharts/abstract_syntax";
import { detectConnections } from "@/statecharts/detect_connections";
import { Conns, coupledExecution, EventDestination, statechartExecution, TimedReactive } from "@/statecharts/timed_reactive";
import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive";
import { RuntimeError } from "../statecharts/interpreter";
import { parseStatechart } from "../statecharts/parser";
import { BigStep, RaisedEvent } from "../statecharts/runtime_types";
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
import { BottomPanel } from "./BottomPanel";
import { usePersistentState } from "./persistent_state";
import { PersistentDetails } from "./PersistentDetails";
import { digitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch";
import { dummyPlant } from "./Plant/Dummy/Dummy";
import { microwavePlant } from "./Plant/Microwave/Microwave";
import { Plant } from "./Plant/Plant";
import { trafficLightPlant } from "./Plant/TrafficLight/TrafficLight";
import { RTHistory } from "./RTHistory";
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
import { InsertMode } from "./TopPanel/InsertModes";
import { TopPanel } from "./TopPanel/TopPanel";
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";
import { checkProperty, PropertyCheckResult } from "./check_property";
import { usePersistentState } from "./persistent_state";
import { useEditor } from "./useEditor";
export type EditHistory = {
current: VisualEditorState,
@ -30,11 +37,13 @@ export type EditHistory = {
future: VisualEditorState[],
}
const plants: [string, Plant<any>][] = [
type UniversalPlantState = {[property: string]: boolean|number};
const plants: [string, Plant<any, UniversalPlantState>][] = [
["dummy", dummyPlant],
["microwave", microwavePlant],
["digital watch", digitalWatchPlant],
["traffic light", trafficLightPlant],
["microwave", microwavePlant as unknown as Plant<any, UniversalPlantState>],
["digital watch", digitalWatchPlant as unknown as Plant<any, UniversalPlantState>],
["traffic light", trafficLightPlant as unknown as Plant<any, UniversalPlantState>],
]
export type TraceItemError = {
@ -46,6 +55,7 @@ export type TraceItemError = {
type CoupledState = {
sc: BigStep,
plant: BigStep,
// plantCleanState: {[prop: string]: boolean|number},
};
export type TraceItem =
@ -53,10 +63,9 @@ export type TraceItem =
| { kind: "bigstep", simtime: number, cause: string, state: CoupledState, outputEvents: RaisedEvent[] };
export type TraceState = {
// executor: TimedReactive<CoupledState>,
trace: [TraceItem, ...TraceItem[]], // non-empty
idx: number,
}; // <-- null if there is no trace
};
export function App() {
const [insertMode, setInsertMode] = usePersistentState<InsertMode>("insertMode", "and");
@ -94,7 +103,7 @@ export function App() {
message: currentTraceItem.error.message,
shapeUid: currentTraceItem.error.highlight[0],
}] : [],
]
];
const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(editorState, setEditHistory);
@ -108,23 +117,7 @@ export function App() {
}
}, [refRightSideBar.current, autoScroll]);
// 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;
// coupled execution
const cE = useMemo(() => ast && coupledExecution({
sc: statechartExecution(ast),
plant: plant.execution,
@ -276,6 +269,27 @@ export function App() {
ast && autoConnect && autoDetectConns(ast, plant, setPlantConns);
}, [ast, plant, autoConnect]);
const [properties, setProperties] = usePersistentState<string[]>("properties", []);
const [propertyResults, setPropertyResults] = useState<PropertyCheckResult[] | null>(null);
const [activeProperty, setActiveProperty] = usePersistentState<number>("activeProperty", 0);
// if some properties change, re-evaluate them:
useEffect(() => {
let timeout: NodeJS.Timeout;
if (trace) {
setPropertyResults(null);
timeout = setTimeout(() => {
Promise.all(properties.map((property, i) => {
return checkProperty(plant, property, trace.trace);
}))
.then(results => {
setPropertyResults(results);
})
})
}
return () => clearTimeout(timeout);
}, [properties, trace, plant]);
return <>
{/* Modal dialog */}
@ -326,12 +340,14 @@ export function App() {
className={showExecutionTrace ? "shadowBelow" : ""}
style={{flex: '0 0 content', backgroundColor: ''}}
>
{/* State tree */}
<PersistentDetails localStorageKey="showStateTree" initiallyOpen={true}>
<summary>state tree</summary>
<ul>
{ast && <ShowAST {...{...ast, trace, highlightActive}}/>}
</ul>
</PersistentDetails>
{/* Input events */}
<PersistentDetails localStorageKey="showInputEvents" initiallyOpen={true}>
<summary>input events</summary>
{ast && <ShowInputEvents
@ -340,14 +356,17 @@ export function App() {
disabled={trace===null || trace.trace[trace.idx].kind === "error"}
showKeys={showKeys}/>}
</PersistentDetails>
{/* Internal events */}
<PersistentDetails localStorageKey="showInternalEvents" initiallyOpen={true}>
<summary>internal events</summary>
{ast && <ShowInternalEvents internalEvents={ast.internalEvents}/>}
</PersistentDetails>
{/* Output events */}
<PersistentDetails localStorageKey="showOutputEvents" initiallyOpen={true}>
<summary>output events</summary>
{ast && <ShowOutputEvents outputEvents={ast.outputEvents}/>}
</PersistentDetails>
{/* Plant */}
<PersistentDetails localStorageKey="showPlant" initiallyOpen={true}>
<summary>plant</summary>
<select
@ -360,18 +379,49 @@ export function App() {
</select>
<br/>
{/* Render plant */}
{<plant.render state={plantState} speed={speed}
{<plant.render state={plant.cleanupState(plantState)} speed={speed}
raiseUIEvent={e => onRaise("plant.ui."+e.name, e.param)}
/>}
</PersistentDetails>
<PersistentDetails localStorageKey="showConnEditor" initiallyOpen={false}>
<summary>connections</summary>
<button title="auto-connect (name-based)" className={autoConnect?"active":""}
onClick={() => setAutoConnect(c => !c)}>
<AutoAwesomeIcon fontSize="small"/>
{/* Connections */}
<PersistentDetails localStorageKey="showConnEditor" initiallyOpen={false}>
<summary>connections</summary>
<button title="auto-connect (name-based)" className={autoConnect?"active":""}
onClick={() => setAutoConnect(c => !c)}>
<AutoAwesomeIcon fontSize="small"/>
</button>
{ast && ConnEditor(ast, plant, plantConns, setPlantConns)}
</PersistentDetails>
{/* Properties */}
<PersistentDetails localStorageKey="showProperty" initiallyOpen={false}>
<summary>properties</summary>
<div className="toolbar">
<button title="add property" onClick={() => setProperties(properties => [...properties, ""])}>
<AddIcon fontSize="small"/>
</button>
{ast && ConnEditor(ast, plant, plantConns, setPlantConns)}
</PersistentDetails>
</div>
{properties.map((property, i) => {
const result = propertyResults && propertyResults[i];
let violated = null, propertyError = null;
if (result) {
violated = result[0] && !result[0][0].satisfied;
propertyError = result[1];
}
return <div style={{width:'100%'}} key={i} className="toolbar">
<div className={"status" + (violated === null ? "" : (violated ? " violated" : " satisfied"))}></div>
&nbsp;
<button title="see in trace (below)" className={activeProperty === i ? "active" : ""} onClick={() => setActiveProperty(i)}>
<VisibilityIcon fontSize="small"/>
</button>
<input type="text" style={{width:'calc(97% - 70px)'}} value={property} onChange={e => setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/>
<button title="delete this property" onClick={() => setProperties(properties => properties.toSpliced(i, 1))}>
<DeleteOutlineIcon fontSize="small"/>
</button>
{propertyError && <div style={{color: 'var(--error-color)'}}>{propertyError}</div>}
</div>;
})}
</PersistentDetails>
{/* Traces */}
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary>
<input id="checkbox-show-plant-items" type="checkbox" checked={showPlantTrace} onChange={e => setShowPlantTrace(e.target.checked)}/>
@ -390,7 +440,8 @@ export function App() {
// minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
}}>
<div ref={refRightSideBar}>
{ast && <RTHistory {...{ast, trace, setTrace, setTime, showPlantTrace}}/>}
{ast && <RTHistory {...{ast, trace, setTrace, setTime, showPlantTrace,
propertyTrace: propertyResults && propertyResults[activeProperty] && propertyResults[activeProperty][0] || []}}/>}
</div>
</div>}
<div style={{flex: '0 0 content'}}>
@ -407,32 +458,7 @@ export function App() {
</>;
}
function ShowEventDestination(dst: EventDestination) {
if (dst.kind === "model") {
return <>{dst.model}.{dst.eventName}</>;
}
else if (dst.kind === "output") {
return <>{dst.eventName}</>;
}
else {
return <>&#x1F5D1;</>; // <-- garbage can icon
}
}
function ShowConns({inputEvents, outputEvents}: Conns) {
return <div>
{/* <div style={{color: "grey"}}>
{Object.entries(inputEvents).map(([eventName, destination]) => <div>{eventName} &#x2192; <ShowEventDestination {...destination}/></div>)}
</div>
{Object.entries(outputEvents).map(([modelName, mapping]) => <>{Object.entries(mapping).map(([eventName, destination]) => <div>{modelName}.{eventName} &#x2192; <ShowEventDestination {...destination}/></div>)}</>)} */}
</div>;
}
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import { trafficLightPlant } from "./Plant/TrafficLight/TrafficLight";
function autoDetectConns(ast: Statechart, plant: Plant<any>, setPlantConns: Dispatch<SetStateAction<Conns>>) {
function autoDetectConns(ast: Statechart, plant: Plant<any, any>, setPlantConns: Dispatch<SetStateAction<Conns>>) {
for (const {event: a} of plant.uiEvents) {
for (const {event: b} of plant.inputEvents) {
if (a === b) {
@ -462,7 +488,8 @@ function autoDetectConns(ast: Statechart, plant: Plant<any>, setPlantConns: Disp
}
}
function ConnEditor(ast: Statechart, plant: Plant<any>, plantConns: Conns, setPlantConns: Dispatch<SetStateAction<Conns>>) {
function ConnEditor(ast: Statechart, plant: Plant<any, any>, plantConns: Conns, setPlantConns: Dispatch<SetStateAction<Conns>>) {
const plantInputs = <>{plant.inputEvents.map(e => <option key={'plant.'+e.event} value={'plant.'+e.event}>plant.{e.event}</option>)}</>
const scInputs = <>{ast.inputEvents.map(e => <option key={'sc.'+e.event} value={'sc.'+e.event}>sc.{e.event}</option>)}</>;
return <>
@ -473,7 +500,8 @@ function ConnEditor(ast: Statechart, plant: Plant<any>, plantConns: Conns, setPl
<select id={`select-dst-sc-${e}`}
style={{width:'50%'}}
value={plantConns['sc.'+e]?.join('.')}
onChange={domEvent => setPlantConns(conns => ({...conns, [`sc.${e}`]: domEvent.target.value.split('.') as [string,string]}))}>
// @ts-ignore
onChange={domEvent => setPlantConns(conns => ({...conns, [`sc.${e}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
<option key="none" value=""></option>
{plantInputs}
</select>
@ -485,7 +513,8 @@ function ConnEditor(ast: Statechart, plant: Plant<any>, plantConns: Conns, setPl
<select id={`select-dst-plant-${e.event}`}
style={{width:'50%'}}
value={plantConns['plant.'+e.event]?.join('.')}
onChange={(domEvent => setPlantConns(conns => ({...conns, [`plant.${e.event}`]: domEvent.target.value.split('.') as [string,string]})))}>
// @ts-ignore
onChange={(domEvent => setPlantConns(conns => ({...conns, [`plant.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))})))}>
<option key="none" value=""></option>
{scInputs}
</select>
@ -497,7 +526,8 @@ function ConnEditor(ast: Statechart, plant: Plant<any>, plantConns: Conns, setPl
<select id={`select-dst-plant-ui-${e.event}`}
style={{width:'50%'}}
value={plantConns['plant.ui.'+e.event]?.join('.')}
onChange={domEvent => setPlantConns(conns => ({...conns, [`plant.ui.${e.event}`]: domEvent.target.value.split('.') as [string,string]}))}>
// @ts-ignore
onChange={domEvent => setPlantConns(conns => ({...conns, [`plant.ui.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
<option key="none" value=""></option>
{scInputs}
{plantInputs}

View file

@ -3,7 +3,7 @@ import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
import { detectConnections } from "@/statecharts/detect_connections";
import { parseStatechart } from "@/statecharts/parser";
import { RT_Statechart } from "@/statecharts/runtime_types";
import { useEffect } from "react";
import { memo, useEffect } from "react";
import { makeStatechartPlant, PlantRenderProps } from "../Plant";
import dwatchConcreteSyntax from "./model.json";
@ -12,6 +12,7 @@ import digitalFont from "./digital-font.ttf";
import "./DigitalWatch.css";
import imgNote from "./noteSmall.png";
import imgWatch from "./watch.png";
import { objectsEqual } from "@/util/util";
export const [dwatchAbstractSyntax, dwatchErrors] = parseStatechart(dwatchConcreteSyntax as ConcreteSyntax, detectConnections(dwatchConcreteSyntax as ConcreteSyntax));
@ -20,33 +21,66 @@ if (dwatchErrors.length > 0) {
throw new Error("there were errors parsing dwatch plant model. see console.")
}
export type DigitalWatchPlantState = {
lightOn: boolean,
beep: boolean,
alarmOn: boolean,
displayingTime: boolean,
displayingAlarm: boolean,
displayingChrono: boolean,
hideH: boolean,
hideM: boolean,
hideS: boolean,
h: number,
m: number,
s: number,
ah: number,
am: number,
as: number,
cm: number,
cs: number,
chs: number,
// these properties are true for as long as the mouse button is down:
topLeftPressed: boolean,
topRightPressed: boolean,
bottomRightPressed: boolean,
bottomLeftPressed: boolean,
}
function dwatchConfigToState(rtConfig: RT_Statechart): DigitalWatchPlantState {
return {
lightOn: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("lightOn")!.uid),
beep: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("beep")!.uid),
alarmOn: rtConfig.environment.get("alarm"),
displayingTime: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("displayingTime")!.uid),
displayingAlarm: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("displayingAlarm")!.uid),
displayingChrono: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("displayingChrono")!.uid),
hideH: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("hideH")!.uid),
hideM: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("hideM")!.uid),
hideS: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("hideS")!.uid),
h: rtConfig.environment.get("h"),
m: rtConfig.environment.get("m"),
s: rtConfig.environment.get("s"),
ah: rtConfig.environment.get("ah"),
am: rtConfig.environment.get("am"),
as: rtConfig.environment.get("as"),
cm: rtConfig.environment.get("cm"),
cs: rtConfig.environment.get("cs"),
chs: rtConfig.environment.get("chs"),
topLeftPressed: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("topLeftPressed")!.uid),
topRightPressed: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("topRightPressed")!.uid),
bottomRightPressed: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("bottomRightPressed")!.uid),
bottomLeftPressed: rtConfig.mode.has(dwatchAbstractSyntax.label2State.get("bottomLeftPressed")!.uid),
}
}
const twoDigits = (n: number) => ("0"+n.toString()).slice(-2);
export function DigitalWatch({state, speed, raiseUIEvent}: PlantRenderProps<RT_Statechart>) {
const displayingTime = state.mode.has("625");
const displayingAlarm = state.mode.has("626");
const displayingChrono = state.mode.has("624");
const lightOn = state.mode.has("630");
const alarm = state.environment.get("alarm");
const h = state.environment.get("h");
const m = state.environment.get("m");
const s = state.environment.get("s");
const ah = state.environment.get("ah");
const am = state.environment.get("am");
const as = state.environment.get("as");
const cm = state.environment.get("cm");
const cs = state.environment.get("cs");
const chs = state.environment.get("chs");
const hideH = state.mode.has("628");
const hideM = state.mode.has("633");
const hideS = state.mode.has("627");
// console.log({cm,cs,chs});
export const DigitalWatch = memo(function DigitalWatch({state: {displayingTime, displayingAlarm, displayingChrono, lightOn, alarmOn, beep, h, m, s, ah, am, as, cm, cs, chs, hideH, hideM, hideS}, speed, raiseUIEvent}: PlantRenderProps<DigitalWatchPlantState>) {
let hhmmss;
if (displayingTime) {
@ -63,8 +97,6 @@ export function DigitalWatch({state, speed, raiseUIEvent}: PlantRenderProps<RT_S
preloadAudio(sndBeep);
const beep = state.mode.has("632");
useEffect(() => {
if (beep) {
playSound(sndBeep, false);
@ -87,40 +119,41 @@ export function DigitalWatch({state, speed, raiseUIEvent}: PlantRenderProps<RT_S
<text x="111" y="126" dominantBaseline="middle" textAnchor="middle" fontFamily="digital-font" fontSize={28} style={{whiteSpace:'preserve'}}>{hhmmss}</text>
<rect className="watchButtonHelper" x={0} y={54} width={24} height={24}
onMouseDown={() => raiseUIEvent({name: "topLeftPressed"})}
onMouseUp={() => raiseUIEvent({name: "topLeftReleased"})}
onMouseDown={() => raiseUIEvent({name: "topLeftMouseDown"})}
onMouseUp={() => raiseUIEvent({name: "topLeftMouseUp"})}
/>
<rect className="watchButtonHelper" x={198} y={54} width={24} height={24}
onMouseDown={() => raiseUIEvent({name: "topRightPressed"})}
onMouseUp={() => raiseUIEvent({name: "topRightReleased"})}
onMouseDown={() => raiseUIEvent({name: "topRightMouseDown"})}
onMouseUp={() => raiseUIEvent({name: "topRightMouseUp"})}
/>
<rect className="watchButtonHelper" x={0} y={154} width={24} height={24}
onMouseDown={() => raiseUIEvent({name: "bottomLeftPressed"})}
onMouseUp={() => raiseUIEvent({name: "bottomLeftReleased"})}
onMouseDown={() => raiseUIEvent({name: "bottomLeftMouseDown"})}
onMouseUp={() => raiseUIEvent({name: "bottomLeftMouseUp"})}
/>
<rect className="watchButtonHelper" x={198} y={154} width={24} height={24}
onMouseDown={() => raiseUIEvent({name: "bottomRightPressed"})}
onMouseUp={() => raiseUIEvent({name: "bottomRightReleased"})}
onMouseDown={() => raiseUIEvent({name: "bottomRightMouseDown"})}
onMouseUp={() => raiseUIEvent({name: "bottomRightMouseUp"})}
/>
{alarm &&
{alarmOn &&
<image x="54" y="98" xlinkHref={imgNote} />
}
</svg>
</>;
}
}, objectsEqual);
export const digitalWatchPlant = makeStatechartPlant({
ast: dwatchAbstractSyntax,
cleanupState: dwatchConfigToState,
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" },
{ kind: "event", event: "topLeftMouseDown" },
{ kind: "event", event: "topRightMouseDown" },
{ kind: "event", event: "bottomRightMouseDown" },
{ kind: "event", event: "bottomLeftMouseDown" },
{ kind: "event", event: "topLeftMouseUp" },
{ kind: "event", event: "topRightMouseUp" },
{ kind: "event", event: "bottomRightMouseUp" },
{ kind: "event", event: "bottomLeftMouseUp" },
],
});

File diff suppressed because one or more lines are too long

View file

@ -1,17 +1,18 @@
import { Plant } from "../Plant";
import { TimedReactive } from "@/statecharts/timed_reactive";
export const dummyExecution: TimedReactive<null> = {
initial: () => [[], null],
export const dummyExecution: TimedReactive<{}> = {
initial: () => [[], {}],
timeAdvance: () => Infinity,
intTransition: () => { throw new Error("dummy never makes intTransition"); },
extTransition: () => [[], null],
extTransition: () => [[], {}],
};
export const dummyPlant: Plant<null> = {
export const dummyPlant: Plant<{}, {}> = {
uiEvents: [],
inputEvents: [],
outputEvents: [],
execution: dummyExecution,
render: (props) => <></>,
}
cleanupState: ({}) => ({}),
render: ({}) => <></>,
};

View file

@ -13,12 +13,13 @@ import { memo, useEffect } from "react";
import "./Microwave.css";
import { useAudioContext } from "../../useAudioContext";
import { comparePlantRenderProps, makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
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";
import { objectsEqual } from "@/util/util";
export const [microwaveAbstractSyntax, microwaveErrors] = parseStatechart(microwaveConcreteSyntax as ConcreteSyntax, detectConnections(microwaveConcreteSyntax as ConcreteSyntax));
@ -27,7 +28,6 @@ if (microwaveErrors.length > 0) {
throw new Error("there were errors parsing microwave plant model. see console.")
}
const imgs = {
"false": { "false": imgSmallClosedOff, "true": imgSmallClosedOn },
"true": { "false": imgSmallOpenedOff, "true": imgSmallOpenedOn },
@ -47,7 +47,19 @@ const DOOR_Y0 = 68;
const DOOR_WIDTH = 353;
const DOOR_HEIGHT = 217;
export const Microwave = memo(function Microwave({state, speed, raiseUIEvent}: PlantRenderProps<RT_Statechart>) {
type MicrowaveState = {
bellRinging: boolean,
magnetronRunning: boolean,
doorOpen: boolean,
timeDisplay: number,
// these booleans are true for as long as the respective button is pressed (i.e., mouse button is down)
startPressed: boolean,
stopPressed: boolean,
incTimePressed: boolean,
}
export const Microwave = memo(function Microwave({state: {bellRinging, magnetronRunning, doorOpen, timeDisplay}, speed, raiseUIEvent}: PlantRenderProps<MicrowaveState>) {
const [playSound, preloadAudio] = useAudioContext(speed);
// preload(imgSmallClosedOff, {as: "image"});
@ -58,11 +70,6 @@ export const Microwave = memo(function Microwave({state, speed, raiseUIEvent}: P
preloadAudio(sndRunning);
preloadAudio(sndBell);
const bellRinging = state.mode.has("12");
const magnetronRunning = state.mode.has("8");
const doorOpen = state.mode.has("7");
const timeDisplay = state.environment.get("timeDisplay");
// a bit hacky: when the bell-state changes to true, we play the bell sound...
useEffect(() => {
if (bellRinging) {
@ -90,16 +97,16 @@ export const Microwave = memo(function Microwave({state, speed, raiseUIEvent}: P
<image xlinkHref={imgs[doorOpen][magnetronRunning]} width={520} height={348}/>
<rect className="microwaveButtonHelper" x={START_X0} y={START_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
onMouseDown={() => raiseUIEvent({name: "startPressed"})}
onMouseUp={() => raiseUIEvent({name: "startReleased"})}
onMouseDown={() => raiseUIEvent({name: "startMouseDown"})}
onMouseUp={() => raiseUIEvent({name: "startMouseUp"})}
/>
<rect className="microwaveButtonHelper" x={STOP_X0} y={STOP_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
onMouseDown={() => raiseUIEvent({name: "stopPressed"})}
onMouseUp={() => raiseUIEvent({name: "stopReleased"})}
onMouseDown={() => raiseUIEvent({name: "stopMouseDown"})}
onMouseUp={() => raiseUIEvent({name: "stopMouseUp"})}
/>
<rect className="microwaveButtonHelper" x={INCTIME_X0} y={INCTIME_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
onMouseDown={() => raiseUIEvent({name: "incTimePressed"})}
onMouseUp={() => raiseUIEvent({name: "incTimeReleased"})}
onMouseDown={() => raiseUIEvent({name: "incTimeMouseDown"})}
onMouseUp={() => raiseUIEvent({name: "incTimeMouseUp"})}
/>
<rect className="microwaveDoorHelper"
x={DOOR_X0} y={DOOR_Y0} width={DOOR_WIDTH} height={DOOR_HEIGHT}
@ -110,20 +117,31 @@ export const Microwave = memo(function Microwave({state, speed, raiseUIEvent}: P
<text x={472} y={106} textAnchor="end" fontFamily="digital-font" fontSize={24} fill="lightgreen">{timeDisplay}</text>
</svg>
</>;
}, comparePlantRenderProps);
}, objectsEqual);
const microwavePlantSpec: StatechartPlantSpec = {
const microwavePlantSpec: StatechartPlantSpec<MicrowaveState> = {
ast: microwaveAbstractSyntax,
cleanupState: (state: RT_Statechart) => {
const bellRinging = state.mode.has(microwaveAbstractSyntax.label2State.get("bell")!.uid);
const magnetronRunning = state.mode.has(microwaveAbstractSyntax.label2State.get("Magnetron on")!.uid);
const doorOpen = state.mode.has(microwaveAbstractSyntax.label2State.get("Door opened")!.uid);
const startPressed = state.mode.has(microwaveAbstractSyntax.label2State.get("startPressed")!.uid);
const stopPressed = state.mode.has(microwaveAbstractSyntax.label2State.get("stopPressed")!.uid);
const incTimePressed = state.mode.has(microwaveAbstractSyntax.label2State.get("incTimePressed")!.uid);
// let startPressed, stopPressed, incTimePressed;
const timeDisplay = state.environment.get("timeDisplay");
return {bellRinging, magnetronRunning, doorOpen, timeDisplay, startPressed, stopPressed, incTimePressed};
},
render: Microwave,
uiEvents: [
{kind: "event", event: "doorMouseDown"},
{kind: "event", event: "doorMouseUp"},
{kind: "event", event: "startPressed"},
{kind: "event", event: "startReleased"},
{kind: "event", event: "stopPressed"},
{kind: "event", event: "stopReleased"},
{kind: "event", event: "incTimePressed"},
{kind: "event", event: "incTimeReleased"},
{kind: "event", event: "startMouseDown"},
{kind: "event", event: "startMouseUp"},
{kind: "event", event: "stopMouseDown"},
{kind: "event", event: "stopMouseUp"},
{kind: "event", event: "incTimeMouseDown"},
{kind: "event", event: "incTimeMouseUp"},
],
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View file

@ -1,9 +1,9 @@
import { ReactElement, ReactNode } from "react";
import { ReactNode } from "react";
import { Statechart } from "@/statecharts/abstract_syntax";
import { EventTrigger } from "@/statecharts/label_ast";
import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types";
import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive";
import { mapsEqual, setsEqual } from "@/util/util";
import { setsEqual } from "@/util/util";
export type PlantRenderProps<StateType> = {
state: StateType,
@ -11,18 +11,19 @@ export type PlantRenderProps<StateType> = {
raiseUIEvent: (e: RaisedEvent) => void,
};
export type Plant<StateType> = {
export type Plant<StateType, CleanStateType> = {
uiEvents: EventTrigger[];
inputEvents: EventTrigger[];
outputEvents: EventTrigger[];
execution: TimedReactive<StateType>;
render: (props: PlantRenderProps<StateType>) => ReactNode;
cleanupState: (state: StateType) => CleanStateType;
render: (props: PlantRenderProps<CleanStateType>) => ReactNode;
}
// Automatically connect Statechart and Plant inputs/outputs if their event names match.
export function autoConnect(ast: Statechart, scName: string, plant: Plant<any>, plantName: string) {
export function autoConnect(ast: Statechart, scName: string, plant: Plant<any, any>, plantName: string) {
const outputs = {
[scName]: {},
[plantName]: {},
@ -44,7 +45,7 @@ export function autoConnect(ast: Statechart, scName: string, plant: Plant<any>,
return outputs;
}
export function exposePlantInputs(plant: Plant<any>, plantName: string, tfm = (s: string) => s) {
export function exposePlantInputs(plant: Plant<any, any>, plantName: string, tfm = (s: string) => s) {
const inputs = {};
for (const i of plant.inputEvents) {
// @ts-ignore
@ -53,25 +54,27 @@ export function exposePlantInputs(plant: Plant<any>, plantName: string, tfm = (s
return inputs
}
export type StatechartPlantSpec = {
export type StatechartPlantSpec<CleanStateType> = {
uiEvents: EventTrigger[],
ast: Statechart,
render: (props: PlantRenderProps<RT_Statechart>) => ReactNode,
cleanupState: (rtConfig: RT_Statechart) => CleanStateType,
render: (props: PlantRenderProps<CleanStateType>) => ReactNode,
}
export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec): Plant<BigStep> {
export function makeStatechartPlant<CleanStateType>({uiEvents, ast, cleanupState, render}: StatechartPlantSpec<CleanStateType>): Plant<BigStep, CleanStateType> {
return {
uiEvents,
inputEvents: ast.inputEvents,
outputEvents: [...ast.outputEvents].map(e => ({kind: "event" as const, event: e})),
execution: statechartExecution(ast),
cleanupState,
render,
}
}
export function comparePlantRenderProps(oldProps: PlantRenderProps<RT_Statechart>, newProps: PlantRenderProps<RT_Statechart>) {
return setsEqual(oldProps.state.mode, newProps.state.mode)
&& oldProps.state.environment === newProps.state.environment // <-- could optimize this further
&& oldProps.speed === newProps.speed
&& oldProps.raiseUIEvent === newProps.raiseUIEvent
}
// export function comparePlantRenderProps(oldProps: PlantRenderProps<RT_Statechart>, newProps: PlantRenderProps<RT_Statechart>) {
// return setsEqual(oldProps.state.mode, newProps.state.mode)
// && oldProps.state.environment === newProps.state.environment // <-- could optimize this further
// && oldProps.speed === newProps.speed
// && oldProps.raiseUIEvent === newProps.raiseUIEvent
// }

View file

@ -11,10 +11,11 @@ import trafficLightConcreteSyntax from "./model.json";
import { parseStatechart } from "@/statecharts/parser";
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
import { detectConnections } from "@/statecharts/detect_connections";
import { comparePlantRenderProps, makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
import { RT_Statechart } from "@/statecharts/runtime_types";
import { useAudioContext } from "@/App/useAudioContext";
import { memo, useEffect } from "react";
import { objectsEqual } from "@/util/util";
const [trafficLightAbstractSyntax, trafficLightErrors] = parseStatechart(trafficLightConcreteSyntax as ConcreteSyntax, detectConnections(trafficLightConcreteSyntax as ConcreteSyntax));
@ -23,19 +24,20 @@ if (trafficLightErrors.length > 0) {
throw new Error("there were errors parsing traffic light plant model. see console.")
}
export const TrafficLight = memo(function TrafficLight({state, speed, raiseUIEvent}: PlantRenderProps<RT_Statechart>) {
type TrafficLightState = {
redOn: boolean,
yellowOn: boolean,
greenOn: boolean,
timerGreen: boolean,
timerValue: number,
}
export const TrafficLight = memo(function TrafficLight({state: {redOn, yellowOn, greenOn, timerGreen, timerValue}, speed, raiseUIEvent}: PlantRenderProps<TrafficLightState>) {
// preload(imgBackground, {as: "image"});
preload(imgRedOverlay, {as: "image"});
preload(imgYellowOverlay, {as: "image"});
preload(imgGreenOverlay, {as: "image"});
const redOn = state.mode.has("85");
const yellowOn = state.mode.has("87");
const greenOn = state.mode.has("89");
const timerGreen = state.mode.has("137");
const timerValue = state.environment.get("t");
const [playURL, preloadAudio] = useAudioContext(speed);
// preloadAudio(sndAtmosphere);
@ -89,10 +91,20 @@ export const TrafficLight = memo(function TrafficLight({state, speed, raiseUIEve
<br/>
<button onClick={() => raiseUIEvent({name: "policeInterrupt"})}>POLICE INTERRUPT</button>
</>;
}, comparePlantRenderProps);
}, (oldProps, newProps) => {
return objectsEqual(oldProps, newProps);
});
const trafficLightPlantSpec: StatechartPlantSpec = {
const trafficLightPlantSpec: StatechartPlantSpec<TrafficLightState> = {
ast: trafficLightAbstractSyntax,
cleanupState: (state: RT_Statechart) => {
const redOn = state.mode.has("85");
const yellowOn = state.mode.has("87");
const greenOn = state.mode.has("89");
const timerGreen = state.mode.has("137");
const timerValue = state.environment.get("t");
return { redOn, yellowOn, greenOn, timerGreen, timerValue };
},
render: TrafficLight,
uiEvents: [
{kind: "event", event: "policeInterrupt"},

Binary file not shown.

Binary file not shown.

View file

@ -12,9 +12,30 @@ type RTHistoryProps = {
ast: Statechart,
setTime: Dispatch<SetStateAction<TimeMode>>,
showPlantTrace: boolean,
propertyTrace: {timestamp: number, satisfied: boolean}[] | null,
}
export function RTHistory({trace, setTrace, ast, setTime, showPlantTrace}: RTHistoryProps) {
function lookupPropertyStatus(simtime: number, propertyTrace: {timestamp: number, satisfied: boolean}[], startAt=0): [number, boolean | undefined] {
let i = startAt;
while (i >= 0 && i < propertyTrace.length) {
const {timestamp} = propertyTrace[i];
if (timestamp === simtime) {
// exact match
break;
}
else if (timestamp > simtime) {
i--;
// too far
break;
}
// continue...
i++;
}
i = Math.min(i, propertyTrace.length-1);
return [i, propertyTrace[i] && propertyTrace[i].satisfied];
}
export function RTHistory({trace, setTrace, ast, setTime, showPlantTrace, propertyTrace}: RTHistoryProps) {
const onMouseDown = useCallback((idx: number, timestamp: number) => {
setTrace(trace => trace && {
...trace,
@ -26,6 +47,7 @@ export function RTHistory({trace, setTrace, ast, setTime, showPlantTrace}: RTHis
if (trace === null) {
return <></>;
}
let j = 0;
return trace.trace.map((item, i) => {
const prevItem = trace.trace[i-1];
// @ts-ignore
@ -33,7 +55,16 @@ export function RTHistory({trace, setTrace, ast, setTime, showPlantTrace}: RTHis
if (!showPlantTrace && isPlantStep) {
return <></>
}
return <RTHistoryItem ast={ast} idx={i} item={item} prevItem={prevItem} isPlantStep={isPlantStep} active={i === trace.idx} onMouseDown={onMouseDown}/>;
let propertyClasses = "status";
if (propertyTrace !== null) {
let satisfied;
[j, satisfied] = lookupPropertyStatus(item.simtime, propertyTrace, j);
// console.log(item.simtime, j, propertyTrace[j]);
if (satisfied !== null && satisfied !== undefined) {
propertyClasses += (satisfied ? " satisfied" : " violated");
}
}
return <RTHistoryItem ast={ast} idx={i} item={item} prevItem={prevItem} isPlantStep={isPlantStep} active={i === trace.idx} onMouseDown={onMouseDown} propertyClasses={propertyClasses} />;
});
}
@ -55,7 +86,7 @@ function RTEventParam(props: {param?: any}) {
return <>{props.param !== undefined && <>({JSON.stringify(props.param)})</>}</>;
}
export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevItem, isPlantStep, active, onMouseDown}: {idx: number, ast: Statechart, item: TraceItem, prevItem?: TraceItem, isPlantStep: boolean, active: boolean, onMouseDown: (idx: number, timestamp: number) => void}) {
export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevItem, isPlantStep, active, onMouseDown, propertyClasses}: {idx: number, ast: Statechart, item: TraceItem, prevItem?: TraceItem, isPlantStep: boolean, active: boolean, onMouseDown: (idx: number, timestamp: number) => void, propertyClasses: string}) {
if (item.kind === "bigstep") {
// @ts-ignore
const newStates = item.state.sc.mode.difference(prevItem?.state.sc.mode || new Set());
@ -63,6 +94,8 @@ export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevIt
className={"runtimeState" + (active ? " active" : "") + (isPlantStep ? " plantStep" : "")}
onMouseDown={useCallback(() => onMouseDown(idx, item.simtime), [idx, item.simtime])}>
<div>
<div className={propertyClasses}/>
&emsp;
{formatTime(item.simtime)}
&emsp;
<div className="inputEvent"><RTCause cause={isPlantStep ? item.state.plant.inputEvent : item.state.sc.inputEvent}/></div>

View file

@ -118,7 +118,7 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp
const value = inputParams[key] || "";
const width = Math.max(value.length, (paramName||"").length)*6;
const shortcut = (i+1)%10;
const KI = (i <= 10) ? KeyInfo : KeyInfoHidden;
const KI = (i < 10) ? KeyInfo : KeyInfoHidden;
return <div key={key} className="toolbarGroup">
<KI keyInfo={<kbd>{shortcut}</kbd>} horizontal={true}>
<button

View file

@ -30,6 +30,8 @@ export const SpeedControl = memo(function SpeedControl({showKeys, timescale, set
}, [onTimeScaleChange, timescale]);
const onKeyDown = useCallback((e: KeyboardEvent) => {
// @ts-ignore
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
if (!e.ctrlKey) {
if (e.key === "s") {
e.preventDefault();

View file

@ -118,6 +118,8 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
}, [setState]);
const onKeyDown = (e: KeyboardEvent) => {
// @ts-ignore
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
if (e.key === "Delete") {
// delete selection
makeCheckPoint();

79
src/App/check_property.ts Normal file
View file

@ -0,0 +1,79 @@
import { RT_Statechart } from "@/statecharts/runtime_types";
import { TraceItem } from "./App";
import { Plant } from "./Plant/Plant";
// const endpoint = "http://localhost:15478/check_property";
const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
export type PropertyTrace = {
timestamp: number,
satisfied: boolean,
}[];
export type PropertyCheckResult = [null|PropertyTrace, null|string];
export async function checkProperty(plant: Plant<RT_Statechart, any>, property: string, trace: [TraceItem, ...TraceItem[]]): Promise<PropertyCheckResult> {
// pre-process data...
const cleanPlantStates0 = trace
.map(v => {
return {
simtime: v.simtime,
state: Object.fromEntries(Object.entries(v.kind === "bigstep" && plant.cleanupState(v.state.plant) || {}).map(([prop, val]) => [prop, Boolean(val)])),
};
});
const cleanPlantStates = cleanPlantStates0 && cleanPlantStates0
// we can never have multiple states at the same point in simtime or Argus will panic
.reduce((trace, entry, i) => {
const prevEntry = cleanPlantStates0[i-1];
if (prevEntry !== undefined) {
if (entry.simtime > prevEntry.simtime) {
return [...trace, entry]; // ok
}
return [...trace.slice(0,-1), entry]; // current entry has same simtime and thus replaces previous entry
}
return [entry];
}, [] as {simtime: number, state: any}[]);
let traces = {} as {[key: string]: [number, any][]};
for (const {simtime, state} of cleanPlantStates) {
for (const [key, value] of Object.entries(state)) {
// just append
traces[key] = traces[key] || [];
const prevSample = traces[key].at(-1);
// only append sample if value changed:
if (!prevSample || prevSample[1] !== value) {
traces[key].push([simtime, value]);
}
}
}
console.log({cleanPlantStates, traces});
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
property,
traces,
}),
});
const json = await response.json();
// console.log('backend result:', json);
if (typeof json === 'string') {
return [null, json];
}
else {
return [json.map(([timestamp, satisfied]) => ({timestamp, satisfied})), null];
}
}
catch (e) {
return [null, e.message];
}
}

View file

@ -7,9 +7,9 @@ import { emptyState } from "@/statecharts/concrete_syntax";
export function useEditor(editorState: VisualEditorState | null, setEditHistory: Dispatch<SetStateAction<EditHistory|null>>) {
useEffect(() => {
console.log("Welcome to StateBuddy!");
console.info("Welcome to StateBuddy!");
() => {
console.log("Goodbye!");
console.info("Goodbye!");
}
}, []);

View file

@ -2,6 +2,7 @@ html, body {
margin: 0;
height: 100%;
font-family: Roboto, sans-serif;
font-size: 10pt;
}
body {
@ -33,8 +34,6 @@ kbd:active { transform: translateY(1px); }
input {
/* border: solid blue 2px; */
accent-color: rgba(0,0,255,0.2);
/* accent-color: blue; */
}
::selection {
@ -44,3 +43,4 @@ input {
label {
user-select: none;
}

View file

@ -64,6 +64,8 @@ export type Statechart = {
uid2State: Map<string, ConcreteState|UnstableState>;
label2State: Map<string, ConcreteState>;
historyStates: HistoryState[];
}
@ -88,6 +90,7 @@ export const emptyStatechart: Statechart = {
internalEvents: [],
outputEvents: new Set(),
uid2State: new Map([["root", emptyRoot]]),
label2State: new Map([]),
historyStates: [],
};

View file

@ -5,7 +5,7 @@ import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
import { parse as parseLabel, SyntaxError } from "./label_parser";
import { Connections } from "./detect_connections";
import { HISTORY_RADIUS } from "../App/parameters";
import { ConcreteSyntax, VisualEditorState } from "@/App/VisualEditor/VisualEditor";
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
import { memoize } from "@/util/util";
export type TraceableError = {
@ -52,6 +52,7 @@ export function parseStatechart(state: ConcreteSyntax, conns: Connections): [Sta
}
const uid2State = new Map<string, ConcreteState|UnstableState>([["root", root]]);
const label2State = new Map<string, ConcreteState>();
const historyStates: HistoryState[] = [];
// we will always look for the smallest parent rountangle
@ -329,6 +330,9 @@ export function parseStatechart(state: ConcreteSyntax, conns: Connections): [Sta
}
else if (parsed.kind === "comment") {
// just append comments to their respective states
if (!label2State.has(parsed.text)) {
label2State.set(parsed.text, belongsToState);
}
belongsToState.comments.push([text.uid, parsed.text]);
}
}
@ -384,6 +388,7 @@ export function parseStatechart(state: ConcreteSyntax, conns: Connections): [Sta
internalEvents,
outputEvents,
uid2State,
label2State,
historyStates,
}, errors];
}

View file

@ -194,12 +194,13 @@ export function coupledExecution<T extends {[name: string]: any}>(models: {[name
throw new Error("cannot make intTransition - timeAdvance is infinity");
},
extTransition: (simtime, c, e) => {
if (!Object.hasOwn(conns, e.name)) {
if (conns[e.name] === undefined) {
console.warn('input event', e.name, 'goes to nowhere');
return [[], c];
}
else {
const [model, eventName] = conns[e.name];
// console.log(conns);
if (model !== null) {
console.log('input event', e.name, 'goes to', `${model}.${eventName}`);
const inputEvent: InputEvent = {

View file

@ -83,4 +83,12 @@ export function mapsEqual<K,V>(a: Map<K,V>, b: Map<K,V>, cmp: (a: V, b: V) => bo
}
return true;
}
}
export function withGrow<T>(arr: T[], i: number, value: T, fill: T) {
if (i >= arr.length) {
arr = [...arr, ...new Array(i - arr.length + 1).map(_ => fill)];
}
return arr.with(i, value);
}

View file

@ -28,7 +28,7 @@
TODO
- bugs
interpreter: pseudo-state semantics is broken
editing SC <-> Plant connections at runtime doesn't seem to work
- testing
use STL for testing
@ -51,15 +51,8 @@ TODO
- hovering over event in side panel should highlight all occurrences of the event in the SC
- hovering over error in bottom panel should highlight that rror in the SC
- highlight selected shapes while making a selection
- highlight about-to-fire transitions
- when there is a runtime error, e.g.,
- variable not found
- stuck in pseudo-state
- ???
don't crash and show the error
- buttons to rotate selection 90 degrees
- integrate undo-history with browser history (back/forward buttons)
- performance:
maybe try this for rendering the execution trace:
@ -69,7 +62,7 @@ TODO
- multiverse execution history
stable tree layout?
https://pub.dev/packages/ploeg_tree_layout
- local scopes
- local variable scopes
for the assignment:
*ALL* features

39
trash/argus.ts Normal file
View file

@ -0,0 +1,39 @@
// Some deleted code for calling argus...
// // Dynamically import the JS loader
// const wasm = await import("argus-wasm/pkg/argus_wasm.js");
// // @ts-ignore
// import wasmfile from "../../node_modules/argus-wasm/pkg/argus_wasm_bg.wasm";
// async function initWasm() {
// // Initialize the module with the URL to the .wasm
// await wasm.default(wasmfile);
// }
// window.initWasm = initWasm;
// initWasm();
// let evaluation = null;
// let propertyError: null | string = null;
// try {
// if (cleanPlantStates) {
// // throws runtime error if Rust panics:
// evaluation = wasm.eval_boolean(property, {entries: cleanPlantStates});
// }
// }
// catch (e) {
// propertyError = "property evaluation panic'ed: " + e.message;
// initWasm();
// }
// let propertyTrace: null | {timestamp: number, satisfied: boolean}[] = null;
// if (typeof evaluation === 'string') {
// propertyError = evaluation;
// }
// else if (evaluation !== null && Array.isArray(evaluation.entries)) {
// // propertyTrace = evaluation.entries.map(({satisfied}) => satisfied);
// propertyTrace = evaluation.entries;
// }