diff --git a/global.d.ts b/global.d.ts index 8a65801..905fa4f 100644 --- a/global.d.ts +++ b/global.d.ts @@ -2,4 +2,6 @@ declare module '*.css'; declare module '*.png'; declare module '*.ttf'; -declare module '*.wav'; \ No newline at end of file +declare module '*.wav'; +declare module '*.opus'; +declare module '*.webp'; \ No newline at end of file diff --git a/src/App/App.tsx b/src/App/App.tsx index c5286e1..8f519c1 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -34,6 +34,7 @@ const plants: [string, Plant][] = [ ["dummy", dummyPlant], ["microwave", microwavePlant], ["digital watch", digitalWatchPlant], + ["traffic light", trafficLightPlant], ] export type TraceItemError = { @@ -357,6 +358,7 @@ export function App() { )} +
{/* Render plant */} { 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, setPlantConns: Dispatch>) { for (const {event: a} of plant.uiEvents) { @@ -463,18 +466,6 @@ function ConnEditor(ast: Statechart, plant: Plant, plantConns: Conns, setPl const plantInputs = <>{plant.inputEvents.map(e => )} const scInputs = <>{ast.inputEvents.map(e => )}; return <> - {/* Plant UI events can go to SC or to Plant */} - {plant.uiEvents.map(e =>
- - -
)} {/* SC output events can go to Plant */} {[...ast.outputEvents].map(e =>
@@ -499,6 +490,19 @@ function ConnEditor(ast: Statechart, plant: Plant, plantConns: Conns, setPl {scInputs}
)]} + + {/* Plant UI events can go to SC or to Plant */} + {plant.uiEvents.map(e =>
+ + +
)} ; } diff --git a/src/App/Plant/Microwave/Microwave.tsx b/src/App/Plant/Microwave/Microwave.tsx index 033f22b..99615d0 100644 --- a/src/App/Plant/Microwave/Microwave.tsx +++ b/src/App/Plant/Microwave/Microwave.tsx @@ -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) { +export const Microwave = memo(function Microwave({state, speed, raiseUIEvent}: PlantRenderProps) { const [playSound, preloadAudio] = useAudioContext(speed); // preload(imgSmallClosedOff, {as: "image"}); @@ -106,10 +106,11 @@ export function Microwave({state, speed, raiseUIEvent}: PlantRenderProps raiseUIEvent({name: "doorMouseDown"})} onMouseUp={() => raiseUIEvent({name: "doorMouseUp"})} /> + {timeDisplay} ; -} +}, comparePlantRenderProps); const microwavePlantSpec: StatechartPlantSpec = { ast: microwaveAbstractSyntax, diff --git a/src/App/Plant/Plant.ts b/src/App/Plant/Plant.ts index bc47f79..3d2c9ba 100644 --- a/src/App/Plant/Plant.ts +++ b/src/App/Plant/Plant.ts @@ -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 = { state: StateType, @@ -17,7 +18,7 @@ export type Plant = { outputEvents: EventTrigger[]; execution: TimedReactive; - render: (props: PlantRenderProps) => ReactElement; + render: (props: PlantRenderProps) => ReactNode; } // Automatically connect Statechart and Plant inputs/outputs if their event names match. @@ -55,7 +56,7 @@ export function exposePlantInputs(plant: Plant, plantName: string, tfm = (s export type StatechartPlantSpec = { uiEvents: EventTrigger[], ast: Statechart, - render: (props: PlantRenderProps) => ReactElement, + render: (props: PlantRenderProps) => ReactNode, } export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec): Plant { @@ -67,3 +68,10 @@ export function makeStatechartPlant({uiEvents, ast, render}: StatechartPlantSpec render, } } + +export function comparePlantRenderProps(oldProps: PlantRenderProps, newProps: PlantRenderProps) { + 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 +} diff --git a/src/App/Plant/TrafficLight/TrafficLight.tsx b/src/App/Plant/TrafficLight/TrafficLight.tsx new file mode 100644 index 0000000..622783f --- /dev/null +++ b/src/App/Plant/TrafficLight/TrafficLight.tsx @@ -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) { + // 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 <> + + + + + + + + + {timerValue >= 0 && <> + + {timerValue} + } + +
+ + ; +}, comparePlantRenderProps); + +const trafficLightPlantSpec: StatechartPlantSpec = { + ast: trafficLightAbstractSyntax, + render: TrafficLight, + uiEvents: [ + {kind: "event", event: "policeInterrupt"}, + ], +} + +export const trafficLightPlant = makeStatechartPlant(trafficLightPlantSpec); diff --git a/src/App/Plant/TrafficLight/atmosphere.opus b/src/App/Plant/TrafficLight/atmosphere.opus new file mode 100644 index 0000000..2f73da7 Binary files /dev/null and b/src/App/Plant/TrafficLight/atmosphere.opus differ diff --git a/src/App/Plant/TrafficLight/background.png b/src/App/Plant/TrafficLight/background.png deleted file mode 100644 index ab10afd..0000000 Binary files a/src/App/Plant/TrafficLight/background.png and /dev/null differ diff --git a/src/App/Plant/TrafficLight/background.webp b/src/App/Plant/TrafficLight/background.webp new file mode 100644 index 0000000..5c2f02f Binary files /dev/null and b/src/App/Plant/TrafficLight/background.webp differ diff --git a/src/App/Plant/TrafficLight/green-overlay.png b/src/App/Plant/TrafficLight/green-overlay.png deleted file mode 100644 index b8cb5d8..0000000 Binary files a/src/App/Plant/TrafficLight/green-overlay.png and /dev/null differ diff --git a/src/App/Plant/TrafficLight/green-overlay.webp b/src/App/Plant/TrafficLight/green-overlay.webp new file mode 100644 index 0000000..333f69f Binary files /dev/null and b/src/App/Plant/TrafficLight/green-overlay.webp differ diff --git a/src/App/Plant/TrafficLight/green.png b/src/App/Plant/TrafficLight/green.png deleted file mode 100644 index 32ff8ac..0000000 Binary files a/src/App/Plant/TrafficLight/green.png and /dev/null differ diff --git a/src/App/Plant/TrafficLight/model.json b/src/App/Plant/TrafficLight/model.json new file mode 100644 index 0000000..3f05762 --- /dev/null +++ b/src/App/Plant/TrafficLight/model.json @@ -0,0 +1 @@ +{"rountangles":[{"uid":"80","topLeft":{"x":67.4999999999999,"y":206.2499999999999},"size":{"x":1999.9999999999995,"y":872.4999999999998},"kind":"and"},{"uid":"129","topLeft":{"x":387.4999999999997,"y":229.99999999999986},"size":{"x":1639.9999999999995,"y":821.2499999999997},"kind":"or"},{"uid":"128","topLeft":{"x":424.9999999999998,"y":326.2499999999998},"size":{"x":1568.7499999999995,"y":688.7499999999999},"kind":"and"},{"uid":"82","topLeft":{"x":943.7499999999994,"y":377.49999999999983},"size":{"x":508.7499999999998,"y":592.4999999999997},"kind":"or"},{"uid":"83","topLeft":{"x":1484.999999999999,"y":379.99999999999983},"size":{"x":478.74999999999983,"y":584.9999999999998},"kind":"or"},{"uid":"81","topLeft":{"x":458.7499999999998,"y":374.99999999999983},"size":{"x":453.7499999999998,"y":599.9999999999995},"kind":"or"},{"uid":"133","topLeft":{"x":99.99999999999994,"y":609.9999999999997},"size":{"x":242.4999999999999,"y":436.2499999999998},"kind":"or"},{"uid":"121","topLeft":{"x":102.49999999999994,"y":229.99999999999986},"size":{"x":241.2499999999999,"y":353.7499999999999},"kind":"or"},{"uid":"89","topLeft":{"x":1591.249999999999,"y":754.9999999999994},"size":{"x":276.2499999999999,"y":146.2499999999999},"kind":"and"},{"uid":"87","topLeft":{"x":1088.7499999999993,"y":756.2499999999994},"size":{"x":258.7499999999999,"y":148.74999999999994},"kind":"and"},{"uid":"88","topLeft":{"x":1598.749999999999,"y":489.9999999999998},"size":{"x":254.99999999999983,"y":121.24999999999994},"kind":"and"},{"uid":"86","topLeft":{"x":1103.7499999999993,"y":498.7499999999998},"size":{"x":226.2499999999999,"y":117.49999999999994},"kind":"and"},{"uid":"85","topLeft":{"x":579.9999999999997,"y":764.9999999999994},"size":{"x":192.49999999999994,"y":131.24999999999994},"kind":"and"},{"uid":"84","topLeft":{"x":576.2499999999997,"y":511.2499999999998},"size":{"x":184.99999999999991,"y":116.24999999999994},"kind":"and"},{"uid":"137","topLeft":{"x":138.74999999999991,"y":937.4999999999994},"size":{"x":162.49999999999994,"y":78.74999999999994},"kind":"and"},{"uid":"134","topLeft":{"x":142.49999999999991,"y":726.2499999999995},"size":{"x":158.74999999999997,"y":66.24999999999991},"kind":"and"},{"uid":"122","topLeft":{"x":142.49999999999991,"y":379.99999999999966},"size":{"x":87.49999999999999,"y":119.99999999999997},"kind":"and"}],"diamonds":[],"history":[],"arrows":[{"uid":"90","start":{"x":732.4999999999995,"y":628.7499999999997},"end":{"x":732.4999999999995,"y":756.2499999999995}},{"uid":"91","start":{"x":611.2499999999997,"y":761.2499999999994},"end":{"x":613.7499999999997,"y":634.9999999999994}},{"uid":"92","start":{"x":1299.9999999999993,"y":613.7499999999997},"end":{"x":1304.9999999999993,"y":751.2499999999997}},{"uid":"93","start":{"x":1137.4999999999993,"y":752.4999999999994},"end":{"x":1136.2499999999993,"y":621.2499999999995}},{"uid":"94","start":{"x":1823.7499999999989,"y":613.7499999999997},"end":{"x":1824.9999999999989,"y":743.7499999999997}},{"uid":"95","start":{"x":1637.499999999999,"y":753.7499999999994},"end":{"x":1638.749999999999,"y":613.7499999999994}},{"uid":"96","start":{"x":1606.249999999999,"y":434.99999999999983},"end":{"x":1678.749999999999,"y":482.49999999999983}},{"uid":"97","start":{"x":1098.7499999999993,"y":417.49999999999983},"end":{"x":1174.9999999999993,"y":489.9999999999998}},{"uid":"98","start":{"x":587.4999999999997,"y":431.24999999999983},"end":{"x":634.9999999999997,"y":499.9999999999998}},{"uid":"99","start":{"x":504.9999999999997,"y":141.24999999999991},"end":{"x":556.2499999999997,"y":198.74999999999991}},{"uid":"112","start":{"x":688.7499999999997,"y":1016.2499999999991},"end":{"x":689.9999999999997,"y":903.7499999999991}},{"uid":"115","start":{"x":1226.2499999999993,"y":1013.7499999999991},"end":{"x":1228.7499999999993,"y":909.9999999999991}},{"uid":"116","start":{"x":1759.9999999999989,"y":1012.4999999999991},"end":{"x":1759.9999999999989,"y":904.9999999999991}},{"uid":"119","start":{"x":1539.9999999999989,"y":323.7499999999999},"end":{"x":1456.2499999999989,"y":319.99999999999983}},{"uid":"123","start":{"x":153.74999999999991,"y":297.4999999999998},"end":{"x":179.99999999999991,"y":371.2499999999998}},{"uid":"125","start":{"x":196.2499999999999,"y":497.49999999999966},"end":{"x":237.4999999999999,"y":448.74999999999966}},{"uid":"131","start":{"x":579.9999999999997,"y":278.74999999999983},"end":{"x":632.4999999999997,"y":317.49999999999983}},{"uid":"136","start":{"x":143.74999999999991,"y":669.9999999999995},"end":{"x":197.49999999999991,"y":722.4999999999995}},{"uid":"138","start":{"x":271.24999999999983,"y":793.7499999999995},"end":{"x":268.74999999999983,"y":933.7499999999995}},{"uid":"139","start":{"x":171.2499999999999,"y":934.9999999999994},"end":{"x":171.2499999999999,"y":794.9999999999994}}],"texts":[{"uid":"100","text":"// red off","topLeft":{"x":663.7499999999997,"y":569.9999999999997}},{"uid":"101","text":"// red on","topLeft":{"x":669.9999999999997,"y":839.9999999999994}},{"uid":"102","text":"// yellow off","topLeft":{"x":1211.2499999999993,"y":561.2499999999997}},{"uid":"103","text":"// yellow on","topLeft":{"x":1211.2499999999993,"y":841.2499999999994}},{"uid":"104","text":"// green off","topLeft":{"x":1729.999999999999,"y":554.9999999999997}},{"uid":"105","text":"// green on","topLeft":{"x":1731.249999999999,"y":827.4999999999994}},{"uid":"106","text":"greenOn","topLeft":{"x":1831.2499999999989,"y":679.9999999999995}},{"uid":"107","text":"greenOff","topLeft":{"x":1622.4999999999989,"y":684.9999999999995}},{"uid":"108","text":"yellowOn","topLeft":{"x":1301.2499999999993,"y":689.9999999999995}},{"uid":"109","text":"yellowOff","topLeft":{"x":1134.9999999999993,"y":689.9999999999995}},{"uid":"110","text":"redOn","topLeft":{"x":738.7499999999995,"y":703.7499999999995}},{"uid":"111","text":"redOff","topLeft":{"x":614.9999999999997,"y":704.9999999999995}},{"uid":"113","text":"displayRed","topLeft":{"x":683.7499999999997,"y":947.4999999999993}},{"uid":"117","text":"displayYellow","topLeft":{"x":1238.7499999999993,"y":943.7499999999993}},{"uid":"118","text":"displayGreen","topLeft":{"x":1759.9999999999989,"y":937.4999999999991}},{"uid":"120","text":"displayNone","topLeft":{"x":1502.4999999999989,"y":306.2499999999999}},{"uid":"126","text":"setTimerValue(v) / t=v","topLeft":{"x":247.49999999999983,"y":491.24999999999966}},{"uid":"127","text":"entry / t=-1","topLeft":{"x":323.74999999999955,"y":161.24999999999994}},{"uid":"132","text":"// light mode","topLeft":{"x":813.7499999999995,"y":273.74999999999983}},{"uid":"140","text":"// timer red","topLeft":{"x":217.4999999999999,"y":762.4999999999995}},{"uid":"141","text":"// timer green","topLeft":{"x":218.74999999999986,"y":987.4999999999994}},{"uid":"142","text":"setTimerGreen","topLeft":{"x":274.99999999999983,"y":851.2499999999994}},{"uid":"143","text":"setTimerRed","topLeft":{"x":174.99999999999991,"y":886.2499999999994}}]} \ No newline at end of file diff --git a/src/App/Plant/TrafficLight/original_material/Screenshot_20251031_101255.png b/src/App/Plant/TrafficLight/original_material/Screenshot_20251031_101255.png new file mode 100644 index 0000000..0daa287 Binary files /dev/null and b/src/App/Plant/TrafficLight/original_material/Screenshot_20251031_101255.png differ diff --git a/src/App/Plant/TrafficLight/original_material/The Traffic Lights of Twin Peaks (Pt. 13) [TubeRipper.aup3 b/src/App/Plant/TrafficLight/original_material/The Traffic Lights of Twin Peaks (Pt. 13) [TubeRipper.aup3 new file mode 100644 index 0000000..65cd59b Binary files /dev/null and b/src/App/Plant/TrafficLight/original_material/The Traffic Lights of Twin Peaks (Pt. 13) [TubeRipper.aup3 differ diff --git a/src/App/Plant/TrafficLight/original_material/Twin+Peaks+Complete+spread+8+mix1&2.png b/src/App/Plant/TrafficLight/original_material/Twin+Peaks+Complete+spread+8+mix1&2.png new file mode 100644 index 0000000..8d31a87 Binary files /dev/null and b/src/App/Plant/TrafficLight/original_material/Twin+Peaks+Complete+spread+8+mix1&2.png differ diff --git a/src/App/Plant/TrafficLight/trafficlight.xcf b/src/App/Plant/TrafficLight/original_material/trafficlight.xcf similarity index 100% rename from src/App/Plant/TrafficLight/trafficlight.xcf rename to src/App/Plant/TrafficLight/original_material/trafficlight.xcf diff --git a/src/App/Plant/TrafficLight/red-overlay.png b/src/App/Plant/TrafficLight/red-overlay.png deleted file mode 100644 index b0421a9..0000000 Binary files a/src/App/Plant/TrafficLight/red-overlay.png and /dev/null differ diff --git a/src/App/Plant/TrafficLight/red-overlay.webp b/src/App/Plant/TrafficLight/red-overlay.webp new file mode 100644 index 0000000..f7ed403 Binary files /dev/null and b/src/App/Plant/TrafficLight/red-overlay.webp differ diff --git a/src/App/Plant/TrafficLight/red.png b/src/App/Plant/TrafficLight/red.png deleted file mode 100644 index f7d9b5f..0000000 Binary files a/src/App/Plant/TrafficLight/red.png and /dev/null differ diff --git a/src/App/Plant/TrafficLight/yellow-overlay.png b/src/App/Plant/TrafficLight/yellow-overlay.png deleted file mode 100644 index ded9f19..0000000 Binary files a/src/App/Plant/TrafficLight/yellow-overlay.png and /dev/null differ diff --git a/src/App/Plant/TrafficLight/yellow-overlay.webp b/src/App/Plant/TrafficLight/yellow-overlay.webp new file mode 100644 index 0000000..7d47e3e Binary files /dev/null and b/src/App/Plant/TrafficLight/yellow-overlay.webp differ diff --git a/src/App/Plant/TrafficLight/yellow.png b/src/App/Plant/TrafficLight/yellow.png deleted file mode 100644 index 76a7bbb..0000000 Binary files a/src/App/Plant/TrafficLight/yellow.png and /dev/null differ diff --git a/src/App/VisualEditor/useMouse.tsx b/src/App/VisualEditor/useMouse.tsx index f234142..9ef94e5 100644 --- a/src/App/VisualEditor/useMouse.tsx +++ b/src/App/VisualEditor/useMouse.tsx @@ -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"]})), ] })) } diff --git a/src/util/util.ts b/src/util/util.ts index 8ced141..1eb6c21 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -66,3 +66,21 @@ export function objectsEqual(a: {[key: string]: T}, b: {[key: string]: T}, cm return true; } + +export function mapsEqual(a: Map, b: Map, 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; +} \ No newline at end of file