From e27d3c4c880158cbb504b9168306068e2434613a Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Tue, 28 Oct 2025 21:52:30 +0100 Subject: [PATCH] digital watch plant is now also a statechart --- src/App/AST.css | 3 + src/App/App.tsx | 124 ++++++++++++- src/App/Plant/DigitalWatch/DigitalWatch.tsx | 187 ++++++++++---------- src/App/Plant/DigitalWatch/model.json | 1 + src/App/Plant/Microwave/Microwave.tsx | 12 +- src/App/Plant/Plant.ts | 1 - src/App/RTHistory.tsx | 2 +- src/App/TopPanel/RotateButtons.tsx | 23 +++ src/App/TopPanel/TopPanel.tsx | 9 +- src/App/VisualEditor/VisualEditor.tsx | 5 +- src/statecharts/timed_reactive.ts | 23 ++- src/util/geometry.ts | 62 +++++++ 12 files changed, 334 insertions(+), 118 deletions(-) create mode 100644 src/App/Plant/DigitalWatch/model.json create mode 100644 src/App/TopPanel/RotateButtons.tsx diff --git a/src/App/AST.css b/src/App/AST.css index d2e6f7f..f2081f6 100644 --- a/src/App/AST.css +++ b/src/App/AST.css @@ -75,6 +75,9 @@ details > summary:hover { color: black; display: inline-block; } +.inputEvent:disabled { + color: darkgrey; +} .inputEvent * { vertical-align: middle; } diff --git a/src/App/App.tsx b/src/App/App.tsx index 41fdc31..cac3ed8 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -21,6 +21,9 @@ import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from " import { TopPanel } from "./TopPanel/TopPanel"; import { getKeyHandler } from "./VisualEditor/shortcut_handler"; import { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; +import { addV2D, rotateLine90CCW, rotateLine90CW, rotatePoint90CCW, rotatePoint90CW, rotateRect90CCW, rotateRect90CW, scaleV2D, subtractV2D, Vec2D } from "@/util/geometry"; +import { HISTORY_RADIUS } from "./parameters"; +import { DigitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch"; export type EditHistory = { current: VisualEditorState, @@ -31,8 +34,7 @@ export type EditHistory = { const plants: [string, Plant][] = [ ["dummy", DummyPlant], ["microwave", MicrowavePlant], - - // ["digital watch", DigitalWatchPlant], + ["digital watch", DigitalWatchPlant], ] export type TraceItemError = { @@ -198,6 +200,121 @@ export function App() { } }); }, [setEditHistory]); + const onRotate = useCallback((direction: "ccw" | "cw") => { + makeCheckPoint(); + setEditHistory(historyState => { + if (historyState === null) return null; + + const selection = historyState.current.selection; + + if (selection.length === 0) { + return historyState; + } + + // determine bounding box... in a convoluted manner + let minX = -Infinity, minY = -Infinity, maxX = Infinity, maxY = Infinity; + + function addPointToBBox({x,y}: Vec2D) { + minX = Math.max(minX, x); + minY = Math.max(minY, y); + maxX = Math.min(maxX, x); + maxY = Math.min(maxY, y); + } + + for (const rt of historyState.current.rountangles) { + if (selection.some(s => s.uid === rt.uid)) { + addPointToBBox(rt.topLeft); + addPointToBBox(addV2D(rt.topLeft, rt.size)); + } + } + for (const d of historyState.current.diamonds) { + if (selection.some(s => s.uid === d.uid)) { + addPointToBBox(d.topLeft); + addPointToBBox(addV2D(d.topLeft, d.size)); + } + } + for (const arr of historyState.current.arrows) { + if (selection.some(s => s.uid === arr.uid)) { + addPointToBBox(arr.start); + addPointToBBox(arr.end); + } + } + for (const txt of historyState.current.texts) { + if (selection.some(s => s.uid === txt.uid)) { + addPointToBBox(txt.topLeft); + } + } + const historySize = {x: HISTORY_RADIUS, y: HISTORY_RADIUS}; + for (const h of historyState.current.history) { + if (selection.some(s => s.uid === h.uid)) { + addPointToBBox(h.topLeft); + addPointToBBox(addV2D(h.topLeft, scaleV2D(historySize, 2))); + } + } + + const center: Vec2D = { + x: (minX + maxX) / 2, + y: (minY + maxY) / 2, + }; + + const mapIfSelected = (shape: {uid: string}, cb: (shape:any)=>any) => { + if (selection.some(s => s.uid === shape.uid)) { + return cb(shape); + } + else { + return shape; + } + } + + return { + ...historyState, + current: { + ...historyState.current, + rountangles: historyState.current.rountangles.map(rt => mapIfSelected(rt, rt => { + return { + ...rt, + ...(direction === "ccw" + ? rotateRect90CCW(rt, center) + : rotateRect90CW(rt, center)), + } + })), + arrows: historyState.current.arrows.map(arr => mapIfSelected(arr, arr => { + return { + ...arr, + ...(direction === "ccw" + ? rotateLine90CCW(arr, center) + : rotateLine90CW(arr, center)), + }; + })), + diamonds: historyState.current.diamonds.map(d => mapIfSelected(d, d => { + return { + ...d, + ...(direction === "ccw" + ? rotateRect90CCW(d, center) + : rotateRect90CW(d, center)), + }; + })), + texts: historyState.current.texts.map(txt => mapIfSelected(txt, txt => { + return { + ...txt, + topLeft: (direction === "ccw" + ? rotatePoint90CCW(txt.topLeft, center) + : rotatePoint90CW(txt.topLeft, center)), + }; + })), + history: historyState.current.history.map(h => mapIfSelected(h, h => { + return { + ...h, + topLeft: (direction === "ccw" + ? subtractV2D(rotatePoint90CCW(addV2D(h.topLeft, historySize), center), historySize) + : subtractV2D(rotatePoint90CW(addV2D(h.topLeft, historySize), center), historySize) + ), + }; + })), + }, + } + }) + }, [setEditHistory]); const scrollDownSidebar = useCallback(() => { if (refRightSideBar.current) { @@ -400,7 +517,7 @@ export function App() { style={{flex: '0 0 content'}} > {editHistory && } {/* Editor */} @@ -458,7 +575,6 @@ export function App() { {plantConns && } {currentBigStep && onRaise("PLANT_UI_"+e.name, e.param)} - raiseOutput={() => {}} />}
setShowExecutionTrace(e.newState === "open")}>execution trace diff --git a/src/App/Plant/DigitalWatch/DigitalWatch.tsx b/src/App/Plant/DigitalWatch/DigitalWatch.tsx index 9f7bbb9..fcc2b61 100644 --- a/src/App/Plant/DigitalWatch/DigitalWatch.tsx +++ b/src/App/Plant/DigitalWatch/DigitalWatch.tsx @@ -1,46 +1,71 @@ +import { useAudioContext } from "@/App/useAudioContext"; +import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor"; +import { detectConnections } from "@/statecharts/detect_connections"; +import { parseStatechart } from "@/statecharts/parser"; +import { BigStep, RT_Statechart } from "@/statecharts/runtime_types"; +import { statechartExecution } from "@/statecharts/timed_reactive"; +import { useEffect } from "react"; +import { Plant, PlantRenderProps } from "../Plant"; + +import dwatchConcreteSyntax from "./model.json"; +import sndBeep from "./beep.wav"; +import digitalFont from "./digital-font.ttf"; +import "./DigitalWatch.css"; import imgNote from "./noteSmall.png"; import imgWatch from "./watch.png"; -import digitalFont from "./digital-font.ttf"; -import { Plant } from "../Plant"; -import { RaisedEvent } from "@/statecharts/runtime_types"; -import sndBeep from "./beep.wav"; -import "./DigitalWatch.css"; -import { useAudioContext } from "@/App/useAudioContext"; -import { useEffect } from "react"; +export const [dwatchAbstractSyntax, dwatchErrors] = parseStatechart(dwatchConcreteSyntax as ConcreteSyntax, detectConnections(dwatchConcreteSyntax as ConcreteSyntax)); -type DigitalWatchState = { - light: boolean; - h: number; - m: number; - s: number; - alarm: boolean; - beep: boolean; +if (dwatchErrors.length > 0) { + console.log({dwatchErrors}); + throw new Error("there were errors parsing dwatch plant model. see console.") } -type DigitalWatchProps = { - state: DigitalWatchState, - speed: number - callbacks: { - onTopLeftPressed: () => void; - onTopRightPressed: () => void; - onBottomRightPressed: () => void; - onBottomLeftPressed: () => void; - onTopLeftReleased: () => void; - onTopRightReleased: () => void; - onBottomRightReleased: () => void; - onBottomLeftReleased: () => void; - }, -} -export function DigitalWatch({state: {light, h, m, s, alarm, beep}, speed, callbacks}: DigitalWatchProps) { - const twoDigits = (n: number) => n < 0 ? " " : ("0"+n.toString()).slice(-2); - const hhmmss = `${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}`; +const twoDigits = (n: number) => ("0"+n.toString()).slice(-2); + +export function DigitalWatch({state, speed, raiseInput}: PlantRenderProps) { + const displayingTime = state.mode.has("265"); + const displayingAlarm = state.mode.has("266"); + const displayingChrono = state.mode.has("264"); + + const lightOn = state.mode.has("389"); + + 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("268"); + const hideM = state.mode.has("271"); + const hideS = state.mode.has("267"); + + // console.log({cm,cs,chs}); + + let hhmmss; + if (displayingTime) { + hhmmss = `${hideH ? " " : twoDigits(h)}:${hideM ? " " : twoDigits(m)}:${hideS ? " " : twoDigits(s)}`; + } + else if (displayingAlarm) { + hhmmss = `${hideH ? " " : twoDigits(ah)}:${hideM ? " " : twoDigits(am)}:${hideS ? " " : twoDigits(as)}`; + } + else if (displayingChrono) { + hhmmss = `${hideH ? " " : twoDigits(cm)}:${hideM ? " " : twoDigits(cs)}:${hideS ? " " : twoDigits(chs)}`; + } const [playSound, preloadAudio] = useAudioContext(speed); preloadAudio(sndBeep); + const beep = state.mode.has("270"); + useEffect(() => { if (beep) { playSound(sndBeep, false); @@ -57,26 +82,26 @@ export function DigitalWatch({state: {light, h, m, s, alarm, beep}, speed, callb - {light && + {lightOn && } {hhmmss} callbacks.onTopLeftPressed()} - onMouseUp={() => callbacks.onTopLeftReleased()} + onMouseDown={() => raiseInput({name: "topLeftPressed"})} + onMouseUp={() => raiseInput({name: "topLeftReleased"})} /> callbacks.onTopRightPressed()} - onMouseUp={() => callbacks.onTopRightReleased()} + onMouseDown={() => raiseInput({name: "topRightPressed"})} + onMouseUp={() => raiseInput({name: "topRightReleased"})} /> callbacks.onBottomLeftPressed()} - onMouseUp={() => callbacks.onBottomLeftReleased()} + onMouseDown={() => raiseInput({name: "bottomLeftPressed"})} + onMouseUp={() => raiseInput({name: "bottomLeftReleased"})} /> callbacks.onBottomRightPressed()} - onMouseUp={() => callbacks.onBottomRightReleased()} + onMouseDown={() => raiseInput({name: "bottomRightPressed"})} + onMouseUp={() => raiseInput({name: "bottomRightReleased"})} /> {alarm && @@ -86,16 +111,25 @@ export function DigitalWatch({state: {light, h, m, s, alarm, beep}, speed, callb ; } -export const DigitalWatchPlant: Plant = { +export const DigitalWatchPlant: Plant = { inputEvents: [ - { kind: "event", event: "setH", paramName: 'h' }, - { kind: "event", event: "setM", paramName: 'm' }, - { kind: "event", event: "setS", paramName: 's' }, - { kind: "event", event: "setLight", paramName: 'lightOn'}, + { kind: "event", event: "displayTime" }, + { kind: "event", event: "displayChrono" }, + { kind: "event", event: "displayAlarm" }, + { kind: "event", event: "beginEdit" }, + { kind: "event", event: "endEdit" }, + { kind: "event", event: "selectNext" }, + { kind: "event", event: "incSelection" }, + { kind: "event", event: "incTime" }, + { kind: "event", event: "incAlarm" }, + { kind: "event", event: "incChrono" }, + { kind: "event", event: "resetChrono" }, + { kind: "event", event: "lightOn"}, + { kind: "event", event: "lightOff"}, { kind: "event", event: "setAlarm", paramName: 'alarmOn'}, { kind: "event", event: "beep", paramName: 'beep'}, - ], - outputEvents: [ + + // UI events { kind: "event", event: "topLeftPressed" }, { kind: "event", event: "topRightPressed" }, { kind: "event", event: "bottomRightPressed" }, @@ -105,49 +139,18 @@ export const DigitalWatchPlant: Plant = { { kind: "event", event: "bottomRightReleased" }, { kind: "event", event: "bottomLeftReleased" }, ], - initial: { - light: false, - alarm: false, - h: 12, - m: 0, - s: 0, - beep: false, - }, - reduce: (inputEvent: RaisedEvent, state: DigitalWatchState) => { - if (inputEvent.name === "setH") { - return { ...state, h: inputEvent.param }; - } - if (inputEvent.name === "setM") { - return { ...state, m: inputEvent.param }; - } - if (inputEvent.name === "setS") { - return { ...state, s: inputEvent.param }; - } - if (inputEvent.name === "lightOn") { - return { ...state, light: true }; - } - if (inputEvent.name === "lightOff") { - return { ...state, light: false }; - } - if (inputEvent.name === "setAlarm") { - return { ...state, alarm: true }; - } - if (inputEvent.name === "unsetAlarm") { - return { ...state, alarm: false }; - } - if (inputEvent.name === "beep") { - return { ...state, beep: inputEvent.param }; - } - return state; // unknown event - ignore it - }, - render: (state, raiseEvent, speed) => raiseEvent({name: "topLeftPressed"}), - onTopRightPressed: () => raiseEvent({name: "topRightPressed"}), - onBottomRightPressed: () => raiseEvent({name: "bottomRightPressed"}), - onBottomLeftPressed: () => raiseEvent({name: "bottomLeftPressed"}), - onTopLeftReleased: () => raiseEvent({name: "topLeftReleased"}), - onTopRightReleased: () => raiseEvent({name: "topRightReleased"}), - onBottomRightReleased: () => raiseEvent({name: "bottomRightReleased"}), - onBottomLeftReleased: () => raiseEvent({name: "bottomLeftReleased"}), - }}/>, + outputEvents: [ + { kind: "event", event: "alarm" }, + + { kind: "event", event: "topLeftPressed" }, + { kind: "event", event: "topRightPressed" }, + { kind: "event", event: "bottomRightPressed" }, + { kind: "event", event: "bottomLeftPressed" }, + { kind: "event", event: "topLeftReleased" }, + { kind: "event", event: "topRightReleased" }, + { kind: "event", event: "bottomRightReleased" }, + { kind: "event", event: "bottomLeftReleased" }, + ], + execution: statechartExecution(dwatchAbstractSyntax), + render: DigitalWatch, } diff --git a/src/App/Plant/DigitalWatch/model.json b/src/App/Plant/DigitalWatch/model.json new file mode 100644 index 0000000..d8689b0 --- /dev/null +++ b/src/App/Plant/DigitalWatch/model.json @@ -0,0 +1 @@ +{"rountangles":[{"uid":"251","topLeft":{"x":258.8281250000001,"y":127.34374999999989},"size":{"x":2542.5781249999986,"y":1096.3281249999998},"kind":"and"},{"uid":"252","topLeft":{"x":1188.5859375,"y":404.33593750000057},"size":{"x":1173.140625,"y":793.296875},"kind":"or"},{"uid":"253","topLeft":{"x":1240.1593750000002,"y":551.365624999999},"size":{"x":1069.7749999999987,"y":602.15625},"kind":"or"},{"uid":"254","topLeft":{"x":280.3125000000002,"y":722.352081298828},"size":{"x":879.4374999999998,"y":478.2812500000001},"kind":"or"},{"uid":"417","topLeft":{"x":2392.4999999999986,"y":162.49999999999991},"size":{"x":392.49999999999994,"y":1028.7499999999995},"kind":"or"},{"uid":"255","topLeft":{"x":658.7031249999999,"y":161.6197967529296},"size":{"x":500.15625,"y":539.2500000000002},"kind":"or"},{"uid":"256","topLeft":{"x":1185.9109375000012,"y":163.97656249999932},"size":{"x":928.378124999999,"y":216.07812499999991},"kind":"or"},{"uid":"396","topLeft":{"x":1218.7499999999993,"y":183.7499999999999},"size":{"x":802.4999999999995,"y":173.74999999999994},"kind":"or"},{"uid":"257","topLeft":{"x":281.95312499999795,"y":160.07812499999966},"size":{"x":360.3124999999998,"y":281.95312499999983},"kind":"or"},{"uid":"387","topLeft":{"x":279.99999999999983,"y":463.7499999999997},"size":{"x":361.2499999999999,"y":237.49999999999991},"kind":"or"},{"uid":"258","topLeft":{"x":1476.7093749999988,"y":717.209374999999},"size":{"x":267,"y":249.125},"kind":"or"},{"uid":"418","topLeft":{"x":2482.4999999999986,"y":314.99999999999983},"size":{"x":82.49999999999997,"y":786.2499999999998},"kind":"and"},{"uid":"259","topLeft":{"x":1824.709374999999,"y":588.334374999999},"size":{"x":258.5999999999999,"y":216.8},"kind":"or"},{"uid":"399","topLeft":{"x":327.49999999999983,"y":497.49999999999966},"size":{"x":293.74999999999994,"y":188.74999999999994},"kind":"or"},{"uid":"260","topLeft":{"x":373.7500000000001,"y":804.832290649414},"size":{"x":160.9375,"y":307.75},"kind":"and"},{"uid":"405","topLeft":{"x":2142.4999999999986,"y":163.74999999999994},"size":{"x":216.2499999999999,"y":212.49999999999983},"kind":"or"},{"uid":"261","topLeft":{"x":1797.459374999999,"y":931.4343749999962},"size":{"x":241.3999999999998,"y":186.4000000000002},"kind":"or"},{"uid":"262","topLeft":{"x":710.9218749999999,"y":308.36979675293003},"size":{"x":367,"y":65.00000000000001},"kind":"and"},{"uid":"263","topLeft":{"x":1318.1328125,"y":427.21093750000045},"size":{"x":307.6249999999999,"y":67.390625},"kind":"and"},{"uid":"264","topLeft":{"x":1790.9765625000007,"y":239.7265624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"265","topLeft":{"x":1329.726562500001,"y":240.9765624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"266","topLeft":{"x":1548.476562500001,"y":239.7265624999991},"size":{"x":188.25,"y":86.93750000000003},"kind":"and"},{"uid":"267","topLeft":{"x":1621.959374999999,"y":808.084374999999},"size":{"x":94,"y":136},"kind":"and"},{"uid":"268","topLeft":{"x":1850.709374999999,"y":644.334374999999},"size":{"x":78,"y":135},"kind":"and"},{"uid":"388","topLeft":{"x":373.7499999999998,"y":518.7499999999997},"size":{"x":186.24999999999997,"y":55.00000000000006},"kind":"and"},{"uid":"389","topLeft":{"x":376.2499999999998,"y":617.4999999999997},"size":{"x":177.49999999999994,"y":56.25},"kind":"and"},{"uid":"269","topLeft":{"x":342.5781249999975,"y":261.71874999999955},"size":{"x":190.00000000000006,"y":51.562499999999986},"kind":"and"},{"uid":"270","topLeft":{"x":333.82812499999795,"y":377.0312499999993},"size":{"x":194.6875,"y":44.68749999999996},"kind":"and"},{"uid":"271","topLeft":{"x":1817.459374999999,"y":961.834374999999},"size":{"x":61.99999999999994,"y":135},"kind":"and"},{"uid":"272","topLeft":{"x":1507.959374999999,"y":809.084374999999},"size":{"x":61.99999999999994,"y":135},"kind":"and"},{"uid":"406","topLeft":{"x":2169.9999999999986,"y":249.99999999999983},"size":{"x":123.74999999999997,"y":65},"kind":"and"},{"uid":"273","topLeft":{"x":1980.709374999999,"y":643.334374999999},"size":{"x":45.99999999999994,"y":136},"kind":"and"},{"uid":"274","topLeft":{"x":1931.459374999999,"y":960.834374999999},"size":{"x":45.99999999999994,"y":136},"kind":"and"}],"diamonds":[{"uid":"275","topLeft":{"x":876.99169921875,"y":827.648956298828},"size":{"x":72.99999999999997,"y":81.99999999999997}},{"uid":"276","topLeft":{"x":888.99169921875,"y":1013.3989562988279},"size":{"x":58.00000000000003,"y":77.99999999999997}},{"uid":"277","topLeft":{"x":990.03857421875,"y":522.0864562988284},"size":{"x":85.00000000000003,"y":76.99999999999997}},{"uid":"278","topLeft":{"x":739.0385742187499,"y":513.1489562988284},"size":{"x":97.00000000000003,"y":77.99999999999997}},{"uid":"279","topLeft":{"x":2225.5718749999996,"y":600.5593749999953},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"280","topLeft":{"x":2235.971875,"y":930.1593749999934},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"281","topLeft":{"x":1254.2718749999997,"y":756.9093749999988},"size":{"x":60.99999999999994,"y":185.60000000000002}},{"uid":"411","topLeft":{"x":607.4999999999997,"y":821.2499999999995},"size":{"x":56.24999999999997,"y":217.4999999999999}}],"history":[],"arrows":[{"uid":"282","start":{"x":1962.709374999999,"y":808.334374999999},"end":{"x":1852.459374999999,"y":956.834374999999}},{"uid":"283","start":{"x":1799.459374999999,"y":1080.834374999999},"end":{"x":1674.959374999999,"y":947.084374999999}},{"uid":"284","start":{"x":1751.709374999999,"y":856.334374999999},"end":{"x":1847.709374999999,"y":753.334374999999}},{"uid":"285","start":{"x":1252.5078125,"y":450.25781250000045},"end":{"x":1308.7421875,"y":459.14843750000045}},{"uid":"286","start":{"x":1721.4093749999988,"y":593.1843750000039},"end":{"x":1820.8593749999986,"y":612.9343750000021}},{"uid":"287","start":{"x":1972.709374999999,"y":683.334374999999},"end":{"x":1932.709374999999,"y":683.334374999999}},{"uid":"288","start":{"x":1927.709374999999,"y":753.334374999999},"end":{"x":1973.709374999999,"y":751.334374999999}},{"uid":"289","start":{"x":1913.5093749999987,"y":609.9343750000007},"end":{"x":1915.309374999999,"y":638.7343750000009}},{"uid":"290","start":{"x":1923.459374999999,"y":1000.834374999999},"end":{"x":1883.459374999999,"y":1000.834374999999}},{"uid":"291","start":{"x":1878.459374999999,"y":1070.834374999999},"end":{"x":1924.459374999999,"y":1068.834374999999}},{"uid":"292","start":{"x":2018.2593749999987,"y":1079.4343749999998},"end":{"x":1980.459374999999,"y":1053.834374999999}},{"uid":"293","start":{"x":1613.959374999999,"y":848.084374999999},"end":{"x":1573.959374999999,"y":848.084374999999}},{"uid":"294","start":{"x":1568.959374999999,"y":918.084374999999},"end":{"x":1614.959374999999,"y":916.084374999999}},{"uid":"295","start":{"x":2086.5093749999987,"y":758.7343749999982},"end":{"x":2238.309374999999,"y":775.1343749999992}},{"uid":"296","start":{"x":1345.8515625000002,"y":542.0390625000005},"end":{"x":1345.6015625000002,"y":500.35156250000045}},{"uid":"297","start":{"x":1537.6760498046865,"y":754.7260375976552},"end":{"x":1536.4260498046865,"y":805.3510375976552}},{"uid":"298","start":{"x":366.9531249999975,"y":225.46874999999955},"end":{"x":406.9531249999975,"y":255.46874999999977}},{"uid":"299","start":{"x":490.7031249999976,"y":321.7187499999993},"end":{"x":489.7656249999976,"y":370.1562499999993}},{"uid":"300","start":{"x":393.20312499999795,"y":373.9062499999993},"end":{"x":392.26562499999795,"y":322.3437499999993}},{"uid":"301","start":{"x":878.5249938964844,"y":891.648956298828},"end":{"x":663.3374938964846,"y":894.648956298828}},{"uid":"302","start":{"x":882.5249938964844,"y":841.648956298828},"end":{"x":664.3374938964846,"y":841.648956298828}},{"uid":"303","start":{"x":531.0541992187502,"y":1078.648956298828},"end":{"x":883.99169921875,"y":1084.648956298828}},{"uid":"304","start":{"x":885.7749938964844,"y":1021.3989562988279},"end":{"x":665.5874938964846,"y":1021.3989562988279}},{"uid":"305","start":{"x":917.5249938964844,"y":1012.3989562988279},"end":{"x":915.5249938964844,"y":919.6489562988279}},{"uid":"306","start":{"x":324.7500000000001,"y":783.586456298828},"end":{"x":377.7500000000001,"y":802.586456298828}},{"uid":"307","start":{"x":757.9218749999999,"y":380.36979675293003},"end":{"x":756.9218749999999,"y":524.3697967529299}},{"uid":"308","start":{"x":1058.5718688964844,"y":528.0864562988284},"end":{"x":1055.5718688964844,"y":380.08645629882847}},{"uid":"309","start":{"x":832.0385742187498,"y":559.0864562988284},"end":{"x":976.03857421875,"y":561.0864562988284}},{"uid":"310","start":{"x":820.5718688964843,"y":524.0864562988284},"end":{"x":820.5718688964843,"y":380.08645629882847}},{"uid":"311","start":{"x":985.4468688964844,"y":530.2114562988284},"end":{"x":986.5718688964844,"y":378.08645629882847}},{"uid":"312","start":{"x":735.2343749999994,"y":219.1406249999999},"end":{"x":774.9218749999994,"y":301.0156249999999}},{"uid":"313","start":{"x":273.4374999999999,"y":68.74999999999993},"end":{"x":343.7499999999999,"y":120.31249999999994}},{"uid":"314","start":{"x":1254.851562500001,"y":273.9765624999991},"end":{"x":1318.289062500001,"y":270.6640624999991}},{"uid":"315","start":{"x":2228.3718749999994,"y":695.159374999997},"end":{"x":2032.3718749999994,"y":710.9593749999999}},{"uid":"316","start":{"x":2243.1718749999995,"y":619.3593749999973},"end":{"x":2032.7718749999995,"y":669.759375000001}},{"uid":"317","start":{"x":2043.0593749999994,"y":1104.4343749999957},"end":{"x":2248.709375,"y":1104.7343749999968}},{"uid":"318","start":{"x":2238.7718750000004,"y":1024.7593749999946},"end":{"x":2042.521875,"y":1031.8593749999968}},{"uid":"319","start":{"x":2253.571875,"y":948.9593749999954},"end":{"x":2044.521875,"y":962.6593749999961}},{"uid":"320","start":{"x":1478.1218749999991,"y":759.0093749999969},"end":{"x":1295.0718749999999,"y":773.3093749999966}},{"uid":"321","start":{"x":1317.471875,"y":850.909374999997},"end":{"x":1508.0218749999995,"y":868.5093749999978}},{"uid":"322","start":{"x":1289.4718750000004,"y":936.5093749999978},"end":{"x":1503.2218750000004,"y":925.3093749999957}},{"uid":"323","start":{"x":1521.249999999999,"y":496.2499999999997},"end":{"x":1518.749999999999,"y":549.9999999999997}},{"uid":"324","start":{"x":1432.499999999999,"y":186.2499999999999},"end":{"x":1431.249999999999,"y":231.2499999999999}},{"uid":"325","start":{"x":1642.4999999999989,"y":186.24999999999991},"end":{"x":1643.7499999999989,"y":233.74999999999991}},{"uid":"326","start":{"x":1888.7499999999986,"y":189.99999999999991},"end":{"x":1888.7499999999986,"y":232.49999999999991}},{"uid":"390","start":{"x":616.2499999999997,"y":633.7499999999997},"end":{"x":563.7499999999997,"y":634.9999999999997}},{"uid":"397","start":{"x":2087.4999999999986,"y":239.99999999999986},"end":{"x":2029.9999999999986,"y":241.24999999999986}},{"uid":"401","start":{"x":616.2499999999997,"y":547.4999999999997},"end":{"x":563.7499999999997,"y":546.2499999999997}},{"uid":"403","start":{"x":359.9999999999998,"y":594.9999999999997},"end":{"x":369.9999999999998,"y":562.4999999999997}},{"uid":"404","start":{"x":306.24999999999983,"y":617.4999999999997},"end":{"x":321.24999999999983,"y":568.7499999999997}},{"uid":"408","start":{"x":2191.2499999999986,"y":203.7499999999999},"end":{"x":2223.7499999999986,"y":247.4999999999999}},{"uid":"409","start":{"x":2299.9999999999986,"y":282.49999999999983},"end":{"x":2263.7499999999986,"y":323.74999999999983}},{"uid":"412","start":{"x":608.7499999999997,"y":927.4999999999994},"end":{"x":538.7499999999997,"y":927.4999999999994}},{"uid":"415","start":{"x":607.4999999999997,"y":1006.2499999999994},"end":{"x":538.7499999999997,"y":1004.9999999999994}},{"uid":"419","start":{"x":2573.7499999999986,"y":346.24999999999983},"end":{"x":2579.9999999999986,"y":404.99999999999983}},{"uid":"420","start":{"x":2468.7499999999986,"y":273.74999999999983},"end":{"x":2521.2499999999986,"y":311.24999999999983}},{"uid":"422","start":{"x":2569.9999999999986,"y":442.49999999999983},"end":{"x":2576.2499999999986,"y":501.24999999999983}},{"uid":"424","start":{"x":2569.9999999999986,"y":529.9999999999998},"end":{"x":2576.2499999999986,"y":588.7499999999998}},{"uid":"426","start":{"x":2571.2499999999986,"y":619.9999999999997},"end":{"x":2577.4999999999986,"y":678.7499999999998}},{"uid":"428","start":{"x":2572.4999999999986,"y":709.9999999999998},"end":{"x":2578.7499999999986,"y":768.7499999999998}},{"uid":"430","start":{"x":2571.2499999999986,"y":912.4999999999997},"end":{"x":2577.4999999999986,"y":971.2499999999997}},{"uid":"432","start":{"x":2573.7499999999986,"y":1002.4999999999995},"end":{"x":2579.9999999999986,"y":1061.2499999999995}},{"uid":"434","start":{"x":2569.9999999999986,"y":809.9999999999994},"end":{"x":2576.2499999999986,"y":868.7499999999993}},{"uid":"436","start":{"x":1029.9999999999993,"y":307.49999999999983},"end":{"x":983.7499999999994,"y":302.49999999999983}}],"texts":[{"uid":"327","text":"// Not editing","topLeft":{"x":1475.0703125,"y":463.57031250000045}},{"uid":"328","text":"selectNext","topLeft":{"x":1942.709374999999,"y":868.334374999999}},{"uid":"329","text":"selectNext","topLeft":{"x":1732.209374999999,"y":1031.584374999999}},{"uid":"330","text":"selectNext","topLeft":{"x":1789.709374999999,"y":818.334374999999}},{"uid":"331","text":"after 500ms","topLeft":{"x":1942.709374999999,"y":760.334374999999}},{"uid":"332","text":"after 500ms","topLeft":{"x":1943.709374999999,"y":665.334374999999}},{"uid":"333","text":"after 500ms","topLeft":{"x":1893.459374999999,"y":1077.834374999999}},{"uid":"334","text":"after 500ms","topLeft":{"x":1894.459374999999,"y":982.834374999999}},{"uid":"335","text":"after 500ms","topLeft":{"x":1583.959374999999,"y":925.084374999999}},{"uid":"336","text":"after 500ms","topLeft":{"x":1584.959374999999,"y":830.084374999999}},{"uid":"337","text":"incSelection","topLeft":{"x":2200.559374999999,"y":792.734375}},{"uid":"338","text":"endEdit","topLeft":{"x":1344.4765625000002,"y":527.1640625000005}},{"uid":"339","text":"// TimeEditor","topLeft":{"x":1823.3192749023442,"y":470.5640563964847}},{"uid":"340","text":"beep","topLeft":{"x":496.3281249999976,"y":347.9687499999993}},{"uid":"341","text":"after 10ms","topLeft":{"x":386.32812499999795,"y":350.7812499999993}},{"uid":"342","text":"// Increasing","topLeft":{"x":452.93750000000045,"y":850.2031249999999}},{"uid":"343","text":"[m >= 60] / m=0; h=(h+1)%24;","topLeft":{"x":779.5249938964846,"y":828.086456298828}},{"uid":"344","text":"incTime / s =s+1;","topLeft":{"x":746.7416992187502,"y":1072.836456298828}},{"uid":"345","text":"[s >= 60] / s=0; m=m+1;","topLeft":{"x":919.5249938964844,"y":963.6489562988277}},{"uid":"346","text":"[s < 60]","topLeft":{"x":753.7750244140627,"y":1011.8364562988279}},{"uid":"347","text":"[m < 60]","topLeft":{"x":754.6250000000002,"y":885.2031249999999}},{"uid":"348","text":"entry /\n^_timeChanged","topLeft":{"x":449.95837402343795,"y":936.8906249999995}},{"uid":"349","text":"// TimeIncreasor","topLeft":{"x":714.683349609375,"y":752.910415649414}},{"uid":"350","text":"incChrono\n/ chs = chs+1","topLeft":{"x":752.9218749999999,"y":478.3697967529299}},{"uid":"351","text":"[cs >= 60] /\ncs=0;\ncm=(cm+1)%100;","topLeft":{"x":1080.5718688964844,"y":452.08645629882835}},{"uid":"352","text":"[chs >= 100] /\nchs=0; cs=cs+1;","topLeft":{"x":903.1968688964844,"y":578.3989562988284}},{"uid":"353","text":"[chs < 100]","topLeft":{"x":820.8218994140624,"y":431.08645629882835}},{"uid":"354","text":"[cm < 60]","topLeft":{"x":980.921875,"y":422.2031250000002}},{"uid":"355","text":"// sound","topLeft":{"x":431.8749999999985,"y":404.76562499999955}},{"uid":"356","text":"// no sound","topLeft":{"x":434.0624999999984,"y":289.14062499999955}},{"uid":"357","text":"// ChronoIncreasor","topLeft":{"x":882.1093749999993,"y":197.2656249999999}},{"uid":"358","text":"// BeepRequestHandler","topLeft":{"x":437.4999999999998,"y":188.4374999999999}},{"uid":"359","text":"entry / h=9;","topLeft":{"x":122.0625,"y":183.44583129882804}},{"uid":"360","text":"entry / m=0;","topLeft":{"x":125.0625,"y":213.44583129882778}},{"uid":"361","text":"entry / s=0;","topLeft":{"x":124.0625,"y":245.44583129882773}},{"uid":"362","text":"entry / cm=0;","topLeft":{"x":119.75,"y":420.3208312988279}},{"uid":"363","text":"entry / cs=0;","topLeft":{"x":117.1875,"y":452.7583312988279}},{"uid":"364","text":"entry / chs=0;","topLeft":{"x":121.1875,"y":488.9458312988279}},{"uid":"365","text":"entry / ah=9;","topLeft":{"x":125.125,"y":299.7583312988277}},{"uid":"366","text":"entry / am=0;","topLeft":{"x":128.25,"y":330.1958312988279}},{"uid":"367","text":"entry / as=5;","topLeft":{"x":125.3125,"y":360.1958312988279}},{"uid":"368","text":"// HideHours","topLeft":{"x":1883.8968749999995,"y":711.9593749999976}},{"uid":"369","text":"// HideMinutes","topLeft":{"x":1841.7093749999995,"y":1030.084374999998}},{"uid":"370","text":"// HideSeconds","topLeft":{"x":1666.7093749999995,"y":874.7718749999981}},{"uid":"371","text":"// DisplayingTime","topLeft":{"x":1423.289062500001,"y":288.9140624999991}},{"uid":"372","text":"[inState(\"DisplayingTime\")] /\nh = (h+1)%24","topLeft":{"x":2141.571874999999,"y":707.7093750000017}},{"uid":"373","text":"[inState(\"DisplayingAlarm\")] /\nah = (ah+1)%24","topLeft":{"x":2136.8218749999996,"y":642.459374999999}},{"uid":"374","text":"incSelection","topLeft":{"x":2145.9593749999995,"y":1093.584374999998}},{"uid":"375","text":"[inState(\"DisplayingTime\")] /\nm = (m+1)%60","topLeft":{"x":2148.9218749999995,"y":1002.8093749999985}},{"uid":"376","text":"[inState(\"DisplayingAlarm\")] /\nam = (am+1)%60","topLeft":{"x":2154.721875,"y":928.3093749999966}},{"uid":"377","text":"incSelection","topLeft":{"x":1376.7718750000006,"y":758.7093749999958}},{"uid":"378","text":"[inState(\"DisplayingTime\")]\n/ s = (s+1)%60","topLeft":{"x":1407.371875,"y":827.9093749999952}},{"uid":"379","text":"[inState(\"DisplayingAlarm\")]\n/ as = (as+1)%60","topLeft":{"x":1391.6718750000007,"y":952.0093749999946}},{"uid":"380","text":"// Editing","topLeft":{"x":1303.1249999999986,"y":588.1249999999997}},{"uid":"381","text":"beginEdit","topLeft":{"x":1523.749999999999,"y":524.9999999999997}},{"uid":"382","text":"// DisplayingAlarm","topLeft":{"x":1642.039062500001,"y":287.6640624999991}},{"uid":"383","text":"// DisplayingChrono","topLeft":{"x":1884.5390625000005,"y":287.6640624999991}},{"uid":"384","text":"displayTime","topLeft":{"x":1434.999999999999,"y":209.9999999999999}},{"uid":"385","text":"displayAlarm","topLeft":{"x":1643.7499999999989,"y":216.2499999999999}},{"uid":"386","text":"displayChrono","topLeft":{"x":1892.4999999999986,"y":214.9999999999999}},{"uid":"393","text":"// light off","topLeft":{"x":464.9999999999997,"y":549.9999999999997}},{"uid":"394","text":"// light on","topLeft":{"x":466.2499999999998,"y":649.9999999999997}},{"uid":"395","text":"lightOn","topLeft":{"x":588.7499999999997,"y":618.7499999999997}},{"uid":"402","text":"lightOff","topLeft":{"x":592.4999999999997,"y":533.7499999999997}},{"uid":"407","text":"entry / alarm=false;","topLeft":{"x":121.24999999999991,"y":559.9999999999997}},{"uid":"410","text":"setAlarm(onOrOff)\n/ alarm = onOrOff","topLeft":{"x":2306.2499999999986,"y":296.24999999999983}},{"uid":"413","text":"[alarm\n &&(h==ah)\n &&(m==am)\n &&(s==as)]\n/ ^alarm","topLeft":{"x":573.7499999999997,"y":892.4999999999994}},{"uid":"416","text":"[!(alarm\n &&(h==ah)\n &&(m==am)\n &&(s==as))]","topLeft":{"x":578.7499999999997,"y":997.4999999999994}},{"uid":"421","text":"topLeftPressed / ^topLeftPressed","topLeft":{"x":2601.2499999999986,"y":369.99999999999983}},{"uid":"423","text":"topLeftReleased / ^topLeftReleased","topLeft":{"x":2597.4999999999986,"y":466.24999999999983}},{"uid":"425","text":"topRightPressed / ^topRightPressed","topLeft":{"x":2597.4999999999986,"y":553.7499999999998}},{"uid":"427","text":"topRightReleased / ^topRightReleased","topLeft":{"x":2598.7499999999986,"y":643.7499999999998}},{"uid":"429","text":"bottomRightPressed / ^bottomRightPressed","topLeft":{"x":2599.9999999999986,"y":733.7499999999998}},{"uid":"431","text":"bottomLeftPressed / ^bottomLeftPressed","topLeft":{"x":2598.7499999999986,"y":936.2499999999997}},{"uid":"433","text":"bottomLeftReleased / ^bottomLeftReleased","topLeft":{"x":2601.2499999999986,"y":1026.2499999999995}},{"uid":"435","text":"bottomRightReleased / ^bottomRightReleased","topLeft":{"x":2597.4999999999986,"y":833.7499999999994}},{"uid":"437","text":"resetChrono / chs=0; cs=0; cm=0","topLeft":{"x":1012.4999999999994,"y":281.24999999999983}}]} \ No newline at end of file diff --git a/src/App/Plant/Microwave/Microwave.tsx b/src/App/Plant/Microwave/Microwave.tsx index 5f45b15..93db7a2 100644 --- a/src/App/Plant/Microwave/Microwave.tsx +++ b/src/App/Plant/Microwave/Microwave.tsx @@ -13,16 +13,10 @@ import { useEffect } from "react"; import "./Microwave.css"; import { useAudioContext } from "../../useAudioContext"; -import { Plant } from "../Plant"; +import { Plant, PlantRenderProps } from "../Plant"; import { statechartExecution } from "@/statecharts/timed_reactive"; import { microwaveAbstractSyntax } from "./model"; -export type MicrowaveProps = { - state: RT_Statechart, - speed: number, - raiseInput: (event: RaisedEvent) => void; - raiseOutput: (event: RaisedEvent) => void; -} const imgs = { "false": { "false": imgSmallClosedOff, "true": imgSmallClosedOn }, @@ -43,7 +37,7 @@ const DOOR_Y0 = 68; const DOOR_WIDTH = 353; const DOOR_HEIGHT = 217; -export function Magnetron({state, speed, raiseInput, raiseOutput}: MicrowaveProps) { +export function Microwave({state, speed, raiseInput}: PlantRenderProps) { const [playSound, preloadAudio] = useAudioContext(speed); // preload(imgSmallClosedOff, {as: "image"}); @@ -134,5 +128,5 @@ export const MicrowavePlant: Plant = { {kind: "event", event: "incTimeReleased"}, ], execution: statechartExecution(microwaveAbstractSyntax), - render: Magnetron, + render: Microwave, } diff --git a/src/App/Plant/Plant.ts b/src/App/Plant/Plant.ts index 492bfef..d6883e7 100644 --- a/src/App/Plant/Plant.ts +++ b/src/App/Plant/Plant.ts @@ -8,7 +8,6 @@ export type PlantRenderProps = { state: StateType, speed: number, raiseInput: (e: RaisedEvent) => void, - raiseOutput: (e: RaisedEvent) => void, }; export type Plant = { diff --git a/src/App/RTHistory.tsx b/src/App/RTHistory.tsx index 9019460..83e3972 100644 --- a/src/App/RTHistory.tsx +++ b/src/App/RTHistory.tsx @@ -66,7 +66,7 @@ export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevIt
{formatTime(item.simtime)}   -
+
diff --git a/src/App/TopPanel/RotateButtons.tsx b/src/App/TopPanel/RotateButtons.tsx new file mode 100644 index 0000000..514fa6e --- /dev/null +++ b/src/App/TopPanel/RotateButtons.tsx @@ -0,0 +1,23 @@ +import { memo } from "react" + +import Rotate90DegreesCcwTwoToneIcon from '@mui/icons-material/Rotate90DegreesCcwTwoTone'; +import Rotate90DegreesCwTwoToneIcon from '@mui/icons-material/Rotate90DegreesCwTwoTone'; +import { Selection } from "../VisualEditor/VisualEditor"; + +export const RotateButtons = memo(function RotateButtons({selection, onRotate}: {selection: Selection, onRotate: (dir: "ccw"|"cw") => void}) { + const disabled = selection.length === 0; + return <> + + + +}); diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx index 2beede9..b90f6ce 100644 --- a/src/App/TopPanel/TopPanel.tsx +++ b/src/App/TopPanel/TopPanel.tsx @@ -19,6 +19,7 @@ import SkipNextIcon from '@mui/icons-material/SkipNext'; import StopIcon from '@mui/icons-material/Stop'; import { InsertModes } from "./InsertModes"; import { usePersistentState } from "@/App/persistent_state"; +import { RotateButtons } from "./RotateButtons"; export type TopPanelProps = { trace: TraceState | null, @@ -26,6 +27,7 @@ export type TopPanelProps = { setTime: Dispatch>, onUndo: () => void, onRedo: () => void, + onRotate: (direction: "ccw"|"cw") => void, onInit: () => void, onClear: () => void, onBack: () => void, @@ -41,7 +43,7 @@ export type TopPanelProps = { const ShortCutShowKeys = ~; -export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) { +export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) { const [displayTime, setDisplayTime] = useState("0.000"); const [timescale, setTimescale] = usePersistentState("timescale", 1); @@ -212,6 +214,11 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on   +
+ +   +
+ {/* execution */}
diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx index 150d21b..9b3b107 100644 --- a/src/App/VisualEditor/VisualEditor.tsx +++ b/src/App/VisualEditor/VisualEditor.tsx @@ -53,10 +53,11 @@ type HistorySelectable = { } type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable | HistorySelectable; -type Selection = Selectable[]; +export type Selection = Selectable[]; export type InsertMode = "and"|"or"|"pseudo"|"shallow"|"deep"|"transition"|"text"; + type VisualEditorProps = { state: VisualEditorState, setState: Dispatch<(v:VisualEditorState) => VisualEditorState>, @@ -75,6 +76,8 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace, const [dragging, setDragging] = useState(false); + window.setState = setState; + // uid's of selected rountangles const selection = state.selection || []; const setSelection = useCallback((cb: (oldSelection: Selection) => Selection) => diff --git a/src/statecharts/timed_reactive.ts b/src/statecharts/timed_reactive.ts index 2afad51..cac9e0f 100644 --- a/src/statecharts/timed_reactive.ts +++ b/src/statecharts/timed_reactive.ts @@ -175,15 +175,20 @@ export function coupledExecution(models: {[name throw new Error("cannot make intTransition - timeAdvance is infinity"); }, extTransition: (simtime, c, e) => { - console.log(e); - const {model, eventName} = conns.inputEvents[e.name]; - console.log('input event', e.name, 'goes to', `${model}.${eventName}`); - const inputEvent: InputEvent = { - kind: "input", - name: eventName, - param: e.param, - }; - return makeModelExtTransition(simtime, c, model, inputEvent); + if (!Object.hasOwn(conns.inputEvents, e.name)) { + console.warn('input event', e.name, 'goes to nowhere'); + return [[], c]; + } + else { + const {model, eventName} = conns.inputEvents[e.name]; + console.log('input event', e.name, 'goes to', `${model}.${eventName}`); + const inputEvent: InputEvent = { + kind: "input", + name: eventName, + param: e.param, + }; + return makeModelExtTransition(simtime, c, model, inputEvent); + } }, } } diff --git a/src/util/geometry.ts b/src/util/geometry.ts index 7d92b23..e209d67 100644 --- a/src/util/geometry.ts +++ b/src/util/geometry.ts @@ -203,3 +203,65 @@ export const sides: [RectSide, (r: Rect2D) => Line2D][] = [ ["right", getRightSide], ["bottom", getBottomSide], ]; + + +export function rotatePoint90CW(p: Vec2D, around: Vec2D): Vec2D { + + const result = { + x: around.x - (p.y - around.y), + y: around.y + (p.x - around.x), + }; + console.log('rotate', p, 'around', around, 'result=', result); + return result; +} + +export function rotatePoint90CCW(p: Vec2D, around: Vec2D): Vec2D { + return { + x: around.x + (p.y - around.y), + y: around.y - (p.x - around.x), + }; +} + +function fixNegativeSize(r: Rect2D): Rect2D { + return { + topLeft: { + x: r.size.x < 0 ? r.topLeft.x + r.size.x : r.topLeft.x, + y: r.size.y < 0 ? r.topLeft.y + r.size.y : r.topLeft.y, + }, + size: { + x: Math.abs(r.size.x), + y: Math.abs(r.size.y), + }, + } +} + +export function rotateRect90CCW(r: Rect2D, around: Vec2D): Rect2D { + const rotated = { + topLeft: rotatePoint90CCW(r.topLeft, around), + size: rotatePoint90CCW(r.size, {x: 0, y: 0}), + }; + return fixNegativeSize(rotated); +} + + +export function rotateRect90CW(r: Rect2D, around: Vec2D): Rect2D { + const rotated = { + topLeft: rotatePoint90CW(r.topLeft, around), + size: rotatePoint90CW(r.size, {x: 0, y: 0}), + }; + return fixNegativeSize(rotated); +} + +export function rotateLine90CCW(l: Line2D, around: Vec2D): Line2D { + return { + start: rotatePoint90CCW(l.start, around), + end: rotatePoint90CCW(l.end, around), + }; +} + +export function rotateLine90CW(l: Line2D, around: Vec2D): Line2D { + return { + start: rotatePoint90CW(l.start, around), + end: rotatePoint90CW(l.end, around), + }; +}