finished traffic light example + use WebP format for images
4
global.d.ts
vendored
|
|
@ -2,4 +2,6 @@
|
||||||
declare module '*.css';
|
declare module '*.css';
|
||||||
declare module '*.png';
|
declare module '*.png';
|
||||||
declare module '*.ttf';
|
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],
|
["dummy", dummyPlant],
|
||||||
["microwave", microwavePlant],
|
["microwave", microwavePlant],
|
||||||
["digital watch", digitalWatchPlant],
|
["digital watch", digitalWatchPlant],
|
||||||
|
["traffic light", trafficLightPlant],
|
||||||
]
|
]
|
||||||
|
|
||||||
export type TraceItemError = {
|
export type TraceItemError = {
|
||||||
|
|
@ -357,6 +358,7 @@ export function App() {
|
||||||
<option>{plantName}</option>
|
<option>{plantName}</option>
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
|
<br/>
|
||||||
{/* Render plant */}
|
{/* Render plant */}
|
||||||
{<plant.render state={plantState} speed={speed}
|
{<plant.render state={plantState} speed={speed}
|
||||||
raiseUIEvent={e => onRaise("plant.ui."+e.name, e.param)}
|
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 AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
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>, setPlantConns: Dispatch<SetStateAction<Conns>>) {
|
||||||
for (const {event: a} of plant.uiEvents) {
|
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 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>)}</>;
|
const scInputs = <>{ast.inputEvents.map(e => <option key={'sc.'+e.event} value={'sc.'+e.event}>sc.{e.event}</option>)}</>;
|
||||||
return <>
|
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 */}
|
{/* SC output events can go to Plant */}
|
||||||
{[...ast.outputEvents].map(e => <div style={{width:'100%', textAlign:'right'}}>
|
{[...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}
|
{scInputs}
|
||||||
</select>
|
</select>
|
||||||
</div>)]}
|
</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 sndBell from "./bell.wav";
|
||||||
import sndRunning from "./running.wav";
|
import sndRunning from "./running.wav";
|
||||||
import { RT_Statechart } from "@/statecharts/runtime_types";
|
import { RT_Statechart } from "@/statecharts/runtime_types";
|
||||||
import { useEffect } from "react";
|
import { memo, useEffect } from "react";
|
||||||
|
|
||||||
import "./Microwave.css";
|
import "./Microwave.css";
|
||||||
import { useAudioContext } from "../../useAudioContext";
|
import { useAudioContext } from "../../useAudioContext";
|
||||||
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
|
import { comparePlantRenderProps, makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
|
||||||
import { detectConnections } from "@/statecharts/detect_connections";
|
import { detectConnections } from "@/statecharts/detect_connections";
|
||||||
import { parseStatechart } from "@/statecharts/parser";
|
import { parseStatechart } from "@/statecharts/parser";
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ const DOOR_Y0 = 68;
|
||||||
const DOOR_WIDTH = 353;
|
const DOOR_WIDTH = 353;
|
||||||
const DOOR_HEIGHT = 217;
|
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);
|
const [playSound, preloadAudio] = useAudioContext(speed);
|
||||||
|
|
||||||
// preload(imgSmallClosedOff, {as: "image"});
|
// preload(imgSmallClosedOff, {as: "image"});
|
||||||
|
|
@ -106,10 +106,11 @@ export function Microwave({state, speed, raiseUIEvent}: PlantRenderProps<RT_Stat
|
||||||
onMouseDown={() => raiseUIEvent({name: "doorMouseDown"})}
|
onMouseDown={() => raiseUIEvent({name: "doorMouseDown"})}
|
||||||
onMouseUp={() => raiseUIEvent({name: "doorMouseUp"})}
|
onMouseUp={() => raiseUIEvent({name: "doorMouseUp"})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<text x={472} y={106} textAnchor="end" fontFamily="digital-font" fontSize={24} fill="lightgreen">{timeDisplay}</text>
|
<text x={472} y={106} textAnchor="end" fontFamily="digital-font" fontSize={24} fill="lightgreen">{timeDisplay}</text>
|
||||||
</svg>
|
</svg>
|
||||||
</>;
|
</>;
|
||||||
}
|
}, comparePlantRenderProps);
|
||||||
|
|
||||||
const microwavePlantSpec: StatechartPlantSpec = {
|
const microwavePlantSpec: StatechartPlantSpec = {
|
||||||
ast: microwaveAbstractSyntax,
|
ast: microwaveAbstractSyntax,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { ReactElement } from "react";
|
import { ReactElement, ReactNode } from "react";
|
||||||
import { Statechart } from "@/statecharts/abstract_syntax";
|
import { Statechart } from "@/statecharts/abstract_syntax";
|
||||||
import { EventTrigger } from "@/statecharts/label_ast";
|
import { EventTrigger } from "@/statecharts/label_ast";
|
||||||
import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types";
|
import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types";
|
||||||
import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive";
|
import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive";
|
||||||
|
import { mapsEqual, setsEqual } from "@/util/util";
|
||||||
|
|
||||||
export type PlantRenderProps<StateType> = {
|
export type PlantRenderProps<StateType> = {
|
||||||
state: StateType,
|
state: StateType,
|
||||||
|
|
@ -17,7 +18,7 @@ export type Plant<StateType> = {
|
||||||
outputEvents: EventTrigger[];
|
outputEvents: EventTrigger[];
|
||||||
|
|
||||||
execution: TimedReactive<StateType>;
|
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.
|
// 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 = {
|
export type StatechartPlantSpec = {
|
||||||
uiEvents: EventTrigger[],
|
uiEvents: EventTrigger[],
|
||||||
ast: Statechart,
|
ast: Statechart,
|
||||||
render: (props: PlantRenderProps<RT_Statechart>) => ReactElement,
|
render: (props: PlantRenderProps<RT_Statechart>) => ReactNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec): Plant<BigStep> {
|
export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec): Plant<BigStep> {
|
||||||
|
|
@ -67,3 +68,10 @@ export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec
|
||||||
render,
|
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.diamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})),
|
||||||
...state.arrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
...state.arrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
||||||
...state.texts.map(t => ({uid: t.uid, parts: ["text"]})),
|
...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;
|
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;
|
||||||
|
}
|
||||||