finished traffic light example + use WebP format for images
4
global.d.ts
vendored
|
|
@ -2,4 +2,6 @@
|
|||
declare module '*.css';
|
||||
declare module '*.png';
|
||||
declare module '*.ttf';
|
||||
declare module '*.wav';
|
||||
declare module '*.wav';
|
||||
declare module '*.opus';
|
||||
declare module '*.webp';
|
||||
|
|
@ -34,6 +34,7 @@ const plants: [string, Plant<any>][] = [
|
|||
["dummy", dummyPlant],
|
||||
["microwave", microwavePlant],
|
||||
["digital watch", digitalWatchPlant],
|
||||
["traffic light", trafficLightPlant],
|
||||
]
|
||||
|
||||
export type TraceItemError = {
|
||||
|
|
@ -357,6 +358,7 @@ export function App() {
|
|||
<option>{plantName}</option>
|
||||
)}
|
||||
</select>
|
||||
<br/>
|
||||
{/* Render plant */}
|
||||
{<plant.render state={plantState} speed={speed}
|
||||
raiseUIEvent={e => onRaise("plant.ui."+e.name, e.param)}
|
||||
|
|
@ -428,6 +430,7 @@ function ShowConns({inputEvents, outputEvents}: Conns) {
|
|||
|
||||
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>>) {
|
||||
for (const {event: a} of plant.uiEvents) {
|
||||
|
|
@ -463,18 +466,6 @@ function ConnEditor(ast: Statechart, plant: Plant<any>, plantConns: Conns, setPl
|
|||
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 <>
|
||||
{/* Plant UI events can go to SC or to Plant */}
|
||||
{plant.uiEvents.map(e => <div style={{width:'100%', textAlign:'right'}}>
|
||||
<label htmlFor={`select-dst-plant-ui-${e.event}`} style={{width:'50%'}}>ui.{e.event} → </label>
|
||||
<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]}))}>
|
||||
<option key="none" value=""></option>
|
||||
{scInputs}
|
||||
{plantInputs}
|
||||
</select>
|
||||
</div>)}
|
||||
|
||||
{/* SC output events can go to Plant */}
|
||||
{[...ast.outputEvents].map(e => <div style={{width:'100%', textAlign:'right'}}>
|
||||
|
|
@ -499,6 +490,19 @@ function ConnEditor(ast: Statechart, plant: Plant<any>, plantConns: Conns, setPl
|
|||
{scInputs}
|
||||
</select>
|
||||
</div>)]}
|
||||
|
||||
{/* Plant UI events can go to SC or to Plant */}
|
||||
{plant.uiEvents.map(e => <div style={{width:'100%', textAlign:'right'}}>
|
||||
<label htmlFor={`select-dst-plant-ui-${e.event}`} style={{width:'50%'}}>ui.{e.event} → </label>
|
||||
<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]}))}>
|
||||
<option key="none" value=""></option>
|
||||
{scInputs}
|
||||
{plantInputs}
|
||||
</select>
|
||||
</div>)}
|
||||
</>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import fontDigital from "../DigitalWatch/digital-font.ttf";
|
|||
import sndBell from "./bell.wav";
|
||||
import sndRunning from "./running.wav";
|
||||
import { RT_Statechart } from "@/statecharts/runtime_types";
|
||||
import { useEffect } from "react";
|
||||
import { memo, useEffect } from "react";
|
||||
|
||||
import "./Microwave.css";
|
||||
import { useAudioContext } from "../../useAudioContext";
|
||||
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
|
||||
import { comparePlantRenderProps, makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
|
||||
import { detectConnections } from "@/statecharts/detect_connections";
|
||||
import { parseStatechart } from "@/statecharts/parser";
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ const DOOR_Y0 = 68;
|
|||
const DOOR_WIDTH = 353;
|
||||
const DOOR_HEIGHT = 217;
|
||||
|
||||
export function Microwave({state, speed, raiseUIEvent}: PlantRenderProps<RT_Statechart>) {
|
||||
export const Microwave = memo(function Microwave({state, speed, raiseUIEvent}: PlantRenderProps<RT_Statechart>) {
|
||||
const [playSound, preloadAudio] = useAudioContext(speed);
|
||||
|
||||
// preload(imgSmallClosedOff, {as: "image"});
|
||||
|
|
@ -106,10 +106,11 @@ export function Microwave({state, speed, raiseUIEvent}: PlantRenderProps<RT_Stat
|
|||
onMouseDown={() => raiseUIEvent({name: "doorMouseDown"})}
|
||||
onMouseUp={() => raiseUIEvent({name: "doorMouseUp"})}
|
||||
/>
|
||||
|
||||
<text x={472} y={106} textAnchor="end" fontFamily="digital-font" fontSize={24} fill="lightgreen">{timeDisplay}</text>
|
||||
</svg>
|
||||
</>;
|
||||
}
|
||||
}, comparePlantRenderProps);
|
||||
|
||||
const microwavePlantSpec: StatechartPlantSpec = {
|
||||
ast: microwaveAbstractSyntax,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { ReactElement } from "react";
|
||||
import { ReactElement, 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";
|
||||
|
||||
export type PlantRenderProps<StateType> = {
|
||||
state: StateType,
|
||||
|
|
@ -17,7 +18,7 @@ export type Plant<StateType> = {
|
|||
outputEvents: EventTrigger[];
|
||||
|
||||
execution: TimedReactive<StateType>;
|
||||
render: (props: PlantRenderProps<StateType>) => ReactElement;
|
||||
render: (props: PlantRenderProps<StateType>) => ReactNode;
|
||||
}
|
||||
|
||||
// Automatically connect Statechart and Plant inputs/outputs if their event names match.
|
||||
|
|
@ -55,7 +56,7 @@ export function exposePlantInputs(plant: Plant<any>, plantName: string, tfm = (s
|
|||
export type StatechartPlantSpec = {
|
||||
uiEvents: EventTrigger[],
|
||||
ast: Statechart,
|
||||
render: (props: PlantRenderProps<RT_Statechart>) => ReactElement,
|
||||
render: (props: PlantRenderProps<RT_Statechart>) => ReactNode,
|
||||
}
|
||||
|
||||
export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec): Plant<BigStep> {
|
||||
|
|
@ -67,3 +68,10 @@ export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec
|
|||
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
|
||||
}
|
||||
|
|
|
|||
86
src/App/Plant/TrafficLight/TrafficLight.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import fontDigital from "../DigitalWatch/digital-font.ttf";
|
||||
import imgBackground from "./background.webp";
|
||||
import imgRedOverlay from "./red-overlay.webp";
|
||||
import imgYellowOverlay from "./yellow-overlay.webp";
|
||||
import imgGreenOverlay from "./green-overlay.webp";
|
||||
import sndAtmosphere from "./atmosphere.opus";
|
||||
import { preload } from "react-dom";
|
||||
|
||||
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 { RT_Statechart } from "@/statecharts/runtime_types";
|
||||
import { useAudioContext } from "@/App/useAudioContext";
|
||||
import { memo, useEffect } from "react";
|
||||
|
||||
const [trafficLightAbstractSyntax, trafficLightErrors] = parseStatechart(trafficLightConcreteSyntax as ConcreteSyntax, detectConnections(trafficLightConcreteSyntax as ConcreteSyntax));
|
||||
|
||||
if (trafficLightErrors.length > 0) {
|
||||
console.log({trafficLightErrors});
|
||||
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>) {
|
||||
// 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);
|
||||
|
||||
// the traffic light makes sound too:
|
||||
useEffect(() => {
|
||||
const stopPlaying = playURL(sndAtmosphere, true);
|
||||
return () => stopPlaying();
|
||||
}, []);
|
||||
|
||||
return <>
|
||||
<style>{`
|
||||
@font-face{
|
||||
font-family: 'digital-font';
|
||||
src: url(${fontDigital});
|
||||
}
|
||||
image {
|
||||
transition: opacity 250ms ease;
|
||||
}
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
`}</style>
|
||||
<svg width={200} height='auto' viewBox="0 0 424 791">
|
||||
<image xlinkHref={imgBackground} width={424} height={791}/>
|
||||
|
||||
<image className={redOn ? "" : "hidden"} xlinkHref={imgRedOverlay} width={424} height={791}/>
|
||||
<image className={yellowOn ? "" : "hidden"} xlinkHref={imgYellowOverlay} width={424} height={791}/>
|
||||
<image className={greenOn ? "" : "hidden"} xlinkHref={imgGreenOverlay} width={424} height={791}/>
|
||||
|
||||
{timerValue >= 0 && <>
|
||||
<rect x={300} y={678} width={110} height={80} fill="black" />
|
||||
<text x={400} y={750} fontFamily="digital-font" fontSize={100} fill={timerGreen ? "#59ae8b" : "#f9172e"} textAnchor="end">{timerValue}</text>
|
||||
</>}
|
||||
</svg>
|
||||
<br/>
|
||||
<button onClick={() => raiseUIEvent({name: "policeInterrupt"})}>POLICE INTERRUPT</button>
|
||||
</>;
|
||||
}, comparePlantRenderProps);
|
||||
|
||||
const trafficLightPlantSpec: StatechartPlantSpec = {
|
||||
ast: trafficLightAbstractSyntax,
|
||||
render: TrafficLight,
|
||||
uiEvents: [
|
||||
{kind: "event", event: "policeInterrupt"},
|
||||
],
|
||||
}
|
||||
|
||||
export const trafficLightPlant = makeStatechartPlant(trafficLightPlantSpec);
|
||||
BIN
src/App/Plant/TrafficLight/atmosphere.opus
Normal file
|
Before Width: | Height: | Size: 176 KiB |
BIN
src/App/Plant/TrafficLight/background.webp
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 25 KiB |
BIN
src/App/Plant/TrafficLight/green-overlay.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 190 KiB |
1
src/App/Plant/TrafficLight/model.json
Normal file
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 86 KiB |
BIN
src/App/Plant/TrafficLight/red-overlay.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 46 KiB |
BIN
src/App/Plant/TrafficLight/yellow-overlay.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
|
@ -322,6 +322,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
|||
...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"]})),
|
||||
...state.history.map(h => ({uid: h.uid, parts: ["history"]})),
|
||||
]
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,3 +66,21 @@ export function objectsEqual<T>(a: {[key: string]: T}, b: {[key: string]: T}, cm
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function mapsEqual<K,V>(a: Map<K,V>, b: Map<K,V>, cmp: (a: V, b: V) => boolean = (a,b)=>a===b) {
|
||||
if (a===b)
|
||||
return true;
|
||||
|
||||
if (a.size !== b.size)
|
||||
return false;
|
||||
|
||||
for (const [keyA,valA] of a.entries()) {
|
||||
const valB = b.get(keyA);
|
||||
if (valB === undefined)
|
||||
return false;
|
||||
if (!cmp(valA, valB))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||